微服务安全之Token机制:从认证到授权的深度实践指南

简介: 本文深入解析微服务架构下Token认证与授权机制,涵盖JWT、OAuth2.0核心原理,结合Spring Boot实战代码,详解Token生成、验证、安全加固及细粒度权限控制,助你构建安全可靠的分布式系统认证体系。

在微服务架构成为主流的今天,服务间的通信安全、用户身份的可信验证以及权限的精细化管控,已经成为架构设计中不可忽视的核心环节。Token机制作为解决分布式系统认证与授权问题的关键方案,其设计的合理性、实现的安全性直接决定了整个微服务体系的安全底线。本文将从底层逻辑出发,结合实战案例,彻底讲透Token机制的认证与授权实现,让你既能理解原理,又能落地实践。

一、Token机制的核心价值:为什么分布式系统离不开它?

在单体应用时代,我们可以通过Session-Cookie机制轻松实现用户状态管理:用户登录后,服务器创建Session存储用户信息,浏览器通过Cookie保存SessionId,后续请求携带SessionId即可完成身份识别。但在微服务架构下,这种模式面临三大致命问题:

  1. Session共享难题:微服务集群中,用户请求可能被路由到任意节点,Session无法跨节点共享(除非引入Redis等分布式存储,但会增加复杂度);
  2. 跨域限制:Cookie天然受同源策略限制,无法支持前后端分离或跨域服务调用;
  3. 服务解耦障碍:每个服务都需要依赖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的工作流程:

image.png

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机制应该满足:

  1. 不可伪造:通过签名算法确保Token无法被篡改;
  2. 不可抵赖:Token中的信息可追溯到具体用户;
  3. 细粒度控制:支持服务级、接口级、数据级的授权;
  4. 可扩展:兼容多端、多服务、多环境的认证需求。

希望本文能帮助你彻底理解Token机制的底层逻辑,并在实际项目中落地安全、可靠的认证授权方案。

目录
相关文章
|
5天前
|
云安全 人工智能 安全
AI被攻击怎么办?
阿里云提供 AI 全栈安全能力,其中对网络攻击的主动识别、智能阻断与快速响应构成其核心防线,依托原生安全防护为客户筑牢免疫屏障。
|
15天前
|
域名解析 人工智能
【实操攻略】手把手教学,免费领取.CN域名
即日起至2025年12月31日,购买万小智AI建站或云·企业官网,每单可免费领1个.CN域名首年!跟我了解领取攻略吧~
|
9天前
|
安全 Java Android开发
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
崩溃堆栈全是 a.b.c?Native 错误查不到行号?本文详解 Android 崩溃采集全链路原理,教你如何把“天书”变“说明书”。RUM SDK 已支持一键接入。
605 214
|
存储 人工智能 监控
从代码生成到自主决策:打造一个Coding驱动的“自我编程”Agent
本文介绍了一种基于LLM的“自我编程”Agent系统,通过代码驱动实现复杂逻辑。该Agent以Python为执行引擎,结合Py4j实现Java与Python交互,支持多工具调用、记忆分层与上下文工程,具备感知、认知、表达、自我评估等能力模块,目标是打造可进化的“1.5线”智能助手。
847 61
|
7天前
|
人工智能 移动开发 自然语言处理
2025最新HTML静态网页制作工具推荐:10款免费在线生成器小白也能5分钟上手
晓猛团队精选2025年10款真正免费、无需编程的在线HTML建站工具,涵盖AI生成、拖拽编辑、设计稿转代码等多种类型,均支持浏览器直接使用、快速出图与文件导出,特别适合零基础用户快速搭建个人网站、落地页或企业官网。
1250 157
|
4天前
|
编解码 Linux 数据安全/隐私保护
教程分享免费视频压缩软件,免费视频压缩,视频压缩免费,附压缩方法及学习教程
教程分享免费视频压缩软件,免费视频压缩,视频压缩免费,附压缩方法及学习教程
241 138
|
7天前
|
存储 安全 固态存储
四款WIN PE工具,都可以实现U盘安装教程
Windows PE是基于NT内核的轻量系统,用于系统安装、分区管理及故障修复。本文推荐多款PE制作工具,支持U盘启动,兼容UEFI/Legacy模式,具备备份还原、驱动识别等功能,操作简便,适合新旧电脑维护使用。
521 109