
今天这篇博文,我会从底层原理到生产落地,把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 点:
- 标准化协议,无技术锁定:OAuth2.0 是行业通用的授权协议,所有主流语言和平台都支持,后续架构扩容、跨语言服务对接都没有障碍
- 无状态设计,完美适配微服务:JWT 令牌自包含用户身份和权限信息,服务端无需存储会话信息,天然适配分布式、容器化部署,水平扩展无压力
- 深度整合 Spring 生态,扩展性极强:和 SpringCloud、SpringBoot 无缝兼容,支持自定义认证逻辑、权限模型、异常处理,能满足各种复杂的业务定制需求
- 安全能力完善: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% 会问这个点,大家一定要记牢:
- 授权码模式(Authorization Code):最安全、最主流的模式,适用于有后端服务的 Web 应用、APP。核心流程是先获取授权码,再通过授权码换取令牌,令牌全程不暴露在前端,安全性最高。
- 密码模式(Resource Owner Password Credentials):用户把用户名密码直接交给客户端,客户端用凭证向授权服务器换取令牌。适用于公司内部的可信系统,比如企业内部管理后台。
- 客户端模式(Client Credentials):无用户参与,客户端以自己的名义向授权服务器申请令牌。适用于服务间调用、定时任务等机器与机器之间的通信场景。
- 简化模式(Implicit):直接在前端获取令牌,无需授权码环节。安全性极低,目前已不推荐使用,仅适用于纯前端静态页面,且无后端服务的极简单场景。
2.2 JWT 令牌结构与核心特性(避坑重点)
JWT 全称 JSON Web Token,是一种轻量级的、自包含的令牌格式,用于在网络环境中传递用户身份和权限信息。很多新手对 JWT 有误解,这里我把核心点讲透,避免踩坑。
2.2.1 JWT 的三段式结构
JWT 由三部分组成,用.分隔,完整格式为:header.payload.signature

我给大家逐段拆解:
-
Header(头部):Base64URL 编码,存储令牌类型和签名算法,示例:
bash{ "alg": "HS256", "typ": "JWT" }alg:签名算法,常用 HS256(对称加密)、RS256(非对称加密,生产环境推荐)typ:令牌类型,固定为 JWT
-
Payload(载荷):Base64URL 编码,存储核心数据,也是我们自定义用户信息的地方。分为三类声明:
- 注册声明:JWT 官方定义的标准字段,比如
iss(签发者)、exp(过期时间)、sub(主题)、jti(令牌唯一 ID) - 公共声明:自定义的通用字段,比如
userId、username、roles - 私有声明:业务自定义的特殊字段
划重点(踩坑无数):
- Payload 仅做 Base64 编码,没有加密 !任何人拿到令牌都能解码看到内容,严禁存储密码、手机号、身份证号等敏感信息
- Payload 内容越多,JWT 长度越长,会导致请求头过大,引发 Nginx 400 错误,只存核心非敏感信息
- 注册声明:JWT 官方定义的标准字段,比如
-
Signature(签名):整个 JWT 的核心安全保障,由编码后的 Header、Payload、密钥,通过 Header 指定的算法生成。
- 作用:验证令牌是否被篡改,只有持有密钥的服务端才能生成和校验签名,确保令牌的合法性
- 生产环境强烈推荐使用 RS256 非对称加密算法,私钥签发令牌,公钥校验令牌,避免对称密钥泄露导致的伪造令牌风险
2.2.2 JWT 的核心优势与局限性
| 优势 | 局限性 |
|---|---|
| 无状态:服务端无需存储会话信息,天然适配分布式集群 | 令牌一旦签发,过期前无法主动撤销,需额外方案解决 |
| 自包含:Payload 包含用户核心信息,减少数据库查询 | 无法控制令牌的使用范围,存在被盗用的风险 |
| 跨语言跨平台:基于 JSON 格式,所有主流语言都支持 | 加密算法配置不当,会引发严重的安全漏洞 |
| 适合微服务架构:网关层一次校验,全链路透传 | 不适合存储大量数据,会增加网络传输开销 |
2.3 Spring Security OAuth2 核心执行流程与组件
Spring Security OAuth2 的核心执行流程,本质上是 Spring Security 的过滤器链 + OAuth2 协议的结合,核心组件我给大家梳理清楚,后续写配置全靠这些:
- AuthenticationManager:Spring Security 的核心认证管理器,负责校验用户的身份凭证
- UserDetailsService:自定义用户信息加载接口,我们通过实现这个接口,从数据库加载用户信息和权限
- AuthorizationServerConfigurerAdapter:授权服务器配置核心类,负责配置客户端信息、令牌管理、授权端点
- ResourceServerConfigurerAdapter:资源服务器配置核心类,负责配置资源的访问规则、令牌校验规则
- TokenStore:令牌存储接口,负责令牌的生成、存储、校验,我们用 JwtTokenStore 实现 JWT 令牌的管理
- JwtAccessTokenConverter:JWT 令牌转换器,负责 JWT 的编码、解码、签名校验
- ClientDetailsService:客户端信息管理接口,负责加载 OAuth2 客户端的配置信息,比如客户端 ID、密钥、授权模式、权限范围
2.4 微服务统一认证授权的完整请求链路
这里我给大家画了完整的请求时序图,把整个认证授权的流程讲清楚,大家可以对照这个图理解后续的代码实现。

