找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4453

积分

0

好友

623

主题
发表于 5 小时前 | 查看: 5| 回复: 0

前言

"JWT 权限管理不是玄学,是架构!"

在微服务架构盛行的今天,无状态认证已成为企业级应用的标准配置。而 JWT(JSON Web Token)凭借其自包含、可验证、跨语言的特性,成为了认证领域的事实标准。

然而,很多开发者对 JWT 的理解仍停留在"复制粘贴代码"的层面:

  • ❌ Token 被篡改了怎么办?  
  • ❌ Token 如何优雅续期?  
  • ❌ 用户注销后 Token 为何还能用?  
  • ❌ 多设备登录如何管理?  
  • ❌ 权限变更如何实时生效?

本文将基于 Spring Boot 3.2 + Spring Security 6.2 的最新特性,带你从零构建一个生产级 JWT 认证系统,涵盖从原理剖析、代码实现到安全加固的完整链路。

一、为什么选择 JWT?

1.1 传统 Session vs JWT

┌─────────────────────────────────────────────────────────────┐
│                    传统 Session 认证                          │
├─────────────────────────────────────────────────────────────┤
│  客户端          服务端          存储层                        │
│    │              │              │                           │
│    │──登录请求──> │              │                           │
│    │              │──创建 Session─>│                       │
│    │<─Set-Cookie──│              │                           │
│    │              │              │                           │
│    │──请求 +Cookie─>│              │ ← 性能瓶颈!              │
│    │              │──查询 Session─>│                           │
│    │<─响应────────│              │                           │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                         JWT 认证                              │
├─────────────────────────────────────────────────────────────┤
│  客户端          服务端          存储层                        │
│    │              │              │                           │
│    │──登录请求──> │              │                           │
│    │              │──生成 JWT────>│ (可选存储)               │
│    │<─Token───────│              │                           │
│    │              │              │                           │
│    │──请求 +Token─> │              │                           │
│    │              │──验证签名───>│ ← 无状态!无需查询!         │
│    │<─响应────────│              │                           │
└─────────────────────────────────────────────────────────────┘

1.2 JWT 的核心优势

特性 说明 适用场景
无状态 服务端无需存储 Session 微服务、分布式系统
跨域支持 天然支持 CORS 前后端分离、多端应用
自包含 Claims 携带用户信息 减少数据库查询
可验证 签名防篡改 高安全要求场景
标准化 RFC 7519 标准 跨语言、跨平台

1.3 JWT 结构解析

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

┌─────────────┐ ┌─────────────────────────────────────┐ ┌──────────────────┐
│   Header    │ │              Payload                │     Signature      │
│  (头部)       │ │            (载荷)                   │     (签名)          │
├─────────────┤ ├─────────────────────────────────────┤ ├──────────────────┤
│ alg: HS256  │ │ sub: "1234567890"                   │ HMACSHA256(        │
│ typ: JWT    │ │ name: "John Doe"                   │   base64UrlEncode( │
│             │ │ iat: 1516239022  ← 过期时间         │     header) + "." │
│             │ │ exp: 1516242622  ← 过期时间         │   + base64UrlEncode│
│             │ │ roles: ["USER","ADMIN"] ← 权限      │     (payload),    │
│             │ │ deviceId: "xxx"   ← 设备标识       │   secretKey)      │
└─────────────┘ └─────────────────────────────────────┘ └──────────────────┘

二、项目架构设计

2.1 技术栈选型

<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot 3.2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.2.0</version>
    </dependency>

    <!-- Spring Security 6.2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>3.2.0</version>
    </dependency>

    <!-- JWT 支持 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.3</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>

    <!-- Redis 缓存(Token 黑名单) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- MyBatis Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.5</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

2.2 系统架构图

┌─────────────────────────────────────────────────────────────────────────┐
│                             客户端层                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │
│  │   Web    │  │  Mobile  │  │   App    │  │ 第三方    │                  │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘                  │
└───────┼─────────────┼─────────────┼─────────────┼──────────────────────┘
        │             │             │             │
        └─────────────┴──────┬──────┴─────────────┘
                             │ JWT Token
┌────────────────────────────┼────────────────────────────────────────────┐
│                       API 网关层                                          │
│  ┌───────────────────────────────┴───────────────────────────────────┐ │
│  │                    AuthenticationFilter                            │ │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                │ │
│  │  │ Token 解析   │  │ 签名验证    │  │ 权限校验    │                │ │
│  │  └─────────────┘  └─────────────┘  └─────────────┘                │ │
│  └───────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────────────┘
                                 │
