在 Spring Boot 中扩展 Keycloak 令牌

Posted

技术标签:

【中文标题】在 Spring Boot 中扩展 Keycloak 令牌【英文标题】:Extend Keycloak token in Spring boot 【发布时间】:2021-06-16 01:12:04 【问题描述】:

我正在使用 Keycloak 来保护我的 Spring Boot 后端。

依赖关系:

<dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-2-adapter</artifactId>
            <version>12.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-tomcat7-adapter-dist</artifactId>
            <version>12.0.3</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-security-adapter</artifactId>
            <version>12.0.3</version>
        </dependency>

安全配置:

    @Override
    protected void configure(HttpSecurity http) throws Exception 

        super.configure(http);
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.cors()
                .and()
                .csrf().disable()                
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 
                .and() 
                .authorizeRequests();

        expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/iam/accounts/promoters*").hasRole("PROMOTER");
        expressionInterceptUrlRegistry.anyRequest().permitAll();
    

一切正常!

但是现在我在 keycloak 令牌“角色”中添加了一个新部分,我需要以某种方式在我的 Spring boot 中扩展 keycloak jwt 类,并编写一些代码来解析角色信息并将其存储到 SecurityContext。大佬能告诉我如何存档目标吗?

【问题讨论】:

你在Keycloak领域或Keycloak客户端注册了角色吗?您的 application.yml 是如何设置的? 终于可以自己动手了。谢谢你的时间。我会尽快发布答案 【参考方案1】:

首先,扩展keycloak AccessToken:

@Data
static class CustomKeycloakAccessToken extends AccessToken 

    @JsonProperty("roles")
    protected Set<String> roles;


然后:

