SpringBoot+JWT实现用户注册、登录、认证

819 阅读4分钟

1 前言

这篇文章借鉴了另一篇大佬的文章,大佬的文章写得比这篇详细,这里贴出链接。这篇文章在大佬文章的基础有我自己的一些理解。

SpringBoot 项目 + JWT 完成用户登录、注册、认证 - 掘金 (juejin.cn)

2 环境准备

该文章涉及统一接口返回、全局异常处理

  1. 项目的创建本文省略
  2. 需要使用的依赖如下
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>
<!-- lombok依赖:简化实体类 -->
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
<!-- 数据库驱动 -->
<dependency>
   <groupId>com.mysql</groupId>
   <artifactId>mysql-connector-j</artifactId>
   <scope>runtime</scope>
</dependency>
<!-- MybatisPlus依赖 -->
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.5.3.1</version>
</dependency>
<!-- JWT依赖 -->
<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>4.3.0</version>
</dependency>
  1. 用户表如下
img
  1. sql如下
CREATE TABLE `test`.`Untitled`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL COMMENT '密码',
  `salt` varchar(255) NOT NULL COMMENT '密码加密盐',
  `create_date` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
);
  1. 实体类如下

这里使用UUID来充当密码加密盐

package com.example.domain.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;
import java.util.UUID;

@Data
@TableName("user")
public class User {
   @TableId(value = "id", type = IdType.AUTO)
   private Integer id;
   private String username; //用户名
   private String password; //密码
   private String salt = UUID.randomUUID().toString().replaceAll("-", ""); //盐
   private Date createTime; //创建时间
}
  1. 配置数据源信息
spring:
  datasource:
    url: jdbc:mysql:///test
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

3 注册

3.1 创建USerMapper

利用MybatisPlus来简化mapper

package com.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.domain.entity.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {
}

3.2 创建dto

创建RegisterUserDto来接收前端的传值。

package com.example.domain.dto;

import lombok.Data;

@Data
public class RegisterUserDto {
   private String username;
   private String password;
}

3.3 创建controller

package com.example.controller;

import com.example.domain.dto.RegisterUserDto;
import com.example.domain.vo.Result;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@ResponseBody
@RequestMapping("/user")
public class UserController {
   @Autowired
   private UserService userService;

   @PostMapping("/register")
   public Result register(@RequestBody RegisterUserDto dto) {
      userService.register(dto);

      return Result.success();
   }
}

3.4 创建service

service接口如下

package com.example.service;

import com.example.domain.dto.RegisterUserDto;

public interface UserService {
   void register(RegisterUserDto dto);
}

实现类如下

将生成的盐与密码进行md5加密

package com.example.service.impl;

import com.example.domain.dto.RegisterUserDto;
import com.example.domain.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

@Service
public class UserServiceImpl implements UserService {
   @Autowired
   private UserMapper userMapper;

   @Override
   public void register(RegisterUserDto dto) {
      //创建User对象
      User user = new User();

      //加密密码
      String password = dto.getPassword();
      String salt = user.getSalt();
      String md5Password = DigestUtils.md5DigestAsHex((password + salt).getBytes());

      user.setUsername(dto.getUsername());
      user.setPassword(md5Password);

      userMapper.insert(user);
   }
}

4 登录

4.1 创建dto

这里为了简化文章导致注册dto和登录dto一样,实际项目中是不会一样的

package com.example.domain.dto;

import lombok.Data;

@Data
public class LoginUserDto {
   private String username;
   private String password;
}

4.2 创建jwt工具类

createJwt方法通过传入的User对象生成token resolveJwt方法解析token生成User对象

package com.example.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.domain.entity.User;
import com.example.domain.vo.Status;
import com.example.exception.SystemException;
import lombok.extern.slf4j.Slf4j;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;

@Slf4j
public class JwtUtils {
   //Jwt秘钥
   private static final String key = "abcdefghijklmn";

   //根据用户信息创建Jwt令牌
   public static String createJwt(User user){
      Calendar calendar = Calendar.getInstance();
      Date now = calendar.getTime();

      //过期时间为7天
      calendar.add(Calendar.SECOND, 3600 * 24 * 7);
      return JWT.create()
            //配置JWT自定义信息
            .withClaim("id", user.getId())
            .withClaim("username", user.getUsername())
            .withExpiresAt(calendar.getTime())  //设置过期时间
            .withIssuedAt(now)    //设置创建时间
            .sign(Algorithm.HMAC256(key));   //最终签名
   }

