我一直在遵循本教程:https://www.baeldung.com/spring-boot-keycloak使用Keycloak设置我的Spring应用程序。身份验证工作正常,但是当我想添加基于角色的身份验证时,例如
.requestMatchers(new AntPathRequestMatcher("/api/v1/user/**")).hasRole("user")
我总是收到以下错误
Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
我已经使用
检查了权威机构SecurityContextHolder.getContext().getAuthentication().getAuthorities();
并且它只有带有
SCOPE_
前缀的。
我还检查了令牌,它正确返回了这个
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"default-master-realm",
"user"
]
},
除了其他领域。
这是我的
KeycloakConfig
课程:
package com.motus.core.shared.config.security;
import com.motus.auth.constants.Authority;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@EnableWebSecurity
@Configuration
public class KeycloakConfig {
private static final String GROUPS = "groups";
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(sessionRegistry());
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, KeycloakLogoutHandler keycloakLogoutHandler) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/v1/public/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/v1/auth/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/v1/admin/**")).hasRole("admin")
.requestMatchers(new AntPathRequestMatcher("/api/v1/user/**")).hasRole("user")
.anyRequest().authenticated()
);
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
http.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
http.csrf(AbstractHttpConfigurer::disable);
http.cors(Customizer.withDefaults());
return http.build();
}
@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_CLAIM)) {
var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
} else if (userInfo.hasClaim(GROUPS)) {
Collection<String> roles = userInfo.getClaim(
GROUPS);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(
REALM_ACCESS_CLAIM);
Collection<String> roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
return mappedAuthorities;
};
}
Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
Collectors.toList());
}
}
看起来GrantedAuthoritesMapper没有被调用,你知道为什么吗? 这是我在
pom.xml
中添加的内容:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
这是我在 application.properties 中添加的内容:
spring.security.oauth2.client.registration.keycloak.client-id=${KEYCLOAK_CLIENT_ID:test-backend}
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=${KEYCLOAK_AUTHORIZATION_GRANT_TYPE:password}
spring.security.oauth2.client.registration.keycloak.scope=${KEYCLOAK_SCOPE:openid}
spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_ISSUER_URI:http://localhost:8080/realms/master}
spring.security.oauth2.client.provider.keycloak.user-name-attribute=${KEYCLOAK_USER_NAME_ATTRIBUTE:username}
spring.security.oauth2.resourceserver.jwt.issuer-uri=${KEYCLOAK_RESOURCE_SERVER_JWT_ISSUER_URI:http://localhost:8080/realms/master}
我使用
http://localhost:8080/realms/master/protocol/openid-connect/token
获取令牌
然后我使用获得的令牌将请求发送到我的应用程序,但我总是收到上述错误。有谁知道我该如何解决这个问题?
hasRole('user')
需要ROLE_user
权限将 Keycloak 角色映射到 Spring Security 权限时,用户
hasAuthority('user')
或添加 ROLE_
前缀。
oauth2ResourceServer
和oauth2Login
Spring Boot 提供不同的启动器是有原因的。我知道 Baeldung 教程就是这样制作的(以及许多其他作者的教程,他们在不了解自己在做什么的情况下互相复制),但这完全是无稽之谈:
oauth2ResourceServer
正在配置一个 OAuth2 资源服务器,其中:
Bearer
标头中的 Authorization
令牌授权请求401 Unauthorized
标头丢失或无效时,应返回
Authorization
Converter<Jwt, ? extends AbstractAuthenticationToken>
中的权限(如果使用JWT解码,或为不透明令牌自定义内省器)oauth2Login
正在使用授权代码配置 OAuth2 客户端,其中:
Bearer
标头中的 Authorization
令牌)302 Redirect to login
GrantedAuthoritiesMapper
(或 使用自定义 OAuth2UserService)映射权限这些要求差异太大,无法放在同一个
Security(Web)FilterChain
bean 中。
使用授权代码流进行用户“登录”的客户端也可以是资源服务器,但对于不同的消费者(可能还有不同的端点):
您可能应该阅读我的教程简介以获得更多 OAuth2 背景知识。
我在同一个存储库中托管的附加启动器可以帮助您仅使用属性为Keycloak配置Spring应用程序(这些Spring应用程序主要是带有
oauth2Login
的OAuth2客户端或OAuth2资源服务器)