SpringCloud 进阶拓展:Spring Security OAuth2+JWT 微服务统一认证授权全实战|生产级方案 + 源码解析 + 踩坑实录

今天这篇博文,我会从底层原理到生产落地,把Spring Security OAuth2+JWT 实现统一认证授权 的全流程讲透。不仅给你能直接复制运行的源码,更给你能解决实际业务问题的生产级方案,还有我这些年在一线踩过的 10 + 个核心坑的完整解决方案。全文干货拉满,建议先点赞收藏 + 关注,面试、做项目都能直接用得上。

一、微服务架构下,为什么必须做统一认证授权?

1.1 单体应用到微服务,认证授权的核心痛点

做微服务开发的同学,一定绕不开认证授权这个核心难题。在单体应用里,我们用 Spring Security 一个注解就能搞定的登录权限,到了分布式微服务架构下,瞬间变得复杂起来:

  • 服务实例集群化:传统 Session 共享方案在多实例、跨节点部署下,扩展性极差,Redis 存储 Session 会带来额外的性能开销和一致性风险
  • 服务调用链路长:一个用户请求可能经过网关、订单、支付、用户多个服务,每个服务都做认证会造成代码冗余,还会引发权限不一致的问题
  • 多端接入场景复杂:Web、APP、小程序、第三方系统对接,不同端的认证模式无法统一,重复开发成本极高
  • 安全风险集中:分散的认证逻辑容易出现越权漏洞,一旦某个服务的权限校验出现问题,整个集群都会面临安全风险

这也是为什么在微服务架构中,统一认证授权是架构设计的第一道门槛,也是保障系统安全的核心基石。

1.2 主流认证授权方案对比与选型

我整理了目前企业级微服务架构中主流的 4 种认证授权方案,从落地成本、安全性、扩展性三个维度做了对比,帮大家快速选型:

方案 核心原理 优势 劣势 适用场景
Session 共享 基于 Redis 存储用户 Session,所有服务共享 Session 数据 开发简单,单体应用迁移成本低 无状态适配差,跨语言难支持,高并发下性能瓶颈 小型单体集群,无多端接入需求
网关统一 Token 校验 网关集中校验 Token,透传用户信息到后端服务 架构简单,业务服务无感知 权限粒度控制弱,无法适配复杂授权场景 简单内部系统,接口权限统一管控
Spring Security OAuth2+JWT 基于 OAuth2.0 协议标准,JWT 自包含令牌实现分布式认证 标准化协议,无状态,支持多端多场景,生态完善 学习曲线较陡,配置项多 中大型企业微服务架构,有第三方接入、多端统一登录需求
第三方认证中间件(Keycloak/MaxKey) 独立部署的认证中间件,开箱即用 功能完善,无需重复开发 定制化成本高,运维复杂度高 标准化企业内部系统,无深度定制需求

1.3 为什么最终选择 Spring Security OAuth2+JWT?

在我经手的绝大多数企业级项目中,最终都会落地Spring Security OAuth2+JWT这套方案,核心原因有 3 点:

  1. 标准化协议,无技术锁定:OAuth2.0 是行业通用的授权协议,所有主流语言和平台都支持,后续架构扩容、跨语言服务对接都没有障碍
  2. 无状态设计,完美适配微服务:JWT 令牌自包含用户身份和权限信息,服务端无需存储会话信息,天然适配分布式、容器化部署,水平扩展无压力
  3. 深度整合 Spring 生态,扩展性极强:和 SpringCloud、SpringBoot 无缝兼容,支持自定义认证逻辑、权限模型、异常处理,能满足各种复杂的业务定制需求
  4. 安全能力完善:Spring Security 提供了完善的安全防护机制,能有效抵御 CSRF、XSS、越权攻击等常见安全风险,符合等保合规要求

二、核心原理深度拆解:先搞懂底层,再写代码不翻车

很多同学写 Security 代码都是 "面向搜索引擎编程",抄来抄去最后连自己怎么登录成功的都不知道。一旦出问题,根本不知道从哪里排查。这一章节,我把核心原理给大家讲透,底层通了,写代码就是水到渠成的事。

2.1 OAuth2.0 核心四角色与四大授权模式

2.1.1 核心四角色

OAuth2.0 的本质,是在不泄露用户凭证的前提下,让第三方客户端安全地获取用户资源的访问权限。整个体系由 4 个核心角色组成,我用一个通俗的例子给大家讲明白:

  • 资源所有者(Resource Owner):最终用户,拥有资源的访问权限,比如用微信登录第三方 APP 的你
  • 客户端(Client):请求访问资源的第三方应用,比如你正在登录的知乎、B 站
  • 授权服务器(Authorization Server):负责验证用户身份,颁发授权令牌的服务,比如微信的授权服务
  • 资源服务器(Resource Server):存储用户资源的服务,需要校验令牌的合法性,比如微信的用户信息接口

通俗来说:你去酒店入住,你是资源所有者,酒店前台是客户端,酒店的公安身份核验系统是授权服务器,你的房间是资源服务器。你给前台身份证,前台去公安系统核验身份拿到授权,才能给你房卡让你进房间 ------ 这就是 OAuth2.0 的核心逻辑。

2.1.2 四大授权模式与适用场景

OAuth2.0 定义了 4 种标准授权模式,覆盖了绝大多数业务场景,面试 90% 会问这个点,大家一定要记牢:

  1. 授权码模式(Authorization Code):最安全、最主流的模式,适用于有后端服务的 Web 应用、APP。核心流程是先获取授权码,再通过授权码换取令牌,令牌全程不暴露在前端,安全性最高。
  2. 密码模式(Resource Owner Password Credentials):用户把用户名密码直接交给客户端,客户端用凭证向授权服务器换取令牌。适用于公司内部的可信系统,比如企业内部管理后台。
  3. 客户端模式(Client Credentials):无用户参与,客户端以自己的名义向授权服务器申请令牌。适用于服务间调用、定时任务等机器与机器之间的通信场景。
  4. 简化模式(Implicit):直接在前端获取令牌,无需授权码环节。安全性极低,目前已不推荐使用,仅适用于纯前端静态页面,且无后端服务的极简单场景。

2.2 JWT 令牌结构与核心特性(避坑重点)

JWT 全称 JSON Web Token,是一种轻量级的、自包含的令牌格式,用于在网络环境中传递用户身份和权限信息。很多新手对 JWT 有误解,这里我把核心点讲透,避免踩坑。

2.2.1 JWT 的三段式结构

JWT 由三部分组成,用.分隔,完整格式为:header.payload.signature

我给大家逐段拆解:

  1. Header(头部):Base64URL 编码,存储令牌类型和签名算法,示例:

    bash 复制代码
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    • alg:签名算法,常用 HS256(对称加密)、RS256(非对称加密,生产环境推荐)
    • typ:令牌类型,固定为 JWT
  2. Payload(载荷):Base64URL 编码,存储核心数据,也是我们自定义用户信息的地方。分为三类声明:

    • 注册声明:JWT 官方定义的标准字段,比如iss(签发者)、exp(过期时间)、sub(主题)、jti(令牌唯一 ID)
    • 公共声明:自定义的通用字段,比如userIdusernameroles
    • 私有声明:业务自定义的特殊字段

    划重点(踩坑无数)

    • Payload 仅做 Base64 编码,没有加密 !任何人拿到令牌都能解码看到内容,严禁存储密码、手机号、身份证号等敏感信息
    • Payload 内容越多,JWT 长度越长,会导致请求头过大,引发 Nginx 400 错误,只存核心非敏感信息
  3. Signature(签名):整个 JWT 的核心安全保障,由编码后的 Header、Payload、密钥,通过 Header 指定的算法生成。

    • 作用:验证令牌是否被篡改,只有持有密钥的服务端才能生成和校验签名,确保令牌的合法性
    • 生产环境强烈推荐使用 RS256 非对称加密算法,私钥签发令牌,公钥校验令牌,避免对称密钥泄露导致的伪造令牌风险
2.2.2 JWT 的核心优势与局限性
优势 局限性
无状态:服务端无需存储会话信息,天然适配分布式集群 令牌一旦签发,过期前无法主动撤销,需额外方案解决
自包含:Payload 包含用户核心信息,减少数据库查询 无法控制令牌的使用范围,存在被盗用的风险
跨语言跨平台:基于 JSON 格式,所有主流语言都支持 加密算法配置不当,会引发严重的安全漏洞
适合微服务架构:网关层一次校验,全链路透传 不适合存储大量数据,会增加网络传输开销

2.3 Spring Security OAuth2 核心执行流程与组件

Spring Security OAuth2 的核心执行流程,本质上是 Spring Security 的过滤器链 + OAuth2 协议的结合,核心组件我给大家梳理清楚,后续写配置全靠这些:

  1. AuthenticationManager:Spring Security 的核心认证管理器,负责校验用户的身份凭证
  2. UserDetailsService:自定义用户信息加载接口,我们通过实现这个接口,从数据库加载用户信息和权限
  3. AuthorizationServerConfigurerAdapter:授权服务器配置核心类,负责配置客户端信息、令牌管理、授权端点
  4. ResourceServerConfigurerAdapter:资源服务器配置核心类,负责配置资源的访问规则、令牌校验规则
  5. TokenStore:令牌存储接口,负责令牌的生成、存储、校验,我们用 JwtTokenStore 实现 JWT 令牌的管理
  6. JwtAccessTokenConverter:JWT 令牌转换器,负责 JWT 的编码、解码、签名校验
  7. ClientDetailsService:客户端信息管理接口,负责加载 OAuth2 客户端的配置信息,比如客户端 ID、密钥、授权模式、权限范围

2.4 微服务统一认证授权的完整请求链路

这里我给大家画了完整的请求时序图,把整个认证授权的流程讲清楚,大家可以对照这个图理解后续的代码实现。

完整流程步骤:

  1. 客户端向用户发起授权请求,用户同意授权
  2. 客户端携带授权凭证,向认证中心(授权服务器)申请令牌
  3. 认证中心校验用户身份和授权凭证,通过后签发 JWT 令牌(access_token+refresh_token)
  4. 客户端携带 JWT 令牌,通过网关访问后端业务服务(资源服务器)
  5. 网关层校验 JWT 令牌的合法性,校验通过后,将用户信息透传到后端服务
  6. 资源服务器本地校验 JWT 签名,解析用户权限,判断是否有权限访问对应资源
  7. 权限校验通过,资源服务器返回业务数据;校验失败,返回 401/403 错误

三、环境与版本说明(90% 的人跑不起来都栽在这)

我见过太多同学,抄了网上的代码,结果启动报错、配置不生效,90% 的问题都出在版本不兼容 上。Spring 生态的版本对应关系非常严格,尤其是 Spring Security OAuth2 已经在 2022 年停止维护,新版本的 Spring Boot 3.x 已经不再兼容旧的配置方式,这里我给大家提供企业生产环境验证过的稳定版本组合,大家直接照着用,绝对不会出现版本兼容问题。

3.1 官方推荐稳定版本对应关系

组件 版本号 核心说明
JDK 1.8 / 11 兼容 99% 企业生产环境,文末会提供 JDK17+Spring Boot 3.x 的适配方案
Spring Boot 2.7.18 长期支持稳定版,与 Spring Security OAuth2 完美兼容,无 API 废弃问题
Spring Cloud 2021.0.8 (Jubilee) Spring 官方对应 Spring Boot 2.7.x 的稳定发行版,经过大规模生产验证
Spring Security OAuth2 2.4.2 最后一个官方维护稳定版,生产环境首选,无高危漏洞
JJWT 0.11.5 JWT 处理主流稳定版,API 完善,无安全漏洞
Spring Cloud Gateway 3.1.8 对应 Spring Cloud 2021.0.x 版本,非阻塞网关,性能优异
Nacos 2.2.3 服务注册与配置中心,可选,用于微服务注册发现
MySQL 8.0 用户与权限数据存储
Redis 6.2+ 令牌黑名单、刷新令牌存储、分布式锁

3.2 项目整体架构与模块规划

我们采用标准的 SpringCloud 微服务架构,整体模块划分如下,大家可以直接照着搭建:

模块名 端口 核心职责
cloud-auth 9001 认证中心(授权服务器),负责用户身份认证、令牌签发与管理
cloud-gateway 8080 网关层,统一请求入口,全局令牌校验、路由转发、跨域处理
cloud-system 9002 系统管理资源服务器,用户、角色、权限相关接口
cloud-order 9003 订单业务资源服务器,演示业务接口权限控制
cloud-common - 公共模块,通用工具类、实体类、常量定义

四、保姆级实战:从零搭建生产级统一认证授权体系

原理讲完了,现在进入实战环节,我会带着大家从零搭建完整的项目,每一行配置都给大家讲清楚作用,保证大家跟着做就能跑通。

4.1 父工程与公共依赖搭建

首先创建 Maven 父工程,统一管理所有依赖的版本,避免子模块版本混乱。

pom.xml 核心配置
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.springcloud.demo</groupId>
    <artifactId>cloud-oauth2-demo</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    <name>cloud-oauth2-demo</name>
    <description>SpringCloud OAuth2+JWT统一认证授权实战项目</description>

    <!-- 统一版本管理 -->
    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.7.18</spring-boot.version>
        <spring-cloud.version>2021.0.8</spring-cloud.version>
        <spring-security-oauth2.version>2.4.2</spring-security-oauth2.version>
        <jjwt.version>0.11.5</jjwt.version>
        <nacos.version>2.2.3.RELEASE</nacos.version>
        <mysql.version>8.0.33</mysql.version>
        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
    </properties>

    <!-- 依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringBoot 父依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- SpringCloud 父依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- Spring Security OAuth2 -->
            <dependency>
                <groupId>org.springframework.security.oauth</groupId>
                <artifactId>spring-security-oauth2</artifactId>
                <version>${spring-security-oauth2.version}</version>
            </dependency>

            <!-- Nacos 服务注册发现 -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.9.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- JJWT JWT处理 -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-api</artifactId>
                <version>${jjwt.version}</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-impl</artifactId>
                <version>${jjwt.version}</version>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-jackson</artifactId>
                <version>${jjwt.version}</version>
                <scope>runtime</scope>
            </dependency>

            <!-- MySQL驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <!-- MyBatis-Plus -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 认证中心(Authorization Server)核心实现

认证中心是整个体系的核心,负责用户身份认证、令牌签发、刷新令牌、令牌吊销等核心能力。我们创建cloud-auth子模块。

4.2.1 核心依赖引入
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.springcloud.demo</groupId>
        <artifactId>cloud-oauth2-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>cloud-auth</artifactId>
    <name>cloud-auth</name>
    <description>认证中心-授权服务器</description>

    <dependencies>
        <!-- SpringBoot 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>

        <!-- Spring Security OAuth2 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>

        <!-- Nacos 服务注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- MySQL + MyBatis-Plus -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>
4.2.2 配置文件 application.yml
bash 复制代码
server:
  port: 9001

spring:
  application:
    name: cloud-auth
  # 数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud_oauth2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_mysql_password
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: your_redis_password
    database: 0
  # Nacos配置
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.springcloud.demo.auth.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 自定义JWT配置
jwt:
  # 签名密钥,生产环境请使用非对称加密,替换为RSA私钥
  secret: your_jwt_secret_key_2024_springcloud_oauth2_demo_1234567890
  # access_token过期时间,单位秒,生产环境建议30分钟
  access-token-expire: 1800
  # refresh_token过期时间,单位秒,生产环境建议7天
  refresh-token-expire: 604800
4.2.3 Spring Security 核心配置

这里要注意,Spring Boot 2.7.x 中WebSecurityConfigurerAdapter已经被标记为废弃,我们采用官方推荐的SecurityFilterChain方式配置,兼容新老版本,避免踩坑。

java 复制代码
package com.springcloud.demo.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import javax.annotation.Resource;

