简体   繁体   English

在 Spring 引导中扩展 Keycloak 令牌

[英]Extend Keycloak token in Spring boot

I'm using Keycloak to secure my Spring boot backend.我正在使用 Keycloak 来保护我的 Spring 引导后端。

Dependencies:依赖项:

<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>

Security config:安全配置:

    @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();
    }

Everything work fine!一切正常!

But now I add a new section in keycloak token "roles" and I need to somehow extend keycloak jwt class in my Spring boot and write some code to parse and store the roles information to SecurityContext.但是现在我在 keycloak 令牌“角色”中添加了一个新部分,我需要在我的Spring 引导中以某种方式扩展 keycloak jwt class 并编写一些代码来解析和存储角色信息到安全上下文。 Could you Guy please tell me how to archive the goal?你能告诉我如何存档目标吗?

I didn't understand why do you need extend Keycloak Token.我不明白你为什么需要扩展 Keycloak 令牌。 The roles already there are in Keycloak Token. Keycloak Token 中已有的角色。 I will try explain how to access it, the Keycloak have two levels for roles, 1) Realm level and 2) Application (Client) level, by default your Keycloak Adapter use realm level, to use application level you need setting the propertie keycloak.use-resource-role-mappings with true in your application.yml我将尝试解释如何访问它,Keycloak 有两个角色级别,1)Realm 级别和 2)应用程序(客户端)级别,默认情况下,您的 Keycloak 适配器使用 realm 级别,要使用应用程序级别,您需要设置属性keycloak。在 application.yml 中使用具有 true的资源角色映射

How to create roles in realm enter image description here如何在 realm 中创建角色 在此处输入图像描述

How to creare roles in client enter image description here如何在客户端中创建角色 在此处输入图像描述

User with roles ADMIN (realm) and ADD_USER (application) enter image description here具有角色 ADMIN(领域)和 ADD_USER(应用程序)的用户在此处输入图像描述

To have access roles you can use KeycloakAuthenticationToken class in your Keycloak Adapter, you can try invoke the following method:要获得访问角色,您可以在 Keycloak Adapter 中使用 KeycloakAuthenticationToken class,您可以尝试调用以下方法:

...
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();
...
}
...

To protect any router using Spring Security you can use this annotation, example below:要保护使用 Spring Security 的任何路由器,您可以使用此注释,示例如下:

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

Obs: The keycloak.use-resource-role-mappings set up using @PreAuthorize Annotation. Obs:使用@PreAuthorize Annotation 设置的 keycloak.use-resource-role-mappings。 If set to true, @PreAuthorize checks roles in token.getRealmAccess().getRoles(), if false it checks roles in token.getResourceAccess().如果设置为 true,@PreAuthorize 检查 token.getRealmAccess().getRoles() 中的角色,如果设置为 false,则检查 token.getResourceAccess() 中的角色。

If you want add any custom claim in token, let me know that I can explain better.如果您想在令牌中添加任何自定义声明,请告诉我,我可以更好地解释。

I put here how I set up my Keycloak Adapter and the properties in my application.yml:我将如何设置 Keycloak 适配器和 application.yml 中的属性放在这里:

SecurityConfig.java安全配置.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应用程序.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
..

First, extends keycloak AccessToken:首先,扩展keycloak AccessToken:

@Data
static class CustomKeycloakAccessToken extends AccessToken {

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

}

Then:然后:

@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;
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM