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

网友投稿 278 2022-07-25


目录一、实现流程1.注册2.登录3.登录保持【状态续签】二、实现方法1.引入依赖2.application配置文件3.mysql建表4.Bean三、测试1.注册2.登录3.状态续签【登录保持】

一、实现流程

1.注册

2.登录

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

二、实现方法

项目结构

1.引入依赖

org.springframework.boot

spring-boot-starter-web

mysql

mysql-connector-java

com.baomidou

mybatis-plus-boot-starter

3.4.3.4

com.alibaba

druid-spring-boot-starter

1.2.8

com.auth0

java-jwt

3.10.3

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建表

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 {

}

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 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 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 lqw = new LambdaQueryWrapper<>();

lqw.eq(User::getUserName, user.getUserName());

List 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());

}

http://@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 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 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.注册

注册成功

数据入库

2.登录

登录成功

查看登录后返回的token

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

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

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

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

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


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:如何在Java中使用正则表达式API
下一篇:Java中的线程生命周期核心概念
相关文章

 发表评论

暂时没有评论,来抢沙发吧~