本文正在参加「金石计划 . 瓜分6万现金大奖」
1、前言
Token 可以说是一个令牌,前端在访问服务端接口时,验证通过后服务端会为其签发一个令牌,之后,客户端就可以携带令牌访问服务器,服务端只需要验证令牌的有效性即可。
我找了一个公司正在开发项目中的token进行解析查看。主要结构如上图所示。解密以后最重要的信息便是uid,或者说是用户在后端中的唯一的用户id,那么通过uid便可以查询到相关的身份认证信息。
后端
登录接口,通过用户名和密码,或者手机号验证码的方式通过验证
public async Task<dynamic> Login([FromServices] IAuthService authService, [FromBody] FormLoginRequest loginModel)
{
return await authService.login(loginModel);
// authoService.login中的逻辑
// 判断是否匹配,匹配成功
// 将token写入redis,并设置超期时间
// 之前业务接口调用时,直接从redis中获取
// 如果有超期,返回给前端一个标识
}
后端服务首先会全局注册环绕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认证过程是这样的,实际项目中相对来说还是比较复杂的,这是我从公司项目中扣取出来的。还有很多代码没有列出来,要不然会显得比较臃肿,而且主要逻辑不容易查看。
前端
通过登录页面,输入登录名和密码,或者手机号和验证码,获取到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针对每次的请求添加请求头
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;
},
}
上面通过-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
})
总结
前端和后端大致的一个过程就在这里简单说完了,梳理完了以后,发现自己更清楚了,其实还有很多的问题要去处理,就当做是优化了。