┌────────────────────────────────┼────────────────────────────────────────┐
│                       业务服务层                                          │
│  ┌─────────────┐  ┌─────────────┴─────────────┐  ┌─────────────┐       │
│  │  用户服务   │  │       认证服务            │  │  权限服务   │       │
│  │  UserService│  │   AuthService             │  │ RoleService │       │
│  └─────────────┘  └───────────────────────────┘  └─────────────┘       │
└─────────────────────────────────────────────────────────────────────────┘
                                 │
┌────────────────────────────────┼────────────────────────────────────────┐
│                       数据访问层                                          │
│  ┌─────────────┐  ┌────────────┴──────────────┐  ┌─────────────┐       │
│  │  MySQL      │  │         Redis             │  │   MongoDB   │       │
│  │  用户数据   │  │  Token 黑名单/刷新 Token  │  │   日志      │       │
│  └─────────────┘  └───────────────────────────┘  └─────────────┘       │
└─────────────────────────────────────────────────────────────────────────┘

2.3 核心模块划分

auth-system/
├── src/main/java/com/example/auth/
│   ├── config/                    # 配置类
│   │   ├── SecurityConfig.java    # Security 配置
│   │   ├── JwtConfig.java         # JWT 配置
│   │   └── RedisConfig.java       # Redis 配置
│   ├── filter/                    # 过滤器
│   │   └── JwtAuthenticationFilter.java
│   ├── handler/                   # 处理器
│   │   ├── AuthSuccessHandler.java
│   │   └── AuthFailureHandler.java
│   ├── controller/                # 控制器
│   │   ├── AuthController.java
│   │   └── UserController.java
│   ├── service/                   # 服务层
│   │   ├── AuthService.java
│   │   ├── UserService.java
│   │   └── JwtService.java
│   ├── entity/                    # 实体类
│   │   ├── User.java
│   │   └── Role.java
│   ├── dto/                       # 数据传输对象
│   │   ├── LoginRequest.java
│   │   ├── RegisterRequest.java
│   │   └── AuthResponse.java
│   ├── security/                  # 安全相关
│   │   ├── UserDetailsImpl.java
│   │   └── JwtTokenProvider.java
│   └── exception/                 # 异常处理
│       └── GlobalExceptionHandler.java
└── src/main/resources/
    ├── application.yml
    └── schema.sql

三、核心代码实现

3.1 数据库设计

-- 用户表
CREATE TABLE `users` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `username` VARCHAR(50) NOT NULL UNIQUE,
    `password` VARCHAR(255) NOT NULL,
    `email` VARCHAR(100),
    `phone` VARCHAR(20),
    `status` TINYINT DEFAULT 1 COMMENT '1-正常 0-禁用',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX `idx_username` (`username`),
    INDEX `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 角色表
CREATE TABLE `roles` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(50) NOT NULL UNIQUE COMMENT '角色标识',
    `description` VARCHAR(255),
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 用户角色关联表
CREATE TABLE `user_roles` (
    `user_id` BIGINT NOT NULL,
    `role_id` BIGINT NOT NULL,
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`user_id`, `role_id`),
    FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
    FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 权限表
CREATE TABLE `permissions` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(100) NOT NULL UNIQUE COMMENT '权限标识',
    `description` VARCHAR(255),
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 角色权限关联表
CREATE TABLE `role_permissions` (
    `role_id` BIGINT NOT NULL,
    `permission_id` BIGINT NOT NULL,
    PRIMARY KEY (`role_id`, `permission_id`),
    FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`),
    FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始化数据
INSERT INTO `roles` (`name`, `description`) VALUES
('ROLE_USER', '普通用户'),
('ROLE_ADMIN', '管理员'),
('ROLE_SUPER_ADMIN', '超级管理员');

INSERT INTO `permissions` (`name`, `description`) VALUES
('user:read', '查看用户'),
('user:write', '编辑用户'),
('user:delete', '删除用户'),
('order:read', '查看订单'),
('order:write', '编辑订单');

INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES
(1, 1), (1, 4),  -- USER: user:read, order:read
(2, 1), (2, 2), (2, 4), (2, 5);  -- ADMIN: user:read, user:write, order:read, order:write

3.2 实体类定义

// entity/User.java
package com.example.auth.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;

import java.time.LocalDateTime;
import java.util.List;

@Data
@Accessors(chain = true)
@TableName("users")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String username;

    private String password;

    private String email;

    private String phone;

    private Integer status;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    @TableField(exist = false)
    private List<Role> roles;
}

// entity/Role.java
package com.example.auth.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

@Data
@TableName("roles")
public class Role {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;

    private String description;