@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter 
    @Override
    protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() 
        return new KeycloakAuthenticationProvider() 

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException 
                KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
                List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

                for (String role : ((CustomKeycloakAccessToken)((KeycloakPrincipal)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles()) 
                    grantedAuthorities.add(new KeycloakRole(role));
                

                return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), new SimpleAuthorityMapper().mapAuthorities(grantedAuthorities));
            

        ;
    

    /**
     * Use NullAuthenticatedSessionStrategy for bearer-only tokens. Otherwise, use
     * RegisterSessionAuthenticationStrategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() 
        return new NullAuthenticatedSessionStrategy();
    

    @Override
    protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception 
        KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
        filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
        filter.setRequestAuthenticatorFactory(new SpringSecurityRequestAuthenticatorFactory() 

            @Override
            public RequestAuthenticator createRequestAuthenticator(HttpFacade facade,
                                                                   HttpServletRequest request, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) 
                return new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, sslRedirectPort) 

                    @Override
                    protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() 
                        return new BearerTokenRequestAuthenticator(deployment) 

                            @Override
                            protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) 
                                log.debug("Verifying access_token");
                                if (log.isTraceEnabled()) 
                                    try 
                                        JWSInput jwsInput = new JWSInput(tokenString);
                                        String wireString = jwsInput.getWireString();
                                        log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
                                     catch (JWSInputException e) 
                                        log.errorf(e, "Failed to parse access_token: %s", tokenString);
                                    
                                
                                try 
                                    TokenVerifier<CustomKeycloakAccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(tokenString, deployment, true, CustomKeycloakAccessToken.class);

                                    // Verify audience of bearer-token
                                    if (deployment.isVerifyTokenAudience()) 
                                        tokenVerifier.audience(deployment.getResourceName());
                                    
                                    token = tokenVerifier.verify().getToken();
                                 catch (VerificationException e) 
                                    log.debug("Failed to verify token");
                                    challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());
                                    return AuthOutcome.FAILED;
                                
                                if (token.getIssuedAt() < deployment.getNotBefore()) 
                                    log.debug("Stale token");
                                    challenge = challengeResponse(exchange,  OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token");
                                    return AuthOutcome.FAILED;
                                
                                boolean verifyCaller;
                                if (deployment.isUseResourceRoleMappings()) 
                                    verifyCaller = token.isVerifyCaller(deployment.getResourceName());
                                 else 
                                    verifyCaller = token.isVerifyCaller();
                                
                                surrogate = null;
                                if (verifyCaller) 
                                    if (token.getTrustedCertificates() == null || token.getTrustedCertificates().isEmpty()) 
                                        log.warn("No trusted certificates in token");
                                        challenge = clientCertChallenge();
                                        return AuthOutcome.FAILED;
                                    

                                    // for now, we just make sure Undertow did two-way SSL
                                    // assume JBoss Web verifies the client cert
                                    X509Certificate[] chain = new X509Certificate[0];
                                    try 
                                        chain = exchange.getCertificateChain();
                                     catch (Exception ignore) 

                                    
                                    if (chain == null || chain.length == 0) 
                                        log.warn("No certificates provided by undertow to verify the caller");
                                        challenge = clientCertChallenge();
                                        return AuthOutcome.FAILED;
                                    
                                    surrogate = chain[0].getSubjectDN().getName();
                                
                                log.debug("successful authorized");
                                return AuthOutcome.AUTHENTICATED;
                            

                        ;
                    
                ;
            
        );
        return filter;
    


【讨论】:

【参考方案2】:

我不明白你为什么需要扩展 Keycloak 令牌。 Keycloak Token 中已有的角色。我将尝试解释如何访问它,Keycloak 有两个角色级别,1)领域级别和 2)应用程序(客户端)级别,默认情况下,您的 Keycloak 适配器使用领域级别,要使用应用程序级别,您需要设置属性 keycloak.use-resource-role-mappings 在您的 application.yml 中设置为 true

如何在领域中创建角色 enter image description here

如何在客户端创建角色 enter image description here

具有 ADMIN(领域)和 ADD_USER(应用程序)角色的用户 enter image description here

要获得访问角色,您可以在 Keycloak Adapter 中使用 KeycloakAuthenticationToken 类,您可以尝试调用以下方法:

...
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken authenticationToken)  
    final AccessToken token = authenticationToken.getAccount().getKeycloakSecurityContext().getToken();
    final Set<String> roles = token.getRealmAccess().getRoles();
    final Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
...

...

要使用 Spring Security 保护任何路由器,您可以使用此注释,示例如下:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken token)  
   return ResponseEntity.ok(service.getUsers());

Obs:使用 @PreAuthorize 注释设置的 keycloak.use-resource-role-mappings。如果设置为 true,@PreAuthorize 检查 token.getRealmAccess().getRoles() 中的角色,如果设置为 false,则检查 token.getResourceAccess() 中的角色。

如果您想在令牌中添加任何自定义声明,请告诉我,我可以更好地解释。

我将如何设置 Keycloak 适配器和我的 application.yml 中的属性放在这里:

SecurityConfig.java

...
@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter 

    @Value("$project.cors.allowed-origins")
    private String origins = "";

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) 
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    

    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() 
        return new KeycloakSpringBootConfigResolver();
    

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() 
        return new NullAuthenticatedSessionStrategy();
    

    @Override
    protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception 
        KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
        filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
        filter.setAuthenticationFailureHandler((request, response, exception) -> 
            response.addHeader("Access-Control-Allow-Origin", origins);
            if (!response.isCommitted()) 
                response.sendError(401, "Unable to authenticate using the Authorization header");
             else if (200 <= response.getStatus() && response.getStatus() < 300) 
                throw new RuntimeException("Success response was committed while authentication failed!", exception);
            
        );
        return filter;
    

    @Override
    protected void configure(final HttpSecurity http) throws Exception 
        super.configure(http);
        http.csrf()
                .disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "**").permitAll()
                .antMatchers("/s/**").authenticated()
                .anyRequest().permitAll();

    

application.yml

..
keycloak: 
    enabled: true 
    auth-server-url: http://localhost:8080/auth 
    resource: myclient 
    realm: myrealm 
    bearer-only: true 
    principal-attribute: preferred_username 
    use-resource-role-mappings: true
..

【讨论】:

以上是关于在 Spring Boot 中扩展 Keycloak 令牌的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Boot 中扩展 Keycloak 令牌

开源 Spring Boot 中 Mongodb 多数据源扩展框架

使用 Spring Boot 在 Mongo 中自动扩展

Spring Boot扩展分析

开源 Spring Boot 中 Mongodb 多数据源扩展框架

如何在 spring-boot 中的微服务中创建 Super/Base/Parent 类 - 在子类中扩展该类