完整流程步骤:
- 客户端向用户发起授权请求,用户同意授权
- 客户端携带授权凭证,向认证中心(授权服务器)申请令牌
- 认证中心校验用户身份和授权凭证,通过后签发 JWT 令牌(access_token+refresh_token)
- 客户端携带 JWT 令牌,通过网关访问后端业务服务(资源服务器)
- 网关层校验 JWT 令牌的合法性,校验通过后,将用户信息透传到后端服务
- 资源服务器本地校验 JWT 签名,解析用户权限,判断是否有权限访问对应资源
- 权限校验通过,资源服务器返回业务数据;校验失败,返回 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) 实现无感刷新,核心设计思路:
- access_token:有效期设置为 30 分钟,用于接口访问,有效期短,降低被盗用的风险
- refresh_token:有效期设置为 7 天,仅用于刷新 access_token,不用于接口访问
- 无感刷新流程 :
- 前端检测到 access_token 过期(接口返回 401),自动携带 refresh_token 调用刷新令牌接口
- 认证中心校验 refresh_token 的合法性,通过后签发新的 access_token 和 refresh_token
- 前端用新的 access_token 重新发起请求,用户无感知
- 刷新令牌采用一次有效机制,每次刷新后,旧的 refresh_token 立即失效,避免被盗用
5.2 JWT 令牌吊销:黑名单机制实现
JWT 的另一个核心痛点是,令牌一旦签发,过期前无法主动撤销。比如用户修改密码、退出登录、被管理员封禁,已经签发的令牌仍然可以使用,存在严重的安全风险。
我们采用Redis 黑名单机制解决这个问题,核心实现方案:
- 每个 JWT 令牌都生成唯一的
jti(令牌 ID),存储在 Payload 中 - 用户退出登录、修改密码、被封禁时,将
jti存入 Redis 黑名单,过期时间设置为令牌的剩余有效期 - 网关层和资源服务器每次校验令牌时,先检查
jti是否在黑名单中,如果在,直接拒绝访问 - 优化方案:access_token 有效期设置为 5-15 分钟,即使出现安全问题,最多只有几分钟的风险窗口,平衡安全性和性能
5.3 细粒度权限控制:RBAC 模型深度整合
生产环境中,我们通常采用RBAC(基于角色的访问控制) 模型,实现细粒度的权限控制,核心设计:
- 用户 - 角色 - 权限三级模型:一个用户拥有多个角色,一个角色拥有多个权限
- 权限粒度:分为菜单权限、按钮权限、接口权限、数据权限
- 整合实现 :
- 用户登录时,从数据库加载用户的角色和权限列表,存入 JWT 的 Payload 中
- 资源服务器通过
@PreAuthorize注解,实现接口级的权限控制 - 网关层做权限初验,拦截无权限的请求
- 数据权限通过 MyBatis-Plus 的插件实现,根据用户角色动态拼接查询条件
5.4 高并发场景下的性能优化方案
在高并发场景下,认证中心会成为整个系统的瓶颈,我们可以通过以下方案优化性能:
- 本地验签为主:资源服务器和网关层本地校验 JWT 签名,无需每次都调用认证中心,减少认证中心的压力
- 用户权限缓存:将用户的角色和权限数据存入 Redis,减少数据库查询次数
- 令牌签发限流:对认证中心的令牌签发接口做限流,防止暴力破解和恶意请求
- 集群部署:认证中心多实例集群部署,通过 Nacos 实现负载均衡
- 非对称加密优化:生产环境使用 RS256 非对称加密,公钥分发给所有资源服务,私钥仅保存在认证中心,提升安全性的同时,减少密钥管理的复杂度
5.5 安全加固:防篡改、防重放、密钥轮换方案
生产环境的安全是重中之重,我们需要做以下安全加固:
- 防篡改:使用强签名算法,生产环境必须使用 RS256 非对称加密,严禁使用 HS256 对称加密,避免密钥泄露导致的令牌伪造
- 防重放攻击 :在 JWT 中加入
iat(签发时间)和jti(唯一 ID),对短时间内重复的请求做拦截 - HTTPS 传输:所有接口必须使用 HTTPS 传输,防止令牌在网络传输过程中被窃听
- 密钥轮换:定期更换签名密钥,避免长期使用同一个密钥导致的泄露风险,通过 Redis 同步公钥给所有资源服务
- 令牌存储安全:前端严禁将令牌存入 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 缓冲区默认大小。解决方案:
- Payload 只存核心非敏感信息,比如 userId、username、角色编码,不要存全量的权限列表
- 权限列表在资源服务本地通过 userId 查询,配合 Redis 缓存
- 调整 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 配置不对。解决方案:
- 请求头名称使用横杠 -,不要用下划线_
- 后端服务配置: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 不同规模项目的落地建议
- 小型创业项目 / 内部系统:优先选择 Sa-Token,快速开发,降低学习成本,满足基本的权限需求即可。
- 中大型企业微服务项目:优先选择 Spring Security OAuth2/Spring Authorization Server,标准化协议,扩展性强,满足复杂的业务需求和合规要求。
- 集团化多系统项目:优先选择 Keycloak 等成熟的认证中间件,统一管控多系统的认证授权,减少重复开发。
7.3 未来架构演进方向
随着零信任架构的普及,微服务的认证授权架构也在不断演进,未来的核心方向是:
- 零信任架构:默认不信任任何内部和外部的请求,每次请求都需要做完整的身份认证和权限校验
- 统一身份治理:打通企业内部所有系统的身份体系,实现统一身份认证、统一权限管控、统一审计日志
- 多因素认证(MFA):除了用户名密码,增加短信、邮箱、人脸识别等多因素认证,提升安全性
- 云原生适配:适配 K8s 容器化部署,实现认证服务的弹性扩缩容,适配 Service Mesh 网格架构
八、总结
这篇博文,我们从微服务认证授权的痛点出发,深入拆解了 OAuth2 和 JWT 的核心原理,从零搭建了完整的 SpringCloud 统一认证授权体系,讲解了生产级的进阶优化方案,还有我这些年踩过的 10 + 个核心坑的解决方案。
如果这篇文章对你有帮助,麻烦点赞、收藏、关注三连,后续我会持续更新 SpringCloud 微服务架构的更多实战内容,包括分布式事务、服务治理、链路追踪、高可用架构等。大家有任何问题,都可以在评论区留言,我会一一回复。