今天我们尝试 Spring Security 整合 Keycloak,并决定建立一个非常简单的 Spring Boot 微服务,使用 Keycloak 作为我的身份验证源,使用 Spring Security 处理身份验证和授权。
设置 Keycloak
首先我们需要一个 Keycloak 实例,让我们启动 Jboss 提供的 Docker 容器:
docker run -d \
–name springboot-security-keycloak-integration \
-e KEYCLOAK_USER=admin \
-e KEYCLOAK_PASSWORD=admin \
-p 9001:8080 \
jboss/keycloak
在此之后,我们只需登录到容器并导航到 bin 文件夹。
docker exec -it springboot-security-keycloak-integration /bin/bash
cd keycloak/bin
首先,我们需要从 CLI 客户端登录 keycloak 服务器,之后我们不再需要身份验证:
./kcadm.sh config credentials –server http://localhost:8080/auth –realm master –user admin –password admin
配置 realm
首先,我们需要创建一个 realm:
./kcadm.sh create realms -s realm=springboot-security-keycloak-integration -s enabled=true
Created new realm with id ‘springboot-security-keycloak-integration’
之后,我们需要创建 2 个客户端,这将为我们的应用程序提供身份验证。首先我们创建一个 cURL 客户端,这样我们就可以通过命令行命令登录:
./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=curl -s enabled=true -s publicClient=true -s baseUrl=http://localhost:8080 -s adminUrl=http://localhost:8080 -s directAccessGrantsEnabled=true
Created new client with id ‘8f0481cd-3bbb-4659-850f-6088466a4d89’
重要的是要注意 2 个选项:publicClient=true 和 directAccessGrantsEnabled=true。第一个使这个客户端公开,这意味着我们的 cURL 客户端可以在不提供任何秘密的情况下启动登录。第二个使我们能够使用用户名和密码直接登录。
其次,我们创建了一个由 REST 服务使用的客户端:
./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=springboot-security-keycloak-integration-client -s enabled=true -s baseUrl=http://localhost:8080 -s bearerOnly=true
Created new client with id ‘ab9d404e-6d5b-40ac-9bc3-9e2e26b68213’
这里的重要配置是 bearerOnly=true。这告诉 Keycloak 客户端永远不会启动登录过程,但是当它收到 Bearer 令牌时,它将检查所述令牌的有效性。
我们应该注意保留这些 ID,因为我们将在接下来的步骤中使用它们。
我们有两个客户端,接下来是为 spring-security-keycloak-example-app 客户创建角色
Admin Role:
./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=admin -s ‘description=Admin role’
Created new role with id ‘admin’
User Role:
./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=user -s ‘description=User role’
Created new role with id ‘user’
注意 client 后的 id 是我们创建客户端输出的 id
最后,我们应该获取客户端的配置,以便稍后提供给我们的应用程序:
./kcadm.sh get clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/installation/providers/keycloak-oidc-keycloak-json -r springboot-security-keycloak-integration
注意 client 后的 id 是我们创建客户端输出的 id
应该返回类似于此的内容:
{
“realm” : “springboot-security-keycloak-integration”,
“bearer-only” : true,
“auth-server-url” : “http://localhost:8080/auth”,
“ssl-required” : “external”,
“resource” : “springboot-security-keycloak-integration-client”,
“verify-token-audience” : true,
“use-resource-role-mappings” : true,
“confidential-port” : 0
}
配置用户
出于演示目的,我们创建 2 个具有 2 个不同角色的用户,以便我们验证授权是否有效。
首先,让我们创建一个具有 admin 角色的用户:
创建 admin 用户:
./kcadm.sh create users -r springboot-security-keycloak-integration -s username=admin -s enabled=true
Created new user with id ’50c11a76-a8ff-42b1-80cb-d82cb3e7616d’
设置 admin 密码:
./kcadm.sh update users/50c11a76-a8ff-42b1-80cb-d82cb3e7616d/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -n
value: 用户密码
追加到 admin 角色中
./kcadm.sh add-roles -r springboot-security-keycloak-integration –uusername=admin –cclientid springboot-security-keycloak-integration-client –rolename admin
注意:从不在生产中使用此方法,它仅用于演示目的!
然后我们创建另一个用户,这次有角色 user:
创建 user 用户:
./kcadm.sh create users -r springboot-security-keycloak-integration -s username=user -s enabled=true
Created new user with id ‘624434c8-bce4-4b5b-b81f-e77304785803’
设置 user 密码:
./kcadm.sh update users/624434c8-bce4-4b5b-b81f-e77304785803/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -n
追加到 user 角色中:
./kcadm.sh add-roles -r springboot-security-keycloak-integration –uusername=user –cclientid springboot-security-keycloak-integration-client –rolename user
Rest 服务
我们已经配置了 Keycloak 并准备使用,我们只需要一个应用程序来使用它!所以我们创建一个简单的 Spring Boot 应用程序。我会在这里使用 maven 构建项目:
<project xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns=”http://maven.apache.org/POM/4.0.0″
xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd”>
<modelVersion>4.0.0</modelVersion>
<groupId>com.edurt.sski</groupId>
<artifactId>springboot-security-keycloak-integration</artifactId>
<packaging>jar</packaging>
<version>1.0.0</version>
<name>springboot security keycloak integration</name>
<description>SpringBoot Security KeyCloak Integration is a open source springboot, spring security, keycloak
integration example.
</description>
<properties>
<!– dependency config –>
<dependency.lombox.version>1.16.16</dependency.lombox.version>
<dependency.springboot.common.version>1.5.6.RELEASE</dependency.springboot.common.version>
<dependency.keycloak.version>3.1.0.Final</dependency.keycloak.version>
<!– plugin config –>
<plugin.maven.compiler.version>3.3</plugin.maven.compiler.version>
<plugin.maven.javadoc.version>2.10.4</plugin.maven.javadoc.version>
<!– environment config –>
<environment.compile.java.version>1.8</environment.compile.java.version>
<!– reporting config –>
<reporting.maven.jxr.version>2.5</reporting.maven.jxr.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${dependency.springboot.common.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!– lombok –>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${dependency.lombox.version}</version>
</dependency>
<!– springboot –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!– keycloak –>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${dependency.keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>${dependency.keycloak.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${plugin.maven.compiler.version}</version>
<configuration>
<source>${environment.compile.java.version}</source>
<target>${environment.compile.java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${plugin.maven.javadoc.version}</version>
<configuration>
<aggregate>true</aggregate>
<!– custom tags –>
<tags>
<tag>
<name>Description</name>
<placement>test</placement>
<head>description</head>
</tag>
</tags>
<!– close jdoclint check document –>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jxr-plugin</artifactId>
<version>${reporting.maven.jxr.version}</version>
</plugin>
</plugins>
</reporting>
</project>
添加所有必需的依赖项:
spring-security 用于保护应用程序
keycloak-spring-boot-starter 使用 Keycloak 和 Spring Boot
keycloak-spring-security-adapter 与 Spring Security 集成
一个简单的应用类:
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* “License”); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an “AS IS” BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.edurt.sski;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* <p> SpringBootSecurityKeyCloakIntegration </p>
* <p> Description : SpringBootSecurityKeyCloakIntegration </p>
* <p> Author : qianmoQ </p>
* <p> Version : 1.0 </p>
* <p> Create Time : 2019-02-18 14:45 </p>
* <p> Author Email: <a href=”mailTo:shichengoooo@163.com”>qianmoQ</a> </p>
*/
@SpringBootApplication
public class SpringBootSecurityKeyCloakIntegration {
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurityKeyCloakIntegration.class, args);
}
}
Rest API 接口:
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* “License”); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an “AS IS” BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.edurt.sski.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p> HelloController </p>
* <p> Description : HelloController </p>
* <p> Author : qianmoQ </p>
* <p> Version : 1.0 </p>
* <p> Create Time : 2019-02-18 14:50 </p>
* <p> Author Email: <a href=”mailTo:shichengoooo@163.com”>qianmoQ</a> </p>
*/
@RestController
public class HelloController {
@GetMapping(value = “/admin”)
@Secured(“ROLE_ADMIN”)
public String admin() {
return “Admin”;
}
@GetMapping(“/user”)
@Secured(“ROLE_USER”)
public String user() {
return “User”;
}
}
最后是 keycloak 配置:
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* “License”); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an “AS IS” BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.edurt.sski.config;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
* <p> KeycloakSecurityConfigurer </p>
* <p> Description : KeycloakSecurityConfigurer </p>
* <p> Author : qianmoQ </p>
* <p> Version : 1.0 </p>
* <p> Create Time : 2019-02-18 14:51 </p>
* <p> Author Email: <a href=”mailTo:shichengoooo@163.com”>qianmoQ</a> </p>
*/
@Configuration
@EnableWebSecurity
public class KeycloakSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter {
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setConvertToUpperCase(true);
return mapper;
}
@Override
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper());
return provider;
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.antMatchers(“/admin”).hasRole(“ADMIN”)
.antMatchers(“/user”).hasRole(“USER”)
.anyRequest().permitAll();
}
@Bean
KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
final KeycloakAuthenticationProcessingFilter filter) {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
final KeycloakPreAuthActionsFilter filter) {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
}
KeycloakSecurityConfigurer 类扩展 KeycloakWebSecurityConfigurerAdapter,这是 Keycloak 提供的类,它提供与 Spring Security 的集成。
然后我们通过添加 SimpleAuthorityMapper 配置身份验证管理器,它负责转换来自 Keycloak 的角色名称以匹配 Spring Security 的约定。基本上 Spring Security 期望以 ROLE_前缀开头的角色,ROLE_ADMIN 可以像 Keycloak 一样命名我们的角色,或者我们可以将它们命名为 admin,然后使用此映射器将其转换为大写并添加必要的 ROLE_前缀:
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setConvertToUpperCase(true);
return mapper;
}
@Override
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper());
return provider;
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
我们还需要为 Keycloak 设置会话策略,但是当我们创建无状态 REST 服务时,我们并不真的想要有会话,因此我们使用 NullAuthenticatedSessionStrategy:
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
通常,Keycloak Spring Security 集成从 keycloak.json 文件中解析 keycloak 配置,但是我们希望有适当的 Spring Boot 配置,因此我们使用 Spring Boot 覆盖配置解析器:
@Bean
KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
然后我们配置 Spring Security 来授权所有请求:
@Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.anyRequest().permitAll();
}
最后,根据文档,我们阻止双重注册 Keycloak 的过滤器:
@Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
final KeycloakAuthenticationProcessingFilter filter) {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
final KeycloakPreAuthActionsFilter filter) {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
最后,我们需要 application.properties 使用之前下载的值配置我们的应用程序:
server.port=9002
keycloak.realm=springboot-security-keycloak-integration
keycloak.bearer-only=true
keycloak.auth-server-url=http://localhost:9001/auth
keycloak.ssl-required=external
keycloak.resource=springboot-security-keycloak-integration-client
keycloak.use-resource-role-mappings=true
keycloak.principal-attribute=preferred_username
使用应用程序
使用 curl 我们创建的客户端进行身份验证,以获取访问令牌:
export TOKEN=`curl -ss –data “grant_type=password&client_id=curl&username=admin&password=admin” http://localhost:9001/auth/realms/springboot-security-keycloak-integration/protocol/openid-connect/token | jq -r .access_token`
这将收到的访问令牌存储在 TOKEN 变量中。
现在我们可以检查我们的管理员是否可以访问自己的 /admin 接口
curl -H “Authorization: bearer $TOKEN” http://localhost:9002/admin
Admin
但它无法访问 /user 接口:
$ curl -H “Authorization: bearer $TOKEN” http://localhost:9002/user
{“timestamp”:1498728302626,”status”:403,”error”:”Forbidden”,”message”:”Access is denied”,”path”:”/user”}
对于 user 用户也是如此,user 用户无法访问 admin 接口。
源码地址:GitHub