带有Keycloak的SpringBoot OAuth2不返回映射的角色作为权威
SpringBoot OAuth2 with Keycloak not returning mapped Roles as Authorities
我正在创建一个简单的 SpringBoot 应用程序并尝试与 OAuth 2.0 提供程序 Keycloak 集成。我在领域级别创建了领域、客户端、角色(Member、PremiumMember),最后创建了用户和分配的角色(Member、PremiumMember)。
如果我使用 Keycloak https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_boot_adapter 提供的 SpringBoot 适配器,那么当我成功登录并检查登录用户的权限时,我能够看到分配的角色,例如 Member、PremiumMember。
Collection<? extends GrantedAuthority> authorities =
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
但是如果我使用通用的 SpringBoot Auth2 客户端配置我可以登录但是当我检查权限时它总是只显示 ROLE_USER, SCOPE_email,SCOPE_openid,SCOPE_profile 并且不包括我映射的角色(Member、PremiumMember)。
我的 SpringBoot OAuth2 配置:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
application.properties
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
我正在使用 SpringBoot 2.5.5 和 Keycloak 15.0.2.
使用这种通用的 OAuth2.0 配置方法(不使用 Keycloak SpringBootAdapter)有没有办法获得分配的角色?
我使用这个配置:
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 for testing. You can also take a look there at a configuration sample (which is different from what I do, but should work fine as well, see https://github.com/ch4mpy/spring-addons/issues/27 for more info about those differences): https://github.com/ch4mpy/starter/tree/master/api/webmvc/common-security-webmvc/src/main/java/com/c4_soft/commons/security
默认情况下,Spring 安全性使用 scope
或 scp
声明中的值和 SCOPE_
前缀生成一个 GrantedAuthority
列表。
Keycloak 在嵌套声明中保留领域角色 realm_access.roles
。您有两个选项来提取角色并将它们映射到 GrantedAuthority
.
的列表
OAuth2 客户端
如果您的应用程序配置为 OAuth2 客户端,则您可以从 ID 令牌或 UserInfo 端点中提取角色。 Keycloak 仅在访问令牌中包含角色,因此您需要更改配置以将它们也包含在 ID 令牌或 UserInfo 端点中(这是我在以下示例中使用的)。您可以从 Keycloak 管理控制台执行此操作,转到 Client Scopes > roles > Mappers > realm roles
然后,在您的 Spring 安全配置中,定义一个 GrantedAuthoritiesMapper
,它从 UserInfo 端点提取角色并将它们映射到 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 安全配置中,定义一个 JwtAuthenticationConverter
bean,它从访问令牌中提取角色并将它们映射到 GrantedAuthority
s。在这里,我将包括特定 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;
}
我正在创建一个简单的 SpringBoot 应用程序并尝试与 OAuth 2.0 提供程序 Keycloak 集成。我在领域级别创建了领域、客户端、角色(Member、PremiumMember),最后创建了用户和分配的角色(Member、PremiumMember)。
如果我使用 Keycloak https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_boot_adapter 提供的 SpringBoot 适配器,那么当我成功登录并检查登录用户的权限时,我能够看到分配的角色,例如 Member、PremiumMember。
Collection<? extends GrantedAuthority> authorities =
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
但是如果我使用通用的 SpringBoot Auth2 客户端配置我可以登录但是当我检查权限时它总是只显示 ROLE_USER, SCOPE_email,SCOPE_openid,SCOPE_profile 并且不包括我映射的角色(Member、PremiumMember)。
我的 SpringBoot OAuth2 配置:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
application.properties
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
我正在使用 SpringBoot 2.5.5 和 Keycloak 15.0.2.
使用这种通用的 OAuth2.0 配置方法(不使用 Keycloak SpringBootAdapter)有没有办法获得分配的角色?
我使用这个配置:
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 for testing. You can also take a look there at a configuration sample (which is different from what I do, but should work fine as well, see https://github.com/ch4mpy/spring-addons/issues/27 for more info about those differences): https://github.com/ch4mpy/starter/tree/master/api/webmvc/common-security-webmvc/src/main/java/com/c4_soft/commons/security
默认情况下,Spring 安全性使用 scope
或 scp
声明中的值和 SCOPE_
前缀生成一个 GrantedAuthority
列表。
Keycloak 在嵌套声明中保留领域角色 realm_access.roles
。您有两个选项来提取角色并将它们映射到 GrantedAuthority
.
OAuth2 客户端
如果您的应用程序配置为 OAuth2 客户端,则您可以从 ID 令牌或 UserInfo 端点中提取角色。 Keycloak 仅在访问令牌中包含角色,因此您需要更改配置以将它们也包含在 ID 令牌或 UserInfo 端点中(这是我在以下示例中使用的)。您可以从 Keycloak 管理控制台执行此操作,转到 Client Scopes > roles > Mappers > realm roles
然后,在您的 Spring 安全配置中,定义一个 GrantedAuthoritiesMapper
,它从 UserInfo 端点提取角色并将它们映射到 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 安全配置中,定义一个 JwtAuthenticationConverter
bean,它从访问令牌中提取角色并将它们映射到 GrantedAuthority
s。在这里,我将包括特定 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;
}