文章目录

  • 前言
  • 零 介绍 JWT Token
  • 0.1 概念介绍
  • 0.2 通用工具类(这篇博客没有用到这个工具类)
  • 一、后端流程
  • 0 实体类user和DTO类UserDTO
  • 1 引入jwt依赖
  • 2 编写jwt工具类
  • 3 前后端token验证流程
  • 1 后端登陆接口和数据处理,返给前端token
  • 1controlle层
  • 2 service层次(这里的getone函数是mybatis-plus service层函数,因为结合使用了mybatis-plus所以dao层函数不用定义了)
  • 2 后端自定义jwt拦截器 并注册拦截器
  • 1 自定义jwt拦截器
  • 2 注册jwt拦截器(记得放行登陆接口)
  • 一.五 设定统一返回类后后端返给前端的信息数据
  • 二、前端流程
  • 1.在登陆请求成功后把后端返回数据存储到浏览器
  • 2 定义request.js 统一处理发送的请求为其加上token -------------------------------------------------------------------------统一处理响应 若包含统一返回类code为 token未验证成功code 401 则让用户到登陆页面重新登陆
  • 三、加餐(基于上述分离流程增加简单的vue隐藏菜单)
  • 1 首先观察我们登录时后端的返回结果
  • 2 在Aside.vue 侧边栏中进行代码编写
  • 总结



前言

本篇简单的token验证流程是学习参考自
程序员青戈的个人博客

道祖且长的jwt验证博客

最好熟悉mybatis-plus、
前端使用vue


零 介绍 JWT Token

0.1 概念介绍

json web token 其实就是一个字符串 string令牌

主要用于服务端和客户端保持登录状态使用

header.payload.singnature
JWT 验证流程

首先客户端登录请求携带用户名密码
服务端接收到以后 查库 查不到创建一个新用户

接着把这个用户的id name 作为payload
设置header

设置签名方式 签名秘钥(通常直接写到服务端常量中)
 token过期时间
最终生成token返回前端 
这个token 有三部分 header Payload sign
sign是对 header + payload 经base64编码后 再通过签名密码进行sign后获得的字符串 

此后用户向服务端发请求 服务端解析携带的token 就能判断是不是这个用户
JWT验证身份原理
因为 Jwt
sign是对 header + payload 经base64编码后 再通过签名密码进行sign后获得的字符串
JWT验证 首先对前端传递的 header + payload 通过服务端保存的签名秘钥 算出sign 字符串
和前端传递来的 sign进行比对 看一致不一致 不一致说明发生了篡改 该用户状态不可信任

详细介绍

1、标头(Header)

 令牌类型(即jwt) + 使用的签名算法

{
    “alg”:"HS256",     //签名算法,还有HMAC,RSA,SHA256
    "typ":"jwt"		   //令牌类型,JWT
}

最后通过Base64编码加密组成字符串变成JWT的一部分。
2、有效载荷(Payload)

 令牌的第二部分,有效负载,包含有关实体和其他数据的声明,除敏感信息,同样使用Base64组码加密组成JWT第二部 分

{
	“sub”:"123456",
    "name":"John Doe",
    "admin":true
}
3、签名(Signature)

 Signature需要使用编码后的header和payload以及我们提供的密钥,再使用header指定的算法(HS256)进行签名。


 **签名的作用保证被篡改的数据不会通过 **
4、总的jwt结构就是

header.payload.Signature(header+payload+私钥)

0.2 通用工具类(这篇博客没有用到这个工具类)

依赖
<dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.5.0</version>
 </dependency>
工具类

/**
 * @Author YuanChangLiang
 * @Date 2020/11/10 20:47
 */
public class TokenUtil {
    /**
     * token过期时间
     */
    private static final long EXPIRE_TIME = 30 * 60 * 1000;
    /**
     * token秘钥
     */
    private static final String TOKEN_SECRET = "YuanChangLiang";


