1 前言
这篇文章借鉴了另一篇大佬的文章,大佬的文章写得比这篇详细,这里贴出链接。这篇文章在大佬文章的基础有我自己的一些理解。
SpringBoot 项目 + JWT 完成用户登录、注册、认证 - 掘金 (juejin.cn)
2 环境准备
该文章涉及统一接口返回、全局异常处理
- 统一接口返回请参考SpringBoot接口统一返回 - 掘金 (juejin.cn)
- 全局异常处理请参考SpringBoot全局异常处理 - 掘金 (juejin.cn)
- 项目的创建本文省略
- 需要使用的依赖如下
<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>
- 用户表如下
- 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`)
);
- 实体类如下
这里使用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; //创建时间
}
- 配置数据源信息
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
,校验token
,token
正确将用户信息添加至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/**"); // 指定拦截的路径
}
}