本文描述了在Spring Boot应用中通过Spring Security集成Keycloak来实现用认证和鉴权。
工具和环境:
<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>
<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>
<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>
以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
说明:
8180
。参见:
在Keycloak上新建Realm、Client、Role和User:
xdevops
springboot-keycloak-demo
http://localhost:8080/
(对应的Valid Redirect URL为http://localhost:8080/*
)admin
- 管理员user
- 普通用户admin
角色下创建william
用户,在user
角色下创建john
用户。参见:
在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
}
创建一个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
的用法参见:
在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()
参见:
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。
Tips: 在Chrome浏览器中按
F12
, 在Console菜单中勾选“Disable Cache”来方便调试页面时不受缓存影响。
本文的完整代码示例: