基于token实现前后端分离用户校验
前言:在前后端分离架构中,单点登录是必须实现的功能。而所谓单点登录,指的是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。如果是通过传统的session的方式的话,在分布式系统中则需要搭建Redis等中间件,同时为了保证Redis的高可用,必须做集群,但是如果说只是为了做一个校验机制还得做缓存集群的话,是需要不小的开销的。而token是一种no session的校验机制,token只依赖于浏览器的cookie,在能够完成身份校验的同时,还可以存储用户的信息。
一、流程分析
1、后端分析
1.1 登录接口
1、校验账号密码
2、校验成功,根据特定的规则生成token字符串(令牌)
3、将token字符串响应给前端
1.2 拦截器(拦截受限接口)
1、从请求头中获取token
2、验证token的有效性(正确性、时效性)
3、token验证通过,进行接口响应
2、前端分析
1、在登录页面中输入账号密码进行登录
2、如果登录成功,将后端返回的token存储在cookie中
3、在受限资源页面中首先从cookie中获取token,将token放在请求头中向后端发送请求
图解如下:
二、Vue3使用cookie工具类
1、在vue项目的utils目录下新建cookie_utils.js
存储的格式为:key1=value1;key2=value2
var operator = "=";export function getCookie(keyStr){var value = null;var s = window.document.cookie;var arr = s.split("; ");for(var i=0; i<arr.length; i++){var str = arr[i];var k = str.split(operator)[0];var v = str.split(operator)[1];if(k === keyStr){value = v;break;}}return value;}export function setCookie(key,value){document.cookie = key+operator+value;}
2、在需要使用cookie的vue文件中引用js的方法,如getCookie()
import {getCookie} from "@/utils/cookie_utils";
3、使用方法获取token
this.token = getCookie("token")
三、使用jwt生成token并完成单点登录
普通的token存在的问题
1、安全性较差(直接暴露在浏览器cookie中)
2、无法处理时效性问题(不能设置过期时间)
jwt简介
jwt是根据特定规则生成的字符串,由三个部分组成:
HEADER、PAYLOAD、VERIFY SIGNATURE
1、HEADER:这部分的内容是公开的,一般为:
{"alg":"HS256",//加密方式"typ":"JWT"//token规则}
2、PAYLOAD:这部分是加密的,用于存储用户的数据
3、VERIFY SIGNATURE:防伪功能
使用jwt
1、添加依赖
<!--jwt依赖(两个)--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency><dependencry><goupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>
2、创建jwt工具类:使用jwt生成token以及解析token
生成token的核心:
加密方式、加密密钥、tokenId、data、过期时间
解析token的核心:
校验token、获取token中存储的数据
import io.jsonwebtoken.*;import java.util.Date;import java.util.HashMap;import java.util.List;import java.util.Map;/*** 使用jwt规则生成token字符串*/public class JwtUtil {private static final String DEFAULT_PWD = "jey0426";//默认加密密钥private static final long DEFAULT_TOKEN_TIME = 24*60*60*1000;//默认过期时间为一天/**** @param data token中携带的数据,String* @param tokenId tokenId,唯一,一般为userId* @param map map(String,Object)比如存放角色和权限信息* @param expirationTime 过期时间(单位:小时)* @return token*/public static String createToken(String data, String tokenId, Map<String,Object> map, int expirationTime){return Jwts.builder().setSubject(data)//token中携带的数据.setIssuedAt(new Date())//设置token的生成时间.setId(tokenId)//设置token的id(一般为用户id).setClaims(map)//可以在里面存放map类型数据(用户角色、权限信息等).setExpiration(new Date(System.currentTimeMillis() + (long) expirationTime * 60 * 60 * 1000))//设置过期时间.signWith(SignatureAlgorithm.HS256, DEFAULT_PWD)//设置加密方式和加密密钥.compact();//生成token}/**** @param data token中携带的数据,String* @param tokenId tokenId,唯一,一般为userId* @return token*/public static String createToken(String data, String tokenId){return Jwts.builder().setSubject(data)//token中携带的数据.setIssuedAt(new Date())//设置token的生成时间.setId(tokenId)//设置token的id(一般为用户id).setExpiration(new Date(System.currentTimeMillis() + DEFAULT_TOKEN_TIME))//设置过期时间为当前时间加1天.signWith(SignatureAlgorithm.HS256, DEFAULT_PWD)//设置加密方式和加密密钥.compact();//生成token}/*** 解析token:只获取subject数据* @param token token* @return Map<String,Object>,正确则put("state","true")*/public static Map<String,Object> parsingToken(String token){HashMap<String, Object> map = new HashMap<>();JwtParser parser = Jwts.parser();parser.setSigningKey(DEFAULT_PWD);//设置解密密钥try {//该方法,如果token(密码正确,有效期内),则正常执行,否则抛出异常Jws<Claims> claimsJws = parser.parseClaimsJws(token);Claims body = claimsJws.getBody();//获取token中的用户数据String subject = body.getSubject();//获取subject数据map.put("subject",subject);map.put("state","true");} catch (Exception e) {//token过期map.put("state","token过期");return map;}return map;}/*** 解析token:可以获取原来token中map中的value* @param token token* @param mapKeyList token中map的key集合* @return Map<String,Object>,token通过则put("state","true")*/public static Map<String,Object> parsingToken(String token, List<String> mapKeyList){HashMap<String, Object> map = new HashMap<>();JwtParser parser = Jwts.parser();parser.setSigningKey(DEFAULT_PWD);//设置解密密钥try {//该方法,如果token(密码正确,有效期内),则正常执行,否则抛出异常Jws<Claims> claimsJws = parser.parseClaimsJws(token);Claims body = claimsJws.getBody();//获取token中的用户数据String subject = body.getSubject();//获取subject数据map.put("subject",subject);//获取生成token时存储的map中的值String value1 = body.get("key1", String.class);//可以指定value类型,默认为Object//直接使用stream并行流拿到token原来map的数据并放入返回的map中mapKeyList.parallelStream().forEach(key -> {Object value = body.get(key);map.put(key,value);});map.put("state","true");} catch (Exception e) {//token过期map.put("state","token过期");return map;}return map;}}
3、新建拦截器校验token
新建拦截器包目录interceptor,在目录中创建拦截器类CheckTokenInterceptor.java
一、类加@Component注解,将类交给Spring容器管理
二、实现HandlerInterceptor接口
三、重写preHandle()方法,从请求头中拿到token,进行逻辑校验,返回true通过,false则不通过
四、可以封装一个doResponse方法来返回提示信息
五、处理http预检请求
package com.jey.springboot.interceptor;import com.fasterxml.jackson.databind.ObjectMapper;import com.jey.springboot.common.utils.JwtUtil;import com.jey.springboot.common.vo.ResultVo;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;import java.util.Map;public class CheckTokenInterceptor implements HandlerInterceptor {public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//处理预检请求String method = request.getMethod();if ("OPTIONS".equalsIgnoreCase(method)){//不分大小写的equals()return true;//预检请求直接放行}//从请求头中拿tokenString token = request.getHeader("token");if (token == null){ResultVo resultVo = ResultVo.ERROR("请先登录");doResponse(response,resultVo);return false;}else {Map<String, Object> map = JwtUtil.parsingToken(token);Object state = map.get("state");if (state instanceof String){String s = (String) state;if ("true".equals(s)){return true;}else {ResultVo resultVo = ResultVo.ERROR("登录过期,请重新登录");doResponse(response,resultVo);return false;}}else {ResultVo resultVo = ResultVo.ERROR("token不合法");doResponse(response,resultVo);return false;}}}private void doResponse(HttpServletResponse response, ResultVo resultVo) throws IOException {response.setContentType("application/json");response.setCharacterEncoding("utf-8");PrintWriter out = response.getWriter();String s = new ObjectMapper().writeValueAsString(resultVo);out.print(s);out.flush();out.close();}}
4、将拦截器注册到容器中
在config包目录中新建拦截器配置类InterceptorConfig.java
1、配置类加@Configuration注解
2、实现WebMvcConfigurer接口
3、使用@Autowired注入自定义拦截器
4、重写addInterceptors(InterceptorRegistry registry)方法
5、将拦截器注册到容器中,并设置要拦截的请求路径与不拦截的请求路径
package com.jey.springboot.config;import com.jey.springboot.interceptor.CheckTokenInterceptor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;public class InterceptorConfig implements WebMvcConfigurer {private CheckTokenInterceptor checkTokenInterceptor;public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(checkTokenInterceptor)//将拦截器注册到容器中.addPathPatterns("/**")//拦截哪些请求.excludePathPatterns("/public/**");//不拦截哪些请求}}
5、前端登录成功后将token存储在cookie中
request({url:"/stu/login",method:"post",headers:{'Content-Type':'application/x-www-form-urlencoded'},params:{stuId:this.form.username,password:this.form.password},withCredentials:true}).then(res => {if (res.code === 200){var token = res.msg;setCookie("token",token)this.$router.push({name:"StuLayout",params:{stuId:this.form.username}})}else {alert(res.msg)}})
6、在资源受限页面中首先在cookie中拿到token,并在发送请求时在请求头中放入token
data() {return{token:""}},//vue生命周期函数中首先获取cookie中的tokencreated() {this.token = getCookie("token")}//发送请求时在headers中放入tokenrequest({method:"get",url:"/stu/selectStu",params:{stuId:1},headers:{token:this.token}}).then(res => {//...})
7、后端在用户登录成功后使用工具类生成token返回给前端
//学生登录public ResultVo checkLogin(String stuId, String pwd) {Stu stu = stuMapper.queryStu(stuId,"正常");if (stu == null){return ResultVo.ERROR("账号不存在!");}if (pwd.equals(stu.getStuPwd())){String token = JwtUtil.createToken(stu.getStuName(), stuId);return ResultVo.OK(token,null);}else {return ResultVo.ERROR("密码错误!");}}
至此,我们利用jwt完成了基于token的用户校验功能。
