会了就涨工资的技巧:oauth2.0资源服务器配置

📌 资源服务器的作用:

资源服务器负责保护你的 API 接口 ,只允许携带有效 JWT Token 的请求访问。它不负责登录或发放 Token,而是验证 Token 的有效性(比如签名、过期时间、颁发者等)。

当你的微服务项目引入了OAuth2 Resource Server的依赖,框架会为应用做两件事

  1. 创建一个JwtDecoder。
  2. 自动为你配置一个过滤器链,在过滤器链中增加了BearerTokenAuthenticationFilter 用来验证token的有效性。
xml 复制代码
<!-- OAuth2 Resource Server -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

应用也可以选择自己暴露一个SecurityFilterChain bean,否则框架就会采用默认的配置。

scss 复制代码
/**
 * 资源服务器配置
 * If the application doesn't expose a SecurityFilterChain bean, then Spring Boot will expose the above default one.
 *
 * @param http
 * @return
 * @throws Exception
 */
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

🔍 一、JwtDecoder 是什么?

java 复制代码
@FunctionalInterface
public interface JwtDecoder {

   /**
    * Decodes the JWT from it's compact claims representation format and returns a
    * {@link Jwt}.
    * @param token the JWT value
    * @return a {@link Jwt}
    * @throws JwtException if an error occurs while attempting to decode the JWT
    */
   Jwt decode(String token) throws JwtException;

}

JwtDecoder 是 Spring Security 提供的一个接口,它的核心作用是:

接收一个 JWT 字符串(比如 eyJhbGciOiJIUzI1Ni...),验证它的签名和有效期,并把它解析成一个 Jwt 对象(包含 header、payload、claims 等信息)。

所以资源服务器要想知道如何去验证解析客户端带来的token,只能依托给JwtDecoderjwt解析器

当你在资源服务器中启用了jwt,但是却没有在容器中配置JwtDecoder的Bean,服务启动的时候就会报错,提示让我们配置一个JwtDecoder来解析即将到来的token

启用jwt:

java 复制代码
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));

控制台报错信息: Method filterChain in com.emrubik.system.security.config.ResourceServerConfig required a bean of type 'org.springframework.security.oauth2.jwt.JwtDecoder' that could not be found.

🛠️ SpringBoot 如何创建 JwtDecoder?(常见方式)

Yaml 复制代码
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${gateway.endpoint}/youlai-auth/oauth2/jwks

只要在属性中配置了jwk-set-uri,容器就会自动创建这个bean,这源于自动配置,发现spring.security.oauth2.resourceserver.jwt.jwk-set-uri属性,配置会生效。框架为我们创建JwtDecoder实例。

less 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JwtDecoder.class)
static class JwtDecoderConfiguration {


@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
JwtDecoder jwtDecoderByJwkKeySetUri(ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> customizers) {
       JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
          .jwsAlgorithms(this::jwsAlgorithms);
       customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
       NimbusJwtDecoder nimbusJwtDecoder = builder.build();
       String issuerUri = this.properties.getIssuerUri();
       OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
             ? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
       nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
       return nimbusJwtDecoder;
    }
  }  

👉 这段代码的意思是:

"如果用户没有自己定义 JwtDecoder,并且配置了 jwk-set-uri,那我就自动帮你创建一个基于 JWK 的 JwtDecoder。"

当然你也可以通过DSL自主配置,通过JwtConfigurerdecoder或者是jwkSetUri方法

kotlin 复制代码
public JwtConfigurer decoder(JwtDecoder decoder) {
   this.decoder = decoder;
   return this;
}

public JwtConfigurer jwkSetUri(String uri) {
   this.decoder = NimbusJwtDecoder.withJwkSetUri(uri).build();
   return this;
}

上面之所以我们主动配置了就生效,是因为在配置JwtAuthenticationProvider的时候,JwtDecoder是通过调用JwtConfigurer的getJwtDecoder方法来使用的。

getJwtDecoder的逻辑就是先判断this.decoder是否为空 如果为空说明我们没有自己主动配置,那么就会从容器中获取。

kotlin 复制代码
JwtDecoder getJwtDecoder() {
   if (this.decoder == null) {
      return this.context.getBean(JwtDecoder.class);
   }
   return this.decoder;
}

AuthenticationProvider getAuthenticationProvider() {
   if (this.authenticationManager != null) {
      return null;
   }
   JwtDecoder decoder = getJwtDecoder();
   Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter = getJwtAuthenticationConverter();
   JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);
   provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
   return postProcess(provider);
}

