程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Python 基础

    • Python基础
  • Python 进阶

    • 装饰器与生成器
    • 异常处理
    • 标准库精讲
    • 模块与包
    • pip包管理工具
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
前端 (opens new window)
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Python 基础

    • Python基础
  • Python 进阶

    • 装饰器与生成器
    • 异常处理
    • 标准库精讲
    • 模块与包
    • pip包管理工具
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
前端 (opens new window)
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • 后端开发

    • Spring Boot多模块项目开发
    • Spring Boot图片资源返回
    • Spring Boot文件上传
    • Spring Boot文件下载
    • 对接第三方文件上传
    • Servlet 原生API
    • HttpServletResponse 和 ResponseEntity
    • 后端解决跨域问题
    • 后端拦截器
    • SpringBoot+Vue实现邮件发送与验证码验证
    • 谷歌验证码
    • 利用hutool工具类实现图片验证码
    • 统一返回格式
    • 通用 普通 登录模块
    • 通用 JWT 登录认证模块
      • 1. 数据库表设计
      • 2. 引入Maven依赖
      • 3. JWT 工具类:JwtUtil
      • 4. Token 存储与管理:Redis 方案
      • 5. 数据传输对象:LoginRequest 和 LoginResponse
      • 6. Mapper 层:UserMapper
      • 7. Service 层:LoginService 和 LoginServiceImpl
      • 8. Controller 层:UserController
      • 9. 通过拦截器实现 JWT 校验
        • 1. 定义 JWT 拦截器
        • 2. 配置拦截器及路径排除规则
        • 3. 处理 Bearer 前缀的解析
      • 10. 前后端 JWT 认证流程详细说明
      • 11. 返回格式示例
      • 13. 扩展功能
        • 1. 加入角色和权限管理
        • 数据库表设计(SQL):
        • 2. 在 JWT 中携带用户角色和权限信息
        • 3. Service 层:获取用户的角色和权限信息
        • 4. 基于权限的请求过滤
        • 5. 用户上下文管理
        • 用户上下文工具类
        • 设置用户上下文
    • 通用 普通 注册模块
    • 基于 MyBatis curd
    • 基于 MyBatis-Plus curd
    • Java 常见对象模型
    • 开发枚举的使用
    • MyBatis与MyBatis-Plus日期类型处理
    • 接口日志拦截基础版
    • 接口日志拦截进阶版
    • 文件操作工具类
    • Spring Boot 数据校验
    • 幂等性
  • 前端开发

  • 开发笔记
  • 后端开发
scholar
2024-12-27
目录

通用 JWT 登录认证模块

# 通用 JWT 登录认证模块

本次实现采用企业风格代码架构,涵盖数据库表设计、Controller 层、Service 层、Mapper 层和 JWT 登录认证逻辑,具有良好的扩展性和复用性。本方案将详细介绍各层的代码实现,确保项目在实际生产环境中能够直接使用。

本方案包含以下部分:

  1. 数据库表设计
  2. JWT 工具类:JwtUtil
  3. Token 存储与管理:Redis 方案
  4. Mapper 层:UserMapper
  5. Service 层:LoginService 和 LoginServiceImpl
  6. Controller 层:UserController
  7. JWT 拦截器与过滤器:JwtAuthenticationFilter
  8. 前后端 JWT 认证流程详细说明

# 1. 数据库表设计

说明:用户表的设计具有良好的扩展性,能够方便地增加新的字段。

数据库表设计(SQL):

