Vue+Golang接入KeyCloak

原文发布在本人博客 https://xuing.cn/program/vue-golang-keycloak.html

Vue+Golang接入KeyCloak

Vue+Golang接入KeyCloak实现简单的角色划分、权限校验。

本人Golang苦手,也是第一次接触Keycloak。网上资料太少,我的方案大概率非最佳实践。仅供参考。欢迎批评意见。

接入预期

本次实践将达到以下几个目的:

  1. 前端Vue接入KeyCloak,必须登录后才进行渲染。
  2. 后端Golang Beego框架接入Keycloak。使用前端传过来的Authorization进行鉴权。
  3. 区分普通用户和管理员两种角色。

KeyCloak搭建、配置

最方便的搭建方式当然就是用Docker了。

docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:15.0.2

  1. 登录管理页面。创建Realme:demo

    我的理解一个Realm对应一个应用。

  2. 首先建立前端使用的Client:demo-front

    client,用于和keycloak进行通信。

    前端无需特别配置。使用Access Type为public的(即不需要ClientSecret)默认配置即可。

    配置 Valid Redirect URIs 允许登录成功后的重定向地址。测试时可以使用*。来允许任意地址。

  3. 接下来再建立后端使用的Clientdemo-back

    1. 配置Access Type为confidential。在Credentials中Tab记录下生成的Secret。供后续使用。
    2. 为了能够有权限查询用户的角色信息,首先开启Service Accounts。在新出现的Service Account Roles Tab中,增加Client Roles。我这里没有做测试,把能增加的权限都加进去了。可能只需要增加realm-managementquery-users权限即可(未测试)。
  4. 新建Roles。

    普通用户角色:demo_user_role

    管理用角色:demo_admin_role

  5. 创建用户,本文不涉及用户注册的操作, 就直接在后台创建两个用户再分别分配上角色就好了。demo-user,demo-admin

Vue接入

VUE的接入文章还是挺多的。这里简略过一下。

  1. 安装导入vue-keycloak-js

  2. main.js中全局引入

    Vue.use(keycloak, {
      init: {
        onLoad: process.env.VUE_APP_KEYCLOAK_ONLOAD,
        checkLoginIframe: false // 防止登陆后重复刷新
      },
      config: {
        'realm': process.env.VUE_APP_KEYCLOAK_REALM,
        'url': process.env.VUE_APP_KEYCLOAK_URL, // auth-server-url
        'clientId': process.env.VUE_APP_KEYCLOAK_CLIENTID, // resource
        // 'credentials': {
        //   'secret': process.env.VUE_APP_KEYCLOAK_CLIENT_SECRET // clientSecret
        // },
      },
      onReady: (keycloak) => {
        new Vue({
          el: '#app',
          router,
          store,
          render: h => h(App)
        })
      }
    })
    

    配置文件参考

    VUE_APP_KEYCLOAK_URL = https://keycloak地址/auth
    VUE_APP_KEYCLOAK_REALM = demo
    VUE_APP_KEYCLOAK_CLIENTID = demo-front
    VUE_APP_KEYCLOAK_ONLOAD = login-required
    

Golang接入

golang接入keycloak,这里使用gocloak库。我使用的是v8版本,目前已经有v9了。我这里是手动维护了一个JWT token 用于和keycloak进行通信,后续可能有更简单的方案。

https://github.com/Nerzal/gocloak

初始化Client

因为使用的是confidential的访问模式,我们需要登录demo-backclient到keycloak。并且维护登录状态。

初始化代码如下:

声明用户类型常量,维护client需要的相关变量并提供刷新(登录)函数。

models/user.go

type UserType int
const (
    ApplicationAdminType UserType = iota
    SuperAdminType                //超级管理员
    UnAuthorizedUserType          //未授权用户
)

var (
    userId    string
    client    = gocloak.NewClient(conf.AppConfig.KeycloakUrl)
    clientJWT *gocloak.JWT
    // retrospecTokenResult存放了clientJWT的过期时间(Exp)等
    retrospecTokenResult *gocloak.RetrospecTokenResult
)

/**
 * 登录到client,并获取clientJWT与retrospecTokenResult。
 */