🔐 简单说: .jwt() 是开启 JWT 支持的关键配置,没有它,资源服务器无法解析 JWT。

JwtAuthenticationProvider

JwtAuthenticationProvider具体来说,它的核心职责和工作流程如下:

  1. 接收和解析 JWT

    • 当客户端向资源服务器发起一个受保护的请求时,通常会在 HTTP 请求头的 Authorization 字段中携带一个 JWT(例如:Bearer eyJhbGciOiJIUzI1Ni...)。
    • Spring Security 的过滤器链(特别是 BearerTokenAuthenticationFilter)会拦截这个请求,提取出 JWT 字符串。
    • JwtAuthenticationProvider 被调用,负责接收这个原始的 JWT 字符串,它会被包装成BearerTokenAuthenticationToken
java 复制代码
   BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
   
   AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
   
    Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);

下图来自官网

java 复制代码
public final class JwtAuthenticationProvider implements AuthenticationProvider {


   private final JwtDecoder jwtDecoder;

   private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter = new JwtAuthenticationConverter();

   public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
      Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
      this.jwtDecoder = jwtDecoder;
   }

   /**
    * @param authentication the authentication request object.
    * @return A successful authentication
    * @throws AuthenticationException if authentication failed for some reason
    */
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
      Jwt jwt = getJwt(bearer);
      AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
      if (token.getDetails() == null) {
         token.setDetails(bearer.getDetails());
      }
      this.logger.debug("Authenticated token");
      return token;
   }

   private Jwt getJwt(BearerTokenAuthenticationToken bearer) {
      try {
         return this.jwtDecoder.decode(bearer.getToken());
      }
      catch (BadJwtException failed) {
         this.logger.debug("Failed to authenticate since the JWT was invalid");
         throw new InvalidBearerTokenException(failed.getMessage(), failed);
      }
      catch (JwtException failed) {
         throw new AuthenticationServiceException(failed.getMessage(), failed);
      }
   }

从上面的源码我们可以看出JwtAuthenticationProvider验证的整个流程:

  • 首先通过jwtDecoder解码器解析校验token,底层的实现类是NimbusJwtDecoder
  • 校验通过返回的是Jwt对象,但是这个jwt需要转换成springsecurity的认证令牌
  • 利用jwtAuthenticationConverter进行转换

🧩 JwtAuthenticationConverter 是什么?

它是 Spring Security 提供的默认转换器,作用是:

把一个 Jwt 对象(解析后的 JWT)转换成 AbstractAuthenticationToken(Spring Security 的认证令牌)

最常见的结果是:JwtAuthenticationToken

java 复制代码
@FunctionalInterface
public interface Converter<S, T> {

   /**
    * Convert the source object of type {@code S} to target type {@code T}.
    * @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
    * @return the converted object, which must be an instance of {@code T} (potentially {@code null})
    * @throws IllegalArgumentException if the source cannot be converted to the desired target type
    */
   @Nullable
   T convert(S source);
}

JwtAuthenticationConverter实现了Converter接口,所以JwtAuthenticationConverter是为了将: Jwt转成AbstractAuthenticationToken

java 复制代码
JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>

但是我们通过源码发现JwtAuthenticationConverter内部持有一个JwtGrantedAuthoritiesConverter,

typescript 复制代码
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

   private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

   private String principalClaimName = JwtClaimNames.SUB;

   @Override
   public final AbstractAuthenticationToken convert(Jwt jwt) {
      Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);

      String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
      return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
   }

   /**
    * Sets the {@link Converter Converter&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt;}
    * to use. Defaults to {@link JwtGrantedAuthoritiesConverter}.
    * @param jwtGrantedAuthoritiesConverter The converter
    * @since 5.2
    * @see JwtGrantedAuthoritiesConverter
    */
   public void setJwtGrantedAuthoritiesConverter(
         Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
      Assert.notNull(jwtGrantedAuthoritiesConverter, "jwtGrantedAuthoritiesConverter cannot be null");
      this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
   }

🔐 JwtGrantedAuthoritiesConverter 是干嘛的?

它的职责非常专一:

Jwt 中提取出 Collection<GrantedAuthority>(用户的权限集合)

比如:

  • scope=read writeSCOPE_read, SCOPE_write
  • roles=ADMIN,USERROLE_ADMIN, ROLE_USER

