SpringBoot+JWT实现注册、登录、状态续签流程分析

作者:什么都干的派森 时间:2022-09-29 09:07:11 

一、实现流程

1.注册

SpringBoot+JWT实现注册、登录、状态续签流程分析

SpringBoot+JWT实现注册、登录、状态续签流程分析

2.登录

SpringBoot+JWT实现注册、登录、状态续签流程分析

SpringBoot+JWT实现注册、登录、状态续签流程分析

3.登录保持【状态续签】

SpringBoot+JWT实现注册、登录、状态续签流程分析

SpringBoot+JWT实现注册、登录、状态续签流程分析

二、实现方法

项目结构

SpringBoot+JWT实现注册、登录、状态续签流程分析

1.引入依赖

<!-- spring-web -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql连接器 -->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Mybatis Plus -->
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.4.3.4</version>
</dependency>
<!-- druid -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.2.8</version>
</dependency>
<!-- jwt -->
<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>3.10.3</version>
</dependency>

2.application配置文件

spring:
 application:
   name: jwtLogin   # 应用名称
 datasource:
   druid:
     url: jdbc:mysql://192.168.0.111:3306/login_test?useSSL=false&serverTimezone=UTC
     username: samon
     password: 123456
     driver-class-name: com.mysql.jdbc.Driver
server:
 port: 8848   # 应用服务 WEB 访问端口

3.mysql建表

用户表
SpringBoot+JWT实现注册、登录、状态续签流程分析

4.Bean

1.bean/user.java
用户bean

package com.cxstar.bean;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
@Data
public class User {
   // 自增长id
   @TableId(type = IdType.AUTO)
   private Integer id;
   private String userName;
   private String passWord;
   private Date createTime;
   private Date lastLogin;
   public  User() {}
   public User(Integer id, String userName) {
       this.id = id;
       this.userName = userName;
   }
}

2.bean/ServiceRes.java
统一Service返回类

package com.cxstar.bean;
import lombok.Data;
@Data
public class ServiceRes {
   private Integer code;
   private String msg;
   private String jwt;
   private ServiceRes() {}
   public ServiceRes(Integer code, String msg) {
       this.code = code;
       this.msg = msg;
   }
   public ServiceRes(Integer code, String msg, String jwt) {
       this.code = code;
       this.msg = msg;
       this.jwt = jwt;
   }
}

3.bean/ControllerRes.java
统一Controller返回类

package com.cxstar.bean;
import lombok.Data;
@Data
public class ControllerRes {
   private Integer code;
   private String msg;
   public ControllerRes(Integer code, String msg) {
       this.code = code;
       this.msg = msg;
   }
}

5.Mapper mapper/UserMapper.java
继承MP

package com.cxstar.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxstar.bean.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

6.service service.userService.interface
登录、注册、改密业务【状态续签测试】

package com.cxstar.service;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
public interface userService {
   // 注册
   ServiceRes register(User user);
   // 登录
   ServiceRes login(User user);
   // 改密【带权限业务,用于状态续签测试】
   ServiceRes changePassWord(User user);
}

service.impl.UserServiceImpl.java
登录、注册、改密业务【状态续签测试】