    private LocalDateTime createdAt;

    @TableField(exist = false)
    private List<Permission> permissions;
}

// entity/Permission.java
package com.example.auth.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("permissions")
public class Permission {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;

    private String description;

    private LocalDateTime createdAt;
}

3.3 JWT 配置类

// config/JwtConfig.java
package com.example.auth.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
    /**
     * JWT 密钥(生产环境应从环境变量读取)
     */
    private String secret = "your-256-bit-secret-key-for-jwt-signing-and-verification";

    /**
     * Access Token 过期时间
     */
    private Duration accessTokenExpiration = Duration.ofMinutes(30);

    /**
     * Refresh Token 过期时间
     */
    private Duration refreshTokenExpiration = Duration.ofDays(7);

    /**
     * Token 前缀
     */
    private String tokenPrefix = "Bearer ";

    /**
     * Token Header 名称
     */
    private String tokenHeader = "Authorization";
}

3.4 JWT 工具类(核心)

// security/JwtTokenProvider.java
package com.example.auth.security;

import com.example.auth.config.JwtConfig;
import com.example.auth.entity.User;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    private final JwtConfig jwtConfig;

    /**
     * 生成 SecretKey
     */
    private SecretKey getSigningKey() {
        byte[] keyBytes = jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * 生成 Access Token
     */
    public String generateAccessToken(Authentication authentication) {
        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        return generateToken(userDetails, jwtConfig.getAccessTokenExpiration());
    }

    /**
     * 生成 Refresh Token
     */
    public String generateRefreshToken(Authentication authentication) {
        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        return generateToken(userDetails, jwtConfig.getRefreshTokenExpiration());
    }

    /**
     * 生成 Token(通用)
     */
    private String generateToken(UserDetailsImpl userDetails, Duration expiration) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration.toMillis());

        // 提取权限字符串
        String authorities = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        return Jwts.builder()
                .subject(String.valueOf(userDetails.getId()))
                .claim("username", userDetails.getUsername())
                .claim("roles", authorities)
                .claim("deviceId", userDetails.getDeviceId())  // 设备标识
                .issuedAt(now)
                .expiration(expiryDate)
                .signWith(getSigningKey(), Jwts.SIG.HS256)
                .compact();
    }

    /**
     * 从 Token 中提取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseClaims(token);
        return claims.get("username", String.class);
    }

    /**
     * 从 Token 中提取用户 ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = parseClaims(token);
        return Long.parseLong(claims.getSubject());
    }

    /**
     * 从 Token 中提取权限
     */
    public String getRolesFromToken(String token) {
        Claims claims = parseClaims(token);
        return claims.get("roles", String.class);
    }

    /**
     * 验证 Token 是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseClaims(token);
            return true;
        } catch (SecurityException ex) {
            log.error("Invalid JWT signature: {}", ex.getMessage());
        } catch (MalformedJwtException ex) {
            log.error("Invalid JWT token: {}", ex.getMessage());
        } catch (ExpiredJwtException ex) {
            log.error("JWT token is expired: {}", ex.getMessage());
        } catch (UnsupportedJwtException ex) {
            log.error("JWT token is unsupported: {}", ex.getMessage());
        } catch (IllegalArgumentException ex) {
            log.error("JWT claims string is empty: {}", ex.getMessage());
        }
        return false;
    }

    /**
     * 检查 Token 是否过期
     */
    public boolean isTokenExpired(String token) {
        try {
            Claims claims = parseClaims(token);
            return claims.getExpiration().before(new Date());
        } catch (ExpiredJwtException e) {
            return true;
        }
    }

    /**
     * 解析 Claims
     */
    private Claims parseClaims(String token) {
        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    /**
     * 刷新 Token(在 Token 即将过期时)
     */
    public String refreshToken(String token) {
        Claims claims = parseClaims(token);

        // 如果 Token 已过期,不允许刷新
        if (claims.getExpiration().before(new Date())) {
            throw new ExpiredJwtException(null, claims, "Token is expired");
        }

        Date now = new Date();
        Date newExpiryDate = new Date(now.getTime() + jwtConfig.getAccessTokenExpiration().toMillis());

        return Jwts.builder()
                .claims(claims)
                .issuedAt(now)
                .expiration(newExpiryDate)
                .signWith(getSigningKey(), Jwts.SIG.HS256)
                .compact();
    }
}

3.5 UserDetailsService 实现

// security/UserDetailsImpl.java
package com.example.auth.security;

import com.example.auth.entity.User;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.stream.Collectors;