    /**
     * 生成签名,30分钟过期
     * @param username 用户名
     * @param loginTime 登录时间
     * @return 生成的token
     */
    public static String sign(String username, String loginTime) {
        try {
            // 设置过期时间
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("Type", "Jwt");
            header.put("alg", "HS256");
            // 返回token字符串
            return JWT.create()
                    .withHeader(header)
                    .withClaim("loginName", username)
                    .withClaim("loginTime", loginTime)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 检验token是否正确
     * @param token 需要校验的token
     * @return 校验是否成功
     */
    public static boolean verify(String token){
        try {
            //设置签名的加密算法:HMAC256
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e){
            return false;
        }
    }
}

一、后端流程

0 实体类user和DTO类UserDTO

前端登陆时后端把前端数据封装成DTO进行操作(这时的DTO中没有token)
然后设置好TOKEN后再把DTO返回给前端

1 引入jwt依赖

<!--        jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <!-- hutool  工具类 方便设置token过期时间等操作-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.2</version>
        </dependency>

2 编写jwt工具类

可以创建一个util包放入该工具类

参考结构如下

java 后端校验token 后端如何验证token_工具类

import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;

public class TokenUtils {
    /**
     * 生成Token
     * @return
     */
    public static String getToken(String userId,String sign){   //以password作为签名
        return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面.作为载荷
                .withExpiresAt(DateUtil.offsetHour(new Date(),2)) //使用huttool里的util设置两小时过期
                .sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
    };
}

java 后端校验token 后端如何验证token_工具类_02

3 前后端token验证流程

前端发送登陆请求 – > 后端登陆接口接受 -->后端数据处理后返给前端token
–> 前端将token存储后 -->每次请求都带着这个token去访问 —>后端设置jwtoken拦截器 -->只放行登陆接口 -->如果前端访问别的接口必须带有token
–> 否则被拦截器拦截 并让前端回复到登陆页面

1 后端登陆接口和数据处理,返给前端token

1controlle层
//登陆接口
    @PostMapping("/login")
    public Result login(@RequestBody UserDTO userDTO){
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        //通过hutool 的工具类对是否为空进行判断
        if (StrUtil.isBlank(username) || StrUtil.isBlank(password)){
            //调用common中的错误函数进行数据包装
            return Result.error(Constants.CODE_400,"参数错误");
        }
        UserDTO dto = userService.login(userDTO);
        return  Result.success(dto);
    }

参考结构

java 后端校验token 后端如何验证token_工具类_03

2 service层次(这里的getone函数是mybatis-plus service层函数,因为结合使用了mybatis-plus所以dao层函数不用定义了)

这里的service层进行一番操作 最终返回带有token的userDTO给前端

java 后端校验token 后端如何验证token_User_04

//登陆操作     判断是否有这个用户
    public UserDTO login(UserDTO userDTO) {

        User one = getUserInfo(userDTO);

        //业务异常
        if (one != null) {
            //把User属性copy给UserDto再返回给前端
            userDTO.setUsername(one.getUsername());
            userDTO.setNickname(one.getNickname());
            userDTO.setAvatarUrl(one.getAvatarUrl());
            //设置token
            String token = TokenUtils.getToken(one.getId().toString(), one.getPassword());
            userDTO.setToken(token);
           
            //返回前端带TOKEN的数据
            return userDTO;
        } else {
            throw new ServiceException(Constants.CODE_600, "用户名或密码错误");
        }
    }
    
    //根据UserDto获得UserInfo方法
    private User getUserInfo(UserDTO userDTO){

        //使用mybatis-plus进行操作
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", userDTO.getUsername());
        queryWrapper.eq("password", userDTO.getPassword());
        User one;  //根据UserDto从数据库查询User
        //sql异常 系统异常
        try {
            one = getOne(queryWrapper); //根据UserDto从数据库查询User
        }catch (Exception e){
            LOG.error(e);
            throw new ServiceException(Constants.CODE_500,"系统错误");
        }

        return one;
    }

2 后端自定义jwt拦截器 并注册拦截器

1 自定义jwt拦截器

代码如下

import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.example.back.common.Constants;
import com.example.back.entity.User;
import com.example.back.exception.ServiceException;
import com.example.back.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class JwtInterceptor extends InterceptorRegistry implements HandlerInterceptor {
    @Autowired
    private UserService userService;


    public boolean preHandle(HttpServletRequest httpServletRequest,
                             HttpServletResponse httpServletResponse, Object object) throws Exception{

        //获取token
       String token = httpServletRequest.getHeader("token");
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        // 执行认证
        if (StrUtil.isBlank(token)) {
            throw new ServiceException(Constants.CODE_401,"没有token,重新登陆");
        }

        //获取token的userid
        String userId;
        try {
            //解密获取
            userId = JWT.decode(token).getAudience().get(0); //得到token中的userid载荷
        } catch (JWTDecodeException j) {
            throw new ServiceException(Constants.CODE_401,"token验证失败,重新登陆");
        }

        //根据userid查询数据库
        User user = userService.getById(userId);

        if(user == null){
            throw new ServiceException(Constants.CODE_401,"token验证失败,重新登陆");
        }

        // 用户密码加签验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            throw new ServiceException(Constants.CODE_401,"token验证失败,重新登陆");
        }

        return true;
    }
}

参考结构如下

java 后端校验token 后端如何验证token_拦截器_05


代码解释如下

java 后端校验token 后端如何验证token_拦截器_06


java 后端校验token 后端如何验证token_拦截器_07

2 注册jwt拦截器(记得放行登陆接口)

代码如下

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")    //拦截所有请求 通过判断token是否合法来决定是否登陆
                .excludePathPatterns("/user/login","/user/register","/**/export","/**/import", "/file/**","/role/**","/menu/**"); //放行接口
    }

}

