Spring cloud gatway使用Spring security适配Keycloak
Keep Team Lv4

  今日折腾之心又扑腾扑腾的支楞起来了,脑子一热想在小云上再搭一个授权服务中心和服务注册中心,这样从别的地方白嫖过来的服务器资源就可以和我的小云联动了。

白嫖怪

  授权服务器嘛,不过就是OAUTH2呗,待我翻出2年前做的Spring 代码那还不分分钟搞定它吗?说干就干,翻出闲置已久差点被删掉的idea,新建spring cloud项目,嗯,一切都很顺利,“不愧是我呀”心里面正美着呢,“咦?怎么现在spring配置器里没有spring-cloud-starter-oauth2了呢?”“重开也没有呢?”“难道是…今日不宜编程?”带着一堆的疑问在网上一搜,我当时的心情是这样的:

哦豁

  为什么一个好端端的服务器框架说没就没了呢?后来在Spring官网找到这样的描述:

No Authorization Server Support


In October 2012, RFC 6749, the OAuth 2.0 Authorization Framework, was published. Subsequently in May 2014, Spring Security OAuth released its 2.0.0 version with support for Authorization Server, Resource Server, and Client. This made a great deal of sense in the absence of OAuth 2.0 libraries and products.


Spring Security’s Authorization Server support was never a good fit. An Authorization Server requires a library to build a product. Spring Security, being a framework, is not in the business of building libraries or products. For example, we don’t have a JWT library, but instead we make Nimbus easy to use. And we don’t maintain our own SAML IdP, CAS or LDAP products.

  大概意思就是说以前我们做了一件很厉害的事儿,不过现在我觉得快搞不动了,也不想维护之前的代码了,所以…大家自便吧。

  这大概也算是开源的一种“自由”吧。

就离谱

  怎么办呢?要么降级到springboot 2.1.4,要么寻找到替代方案。不过既然是折腾,那就折腾到底吧!经过一系列的折腾,成功入坑Keycloak,嗯,就是字面是入坑的意思,截止到此时此刻我依然在坑中躺平。

Keycloak

  Keycloak可不得了,他来自名门世家(RedHat),还记得当年接触的第一款Linux就是RedHat 5,还记得那个时候第一次使用多桌面,觉得好特么神奇,也好特么鸡肋的功能。在Keycloak的个人介绍里面赫然写着:

Add authentication to applications and secure services with minimum fuss. No need to deal with storing users or authenticating users. It’s all available out of the box.

You’ll even get advanced features such as User Federation, Identity Brokering and Social Login.

  就问给不给力,凡是现在流行的登录方式,能想到的都给打包了。而它的用法特简单,基本上做到了开箱即用。相关的用法可在网上搜索,这里就不啰嗦了。

  但是如果使用的是docker镜像,Keycloak有一个巨大的坑,对!就我躺平的这个,关于这个坑后面单独再开一篇水文吧。关于Keycloak的配置,建议配置一个confidential的client,并作为下游Spring Oauth2的client-id以及secret。

  接下来说回Spring如何适配Keycloak。

Spring security

  其实适配方式挺简单的,毕竟Spring提供了现成的OAuth2客户端,当然也可以用Keycloak提供的Spring适配器。不过我不是特别建议这样做,因为我们希望网关仅仅是作为sso统一入口而已。那么它需要做到的无外乎是用户验证,而非授权判断。授权信息(token)的校验,应该由各个资源服务自行判断。对于用户验证,Spring security够用了。希望数据流转拓扑如下:

数据流

  1. 用户资源访问请求首先达到Gateway(比如请求访问微服务1提供的资源)
  2. Gateway将用户登录信息(如用户名密码亦或者Client ID Secert等)送给验证服务(keycloak),用于用户身份验证。验证通过后验证服务会传回一个token,用于标识用户信息(如用户名,权限,角色等)。
  3. Gateway从discovery 服务中获取所有注册上来的微服务,从中找到需要使用的微服务
  4. Gateway将用户请求以及token路由至正确的微服务
  5. 微服务获取到请求后,会去验证token是否合法并解析token中包含的用户信息,用于判断所请求的资源是否有权限访问。

  对于最后一步,权限判断可放在Gateway,也可放在各微服务当中,他们实现的效果完全一样。但是个人还是倾向Gateway只做登录验证,权限验证交给资源服务(即后端的各个微服务)完成。原因如下:

  1. 微服务架构本是为快速响应,快速变化而生的,即DeveOps。随着业务的发展,提供的资源越来越多样,那么需要的微服务也就越来越多。如果权限验证全由Gateway或前端控制,则意味着每更改一个微服务,Gateway或前端都可能需要更新,这和初衷是相违背的。
  2. 每个微服务应该是独立的,解耦的模块,而权限控制完全属于其专属的业务逻辑,理应由其维护。

