在 Spring Boot 中扩展 Keycloak 令牌
Posted
技术标签:
【中文标题】在 Spring Boot 中扩展 Keycloak 令牌【英文标题】:Extend Keycloak token in Spring boot 【发布时间】: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 是如何设置的? 终于可以自己动手了。谢谢你的时间。我会尽快发布答案 【参考方案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
..
【讨论】:
以上是关于在 Spring Boot 中扩展 Keycloak 令牌的主要内容,如果未能解决你的问题,请参考以下文章
开源 Spring Boot 中 Mongodb 多数据源扩展框架