@Data
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
    private final Long id;
    private final String username;
    private final String password;
    private final String deviceId;
    private final Collection<? extends GrantedAuthority> authorities;
    private final boolean enabled;

    public static UserDetailsImpl fromUser(User user, Collection<GrantedAuthority> authorities) {
        return new UserDetailsImpl(
                user.getId(),
                user.getUsername(),
                user.getPassword(),
                null,  // deviceId 可在登录时传入
                authorities,
                user.getStatus() == 1
        );
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
// service/CustomUserDetailsService.java
package com.example.auth.service;

import com.example.auth.entity.User;
import com.example.auth.security.UserDetailsImpl;
import com.example.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;
    private final RoleRepository roleRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));

        if (user.getStatus() != 1) {
            throw new UsernameNotFoundException("User account is disabled: " + username);
        }

        // 加载用户角色和权限
        List<SimpleGrantedAuthority> authorities = roleRepository
                .findByUserId(user.getId())
                .stream()
                .flatMap(role -> role.getPermissions().stream())
                .map(permission -> new SimpleGrantedAuthority(permission.getName()))
                .collect(Collectors.toList());

        // 添加角色
        roleRepository.findByUserId(user.getId())
                .forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getName())));

        return UserDetailsImpl.fromUser(user, authorities);
    }
}

3.6 Security 配置(Spring Security 6 新特性)

// config/SecurityConfig.java
package com.example.auth.config;

import com.example.auth.filter.JwtAuthenticationFilter;
import com.example.auth.handler.AuthFailureHandler;
import com.example.auth.handler.AuthSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
    private final CustomUserDetailsService userDetailsService;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final AuthSuccessHandler authSuccessHandler;
    private final AuthFailureHandler authFailureHandler;

    /**
     * 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    /**
     * 认证提供者
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    /**
     * 认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    /**
     * Security 过滤链配置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF(使用 Token 无需 CSRF)
            .csrf(AbstractHttpConfigurer::disable)

            // 配置 CORS
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))

            // 配置授权规则
            .authorizeHttpRequests(auth -> auth
                // 公开接口
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                // 静态资源
                .requestMatchers(HttpMethod.GET, "/static/**").permitAll()
                // 其他请求需要认证
                .anyRequest().authenticated()
            )

            // 无状态 Session
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            // 配置登录处理
            .formLogin(form -> form
                .successHandler(authSuccessHandler)
                .failureHandler(authFailureHandler)
            )

            // 配置异常处理
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(401);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write("{\"code\":401,\"message\":\"未授权\"}");
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setStatus(403);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
                })
            )

            // 添加 JWT 过滤器
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    /**
     * CORS 配置
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        return request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(List.of("http://localhost:3000", "https://yourdomain.com"));
            config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
            config.setAllowedHeaders(List.of("*"));
            config.setAllowCredentials(true);
            config.setMaxAge(3600L);
            return config;
        };
    }
}

3.7 JWT 认证过滤器

// filter/JwtAuthenticationFilter.java
package com.example.auth.filter;

import com.example.auth.security.JwtTokenProvider;
import com.example.auth.service.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private final CustomUserDetailsService userDetailsService;
    private final TokenBlacklistService tokenBlacklistService;

    // 不需要 Token 验证的路径
    private static final List<String> WHITELIST = Arrays.asList(
        "/api/auth/login",
        "/api/auth/register",
        "/api/auth/refresh",
        "/swagger-ui",
        "/v3/api-docs"
    );

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain)
            throws ServletException, IOException {

        String requestUri = request.getRequestURI();

        // 白名单路径直接放行
        if (WHITELIST.stream().anyMatch(requestUri::startsWith)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 从请求头获取 Token
        String token = extractTokenFromRequest(request);

        if (StringUtils.hasText(token)) {
            // 检查 Token 是否在黑名单中(已注销)
            if (tokenBlacklistService.isBlacklisted(token)) {
                log.warn("Token is blacklisted: {}", token.substring(0, 20));
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"code\":401,\"message\":\"Token 已失效,请重新登录\"}");
                return;
            }

            // 验证 Token
            if (jwtTokenProvider.validateToken(token)) {
                // 提取用户名
                String username = jwtTokenProvider.getUsernameFromToken(token);

                // 加载用户信息
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                // 创建认证对象
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities());

                authentication.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request));

                // 设置到 SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authentication);

                log.debug("Set authentication for user: {}", username);
            }
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 从请求头提取 Token
     */
    private String extractTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }

        return null;
    }
}

3.8 认证服务层

// service/AuthService.java
package com.example.auth.service;

