在微服务架构成为主流的今天,服务间的通信安全、用户身份的可信验证以及权限的精细化管控,已经成为架构设计中不可忽视的核心环节。Token机制作为解决分布式系统认证与授权问题的关键方案,其设计的合理性、实现的安全性直接决定了整个微服务体系的安全底线。本文将从底层逻辑出发,结合实战案例,彻底讲透Token机制的认证与授权实现,让你既能理解原理,又能落地实践。
一、Token机制的核心价值:为什么分布式系统离不开它?
在单体应用时代,我们可以通过Session-Cookie机制轻松实现用户状态管理:用户登录后,服务器创建Session存储用户信息,浏览器通过Cookie保存SessionId,后续请求携带SessionId即可完成身份识别。但在微服务架构下,这种模式面临三大致命问题:
- Session共享难题:微服务集群中,用户请求可能被路由到任意节点,Session无法跨节点共享(除非引入Redis等分布式存储,但会增加复杂度);
- 跨域限制:Cookie天然受同源策略限制,无法支持前后端分离或跨域服务调用;
- 服务解耦障碍:每个服务都需要依赖Session存储,导致服务间耦合度提升,且不利于扩展(如引入非Java服务时,Session机制无法兼容)。
Token机制的出现恰好解决了这些问题:它本质是服务端生成的一串加密字符串,包含用户身份、权限等核心信息,客户端携带Token发起请求,服务端通过验证Token的有效性完成认证与授权。其核心优势在于:
- 无状态性:服务端无需存储Token(或仅存储黑名单),所有状态都包含在Token中,便于水平扩展;
- 跨域支持:Token可通过Header、参数等方式传递,不受同源策略限制;
- 多端兼容:无论是Web、移动端还是第三方服务,都能通过统一的Token机制完成认证;
- 权限精细化:Token可携带细粒度的权限信息,支持服务级、接口级甚至数据级的授权控制。
二、Token的分类与底层原理:JWT、OAuth2.0与SAML的区别
市面上主流的Token机制包括JWT(JSON Web Token)、OAuth2.0和SAML,三者定位不同但常结合使用。我们需要先理清它们的核心差异:
2.1 JWT:轻量级的身份凭证
JWT是一种紧凑的、自包含的Token格式,定义了如何将JSON对象编码为字符串,以便在各方之间安全传输。其结构分为三部分:
- Header(头部):指定Token类型(JWT)和签名算法(如HS256、RS256);
- Payload(载荷):存储用户ID、角色、权限等核心信息(避免存储敏感数据,如密码);
- Signature(签名):使用Header指定的算法,结合密钥对Header和Payload加密生成,用于验证Token是否被篡改。
JWT的工作流程:
2.2 OAuth2.0:授权框架而非Token格式
OAuth2.0不是一种Token格式,而是一种授权协议,定义了第三方应用如何获取用户授权(如微信登录、GitHub授权)。它的核心是通过不同的授权模式(如授权码模式、密码模式)生成访问令牌(Access Token)和刷新令牌(Refresh Token):
- Access Token:用于访问受保护资源的短期Token;
- Refresh Token:用于在Access Token过期时获取新的Token,有效期更长。
OAuth2.0常与JWT结合使用:Access Token可以是JWT格式,便于资源服务器直接解析验证,无需调用认证服务器。
2.3 SAML:企业级的XML令牌
SAML(安全断言标记语言)是基于XML的Token格式,主要用于企业级单点登录(SSO),但因其体积大、解析复杂,在微服务中使用较少,本文不做深入讨论。
关键区分:
- JWT是Token的具体格式,解决“如何传递身份信息”;
- OAuth2.0是授权流程,解决“如何安全获取Token”;
- 实际项目中,通常用OAuth2.0的授权码模式生成JWT格式的Access Token。
三、JWT的实战实现:从生成到验证的完整代码
接下来我们通过Spring Boot 3.x + JDK 17实现JWT的生成、验证与刷新机制,确保代码可直接运行。
3.1 环境准备:Maven依赖配置
首先在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>
3.2 配置文件:application.yml
配置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
3.3 数据库表设计:用户与角色表
创建用户表(sys_user)和角色表(sys_role),并建立关联表(sys_user_role):
-- 创建数据库
CREATE DATABASE IF NOT EXISTS jwt_demo DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jwt_demo;
-- 用户表
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT '加密密码',
nickname VARCHAR(50) COMMENT '昵称',
status TINYINT DEFAULT 1 COMMENT '状态(1-正常,0-禁用)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) COMMENT '系统用户表';
-- 角色表
CREATE TABLE sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '角色ID',
role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
role_code VARCHAR(50) NOT NULL COMMENT '角色编码(如ADMIN、USER)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '系统角色表';
-- 用户角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES sys_user(id),
FOREIGN KEY (role_id) REFERENCES sys_role(id)
) COMMENT '用户角色关联表';
-- 插入测试数据
INSERT INTO sys_role (role_name, role_code) VALUES ('管理员', 'ADMIN'), ('普通用户', 'USER');
INSERT INTO sys_user (username, password, nickname) VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '系统管理员'); -- 密码:admin123
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1);
3.4 核心实体类设计
3.4.1 用户实体(SysUser.java)
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")
public class 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;
}
3.4.2 角色实体(SysRole.java)
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")
public class SysRole {
/**
* 角色ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 角色名称
*/
private String roleName;
/**
* 角色编码
*/
private String roleCode;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
3.4.3 Token响应类(TokenResponse.java)
package com.jam.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Token响应对象
* @author ken
*/
@Data
@Schema(description = "Token响应信息")
public class TokenResponse {
/**
* 访问令牌
*/
@Schema(description = "访问令牌")
private String accessToken;
/**
* 刷新令牌
*/
@Schema(description = "刷新令牌")
private String refreshToken;
/**
* 令牌类型
*/
@Schema(description = "令牌类型")
private String tokenType = "Bearer";
/**
* 过期时间(秒)
*/
@Schema(description = "Access Token过期时间(秒)")
private Long expiresIn;
}
3.5 JWT工具类:生成与验证Token
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
public class 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));
}
}
3.6 用户认证服务:Spring Security集成
3.6.1 用户详情服务(UserDetailsServiceImpl.java)
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
public class UserDetailsServiceImpl implements UserDetailsService {
private final SysUserMapper sysUserMapper;
private final 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);
throw new 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();
}
}
3.6.2 Spring Security配置(SecurityConfig.java)
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
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new 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();
}
}
3.7 JWT认证过滤器:拦截请求验证Token
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
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final 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);
}
}
3.8 认证控制器:登录与刷新Token
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
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserDetailsServiceImpl userDetailsService;
private final 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)) {
throw new 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;
}
}
3.9 测试接口:验证授权控制
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 = "权限控制测试")
public class TestController {
/**
* 所有认证用户可访问
*/
@GetMapping("/public")
@Operation(summary = "公开接口", description = "所有已认证用户可访问")
public String publicApi() {
return "这是所有认证用户都能访问的接口";
}
/**
* 仅管理员可访问
*/
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "管理员接口", description = "仅ADMIN角色可访问")
public String adminApi() {
return "这是管理员专属接口";
}
}
四、Token机制的安全加固:避坑指南与最佳实践
即使实现了基础的Token机制,若不注意细节,仍可能导致安全漏洞。以下是必须遵循的安全原则:
4.1 避免在JWT中存储敏感数据
JWT的Payload部分是Base64编码(非加密),任何人都能解码查看内容。因此,绝对不能存储密码、手机号、邮箱等敏感信息,只能存储用户ID、角色、权限等非敏感数据。
4.2 使用非对称加密算法(RSA)
本文示例中使用的是HS256对称加密(同一密钥用于签名和验证),但在生产环境中,建议使用RS256非对称加密:私钥保存在认证服务器用于签名Token,公钥分发给各微服务用于验证Token,避免密钥泄露导致Token伪造。
RSA密钥生成示例:
// 生成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);
4.3 合理设置Token有效期
- Access Token:建议设置为30分钟以内(如15分钟),减少Token泄露后的风险;
- Refresh Token:可设置为7天或更长,但需存储在服务端并设置黑名单机制(如Redis),支持主动失效。
4.4 实现Token黑名单机制
当用户登出、密码修改或账号被禁用时,需要主动失效Token。此时可将Token存入Redis黑名单(设置过期时间与Token有效期一致),验证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);
}
4.5 使用HTTPS传输Token
Token在网络传输过程中若被劫持,攻击者可直接冒充用户身份。因此,所有请求必须通过HTTPS传输,避免Token被明文窃取。
4.6 客户端Token存储安全
- Web端:优先使用HttpOnly + Secure Cookie存储Token(防止XSS攻击),其次使用LocalStorage(需防范XSS);
- 移动端:存储在安全的KeyStore或Keychain中,避免存储在SharedPreferences或明文文件中。
五、微服务场景下的Token授权:从粗粒度到细粒度
在微服务架构中,授权不仅要控制“谁能访问服务”,还要控制“能访问服务中的哪些资源”。以下是三种常见的授权模式:
5.1 基于角色的访问控制(RBAC)
RBAC是最常用的授权模式,通过“用户-角色-权限”的映射关系实现授权。例如:
- 管理员角色(ADMIN)拥有所有接口的访问权限;
- 普通用户角色(USER)仅能访问公开接口和个人资源接口。
在Spring Security中,可通过@PreAuthorize("hasRole('ADMIN')")或hasAuthority('order:create')实现。
5.2 基于资源的访问控制(ReBAC)
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);
}
5.3 基于属性的访问控制(ABAC)
ABAC是更细粒度的授权模式,通过“用户属性、资源属性、环境属性”的组合规则实现授权。例如:
- 只有VIP用户在非高峰期可以访问秒杀接口;
- 内部IP可以访问后台管理接口。
实现方式是自定义权限评估器:
@Component
public class 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())) {
return true;
} else {
return order.getUserId().equals(user.getId());
}
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
// 实现根据资源ID和类型的授权逻辑
return false;
}
}
六、总结:Token机制的核心是“安全”与“平衡”
Token机制并非银弹,它的设计需要在“安全性”和“易用性”之间找到平衡:
- 安全性:通过非对称加密、短有效期、黑名单机制、HTTPS等手段,最大化降低Token泄露和伪造的风险;
- 易用性:通过统一的Token格式(如JWT)、标准化的授权流程(如OAuth2.0),简化微服务间的认证授权集成。
最终,一个健壮的Token机制应该满足:
- 不可伪造:通过签名算法确保Token无法被篡改;
- 不可抵赖:Token中的信息可追溯到具体用户;
- 细粒度控制:支持服务级、接口级、数据级的授权;
- 可扩展:兼容多端、多服务、多环境的认证需求。
希望本文能帮助你彻底理解Token机制的底层逻辑,并在实际项目中落地安全、可靠的认证授权方案。