前面介绍了前端应用如何集成Keycloak实现统一身份验证、权限控制,可参考 vue-element-admin集成Keycloak实现统一身份验证、权限控制 。如果对Keycloak还不太了解的话,可以参考Keycloak快速上手指南对Keycloak的基本概念进行了解。本文将讲述典型的Spring Boot/Spring Security服务端的应用如何集成Keycloak,以实现SSO登录、统一身份验证、权限控制等功能。
服务端的web类型的应用,最常见的就是提供web页面服务以及Restful API。不同类型的应用,Keycloak后台创建的配置会稍微有些区别。下面将创建3个客户端:
spring-boot-keycloak-web srping-boot-keycloak-security-api spring-boot-keycloak-webapi
 
 
  
 
  
 
 之前的文章已经说明过,这里再列一下,Keycloak目前的访问类型共有3种:
 confidential :适用于服务端应用,且需要浏览器登录以及需要通过密钥获取 access token 的场景。典型的使用场景就是服务端渲染的web系统。 
 public :适用于客户端应用,且需要浏览器登录的场景。典型的使用场景就是前端web系统,包括采用vue、react实现的前端项目等。 
 bearer-only :适用于服务端应用,不需要浏览器登录,只允许使用 bearer token 请求的场景。典型的使用场景就是restful api。 
分别创建用户admin、customer,及角色ROLE_ADMIN、ROLE_CUSTOMER,并进行绑定
 
 
  
 
 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>
复制代码 
 这里以同时提供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/html 、 text/* 、 */* 这些值  如需集成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();
    }
}
复制代码 
 @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,看下相关的效果