Spring Boot(Spring Security)应用集成Keycloak实现统一身份验证、权限控制

10,873 阅读5分钟

前面介绍了前端应用如何集成Keycloak实现统一身份验证、权限控制,可参考vue-element-admin集成Keycloak实现统一身份验证、权限控制。如果对Keycloak还不太了解的话,可以参考Keycloak快速上手指南对Keycloak的基本概念进行了解。本文将讲述典型的Spring Boot/Spring Security服务端的应用如何集成Keycloak,以实现SSO登录、统一身份验证、权限控制等功能。

Keycloak后台配置

创建Keycloak客户端

服务端的web类型的应用,最常见的就是提供web页面服务以及Restful API。不同类型的应用,Keycloak后台创建的配置会稍微有些区别。下面将创建3个客户端:

  • spring-boot-keycloak-web:对应于纯web页面的应用
  • srping-boot-keycloak-security-api:对应于只提供Restful API服务的应用
  • spring-boot-keycloak-webapi:对应于同时提供web页面以及Restful API服务的应用

创建spring-boot-keycloak-web客户端

keycloak-client-1

创建srping-boot-keycloak-security-api客户端

keycloak-client-2

创建spring-boot-keycloak-webapi客户端

keycloak-client-3

客户端访问类型(Access Type)说明

之前的文章已经说明过,这里再列一下,Keycloak目前的访问类型共有3种:

confidential:适用于服务端应用,且需要浏览器登录以及需要通过密钥获取access token的场景。典型的使用场景就是服务端渲染的web系统。

public:适用于客户端应用,且需要浏览器登录的场景。典型的使用场景就是前端web系统,包括采用vue、react实现的前端项目等。

bearer-only:适用于服务端应用,不需要浏览器登录,只允许使用bearer token请求的场景。典型的使用场景就是restful api。

创建角色、用户,并进行绑定

分别创建用户admin、customer,及角色ROLE_ADMIN、ROLE_CUSTOMER,并进行绑定

keycloak-web-add-user-role-1

keycloak-web-add-user-role-2

Spring Boot(Spring Security)集成Keycloak配置及代码

添加Maven依赖

Spring Boot集成keycloak依赖

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>${keycloak.version}</version>
</dependency>

如需使用Spring Security,则添加如下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Keycloak关键配置

Keycloak配置文件示例

这里以同时提供web页面及Restful API服务的项目spring-boot-keycloak-webapi的配置为例,来看下怎么进行keycloak相关的配置

server:
  port: 8083

keycloak:
  realm: demo
  auth-server-url: http://127.0.0.1:8080/auth
  resource: spring-boot-keycloak-webapi
  ssl-required: external
  credentials:
    secret: d50a059a-484b-4138-8403-22a491fbc488
  use-resource-role-mappings: false
  bearer-only: false
  autodetect-bearer-only: true
  security-constraints:
    - authRoles:
        - ROLE_CUSTOMER
      securityCollections:
        - name: customer
          patterns:
            - /customer
    - authRoles:
        - ROLE_ADMIN
      securityCollections:
        - name: admin
          patterns:
            - /admin

realm:Keycloak后台对应的realm

auth-server-url:Keycloak的地址

resource:Keycloak后台创建的对应的Client

credentials.secret:Keycloak添加客户端后Credentials Tab内对应的内容

use-resource-role-mappings:使用realm级别还是应用级别的角色控制

bearer-only:应用的Keycloak访问类型是bearer-only设置为true,否则设为false

autodetect-bearer-only:应用同时提供web页面跟Restful API服务时需设置为true,Keycloak会根据请求的方式,将未通过认证的请求重定向到登录页或者直接返回401状态码

security-constraints:针对不同的路径定义相应的角色以实现权限管理,如果是集成Spring Security,则不需要此配置,改为在Spring Security相关的配置中控制

更多的配置控制可以查阅官方文档:Keycloak官方Java适配器配置

