【问题标题】:Extend Keycloak token in Spring boot在 Spring Boot 中扩展 Keycloak 令牌
【发布时间】: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 是如何设置的?
  • 终于可以自己动手了。谢谢你的时间。我会尽快发布答案

标签: spring spring-boot jwt keycloak


【解决方案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
    ..
    

    【讨论】:

      猜你喜欢
      • 2021-08-11
      • 2017-09-13
      • 1970-01-01
      • 2021-08-16
      • 2023-02-25
      • 2020-02-08
      • 2022-08-03
      • 2017-01-01
      • 2021-11-11
      相关资源
      最近更新 更多