/**
 * Spring Security 核心配置类
 * 负责用户身份认证、密码加密、安全规则配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Resource
    private UserDetailsService userDetailsService;

    /**
     * 密码加密器,生产环境必须使用BCrypt,严禁明文存储密码
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器,OAuth2密码模式必须注入这个Bean
     */
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
    }

    /**
     * 安全过滤链配置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF防护,前后端分离架构无需开启
                .csrf().disable()
                // 关闭Session,使用JWT无状态认证
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 请求权限配置
                .authorizeHttpRequests()
                // 放行OAuth2相关端点
                .antMatchers("/oauth/**", "/actuator/**").permitAll()
                // 其他所有请求都需要认证
                .anyRequest().authenticated();

        return http.build();
    }

    /**
     * 静态资源放行配置
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().antMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico");
    }
}
4.2.4 自定义用户信息加载实现

我们通过实现UserDetailsService接口,从数据库加载用户信息和权限,这是对接我们自己的用户体系的核心。

首先创建用户实体类SysUser

java 复制代码
package com.springcloud.demo.auth.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;

/**
 * 系统用户实体
 */
@Data
@TableName("sys_user")
public class SysUser {
    @TableId(type = IdType.AUTO)
    private Long userId;
    private String username;
    private String password;
    private String nickname;
    private String phone;
    private Integer status; // 0-禁用 1-正常
    private LocalDateTime createTime;
}

然后实现UserDetailsService

java 复制代码
package com.springcloud.demo.auth.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.springcloud.demo.auth.entity.SysUser;
import com.springcloud.demo.auth.mapper.SysUserMapper;
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 javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * 自定义用户信息加载服务
 * 从数据库加载用户信息和权限,供Spring Security认证使用
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名查询用户
        SysUser user = sysUserMapper.selectOne(
                new LambdaQueryWrapper<SysUser>()
                        .eq(SysUser::getUsername, username)
                        .eq(SysUser::getStatus, 1)
        );

        // 2. 用户不存在,抛出异常
        if (user == null) {
            throw new UsernameNotFoundException("用户【" + username + "】不存在");
        }

        // 3. 加载用户权限列表,生产环境从数据库查询用户的角色和权限
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        // 这里简化处理,给用户添加管理员角色,实际项目从数据库查询
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        authorities.add(new SimpleGrantedAuthority("system:user:list"));
        authorities.add(new SimpleGrantedAuthority("order:info:query"));

        // 4. 返回UserDetails对象,供Spring Security认证
        return new User(
                user.getUsername(),
                user.getPassword(),
                true,
                true,
                true,
                true,
                authorities
        );
    }
}
4.2.5 JWT 令牌配置与签名管理

这是 JWT 令牌的核心配置,负责 JWT 的生成、解码、签名校验,以及令牌的存储管理。

首先创建 JWT 令牌转换器配置:

java 复制代码
package com.springcloud.demo.auth.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * JWT令牌配置类
 * 负责JWT令牌的生成、签名、存储配置
 */
@Configuration
public class JwtTokenConfig {

    @Value("${jwt.secret}")
    private String jwtSecret;

    /**
     * JWT令牌转换器,负责令牌的编码、解码、签名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 设置签名密钥
        converter.setSigningKey(jwtSecret);
        return converter;
    }

    /**
     * 令牌存储实现,使用JWT无状态存储
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
}

然后是授权服务器核心配置,这是整个认证中心的核心:

java 复制代码
package com.springcloud.demo.auth.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.annotation.Resource;

/**
 * 授权服务器核心配置类
 * 开启授权服务器功能,配置客户端信息、令牌管理、授权端点
 */
@Configuration
@EnableAuthorizationServer // 开启授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private TokenStore tokenStore;
    @Resource
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Value("${jwt.access-token-expire}")
    private int accessTokenExpire;
    @Value("${jwt.refresh-token-expire}")
    private int refreshTokenExpire;

    /**
     * 授权服务器安全配置
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 允许客户端表单认证
                .allowFormAuthenticationForClients()
                // 放行令牌校验端点
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    /**
     * 客户端信息配置
     * 配置哪些客户端可以访问授权服务器,支持内存配置和数据库配置
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                // 客户端ID,唯一标识
                .withClient("cloud-client")
                // 客户端密钥,必须加密
                .secret(passwordEncoder.encode("cloud-client-secret-123456"))
                // 授权模式,支持授权码、密码、客户端模式、刷新令牌
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "refresh_token")
                // 权限范围,自定义
                .scopes("all", "system", "order")
                // 回调地址,授权码模式必须配置
                .redirectUris("http://localhost:8080/callback")
                // access_token过期时间
                .accessTokenValiditySeconds(accessTokenExpire)
                // refresh_token过期时间
                .refreshTokenValiditySeconds(refreshTokenExpire)
                // 是否自动授权,授权码模式使用
                .autoApprove(true);
    }

    /**
     * 授权端点配置
     * 配置认证管理器、令牌存储、用户信息服务
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                // 认证管理器,密码模式必须配置
                .authenticationManager(authenticationManager)
                // 用户信息服务,刷新令牌必须配置
                .userDetailsService(userDetailsService)
                // 令牌存储
                .tokenStore(tokenStore)
                // JWT令牌转换器
                .accessTokenConverter(jwtAccessTokenConverter);
    }
}
4.2.6 自定义异常处理(生产必备)

默认的异常返回格式不符合我们的业务需求,我们自定义异常处理,统一返回格式,方便前端处理。

java 复制代码
package com.springcloud.demo.auth.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义认证异常处理
 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("msg", "认证失败:" + authException.getMessage());
        result.put("data", null);

        ObjectMapper objectMapper = new ObjectMapper();
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

4.3 资源服务器(Resource Server)核心实现

资源服务器就是我们的业务服务,负责校验令牌的合法性,控制接口的访问权限。我们以cloud-system模块为例,cloud-order模块配置完全一致。

4.3.1 核心依赖引入
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.springcloud.demo</groupId>
        <artifactId>cloud-oauth2-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>cloud-system</artifactId>
    <name>cloud-system</name>
    <description>系统管理资源服务器</description>

    <dependencies>
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Security OAuth2 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>

        <!-- Nacos 服务注册 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>
4.3.2 配置文件 application.yml
bash 复制代码
server:
  port: 9002

spring:
  application:
    name: cloud-system
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

# JWT配置,必须和认证中心保持一致
jwt:
  secret: your_jwt_secret_key_2024_springcloud_oauth2_demo_1234567890
4.3.3 资源服务器核心配置
java 复制代码
package com.springcloud.demo.system.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * 资源服务器配置类
 * 开启资源服务器功能,配置资源访问规则、令牌校验
 */
@Configuration
@EnableResourceServer // 开启资源服务器
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级权限注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Value("${jwt.secret}")
    private String jwtSecret;

