【问题标题】:By extending WebSecurityConfigurerAdapter, How to construct configure with custom authentication logic通过扩展 WebSecurityConfigurerAdapter,如何使用自定义身份验证逻辑构造配置
【发布时间】:2020-09-30 14:46:21
【问题描述】:

我正在使用 okta 进行身份验证。我们公司的 okta 禁用了“默认”授权服务器。所以现在我不能使用“okta-spring-security-starter”来简单地验证从 url 标头传递的令牌:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/health").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().jwt();

        http.cors();

        Okta.configureResourceServer401ResponseBody(http);

    }
}

所以我需要点击 okta introspect 端点 (https://developer.okta.com/docs/reference/api/oidc/#introspect) 进行验证。所以我想知道我能否将此过程集成到WebSecurityConfigurerAdapter 的配置中。也许是这样的???:

import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/health").permitAll()
                .anyRequest().authenticated()
                .and()
                /*add something there*/

        http.cors();


    }
}

我看到类似覆盖 AuthenticationProvider(Custom Authentication provider with Spring Security and Java Config),并使用 httpbasic auth。如果我使用 .oauth2ResourceServer().jwt(),我可以做类似的事情吗?

我的想法是覆盖身份验证提供程序并在提供程序中,点击 okta introspect 端点,这会工作吗???

