在微服务架构中,服务之间的调用往往需要验证请求方的身份,而OAuth2体系中的资源服务器(Resource Server)正是负责这件事的角色。它本身不发放令牌,只负责校验令牌的合法性,并决定当前请求是否有权访问受保护的接口。Spring Security从5.x开始提供了spring-security-oauth2-resource-server模块,配合Spring Boot的自动配置,整合过程比以往的老OAuth2 starter简洁得多。本文将以JWT令牌为例,完整演示整合流程,并深入讨论配置细节与常见问题。

一、核心概念与整体流程
先厘清几个容易混淆的角色。授权服务器负责认证用户并签发令牌,客户端持有令牌去请求业务接口,而资源服务器就是承载这些业务接口的一端。资源服务器拿到请求头中的Bearer Token后,需要完成两件事:一是验证令牌的签名是否有效、是否过期,二是根据令牌中的权限信息判断当前用户能否访问目标接口。很多项目把这三者的职责混在一起,导致安全逻辑分散在各处,后期难以维护。
Spring Security处理令牌校验的核心组件是BearerTokenAuthenticationFilter和JwtDecoder。前者从请求头提取令牌并构造一个未认证的BearerTokenAuthenticationToken,后者负责把字符串形式的JWT解析为带签名的Jwt对象。校验通过后,JwtAuthenticationConverter会把JWT中的Claim转换成Spring Security的Authentication对象,其中的GrantedAuthority集合就是后续授权判断的依据。理解这条链路,排查401或403问题时就不会无从下手。
需要注意的是,Resource Server默认只做认证,不做细粒度授权。也就是说,只要令牌合法,默认所有接口都能访问。如果要按角色或权限拦截,必须显式配置requestMatchers或者使用方法级注解,这一点在后面的配置部分会详细展开。
二、依赖引入与基础配置
整合的第一步是引入依赖。在pom.xml中添加spring-boot-starter-oauth2-resource-server即可,它已经包含了Spring Security和OAuth2 Resource Server的实现,不需要再额外引入security starter,重复引入反而可能造成版本冲突。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>如果是通过授权服务器的发现端点自动获取公钥,只需在application.yml中配置issuer即可。Spring Boot会请求该地址下的jwks_uri拿到签名公钥,并自动完成JWT校验配置。
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://ipipp.com/auth/realms/demo如果授权服务器不提供发现端点,或者出于网络隔离的原因无法在服务启动时访问它,可以直接指定JWKS地址:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://ipipp.com/auth/realms/demo/protocol/openid-connect/certs接下来编写SecurityFilterChain配置。这是新版写法,取代了以往继承WebSecurityConfigurerAdapter的方式:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
// 把JWT中的scope或角色Claim转换为GrantedAuthority
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthorityPrefix("SCOPE_");
converter.setAuthoritiesClaimName("scope");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}这段配置的含义是:公开接口直接放行,管理接口要求令牌中包含admin这个scope,其余接口只要令牌合法即可。加上@EnableMethodSecurity后,还可以在业务方法上使用@PreAuthorize("hasAuthority('SCOPE_admin')")做方法级别的控制,灵活度更高。
三、自定义JWT解析与权限映射
实际项目中,令牌里的Claim结构往往不符合默认约定。以Keycloak为例,角色信息存放在realm_access.roles这样的嵌套结构里,默认的转换器根本读不到。这时需要自己实现一个Converter<Jwt, AbstractAuthenticationToken>,手动解析Claim并构造权限集合。
public class CustomJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
return new JwtAuthenticationToken(jwt, authorities);
}
private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
// 解析Keycloak风格的嵌套角色结构
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess == null || !realmAccess.containsKey("roles")) {
return List.of();
}
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
}把上面这个转换器注册到SecurityFilterChain中,替换掉默认的converter即可。这样令牌中携带的角色就能映射成ROLE_前缀的权限,与hasRole、@PreAuthorize("hasRole('admin')")等表达式无缝配合。
如果使用的是对称密钥签发的JWT(比如用自研服务直接签发),则需要自定义JwtDecoder,用SecretKeySpec配合NimbusJwtDecoder.withSecretKey构建,并在配置类中覆盖默认的Decoder Bean。对称密钥方案要注意密钥的保管和轮换,生产环境更推荐RSA等非对称算法,资源服务器只持有公钥,泄露风险更小。
四、异常处理与调试技巧
默认情况下,令牌无效时资源服务器返回401,权限不足时返回403,响应体是不带任何提示的空白页,前端很难处理。可以通过自定义AuthenticationEntryPoint和AccessDeniedHandler来输出结构化的JSON错误信息。
http
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, e) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"msg\":\"令牌无效或已过期\"}");
})
.accessDeniedHandler((request, response, e) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"code\":403,\"msg\":\"权限不足\"}");
})
);调试阶段建议开启Spring Security的DEBUG日志,能清楚看到令牌校验每一步的输出。常见的坑有几类:一是系统时间不同步导致令牌被判为未生效,容器化部署时尤其常见,可以用JwtDecoders的ClockSkew配置放宽一点容差;二是issuer-uri与令牌中的iss Claim不完全一致,多一个斜杠都会校验失败;三是JWKS缓存问题,授权服务器轮换密钥后资源服务器还拿着旧公钥,可以调整缓存时间或在测试环境直接重启验证。
总体来说,Spring Boot整合OAuth2 Resource Server的工作量主要在权限映射和异常处理这两块,认证部分框架已经基本封装完毕。只要理清授权服务器、客户端、资源服务器三者的边界,按照本文的链路逐层排查,就能搭建出一套既安全又易于维护的接口保护方案。
Spring BootOAuth2Resource Server修改时间:2026-09-08 05:19:34