autodetect-bearer-only机制说明

官方文档对于autodetect-bearer-only的说明比较含糊

Keycloak auto-detects SOAP or REST clients based on typical headers like X-Requested-With, SOAPAction or Accept.

针对HTTP请求头到底取什么样的值才被识别为是API类型的请求,其实并没有描述的很清楚。我们只能找到相应的源码来具体看下,实际实现到底是怎么样的。

源码位于keycloak-adapter-core jar包中的RequestAuthenticator抽像类中

protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) {
    if (!deployment.isAutodetectBearerOnly()) return false;

    String headerValue = facade.getRequest().getHeader("X-Requested-With");
    if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) {
        return true;
    }

    headerValue = facade.getRequest().getHeader("Faces-Request");
    if (headerValue != null && headerValue.startsWith("partial/")) {
        return true;
    }

    headerValue = facade.getRequest().getHeader("SOAPAction");
    if (headerValue != null) {
        return true;
    }

    List<String> accepts = facade.getRequest().getHeaders("Accept");
    if (accepts == null) accepts = Collections.emptyList();

    for (String accept : accepts) {
        if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) {
            return false;
        }
    }

    return true;
}

通过上面这段源码,我们就能很清晰的知道,在autodetect-bearer-only配置设置为true时,HTTP请求头需满足以下几种情况,才被认为是API类型请求,这种情况下未通过认证直接返回401状态码而不是重定向到登录页

  • X-Requested-With请求头的值为XMLHttpRequest
  • Faces-Request请求头的值以partial/开头
  • 出现SOAPAction请求头
  • Accept的请求头的值,不能包含text/htmltext/**/*这些值

集成Spring Security的配置说明

如需集成Spring Security,则Spring Boot配置中的security-constraints可以删除,使用代码进行如下配置

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

    /**
     * Read Keycloak config from spring boot config file
     */
    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/customer/**").hasRole("CUSTOMER")
                .antMatchers("/admin/**").hasAnyRole("ADMIN")
                .anyRequest().permitAll();
    }
}

Controller相关代码

@RequestMapping(value = "/", method = RequestMethod.GET)
public String index() {
    return "index";
}

@RequestMapping(value = "/customer", method = {RequestMethod.GET, RequestMethod.POST})
public String customer(HttpServletRequest request) {
    KeycloakSecurityContext keycloak = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
    return "customer :: " + keycloak.getTokenString();
}

@RequestMapping(value = "/admin", method = {RequestMethod.GET, RequestMethod.POST})
public String admin(HttpServletRequest request) {
    KeycloakSecurityContext keycloak = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
    return "admin :: " + keycloak.getTokenString();
}

@RequestMapping(value = "/logout", method = {RequestMethod.GET, RequestMethod.POST})
public String logout(HttpServletRequest request) {
    try {
        request.logout();
        return "logout success";
    } catch (ServletException e) {
        LOGGER.error("keycloak logout error", e);
        return "logout fail";
    }
}

项目效果演示

运行同时提供web页面及Restful API服务的项目spring-boot-keycloak-webapi,看下相关的效果

浏览器使用customer用户登录并访问/customer

keycloak-result-web-1

浏览器使用customer用户登录并访问/admin

keycloak-result-web-2

Postman直接请求/customer

keycloak-result-api-1

Postman带上Accept请求头请求/customer

keycloak-result-api-2

Postman带上Accept、Authorization请求头访问/customer

Authorization为上面浏览器customer用户登录后获取的access token值

keycloak-result-api-3

Postman带上Accept、Authorization请求头访问/admin

Authorization为上面浏览器customer用户登录后获取的access token值

keycloak-result-api-4

总结

得益于Keycloak官方提供的keycloak-spring-boot-starter包,Spring Boot(Spring Security)应用集成Keycloak非常容易,只需少量的配置及代码,就可完成统一身份验证、权限控制功能。

项目示例地址:spring-boot-keycloak