CREATE TABLE `user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(50) NOT NULL COMMENT '用户名',
  `password` VARCHAR(100) NOT NULL COMMENT '密码,已加密',
  `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
  `phone` VARCHAR(20) DEFAULT NULL COMMENT '电话',
  `status` TINYINT(1) DEFAULT 1 COMMENT '账户状态:1-启用,0-禁用',
  `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
1
2
3
4
5
6
7
8
9
10
11
12
13

重要字段说明:

  • username:用户登录名,唯一。
  • password:加密后的密码。
  • status:用户账户状态,1 表示启用,0 表示禁用。

# 2. 引入Maven依赖

<dependencies>
    <!-- Spring Boot Web Starter: 提供 MVC 和 RESTful API 的基础支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Data Redis: 集成 Redis,用于缓存和存储,如 JWT Token 的管理 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- MyBatis Starter: 提供 MyBatis 与 Spring Boot 的集成,支持数据持久化操作 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>

    <!-- JWT (io.jsonwebtoken): 生成和解析 JWT Token,实现安全身份认证 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

    <!-- Hutool 工具包: 提供各种实用工具,如加密、日期处理等 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.10</version>
    </dependency>

    <!-- Spring Boot DevTools (可选): 用于本地开发时热部署,提升开发效率 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>

    <!-- Lombok (可选): 简化 Java 开发中的样板代码,如 Getters 和 Setters -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 3. JWT 工具类:JwtUtil

首先在YML配置文件中使用了 JWT 的密钥和过期时间配置,然后在工具类中去读取

jwt:
  secret: mySecretKey  # JWT 签名密钥,用于加密和解密 Token,建议将此密钥设置为复杂且不易被猜测的字符串
  expiration: 3600000  # JWT 过期时间,单位为毫秒,此处表示 1 小时 (60 * 60 * 1000 毫秒)
1
2
3
  • secret: 是用于签名和验证 JWT 的密钥,建议将其配置为复杂且不易被猜测的字符串,并妥善保管,避免泄露。
  • expiration: 是 JWT 的过期时间,以毫秒为单位。在此示例中,3600000 表示 1 小时(60 分钟)。

说明:JWT 工具类负责生成、解析和验证 Token。示例中采用 io.jsonwebtoken 库进行 JWT 的操作。

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @description JWT 工具类,负责生成、解析和校验 Token
 */
@Component
public class JwtUtil {

    // 签名密钥和过期时间从配置文件中获取
    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration}")
    private long expirationTime;

    /**
     * 生成 JWT Token
     *
     * @param username 用户名
     * @return 生成的 Token
     */
    public String generateToken(String username) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + expirationTime);

        return Jwts.builder()
                .setSubject(username) // 设置主题,即用户名
                .setIssuedAt(now) // 签发时间
                .setExpiration(expiration) // 过期时间
                .signWith(SignatureAlgorithm.HS512, secretKey) // 设置签名算法和密钥
                .compact();
    }

    /**
     * 解析 JWT Token
     *
     * @param token JWT Token
     * @return 包含用户信息的 Claims 对象
     */
    public Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw e; // 让调用者处理过期异常
        } catch (Exception e) {
            throw new RuntimeException("解析 JWT Token 失败", e);
        }
    }

    /**
     * 验证 Token 是否过期
     *
     * @param token JWT Token
     * @return true 如果 Token 已过期,false 如果 Token 有效
     */
    public boolean isTokenExpired(String token) {
        Claims claims = parseToken(token);
        return claims.getExpiration().before(new Date());
    }

    /**
     * 从 Token 中获取用户名
     *
     * @param token JWT Token
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

重要 API 和参数说明:

  • generateToken(String username):生成包含用户名的 JWT Token,设置了过期时间和签名。
  • parseToken(String token):解析 Token,返回包含用户信息的 Claims 对象。
  • isTokenExpired(String token):检查 Token 是否过期。
  • getUsernameFromToken(String token):从 Token 中提取用户名。

# 4. Token 存储与管理:Redis 方案

说明:在企业级项目中,JWT Token 通常需要进行存储和管理,以便后续的验证和处理。我们选择将 Token 存储在 Redis 中,确保在用户登出时能够立即失效。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @description Redis 工具类,负责操作和管理 Token 的存储
 */
@Component
public class RedisUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 保存 Token 到 Redis,并设置过期时间
     *
     * @param username 用户名
     * @param token    JWT Token
     */
    public void saveToken(String username, String token) {
        redisTemplate.opsForValue().set("token:" + username, token, 1, TimeUnit.HOURS);
    }

    /**
     * 从 Redis 中获取 Token
     *
     * @param username 用户名
     * @return 存储的 JWT Token
     */
    public String getToken(String username) {
        return redisTemplate.opsForValue().get("token:" + username);
    }

    /**
     * 删除 Redis 中的 Token
     *
     * @param username 用户名
     */
    public void deleteToken(String username) {
        redisTemplate.delete("token:" + username);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

重要 API 和参数说明:

  • saveToken(String username, String token):将生成的 JWT Token 保存到 Redis,并设置过期时间。
  • getToken(String username):从 Redis 中获取已保存的 Token。
  • deleteToken(String username):用户登出时删除对应的 Token。

# 5. 数据传输对象:LoginRequest 和 LoginResponse

说明:数据传输对象用于封装前端请求的数据和后端响应的数据。

登录请求 DTO:

/**
 * @description 登录请求数据对象,用于封装前端传递的用户名和密码
 */
@Data
public class LoginRequest {
	private String username;
	private String password;
	private String email;
	private String vercode; // 验证码
}
1
2
3
4
5
6
7
8
9
10

登录响应 DTO:

/**
 * @description 登录响应数据对象,用于封装登录成功后的用户信息
 */
@Data
public class LoginResponse {
	private String username;
	private String avatar;
	private String token;
}
1
2
3
4
5
6
7
8
9

重要 API 和参数说明:

  • LoginRequest:用于接收前端的用户名和密码。
  • LoginResponse:用于返回登录成功后的用户信息。

# 6. Mapper 层:UserMapper

说明:使用 MyBatis 进行数据持久化操作,UserMapper 负责数据库的 CRUD 操作。

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

/**
 * @description 用户持久化接口,定义数据库操作方法
 */
@Mapper
public interface UserMapper {

    /**
     * 根据用户名查询用户信息
     *
     * @param username 用户名
     * @return 用户信息
     */
    @Select("SELECT * FROM user WHERE username = #{username}")
    User findByUsername(String username);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

重要 API 和参数说明:

  • findByUsername(String username):根据用户名查询用户信息。

# 7. Service 层:LoginService 和 LoginServiceImpl

说明:登录服务层负责处理用户的认证逻辑,包括验证用户名和密码、生成 JWT Token、保存 Token 到 Redis。

登录服务接口:

/**
 * @description 登录服务接口,定义登录相关的方法
 */
public interface LoginService {

    /**
     * 用户登录验证
     *
     * @param loginRequest 登录请求对象
     * @return 登录响应对象
     */
    LoginResponse login(LoginRequest loginRequest);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

登录服务实现类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @description 登录服务实现类,处理用户登录逻辑并生成 JWT Token
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisUtil redisUtil;
    
    @Autowired
    private JwtUtil jwtUtil; // 使用依赖注入注入 JwtUtil 实例    

    @Override
    public LoginResponse login(LoginRequest loginRequest) {
        User user = userMapper.findByUsername(loginRequest.getUsername());

        if (user == null || user.getStatus() == 0) {
            throw new CustomException(ResultCode.FAILED, "用户不存在或已被禁用");
        }

        String encryptedPassword = DigestUtil.md5Hex(loginRequest.getPassword());
        if (!user.getPassword().equals(encryptedPassword)) {
            throw new CustomException(ResultCode.VALIDATE_FAILED, "用户名或密码错误");
        }

        // 生成 JWT Token
        String token = jwtUtil.generateToken(user.getUsername());

        // 将 Token 保存到 Redis 中
        redisUtil.saveToken(user.getUsername(), token);

        // 构建登录响应对象
        LoginResponse response = new LoginResponse();
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        response.setPhone(user.getPhone());
        response.setToken(token); // 返回 Token 给前端

        return response;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 8. Controller 层:UserController

说明:

控制器层负责接收登录请求,并返回处理结果。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @description 用户控制器,处理用户登录请求
 */
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    private LoginService loginService;

    /**
     * 登录接口
     *
     * @param loginRequest 登录请求对象
     * @return 登录响应对象
     */
    @PostMapping("/login")
    public CommonResult<LoginResponse> login(@RequestBody LoginRequest loginRequest) {
        LoginResponse response = loginService.login(loginRequest);
        return CommonResult.success(response);
    }

    /**
     * 获取用户信息接口(需要 JWT 验证)
     *
     * @return 用户信息
     */
    @GetMapping("/info")
    public CommonResult<String> getUserInfo() {
        // 假设用户信息已在过滤器中注入到上下文中
        String username = "exampleUser"; // 替换为实际上下文获取
        return CommonResult.success("当前用户:" + username);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

重要 API 和参数说明:

  • @PostMapping("/login"):处理登录请求,返回 CommonResult<LoginResponse>。

# 9. 通过拦截器实现 JWT 校验

说明:在每次请求到达控制器之前,拦截器 会校验 Token 的合法性、有效性以及是否存在于 Redis 中。

# 1. 定义 JWT 拦截器

import com.scholar.springbootscaffolding.utils.JwtUtil;
import com.scholar.springbootscaffolding.utils.RedisUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.MalformedJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

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

/**
 * @description JWT 拦截器,拦截并验证请求中的 JWT Token
 */
@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private JwtUtil jwtUtil; // 使用依赖注入注入 JwtUtil 实例

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头中的 Token
        String token = request.getHeader("Authorization");

        if (token == null || !token.startsWith("Bearer ")) {
            handleUnauthorizedResponse(response, "Token 缺失或无效");
            return false;
        }

        token = token.substring(7); // 去掉 "Bearer " 前缀

        try {
            String username = jwtUtil.getUsernameFromToken(token);
            String redisToken = redisUtil.getToken(username);

            // 校验 Token 是否存在且未过期
            if (redisToken != null && redisToken.equals(token) && !jwtUtil.isTokenExpired(token)) {
                // 处理通过验证后的用户信息 (例如将用户信息存入 SecurityContext 或其他上下文中)
                // TODO: 将用户信息存入上下文
                return true;
            } else {
                handleUnauthorizedResponse(response, "Token 无效或已过期");
                return false;
            }
        } catch (ExpiredJwtException e) {
            handleUnauthorizedResponse(response, "Token 已过期,请重新登录");
            return false;
        } catch (SignatureException | MalformedJwtException e) {
            handleUnauthorizedResponse(response, "Token 签名无效或格式错误");
            return false;
        } catch (Exception e) {
            handleUnauthorizedResponse(response, "Token 校验失败");
            return false;
        }
    }

    // 处理未经授权的响应,返回给前端401状态码
    private void handleUnauthorizedResponse(HttpServletResponse response, String message) throws Exception {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 设置 401 状态码
        response.setContentType("application/json; charset=UTF-8");
        response.getWriter().write("{\"error\": \"" + message + "\"}");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
  • ** ExpiredJwtException**:当 JwtUtil.getUsernameFromToken(token) 或 JwtUtil.isTokenExpired(token) 方法在解析 JWT 时,检测到 Token 已经过期,io.jsonwebtoken.ExpiredJwtException 异常会被抛出。
  • ** SignatureException**:当 JwtUtil.getUsernameFromToken(token) 或 JwtUtil.isTokenExpired(token) 方法在解析 JWT 时,检测到 Token 签名无效或被篡改,io.jsonwebtoken.SignatureException 异常会被抛出。
  • ** MalformedJwtException**:当 JwtUtil.getUsernameFromToken(token) 或 JwtUtil.isTokenExpired(token) 方法在解析 JWT 时,检测到 Token 格式不正确,io.jsonwebtoken.MalformedJwtException 异常会被抛出。
  • ** Exception**:当 JwtUtil.getUsernameFromToken(token) 或 JwtUtil.isTokenExpired(token) 方法在解析 JWT 时,发生其他未预期的错误,java.lang.Exception 异常会被抛出。

image-20240826231422172

# 2. 配置拦截器及路径排除规则

通过配置类,将拦截器注册到 Spring MVC 中,同时可以灵活配置需要拦截的路径和排除的路径。

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 WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**") // 拦截 /api/ 下的所有请求
                .excludePathPatterns("/api/user/login", "/api/user/register", "/api/public/**"); // 排除不需要拦截的路径
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3. 处理 Bearer 前缀的解析

说明:在前端发送请求时,通常会在 Authorization 请求头中添加 Bearer 前缀。后端需要在解析 Token 前去掉这个前缀,以获得实际的 JWT。

解析 Bearer 前缀的步骤:

  1. 前端发送请求时添加 Bearer 前缀:

    axios.get('/api/protected', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    
    1
    2
    3
    4
    5
  2. 后端解析时去掉前缀: 在上面的 JwtAuthenticationFilter 中,去掉 Bearer 前缀后,再进行 Token 的解析和验证:

    token = token.substring(7); // 去掉 "Bearer " 前缀
    
    1

这样做确保了前后端遵循 RESTful API 标准,同时保证了 Token 的正确解析。

# 10. 前后端 JWT 认证流程详细说明

前端流程:

  1. 用户在登录页面输入用户名和密码。
  2. 前端将用户名和密码发送到后端登录接口。
  3. 登录成功后,后端返回一个 JWT Token,前端将 Token 存储在 localStorage 或 sessionStorage 中。
  4. 在后续的请求中,前端将 Token 添加到请求头中,每次请求都需要携带该 Token。

后端流程:

  1. 用户登录时验证用户名和密码,如果验证通过,生成 JWT Token 并存储在 Redis 中。
  2. 在每次请求到达控制器之前,JWT 过滤器会拦截并验证 Token 的有效性。
  3. 如果 Token 无效或过期,返回 401 Unauthorized 状态,提示用户重新登录。
  4. 如果 Token 有效,允许请求继续执行并在业务逻辑中获取用户信息。

# 11. 返回格式示例

在之前的实现基础上,确保所有模块能够协同工作,最终实现完整的 JWT 登录认证流程。以下是登录成功后的响应格式:

登录成功返回示例(包含 Token):

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "username": "admin",
    "email": "admin@example.com",
    "phone": "1234567890",
    "token": "eyJhbGciOiJIUzUxMiJ9..."
  }
}
1
2
3
4
5
6
7
8
9
10

访问受保护资源时的错误示例(Token 无效或过期):

{
  "code": 401,
  "message": "Token 无效或已过期",
  "data": null
}
1
2
3
4
5

总结

通过引入 Redis 管理 Token,有效地解决了 Token 的验证和管理问题。该方案具备以下优势:

  • 安全性:通过 Redis 存储和验证 Token,确保用户在登出时 Token 能立即失效。
  • 可扩展性:支持灵活扩展,如加入角色权限、用户上下文管理等。
  • 完整的认证流程:前后端协作,确保认证过程的严谨性和稳定性。

# 13. 扩展功能

如果需要在现有的 JWT 登录认证模块中加入权限和用户上下文管理,可以通过以下步骤实现:

  1. 用户角色与权限的定义:在数据库中新增角色和权限字段,并建立用户与角色、角色与权限的关联。
  2. JWT 中携带用户角色和权限信息:在生成 JWT 时,将用户的角色和权限信息也加入到 Token 中。
  3. 基于权限的请求过滤:在 JWT 过滤器中解析 Token,获取用户的权限,并根据不同的请求路径验证是否具有访问权限。
  4. 用户上下文管理:将解析后的用户信息存入上下文中,供后续业务逻辑使用。

# 1. 加入角色和权限管理

# 数据库表设计(SQL):

-- 用户表
CREATE TABLE `user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(50) NOT NULL COMMENT '用户名',
  `password` VARCHAR(100) NOT NULL COMMENT '密码,已加密',
  `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
  `phone` VARCHAR(20) DEFAULT NULL COMMENT '电话',
  `status` TINYINT(1) DEFAULT 1 COMMENT '账户状态:1-启用,0-禁用',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
CREATE TABLE `role` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
  `description` VARCHAR(100) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 权限表
CREATE TABLE `permission` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `permission_name` VARCHAR(50) NOT NULL COMMENT '权限名称',
  `url` VARCHAR(100) NOT NULL COMMENT '权限对应的URL',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

-- 用户与角色关联表
CREATE TABLE `user_role` (
  `user_id` BIGINT(20) NOT NULL,
  `role_id` BIGINT(20) NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`),
  FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
  FOREIGN KEY (`role_id`) REFERENCES `role`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户与角色关联表';