    // 资源ID,唯一标识
    private static final String RESOURCE_ID = "system-resource";

    /**
     * JWT令牌转换器,必须和认证中心保持一致
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(jwtSecret);
        return converter;
    }

    /**
     * 令牌存储,必须和认证中心保持一致
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 资源服务器安全配置
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID)
                .tokenStore(tokenStore())
                .stateless(true);
    }

    /**
     * 请求权限配置
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 放行接口
                .antMatchers("/system/hello").permitAll()
                // 其他所有接口都需要认证
                .anyRequest().authenticated();
    }
}
4.3.4 测试接口编写

我们编写几个测试接口,演示不同的权限控制方式:

java 复制代码
package com.springcloud.demo.system.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

/**
 * 系统管理测试接口
 */
@RestController
@RequestMapping("/system")
public class SystemController {

    /**
     * 公开接口,无需认证
     */
    @GetMapping("/hello")
    public String hello() {
        return "Hello,这是system服务的公开接口,无需认证即可访问";
    }

    /**
     * 需要认证的接口
     */
    @GetMapping("/user/info")
    public Principal userInfo(Principal principal) {
        return principal;
    }

    /**
     * 需要角色权限的接口
     */
    @GetMapping("/user/list")
    @PreAuthorize("hasRole('ADMIN')")
    public String userList() {
        return "查询用户列表成功,当前用户拥有ADMIN角色权限";
    }

    /**
     * 需要具体权限标识的接口
     */
    @GetMapping("/user/add")
    @PreAuthorize("hasAuthority('system:user:add')")
    public String userAdd() {
        return "新增用户成功,当前用户拥有system:user:add权限";
    }
}

4.4 SpringCloud Gateway 网关层统一鉴权实现

在微服务架构中,我们通常会把令牌校验、权限初验的逻辑上移到网关层,做统一的入口管控。这样做的好处是:

  • 避免每个资源服务都重复写令牌校验逻辑,减少代码冗余
  • 非法请求在网关层直接拦截,不会转发到后端服务,减少服务压力
  • 统一处理跨域、白名单、限流等公共逻辑,便于维护

我们创建cloud-gateway网关模块,实现全局鉴权。

4.4.1 核心依赖引入
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.springcloud.demo</groupId>
        <artifactId>cloud-oauth2-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>cloud-gateway</artifactId>
    <name>cloud-gateway</name>
    <description>网关服务-统一鉴权</description>

    <dependencies>
        <!-- SpringCloud Gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- Nacos 服务注册发现 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- JJWT JWT处理 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>
4.4.2 配置文件 application.yml
bash 复制代码
server:
  port: 8080

spring:
  application:
    name: cloud-gateway
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: your_redis_password
    database: 0
  # Nacos配置
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    # 网关路由配置
    gateway:
      routes:
        # 认证中心路由
        - id: auth-route
          uri: lb://cloud-auth
          predicates:
            - Path=/oauth/**
        # 系统服务路由
        - id: system-route
          uri: lb://cloud-system
          predicates:
            - Path=/system/**
        # 订单服务路由
        - id: order-route
          uri: lb://cloud-order
          predicates:
            - Path=/order/**
      # 跨域配置
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: "*"
            allowed-methods: "*"
            allowed-headers: "*"
            allow-credentials: true
            max-age: 3600

# 自定义配置
auth:
  # 白名单路径,无需认证即可访问
  white-list:
    - /oauth/**
    - /system/hello
  # JWT配置
  jwt:
    secret: your_jwt_secret_key_2024_springcloud_oauth2_demo_1234567890
4.4.3 全局鉴权过滤器实现

这是网关层统一鉴权的核心,我们通过实现GlobalFilter全局过滤器,拦截所有请求,校验令牌的合法性。

java 复制代码
package com.springcloud.demo.gateway.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * 全局鉴权过滤器
 * 统一拦截所有请求,校验令牌合法性
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Value("${auth.white-list}")
    private List<String> whiteList;

    @Value("${auth.jwt.secret}")
    private String jwtSecret;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    private final ObjectMapper objectMapper = new ObjectMapper();

    // 黑名单KEY前缀
    private static final String BLACK_LIST_KEY = "auth:blacklist:";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String requestPath = request.getPath().value();

        // 1. 白名单路径,直接放行
        for (String path : whiteList) {
            if (antPathMatcher.match(path, requestPath)) {
                return chain.filter(exchange);
            }
        }

        // 2. 获取请求头中的令牌
        String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (token == null || !token.startsWith("Bearer ")) {
            return buildErrorResponse(response, "令牌不存在,请先登录", HttpStatus.UNAUTHORIZED);
        }
        // 去除Bearer前缀
        token = token.substring(7);

        try {
            // 3. 校验JWT签名和过期时间
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

            // 4. 校验令牌是否在黑名单中(用户登出、封禁)
            String jti = claims.getId();
            if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(BLACK_LIST_KEY + jti))) {
                return buildErrorResponse(response, "令牌已失效,请重新登录", HttpStatus.UNAUTHORIZED);
            }

            // 5. 解析用户信息,透传到后端服务
            String username = claims.getSubject();
            Long userId = claims.get("userId", Long.class);
            ServerHttpRequest mutableRequest = request.mutate()
                    .header("X-User-Id", String.valueOf(userId))
                    .header("X-Username", username)
                    .build();

            // 6. 放行请求
            return chain.filter(exchange.mutate().request(mutableRequest).build());

        } catch (Exception e) {
            // 令牌校验失败,返回错误信息
            return buildErrorResponse(response, "令牌无效或已过期,请重新登录", HttpStatus.UNAUTHORIZED);
        }
    }

    /**
     * 构建错误响应
     */
    private Mono<Void> buildErrorResponse(ServerHttpResponse response, String msg, HttpStatus status) {
        response.setStatusCode(status);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        Result result = new Result();
        result.setCode(status.value());
        result.setMsg(msg);
        result.setData(null);

        try {
            byte[] bytes = objectMapper.writeValueAsString(result).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bytes);
            return response.writeWith(Mono.just(buffer));
        } catch (JsonProcessingException e) {
            return response.setComplete();
        }
    }

    /**
     * 过滤器执行顺序,数值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    /**
     * 统一响应结果
     */
    @Data
    static class Result {
        private Integer code;
        private String msg;
        private Object data;
    }
}

