基于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;//预检请求直接放行
}
//从请求头中拿token
String 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中的token
created() {
this.token = getCookie("token")
}
//发送请求时在headers中放入token
request({
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的用户校验功能。