【问题讨论】:

    标签: java spring-boot spring-security okta okta-api


    【解决方案1】:

    Spring Security 5.2 附带了对自省端点的支持。请查看 GitHub 存储库中的 Opaque Token sample

    不过,要在这里简要回答,您可以这样做:

    http
        .authorizeRequests(authz -> authz
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .opaqueToken(opaque -> opaque
                .introspectionUri("the-endpoint")
                .introspectionClientCredentials("client-id", "client-password")
            )
        );
    

    如果您使用的是 Spring Boot,那么它会更简单一些。您可以在 application.yml 中提供这些属性:

    spring:
      security:
        oauth2:
          resourceserver:
            opaquetoken:
              introspection-uri: ...
              client-id: ...
              client-secret: ...
    

    然后你的 DSL 可以指定 opaqueToken:

    http
        .authorizeRequests(authz -> authz
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .opaqueToken(opaque -> {})
        );
    

    【讨论】:

    • 它是所需的客户端机密吗?如果我们将授权码流与 PKCE 一起使用会怎样?
    • 我认为这取决于 Okta。 RFC 要求自省端点需要客户端 ID 和密码。
    • 此外,流程(授权代码、客户端凭据等)通常由 OAuth 2.0 客户端完成,而不是由 OAuth 2.0 资源服务器完成。这两个概念(授权代码和自省)通常不会一起出现。
    • 是的,内省和流程是不同的东西,很抱歉造成混淆。因此,如果我的 okta 应用程序正在使用 PKCE,我不需要提供客户端机密,对吗? oauth2ResourceServer lib(或函数)不会抛出错误吧?
    • PKCE 是 OAuth 2.0 客户端使用的一种机制,它与授权代码协调以交换令牌代码。自省是 OAuth 2.0 资源服务器用来验证令牌有效性并提取其声明的机制。他们彼此没有任何关系。根据 Okta 的文档,需要一个 client_id 和 secret,这与 RFC 一致。
    【解决方案2】:

    我不使用 Okta,因此我不知道它是如何工作的。但我有两个假设:

    • 每个请求的 Authorization 标头中都包含一个 accessToken
    • 您向 ${baseUrl}/v1/introspect 发出 POST 请求,它会以 true 或 false 回答您,表明 accessToken 是否有效

    考虑到这两个假设,如果我必须手动实现自定义安全逻辑身份验证,我会执行以下步骤:

    • 注册并实施CustomAuthenticationProvider
    • 添加过滤器以从请求中提取访问令牌

    注册自定义身份验证提供程序:

    // In OktaOAuth2WebSecurityConfig.java
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider());
    }
    
    @Bean
    CustomAuthenticationProvider customAuthenticationProvider(){
        return new CustomAuthenticationProvider();
    }
    

    CustomAuthenticationProvider:

    public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        logger.debug("Authenticating authenticationToken");
        OktaTokenAuthenticationToken auth = (OktaTokenAuthenticationToken) authentication;
        String accessToken = auth.getToken();
    
        // You should make a POST request to ${oktaBaseUrl}/v1/introspect
        // to determine if the access token is good or bad
    
        // I just put a dummy if here
    
        if ("ThanhLoyal".equals(accessToken)){
            List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
            logger.debug("Good access token");
            return new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities);
        }
        logger.debug("Bad access token");
        return null;
    }
    
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz == OktaTokenAuthenticationToken.class;
    }
    

    }

    注册过滤器以从请求中提取accessToken:

    // Still in OktaOAuth2WebSecurityConfig.java
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterAfter(accessTokenExtractorFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests().anyRequest().authenticated();
                // And other configurations
    
    }
    
    @Bean
    AccessTokenExtractorFilter accessTokenExtractorFilter(){
        return new AccessTokenExtractorFilter();
    }
    

    还有它自己的过滤器:

    public class AccessTokenExtractorFilter extends OncePerRequestFilter {
    
    private static final Logger logger = LoggerFactory.getLogger(AccessTokenExtractorFilter.class);
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.debug("Filtering request");
        Authentication authentication = getAuthentication(request);
        if (authentication == null){
            logger.debug("Continuing filtering process without an authentication");
            filterChain.doFilter(request, response);
        } else {
            logger.debug("Now set authentication on the request");
            SecurityContextHolder.getContext().setAuthentication(authentication);
            filterChain.doFilter(request, response);
        }
    }
    
    private Authentication getAuthentication(HttpServletRequest request) {
        String accessToken = request.getHeader("Authorization");
        if (accessToken != null){
            logger.debug("An access token found in request header");
            List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
            return new OktaTokenAuthenticationToken(accessToken, authorities);
        }
    
        logger.debug("No access token found in request header");
        return null;
    }
    

    }

    我在这里上传了一个简单的项目供您参考:https://github.com/MrLoyal/spring-security-custom-authentication

    它是如何工作的:

    • AccessTokenExtractorFilter 位于 UsernamePasswordAuthenticationFilter 之后,这是 Spring Security 的默认过滤器
    • 一个请求到达,上面的过滤器从中提取accessToken并放在SecurityContext中
    • 稍后,AuthenticationManager 调用 AuthenticationProvider(s) 来验证请求。在这种情况下,调用了 CustomAuthenticationProvider

    顺便说一句,您的问题应该包含spring-security 标签。

    更新 1:关于 AuthenticationEntryPoint

    AuthenticationEntryPoint 声明当未经身份验证的请求到达时要做什么(在我们的例子中,当请求不包含有效的“授权”标头时要做什么)。

    在我的 REST API 中,我只是向客户端响应 401 HTTP 状态代码。

    // CustomAuthenticationEntryPoint
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.reset();
        response.setStatus(401);
        // A utility method to add CORS headers to the response
        SecUtil.writeCorsHeaders(request, response);
    }
    

    Spring 的LoginUrlAuthenticationEntryPoint 将用户重定向到登录页面(如果已配置)。

    因此,如果您想将未经身份验证的请求重定向到 Okta 的登录页面,您可以使用 AuthenticationEntryPoint。

    【讨论】:

    • 我会更新我的答案,附加答案太长,无法评论。
    • new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities); 据我了解,当局就像“角色”对吧?所以我们可以像antMatchers("/health").hasAuthorities('Admin')一样使用它
    • 我们可以把/introspect http调用(验证过程)放到addFilterAfter中吗?
    • 是的,它就像“角色”,除了少数情况,例如@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasAuthority('ADMIN')") (请自行判断)。但是如果你将一个空的List&lt;&gt; authorities 传递给UsernamePasswordAuthenticationToken 构造函数,Spring Security 会将此令牌视为 UNAUTHORIZED,所以我只是将一个虚拟的“USER”权限传递给构造函数以通过检查。
    猜你喜欢
    • 2021-11-23
    • 1970-01-01
    • 2019-03-23
    • 1970-01-01
    • 2015-05-14
    • 2023-04-02
    • 1970-01-01
    • 2021-06-02
    • 2016-07-24
    相关资源
    最近更新 更多