Spring Security与Keycloak用openid的方式集成
因为项目的要求,第一次了解keycloak,之前也没接触过,所以只能摸着石头过河,一步一步的探索,中间踩了不少坑,这些坑值得总结一下,本人也是小白,如果有理解不对的地方,希望大神们多多指教。
顺便说一下,项目是前后端分离,这里先只介绍后端部分
(因为前端不会,嘻嘻)
安装keycloak
windows版本安装
先贴出官方下载地址:keycloak 官方下载地址.选择自己想要的版本。
我用的是postgres数据库,如果使用别的数据库,可以参考其他文章,这种安装文章网上有很多的。
- 先在keycloak-15.1.1\modules\system\layers\keycloak\org目录下创建名字为postgresql的文件夹,在postgresql文件夹下创建名字为main的文件夹,在main文件夹下创建名字为module.xml的文件,另外在将postgres的jar包贴到main文件夹下。
module.xml:
<?xml version="1.0" encoding="UTF-8"?>
<module name="org.postgresql" xmlns="urn:jboss:module:1.5">
<resources>
<!-- 注意修改path里的值为自己下载的jar包 -->
<resource-root path="postgresql-42.3.3.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
<module name="javax.servlet.api" optional="true"/>
</dependencies>
</module>
postgres的jar包下载地址:jar包下载地址
- 需要修改的文件:\keycloak-15.1.1\standalone\configuration\standalone.xml
直接贴上我的配置文件
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
<datasources>
<!-- 这一部分为自己需要修改的配置,配置自己的数据库链接参数,为了安全需要,我就把我自己的参数先注释掉了 -->
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
<connection-url>jdbc:postgresql://服务器地址:端口/数据库的名称</connection-url>
<!-- driver与下面配置的driver的name保持一致 -->
<driver>postgresql</driver>
<security>
<user-name>用户名</user-name>
<password>密码</password>
</security>
<pool>
<max-pool-size>100</max-pool-size>
</pool>
</datasource>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
<drivers>
<!-- 这里module的值要与前面自己创建的文件名保持一致 -->
<driver name="postgresql" module="org.postgresql">
<driver-class>org.postgresql.Driver</driver-class>
<xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
</driver>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
如果想要修改端口号,修改下面的参数
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
<socket-binding name="ajp" port="${jboss.ajp.port:8082}"/>
<socket-binding name="http" port="${jboss.http.port:8081}"/>
<!-- 更改端口号需要修改该值 -->
<socket-binding name="https" port="${jboss.https.port:8083}"/>
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9991}"/>
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
<socket-binding name="txn-recovery-environment" port="4712"/>
<socket-binding name="txn-status-manager" port="4713"/>
<outbound-socket-binding name="mail-smtp">
<remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
</outbound-socket-binding>
</socket-binding-group>
点击\keycloak-15.1.1\bin\standalone.bat启动keycloak,在浏览器输入端口号就可以进入到keycloak的登录界面
linux版本安装
我不是用docker的方式安装的,直接在网上在下tar包解压后,剩下的安装步骤就跟windows一样的了
Spring Security与Keycloak集成
keycloak的页面设置先跳过,直接附上后端代码部分
- maven依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>17.0.1</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
<version>17.0.1</version>
</dependency>
- 创建KeycloakSecurityConfig类,继承KeycloakWebSecurityConfigurerAdapter类。因为这个类比较重要,所以这里先说最重要的两个方法,configure(AuthenticationManagerBuilder auth)方法和configure(HttpSecurity http)方法
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomizeLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
@Qualifier("userDetailsServiceImpl")
public UserDetailsService userDetailsService;
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Autowired
private SecurityAuthenticationProvider securityAuthenticationProvider;
/**
* 自定义provider,提供用户认证的方式
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setPrefix("ROLE_");
SecurityAuthenticationProvider securityAuthenticationProvider = this.securityAuthenticationProvider;
securityAuthenticationProvider.setGrantedAuthoritiesMapper(mapper);
auth.authenticationProvider(securityAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 配置资源访问权限
.authorizeRequests()
.antMatchers("/xxxxx").permitAll()
.antMatchers("/private/**").hasRole(AuthorityEnum.ADMIN.getName())
.antMatchers("/public/**").hasRole(AuthorityEnum.USER.getName())
.anyRequest().authenticated()
//配置异常处理
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
//配置登录成功的处理
.and()
.formLogin().loginPage("/authentications/login").loginProcessingUrl("/private/login")
.permitAll().successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
//配置登出的处理
.and()
.logout().logoutUrl("/private/logout").logoutSuccessHandler(logoutSuccessHandler).clearAuthentication(true).deleteCookies("JSESSIONID")
//配置过滤器
.and()
.addFilterBefore(keycloakAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
//配置单点登录的限制 允许同一个账号最多同时登录的数量
.sessionManagement().maximumSessions(5).expiredSessionStrategy(sessionInformationExpiredStrategy);
http.headers().cacheControl();
}
}
需要说明的是:
在configure(AuthenticationManagerBuilder auth)方法里自定义的provider------SecurityAuthenticationProvider:Provider主要是提供认证,可以结合自己的业务需求写自己的认证逻辑,下面是我自己的provider
import com.vgc.price_index.mapper.AuthorityMapper;
import com.vgc.price_index.mapper.UserMapper;
import com.vgc.price_index.model.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.account.KeycloakRole;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.representations.AccessToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
@Component
public class SecurityAuthenticationProvider implements AuthenticationProvider {
private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
@Autowired
//自己定义的mapper
private UserMapper userMapper;
@Autowired
//自己定义的mapper
private AuthorityMapper authorityMapper;
public void setGrantedAuthoritiesMapper(GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
}
/**
* 验证Authentication,建立系统使用者信息principal(token)
* 核心方法就是这个,通过token里的信息解析出登陆人的账号,然后在数据库里查询这个人有哪些权限
*/
@Override
public Authentication authenticate(Authentication authentication) throws RuntimeException {
//从token中获取用户信息
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
AccessToken accessToken = getAccessToken((token));
String ntAccount = accessToken.getPreferredUsername();
//查询用户是否存在,若不存在则存入数据库
User user = userMapper.selectByNtaccount(ntAccount);
if (null == user) return null;
//根据userId查询本系统用户权限,放入token中
List<String> authorities = authorityMapper.allPermissionByNtaccount(user.getNtaccount());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for(String authority : authorities){
KeycloakRole keycloakRole = new KeycloakRole(authority);
grantedAuthorities.add(keycloakRole);
}
return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), mapAuthorities(grantedAuthorities));
}
private AccessToken getAccessToken(KeycloakAuthenticationToken principal) {
KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) principal.getPrincipal();
KeycloakSecurityContext context = keycloakPrincipal.getKeycloakSecurityContext();
return context.getToken();
}
private Collection<? extends GrantedAuthority> mapAuthorities(
Collection<? extends GrantedAuthority> authorities) {
return grantedAuthoritiesMapper != null
? grantedAuthoritiesMapper.mapAuthorities(authorities)
: authorities;
}
@Override
public boolean supports(Class<?> aClass) {
return KeycloakAuthenticationToken.class.isAssignableFrom(aClass);
}
}
在configure(HttpSecurity http)方法里的各种自定义的handler:
authenticationEntryPoint:异常处理的handler,出异常是调用此类
authenticationSuccessHandler:认证成功的处理
authenticationFailureHandler:认证失败的处理
logoutSuccessHandler:登出成功的处理
这些没有贴出代码的原因是,当集成了keycloak之后,keycloak会自己做处理,对应的类名为:
认证成功的处理:KeycloakAuthenticationSuccessHandler
认证失败的处理:KeycloakAuthenticationFailureHandler
异常的处理:KeycloakAuthenticationEntryPoint
关于config(HttpSecurity http)这个方法的配置,可以参考KeycloakWebSecurityConfigurerAdapter类里的配置
- 这个类里还有一个比较重要的bean:KeycloakConfigResolver,他的作用主要是解析keycloak的配置信息,并和token里的信息进行校验。这个有两种配置方式,根据官网的说法,一个是json方式,一个是直接在yml里配置。
这里我选择了json的方式,因为在yml里读不到配置。
/**
* 读取keycloak的配置,keycloak_xxx.json
*/
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakConfigResolver() {
private KeycloakDeployment keycloakDeployment;
@Override
public KeycloakDeployment resolve(HttpFacade.Request facade) {
if (keycloakDeployment != null) {
return keycloakDeployment;
}
//keycloakConfigFilePath是json所在的目录
String path = keycloakConfigFilePath;
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;
}
};
}
json配置如下:
{
"realm": "xxx",
"auth-server-url": "http://xxxx:port/auth",
"ssl-required": "none",
"resource": "xxx",
"bearer-only": true,
"credentials": {
"secret": "xxx"
}
}
- 还有一个值得说的是加入的过滤器:将这个过滤器加入到config中
.and()
.addFilterBefore(keycloakAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
/**
* 将keycloak的过滤器加入到spring security中
*/
@Bean
public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
return new KeycloakAuthenticationProcessingFilter(authenticationManager());
}
- 剩下的bean就是完全按着官网扒下来的, 最终这个类的代码如下
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.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory;
import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
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.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import java.io.InputStream;
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomizeLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
@Qualifier("userDetailsServiceImpl")
public UserDetailsService userDetailsService;
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Autowired
private SecurityAuthenticationProvider securityAuthenticationProvider;
@Value("${keycloak.configFile}")
private String keycloakConfigFilePath;
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 自定义provider,提供用户认证的方式
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setPrefix("ROLE_");
SecurityAuthenticationProvider securityAuthenticationProvider = this.securityAuthenticationProvider;
securityAuthenticationProvider.setGrantedAuthoritiesMapper(mapper);
auth.authenticationProvider(securityAuthenticationProvider);
}
/**
* 读取keycloak的配置,keycloak_xxx.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 = keycloakConfigFilePath;
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;
}
};
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 配置资源访问权限
.authorizeRequests()
.antMatchers("xxx").permitAll()
.antMatchers("/private/**").hasRole(AuthorityEnum.ADMIN.getName())
.antMatchers("/public/**").hasRole(AuthorityEnum.USER.getName())
.anyRequest().authenticated()
//配置异常处理
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
//配置登录成功的处理
.and()
.formLogin().loginPage("/authentications/login").loginProcessingUrl("/private/login")
.permitAll().successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
//配置登出的处理
.and()
.logout().logoutUrl("/private/logout").logoutSuccessHandler(logoutSuccessHandler).clearAuthentication(true).deleteCookies("JSESSIONID")
//配置过滤器
.and()
.addFilterBefore(keycloakAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
//配置单点登录的限制 允许同一个账号最多同时登录的数量
.sessionManagement().maximumSessions(5).expiredSessionStrategy(sessionInformationExpiredStrategy);
http.headers().cacheControl();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Bean
protected SessionRegistry buildSessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
@Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
}
/**
* 将keycloak的过滤器加入到spring security中
*/
@Bean
public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
return new KeycloakAuthenticationProcessingFilter(authenticationManager());
}
}
6.细节
- 在header里放token时要加上前缀Bearer ,注意在Bearer后面有空格。
- 自己定义的数据库角色名要加ROLE_前缀才可以和keycloak的角色匹配上
- 剩下的等我想到了再补充吧。