依赖(pom)

  首先来看看需要依赖那些Spring组件。

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

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

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

<dependency> 
    <groupId>org.springframework.cloud</groupId> 
    <artifactId>spring-cloud-starter-gateway</artifactId> 
</dependency> 

<dependency> 
    <groupId>org.springframework.cloud</groupId> 
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
</dependency>

  均采用Spring security相关组件,这样假如爬不出Keycloak的坑,更换别的授权服务,我的代码变动也会小一点吧。

小机灵鬼

关键代码

  既然用到了Spring security,那么少不了关键的安全配置,让我们来瞅瞅这个“炒鸡复杂”的配置方法吧

import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 
import org.springframework.security.config.web.server.ServerHttpSecurity; 
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; 
import org.springframework.security.web.server.SecurityWebFilterChain; 

@Configuration 
@EnableWebFluxSecurity 
public class SecurityConfiguration { 

    @Bean 
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository reactiveClientRegistrationRepository){ 
        return http.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated()) 
            .oauth2Login() 
            .and() 
            .build(); 
    } 
}

  没错,就只有这么一点即可实现sso登录验证。完全不用配置那些花里胡哨妖艳儿货。代码内容很简单,基本上算自注释了。

关键配置

  这一步是真关键,在数据流程拓扑中,不仅要路由,还要带上token,如何带上呢?这就需要在配置中完成了。

security:
  oauth2:
    client:
      provider:
        keycloak:
          token-uri: ${keycloak.hostname}/auth/realms/${keycloak.realm}/protocol/openid-connect/token
          authorization-uri: ${keycloak.hostname}/auth/realms/${keycloak.realm}/protocol/openid-connect/auth
          userinfo-uri: ${keycloak.hostname}/auth/realms/${keycloak.realm}/protocol/openid-connect/userinfo
          user-name-attribute: preferred_username
      registration:
        beta:
          provider: keycloak
          client-id: ${keycloak.client-id}
          client-secret: ${keycloak.client-secret}
          authorization-grant-type: authorization_code
          redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
cloud:
  gateway:
    default-filters:
      - TokenRelay
    discovery:
      locator:
        lower-case-service-id: true
        enabled: true
        filters:
          - StripPrefix=1

  这里有两个重点配置内容,第一个是按照OAuth2方式,将相关信息配置给Spring OAuth2 Client。另一个是给路由配置TokenRelay filter。

  OAuth2配置可以让我们正确与Keycloak交互,而路由filter则可以在成功登录后将token路由给下游微服务,以便下游进一步验证权限。

测试验证

  可通过构造一个测试端点来验证Keycloak返回的token。

@RestController
@RequestMapping("/")
public class testEndpoint {

    @GetMapping("/")
    public String home(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
        return String.format("Welcome %s<br> Your Access Token is:<>%s", authorizedClient.getPrincipalName(), authorizedClient.getAccessToken().getTokenValue());

    }
}

  运行起来后,可通过访问测试端点获取到token,大约是一长串的jwt字符,在Jwt.io中可对字符解码,解码后的信息如下:

  "iat": 1635174991,
  "auth_time": 1635174688,
  <数据省略>
  "realm_access": {
    "roles": [
      "user-role", //包含分配的角色信息

    ]
  },
  <数据省略>
  "sid": "d789eba1-1461-44b6-bd50-660b4e5914ad",
  "email_verified": true,
  "name": "xxxxx" //在Keycloak中创建的用户名,
  "preferred_username": "admin",
  "given_name": "xxxx" //在Keycloak中创建的用户名,
  "family_name": "admin",
  "email": "xx@xxxxxxx" //在Keycloak中创建的用户邮件地址
}

  可以看出token中已包含有用户相关信息,可进一步传递给下游,作为权限判断输入,符合上文中的预期。好了,用户登录有了,剩下的就是在资源服务器(即下游微服务)上实现权限验证了。

  后面再单独写一章吧,因为资源服务器的实现略有不同。