【问题标题】:spring security filter issues春季安全过滤器问题
【发布时间】:2020-09-25 09:26:34
【问题描述】:

使用 Boot 2.1.14.Release / security 5.1.10

我有以下需要指定安全性的端点

/token/exchange - 只有在请求具有 Okta JWT 时,此端点才应允许访问。它返回一个我通过 JJWT 手动创建的自定义 JWT。基本上,用户不会传递用户凭据,而是已经通过 Okta 进行了身份验证,并将提供该令牌作为他们的凭据。

我已经添加了 Okta 启动器,它按预期工作


/api/** - /api 下的任何端点都需要在授权标头中使用我的自定义 JWT


我有以下安全配置:

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
@Configuration
public class AppWebSecurityConfigurerAdapter {

    @Configuration
    @Order(1)
    public static class OktaWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .antMatcher("/token/exchange") <------- this should "pin" this config right?
                    .authorizeRequests()
                    .antMatchers("/token/exchange").authenticated() <--- is this needed?
                    .and()
                    .oauth2ResourceServer().jwt();
            http.cors();
            http.csrf().disable();
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            Okta.configureResourceServer401ResponseBody(http);
        }
    }

    @Configuration
    @Order(2)
    @RequiredArgsConstructor
    public static class ApiWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
        private final CustomSecurityConfig customSecurityConfig; <--- JWT secret key in here
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/api/**")
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .addFilter(new JwtFilter(authenticationManager(), customSecurityConfig))
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .cors();
        }
    }
}

还有以下JwtFilter

@Slf4j
public class JwtFilter extends BasicAuthenticationFilter {

    private final CustomSecurityConfig customSecurityConfig;

    public JwtFilter(AuthenticationManager authenticationManager, CustomSecurityConfig customSecurityConfig) {
        super(authenticationManager);
        this.customSecurityConfig = customSecurityConfig;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        try {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
            if (authentication == null) {
                chain.doFilter(request, response);
                return;
            }
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } catch (Exception exception){
            log.error("API authentication failed", exception);
            SecurityContextHolder.clearContext();
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = new DefaultBearerTokenResolver().resolve(request);
        if (token == null) {
            return null;
        }
        Algorithm algorithm = Algorithm.HMAC256(customSecurityConfig.getSecret());
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer(CustomSecurityConfig.ISSUER)
                .build();
        DecodedJWT jwt = verifier.verify(token);
        return new UsernamePasswordAuthenticationToken(
                jwt.getClaim("user_name").asString(),
                null,
                jwt.getClaim("authorities")
                        .asList(String.class)
                        .stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList()));
    }
}

我的/api 调用全部返回 401,因为它们正在由BearerTokenAuthenticationFilter(由我的OktaWebSecurityConfigurerAdapter 使用)而不是我的JwtFilter 处理。自然,两个令牌之间的签名不匹配。我很困惑为什么我的 /api 调用甚至被该过滤器处理,因为我只为我的 Okta 处理程序应用 .oauth2ResourceServer().jwt(); 配置

我的日志如下所示:

SecurityContextHolder now cleared, as request processing completed
Checking match of request : '/api/entities'; against '/token/exchange'
Checking match of request : '/api/entities'; against '/api/**'
/api/entities at position 1 of 13 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
/api/entities at position 2 of 13 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
/api/entities at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
/api/entities at position 4 of 13 in additional filter chain; firing Filter: 'CorsFilter'
/api/entities at position 5 of 13 in additional filter chain; firing Filter: 'LogoutFilter'
Trying to match using Ant [pattern='/logout', GET]
Checking match of request : '/api/entities'; against '/logout'
Trying to match using Ant [pattern='/logout', POST]
Request 'GET /api/entities' doesn't match 'POST /logout'
Trying to match using Ant [pattern='/logout', PUT]
Request 'GET /api/entities' doesn't match 'PUT /logout'
Trying to match using Ant [pattern='/logout', DELETE]
Request 'GET /api/entities' doesn't match 'DELETE /logout'
No matches found
/api/entities at position 6 of 13 in additional filter chain; firing Filter: 'BearerTokenAuthenticationFilter'
Authentication attempt using org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider
No event was found for the exception org.springframework.security.oauth2.core.OAuth2AuthenticationException
Authentication request for failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@13691de5
SecurityContextHolder now cleared, as request processing completed

我整日整夜都在用这个敲打我的头......谢谢你的帮助!

【问题讨论】:

    标签: spring-boot spring-security jwt spring-security-oauth2


    【解决方案1】:

    我将从回答您的问题开始(我认为)。您正在使用addFilter(...) 添加您的过滤器,它没有指定任何订单信息。请改用addFilterBefore(...)

    我会提醒您不要将 JWT 用于某种会话令牌,除非有办法撤销它们。 https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens

    在您的情况下,听起来您可能会将一个令牌换成一个“较弱”的令牌。您可以采取一些措施来减轻这种风险,例如限制令牌有效的持续时间等。我没有您的用例的所有上下文或您如何使用它,所以请谨慎对待盐:)

    【讨论】:

    • > 在您的情况下,听起来您可能正在将一个令牌交换为一个“较弱”的令牌......我继承了一些非常糟糕的代码,我试图不改变域逻辑需要一个签名的令牌(我什至不会称它为 JWT)。我的目标是使用 OAuth2 保护单个路由,该路由将返回自定义构建和签名令牌。 /api/** 后面的任何端点都需要该签名令牌。 @brian-demers
    【解决方案2】:

    我在JJWT自述文件中看到了这个

    但是您可能已经注意到一些事情 - 如果您的应用程序不只使用一个 SecretKey 或 KeyPair 怎么办?如果 JWS 可以使用不同的 SecretKey 或公钥/私钥或两者的组合创建会怎样?如果您不能先​​检查 JWT,您如何知道要指定哪个密钥?

    这让我重新思考了设计。

    基本上,我所有的路线都需要一个 Bearer 令牌。其中之一 (/token/exchange) 预计令牌来自 Okta。我所有的 /api/** 路由都希望令牌由 API 服务器本身签名。为此,我设置了一个单一的安全配置,如下所示:

    @Configuration
    @RequiredArgsConstructor
    public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        private final JwtFilter jwtFilter;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/public/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
            http
                    .cors().disable()
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
    

    我的JwtFilter

    @Slf4j
    @Component
    public class JwtFilter extends OncePerRequestFilter {
    
        private static final String HEADER = HttpHeaders.AUTHORIZATION;
    
        private final OktaTokenUtils oktaTokenUtils;
        private final ApiTokenUtils apiTokenUtils;
    
        public JwtFilter(OktaTokenUtils oktaTokenUtils, ApiTokenUtils apiTokenUtils) {
            this.oktaTokenUtils = oktaTokenUtils;
            this.apiTokenUtils = apiTokenUtils;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain filterChain) throws ServletException, IOException {
            final String header = request.getHeader(HEADER);
            if (header != null && !header.isBlank()) {
                final String token = header.substring(7);
                log.debug("{} TOKEN: {}", HEADER, token);
                String uri = request.getRequestURI();
                if (uri.equals("/token/exchange")) {
                    SecurityContextHolder.getContext().setAuthentication(oktaTokenUtils.authenticate(token));
                } else {
                    SecurityContextHolder.getContext().setAuthentication(apiTokenUtils.authenticate(token));
                }
            }
            filterChain.doFilter(request, response);
        }
    
    }
    

    我的 OktaUtils 和 ApiUtils 类负责验证令牌并返回 Authentication 对象,以便过滤器可以将其添加到安全上下文持有者

    @Slf4j
    @Component
    public class ApiTokenUtils {
    
        private final JwtParser parser;
    
        public ApiTokenUtils(ApiTokenConfig config){
            parser = Jwts.parserBuilder()
                    .requireIssuer(config.getIssuer())
                    .setSigningKey(config.getSecret())
                    .build();
        }
    
        public Authentication authenticate(String token) {
            try {
                Jws<Claims> claims = parser.parseClaimsJws(token);
                String username = claims.getBody().get("user_name", String.class);
                log.debug("valid API token for username {}", username);
                List<String> authorities = claims.getBody().get("authorities", List.class);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        username,
                        null,
                        authorities.stream()
                                .map(SimpleGrantedAuthority::new)
                                .collect(Collectors.toList())
                );
                authentication.setDetails(MyUserDetails.builder()
                        .token(token)
                        .authorities(authorities)
                        .setUserId(claims.getBody().get("userId", String.class))
                        .email(claims.getBody().get("email", String.class))
                        .username(username)
                        .build());
                return authentication;
            } catch (Exception e) {
                if( log.isDebugEnabled() ) {
                    log.error("API token verification failed", e);
                } else {
                    log.error("API token verification failed");
                }
                return null;
            }
        }
    }
    
    @Slf4j
    @Component
    public class OktaTokenUtils {
    
        private final AccessTokenVerifier verifier;
    
        public OktaTokenUtils(OktaConfig oktaConfig) {
            verifier = JwtVerifiers.accessTokenVerifierBuilder()
                    .setIssuer(oktaConfig.getIssuer())
                    .setAudience(oktaConfig.getAudience()) // defaults to 'api://default'
                    .setConnectionTimeout(Duration.ofSeconds(3)) // defaults to 1s
                    .setReadTimeout(Duration.ofSeconds(3)) // defaults to 1s
                    .build();
        }
    
        public Authentication authenticate(String token) {
            try {
                Jwt jwt = verifier.decode(token);
                String subject = jwt.getClaims().get("sub").toString();
                log.debug("valid Okta token for SUB {}", subject);
                return new UsernamePasswordAuthenticationToken(
                        subject,
                        token,
                        Collections.emptyList());
            } catch (Exception e) {
                if( log.isDebugEnabled() ) {
                    log.error("Okta token verification failed", e);
                } else {
                    log.error("Okta token verification failed");
                }
                return null;
            }
        }
    
    }
    

    一些帮助我的链接:

    【讨论】:

      猜你喜欢
      • 2020-09-27
      • 1970-01-01
      • 1970-01-01
      • 2023-03-06
      • 2013-12-22
      • 1970-01-01
      • 2018-08-01
      相关资源
      最近更新 更多