SpringCloudOAuth2JWT微服务统认证方案
SpringCloudOAuth2+JWT:微服务统⼀认证方案
[
『Java分布式系统开发:从理论到实践』征文活动
10w+人浏览
300人参与
](
)
一、概念
1.1 问题背景
在微服务架构下,传统的单体应用Session方案(将用户状态存储在服务器内存或Redis中)面临巨大挑战:
- 状态持久化问题:每个微服务都是无状态的,Session难以共享和维护。
- 跨域问题:服务分散在不同域名下,Cookie携带困难。
- 性能瓶颈:每次请求都需要查询中央Session存储,增加网络开销和延迟。
Spring Cloud OAuth2 + JWT 的组合完美解决了这些问题:
- 无状态 (Stateless):JWT自包含所有信息,服务端无需存储会话信息。
- 单点登录 (SSO):一个认证中心,所有微服务(资源服务器)都信任其颁发的Token。
- 解耦与集中管理:认证和授权逻辑被抽离到专门的认证服务中,各业务微服务只需关注自身业务。
- 安全与灵活性:OAuth2提供了完善的授权流程,JWT提供了轻量级且安全的令牌格式。
1.2 概念
首先,这个方案是由三个核心概念组成的:
- 微服务架构:一种将单个应用程序拆分为一组小型、独立服务的设计风格。每个服务运行在自己的进程中,服务间通过轻量级机制(如 HTTP API)通信。
- OAuth 2.0:一个授权框架,而非认证协议。它专注于解决第三方应用在用户的授权下,安全地访问用户资源的问题。它定义了四种授权模式,最常用的是密码模式(用于第一方可信应用)和授权码模式(用于第三方应用)。
- JWT:一种令牌格式。全称是 JSON Web Token,它是一种紧凑的、URL安全的方式,用于在双方之间传输信息。这些信息可以被验证和信任,因为它是数字签名的。
Spring Cloud OAuth2 是 Spring 官方对 OAuth 2.0 协议的实现,它极大地简化了在 Spring Boot/Cloud 应用中构建授权服务器和资源服务器的过程。
因此,这个方案的核心是:使用 Spring Cloud OAuth2 构建符合 OAuth 2.0 标准的授权服务器,并让其颁发 JWT 格式的令牌(Access Token)。
Spring Cloud OAuth2 + JWT 是微服务架构下构建安全、高效、统一认证授权体系的黄金标准。它通过中心化认证、分布式校验的模式,完美契合了微服务的分布式、无状态特性。虽然在令牌管理上存在一些挑战(如注销),但通过合理的设计(短有效期、黑名单等)可以有效规避。
1.3 核心价值与优势
无状态性与解耦
- 传统痛点:在单体应用中,通常使用 Session 机制。用户登录后,服务器会创建一个 Session 并将 Session ID 通过 Cookie 返回给客户端。服务器需要存储这个 Session 信息,在集群环境下需要做 Session 同步,增加了复杂度和耦合度。
- 解决方案:JWT 是自包含的,所有必要的用户身份和权限信息都存储在令牌本身。资源服务器只需要验证 JWT 的签名并解析其内容即可获取用户信息,无需每次都向授权服务器或数据库查询。这使得服务彻底无状态,易于水平扩展。
安全的单点登录与统一认证
- 传统痛点:在微服务体系中,有几十甚至上百个服务。用户不可能在每个服务上都登录一次。
- 解决方案:建立一个统一的授权服务中心。用户只需在授权中心登录一次,即可获得一个 Access Token。凭借这个 Token,用户就可以访问所有其它微服务(资源服务器)。这完美实现了单点登录。
精细化的授权管理
- OAuth 2.0 的核心是授权,它通过 Scope 的概念来控制客户端(如 Web前端、移动APP)的访问权限范围(例如:只能读信息、可以写信息等)。
- JWT 的 payload 中可以自定义声明,通常我们会把用户的角色信息放入其中。资源服务器可以根据角色信息进行更细粒度的访问控制。
性能与可扩展性
- 由于 JWT 的自包含特性,资源服务器本地验证令牌即可,避免了大量的网络IO(如查询数据库或远程调用认证服务)。这在海量请求下极大地减轻了授权服务器的压力,提升了系统整体性能。
多客户端支持
- OAuth 2.0 协议原生支持多种客户端类型,如Web应用、浏览器、手机APP、IoT设备等。一套认证授权体系可以服务于所有前端平台。
1.3 功能边界
它能做/负责的:
- 身份认证:验证用户是谁(通过用户名密码、短信码等)。
- 访问授权:颁发令牌,并定义该令牌的访问范围和作用域。
- API保护:校验访问资源服务的令牌是否有效、是否过期、是否有权访问。
- 单点登录:作为整个系统的认证中心。
它不能做/不擅长的:
用户管理:用户的注册、资料维护、密码重置等业务功能。它依赖一个“用户详情服务”来获取用户信息。
权限模型的设计:权限模型是RBAC(角色基于访问控制)还是ABAC(属性基于访问控制)?这属于业务设计范畴,方案只提供了实现工具(如将角色放入JWT)。
JWT令牌的注销:这是JWT的一个著名缺点。由于无状态,服务器无法在颁发后直接让单个JWT失效。通常的解决方案是:
- 设置较短的令牌有效期,并配合使用 Refresh Token 来更新。
- 使用令牌黑名单,但这又引入了状态存储,部分违背了无状态的初衷。
传输层安全:它不保证传输过程中的安全。必须使用 HTTPS 来加密传输令牌,防止令牌被窃取。
防止令牌泄露:如果 Access Token 被泄露,攻击者可以在有效期内冒充用户。因此需要妥善保管令牌。
1.4 应用场景
这个方案几乎适用于所有中大型的、基于 Spring Cloud 技术栈的分布式系统。
企业级后台管理系统
- 多个后台服务(用户管理服务、订单服务、库存服务等)需要统一的登录入口和权限控制。管理员登录一次,即可在各个子系统间切换。
前后端分离应用
- 前端(Vue/React/Angular) + 后端多个微服务API。前端应用使用密码模式或授权码模式获取 JWT,之后在调用所有 API 时在 HTTP Header 中携带该 Token。
第三方应用授权
- 允许用户使用本系统的账号授权登录第三方网站或APP(类似“微信登录”)。这是 OAuth 2.0 最原始的目的,使用授权码模式。
移动端APP
- APP 启动后引导用户登录,获取 Token 后保存在本地,后续所有请求都携带此 Token。
服务与服务之间的内部调用
- 虽然更推荐使用更简单的认证方式(如基于API Gateway的密钥),但也可以使用 OAuth 2.0 的客户端凭证模式,为每个微服务客户端颁发令牌,用于服务间的认证。
二、原理
2.1 核心概念解析
2.1.1 OAuth 2.0 (Open Authorization 2.0)
概念:一个开放的授权框架标准(注意,它不是认证协议,但常被用于构建认证方案)。
核心问题:解决第三方应用在用户的授权下,安全地访问该用户在服务提供商的受保护资源的问题。例如,用微信登录某个论坛、授权滴滴访问你的微信头像。
四种授权模式:
- 授权码模式 (Authorization Code):最安全、最常用,适用于有后端的Web应用。
- 密码模式 (Resource Owner Password Credentials):用户把用户名密码直接交给客户端(受信任的官方应用),客户端用其换取Token。在我们的微服务架构中,内部系统通常采用此模式。
- 隐藏式 (Implicit):适用于纯前端SPA应用,无后端。
- 客户端凭证 (Client Credentials):适用于机器对机器的认证,比如微服务之间的调用。
摒弃密码模式: 在新项目中,即使内部系统也应使用授权码模式或客户端凭证模式,安全性更高。
2.1.2 JWT (JSON Web Token)
概念: 一个开放的、紧凑的、自包含的(self-contained)标准,用于在网络各方之间安全地传输信息作为JSON对象。
结构: 由三部分组成,用点符号**
.
**连接:Header.Payload.Signature
- Header:通常包含令牌类型(如JWT)和使用的签名算法(如HMAC SHA256或RSA)。
- Payload:包含声明(Claims),即关于实体(通常是用户)和其他数据的语句。常见的声明有
sub
(用户ID)、exp
(过期时间)、authorities
(权限列表)等。 - Signature:对前两部分的签名,用于验证消息在传递过程中是否被篡改,以及验证令牌的发行者。
关键特性:自包含
- 传统Session方案中,服务端需要存储Session信息,每次请求需要查询Session存储来验证用户状态。
- JWT无需服务端存储。所有必要信息(用户ID、权限等)都直接存储在令牌本身的Payload里。服务端只需用密钥验证签名有效,即可信任其中的内容。这使得它非常适合无状态的分布式系统。
JWT 令牌的流转:
- 用户登录,客户端携带用户名密码请求 认证服务 的
/oauth/token
端点。 - 认证服务验证通过,生成包含用户信息和权限的 JWT 令牌并返回。
- 客户端将 JWT 令牌存储在本地(如 LocalStorage/Cookie),并在后续请求的
Authorization
Header 中携带(Bearer <token>
)。 - 网关 拦截请求,验证 JWT 签名和有效性,并转发请求到具体的业务服务。
- 业务服务 从请求头中获取 JWT,解析出用户信息,处理业务逻辑并返回结果。
- 用户登录,客户端携带用户名密码请求 认证服务 的
2.2 架构与核心组件
下面是每个核心组件的详细职责:
认证授权服务 (Auth Service):这是 OAuth2 体系中的认证服务器(Authorization Server),是系统的安全核心。
- 职责:负责验证用户身份(如用户名/密码)、客户端身份(如
client_id
,client_secret
),并颁发 JWT 格式的 Access Token 和 Refresh Token。 - 关键依赖:
spring-cloud-starter-oauth2
,spring-security
,JWT
库(如jjwt
)。
- 职责:负责验证用户身份(如用户名/密码)、客户端身份(如
API 网关 (Gateway Service):作为系统的唯一入口,是所有客户端请求的统一流量入口和第一道安全防线。
- 职责:拦截所有外部请求,进行 Token 的统一校验(签名、有效期)、路由转发到正确的微服务,并可能承担权限预检、限流等功能。
- 关键依赖:
spring-cloud-starter-gateway
或Zuul
。
资源服务 (Resource Services):即各个业务微服务(如用户服务、订单服务),是 OAuth2 体系中的资源服务器(Resource Server)。
- 职责:处理具体的业务逻辑。它们信任认证服务颁发的 JWT。网关校验通过后,会将解析后的 JWT 中的用户信息(如用户名、权限)传递给资源服务,资源服务据此进行业务处理和可能的更细粒度的权限控制。
- 关键依赖:
spring-security-oauth2-resource-server
或spring-security-oauth2-jose
。 - 关键原则:安全相关的逻辑只存在于认证服务和网关服务中,其他服务无安全逻辑,只是单纯地提供服务。
客户端 (Client):请求资源的终端,如 Web 前端、手机 App、小程序等。
- 职责:获取用户的认证凭证,向认证服务请求 Token,并在后续请求中在 HTTP Header(通常是
Authorization: Bearer <token>
)中携带 JWT Token。
- 职责:获取用户的认证凭证,向认证服务请求 Token,并在后续请求中在 HTTP Header(通常是
在已经使用API网关进行统一鉴权的情况下,资源服务中仍需安全配置:
维度 | API网关 (前台保安) | 资源服务 (办公室门禁) |
---|---|---|
主要职责 | 入口级检查、路由转发、流量控制 | 业务级权限控制、数据权限 |
检查内容 | Token是否有效、是否过期 | 用户是否有权限执行特定操作 |
验证方式 | 验签、过期检查、黑名单检查 | 角色/权限验证、业务规则验证 |
失败响应 | 直接返回401/403,不会转发请求 | 可返回更具体的业务错误信息 |
用一个比喻来解释:API网关就像大厦前台的保安,而资源服务的安全配置就像各个办公室的门禁系统。
2.3 核心原理
2.3.1 获取 JWT 令牌 (Authentication)
请求:客户端(如前端APP)向认证服务器的 /oauth/token
端点发起POST请求(通常基于密码模式)。
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&
username=user&
password=pass&
client_id=client_app_id&
client_secret=client_app_secret
认证服务器处理:
验证
client_id
和client_secret
的有效性。验证
username
和password
的有效性(委托给UserDetailsService
)。验证通过后,生成JWT:
- 创建Payload(包含标准声明如
sub
,exp
,iat
和自定义声明如user_name
,authorities
)。 - 用配置的密钥(如RSA私钥或HMAC密钥) 对Header和Payload进行签名,生成Signature。
- 组合成最终的JWT字符串。
- 创建Payload(包含标准声明如
将JWT作为响应返回给客户端。
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 43199,
"scope": "read write"
}
2.3.2 访问受保护资源 (Authorization)
请求:客户端在后续请求的Header中携带JWT。
GET /orders/123
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
资源服务器处理:
拦截请求:
OAuth2AuthenticationProcessingFilter
会拦截携带Bearer Token的请求。提取并验证JWT:
- 解密JWT的Header,获取签名算法。
- 使用与认证服务器约定好的密钥(如RSA公钥或相同的HMAC密钥)对Header和Payload部分重新计算签名。
- 将计算出的签名与JWT中的Signature进行对比。如果一致,证明Token是合法且未被篡改的。
- 检查过期时间 (
exp
) 等声明。
构建安全上下文:解析JWT Payload中的用户信息和权限信息(如
user_name
,authorities
),并将其填充到SecurityContextHolder
中,供后续业务逻辑使用(如@PreAuthorize("hasRole('ADMIN')")
)。执行业务逻辑:至此,资源服务器已经完全信任JWT中的身份,开始执行具体的Controller逻辑。
三、使用
3.1 使用步骤
基于SpringCloud OAuth2、JWT、Spring Gateway和MySQL搭建统一认证和授权方案,能有效管理微服务架构下的安全访问和流量控制。
3.1.1 环境准备与依赖配置
创建Spring Cloud项目:使用Spring Initializr创建父工程,然后创建认证服务(如 auth-service
)、网关服务(如 api-gateway
)和业务微服务(如 user-service
)。
添加依赖:在各服务的 pom.xml
中添加必要依赖。
- 认证服务 (
auth-service
):需要Spring Security OAuth2
、JWT
、Spring Data JPA
、MySQL驱动
等。
<!-- OAuth2 授权服务器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId> <!-- 处理JOSE协议,包含JWT -->
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 网关服务 (
api-gateway
):需要Spring Cloud Gateway
、Spring Security OAuth2 资源服务器
、Spring Boot Data Redis Reactive
(用于限流)等。
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- OAuth2 资源服务器 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- Spring Security JWT (用于资源服务器解析JWT) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<!-- 响应式Redis依赖 (用于限流) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
- 业务微服务 (如
user-service
):需要Spring Web
、Spring Security OAuth2 资源服务器
(如果服务自身也想解析JWT进行细粒度控制)或其他安全框架(如Spring Security、Shiro)。若只想接收网关转发来的用户信息,可考虑不直接引入安全框架。
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 若业务服务需要自行解析JWT进行细粒度鉴权,则添加资源服务器依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
3.1.2 搭建OAuth2认证服务 (auth-service
)
认证服务负责用户身份验证和JWT令牌的颁发。
- 配置数据源与JPA:在
application.yml
中配置MySQL数据库连接、JPA以及OAuth2客户端信息(也可存入数据库)。
spring:
datasource:
url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
- 配置OAuth2授权服务器:创建一个配置类
AuthorizationServerConfig
,继承AuthorizationServerConfigurerAdapter
。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager; // 需要配置AuthenticationManager
@Autowired
private DataSource dataSource;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer jwtTokenEnhancer; // 自定义Token增强器,用于在JWT中添加额外信息
// 配置客户端详情信息(可以基于内存或JDBC)
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource); // 从数据库读取客户端配置
// 内存示例:
// clients.inMemory()
// .withClient("client_app")
// .secret(passwordEncoder.encode("123456")) // 客户端密码需加密
// .scopes("read", "write")
// .authorizedGrantTypes("password", "refresh_token")
// .accessTokenValiditySeconds(3600)
// .refreshTokenValiditySeconds(86400);
}
// 配置令牌端点(Token Endpoint)的安全约束与认证管理器
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore()) // 使用JWT令牌存储,Token不会直接存储,JWT的特性
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain);
}
// 配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()"); // 允许已认证的请求检查令牌
security.allowFormAuthenticationForClients(); // 允许客户端使用表单认证
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter()); // 使用JWT存储令牌
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("your-secret-key"); // 设置签名密钥(HS256),生产环境请使用强密码或RSA密钥对
// 使用RSA:converter.setKeyPair(keyPair());
return converter;
}
// 可选:自定义Token增强器,向JWT中添加额外信息(如用户ID、更多权限详情等)
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> additionalInfo = new HashMap<>();
// 添加自定义声明
// additionalInfo.put("user_id", ((UserDetails) authentication.getPrincipal()).getSomeId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
}
- 配置Spring Security:定义用户详情服务(
UserDetailsService
)和密码编码器。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService; // 需要实现自己的UserDetailsService
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
// 其他安全配置,如放行oauth/token端点等
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll() // 放行认证端点
.anyRequest().authenticated();
}
}
- 实现
UserDetailsService
接口,从数据库加载用户信息。
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository; // 假设你的UserRepository
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
// 假设你的User实体实现了UserDetails,或者你需要在这里构建一个UserDetails对象
// 这里需要返回包括用户名、密码、权限等信息的UserDetails对象
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword()) // 数据库中的密码应该是加密后的
.authorities(getAuthorities(user)) // 从用户对象中获取权限/角色
.build();
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// 根据你的用户权限/角色结构返回GrantedAuthority集合
// 例如:return user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
return Collections.emptyList(); // 示例
}
}
3.1.3 配置API网关 (api-gateway
)
网关负责路由转发、JWT验证、限流等。
- 配置路由与JWT资源服务器:在
application.yml
中配置路由规则和JWT公钥(如果使用RSA)或签名密钥(如果使用HS256)。
spring:
cloud:
gateway:
routes:
- id: user-service-route
uri: lb://user-service # 假设已集成服务发现(如Nacos)
predicates:
- Path=/api/users/**
filters:
- name: RequestRateLimiter # 限流过滤器
args:
redis-rate-limiter.replenishRate: 10 # 每秒产生的令牌数
redis-rate-limiter.burstCapacity: 20 # 令牌桶总容量
key-resolver: "#{@userKeyResolver}" # 限流键解析器Bean(按用户限流)
- StripPrefix=1 # 去掉路径的第一部分(例如 /api)
- id: auth-service-route # 认证服务路由,通常获取token的端点需要暴露,也可直接不经过网关
uri: lb://auth-service
predicates:
- Path=/oauth/token
# 注意:获取token的端点通常不需要JWT验证,网关安全配置需要放行
security:
oauth2:
resourceserver:
jwt:
secret-key: your-secret-key # HS256的密钥,与认证服务一致
# 如果使用RSA:
# public-key-location: classpath:public.key
# 或者通过issuer-uri从认证服务发现jwks端点(推荐)
# issuer-uri: http://auth-service
redis:
host: localhost
port: 6379
database: 0
- 配置网关安全:创建一个配置类,定义Spring Security规则,保护除认证端点外的其他路由。
@Configuration
@EnableWebFluxSecurity // 注意是WebFlux
public class GatewaySecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/oauth/token").permitAll() // 放行获取token的端点
.anyExchange().authenticated() // 其他所有请求都需要认证
)
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt) // 启用JWT资源服务器
.csrf().disable();
return http.build();
}
}
Spring Cloud Gateway会自动验证JWT的有效性和过期时间。
- 实现限流配置:创建一个按用户(从JWT中获取 subject)限流的键解析器。
@Configuration
public class RateLimiterConfig {
@Bean
KeyResolver userKeyResolver() {
return exchange -> {
// 从SecurityContext中获取认证信息,其中包含JWT解析后的内容
return exchange.getPrincipal()
.map(Principal::getName) // 获取用户名(JWT的subject)
.defaultIfEmpty("anonymous"); // 如果没有认证用户,使用"anonymous"
};
}
}
- (可选) 实现黑名单/令牌失效:可以创建一个全局过滤器,检查JWT是否在黑名单(如Redis中)以实现注销失效。
@Component
public class JwtBlacklistFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String jwtToken = authHeader.substring(7);
// 从JWT中解析出jti (JWT ID) 或其他唯一标识
// 这里需要解析JWT,可以使用JwtHelper(注意:资源服务器已经解析过,避免重复解析,可以考虑在之前的过滤器中设置属性)
// 简单示例:检查jti是否在黑名单Set中
String jti = parseJtiFromToken(jwtToken); // 需要实现parseJtiFromToken方法
if (jti != null && redisTemplate.hasKey("jwt_blacklist:" + jti)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
return chain.filter(exchange);
}
private String parseJtiFromToken(String jwtToken) {
// 使用JwtHelper解析JWT,获取claims,然后取出jti声明
// 注意:这只是示例,Gateway的OAuth2资源服务器已经验证并解析了JWT,可以考虑在其后添加此过滤器,并从SecurityContext获取信息避免重复解析。
try {
String[] splitToken = jwtToken.split("\\.");
String claimsStr = new String(Base64.getUrlDecoder().decode(splitToken[1]));
Map<String, Object> claims = new ObjectMapper().readValue(claimsStr, Map.class);
return (String) claims.get("jti");
} catch (Exception e) {
return null;
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1; // 在认证过滤器之后执行
}
}
3.1.4 实现业务微服务 (user-service
)
业务微服务处理具体业务,并进行更细粒度的权限控制。
- 配置JWT资源服务器(可选):如果微服务想自行解析JWT进行细粒度鉴权,需要在
application.yml
中配置与网关相同的JWT设置。
spring:
security:
oauth2:
resourceserver:
jwt:
secret-key: your-secret-key # 与认证服务、网关一致
# issuer-uri: http://auth-service # 或者使用issuer-uri
并在安全配置中启用资源服务器:
@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/users/me").hasAuthority("SCOPE_profile") // 示例:需要scope
.antMatchers(HttpMethod.GET, "/users/**").hasRole("USER") // 示例:需要角色
.antMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
- 从JWT中获取用户信息:在控制器中,可以使用
@AuthenticationPrincipal
注入JWT解析后的主体(通常是用户名或一个Jwt
对象)。
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/me")
public String getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
// 从JWT claims中获取信息
String username = jwt.getSubject();
String customClaim = (String) jwt.getClaims().get("custom_claim");
return "Hello, " + username + ". Your custom claim: " + customClaim;
}
// 或者如果主体是用户名字符串
// public String getCurrentUser(@AuthenticationPrincipal(expression = "subject") String username) { ... }
}
- 实现业务逻辑权限校验:在Service层或自定义切面中,根据业务规则进行更复杂的权限判断。
- 统一异常处理与返回格式:使用
@RestControllerAdvice
和@ExceptionHandler
捕获异常,并返回统一的、更具体的业务错误信息格式。
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ResultData<?>> handleBusinessException(BusinessException e) {
// ResultData 是你定义的统一返回体
ResultData<?> resultData = ResultData.fail(e.getErrorCode(), e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(resultData); // 或其他合适的状态码
}
// 处理访问拒绝(权限不足)
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ResultData<?>> handleAccessDeniedException(AccessDeniedException e) {
ResultData<?> resultData = ResultData.fail("403", "没有权限执行此操作");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(resultData);
}
// 处理其他异常...
@ExceptionHandler(Exception.class)
public ResponseEntity<ResultData<?>> handleOtherException(Exception e) {
ResultData<?> resultData = ResultData.fail("500", "系统内部错误: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(resultData);
}
}
// 统一的返回结果类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultData<T> {
private String code;
private String message;
private T data;
private long timestamp = System.currentTimeMillis();
public static <T> ResultData<T> success(T data) {
return new ResultData<>("200", "成功", data, System.currentTimeMillis());
}
public static <T> ResultData<T> fail(String code, String message) {
return new ResultData<>(code, message, null, System.currentTimeMillis());
}
}
3.1.5 JWT 的最佳实践与增强安全
- 合理设置 Token 有效期:Access Token 设置较短的有效期(如 15-30 分钟),Refresh Token 设置较长的有效期(如 7 天)。使用 Refresh Token 来获取新的 Access Token,减少 Access Token 泄露的风险。
- 使用非对称加密 (RS256):避免对称加密密钥共享带来的风险。
- 避免在 JWT Payload 中存放敏感信息:因为 Payload 只是 Base64 编码,并非加密。
- 实现 Token 黑名单/注销机制:虽然 JWT 本身是无状态的,但如果需要实现用户注销使 Token 立即失效,可以维护一个黑名单(如使用 Redis)。认证服务在注销时将未过期的 Token 加入黑名单,网关和资源服务在验证 Token 时需检查黑名单(这会引入一定的状态,需权衡)。
- 强制使用 HTTPS:防止 Token 在传输过程中被窃取。
- 密钥管理是生命线: 尤其是私钥,绝不能硬编码在代码中或提交到Git。应使用配置中心、环境变量或K8s Secret等方式管理。
- 保护好你的端点: 认证服务器的端点(如
/oauth/authorize
,/oauth/token
)本身也需要做好安全防护。 - 自定义登录接口:默认的 Token 端点 (
/oauth/token
) 可能不符合你的 RESTful 风格或项目需求。你可以通过配置覆盖它:
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.pathMapping("/oauth/token", "/api/auth/login");
}
监控和日志: 详细记录令牌的颁发和验证日志,便于审计和排查问题。
新项目技术选型注意:
- Spring官方已不再积极开发
spring-security-oauth2
,并推出了新的 Spring Authorization Server 项目来替代它。 - 新项目强烈建议直接采用 Spring Authorization Server。它功能更强大、符合最新规范、并且与Spring生态集成更好。需要学习的是新的配置方式(如
RegisteredClientRepository
等)。
- Spring官方已不再积极开发
3.2 配置项详解
在 Spring Cloud OAuth2 (Spring Authorization Server 或已停更的 Spring Security OAuth2) 中,配置主要围绕 @EnableAuthorizationServer
, @EnableResourceServer
等注解以及相关的 **Configurer
**类进行。JWT 作为令牌格式,其配置主要与 JwtAccessTokenConverter
和 TokenStore
相关。
**授权服务器 (Authorization Server) 配置:**授权服务器负责对用户身份进行认证,并颁发访问令牌 (Access Token) 和刷新令牌 (Refresh Token)。
配置类别 | 具体配置项 | 说明/可选值 | 示例或默认值 |
---|---|---|---|
客户端详情 | clientId | OAuth2 客户端标识符,必填 | "web-app" |
ClientDetailsServiceConfigurer | clientSecret | 客户端密钥,必填(对于 confidential 客户端) | "{bcrypt}... " |
scope | 客户端申请的权限范围 | ["read", "write"] | |
authorizedGrantTypes | 支持的授权模式 | ["authorization_code", "password", "refresh_token", "implicit"] | |
authorities | 客户端自身的权限(如 ROLE_CLIENT) | ["ROLE_CLIENT"] | |
redirectUris | 授权码模式中,认证后重定向的 URI | ["https://myapp.com/login"] | |
accessTokenValiditySeconds | 访问令牌有效期(秒) | 3600 (1小时) | |
refreshTokenValiditySeconds | 刷新令牌有效期(秒) | 2592000 (30天) | |
autoApprove | 是否自动批准特定 scope 的授权 | true 或 "read" | |
令牌管理 | tokenStore | 令牌存储方式,使用 JWT 时设置为 JwtTokenStore | new JwtTokenStore(jwtAccessTokenConverter()) |
AuthorizationServerEndpointsConfigurer | tokenEnhancer | 令牌增强器,用于生成 JWT | jwtAccessTokenConverter() |
authenticationManager | 密码模式需要注入的认证管理器 | @Autowired AuthenticationManager | |
userDetailsService | 用于刷新令牌和密码模式时加载用户信息 | @Autowired UserDetailsService | |
authorizationCodeServices | 授权码的存储方式(内存或数据库) | new InMemoryAuthorizationCodeServices() | |
tokenServices | 定义令牌服务的属性,如是否支持刷新令牌 | DefaultTokenServices | |
端点URL | pathMapping | 自定义认证、令牌等端点的 URL | .pathMapping("/oauth/token", "/api/auth/token") |
AuthorizationServerEndpointsConfigurer | |||
JWT 特定配置 | signingKey | 对称加密的签名密钥(HS256) | "my-secret-key" |
JwtAccessTokenConverter | keyPair | 非对称加密的密钥对(RS256) | keyPair() (从 keystore 加载) |
verifierKey | 非对称加密时,用于资源服务器验证的公钥 | "-----BEGIN PUBLIC KEY-----..." | |
accessTokenConverter | 自定义 JWT 与 OAuth2 认证信息之间的转换逻辑 | new CustomAccessTokenConverter() | |
其他安全配置 | allowFormAuthenticationForClients | 是否允许客户端使用表单认证(否则为 Basic Auth) | true |
AuthorizationServerSecurityConfigurer | tokenKeyAccess | 定义访问 /oauth/token_key 端点的权限(获取公钥) | "permitAll()" 或 "isAuthenticated()" |
checkTokenAccess | 定义访问 /oauth/check_token 端点的权限(验证令牌) | "isAuthenticated()" |
**资源服务器 (Resource Server) 配置:**资源服务器提供受保护的 API 资源,负责验证访问令牌并授权。
配置类别 | 具体配置项 | 说明/可选值 | 示例或默认值 |
---|---|---|---|
资源ID | resourceId | 资源标识符,需与令牌中 aud 声明匹配(可选) | "order-service" |
ResourceServerConfigurer | |||
令牌服务 | tokenServices | 定义如何解析和验证令牌(RemoteTokenServices 或 DefaultTokenServices) | 通常注入与授权服务器相同的 TokenStore |
ResourceServerConfigurer | tokenStore | 推荐方式:直接注入 TokenStore (如 JwtTokenStore)来本地验证 JWT | @Autowired TokenStore |
JWT 验证配置 | signingKey | 对称加密时,设置与授权服务器相同的签名密钥 | "my-secret-key" |
JwtAccessTokenConverter | verifierKey | 非对称加密时,设置授权服务器的公钥 | "-----BEGIN PUBLIC KEY-----..." |
jwt | 直接提供一个 JwtDecoder (Spring Security 5.x+) | NimbusJwtDecoder.withPublicKey(publicKey).build() | |
访问规则 | antMatchers(...).permitAll() | 配置不需要认证的公开端点 | .antMatchers("/api/public/**").permitAll() |
HttpSecurity | antMatchers(...).hasRole("ADMIN") | 配置需要特定权限才能访问的端点 | .antMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN") |
antMatchers(...).authenticated() | 配置任何认证用户都可访问的端点 | .anyRequest().authenticated() | |
access | 使用 SpEL 表达式进行复杂的权限控制 | .access("@permissionService.check(authentication, request)") |
**客户端 (Client Application / SPA) 配置:**客户端是请求访问受保护资源的应用(如前端 Vue/React 应用、移动 App、其他服务)。
配置类别 | 具体配置项 | 说明/可选值 | 示例或默认值 |
---|---|---|---|
客户端信息 | client-id | 在授权服务器注册的 clientId | "web-app" |
(application.yml ) | client-secret | 在授权服务器注册的 clientSecret | "secret" |
scope | 申请的权限范围 | "read write" | |
认证服务器信息 | access-token-uri | 获取 Token 的端点地址 | http://auth-server/oauth/token |
(application.yml ) | user-authorization-uri | 获取授权码的端点地址(授权码模式) | http://auth-server/oauth/authorize |
用户信息 | user-info-uri | 获取当前用户信息的端点地址 | http://auth-server/user/me |
(application.yml ) | pre-established-redirect-uri | 预配置的重定向 URI | https://myclient.com/login |
令牌处理 | jwt.key-value | (如果使用对称加密)配置签名密钥以本地解析 JWT | my-secret-key |
jwt.key-uri | (如果使用非对称加密)配置获取公钥的端点 | http://auth-server/oauth/token_key |
网关 (API Gateway) 配置:网关作为所有流量的入口,通常承担着统一鉴权和令牌中继的角色。
配置类别 | 具体配置项 | 说明/可选值 | 示例(如 Spring Cloud Gateway) |
---|---|---|---|
路由规则 | predicates | 定义哪些请求需要经过认证/转发到认证服务 | - Path=/api/** |
(application.yml ) | filters | 在过滤器中进行令牌校验和中继 | - StripPrefix=1 |
令牌校验 | 自定义全局过滤器 | 在网关层统一验证 JWT 的有效性和权限,避免无效请求打到后端服务 | 实现 GlobalFilter , 使用 ReactiveJwtDecoder 解析 JWT |
令牌中继 | 自定义全局过滤器 | 将解析后的 JWT 中的身份信息(如用户名、权限)以 HTTP Header 形式转发给下游微服务 | 在 ServerWebExchange 的请求中添加 X-User-Name , X-User-Authorities 等 Header |
白名单 | whitelist | 配置直接放行、不需要认证的路由(如登录、注册、静态资源) | 在自定义过滤器的逻辑中排除 /auth/login , /public/** 等路径 |