前言
用到的技术手段
- 后端
SpringMVC请求拦截器-Intercepter - 前端
Vue路由守卫
axios请求拦截器
axios响应拦截器
流程
下面是一个单线的流程,拦截器一个都不加
@PostMapping("/login")
public Result login(@RequestParam String username,@RequestParam String password){
User u =service.login(username);
if(ObjectUtils.isEmpty(u)){
//return不存在该用户
}
//密码不能打印在日志里
password = DigestUtils.md5Hex(password);
if(!password.equals(u.getPwd())){
log.warn("登录信息有误");
//return用户名或密码错误
}
/*正常用户
* 创建AccessToken 转成json 加密后返回
*/
AccessToken accessToken = new AccessToken(username);
String json = JSONObject.toJSONString(accessToken);
return r.success(EncryptUtil.encrypt(json));
}
Token这里我用的是一个对象有两个属性,一个保存用户名,一个记录时间信息后期需要判断token是否过期(默认5分钟),如下
public class AccessToken {
private String username;
private Long expire;
public AccessToken(String username){
//默认过期时间五分钟,过了这个时间戳就不能用了
this(username,System.currentTimeMillis()+1000*60*5);
}
}
jasypt加解密工具👇
public class EncryptUtil {
private static final StandardPBEStringEncryptor encryptor =new StandardPBEStringEncryptor();
static {
//用于记录config的信息
EnvironmentStringPBEConfig config =new EnvironmentStringPBEConfig();
config.setAlgorithm("PBEWithMD5AndDES");
config.setPassword("author:shang");
encryptor.setConfig(config);
}
/*加密方法*/
public static String encrypt(String plainText){
return encryptor.encrypt(plainText);
}
/*解密方法*/
public static String decrypt(String cipher){
return encryptor.decrypt(cipher);
}
}
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<version>1.9.2</version>
</dependency>
验证
今天主要是说登录的验证,我们重头戏在于token的校验上,
主要集中在,前端拿到token后再次发请求,我们能否对应上用户身份?
后端
我用的是request拦截器,当然了Aop也可以做到,Intercepter👇
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
Result r;
private static final String TOKEN ="token";
@Autowired
UserService service;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1检查请求方法 如果是Options请求 则直接放行
String method =request.getMethod();
if(StringUtils.equals("OPTIONS",method.toUpperCase())){
return true;
}
//2验证球求头种是否有token 如果没有 则直接返回 如果有 进入第三步
if(StringUtils.isEmpty(request.getHeader(TOKEN))) {
this.noAuth(response,"没有携带认证信息");
return false;
}
try{
//3解析token
String jsonToken = EncryptUtil.decrypt(request.getHeader(TOKEN));//解密
AccessToken accessToken = JSONObject.parseObject(jsonToken, AccessToken.class);//解Json
//4.验证token是否过期
if(System.currentTimeMillis()-accessToken.getExpire()>0){
log.warn("token过期");
this.noAuth(response,"认证信息过期,请重新登录");
return false;
}
//5.获取用户名
String username =accessToken.getUsername();
User user = service.login(username);
if(ObjectUtils.isEmpty(user)){
log.warn("用户名{}错误",username);
this.noAuth(response,"未查询到用户信息");
return false;
}
//6如果判断有效期在1分钟之内 重新生成一个token
if(accessToken.getExpire()-System.currentTimeMillis()<60000){
accessToken =new AccessToken(username);
String newToken =EncryptUtil.encrypt(JSONObject.toJSONString(accessToken));
response.setHeader(TOKEN,newToken);
}
return true;
}catch (Exception e){
log.error(e.getMessage(),e);
this.noAuth(response,"没有访问权限");
return false;
}
}
private void noAuth(HttpServletResponse response, String str) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
PrintWriter pw =response.getWriter();
pw.write(JSONObject.toJSONString(r.noToken(str)));
}
}
MVCconfig配置拦截器
@Configuration
public class MVCconfig implements WebMvcConfigurer {
//拦截器注入
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login/**")//不拦截登录请求
}
}
也就是说出了登录请求,其他任何请求我峨嵋你都得看看token是否合法再放行
前端
- 请求拦截器
main.js
axios.interceptors.request.use(config=>{
//你要去请求后端登录接口我放行
if(config.url==='user/login'){
return config
}
//将token塞进请求头
let token =sessionStorage.getItem('token')
config.headers.token =token
return config
}, error=>{
...
})
后端过滤器需要添点东西,不然会出跨域问题👇如下所示,在我们的过滤器种手动添上token,其他不动
httpResponse.addHeader("Access-Control-Allow-Headers",
"token,Accept,Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With");
- 路由守卫
router.beforeEach((to, from, next) => {
if(from.path!=='/'&&to.path ===from.path){
return
}
// 如果访问/login 则直接跳转
if(to.path==='/login'){
return next()
}
//如果不是访问/login 则判断是否登录(也就是SessionStorage中是否有Token)
let token =sessionStorage.getItem('token')
if(token){
return next()
}else{
return next('/login')
}
})
- 响应拦截器
axios.interceptors.response.use(response=>{
if(response.config.url==='/user/login'){
return response
}
let code =response.data.code
if(code == 999){
sessionStorage.clear()
router.push('/login')
return Promise.reject(response.data.msg)
}
let token =response.headers.token
if(token){
sessionStorage.setItem('token',token)
}
return response
})
这里也是我们的过滤器不允许任意在响应头里写东西,所以过期后新生成的token我们写在响应头里会出现跨域问题,也是在过滤器种配置一下,加上这个
//如果额外设置自己的头需要在这定义
httpResponse.setHeader("Access-Control-Expose-Headers", "token");
总结
登录验证的整个流程就这些东西啦,整个前端的流程图
绿框第一次请求登录的流程把它扩展开就是一开始我画的这张图
这下整体的视野就打开了,那么登录验证的内容就这么多啦~