-- 角色与权限关联表
CREATE TABLE `role_permission` (
  `role_id` BIGINT(20) NOT NULL,
  `permission_id` BIGINT(20) NOT NULL,
  PRIMARY KEY (`role_id`, `permission_id`),
  FOREIGN KEY (`role_id`) REFERENCES `role`(`id`) ON DELETE CASCADE,
  FOREIGN KEY (`permission_id`) REFERENCES `permission`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色与权限关联表';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 2. 在 JWT 中携带用户角色和权限信息

说明:在生成 JWT 时,可以将用户的角色和权限信息一起加入到 Token 的 Claims 中。

更新 JWT 工具类:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @description JWT 工具类,负责生成、解析和校验 Token
 */
public class JwtUtil {

    private static final String SECRET_KEY = "mySecretKey";
    private static final long EXPIRATION_TIME = 60 * 60 * 1000;

    /**
     * 生成 JWT Token,并包含角色和权限信息
     *
     * @param username 用户名
     * @param roles 用户角色
     * @param permissions 用户权限
     * @return 生成的 Token
     */
    public static String generateToken(String username, List<String> roles, List<String> permissions) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + EXPIRATION_TIME);

        return Jwts.builder()
                .setSubject(username)
                .claim("roles", roles) // 添加角色信息
                .claim("permissions", permissions) // 添加权限信息
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

    /**
     * 从 Token 中获取角色信息
     *
     * @param token JWT Token
     * @return 用户角色列表
     */
    public static List<String> getRolesFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("roles", List.class);
    }

    /**
     * 从 Token 中获取权限信息
     *
     * @param token JWT Token
     * @return 用户权限列表
     */
    public static List<String> getPermissionsFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("permissions", List.class);
    }
    
    // 其他方法保持不变...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 3. Service 层:获取用户的角色和权限信息

