首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >微服务安全之Token机制:从认证到授权的深度实践指南

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

作者头像
果酱带你啃java
发布2026-04-14 13:50:37
发布2026-04-14 13:50:37
280
举报

在微服务架构成为主流的今天,服务间的通信安全、用户身份的可信验证以及权限的精细化管控,已经成为架构设计中不可忽视的核心环节。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的工作流程:

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中引入核心依赖:

代码语言:javascript
复制
<?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密钥、有效期及数据库连接:

代码语言:javascript
复制
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):

代码语言:javascript
复制
-- 创建数据库
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);

3.4 核心实体类设计

3.4.1 用户实体(SysUser.java)
代码语言:javascript
复制
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;
}
3.4.2 角色实体(SysRole.java)
代码语言:javascript
复制
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;
}
3.4.3 Token响应类(TokenResponse.java)
代码语言:javascript
复制
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;
}

3.5 JWT工具类:生成与验证Token

代码语言:javascript
复制
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));
    }
}

3.6 用户认证服务:Spring Security集成

3.6.1 用户详情服务(UserDetailsServiceImpl.java)
代码语言:javascript
复制
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();
    }
}
3.6.2 Spring Security配置(SecurityConfig.java)
代码语言:javascript
复制
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();
    }
}

3.7 JWT认证过滤器:拦截请求验证Token

代码语言:javascript
复制
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);
    }
}

3.8 认证控制器:登录与刷新Token

代码语言:javascript
复制
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;
    }
}

3.9 测试接口:验证授权控制

代码语言:javascript
复制
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机制的安全加固:避坑指南与最佳实践

即使实现了基础的Token机制,若不注意细节,仍可能导致安全漏洞。以下是必须遵循的安全原则:

4.1 避免在JWT中存储敏感数据

JWT的Payload部分是Base64编码(非加密),任何人都能解码查看内容。因此,绝对不能存储密码、手机号、邮箱等敏感信息,只能存储用户ID、角色、权限等非敏感数据。

4.2 使用非对称加密算法(RSA)

本文示例中使用的是HS256对称加密(同一密钥用于签名和验证),但在生产环境中,建议使用RS256非对称加密:私钥保存在认证服务器用于签名Token,公钥分发给各微服务用于验证Token,避免密钥泄露导致Token伪造。

RSA密钥生成示例:
代码语言:javascript
复制
// 生成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黑名单实现示例:
代码语言:javascript
复制
/**
 * 将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,然后查询资源的归属关系:

代码语言:javascript
复制
@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可以访问后台管理接口。

实现方式是自定义权限评估器:

代码语言:javascript
复制
@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机制并非银弹,它的设计需要在“安全性”和“易用性”之间找到平衡:

  • 安全性:通过非对称加密、短有效期、黑名单机制、HTTPS等手段,最大化降低Token泄露和伪造的风险;
  • 易用性:通过统一的Token格式(如JWT)、标准化的授权流程(如OAuth2.0),简化微服务间的认证授权集成。

最终,一个健壮的Token机制应该满足:

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在微服务架构成为主流的今天,服务间的通信安全、用户身份的可信验证以及权限的精细化管控,已经成为架构设计中不可忽视的核心环节。Token机制作为解决分布式系统认证与授权问题的关键方案,其设计的合理性、实现的安全性直接决定了整个微服务体系的安全底线。本文将从底层逻辑出发,结合实战案例,彻底讲透Token机制的认证与授权实现,让你既能理解原理,又能落地实践。
    • 一、Token机制的核心价值:为什么分布式系统离不开它?
    • 二、Token的分类与底层原理:JWT、OAuth2.0与SAML的区别
      • 2.1 JWT:轻量级的身份凭证
      • 2.2 OAuth2.0:授权框架而非Token格式
      • 2.3 SAML:企业级的XML令牌
      • 关键区分:
    • 三、JWT的实战实现:从生成到验证的完整代码
      • 3.1 环境准备:Maven依赖配置
      • 3.2 配置文件:application.yml
      • 3.3 数据库表设计:用户与角色表
      • 3.4 核心实体类设计
      • 3.5 JWT工具类:生成与验证Token
      • 3.6 用户认证服务:Spring Security集成
      • 3.7 JWT认证过滤器:拦截请求验证Token
      • 3.8 认证控制器:登录与刷新Token
      • 3.9 测试接口:验证授权控制
    • 四、Token机制的安全加固:避坑指南与最佳实践
      • 4.1 避免在JWT中存储敏感数据
      • 4.2 使用非对称加密算法(RSA)
      • 4.3 合理设置Token有效期
      • 4.4 实现Token黑名单机制
      • 4.5 使用HTTPS传输Token
      • 4.6 客户端Token存储安全
    • 五、微服务场景下的Token授权:从粗粒度到细粒度
      • 5.1 基于角色的访问控制(RBAC)
      • 5.2 基于资源的访问控制(ReBAC)
      • 5.3 基于属性的访问控制(ABAC)
    • 六、总结:Token机制的核心是“安全”与“平衡”
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档