首页 > 解决方案 > 如何在 Spring Boot 应用程序中添加自定义 OpenId 过滤器?

问题描述

我正在尝试实现 OpenId Connect 身份验证的后端。它是一个无状态 API,所以我添加了一个过滤器来处理 Bearer 令牌。

我创建了处理身份验证的 OpenIdConnect 过滤器并将其添加到 WebSecurityConfigurerAdapter 中。

public class OpenIdConnectFilter extends 
   AbstractAuthenticationProcessingFilter {
 @Value("${auth0.clientId}")
 private String clientId;

 @Value("${auth0.issuer}")
 private String issuer;

 @Value("${auth0.keyUrl}")
 private String jwkUrl;

private TokenExtractor tokenExtractor = new BearerTokenExtractor();

public OpenIdConnectFilter() {
    super("/connect/**");
    setAuthenticationManager(new NoopAuthenticationManager());
}

@Bean
public FilterRegistrationBean registration(OpenIdConnectFilter filter) {
  FilterRegistrationBean registration = new FilterRegistrationBean(filter);
  registration.setEnabled(false);
  return registration;
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {


    try {
      Authentication authentication = tokenExtractor.extract(request);

      String accessToken = (String) authentication.getPrincipal();
        String kid = JwtHelper.headers(accessToken)
            .get("kid");
        final Jwt tokenDecoded = JwtHelper.decodeAndVerify(accessToken, verifier(kid));
        final Map<String, Object> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
        verifyClaims(authInfo);
        Set<String> scopes = new HashSet<String>(Arrays.asList(((String) authInfo.get("scope")).split(" ")));
        int expires = (Integer) authInfo.get("exp");
        OpenIdToken openIdToken = new OpenIdToken(accessToken, scopes, Long.valueOf(expires), authInfo);
        final OpenIdUserDetails user = new OpenIdUserDetails((String) authInfo.get("sub"), "Test", openIdToken);

        return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    } catch (final Exception e) {
        throw new BadCredentialsException("Could not obtain user details from token", e);
    }

}

public void verifyClaims(Map claims) {
    int exp = (int) claims.get("exp");
    Date expireDate = new Date(exp * 1000L);
    Date now = new Date();
    if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("azp").equals(clientId)) {
        throw new RuntimeException("Invalid claims");
    }
}


private RsaVerifier verifier(String kid) throws Exception {
    JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
    Jwk jwk = provider.get(kid);
    return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}

这是安全配置:

@Configuration
@EnableWebSecurity
public class OpenIdConnectWebServerConfig extends 
WebSecurityConfigurerAdapter {

@Bean
public OpenIdConnectFilter myFilter() {
   final OpenIdConnectFilter filter = new OpenIdConnectFilter();
   return filter;
 }

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.cors();
  http.antMatcher("/connect/**").authorizeRequests() 
  .antMatchers(HttpMethod.GET, "/connect/public").permitAll()
  .antMatchers(HttpMethod.GET, "/connect/private").authenticated()
  .antMatchers(HttpMethod.GET, "/connect/private- 
       messages").hasAuthority("read:messages")
  .antMatchers(HttpMethod.GET, "/connect/private- 
      roles").hasAuthority("read:roles")
  .and()
    .addFilterBefore(myFilter(), 
       UsernamePasswordAuthenticationFilter.class);
}

休息端点如下所示:

  @RequestMapping(value = "/connect/public", method = RequestMethod.GET, 
     produces = "application/json")
  @ResponseBody
   public String publicEndpoint() throws JSONException {
    return new JSONObject()
          .put("message", "All good. You DO NOT need to be authenticated to 
         call /api/public.")
          .toString();
  }

   @RequestMapping(value = "/connect/private", method = RequestMethod.GET, 
      produces = "application/json")
     @ResponseBody
     public String privateEndpoint() throws JSONException {
      return new JSONObject()
          .put("message", "All good. You can see this because you are 
     Authenticated.")
          .toString();

}

如果我完全删除配置过滤器以及 @Bean 定义,则配置按预期工作: /connect/public 可访问,而 /connect/private 被禁止。

如果我保留@Bean 定义并将其添加到过滤器链中,则响应会为 /connect/public 和 /connect/private 上的请求返回 Not Found 状态:

"timestamp": "18.01.2019 09:46:11",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/

调试时我注意到过滤器正在处理令牌并返回身份验证的实现。

  1. 过滤器是否正确添加到过滤器链中并且位置正确?
  2. 为什么应该在 /connect/public 路径上调用过滤器,而这应该是公共的。它是否应用于匹配 super("/connect/**") 调用的所有路径?

  3. 为什么在 /connect/private 发出请求时将路径返回为“/”

似乎过滤器有问题,因为每次应用它时,响应都搞砸了。

标签: spring-bootspring-security-oauth2openid-connect

解决方案


推荐阅读