美文网首页
Spring oauth2 资源服务器 - Servlet

Spring oauth2 资源服务器 - Servlet

作者: 轻轻敲醒沉睡的心灵 | 来源:发表于2024-09-14 17:20 被阅读0次

前面记录了认证服务器 Spring Authorization Server的配置。这里我们记录一下 资源服务器ResourceServer。其实资源服务器的改动并不大,我用这个是分成了 2个步骤 来理解的。

1. 资源服务器的2个作用

  • 1是验证Token,主要是 验证签名、Token的有效期以及Token中的一些字段信息。
    当然token中也可以包含权限信息,可以用oauth2.1自带的逻辑来验证权限,但是这个效果不太理想,所以将验证权限单独拿出来,放到第2步了;
  • 2是验证权限,权限一般放缓存,但要想拿权限,就要从token中解析一个key用,所以第一步才要验签来保证key的准确。拿到权限后就可以自己写验证逻辑了

2. 资源服务器和jwt的流程

官方文档
我们先看一下资源服务器的整个流程:

image.png
  1. 当用户提交一个Token时,BearerTokenAuthenticationFilter通过从HttpServletRequest中提取出来的Token来创建一个BearerTokenAuthenticationToken。
  2. BearerTokenAuthenticationToken被传递到AuthenticationManager调用Authenticated方法进行身份验证。
  3. 如果失败则调用失败处理器
  4. 如果成功则可以继续访问资源接口

jwt的验证流程


image.png
  1. 将Token提交给ProviderManager。
  2. ProviderManager会找到JwtAuthenticationProvider,并调用authenticate方法。
  3. authenticate方法里面通过JwtDecoder来验证token,并转换为Jwt对象。
  4. authenticate方法里面通过JwtAuthenticationConverter将Jwt对象转换为已验证的Token对象。
  5. 验证成功就返回JwtAuthenticationToken对象。

3. 资源服务器配置

资源服务器有2种使用场景,当把微服务作为资源时,资源服务器有2种加法,

  • 一是直接用在微服务上,每个微服务都加;
  • 一是将资源服务器加在网关上,在网关统一鉴权。

在微服务鉴权一般是基于Servlet的,而在网关鉴权(如果网关是Gateway)是基于webFlux的,两者在一些api上是有区别的。
这里我们先说基于servlet的。Springboot版本是3.3.3。

3.1 pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.32</version>
</dependency>
3.2 ResourceServer配置类

要想配置这个呢,必须要清楚几点:

  • 1. jwt验证来源
    jwt的解析需要公钥的,我们可以用认证服务器的接口(issuer-uri或者jwk-set-uri)获取元数据,从而拿到公钥。也可以将公钥配置在资源服务器中使用。公钥验签...
  • 2. jwtDecoder
    这个就是解析jwt的,也就是验证jwt的。jar包提供了默认解析,我们也可以 实现解析接口,自己写逻辑。
  • 3. jwtAuthenticationConverter
    这是个jwt转换,主要干什么呢?
    OAuth2授权服务器发出的JWT的claim中通常会有一个 scope 或 scp 属性,表明它被授予的scope(或权限),例如:scope: ["s1", "s2"]。而资源服务器默认将scope内容转换成一个权限列表,并且在每个scope前面加上 "SCOPE_" 字符串,变成:"SCOPE_s1","SCOPE_s2"。然后用这个来做权限对比。所以,如果我们使用默认的权限验证方式的话,是这么写的:
    request.requestMatchers("/user/list").hasAuthority("SCOPE_s1")
    而这个jwt转换,可以配置2个地方:
      1. 配置权限在claim的哪个字段中(不一定在scope,可以配置成 perm);
      1. 配置权限列表用不用前缀,用哪个前缀(可以不用前缀,也可以将前缀配置成perm_);
        实际开发中,我没用这个进行权限鉴定,所以一般不用配这个。
        这个转换一般在资源服务器初始化的时候就会执行。
  • 4. AuthorizationManager
    这个就很重要了,我权限鉴定的逻辑都在这个里面。