4.5 全流程联调测试(Postman 完整示例)

所有模块开发完成后,我们启动 Nacos、MySQL、Redis,然后依次启动认证中心、网关、业务服务,用 Postman 进行联调测试。

4.5.1 密码模式获取令牌

请求地址:POST http://localhost:8080/oauth/token

请求参数(form-data):

参数名 参数值 说明
grant_type password 授权模式,密码模式
username admin 用户名
password 123456 密码
client_id cloud-client 客户端 ID
client_secret cloud-client-secret-123456 客户端密钥

成功响应结果:

bash 复制代码
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx.xxx",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx.xxx",
    "expires_in": 1799,
    "scope": "all system order",
    "jti": "f8a7c6d5-1234-5678-90ab-cdef01234567"
}
4.5.2 刷新令牌

请求地址:POST http://localhost:8080/oauth/token

请求参数(form-data):

参数名 参数值 说明
grant_type refresh_token 授权模式,刷新令牌
refresh_token 上一步获取的 refresh_token 刷新令牌
client_id cloud-client 客户端 ID
client_secret cloud-client-secret-123456 客户端密钥
4.5.3 访问业务接口

请求地址:GET http://localhost:8080/system/user/info

请求头:

Authorization Bearer 你的 access_token

成功响应,返回用户信息和权限列表。

五、进阶拓展:生产级优化与高阶能力实现

上面的基础实现只能跑通 demo,想要落地到生产环境,还需要做很多优化和高阶能力的实现。这一章节,我把生产环境必备的进阶方案给大家讲透。

5.1 双 Token 机制:无感刷新与续签方案

JWT 的一个核心痛点是,access_token 过期后,用户需要重新登录,体验极差。我们采用双 Token 机制(access_token+refresh_token) 实现无感刷新,核心设计思路:

  1. access_token:有效期设置为 30 分钟,用于接口访问,有效期短,降低被盗用的风险
  2. refresh_token:有效期设置为 7 天,仅用于刷新 access_token,不用于接口访问
  3. 无感刷新流程
    • 前端检测到 access_token 过期(接口返回 401),自动携带 refresh_token 调用刷新令牌接口
    • 认证中心校验 refresh_token 的合法性,通过后签发新的 access_token 和 refresh_token
    • 前端用新的 access_token 重新发起请求,用户无感知
    • 刷新令牌采用一次有效机制,每次刷新后,旧的 refresh_token 立即失效,避免被盗用

5.2 JWT 令牌吊销:黑名单机制实现

JWT 的另一个核心痛点是,令牌一旦签发,过期前无法主动撤销。比如用户修改密码、退出登录、被管理员封禁,已经签发的令牌仍然可以使用,存在严重的安全风险。

我们采用Redis 黑名单机制解决这个问题,核心实现方案:

  1. 每个 JWT 令牌都生成唯一的jti(令牌 ID),存储在 Payload 中
  2. 用户退出登录、修改密码、被封禁时,将jti存入 Redis 黑名单,过期时间设置为令牌的剩余有效期
  3. 网关层和资源服务器每次校验令牌时,先检查jti是否在黑名单中,如果在,直接拒绝访问
  4. 优化方案:access_token 有效期设置为 5-15 分钟,即使出现安全问题,最多只有几分钟的风险窗口,平衡安全性和性能

5.3 细粒度权限控制:RBAC 模型深度整合

生产环境中,我们通常采用RBAC(基于角色的访问控制) 模型,实现细粒度的权限控制,核心设计:

  1. 用户 - 角色 - 权限三级模型:一个用户拥有多个角色,一个角色拥有多个权限
  2. 权限粒度:分为菜单权限、按钮权限、接口权限、数据权限
  3. 整合实现
    • 用户登录时,从数据库加载用户的角色和权限列表,存入 JWT 的 Payload 中
    • 资源服务器通过@PreAuthorize注解,实现接口级的权限控制
    • 网关层做权限初验,拦截无权限的请求
    • 数据权限通过 MyBatis-Plus 的插件实现,根据用户角色动态拼接查询条件

