文章目录
在Spring Boot应用中集成Keycloak作认证和鉴权
前言
本文描述了在Spring Boot应用中通过Spring Security集成Keycloak来实现用认证和鉴权。
工具和环境:
- Spring Boot 2.4.0
- Spring Security
- Spring Boot Thymeleaf
- Keycloak 12.0.1
引入依赖
Spring Security依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
Keycloak依赖
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>12.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Thymeleaf依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
安装Keycloak
以Docker方式安装Keycloak:
#!/bin/bash
# Create a user defined network
docker network create keycloak-network
# Start a MySQL instance
docker run --name keycloak-mysql \
-d \
--net keycloak-network \
-v $HOME/keycloak/mysql-data:/var/lib/mysql \
-e MYSQL_DATABASE=keycloak \
-e MYSQL_USER=keycloak \
-e MYSQL_PASSWORD=keycloak123 \
-e MYSQL_ROOT_PASSWORD=keycloak123 \
mysql:8.0
# Start a Keycloak instance
docker run --name keycloak \
-d \
--net keycloak-network \
-p 8180:8080 \
-e DB_VENDOR=mysql \
-e DB_ADDR=keycloak-mysql \
-e DB_DATABASE=keycloak \
-e DB_USER=keycloak \
-e DB_PASSWORD=keycloak123 \
-e KEYCLOAK_USER=admin \
-e KEYCLOAK_PASSWORD=admin \
quay.io/keycloak/keycloak:12.0.1
# check logs
# docker logs -f keycloak
说明:
- 采用MySQL来持久化Keycloak配置。
- 设置Keycloak的端口为
8180
。
参见:
- https://www.keycloak.org/getting-started/getting-started-docker
- https://hub.docker.com/r/jboss/keycloak/
在Keycloak上配置
在Keycloak上新建Realm、Client、Role和User:
- 创建一个新Realm -
xdevops
- 在该Realm下创建一个Client
- Client ID -
springboot-keycloak-demo
- Root URL -
http://localhost:8080/
(对应的Valid Redirect URL为http://localhost:8080/*
)
- Client ID -
- 在该Realm下创建两个Role
admin
- 管理员user
- 普通用户
- 在
admin
角色下创建william
用户,在user
角色下创建john
用户。
参见:
构建Spring Boot应用
配置Keycloak属性
在application.yaml
中配置Keycloak属性:
keycloak:
# the name of the realm, required
realm: xdevops
# the client-id of the application, required
resource: springboot-keycloak-demo
# the base URL of the Keycloak server, required
auth-server-url: http://localhost:8180/auth
# establishes if communications with the Keycloak server must happen over HTTPS
# set to external, meaning that it's only needed for external requests (default value)
# In production, instead, we should set it to all. Optional
ssl-required: external
# prevents the application from sending credentials to the Keycloak server (false is the default value)
# set it to true whenever we use public clients instead of confidential
public-client: true
# the attribute with which to populate the UserPrincipal name
principal-attribute: preferred_username
说明:
-
realm
为上面创建的Relam。 -
resource
为上面创建的Client ID。 -
auth-server-url
为Keycloak server的auth url。 -
默认创建的Client的Access Type为
public
,所以这里设置public-client
为true
-
principal-attribute: preferred_username
表示用Keycloak User的preferred_username
属性作为Spring Security Principal的name
。
如果需要配置Client的Access Type为confidential
,则需要在应用配置中设置public-client
为false,并提供Client Secret。
示例如下:
# prevents the application from sending credentials to the Keycloak server (false is the default value)
# set it to true whenever we use public clients instead of confidential
# when access type in keycloak is `confidential` must set `public-client: false`
public-client: false
# client secret: client id and secret
# https://www.keycloak.org/docs/latest/securing_apps/index.html#_client_authentication_adapter
credentials:
secret: "<client-secret>"
参见:
可以在Keycloak中查看Client Settings,打开Installation页,选择“Keycloak OIDC JSON”格式来查看Keycloak属性:
示例:
{
"realm": "xdevops",
"auth-server-url": "http://localhost:8180/auth/",
"ssl-required": "external",
"resource": "springboot-keycloak-demo",
"credentials": {
"secret": "<client-secret>"
},
"confidential-port": 0
}
Keycloak安全配置
创建一个SecurityConfig
类:
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/manager").hasRole("admin")
.antMatchers("/books").hasAnyRole("user", "admin")
.anyRequest().permitAll();
}
/**
* Make sure roles are not prefixed with ROLE_.
* @param builder
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder builder) {
KeycloakAuthenticationProvider provider = keycloakAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
builder.authenticationProvider(provider);
}
/**
* Use the Spring Boot application properties file support instead of the default keycloak.json.
* @return
*/
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
}
说明:
- 访问控制
- 配置了只有
admin
角色时才能访问/manager
端点。 - 配置了只有
user
或admin
角色时才能访问/books
端点。 - 访问其他端点,不作控制。
- 配置了只有
- 注入
configureGlobal
,不让Spring Security默认在Role前添加ROLE_
。 - 注入
keycloakConfigResolver
,让Spring Boot从application properties/yaml 中读取Keycloak配置,而不是从默认的类路径的key cloak.json
中读取配置。
关于httpsecurity
的用法参见:
Web层
在LibraryController
类中定义了两个端点:
/books
- 普通用户或管理员都可以浏览图书。/manager
- 管理员才可以管理图书。
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
@Controller
public class LibraryController {
private final BookRepository bookRepository;
public LibraryController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping("/books")
public String getBooks(Model model, Principal principal) {
model.addAttribute("books", bookRepository.readAll());
model.addAttribute("name", principal.getName());
return "books";
}
@GetMapping("/manager")
public String manageBooks(Model model, HttpServletRequest request) {
model.addAttribute("books", bookRepository.readAll());
model.addAttribute("name", SecurityUtils.getIDToken(request).getGivenName());
return "manager";
}
}
说明:
getBooks
方法演示了直接通过Spring Security Principal获取当前用户的名称,这里是Keycloak User的preferred_username
。manageBooks
方法演示了通过一个工具类从request中获取Keycloak User的详细信息。
其他获取当前用户信息的方式:
// 在Controller方法中传入Authentication对象
authentication.getName()
// 在Controller方法中传入HttpServletRequest对象
request.getUserPrincipal().getName()
// 通过SecurityContextHolder工具类获取
SecurityContextHolder.getContext().getAuthentication().getName()
参见:
Keycloak工具类
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.IDToken;
import javax.servlet.http.HttpServletRequest;
public final class SecurityUtils {
private SecurityUtils() {
}
public static KeycloakSecurityContext getKeycloakSecurityContext(HttpServletRequest request) {
return (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
}
public static IDToken getIDToken(HttpServletRequest request) {
return SecurityUtils.getKeycloakSecurityContext(request).getIdToken();
}
}
说明:
getIDToken
方法返回了Keycloak security context,其中包含当前登录的Keycloak User的详细信息。
测试访问页面
在浏览器中访问http://localhost:8080。
- 全部用户都可以访问Home页。
- 只有普通用户和管理员都可以访问Browse Books页,看到图书列表。
- 只有管理员才可以访问Manage Library页,看到一张图书馆照片。
- 访问Browse Books页和Manage Library页时,如果用户还未登录,则会跳转到Keycloak的登录页面。
Tips: 在Chrome浏览器中按
F12
, 在Console菜单中勾选“Disable Cache”来方便调试页面时不受缓存影响。
小结
本文的完整代码示例:
参考文档
- Keycloak Spring Boot Adapter
- Keycloak Spring Security Adapter
- A Quick Guide to Using Keycloak with Spring Boot
- https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-keycloak
- Spring Security and Keycloak to Secure a Spring Boot Application - A First Look
- https://github.com/ThomasVitale/spring-keycloak-tutorials/tree/master/keycloak-spring-security-first-look
- Easily secure your Spring Boot applications with Keycloak