java 后端校验token 后端如何验证token_java_08

一.五 设定统一返回类后后端返给前端的信息数据

成功状态

{
    "code": 200,
    "msg": "SUCCESS",
    "data": {
        "username": "xiaogang",
        "password": "123123123",
        "token": "eyJ0eXAiOiJKV1QqweeiJIUzI1NiJ9.eyJhdWQiOiIqweqwehwIjoxNjUwMDI4OTEzfQ.DdTGv9dguw65SZtmQUwtdWS1uT5_15x4CBVgLApM5uE"
    }
}

失败状态

{
    "code": 401,
    "msg": "fail",
    "data": null

二、前端流程

1.在登陆请求成功后把后端返回数据存储到浏览器

java 后端校验token 后端如何验证token_java 后端校验token_09

<script>

import request from "@/utils/request";

export default {
  name: "Login",
  data() {
    return {
      user: {},
      rules: {
        username: [
          {required: true, message: '请输入用户名', trigger: 'blur'},
          {min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur'}
        ],
        password: [
          {required: true, message: '请输入密码', trigger: 'blur'},
          {min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur'}
        ],
      }
    }
  },
  methods: {
    login(){
      request.post("/user/login", this.user).then(res => {
        if(res.code === '200'){
          this.$router.push("/")
          this.$message.success("登陆成功")
          localStorage.setItem("user",JSON.stringify(res.data)) //把用户信息存到浏览器
        }else {
          this.$message.error(res.msg)
        }
      })
    }
  }
}
</script>

2 定义request.js 统一处理发送的请求为其加上token -------------------------------------------------------------------------统一处理响应 若包含统一返回类code为 token未验证成功code 401 则让用户到登陆页面重新登陆

import axios from 'axios'
import router from "@/router";

const request = axios.create({
    baseURL: 'http://localhost:8081',  // 注意!! 这里是全局统一加上了 '/api' 前缀,也就是说所有接口都会加上'/api'前缀在,页面里面写接口的时候就不要加 '/api'了,否则会出现2个'/api',类似 '/api/api/user'这样的报错,切记!!!
    timeout: 5000
})

// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'application/json;charset=utf-8';
    //从前端拿到user对象 登陆时进行了存储
    let user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null
    if (user) {
        config.headers['token'] = user.token;  // 设置请求头
    }
    return config
}, error => {
    return Promise.reject(error)
});

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
    response => {
        let res = response.data;
        // 如果是返回的文件
        if (response.config.responseType === 'blob') {
            return res
        }
        // 兼容服务端返回的字符串数据
        if (typeof res === 'string') {
            res = res ? JSON.parse(res) : res
        }

        // 当权限验证不通过的时候给出提示
        if(res.code === '401'){
            router.push("/login")
        }

        return res;
    },
    error => {
        console.log('err' + error) // for debug
        return Promise.reject(error)
    }
)