   //根据Jwt验证并解析用户信息
   public static User resolveJwt(String token){
      try {
         JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(key)).build();
         DecodedJWT verify = jwtVerifier.verify(token);  //对JWT令牌进行验证,看看是否被修改
         Map<String, Claim> claims = verify.getClaims();  //获取令牌中内容

         User user = new User();
         user.setId(claims.get("id").asInt());
         user.setUsername(claims.get("username").asString());
         
         return user;
      } catch (TokenExpiredException e) {
         throw new SystemException(Status.FAIL.getCode(), "token过期!");
      } catch (Exception e) {
         throw new SystemException(Status.FAIL.getCode(), "token有误!");
      }
   }
}

4.3 controller

package com.example.controller;

import com.example.domain.dto.LoginUserDto;
import com.example.domain.dto.RegisterUserDto;
import com.example.domain.vo.Result;
import com.example.domain.vo.Status;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;

@RestController
@ResponseBody
@RequestMapping("/user")
public class UserController {
   @Autowired
   private UserService userService;

   @PostMapping("/login")
   public Result login(@RequestBody LoginUserDto dto) {
      return Optional.ofNullable(userService.login(dto))
            .map(Result::success)
            .orElse(Result.fail(Status.FAIL.getCode(), "用户名或密码错误!"));
   }
}

4.4 service

UserService接口

package com.example.service;

import com.example.domain.dto.LoginUserDto;
import com.example.domain.dto.RegisterUserDto;

public interface UserService {
   String login(LoginUserDto dto);
}

实现类

通过用户名查询出用户,将前端上传的密码与盐md5加密与查询出的密码进行比较,相同生成token,不同返回null

package com.example.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.domain.dto.LoginUserDto;
import com.example.domain.dto.RegisterUserDto;
import com.example.domain.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import com.example.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.util.Optional;

@Service
public class UserServiceImpl implements UserService {
   @Autowired
   private UserMapper userMapper;

   @Override
   public String login(LoginUserDto dto) {
      return Optional
            //通过email查询user
            .ofNullable(userMapper.selectOne(Wrappers.<User>query().eq("username", dto.getUsername())))
            //判断密码是否正确
            .filter(user -> {
               String md5Password = DigestUtils.md5DigestAsHex((dto.getPassword() + user.getSalt()).getBytes());
               return md5Password.equals(user.getPassword());
            })
            //密码正确生成token
            .map(JwtUtils::createJwt)
            .orElse(null);
   }
}

5 认证

5.1 创建UserThreadLocal

service层往往我们需要知道当前操作的用户是谁?如查询当前用户的信息、添加当前用户的信息等等,我们知道一个请求对应一个线程,所以可以使用ThreadLocal来存储用户信息。

package com.example.utils;

import com.example.domain.entity.User;

/**
 * 利用ThreadLocal存储登录后的用户信息
 */
public class UserThreadLocal {
   private static final ThreadLocal<User> LOCAL = new ThreadLocal<>();

   private UserThreadLocal() {}

   public static void put(User user) {
      LOCAL.set(user);
   }
   public static User get() {
      return LOCAL.get();
   }
   public static void remove() {
      LOCAL.remove();
   }
}

5.2 创建拦截器

利用拦截器拦截指定请求,获取请求头中的token,校验tokentoken正确将用户信息添加至threadlocal并放行,token有误拦截该请求

package com.example.interceptor;

import com.example.domain.entity.User;
import com.example.domain.vo.Status;
import com.example.exception.SystemException;
import com.example.utils.JwtUtils;
import com.example.utils.UserThreadLocal;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class LoginInterceptor implements HandlerInterceptor {
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      //访问的其他资源直接放行
      if (!(handler instanceof HandlerMethod)) {
         return true;
      }

      //首先从Header中取出JWT
      String authorization = request.getHeader("Authorization");

      //判断是否包含JWT且格式正确
      User user = null;
      if (authorization != null && authorization.startsWith("Bearer ")) {
         user = JwtUtils.resolveJwt(authorization.substring(7));
      }

      if (user == null) {
         throw new SystemException(Status.FAIL.getCode(), "未登录!");
      }

      //将用户信息存入threadlocal
      UserThreadLocal.put(user);

      return true;
   }

   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      //请求线程结束后释放threadlocal中的数据,以防内存泄露
      UserThreadLocal.remove();
   }
}

指定哪些请求需要拦截

package com.example.config;

import com.example.interceptor.LoginInterceptor;
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;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
   @Autowired
   private LoginInterceptor loginInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(loginInterceptor)
            .addPathPatterns("/api/anime/**"); // 指定拦截的路径
   }
}