5.4 高并发场景下的性能优化方案

在高并发场景下,认证中心会成为整个系统的瓶颈,我们可以通过以下方案优化性能:

  1. 本地验签为主:资源服务器和网关层本地校验 JWT 签名,无需每次都调用认证中心,减少认证中心的压力
  2. 用户权限缓存:将用户的角色和权限数据存入 Redis,减少数据库查询次数
  3. 令牌签发限流:对认证中心的令牌签发接口做限流,防止暴力破解和恶意请求
  4. 集群部署:认证中心多实例集群部署,通过 Nacos 实现负载均衡
  5. 非对称加密优化:生产环境使用 RS256 非对称加密,公钥分发给所有资源服务,私钥仅保存在认证中心,提升安全性的同时,减少密钥管理的复杂度

5.5 安全加固:防篡改、防重放、密钥轮换方案

生产环境的安全是重中之重,我们需要做以下安全加固:

  1. 防篡改:使用强签名算法,生产环境必须使用 RS256 非对称加密,严禁使用 HS256 对称加密,避免密钥泄露导致的令牌伪造
  2. 防重放攻击 :在 JWT 中加入iat(签发时间)和jti(唯一 ID),对短时间内重复的请求做拦截
  3. HTTPS 传输:所有接口必须使用 HTTPS 传输,防止令牌在网络传输过程中被窃听
  4. 密钥轮换:定期更换签名密钥,避免长期使用同一个密钥导致的泄露风险,通过 Redis 同步公钥给所有资源服务
  5. 令牌存储安全:前端严禁将令牌存入 localStorage,防止 XSS 攻击,建议存入 HttpOnly 的 Cookie 中,配合 CSRF 防护

六、踩坑实录:我在生产环境踩过的 10 + 个核心坑与解决方案

这一章节,我把这些年在生产环境踩过的核心坑全部分享出来,大家可以提前避坑,90% 的问题你都会遇到。

6.1 版本兼容类坑点

坑 1:Spring Boot 2.7.x 版本 WebSecurityConfigurerAdapter 废弃,配置不生效

现象 :抄了网上的旧版本代码,继承 WebSecurityConfigurerAdapter,结果启动不报错,但是配置完全不生效。原因 :Spring Boot 2.7.x 开始,WebSecurityConfigurerAdapter 被标记为废弃,Spring Boot 3.x 完全移除了这个类。解决方案:采用官方推荐的 SecurityFilterChain 方式配置,也就是我们上面实战中用的方式,兼容新老版本。

坑 2:Spring Cloud 版本和 Spring Boot 版本不匹配,启动报错

现象 :启动时报各种类找不到、方法不存在的异常。原因 :Spring Cloud 和 Spring Boot 有严格的版本对应关系,乱搭配会导致兼容问题。解决方案:严格按照我们上面提供的版本对应表使用,不要盲目追新,生产环境用经过验证的稳定版。

6.2 令牌处理类坑点

坑 3:JWT Payload 内容过多,导致请求头过大,Nginx 返回 400 错误

现象 :用户权限多的时候,接口请求直接返回 400 Bad Request,后端服务没有收到请求。原因 :Payload 里存了太多的权限数据,导致 JWT 长度过长,超过了 Nginx 的 header 缓冲区默认大小。解决方案

  1. Payload 只存核心非敏感信息,比如 userId、username、角色编码,不要存全量的权限列表
  2. 权限列表在资源服务本地通过 userId 查询,配合 Redis 缓存
  3. 调整 Nginx 的配置,增大缓冲区:large_client_header_buffers 4 16k;
坑 4:令牌过期时间配置不生效

现象 :配置了 access_token 的过期时间,但是生成的令牌过期时间不对。原因 :客户端配置的过期时间会覆盖全局配置,同时 JWT 的 exp 时间是秒级时间戳,很多同学搞错了单位。解决方案:在客户端配置中明确设置 accessTokenValiditySeconds,单位是秒,不要在其他地方重复配置。

6.3 权限控制类坑点

坑 5:@PreAuthorize 注解不生效

现象 :加了 @PreAuthorize 注解,但是没有权限的用户仍然能访问接口。原因 :没有开启方法级权限注解,需要在配置类上加 @EnableGlobalMethodSecurity (prePostEnabled = true)。解决方案:在资源服务器配置类上加上这个注解,同时确保 Spring Security 的配置被正确加载。

坑 6:角色权限校验失败,hasRole 不生效

现象 :用户有对应的角色,但是 hasRole 校验失败。原因 :Spring Security 的 hasRole 会自动给角色名加上 ROLE_前缀,比如 hasRole ('ADMIN'),实际校验的是用户是否有 ROLE_ADMIN 权限。解决方案:给用户的角色权限加上 ROLE_前缀,或者使用 hasAuthority 注解,直接校验权限标识。

6.4 网关与跨域类坑点

坑 7:OPTIONS 预检请求被拦截,导致跨域失败

现象 :前端发起跨域请求时,OPTIONS 预检请求返回 401,接口请求失败。原因 :网关的鉴权过滤器拦截了 OPTIONS 预检请求,而浏览器的预检请求不会携带 Authorization 头。解决方案:在网关的白名单中放行 OPTIONS 请求,或者在过滤器中判断请求方法,如果是 OPTIONS 直接放行。

坑 8:网关转发请求后,用户信息透传失败