export default request

三、加餐(基于上述分离流程增加简单的vue隐藏菜单)

1 首先观察我们登录时后端的返回结果

java 后端校验token 后端如何验证token_java_10

2 在Aside.vue 侧边栏中进行代码编写

java 后端校验token 后端如何验证token_java 后端校验token_11

<template>
  <el-menu :default-openeds="['1', '3']" style="min-height: 100vh; overflow-x: hidden"

           background-color="rgb(48,65,86)"
           text-color="#fff"
           active-text-color="#ffd04b"
           :collapse-transition="false"
           :collapse="isCollapse"
           router
           @select="handleSelect"
  >

    <div style="height: 60px; line-height: 60px; text-align: center">
      <img src="../assets/logo.png" style="width: 20px; position: relative; top: 5px; margin-right: 5px">
      <b style="color: white" v-show="logoTextShow">后台管理系统</b>
    </div>

    <el-menu-item  index="/home">
      <template slot="title"><i class="el-icon-house"></i>
        <span>主页</span>
      </template>
    </el-menu-item>

<!--    <el-menu-item  index="/person">-->
<!--      <template slot="title"><i class="el-icon-moon"></i>-->
<!--        <span>个人信息</span>-->
<!--      </template>-->
<!--    </el-menu-item>-->

    <el-submenu index="2">
      <template slot="title"><i class="el-icon-menu"></i>
        <span>系统管理</span>
      </template>
      <el-menu-item-group>

        <el-menu-item index="/user" :hidden="this.hidden">
          <template slot="title"><i class="el-icon-s-custom"></i>
            <span>用户管理</span>
          </template>
        </el-menu-item>

        <el-menu-item index="/admin" :hidden="this.hidden">
          <template slot="title"><i class="el-icon-s-custom"></i>
            <span>管理员管理</span>
          </template>
        </el-menu-item>
        <el-menu-item index="/equipment"  >
          <template slot="title"><i class="el-icon-document"></i>
            <span>查看设备</span>
          </template>
        </el-menu-item>
        <el-menu-item index="/historyAll">
          <template slot="title"><i class="el-icon-document"></i>
            <span>全部历史记录</span>
          </template>
        </el-menu-item>
        <el-menu-item index="/pay" :hidden="this.hidden">
          <template slot="title"><i class="el-icon-document"></i>
            <span>设备支付记录</span>
          </template>
        </el-menu-item>
<!--        <el-menu-item index="/menu">-->
<!--          <template slot="title"><i class="el-icon-document"></i>-->
<!--            <span>菜单管理</span>-->
<!--          </template>-->
<!--        </el-menu-item>-->
      </el-menu-item-group>

    </el-submenu>
  </el-menu>
</template>

<script>
import router from "@/router";
export default {
  name: "Aside",
  props: {
    isCollapse: Boolean,
    logoTextShow: Boolean
  },
  methods: {
  },
  data() {
    return {
      //模糊用户名查询用
      user: "",
      phone: "",
      //表格数据
      tableDate: [],
      role:'',
      hidden:''
    }
  },
  created() {
    //从前端拿到user对象 登陆时进行了存储
    let user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null
    if (user) {
      this.role = user.role;  // 设置请求头
    }
    if(this.role === 3){
      this.hidden=false;
    }else {
      this.hidden=true;
    }
    console.log(this.role)
  }
}
</script>

<style scoped>

</style>

总结

前端发送登陆请求 – > 后端登陆接口接受 -->后端数据处理后返给前端token
–> 前端将token存储后 -->每次请求都带着这个token去访问 —>后端设置jwtoken拦截器 -->只放行登陆接口 -->如果前端访问别的接口必须带有token
–> 否则被拦截器拦截 并让前端回复到登陆页面