说明:在登录服务中,登录成功后从数据库获取用户的角色和权限信息,并将其添加到 JWT 中。

登录服务实现类更新:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @description 登录服务实现类,处理用户登录逻辑并生成 JWT Token
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public LoginResponse login(LoginRequest loginRequest) {
        User user = userMapper.findByUsername(loginRequest.getUsername());

        if (user == null || user.getStatus() == 0) {
            throw new CustomException(ResultCode.FAILED, "用户不存在或已被禁用");
        }

        String encryptedPassword = DigestUtil.md5Hex(loginRequest.getPassword());
        if (!user.getPassword().equals(encryptedPassword)) {
            throw new CustomException(ResultCode.VALIDATE_FAILED, "用户名或密码错误");
        }

        // 获取用户的角色和权限信息
        List<String> roles = userMapper.getUserRoles(user.getId());
        List<String> permissions = userMapper.getUserPermissions(user.getId());

        // 生成 JWT Token,并将角色和权限信息一并添加
        String token = JwtUtil.generateToken(user.getUsername(), roles, permissions);

        // 将 Token 保存到 Redis 中
        redisUtil.saveToken(user.getUsername(), token);

        // 构建登录响应对象
        LoginResponse response = new LoginResponse();
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        response.setPhone(user.getPhone());
        response.setToken(token);

        return response;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

重要 API 和参数说明:

  • getUserRoles(Long userId):获取用户的角色信息。
  • getUserPermissions(Long userId):获取用户的权限信息。

# 4. 基于权限的请求过滤

说明:在 JWT 过滤器中,通过解析 Token 获取用户的权限,并根据请求路径判断用户是否具有访问权限。

更新 JWT 过滤器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * @description JWT 过滤器,基于用户权限进行请求过滤
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);

            String username = JwtUtil.getUsernameFromToken(token);
            String redisToken = redisUtil.getToken(username);

            if (redisToken != null && redisToken.equals(token) && !JwtUtil.isTokenExpired(token)) {
                // 获取用户的权限信息
                List<String> permissions = JwtUtil.getPermissionsFromToken(token);

                // 获取当前请求路径
                String requestURI = request.getRequestURI();

                // 验证用户是否有访问该路径的权限
                if (permissions.contains(requestURI)) {
                    // 用户有权限,放行请求
                    filterChain.doFilter(request, response);
                    return;
                } else {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.getWriter().write("没有访问权限");
                    return;
                }
            } else {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("Token 无效或已过期");
                return;
            }
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Token 缺失或无效");
            return;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 5. 用户上下文管理