3.2.1 最简单的配置
  1. 配置文件application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/issuer
  1. 配置类ResourceServerConfig
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
    
    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuerUri;
    
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.httpBasic(AbstractHttpConfigurer::disable);
        // csrf关闭
        http.csrf(csrf -> csrf.disable());
        // 跨域处理
        http.cors(Customizer.withDefaults());
        // 资源服务器配置
        http.oauth2ResourceServer(server -> server
                  // 使用jwt默认配置
                  .jwt(Customizer.withDefaults())
        );

        http.authorizeHttpRequests(request -> request
                // 放行接口
                .requestMatchers(
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/swagger-ui/**",
                        "/actuator/**",
                        "/instances/**").permitAll()
             // 需要指定权限的接口
            .requestMatchers("/user/list").hasAuthority("SCOPE_s1")
            // 其他都需要登陆鉴权
            .anyRequest().authenticated()
        );
        
        return http.build();
    }
}

基本上都是默认的,资源服务器会自动拿token,验证签名的;还放行了一些接口。

3.2.2 加入自定义配置,我一般用这个

注意:若在资源服务器配置了公钥,就不用issuer-uri了。

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
    
  @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
  private String issuerUri;
    
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.httpBasic(AbstractHttpConfigurer::disable);
        // csrf关闭
        http.csrf(csrf -> csrf.disable());
        // 跨域处理
        http.cors(Customizer.withDefaults());
        // 资源服务器配置
        http.oauth2ResourceServer(server -> server
                // 权限不通过时,自定义返回
                .accessDeniedHandler(new MyAccessDeniedHandler())
                // 未登录或者登陆验证失败时(token有问题),自定义返回
                .authenticationEntryPoint(new MyAuthenticationEntryPoint())
                // 使用jwt默认配置
//              .jwt(Customizer.withDefaults())
                // jwt自定义校验
                .jwt(jwt -> jwt
                        // 当无法提供issuer-uri的时候,可以拿到jwk,包含有私钥
                        // 可以不在这配置,在decoder中也可以配置从什么地方拿私钥验签
//                          .jwkSetUri("http://127.0.0.1:9001/oauth2/oauth2/jwks")
                        .decoder(jwtDecoder())
                        // 指定jwt转换
                        .jwtAuthenticationConverter(jwtAuthenticationConverter())
                    )
        );
        // 权限配置
        http.authorizeHttpRequests(request -> request
                 // 放行接口
                .requestMatchers(
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/swagger-ui/**",
                        "/actuator/**",
                        "/instances/**").permitAll()
                // 自定义权限校验逻辑
                .anyRequest().access(new MyAuthorizationManager())
        );
        
        return http.build();
    }
    
    /**
     * jwt转换器
     */
    private JwtAuthenticationConverter jwtAuthenticationConverter() {
            JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); 
            JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
            // 指定token中权限字段的名字
            authoritiesConverter.setAuthoritiesClaimName("perms");
            // 指定权限字符串前缀,空表示无前缀
            authoritiesConverter.setAuthorityPrefix("");
            converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
            return converter;
    }
    
    // 自定义jwtDecoder,一般是校验Payload中的字段
    JwtDecoder jwtDecoder() {
        // 使用issuerUri创建decoder,decoder会自动验证token签名
//        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(issuerUri + "/oauth2/jwks").build();
        // 使用本地公钥创建decoder
        NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(getPublicKey()).build();
        // 创建验证器委托,可以包含多个验证器,官方提供有这个类DelegatingOAuth2TokenValidator,但是逻辑不符合需求
        // 比如,加入2个验证器,当第一个验证器不通过时,应直接返回错误,不验证第2个了,DelegatingOAuth2TokenValidator是验证完所有再返回所有错误信息。所以参照DelegatingOAuth2TokenValidator,写了自己需要的
        OAuth2TokenValidator<Jwt> orderJwtValidator = new OrderJwtValidator<>(
            // 官方的默认验证器,验证token过期时间、生效时间(nbf)、X509证书。还有其他的可以使用:JwtIssuerValidator、JwtClaimValidator等(按顺序加入,先加先验证)
            JwtValidators.createDefaultWithIssuer(issuerUri),
            // 自定义验证逻辑,写在这个里面
            new MyJwtValidator()
        );
        jwtDecoder.setJwtValidator(orderJwtValidator);
        return jwtDecoder;
    }
    
  /**
   * 字符串转PublicKey
   */
  private RSAPublicKey getPublicKey() {
      String publicKeyBase64 = "111222333LggDJeXlA/XN4kRPY9sW7+VQpr1MPJjB5tQYVkPLvv3L8v/7k5hcPEoHIFwQo";
      X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBase64));
      RSAPublicKey rsaPublicKey = null;
      try {
          KeyFactory keyFactory = KeyFactory.getInstance("RSA");
          rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
      } catch (Exception e) {
          e.printStackTrace();
      }
        
      return rsaPublicKey;
  }
}
3.3 自定义的异常返回2个
  • MyAccessDeniedHandler
/**
 * 登陆了,没有权限时,触发异常 返回信息
 */
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    
    private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        accessDeniedException.printStackTrace();
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        Result<Integer> res = new Result<Integer>().error(accessDeniedException.getLocalizedMessage());
        httpResponseConverter.write(res, null, httpResponse);
    }
}
  • MyAuthenticationEntryPoint
/**
 * 未认证(没有登录)时,返回异常 信息
 */
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) throws IOException, ServletException {
        authException.printStackTrace();
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        
        Result<String> res = new Result<String>().error(CodeMsg.USER_LOGIN_ERROR);
        
        httpResponseConverter.write(res, null, httpResponse);
    }
}
3.4 自定义的decoder
  • MyJwtValidator
public class MyJwtValidator implements OAuth2TokenValidator<Jwt> {
    
    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        System.out.println("----decode校验逻辑-----");
        // 校验成功,返回
        return OAuth2TokenValidatorResult.success();
    }
}
  • OrderJwtValidator
public class OrderJwtValidator<T extends OAuth2Token> implements OAuth2TokenValidator<T> {

    private final Collection<OAuth2TokenValidator<T>> tokenValidators;
    
    public OrderJwtValidator(Collection<OAuth2TokenValidator<T>> tokenValidators) {
        Assert.notNull(tokenValidators, "tokenValidators cannot be null");
        this.tokenValidators = new ArrayList<>(tokenValidators);
    }

    @SafeVarargs
    public OrderJwtValidator(OAuth2TokenValidator<T>... tokenValidators) {
        this(Arrays.asList(tokenValidators));
    }
    
    @Override
    public OAuth2TokenValidatorResult validate(T token) {
        Collection<OAuth2Error> errors = new ArrayList<>();
        for (OAuth2TokenValidator<T> validator : this.tokenValidators) {
            errors = validator.validate(token).getErrors();
            if (!errors.isEmpty()) {
                break;
            }
        }
        return OAuth2TokenValidatorResult.failure(errors);
    }
}
3.5 自定义权限校验MyAuthorizationManager
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
        HttpServletRequest request = context.getRequest();
        System.out.println(request.getRequestURI());
        UserDetails user = (UserDetails) authentication.get().getPrincipal();
        System.out.println(user);
        
        return new AuthorizationDecision(true);
    }
}

到这,基本配置完了,目录如下:


image.png

相关文章

网友评论

      本文标题:Spring oauth2 资源服务器 - Servlet

      本文链接:https://www.haomeiwen.com/subject/orwvljtx.html