也就是说JwtGrantedAuthoritiesConverter也是种转换器,专门用来从jwt中提取权限的,

JwtGrantedAuthoritiesConverter实现了Converter接口,但是泛型明显不同,它专门是为了转换权限点GrantedAuthority的

java 复制代码
public final class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

   private final Log logger = LogFactory.getLog(getClass());

   private static final String DEFAULT_AUTHORITY_PREFIX = "SCOPE_";

   private static final String DEFAULT_AUTHORITIES_CLAIM_DELIMITER = " ";

   private static final Collection<String> WELL_KNOWN_AUTHORITIES_CLAIM_NAMES = Arrays.asList("scope", "scp");

   private String authorityPrefix = DEFAULT_AUTHORITY_PREFIX;

   private String authoritiesClaimDelimiter = DEFAULT_AUTHORITIES_CLAIM_DELIMITER;

   private String authoritiesClaimName;

   /**
    * Extract {@link GrantedAuthority}s from the given {@link Jwt}.
    * @param jwt The {@link Jwt} token
    * @return The {@link GrantedAuthority authorities} read from the token scopes
    */
   @Override
   public Collection<GrantedAuthority> convert(Jwt jwt) {
      Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
      for (String authority : getAuthorities(jwt)) {
         grantedAuthorities.add(new SimpleGrantedAuthority(this.authorityPrefix + authority));
      }
      return grantedAuthorities;
   }

最后返回一个经过验证的JwtAuthenticationToken

new JwtAuthenticationToken(jwt, authorities, principalClaimValue);

定制化JwtGrantedAuthoritiesConverter

场景:比如你的 JWT 用 permissions 字段传权限

json 复制代码
{
  "sub": "123456",
  "permissions": ["user:read", "order:write"]
}

但是JwtGrantedAuthoritiesConverter默认使用的是scope scp字段

arduino 复制代码
private static final Collection<String> WELL_KNOWN_AUTHORITIES_CLAIM_NAMES = Arrays.asList("scope", "scp");

我们可以使用authoritiesConverter.setAuthoritiesClaimName("permissions") 覆盖默认配置

java 复制代码
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix(""); // 不要默认的 "SCOPE_"
authoritiesConverter.setAuthoritiesClaimName("permissions"); // 改查 permissions 字段

JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); // 替换!

因为框架在提取的时候会先判断用户是否设置了authoritiesClaimName,如果设置了就用用户自己设置的,否则就用默认的: WELL_KNOWN_AUTHORITIES_CLAIM_NAMES

kotlin 复制代码
private String getAuthoritiesClaimName(Jwt jwt) {
   if (this.authoritiesClaimName != null) {
      return this.authoritiesClaimName;
   }
   for (String claimName : WELL_KNOWN_AUTHORITIES_CLAIM_NAMES) {
      if (jwt.hasClaim(claimName)) {
         return claimName;
      }
   }
   return null;
}

总结

方法 说明
setAuthorityPrefix("ROLE_") 权限前缀(默认 SCOPE_
setAuthoritiesClaimName("roles") 指定从哪个 claim 读权限(默认 scopescp
setAuthorityPrefix("") 去掉前缀

使用方式如下:

ini 复制代码
@Bean
public Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    jwtGrantedAuthoritiesConverter.setAuthorityPrefix(Strings.EMPTY);
    jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
less 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2.jwt((jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter()))));
    return http.build();
}
相关推荐
yunteng5211 小时前
通用架构(同城双活)(单点接入)
架构·同城双活·单点接入
麦聪聊数据2 小时前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构
程序员侠客行2 小时前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
bobuddy4 小时前
射频收发机架构简介
架构·射频工程
桌面运维家4 小时前
vDisk考试环境IO性能怎么优化?VOI架构实战指南
架构
金牌归来发现妻女流落街头5 小时前
【从SpringBoot到SpringCloud】
java·spring boot·spring cloud
一个骇客6 小时前
让你的数据成为“操作日志”和“模型饲料”:事件溯源、CQRS与DataFrame漫谈
架构
鹏北海-RemHusband6 小时前
从零到一:基于 micro-app 的企业级微前端模板完整实现指南
前端·微服务·架构
7哥♡ۣۖᝰꫛꫀꪝۣℋ7 小时前
Spring-cloud\Eureka
java·spring·微服务·eureka
2的n次方_9 小时前
Runtime 内存管理深化:推理批处理下的内存复用与生命周期精细控制
c语言·网络·架构