说明:用户上下文管理通常用于在后续的业务逻辑中获取当前登录用户的信息。我们可以将用户信息存储在 ThreadLocal 中,确保多线程环境下的安全性。

# 用户上下文工具类

public class UserContext {

    private static final ThreadLocal<UserInfo> userContext = new ThreadLocal<>();

    /**
     * 设置当前用户信息
     *
     * @param userInfo 用户信息
     */
    public static void setUser(UserInfo userInfo) {
        userContext.set(userInfo);
    }

    /**
     * 获取当前用户信息
     *
     * @return 用户信息
     */
    public static UserInfo getUser() {
        return userContext.get();
    }

    /**
     * 清除当前用户信息
     */
    public static void clear() {
        userContext.remove();
    }
}

class UserInfo {
    private String username;
    private List<String> roles;
    private List<String> permissions;

    // Getters 和 Setters 省略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 设置用户上下文

在 JWT 过滤器中,解析 Token 后,我们将用户信息设置到用户上下文中。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * @description JWT 过滤器,拦截并验证请求中的 JWT Token
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取请求头中的 Token
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7); // 去掉 "Bearer " 前缀

            String username = JwtUtil.getUsernameFromToken(token);
            String redisToken = redisUtil.getToken(username);

            // 校验 Token 是否存在且未过期
            if (redisToken != null && redisToken.equals(token) && !JwtUtil.isTokenExpired(token)) {
                // 获取角色和权限信息
                List<String> roles = JwtUtil.getRolesFromToken(token);
                List<String> permissions = JwtUtil.getPermissionsFromToken(token);

                // 设置用户上下文
                UserInfo userInfo = new UserInfo();
                userInfo.setUsername(username);
                userInfo.setRoles(roles);
                userInfo.setPermissions(permissions);
                UserContext.setUser(userInfo);

                // 放行请求
                try {
                    filterChain.doFilter(request, response);
                } finally {
                    // 清除用户上下文
                    UserContext.clear();
                }
            } else {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("Token 无效或已过期");
                return;
            }
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Token 缺失或无效");
            return;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

用户上下文管理的完整流程

  1. 在用户登录时,生成 JWT Token 时包含用户的角色和权限信息。
  2. 每次请求时,JWT 过滤器会解析 Token 并将用户信息(如用户名、角色、权限)设置到用户上下文中。
  3. 在业务逻辑中,随时可以通过 UserContext.getUser() 获取当前请求的用户信息。
  4. 请求处理完毕后,过滤器会清除用户上下文,确保线程安全。

通过这样的设计,可以在多线程环境中安全、方便地获取和使用用户信息,避免信息混乱的问题。

编辑此页 (opens new window)
上次更新: 2025/03/16, 22:19:39
通用 普通 登录模块
通用 普通 注册模块

← 通用 普通 登录模块 通用 普通 注册模块→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式