vlambda博客
学习文章列表

基于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;
@Componentpublic class CheckTokenInterceptor implements HandlerInterceptor {
@Override 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;
@Configurationpublic class InterceptorConfig implements WebMvcConfigurer { @Autowired private CheckTokenInterceptor checkTokenInterceptor;
@Override 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返回给前端

//学生登录@Overridepublic 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的用户校验功能。