大家好,我是 那个曾经的少年回来了
。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。
最近在接触一点后端,把自己的思考记录一下。
1、前言
啥也不说了,直接进入正题,来学习一下Token在前端和后端的简单应用分析
Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码进行对比,判断用户名和密码是否正确,并作出相应提示,在这样的背景下,Token便应运而生。
Token实际上就是在第一个登录的时候通过用户名和密码,在服务端验证OK后生成的一串字符串,也可以说是验证通过后服务端为其签发一个令牌,随后前端在访问服务端接口时,客户端就可以携带这个TOken令牌访问服务器,服务端只需要验证令牌的有效性即可。
下面便是请求接口的一个大致过程
- 先登录,获取Token
- 调用业务接口,后端要先验证Token
- 验证OK,才继续调用业务接口返回数据
- 验证失败,则返回给前端,比如Token过期,则重新跳转到登录
2、后端
登录接口,通过用户名和密码,或者手机号验证码的方式通过验证
public async Task<dynamic> Login([FromServices] IAuthService authService, [FromBody] FormLoginRequest loginModel) { return await authService.login(loginModel); // authoService.login中的逻辑 // 判断是否匹配,匹配成功 // 创建token并写入redis,并设置超期时间 // 之前业务接口调用时,直接从redis中获取 // 如果有超期,返回给前端一个标识 }
这里有一个创建Token的过程,我们来看一下token的组成
我找了一个公司正在开发项目中的token进行解析查看。主要结构如上图所示。解密以后最重要的信息便是uid,或者说是用户在后端中的唯一的用户id,那么通过uid便可以查询到相关的身份认证信息。
截图所示便是JSON Web Token的组成结构,从截图左侧仔细可以看到,中间有两个.
将JWT分成了三个部分
- HEADER
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256) typ属性表示这个令牌(token)的类型(type)
- PAYLOAD
中间部分存放的就是实际要传输的数据
- Signature
Signature部分是对前面的两部分的数据进行签名,防止数据篡改。
- 最终生成便是
首先明确一点,是在后端生成的Token,后端会先定义一个秘钥,这个秘钥只有后端服务器才知道, 不能泄露给用户,然后使用Header中指定的签名算法(默认情况是HMAC SHA256), 算出签名以后将Header、Payload、Signature三部分拼成一个字符串,每个部分用`.`分割开来, 就可以返给用户了。
前端在登录认证通过获得Token并保存到前端以后,再调用业务接口的时候每次便会携带Token
后端服务会通过全局注册的环绕AOP,处理每次前端有请求到达后端的时候来对token校验
AllowAnonymousAttribute allowAnonymousAttribute = descriptor.MethodInfo.GetCustomAttribute<AllowAnonymousAttribute>(false); // 判断可不验证token的接口 if (allowAnonymousAttribute != null) { await next(); return; } //获取请求头中的Authorization string token = context.HttpContext.Request.Headers["Authorization"]; // 相当于对前端传递的token进行转换 string tokenKey = "sso." + Utils.MD5(token); // redis获取,看看是否有效,直接取出返回 string loginUserJson = await RedisHelper.GetAsync(tokenKey); if (!loginUserJson.IsNullOrWhiteSpace()) { RedisSSOVerifyResult resultInfo = JsonSerializer.Deserialize<RedisSSOVerifyResult>(loginUserJson); if(resultInfo.ExpiresAt > DateTime.now()) { loginUser = resultInfo.LoginUser; } else { RedisHelper.RemoveAsync(tokenKey); // 无效了 从redis中移除 throw new ValidException("Token认证过期,请重新登录", -2); // 这里用-2跟前端做好约定 } } else { throw new ValidException("Token认证过期,请重新登录", -2); // 这里用-2跟前端做好约定 }
大致的一个token认证过程是这样的,实际项目中相对来说还是比较复杂的,这是我从公司项目中扣取出来的。还有很多代码没有列出来,要不然会显得比较臃肿,而且主要逻辑不容易查看。
3、前端
通过登录页面,输入登录名和密码,或者手机号和验证码,获取到token,现将token存储到localStorage中,再通过token获取其他业务接口的数据。 通常可能首先通过token获取个人信息或者一些权限数据(这里只是提一下)。
const adminLogin = async () => { // state.loading = true const res = await loginByMobile({ mobile: state.loginForm.phone, captchaValue: state.loginForm.verificationCode, }); state.loading = false; if (res?.code === 200) { localStorage.setItem( "token", JSON.stringify({ ...res.data, account: state.loginForm.phone, }) ); store.dispatch("fetchMenu"); } };
我这里登录完,直接通过token来获取当前登录用户的个人信息以及后台勾选的菜单权限,后端分别通过两个接口进行的数据返回。
async fetchMenu({ commit }) { try { const information = await getMyInformation() if (information?.code === 200) { console.log(information, 'information') commit("setMyInformation" , information.data) const res = await getMyMenu() if(res?.code === 200) { commit("changeMenuList",res.data) window.location.href = "/" } } } catch (error) { } },
这里是axios针对每次的请求添加请求头的Authorization
instance.interceptors.request.use( (request) => { const token = localStorage.token ? JSON.parse(localStorage.token) : {}; request.headers = { "Authorization": token.authorization || '', "Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/json", }; return request; }, (error) => Promise.reject(error) );
这里是针对后端接口返回数据的判断处理,其中有一个-2的特殊判断,这里是跟后端返回一起约定的code
instance.interceptors.response.use( (response) => { // token if (response.data.code === -2) { // token失效 ElMessage({ message: "身份认证无效,请重新登录", type: "warning", }); // localStorage.clear(); clear() window.location.href = "/"; return false; } if (response.data.code !== 200) { return Promise.reject(new Error(response.data.message)); } /// ..... 其他的逻辑判断 return response.data; }, }
上面通过 code为-2
进行判断 ,然后清除掉缓存数据,那么在vue-router路由中会进行判断处理
router.beforeEach((to, _from, next) => { NProgress.start() if (to.path === '/login' || to.path === '/init-password' || to.path === '/login-cellphone') { next() return false; } if (!localStorage.getItem('token')) { next('/login') return false } if (to.name) { next() return false } if (childrenPath.some((item) => to.path.includes(item))) { next() console.log('child'); return false } // 如果找不到路由跳转到404 next("/404") return false })
总结
前端和后端大致的一个过程就在这里简单说完了,梳理完了以后,发现自己更清楚了,其实还有很多的问题要去处理,比如
- 请求业务接口Token超期失效了该怎么办? 可以通过每次调用业务接口的前,只要验证Token成功,就延迟Token的超期时间,但是这种方式每次都要去处理Token的时间,相对来说就比较麻烦,而且对服务器有一定的损耗。
- 那还有更好的办法吗? 当然也是有的。比如通过双Token进行无痛刷新,就是当一个token失效或者超期后,通过另外一个refresh_token来重新获取token的处理,获取成功后,再重新调用期间异常的业务接口
- 当然肯定还有其他的方式吧,暂时能想到的就这么多了。
我的个人博客:vue.tuokecat.com/blog
我的个人github:github.com/aehyok
我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化
不断完善中,整体框架都有了
在线预览:vue.tuokecat.com
github源码:github.com/aehyok/vue-…