现象 :网关把用户信息放到请求头中,后端服务获取不到。原因 :Spring Cloud Gateway 默认会过滤掉带下划线的请求头,或者后端服务的 server.forward-headers-strategy 配置不对。解决方案

  1. 请求头名称使用横杠 -,不要用下划线_
  2. 后端服务配置:server.forward-headers-strategy=framework

6.5 生产运维类坑点

坑 9:服务器时间不同步,导致令牌校验失败

现象 :部分服务器上的服务校验令牌失败,提示令牌已过期,但是令牌明明在有效期内。原因 :不同服务器的系统时间偏差超过 5 分钟,JWT 的时间校验失败。解决方案:所有服务器同步 NTP 时间,确保系统时间一致,同时给令牌校验设置 30 秒的时间容差。

坑 10:签名密钥泄露,导致令牌被伪造

现象 :出现非法用户访问系统,日志显示令牌是合法的,但是用户并没有登录。原因 :使用了 HS256 对称加密,密钥在多个服务中配置,导致密钥泄露,攻击者伪造了令牌。解决方案:生产环境必须使用 RS256 非对称加密,私钥仅保存在认证中心,公钥公开用于验签,即使公钥泄露,也无法伪造令牌。

七、方案对比与行业最佳实践

7.1 主流认证授权方案对比

很多同学会问,现在官方已经推荐 Spring Authorization Server(SAS)了,还有 Keycloak、Sa-Token 这些方案,我该怎么选?这里我给大家做了完整的对比,帮大家选型。

方案 优势 劣势 适用场景
Spring Security OAuth2+JWT 生态成熟,资料多,兼容 Spring Boot 2.x,企业落地案例多 官方已停止维护,不支持 Spring Boot 3.x 现有 Spring Boot 2.x 项目,无需大版本升级的企业
Spring Authorization Server Spring 官方主推,替代旧版 OAuth2,支持 OAuth2.1,持续维护 学习曲线陡,资料相对较少,对 Spring Boot 版本要求高 新项目,使用 Spring Boot 3.x,需要长期维护的系统
Keycloak 开源成熟的认证中间件,开箱即用,功能完善,支持多种协议 定制化成本高,运维复杂度高,不适合深度业务定制 标准化企业内部系统,无深度定制需求,快速落地
Sa-Token 国内开源轻量级权限框架,API 简单,上手快,中文文档完善 生态不如 Spring Security 完善,跨语言支持弱 中小型项目,快速开发,无需严格遵循 OAuth2 协议

7.2 不同规模项目的落地建议

  1. 小型创业项目 / 内部系统:优先选择 Sa-Token,快速开发,降低学习成本,满足基本的权限需求即可。
  2. 中大型企业微服务项目:优先选择 Spring Security OAuth2/Spring Authorization Server,标准化协议,扩展性强,满足复杂的业务需求和合规要求。
  3. 集团化多系统项目:优先选择 Keycloak 等成熟的认证中间件,统一管控多系统的认证授权,减少重复开发。

7.3 未来架构演进方向

随着零信任架构的普及,微服务的认证授权架构也在不断演进,未来的核心方向是:

  1. 零信任架构:默认不信任任何内部和外部的请求,每次请求都需要做完整的身份认证和权限校验
  2. 统一身份治理:打通企业内部所有系统的身份体系,实现统一身份认证、统一权限管控、统一审计日志
  3. 多因素认证(MFA):除了用户名密码,增加短信、邮箱、人脸识别等多因素认证,提升安全性
  4. 云原生适配:适配 K8s 容器化部署,实现认证服务的弹性扩缩容,适配 Service Mesh 网格架构

八、总结

这篇博文,我们从微服务认证授权的痛点出发,深入拆解了 OAuth2 和 JWT 的核心原理,从零搭建了完整的 SpringCloud 统一认证授权体系,讲解了生产级的进阶优化方案,还有我这些年踩过的 10 + 个核心坑的解决方案。

如果这篇文章对你有帮助,麻烦点赞、收藏、关注三连,后续我会持续更新 SpringCloud 微服务架构的更多实战内容,包括分布式事务、服务治理、链路追踪、高可用架构等。大家有任何问题,都可以在评论区留言,我会一一回复。

相关推荐
用户3983461612018 小时前
Go-Spring 实战第 11 课 —— 依赖注入的目标:单 Bean 注入和集合注入
spring·go
JAVA面经实录91718 小时前
完整版Spring全家桶学习体系
java·spring boot·spring·面试
架构源启19 小时前
Spring AI进阶系列(09) 工作流引擎设计:LangGraph风格编排、条件分支与并行执行实战
java·人工智能·spring
未若君雅裁19 小时前
RabbitMQ 消息可靠性:生产者确认、持久化、消费者ACK与幂等消费
分布式·微服务·rabbitmq
_Aaron___19 小时前
Spring AI 2.0 之后,MCP Server 该按远程企业服务来设计
java·人工智能·spring
消失的旧时光-194319 小时前
企业认证与安全体系(三):一篇讲透 JWT 原理与企业级实践
安全·jwt·登录鉴权
多加点辣也没关系20 小时前
Spring MessageSource 国际化方案
java·后端·spring
Devin~Y20 小时前
大厂Java面试实录:Spring Boot/Cloud、Redis+Kafka、JVM调优与RAG/Agent(Spring AI)三轮递进问答
java·jvm·spring boot·redis·spring cloud·kafka·rag
huipeng9261 天前
企业级微服务开发实战(一):项目启动与工程化设计
java·开发语言·spring boot·spring cloud·微服务·云原生·架构