java 带有Keycloak的SpringBoot OAuth2未将Map的角色返回为权限

hivapdat  于 2023-01-11  发布在  Java
关注(0)|答案(2)|浏览(172)

我正在创建一个简单的SpringBoot应用程序,并尝试与OAuth 2.0提供程序Keycloak集成。我已经在领域级别创建了一个领域、客户端、角色(成员、高级成员),最后创建了用户和分配的角色(成员、高级成员)。
如果我使用Keycloak www.example.com提供的SpringBoot适配器https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_boot_adapter,那么当我成功登录并检查登录用户的权限时,我能够看到分配的角色,如会员、高级会员。

Collection<? extends GrantedAuthority> authorities = 
 SecurityContextHolder.getContext().getAuthentication().getAuthorities();

但如果我使用通用的SpringBoot Auth2客户端配置,我可以登录,但当我检查权限时,它总是只显示ROLE_USER、SCOPE_email、SCOPE_openid、SCOPE_profile,而不包括我Map的角色(会员、高级会员)。
我的SpringBoot OAuth2配置:

聚合物.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

应用程序.属性

spring.security.oauth2.client.provider.spring-boot-thymeleaf-client.issuer-uri=http://localhost:8181/auth/realms/myrealm

spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.client-id=spring-boot-app
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.client-secret=XXXXXXXXXXXXXX
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.scope=openid,profile,roles
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.redirect-uri=http://localhost:8080/login/oauth2/code/spring-boot-app

我使用的是Spring启动2.5.5键盘斗篷15.0.2
使用这种通用的OAuth2.0配置方法(不使用Keycloak SpringBootAdapter),是否可以获得分配的角色?

ecbunoof

ecbunoof1#

默认情况下,Spring Security使用scopescp声明中的值以及SCOPE_前缀生成GrantedAuthority列表。
Keycloak将领域角色保存在嵌套声明realm_access.roles中。您有两个选项来提取角色并将其Map到GrantedAuthority列表。

OAuth2客户端

如果您的应用程序配置为OAuth2客户端,则可以从ID令牌或UserInfo端点提取角色。Keycloak仅在访问令牌中包括角色,因此您需要更改配置以将它们也包括在ID令牌或UserInfo端点中(这是我在下面的示例中使用的)。您可以从Keycloak管理控制台转到Client Scopes > roles > Mappers > realm roles来执行此操作

然后,在Spring Security配置中,定义一个GrantedAuthoritiesMapper,它从UserInfo端点提取角色,并将它们Map到GrantedAuthority。在这里,我将包括特定bean的外观。在我的GitHub上有一个完整的示例:https://github.com/ThomasVitale/spring-security-examples/tree/main/oauth2/login-user-authorities

@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            var authority = authorities.iterator().next();
            boolean isOidc = authority instanceof OidcUserAuthority;

            if (isOidc) {
                var oidcUserAuthority = (OidcUserAuthority) authority;
                var userInfo = oidcUserAuthority.getUserInfo();

                if (userInfo.hasClaim("realm_access")) {
                    var realmAccess = userInfo.getClaimAsMap("realm_access");
                    var roles = (Collection<String>) realmAccess.get("roles");
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            } else {
                var oauth2UserAuthority = (OAuth2UserAuthority) authority;
                Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                if (userAttributes.containsKey("realm_access")) {
                    var realmAccess =  (Map<String,Object>) userAttributes.get("realm_access");
                    var roles =  (Collection<String>) realmAccess.get("roles");
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            }

            return mappedAuthorities;
        };
    }

Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
}

OAuth2资源服务器

如果你的应用程序被配置为OAuth2资源服务器,那么你可以从访问令牌中提取角色。在你的Spring Security配置中,定义一个JwtAuthenticationConverter bean,它从访问令牌中提取角色,并将它们Map到GrantedAuthority。在这里,我将包括特定bean的外观。在我的GitHub上有一个完整的示例:https://github.com/ThomasVitale/spring-security-examples/tree/main/oauth2/resource-server-jwt-authorities

public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() {
    Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = jwt -> {
        Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
        Collection<String> roles = realmAccess.get("roles");
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    };

    var jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);

    return jwtAuthenticationConverter;
}
xmq68pz9

xmq68pz92#

我使用以下配置:

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // We can safely disable CSRF protection on the REST API because we do not rely on cookies (https://security.stackexchange.com/questions/166724/should-i-use-csrf-protection-on-rest-api-endpoints)
        http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringAntMatchers("/api/**"));
        http.cors();
        http.authorizeRequests(registry -> {
            registry.mvcMatchers("/api-docs/**", "/architecture-docs/**").permitAll();
            registry.mvcMatchers("/api/integrationtest/**").permitAll();
            registry.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
            registry.mvcMatchers("/actuator/info", "/actuator/health").permitAll();
            registry.anyRequest().authenticated();
        });
        http.oauth2ResourceServer()
            .jwt()
            .jwtAuthenticationConverter(jwtAuthenticationConverter());
    }

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // Although this seems like useless code,
        // it is required to prevent Spring Boot creating a default password
        return super.authenticationManagerBean();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwtToAuthorityConverter());
        return converter;
    }

    @Bean
    public Converter<Jwt, Collection<GrantedAuthority>> jwtToAuthorityConverter() {
        return new Converter<Jwt, Collection<GrantedAuthority>>() {
            @Override
            public List<GrantedAuthority> convert(Jwt jwt) {
                Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
                if (realmAccess != null) {
                    @SuppressWarnings("unchecked")
                    List<String> roles = (List<String>) realmAccess.get("roles");
                    if (roles != null) {
                        return roles.stream()
                                    .map(rn -> new SimpleGrantedAuthority("ROLE_" + rn))
                                    .collect(Collectors.toList());
                    }
                }

                return Collections.emptyList();
            }
        };
    }
}

具有这些依赖项:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

而这个属性:

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8181/auth/realms/myrealm

额外提示:使用https://github.com/ch4mpy/spring-addons进行测试。您还可以查看配置示例(与我所做的不同,但应该也能正常工作,请参阅https://github.com/ch4mpy/spring-addons/issues/27以了解有关这些差异的更多信息):https://github.com/ch4mpy/starter/tree/master/api/webmvc/common-security-webmvc/src/main/java/com/c4_soft/commons/security

相关问题