package com.cxstar.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
import com.cxstar.mapper.UserMapper;
import com.cxstar.service.userService;
import com.cxstar.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class UserServiceImpl implements userService {
   @Autowired
   UserMapper userMapper;
   /**
    * 注册
    * @param user 用户类
    * @return ServiceRes
    */
   @Override
   public ServiceRes register(User user) {
       // 判断用户名是否唯一
       if(this.checkUserNameIsUnique(user)) {
           // 判断用户名密码是否合法
           if(this.checkUserNameAndPassword(user)) {
               // 密码MD5加密
               user.setPassWord(this.MD5Code(user.getPassWord()));
               // 加入创建时间
               user.setCreateTime(new Date());
               // 入库
               userMapper.insert(user);
               return new ServiceRes(1, "注册成功");
           } else return new ServiceRes(-1, "用户名或密码不合法");
       } else return new ServiceRes(-1, "用户名已存在");
   }
   /**
    * 登录
    * @param user 用户类
    * @return ServiceRes
    */
   @Override
   public ServiceRes login(User user) {
       // 判断用户名密码是否合法
       if(this.checkUserNameAndPassword(user)) {
           // 密码MD5加密
           user.setPassWord(this.MD5Code(user.getPassWord()));
           // 检查用户是否存在
           User curUser = this.checkUserIsExit(user);
           if(curUser!=null) {
               // 更新用户最后登录时间
               curUser.setLastLogin(new Date());
               userMapper.updateById(curUser);
               // 生成jwt
               Map<String, String> payload = new HashMap<>();
               payload.put("userId", curUser.getId().toString()); // 加入一些非敏感的用户信息
               payload.put("userName", curUser.getUserName());    // 加入一些非敏感的用户信息
               String jwt = JwtUtil.generateToken(payload);
               return new ServiceRes(1, "登录成功", jwt);
           } else return new ServiceRes(-1, "用户名或密码错误");
       } else return new ServiceRes(-1, "用户名或密码不合法");
   }
   /**
    * 改密业务
    * @return ServiceRes
    */
   @Override
   public ServiceRes changePassWord(User user) {
       if(this.updatePassWord(user)) return new ServiceRes(1, "改密成功");
       else return new ServiceRes(-1, "改密失败");
   }
   /**
    * 非对称加密
    * @param text 明文
    * @return 密文
    */
   private String MD5Code(String text) {
       return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8));
   }
   /**
    * 修改密码方法
    * @param user 传入用户名和新密码
    * @return 改密成功返回 true 失败返回 false
    */
   private Boolean updatePassWord(User user) {
       // 密码非对称加密
       user.setPassWord(this.MD5Code(user.getPassWord()));
       // 更新密码
       return userMapper.updateById(user)>0;
   }
   /**
    * 检查用户是否存在【用户名密码相同】
    * @param user 用户类
    * @return 用户存在返回 用户对象 不存在返回 null
    */
   private User checkUserIsExit(User user) {
       LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
       lqw.eq(User::getUserName, user.getUserName());
       lqw.eq(User::getPassWord, user.getPassWord());
       return userMapper.selectOne(lqw);
   }
   /**
    * 判断用户名是否唯一
    * @param user 用户类
    * @return 唯一返回 true 不唯一返回 false
    */
   private Boolean checkUserNameIsUnique(User user) {
       LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
       lqw.eq(User::getUserName, user.getUserName());
       List<User> userList = userMapper.selectList(lqw);
       return userList.size() == 0;
   }
   /**
    * 判断用户名密码是否合法
    * @param user 用户类
    * @return 满足 【英文字母、数字、下划线】 返回 true,否则返回 false
    */
   private Boolean checkUserNameAndPassword(User user) {
       String regex = "^[_a-z0-9A-Z]+$";
       return user.getUserName().matches(regex) && user.getPassWord().matches(regex);
   }

}

6.Controller controller/UserController.java
登录、注册、改密业务【状态续签测试】

package com.cxstar.controller;
import com.cxstar.bean.ControllerRes;
import com.cxstar.bean.ServiceRes;
import com.cxstar.bean.User;
import com.cxstar.service.userService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
   @Autowired
   userService userService;
   @PostMapping("/register")
   public ControllerRes register(User user) {
       // 注册
       ServiceRes serviceRes = userService.register(user);
       return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
   }
   @PostMapping("/login")
   public ControllerRes login(User user, HttpServletResponse response) {
       // 登录
       ServiceRes serviceRes = userService.login(user);
       // 登录成功后往响应头插入jwt
       if(serviceRes.getJwt() != null) response.addHeader("access-token", serviceRes.getJwt());
       return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
   }
   @PutMapping("/pwd")
   public ControllerRes changePassWord(User user, HttpServletRequest request) {
       // 取出jwt中的用户
       User jwtUser = (User)request.getAttribute("jwt-user");
       // 合并jwt中用户的用户名与传入用户的新密码
       // 此处不能直接使用传入的用户名,防止恶意修改其他用户的密码
       user.setId(jwtUser.getId());
       // 改密
       ServiceRes serviceRes = userService.changePassWord(user);
       return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg());
   }
}

7.JWT工具类 utils/JwtUtil.java
生成和解析 token 的方法

package com.cxstar.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
   // 签名密钥
   private static final String SECRET = "hello JWT *%$#$&";
   /**
    * 生成token
    * @param payload token携带的信息
    * @return token字符串
    */
   public static String generateToken(Map<String,String> payload){
       // 指定token过期时间
       Calendar calendar = Calendar.getInstance();
       calendar.add(Calendar.HOUR, 24);  // 24小时
       JWTCreator.Builder builder = JWT.create();
       // 构建payload
       payload.forEach(builder::withClaim);
       // 指定签发时间、过期时间 和 签名算法,并返回token
       String token = builder.withIssuedAt(new Date()).withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET));
       return token;
   }

/**
    * 解析token
    * @param token token字符串
    * @return 解析后的token类
    */
   public static DecodedJWT decodeToken(String token){
       JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
       DecodedJWT decodedJWT = jwtVerifier.verify(token);
       return decodedJWT;
   }
}

8.HandlerInterceptor * interceptor.java
* 业务实现

package com.cxstar.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.cxstar.bean.ControllerRes;
import com.cxstar.bean.User;
import com.cxstar.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* 拦截需要授权的接口
*/
@Slf4j
public class PermisssionInterceptor implements HandlerInterceptor {

// 目标方法执行前调用
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

// 检查用户JWT
       String jwt = request.getHeader("access-token");

// 校验并取出私有信息
       try {

// token 解码
           DecodedJWT dj = JwtUtil.decodeToken(jwt);

// 取出基本用户信息加入请求头 --------------------------------------------------------------------------------
           String userId = dj.getClaim("userId").asString();
           String userName = dj.getClaim("userName").asString();
           // jwt校验合格的,将 jwt 中存的用户信息加入请求头,不合格的,请求头存个空用户
           request.setAttribute("jwt-user", userId!=null?new User(Integer.valueOf(userId), userName):new User());
           // -------------------------------------------------------------------------------------------------------

// 计算当前时间是否超过过期时间的一半,如果是就帮用户续签 --------------------------
           // 此处并不是永久续签,只是为 大于过期时间的一半 且 小于过期时间 的 token 续签
           Long expTime = dj.getExpiresAt().getTime();
           Long iatTime = dj.getIssuedAt().getTime();
           Long nowTime = new Date().getTime();
           if((nowTime-iatTime) > (expTime-iatTime)/2) {
               // 生成新的jwt
               Map<String, String> payload = new HashMap<>();
               payload.put("userId", userId); // 加入一些非敏感的用户信息
               payload.put("userName", userName);    // 加入一些非敏感的用户信息
               String newJwt = JwtUtil.generateToken(payload);
               // 加入返回头
               response.addHeader("access-token", newJwt);
           }
           // -----------------------------------------------------------------------

return true;

} catch (JWTDecodeException e) {
           log.error("令牌错误");
           addResBody(response, new ControllerRes(-1, "令牌错误"));  // 新增返回体
           return false;

} catch (TokenExpiredException e) {
           log.error("令牌过期");
           addResBody(response, new ControllerRes(-1, "令牌过期"));  // 新增返回体
           return false;
       }

}

// 目标方法执行后调用
   @Override
   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
       HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
   }

// 页面渲染前调用
   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
   }

