
在单体应用时代,我们可以通过Session-Cookie机制轻松实现用户状态管理:用户登录后,服务器创建Session存储用户信息,浏览器通过Cookie保存SessionId,后续请求携带SessionId即可完成身份识别。但在微服务架构下,这种模式面临三大致命问题:
Token机制的出现恰好解决了这些问题:它本质是服务端生成的一串加密字符串,包含用户身份、权限等核心信息,客户端携带Token发起请求,服务端通过验证Token的有效性完成认证与授权。其核心优势在于:
市面上主流的Token机制包括JWT(JSON Web Token)、OAuth2.0和SAML,三者定位不同但常结合使用。我们需要先理清它们的核心差异:
JWT是一种紧凑的、自包含的Token格式,定义了如何将JSON对象编码为字符串,以便在各方之间安全传输。其结构分为三部分:

OAuth2.0不是一种Token格式,而是一种授权协议,定义了第三方应用如何获取用户授权(如微信登录、GitHub授权)。它的核心是通过不同的授权模式(如授权码模式、密码模式)生成访问令牌(Access Token)和刷新令牌(Refresh Token):
OAuth2.0常与JWT结合使用:Access Token可以是JWT格式,便于资源服务器直接解析验证,无需调用认证服务器。
SAML(安全断言标记语言)是基于XML的Token格式,主要用于企业级单点登录(SSO),但因其体积大、解析复杂,在微服务中使用较少,本文不做深入讨论。
接下来我们通过Spring Boot 3.x + JDK 17实现JWT的生成、验证与刷新机制,确保代码可直接运行。
首先在pom.xml中引入核心依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/><!-- lookup parent from repository -->
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>jwt-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt-demo</name>
<description>JWT认证授权示例</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置JWT密钥、有效期及数据库连接:
spring:
# 数据库配置
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:3306/jwt_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username:root
password:root
# Spring Security配置
security:
user:
name:test
password:test
# JWT配置
jwt:
# 签名密钥(建议使用RSA非对称加密,此处为演示用对称加密)
secret:7786df7fc3a34e26a61c034d5ec8245d
# Access Token有效期:30分钟(单位:秒)
access-token-expire:1800
# Refresh Token有效期:7天(单位:秒)
refresh-token-expire:604800
# MyBatis Plus配置
mybatis-plus:
mapper-locations:classpath:mapper/**/*.xml
type-aliases-package:com.jam.demo.entity
configuration:
map-underscore-to-camel-case:true
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
# Swagger3配置
springdoc:
swagger-ui:
path:/swagger-ui.html
enabled:true
api-docs:
enabled:true
创建用户表(sys_user)和角色表(sys_role),并建立关联表(sys_user_role):
-- 创建数据库
CREATEDATABASEIFNOTEXISTS jwt_demo DEFAULTCHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jwt_demo;
-- 用户表
CREATETABLE sys_user (
idBIGINT AUTO_INCREMENT PRIMARY KEYCOMMENT'用户ID',
username VARCHAR(50) NOTNULLUNIQUECOMMENT'用户名',
passwordVARCHAR(100) NOTNULLCOMMENT'加密密码',
nickname VARCHAR(50) COMMENT'昵称',
statusTINYINTDEFAULT1COMMENT'状态(1-正常,0-禁用)',
create_time DATETIME DEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
update_time DATETIME DEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间'
) COMMENT'系统用户表';
-- 角色表
CREATETABLE sys_role (
idBIGINT AUTO_INCREMENT PRIMARY KEYCOMMENT'角色ID',
role_name VARCHAR(50) NOTNULLCOMMENT'角色名称',
role_code VARCHAR(50) NOTNULLCOMMENT'角色编码(如ADMIN、USER)',
create_time DATETIME DEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间'
) COMMENT'系统角色表';
-- 用户角色关联表
CREATETABLE sys_user_role (
user_id BIGINTNOTNULLCOMMENT'用户ID',
role_id BIGINTNOTNULLCOMMENT'角色ID',
PRIMARY KEY (user_id, role_id),
FOREIGNKEY (user_id) REFERENCES sys_user(id),
FOREIGNKEY (role_id) REFERENCES sys_role(id)
) COMMENT'用户角色关联表';
-- 插入测试数据
INSERTINTO sys_role (role_name, role_code) VALUES ('管理员', 'ADMIN'), ('普通用户', 'USER');
INSERTINTO sys_user (username, password, nickname) VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '系统管理员'); -- 密码:admin123
INSERTINTO sys_user_role (user_id, role_id) VALUES (1, 1);
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体类
* @author ken
*/
@Data
@TableName("sys_user")
publicclass SysUser {
/**
* 用户ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 加密密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 状态(1-正常,0-禁用)
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统角色实体类
* @author ken
*/
@Data
@TableName("sys_role")
publicclass SysRole {
/**
* 角色ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 角色名称
*/
private String roleName;
/**
* 角色编码
*/
private String roleCode;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
package com.jam.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Token响应对象
* @author ken
*/
@Data
@Schema(description = "Token响应信息")
publicclass TokenResponse {
/**
* 访问令牌
*/
@Schema(description = "访问令牌")
private String accessToken;
/**
* 刷新令牌
*/
@Schema(description = "刷新令牌")
private String refreshToken;
/**
* 令牌类型
*/
@Schema(description = "令牌类型")
private String tokenType = "Bearer";
/**
* 过期时间(秒)
*/
@Schema(description = "Access Token过期时间(秒)")
private Long expiresIn;
}
package com.jam.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* JWT工具类:生成、解析、验证Token
* @author ken
*/
@Component
@Slf4j
publicclass JwtUtil {
/**
* JWT签名密钥
*/
@Value("${jwt.secret}")
private String secret;
/**
* Access Token有效期(秒)
*/
@Value("${jwt.access-token-expire}")
private Long accessTokenExpire;
/**
* Refresh Token有效期(秒)
*/
@Value("${jwt.refresh-token-expire}")
private Long refreshTokenExpire;
/**
* 生成签名密钥
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 从Token中获取用户名
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 从Token中获取过期时间
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* 提取Token中的自定义Claims
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 解析Token获取所有Claims
*/
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 检查Token是否过期
*/
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 生成Access Token(含用户角色信息)
*/
public String generateAccessToken(UserDetails userDetails, Map<String, Object> extraClaims) {
return createToken(extraClaims, userDetails.getUsername(), accessTokenExpire);
}
/**
* 生成Refresh Token(仅含用户名)
*/
public String generateRefreshToken(UserDetails userDetails) {
return createToken(new HashMap<>(), userDetails.getUsername(), refreshTokenExpire);
}
/**
* 创建Token核心方法
* @param extraClaims 额外Claims信息
* @param subject 主题(用户名)
* @param expireTime 过期时间(秒)
*/
private String createToken(Map<String, Object> extraClaims, String subject, Long expireTime) {
return Jwts.builder()
.claims(extraClaims)
.subject(subject)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expireTime * 1000))
.signWith(getSigningKey())
.compact();
}
/**
* 验证Token有效性
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.SysRole;
import com.jam.demo.entity.SysUser;
import com.jam.demo.mapper.SysUserMapper;
import com.jam.demo.service.SysRoleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.util.CollectionUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户详情服务实现类(Spring Security认证核心)
* @author ken
*/
@Service
@RequiredArgsConstructor
@Slf4j
publicclass UserDetailsServiceImpl implements UserDetailsService {
privatefinal SysUserMapper sysUserMapper;
privatefinal SysRoleService sysRoleService;
/**
* 根据用户名查询用户信息及角色
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户基本信息
SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username)
.eq(SysUser::getStatus, 1));
if (sysUser == null) {
log.error("用户[{}]不存在或已禁用", username);
thrownew UsernameNotFoundException("用户不存在或已禁用");
}
// 2. 查询用户角色
List<SysRole> roleList = sysRoleService.getRolesByUserId(sysUser.getId());
List<SimpleGrantedAuthority> authorities = CollectionUtils.isEmpty(roleList) ?
List.of() :
roleList.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleCode()))
.collect(Collectors.toList());
// 3. 返回Spring Security用户详情对象
return User.withUsername(sysUser.getUsername())
.password(sysUser.getPassword())
.authorities(authorities)
.build();
}
}
package com.jam.demo.config;
import com.jam.demo.filter.JwtAuthenticationFilter;
import com.jam.demo.service.impl.UserDetailsServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.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;
/**
* Spring Security配置类
* @author ken
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
publicclass SecurityConfig {
privatefinal UserDetailsServiceImpl userDetailsService;
privatefinal JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
returnnew BCryptPasswordEncoder();
}
/**
* 认证提供者
*/
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
/**
* 安全过滤链配置
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(跨站请求伪造),因为JWT本身不需要
.csrf(csrf -> csrf.disable())
// 禁用Session,使用JWT的无状态认证
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 授权规则配置
.authorizeHttpRequests(auth -> auth
// 开放登录接口和Swagger文档
.requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 其他接口需要认证
.anyRequest().authenticated()
)
// 添加JWT认证过滤器(在用户名密码认证过滤器之前)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
package com.jam.demo.filter;
import com.jam.demo.service.impl.UserDetailsServiceImpl;
import com.jam.demo.util.JwtUtil;
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.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;
/**
* JWT认证过滤器:拦截所有请求,验证Token有效性并设置认证信息
* @author ken
*/
@Component
@RequiredArgsConstructor
@Slf4j
publicclass JwtAuthenticationFilter extends OncePerRequestFilter {
privatefinal JwtUtil jwtUtil;
privatefinal UserDetailsServiceImpl userDetailsService;
/**
* 核心过滤逻辑
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 1. 从Header中获取Token(格式:Bearer <token>)
String authHeader = request.getHeader("Authorization");
String jwt = null;
String username = null;
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
// 2. 验证Token有效性并设置认证信息
if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
// 创建认证Token并设置到SecurityContext中
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
log.error("JWT认证失败:{}", e.getMessage());
}
// 继续执行过滤链
filterChain.doFilter(request, response);
}
}
package com.jam.demo.controller;
import com.jam.demo.service.impl.UserDetailsServiceImpl;
import com.jam.demo.util.JwtUtil;
import com.jam.demo.vo.LoginRequest;
import com.jam.demo.vo.TokenResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 认证控制器:处理登录、刷新Token等请求
* @author ken
*/
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Tag(name = "认证接口", description = "用户登录、Token刷新")
@Slf4j
publicclass AuthController {
privatefinal AuthenticationManager authenticationManager;
privatefinal UserDetailsServiceImpl userDetailsService;
privatefinal JwtUtil jwtUtil;
@Value("${jwt.access-token-expire}")
private Long accessTokenExpire;
/**
* 用户登录接口
*/
@PostMapping("/login")
@Operation(summary = "用户登录", description = "输入用户名密码获取Token")
public TokenResponse login(@RequestBody LoginRequest loginRequest) {
// 1. 验证用户名密码
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
// 2. 获取用户详情并生成Token
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 构造额外Claims(包含角色信息)
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
// 生成Access Token和Refresh Token
String accessToken = jwtUtil.generateAccessToken(userDetails, extraClaims);
String refreshToken = jwtUtil.generateRefreshToken(userDetails);
// 封装响应结果
TokenResponse response = new TokenResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(accessTokenExpire);
return response;
}
/**
* 刷新Token接口
*/
@PostMapping("/refresh")
@Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token")
public TokenResponse refreshToken(@RequestParam String refreshToken) {
// 1. 验证Refresh Token有效性
String username = jwtUtil.extractUsername(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!jwtUtil.validateToken(refreshToken, userDetails)) {
thrownew RuntimeException("Refresh Token无效或已过期");
}
// 2. 生成新的Access Token
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
String newAccessToken = jwtUtil.generateAccessToken(userDetails, extraClaims);
// 封装响应结果
TokenResponse response = new TokenResponse();
response.setAccessToken(newAccessToken);
response.setRefreshToken(refreshToken); // Refresh Token不变,直到过期
response.setExpiresIn(accessTokenExpire);
return response;
}
}
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试控制器:验证不同角色的访问权限
* @author ken
*/
@RestController
@RequestMapping("/api/test")
@Tag(name = "测试接口", description = "权限控制测试")
publicclass TestController {
/**
* 所有认证用户可访问
*/
@GetMapping("/public")
@Operation(summary = "公开接口", description = "所有已认证用户可访问")
public String publicApi() {
return"这是所有认证用户都能访问的接口";
}
/**
* 仅管理员可访问
*/
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "管理员接口", description = "仅ADMIN角色可访问")
public String adminApi() {
return"这是管理员专属接口";
}
}
即使实现了基础的Token机制,若不注意细节,仍可能导致安全漏洞。以下是必须遵循的安全原则:
JWT的Payload部分是Base64编码(非加密),任何人都能解码查看内容。因此,绝对不能存储密码、手机号、邮箱等敏感信息,只能存储用户ID、角色、权限等非敏感数据。
本文示例中使用的是HS256对称加密(同一密钥用于签名和验证),但在生产环境中,建议使用RS256非对称加密:私钥保存在认证服务器用于签名Token,公钥分发给各微服务用于验证Token,避免密钥泄露导致Token伪造。
// 生成RSA密钥对
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
PrivateKey privateKey = keyPair.getPrivate(); // 私钥(认证服务器保存)
PublicKey publicKey = keyPair.getPublic(); // 公钥(分发给资源服务器)
// 生成Token时使用私钥签名
Jwts.builder()
.subject(username)
.signWith(privateKey)
.compact();
// 验证Token时使用公钥验证
Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token);
当用户登出、密码修改或账号被禁用时,需要主动失效Token。此时可将Token存入Redis黑名单(设置过期时间与Token有效期一致),验证Token时先检查是否在黑名单中。
/**
* 将Token加入黑名单
*/
public void addToBlacklist(String token) {
Long expireTime = jwtUtil.extractExpiration(token).getTime() - System.currentTimeMillis();
redisTemplate.opsForValue().set("blacklist:" + token, "invalid", expireTime, TimeUnit.MILLISECONDS);
}
/**
* 检查Token是否在黑名单中
*/
public boolean isBlacklisted(String token) {
return redisTemplate.hasKey("blacklist:" + token);
}
Token在网络传输过程中若被劫持,攻击者可直接冒充用户身份。因此,所有请求必须通过HTTPS传输,避免Token被明文窃取。
在微服务架构中,授权不仅要控制“谁能访问服务”,还要控制“能访问服务中的哪些资源”。以下是三种常见的授权模式:
RBAC是最常用的授权模式,通过“用户-角色-权限”的映射关系实现授权。例如:
在Spring Security中,可通过@PreAuthorize("hasRole('ADMIN')")或hasAuthority('order:create')实现。
ReBAC关注“用户与资源的关系”,例如:
实现方式是在接口中通过Token获取用户ID,然后查询资源的归属关系:
@GetMapping("/orders/{id}")
@PreAuthorize("hasRole('USER')")
public OrderVO getOrder(@PathVariable Long id) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
SysUser user = userService.getUserByUsername(username);
Order order = orderService.getById(id);
if (!order.getUserId().equals(user.getId())) {
throw new AccessDeniedException("无权访问该订单");
}
return orderConverter.toVO(order);
}
ABAC是更细粒度的授权模式,通过“用户属性、资源属性、环境属性”的组合规则实现授权。例如:
实现方式是自定义权限评估器:
@Component
publicclass AbacPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
// 获取用户属性
UserDetails userDetails = (UserDetails) auth.getPrincipal();
SysUser user = userService.getUserByUsername(userDetails.getUsername());
// 获取资源属性
Order order = (Order) targetDomainObject;
// 评估规则:VIP用户可访问所有订单,普通用户仅能访问自己的订单
if ("VIP".equals(user.getVipLevel())) {
returntrue;
} else {
return order.getUserId().equals(user.getId());
}
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
// 实现根据资源ID和类型的授权逻辑
returnfalse;
}
}
Token机制并非银弹,它的设计需要在“安全性”和“易用性”之间找到平衡:
最终,一个健壮的Token机制应该满足:
希望本文能帮助你彻底理解Token机制的底层逻辑,并在实际项目中落地安全、可靠的认证授权方案。