import com.example.auth.config.JwtConfig;
import com.example.auth.dto.*;
import com.example.auth.entity.User;
import com.example.auth.repository.UserRepository;
import com.example.auth.security.JwtTokenProvider;
import com.example.auth.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider jwtTokenProvider;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenBlacklistService tokenBlacklistService;
    private final JwtConfig jwtConfig;

    /**
     * 用户登录
     */
    @Transactional
    public AuthResponse login(LoginRequest request) {
        log.info("User login attempt: username={}", request.getUsername());

        // 1. 认证
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getUsername(),
                        request.getPassword()));

        // 2. 设置到 SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 3. 生成 Token
        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        String accessToken = jwtTokenProvider.generateAccessToken(authentication);
        String refreshToken = jwtTokenProvider.generateRefreshToken(authentication);

        // 4. 保存 Refresh Token(用于后续刷新和注销)
        tokenBlacklistService.saveRefreshToken(userDetails.getId(), refreshToken);

        log.info("User login successful: username={}, userId={}", 
                request.getUsername(), userDetails.getId());

        return AuthResponse.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .tokenType(jwtConfig.getTokenPrefix())
                .expiresIn(jwtConfig.getAccessTokenExpiration().getSeconds())
                .username(userDetails.getUsername())
                .build();
    }

    /**
     * 用户注册
     */
    @Transactional
    public AuthResponse register(RegisterRequest request) {
        log.info("User registration attempt: username={}", request.getUsername());

        // 1. 检查用户名是否已存在
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new BusinessException("用户名已存在");
        }

        // 2. 检查邮箱是否已存在
        if (request.getEmail() != null && userRepository.existsByEmail(request.getEmail())) {
            throw new BusinessException("邮箱已被注册");
        }

        // 3. 创建用户
        User user = new User()
                .setUsername(request.getUsername())
                .setPassword(passwordEncoder.encode(request.getPassword()))
                .setEmail(request.getEmail())
                .setPhone(request.getPhone())
                .setStatus(1);

        userRepository.save(user);

        // 4. 分配默认角色(ROLE_USER)
        // roleRepository.assignRoleToUser(user.getId(), "ROLE_USER");

        log.info("User registration successful: userId={}", user.getId());

        // 5. 自动登录
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getUsername(),
                        request.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String accessToken = jwtTokenProvider.generateAccessToken(authentication);
        String refreshToken = jwtTokenProvider.generateRefreshToken(authentication);

        tokenBlacklistService.saveRefreshToken(user.getId(), refreshToken);

        return AuthResponse.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .tokenType(jwtConfig.getTokenPrefix())
                .expiresIn(jwtConfig.getAccessTokenExpiration().getSeconds())
                .username(user.getUsername())
                .build();
    }

    /**
     * 刷新 Token
     */
    @Transactional
    public AuthResponse refreshToken(RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();

        // 1. 验证 Refresh Token
        if (!jwtTokenProvider.validateToken(refreshToken)) {
            throw new BusinessException("Refresh Token 无效");
        }

        // 2. 检查是否在黑名单中
        if (tokenBlacklistService.isBlacklisted(refreshToken)) {
            throw new BusinessException("Refresh Token 已失效");
        }

        // 3. 提取用户信息
        Long userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
        String username = jwtTokenProvider.getUsernameFromToken(refreshToken);

        // 4. 重新认证
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(username, null));

        // 5. 生成新 Token
        String newAccessToken = jwtTokenProvider.generateAccessToken(authentication);
        String newRefreshToken = jwtTokenProvider.generateRefreshToken(authentication);

        // 6. 将旧 Refresh Token 加入黑名单
        tokenBlacklistService.blacklist(refreshToken);
        tokenBlacklistService.saveRefreshToken(userId, newRefreshToken);

        log.info("Token refreshed for user: {}", username);

        return AuthResponse.builder()
                .accessToken(newAccessToken)
                .refreshToken(newRefreshToken)
                .tokenType(jwtConfig.getTokenPrefix())
                .expiresIn(jwtConfig.getAccessTokenExpiration().getSeconds())
                .username(username)
                .build();
    }

    /**
     * 注销登录
     */
    @Transactional
    public void logout(String accessToken, String refreshToken) {
        // 1. 将 Access Token 加入黑名单
        if (accessToken != null) {
            tokenBlacklistService.blacklist(accessToken);
        }

        // 2. 将 Refresh Token 加入黑名单
        if (refreshToken != null) {
            tokenBlacklistService.blacklist(refreshToken);
        }

        // 3. 清除 SecurityContext
        SecurityContextHolder.clearContext();

        log.info("User logout successful");
    }
}

3.9 Token 黑名单服务(Redis)

