Spring Security整合KeyCloak保护Rest API

10次阅读

共计 16303 个字符,预计需要花费 41 分钟才能阅读完成。

今天我们尝试 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

正文完
 0