private void addResBody(HttpServletResponse response, ControllerRes res) throws IOException {

response.setStatus(HttpServletResponse.SC_FORBIDDEN);        // 设置状态码

response.setCharacterEncoding("UTF-8");
       response.setContentType("application/json; charset=utf-8");
       PrintWriter out = response.getWriter();
       out.write(JSONObject.toJSONString(res));
       out.flush();
       out.close();

}

}

config/PermissionWebConfig.java
* 拦截规则

package com.cxstar.config;
import com.cxstar.interceptor.PermisssionInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class PermissionWebConfig implements WebMvcConfigurer {
   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new PermisssionInterceptor())
               .addPathPatterns("/**")    // 拦截哪些页面
               .excludePathPatterns("/user/login", "/user/register");   // 放行哪些页面
   }
}

三、测试

1.注册

注册成功

SpringBoot+JWT实现注册、登录、状态续签流程分析

数据入库

SpringBoot+JWT实现注册、登录、状态续签流程分析

2.登录

登录成功

SpringBoot+JWT实现注册、登录、状态续签流程分析

查看登录后返回的token

SpringBoot+JWT实现注册、登录、状态续签流程分析

3.状态续签【登录保持】

使用上一步登录返回的 token 请求改密业务

SpringBoot+JWT实现注册、登录、状态续签流程分析

当 JWT 存在时间小于 JWT 过期时间的一半时
业务会执行成功
执行业务不会返回续签的 token

SpringBoot+JWT实现注册、登录、状态续签流程分析

当 JWT 存在时间大于 JWT 过期时间的一半 且 小于过期时间 时
业务会执行成功
执行业务会返回续签的 token,前端的下次请求需要使用新续签的 token

SpringBoot+JWT实现注册、登录、状态续签流程分析

当 JWT 存在时间大于 JWT 过期时间 时
业务会执行失败
执行业务不会返回续签的 token

SpringBoot+JWT实现注册、登录、状态续签流程分析

来源:https://blog.csdn.net/weixin_43721000/article/details/125042403

标签:SpringBoot,JWT,登录,状态续签
0
投稿

猜你喜欢

  • Java Callable接口实现细节详解

    2023-11-10 05:34:26
  • 将本地jar包安装进入maven仓库(实现方法)

    2022-06-18 00:35:54
  • springboot+vue制作后台管理系统项目

    2022-02-26 06:08:37
  • CentOS 7系统下配置自定义JDK的教程

    2022-02-27 13:46:26
  • java多线程下载实例详解

    2023-05-19 05:53:37
  • spring BeanProcessor接口详解

    2021-12-26 07:32:17
  • Java 十大排序算法之归并排序刨析

    2022-03-15 17:29:20
  • Android实现倒计时效果

    2021-10-13 03:25:29
  • Java实现线程安全单例模式的五种方式的示例代码

    2023-09-26 16:41:23
  • SpringMVC使用MultipartFile实现文件上传

    2021-07-01 03:50:22
  • Android利用属性动画实现优酷菜单

    2022-06-15 13:16:44
  • SpringBoot redis分布式缓存实现过程解析

    2023-10-10 22:32:25
  • java的泛型你真的了解吗

    2022-07-25 09:40:06
  • SpringBoot加载读取配置文件过程详细分析

    2021-12-29 03:07:36
  • 聊聊Spring——AOP详解(AOP概览)

    2023-11-01 04:44:18
  • Java的二叉树排序以及遍历文件展示文本格式的文件树

    2021-08-04 07:13:43
  • SpringBoot配置shiro安全框架的实现

    2023-09-07 19:55:38
  • C#几种截取字符串的方法小结

    2023-07-16 09:55:10
  • 大前端代码重构之事件拦截iOS Flutter Vue示例分析

    2021-12-11 20:16:53
  • C#绘制飞行棋地图小程序

    2021-11-20 23:19:10
  • asp之家 软件编程 m.aspxhome.com