// service/TokenBlacklistService.java
package com.example.auth.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
    private final RedisTemplate<String, String> redisTemplate;

    private static final String BLACKLIST_PREFIX = "token:blacklist:";
    private static final String REFRESH_TOKEN_PREFIX = "token:refresh:";

    /**
     * 将 Token 加入黑名单
     */
    public void blacklist(String token) {
        // 解析 Token 获取过期时间
        long expiration = getExpirationFromToken(token);

        redisTemplate.opsForValue().set(
                BLACKLIST_PREFIX + token,
                "blacklisted",
                expiration,
                TimeUnit.SECONDS
        );

        log.debug("Token added to blacklist: {}", token.substring(0, 20));
    }

    /**
     * 检查 Token 是否在黑名单中
     */
    public boolean isBlacklisted(String token) {
        Boolean exists = redisTemplate.hasKey(BLACKLIST_PREFIX + token);
        return Boolean.TRUE.equals(exists);
    }

    /**
     * 保存 Refresh Token
     */
    public void saveRefreshToken(Long userId, String refreshToken) {
        long expiration = 7 * 24 * 60 * 60;  // 7 天

        redisTemplate.opsForValue().set(
                REFRESH_TOKEN_PREFIX + userId,
                refreshToken,
                expiration,
                TimeUnit.SECONDS
        );
    }

    /**
     * 获取 Refresh Token
     */
    public String getRefreshToken(Long userId) {
        return redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + userId);
    }

    /**
     * 删除 Refresh Token
     */
    public void deleteRefreshToken(Long userId) {
        redisTemplate.delete(REFRESH_TOKEN_PREFIX + userId);
    }

    /**
     * 从 Token 中提取剩余有效期(秒)
     */
    private long getExpirationFromToken(String token) {
        // 简化实现,实际应从 Token 中解析
        return 1800;  // 30 分钟
    }
}

3.10 控制器层

// controller/AuthController.java
package com.example.auth.controller;

import com.example.auth.dto.*;
import com.example.auth.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;

    /**
     * 用户登录
     */
    @PostMapping("/login")
    public ResponseEntity<ApiResponse<AuthResponse>> login(
            @Valid @RequestBody LoginRequest request) {
        log.info("Login request: username={}", request.getUsername());

        AuthResponse response = authService.login(request);

        return ResponseEntity.ok(ApiResponse.success(response));
    }

    /**
     * 用户注册
     */
    @PostMapping("/register")
    public ResponseEntity<ApiResponse<AuthResponse>> register(
            @Valid @RequestBody RegisterRequest request) {
        log.info("Register request: username={}", request.getUsername());

        AuthResponse response = authService.register(request);

        return ResponseEntity.ok(ApiResponse.success(response));
    }

    /**
     * 刷新 Token
     */
    @PostMapping("/refresh")
    public ResponseEntity<ApiResponse<AuthResponse>> refreshToken(
            @Valid @RequestBody RefreshTokenRequest request) {
        AuthResponse response = authService.refreshToken(request);

        return ResponseEntity.ok(ApiResponse.success(response));
    }

    /**
     * 注销登录
     */
    @PostMapping("/logout")
    public ResponseEntity<ApiResponse<Void>> logout(
            @RequestBody LogoutRequest request,
            HttpServletRequest httpRequest) {

        String accessToken = extractTokenFromRequest(httpRequest);

        authService.logout(accessToken, request.getRefreshToken());

        return ResponseEntity.ok(ApiResponse.success());
    }

    /**
     * 获取当前用户信息
     */
    @GetMapping("/me")
    public ResponseEntity<ApiResponse<UserInfoResponse>> getCurrentUser(
            HttpServletRequest httpRequest) {

        Long userId = (Long) httpRequest.getAttribute("userId");

        UserInfoResponse userInfo = authService.getUserInfo(userId);

        return ResponseEntity.ok(ApiResponse.success(userInfo));
    }

    private String extractTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

3.11 全局异常处理

