前后端分离的项目中在后端使用Spring Security与Keycloak用openid的方式集成

因为项目的要求,第一次了解keycloak,之前也没接触过,所以只能摸着石头过河,一步一步的探索,中间踩了不少坑,这些坑值得总结一下,本人也是小白,如果有理解不对的地方,希望大神们多多指教。
顺便说一下,项目是前后端分离,这里先只介绍后端部分
(因为前端不会,嘻嘻)

安装keycloak

windows版本安装

先贴出官方下载地址:keycloak 官方下载地址.选择自己想要的版本。

我用的是postgres数据库,如果使用别的数据库,可以参考其他文章,这种安装文章网上有很多的。

  1. 先在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包下载地址

  1. 需要修改的文件:\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的页面设置先跳过,直接附上后端代码部分

  1. 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>
  1. 创建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类里的配置

  1. 这个类里还有一个比较重要的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"
  }
}
  1. 还有一个值得说的是加入的过滤器:将这个过滤器加入到config中
    .and()
    .addFilterBefore(keycloakAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
    /**
     * 将keycloak的过滤器加入到spring security中
     */
    @Bean
    public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
        return new KeycloakAuthenticationProcessingFilter(authenticationManager());
    }
  1. 剩下的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的角色匹配上
  • 剩下的等我想到了再补充吧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值