func updateClient() {
    var err error
    clientJWT, err = client.LoginClient(context.Background(), conf.AppConfig.KeycloakClientId, conf.AppConfig.KeycloakClientSecret, conf.AppConfig.KeycloakRealm)
    if err != nil || clientJWT == nil {
        tools.Panic(tools.ErrCodeInitKeyCloakFailed, "failed to Login KeyCloak Client ", err)
    }
    retrospecTokenResult, err = client.RetrospectToken(context.Background(), clientJWT.AccessToken, conf.AppConfig.KeycloakClientId, conf.AppConfig.KeycloakClientSecret, conf.AppConfig.KeycloakRealm)
    if err != nil || retrospecTokenResult == nil {
        tools.Panic(tools.ErrCodeInitKeyCloakFailed, "failed to Retrospect KeyCloak Token ", err)
    }
}

func init() {
    updateClient()
    ...

路由鉴权

为api接口增加鉴权,获取Authorization Header中的AccessToken,并发送给Keycloak,获取用户的基本信息,主要是Sub(即用户id)。

再遍历User的Role信息,确定用户角色。

filter/auth.go

// 初始化,添加路由鉴权
func init() {
    beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
        AllowAllOrigins:  true,
        AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowHeaders:     []string{"Origin", "Authorization", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"},
        ExposeHeaders:    []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"},
        AllowCredentials: true,
    }))
    
    beego.InsertFilter("/v1/api/*", beego.BeforeRouter, authApi)
    
    //....
}

// 校验函数
func authApi(ctx *context.Context) {
    AccessToken := ctx.Input.Header("Authorization")
    if AccessToken == "" {
        ctx.Output.JSON(map[string]interface{}{
            "status": http.StatusUnauthorized, "description": http.StatusText(http.StatusUnauthorized)},
            false, false)
        return
    }

    if strings.Index(AccessToken, "Bearer ") == 0 {
        AccessToken = AccessToken[7:]
    }

    UserInfo, err := models.GetUserInfo(AccessToken)
    if err != nil || UserInfo == nil {
        log.Println(err)
        ctx.Output.JSON(map[string]interface{}{
            "status": http.StatusUnauthorized, "description": http.StatusText(http.StatusUnauthorized)},
            false, false)
        return
    }

    userType, err := models.GetUserRole(*UserInfo.Sub)
    if err != nil || userType == models.UnAuthorizedUserType {
        log.Println(err)
        ctx.Output.JSON(map[string]interface{}{
            "status": http.StatusUnauthorized, "description": http.StatusText(http.StatusUnauthorized) + " 未查询到使用授权信息。"},
            false, false)
        return
    }
    ctx.Input.SetData("UserType", userType)

    ctx.Input.SetData("UserId", *UserInfo.Sub)
    
    // 具体业务代码,如获取用户名、根据用户角色进行不同的鉴权处理。
    // ....
}


具体实现

获取用户信息和获取用户角色的实现如下。代码可根据业务进行调整。

models/user.go

// 获取用户基础信息
func GetUserInfo(accessToken string) (user *gocloak.UserInfo, err error) {
    user, err = client.GetUserInfo(context.Background(), accessToken, conf.AppConfig.KeycloakRealm)
    return
}

// 获取用户角色信息
func GetUserRole(userId string) (userType UserType, err error) {
    userType = UnAuthorizedUserType
    //判断是否过期
    if int64(*retrospecTokenResult.Exp) < time.Now().Unix() {
        updateClient()
    }
    mappingsRepresentation, err := client.GetRoleMappingByUserID(context.Background(), clientJWT.AccessToken, conf.AppConfig.KeycloakRealm, userId)
    for _, v := range *mappingsRepresentation.RealmMappings {
        if *v.Name == "demo_admin_role" {
            userType = SuperAdminType
            break
        }
        if *v.Name == "demo_user_role" {
            userType = ApplicationAdminType
        }
    }
    return userType, err
}

维护client的JWT Token的任务,我直接写到获取用户角色这里了。我这里测试,获取用户基础信息的话,是不需要client的Access Token的。

后记

目前的实现是能满足我的业务需求呢,但keycloak的强大之处,我可能还远远没有用上。

希望能提供一些帮助 hhhh。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容