// exception/GlobalExceptionHandler.java
package com.example.auth.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BadCredentialsException.class)
    public ResponseEntity<ApiResponse<Void>> handleBadCredentials(BadCredentialsException ex) {
        log.warn("Bad credentials: {}", ex.getMessage());
        return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body(ApiResponse.error(401, "用户名或密码错误"));
    }

    @ExceptionHandler(DisabledException.class)
    public ResponseEntity<ApiResponse<Void>> handleDisabledAccount(DisabledException ex) {
        log.warn("Disabled account: {}", ex.getMessage());
        return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body(ApiResponse.error(401, "账户已被禁用"));
    }

    @ExceptionHandler(LockedException.class)
    public ResponseEntity<ApiResponse<Void>> handleLockedAccount(LockedException ex) {
        log.warn("Locked account: {}", ex.getMessage());
        return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body(ApiResponse.error(401, "账户已被锁定"));
    }

    @ExceptionHandler(UsernameNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleUserNotFound(UsernameNotFoundException ex) {
        log.warn("User not found: {}", ex.getMessage());
        return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body(ApiResponse.error(401, "用户不存在"));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error(400, "参数验证失败", errors));
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
        log.error("Business exception: {}", ex.getMessage());
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error(400, ex.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
        log.error("Unexpected exception: ", ex);
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error(500, "服务器内部错误"));
    }
}

四、高级特性实现

4.1 基于注解的权限控制

// controller/UserController.java
package com.example.auth.controller;

import com.example.auth.dto.ApiResponse;
import com.example.auth.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    /**
     * 查看用户列表(需要 user:read 权限)
     */
    @GetMapping
    @PreAuthorize("hasAuthority('user:read')")
    public ResponseEntity<ApiResponse<?>> listUsers(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return ResponseEntity.ok(ApiResponse.success(userService.listUsers(page, size)));
    }

    /**
     * 查看用户详情(需要 user:read 权限)
     */
    @GetMapping("/{id}")
    @PreAuthorize("hasAuthority('user:read')")
    public ResponseEntity<ApiResponse<?>> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(ApiResponse.success(userService.getUser(id)));
    }

    /**
     * 创建用户(需要 user:write 权限)
     */
    @PostMapping
    @PreAuthorize("hasAuthority('user:write')")
    public ResponseEntity<ApiResponse<?>> createUser(@Valid @RequestBody CreateUserRequest request) {
        return ResponseEntity.ok(ApiResponse.success(userService.createUser(request)));
    }

    /**
     * 删除用户(需要 user:delete 权限)
     */
    @DeleteMapping("/{id}")
    @PreAuthorize("hasAuthority('user:delete')")
    public ResponseEntity<ApiResponse<?>> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.ok(ApiResponse.success());
    }

    /**
     * 仅管理员可访问
     */
    @GetMapping("/admin/stats")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<ApiResponse<?>> getAdminStats() {
        return ResponseEntity.ok(ApiResponse.success(userService.getAdminStats()));
    }
}

4.2 动态权限刷新

// service/PermissionRefreshService.java
package com.example.auth.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class PermissionRefreshService {
    private final RedisTemplate<String, String> redisTemplate;
    private final SimpMessagingTemplate messagingTemplate;

    private static final String PERMISSION_UPDATE_PREFIX = "permission:update:";

    /**
     * 通知用户权限已更新
     */
    public void notifyPermissionUpdate(Long userId) {
        // 1. Redis 发布消息
        redisTemplate.convertAndSend(
                "permission:update:" + userId,
                "permission_updated");

        // 2. WebSocket 推送消息
        messagingTemplate.convertAndSend(
                "/topic/user/" + userId + "/permission",
                new PermissionUpdateMessage(userId));

        log.info("Permission update notified for user: {}", userId);
    }

    /**
     * 检查权限是否需要刷新
     */
    public boolean shouldRefreshPermissions(Long userId) {
        String lastUpdate = redisTemplate.opsForValue()
                .get("permission:lastupdate:" + userId);

        if (lastUpdate == null) {
            return false;
        }

        long lastUpdateTime = Long.parseLong(lastUpdate);
        long currentTime = System.currentTimeMillis();

        // 如果权限在 Token 签发后更新过,需要刷新
        return currentTime > lastUpdateTime;
    }
}

4.3 多设备登录管理

// service/DeviceTokenService.java
package com.example.auth.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
@RequiredArgsConstructor
public class DeviceTokenService {
    private final RedisTemplate<String, String> redisTemplate;

    private static final String DEVICE_TOKEN_PREFIX = "device:token:";
    private static final int MAX_DEVICES_PER_USER = 5;

    /**
     * 绑定设备 Token
     */
    public void bindDeviceToken(Long userId, String deviceId, String token) {
        String key = DEVICE_TOKEN_PREFIX + userId;

        // 获取现有设备列表
        Map<String, String> devices = redisTemplate.opsForHash().entries(key);

        // 如果设备数已达上限,删除最早的设备
        if (devices.size() >= MAX_DEVICES_PER_USER) {
            String oldestDevice = Collections.min(devices.keySet());
            redisTemplate.opsForHash().delete(key, oldestDevice);
        }

        // 绑定新设备
        redisTemplate.opsForHash().put(key, deviceId, token);
        redisTemplate.expire(key, 30, TimeUnit.DAYS);
    }

