KeyCloak 统一权限管理

如果你的系统需要对外提供 SSO 能力,那么可以使用 KeyCloak

能力介绍

KeyCloak 一般情况下,不需要使用,但是如果我们作为用户中心需要接入外部系统,即外部系统需要由我们管理用户体系,提供统一鉴权管理的能力,那么可以外接 KeyCloak 系统

集成方案

网上关于 KeyCloak 有很多说明,可以结合来看,本文档主要描述目前我们的项目如何集成 KeyCloak,用防疫子系统为例,来进行说明

KeyCloack 的配置

每个子系统都作为独立应用接入,即我们将会有以下 Clients,全部应用都由 TerryQi 来支持大家: Clients 状态 说明 负责人 fuxin_server 已创建 Server 端 TerryQi fuxin_web 已创建 Web 端 TerryQi

服务端应用创建方法

Springboot 的集成

本期项目我们使用 KeyCloak 来进行 SSO 和用户校验,具体权限通过 RBAC 管理。目前没有做 Security 的集成,我们的项目框架默认集成了 Security,因此需要先禁用 Security,即 FangYiApplication

在 pom.xml 中引入

<properties>
<log4j2.version>2.17.0</log4j2.version>
<qrqy.developer.version>3.0.5</qrqy.developer.version>
<keycloak.version>18.0.2</keycloak.version>
</properties>

<!-- keycloak -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak.version}</version>
        </dependency>
 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>${keycloak.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

yml 的配置文件 application-dev.yml

# keycloak
keycloak:
  # 表示是一个public的client
  public-client: true
  # bearer-only
  bearer-only: true
  # keycloak的地址
  auth-server-url: http://xxxxxxx:8080
  # keycloak中的realm
  realm: FuxinRealm
  # client ID
  resource: yqfk_server
  # 安全约束
  securityConstraints:
    # 不设定角色,不进行约束
    - authRoles:
      securityCollections:
        # name可以随便写
        - name: public
          patterns:
            - /api/common/*
    # 以下路径需要base_user角色才能访问
    - authRoles:
        - base_user
      securityCollections:
        # name可以随便写
        - name: private
          patterns:
            - /api/*
  ssl-required: none #一定是none

如何联调

进行登陆获取 token,调用接口为:

  • 获取 AccessToken:http://xxxxxxx:8080/realms/FuxinRealm/protocol/openid-connect/token
  • 刷新 AccessToken:http://xxxxxxx:8080/realms/FuxinRealm/protocol/openid-connect/token

调用 api/*的接口,需要在 header 中设置 Authorization

获取用户信息

UserUtil 方法中,curUser 方法

    /**
     * 获取当前的登陆用户
     *
     * @return 当前登陆用户信息
     */
    @SneakyThrows
    public IBaseUserDetails<String> curUser() {
        UserLoginDo<String> userLoginDo = new UserLoginDo();
        RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
        if (session == null) {
            throw new BizException(CommonResponseCode.USER_INVALID, "用户未登录或无效");
        }
        AccessToken token = session.getToken();
        //TODO 等待OA团队获取userId
        userLoginDo.setUserId(token.getSubject());
        userLoginDo.setRealName(token.getPreferredUsername());

        return userLoginDo;
    }

APP 端的集成

APP 集成的流程是先调用登陆接口,获取 token,后续的接口都将 token 放在 Header 中

如果接口返回 403,则重新刷新一下 token,否则重新登陆

Web 集成

Vue 的 Web 集成非常方便,直接在项目中引入 keycloak.js 即可,获取接口用其描述的方法,也是自动刷新 token 参考文档:https://www.keycloak.org/securing-apps/vue

前端集成非常简单,原则上,因为已经集成了 KeyCloak,所以就不需要开发登陆页面了

import Keycloak from "keycloak-js";

let initOptions = {
  url: "http://keycloak.learn.isart.me:8080",
  realm: "FuxinRealm",
  clientId: "fuxin_web",
  onLoad: "login-required",
};

let keycloak = Keycloak(initOptions);

keycloak
  .init({ onLoad: initOptions.onLoad })
  .then((auth) => {
    if (!auth) {
      window.location.reload();
    } else {
      Vue.$log.info("Authenticated");

      new Vue({
        el: "#app",
        render: (h) => h(App, { props: { keycloak: keycloak } }),
      });
    }

    //Token Refresh
    setInterval(() => {
      keycloak
        .updateToken(70)
        .then((refreshed) => {
          if (refreshed) {
            Vue.$log.info("Token refreshed" + refreshed);
          } else {
            Vue.$log.warn("Token not refreshed, valid for " + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + " seconds");
          }
        })
        .catch(() => {
          Vue.$log.error("Failed to refresh token");
        });
    }, 6000);
  })
  .catch(() => {
    Vue.$log.error("Authenticated Failed");
  });
Last Updated:
Contributors: TerryQi