Springboot中结合Keycloak和Spring security 开启细粒度权限控制
背景说明
Keycloak作为开源轻量级的统一账号管理系统,可以帮助我们快速搭建一个完整,安全的支持单点登录,开放平台,鉴权及授权的集中式账号管理系统。
加上Keycloak的Authorization Services,提供给我们灵活强大的细粒度权限控制,而不再局限在角色访问控制。
这篇文章重点在于,如何在springboot中集成spring security,以及keycloak,以及配置相关访问权限配置。
而 Keycloak服务的搭建,领域的配置,Authorization Services的配置则不在这里涉及。
引入依赖:Spring Security Adapter
Keycloak官方文档,提供了 Keycloak Java适配器的相关配置说明,集成Springboot说明,以及 Spring security中的集成,这篇文章主要参考
https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_security_adapter
可以作为官方文档的一个实践。
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>12.0.1</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>12.0.1</version>
</dependency>
结合spring security则必须加上 spring security的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
keycloak.json配置
keycloak的配置可以放在 application.yaml中,也可以单独放在自己的keycloak.json中;
放在不同位置,只需要在下面的配置文件中添加对应的resolver即可;
在这里,我们放在单独的keycloak.json中
{
"realm": "kulchao-mall",
"auth-server-url": "http://127.0.0.1:8018/auth",
"ssl-required": "none",
"resource": "mall_system",
"bearer-only": true,
"credentials": {
"secret": "a9e0d2ce-25a4-4e10-b8bf-577f28468a42"
},
"policy-enforcer": {
"paths": [
{
"name": "commodity",
"path": "/",
"methods": [
{
"method": "GET",
"scopes": ["view"]
},
{
"method": "POST",
"scopes": ["edit"]
}
]
}
]
}
}
KeycloakSecurityConfig配置
完成上述步骤后,就需要参照Spring Security的使用,配置下SecurityConfig,形式如下:
@EnableWebSecurity(debug = true)
@KeycloakConfiguration
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
...
}
下面一步步讲解其中需要注意的地方:
配置如何读取keycloak
我们这里把keycloak的配置放在了单独的keycloak.json文件,对应的就需要配置一下读取该文件信息:
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakConfigResolver() {
private KeycloakDeployment keycloakDeployment;
@Override
public KeycloakDeployment resolve(HttpFacade.Request facade) {
if (keycloakDeployment != null) {
return keycloakDeployment;
}
String path = "/keycloak.json";
InputStream configInputStream = getClass().getResourceAsStream(path);
if (configInputStream == null) {
throw new RuntimeException("Could not load Keycloak deployment info: " + path);
} else {
keycloakDeployment = KeycloakDeploymentBuilder.build(configInputStream);
}
return keycloakDeployment;
}
};
}
具体如何起作用,查看下源码都是很清楚,不再赘述;
配置会话鉴权策略
SessionAuthenticationStrategy
主要用于会话相关的防护,比如:固定会话攻击等.
这里需要我们配一下具体的策略,相应的说明如下:
- public or confidential 应用设置为
RegisterSessionAuthenticationStrategy
- bearer-only应用设置为
NullAuthenticatedSessionStrategy
对应的配置如下:
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
// return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
return new NullAuthenticatedSessionStrategy();
}
避免重复注册过滤器
由于KeycloakSpringBoot中会进行提前注册相关的过滤器,然后在Spring Security中也进行相关过滤器的添加,会导致重复注册;
我们只需要在 securityconfig中添加FilterRegistrationBean,则对应的@ConditionalOnMissingBean注解条件不成立,就可以避免上述情况的发生.
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// avoid double registration
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
完整的配置如下
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
import java.io.InputStream;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticatedActionsFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;
import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
*
* @author admin
*/
@EnableWebSecurity(debug = true)
@KeycloakConfiguration
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private final static Logger LOGGER = LoggerFactory.getLogger(KeycloakSecurityConfig.class);
/**
* Registers the KeycloakAuthenticationProvider with the authentication
* manager.
*
* @param auth
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keyCloakAuthProvider = keycloakAuthenticationProvider();
keyCloakAuthProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keyCloakAuthProvider);
}
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
// return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
return new NullAuthenticatedSessionStrategy();
}
@Override
protected AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, authException) -> {
LOGGER.warn("401 Unauthorized while processing " + request.getRequestURI(), authException);
response.sendError(401, "Unauthorized");
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors().and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and();
http.authorizeRequests().anyRequest().authenticated().and();
}
/**
* Overrides default keycloak config resolver behaviour
* (/WEB-INF/keycloak.json) by a simple mechanism.
* <p>
* This example loads other-keycloak.json when the parameter use.other is
* set to true, e.g.: {@code ./gradlew bootRun -Duse.other=true}
*
* @return keycloak config resolver
*/
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakConfigResolver() {
private KeycloakDeployment keycloakDeployment;
@Override
public KeycloakDeployment resolve(HttpFacade.Request facade) {
if (keycloakDeployment != null) {
return keycloakDeployment;
}
String path = "/keycloak.json";
InputStream configInputStream = getClass().getResourceAsStream(path);
if (configInputStream == null) {
throw new RuntimeException("Could not load Keycloak deployment info: " + path);
} else {
keycloakDeployment = KeycloakDeploymentBuilder.build(configInputStream);
}
return keycloakDeployment;
}
};
}
// avoids double registration of keycloak filters
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@SuppressWarnings("unchecked")
public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// avoid double registration
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
}
Policy Enforcers相关配置
只要policy-enforcer有定义,就会开启 PEP流程
如
{
"policy-enforcer": {}
}
paths主要配置如下:
- name: 资源名称,需要跟资源服务上的资源名称对应起来.定义了该属性值,则uris匹配规则失效
- path: 资源路径;在不配置name的情况下,根据path匹配对应的资源;
- methods-method: 请求方法
- methods-scopes: 需要的scopes数组
- methods-scopes-enforcement-mode: 表明上面的scopes是只需要一个满足(ANY)还是都需要满足(ALL)
- enforcement-mode: enforcement执行的模式是强制还是关闭