    /**
     * 验证设备 Token
     */
    public boolean validateDeviceToken(Long userId, String deviceId, String token) {
        String key = DEVICE_TOKEN_PREFIX + userId;
        String storedToken = (String) redisTemplate.opsForHash().get(key, deviceId);
        return token.equals(storedToken);
    }

    /**
     * 注销指定设备
     */
    public void logoutDevice(Long userId, String deviceId) {
        String key = DEVICE_TOKEN_PREFIX + userId;
        redisTemplate.opsForHash().delete(key, deviceId);
    }

    /**
     * 注销所有设备
     */
    public void logoutAllDevices(Long userId) {
        String key = DEVICE_TOKEN_PREFIX + userId;
        redisTemplate.delete(key);
    }

    /**
     * 获取用户所有设备
     */
    public Map<String, String> getUserDevices(Long userId) {
        String key = DEVICE_TOKEN_PREFIX + userId;
        return redisTemplate.opsForHash().entries(key);
    }
}

五、配置与部署

5.1 应用配置

# application.yml
spring:
  application:
    name: auth-service

  datasource:
    url: jdbc:mysql://localhost:3306/auth_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: ${DB_PASSWORD:root}
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: localhost
    port: 6379
    password: ${REDIS_PASSWORD:}
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

# JWT 配置
jwt:
  secret: ${JWT_SECRET:your-256-bit-secret-key-for-jwt-signing-and-verification}
  access-token-expiration: 30m
  refresh-token-expiration: 7d
  token-prefix: "Bearer "
  token-header: "Authorization"

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /

# 日志配置
logging:
  level:
    root: INFO
    com.example.auth: DEBUG
    org.springframework.security: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

5.2 Docker 部署

# Dockerfile
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

COPY target/auth-service-*.jar app.jar

EXPOSE 8080

ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
version: '3.8'

services:
  auth-service:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_PASSWORD=your_password
      - REDIS_PASSWORD=your_password
      - JWT_SECRET=your-secret-key
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: your_password
      MYSQL_DATABASE: auth_db
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:

六、安全加固建议

6.1 JWT 安全最佳实践

风险 防护措施
Token 泄露 使用 HTTPS、设置 HttpOnly Cookie
Token 篡改 使用强密钥(256 位+)、定期轮换
Token 重放 加入 jti 唯一标识、使用黑名单
暴力破解 限制登录尝试次数、添加验证码
权限提升 服务端验证权限、不信任客户端数据

6.2 密钥管理

// config/KeyRotationService.java
@Service
public class KeyRotationService {
    // 使用密钥 ID 支持密钥轮换
    private Map<String, SecretKey> keyRing = new ConcurrentHashMap<>();
    private String currentKeyId = "key-1";

    public String signWithKeyRotation(Claims claims) {
        return Jwts.builder()
                .claims(claims)
                .header().add("kid", currentKeyId)  // 密钥 ID
                .and()
                .signWith(getKey(currentKeyId))
                .compact();
    }

    public Claims verifyWithKeyRotation(String token) {
        String kid = Jwts.parser().build().parseUnsignedClaims(token).getHeader().get("kid", String.class);
        return Jwts.parser()
                .verifyWith(getKey(kid))
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
}

总结

通过本文的完整实战,我们构建了基于 Spring Boot 3 + Spring Security 6 的企业级 JWT 认证系统,涵盖了:

  1. 核心认证流程:登录、注册、刷新 Token、注销  
  2. 权限管理:基于角色的访问控制(RBAC)、注解式权限校验  
  3. 安全加固:Token 黑名单、设备管理、密钥轮换  
  4. 生产部署:Docker 容器化、配置管理  

关键要点回顾

  • ✅ 使用 Spring Security 6 的新配置方式(SecurityFilterChain)  
  • ✅ JWT Token 包含用户信息和权限,减少数据库查询  
  • ✅ Refresh Token 机制实现无感续期  
  • ✅ Redis 黑名单处理 Token 注销  
  • ✅ 多设备登录管理和权限动态刷新  

JWT 权限管理不是玄学,而是需要深入理解的架构设计。希望本文能帮助你在实际项目中构建安全、高效的认证系统。

如需进一步学习 Java 生态下的安全实践,可参考云栈社区提供的 后端 & 架构 专题资源。




上一篇:OpenClaw记忆架构详解:Agent为何失控及三层防御解决方案
下一篇:AI投毒原理深度剖析:GEO如何通过三种攻击方式操控大模型推荐
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-17 07:13 , Processed in 0.829783 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表