Vue+Golang接入KeyCloak

原文發(fā)布在本人博客 https://xuing.cn/program/vue-golang-keycloak.html

Vue+Golang接入KeyCloak

Vue+Golang接入KeyCloak實(shí)現(xiàn)簡(jiǎn)單的角色劃分、權(quán)限校驗(yàn)。

本人Golang苦手,也是第一次接觸Keycloak。網(wǎng)上資料太少,我的方案大概率非最佳實(shí)踐。僅供參考。歡迎批評(píng)意見(jiàn)。

接入預(yù)期

本次實(shí)踐將達(dá)到以下幾個(gè)目的:

  1. 前端Vue接入KeyCloak,必須登錄后才進(jìn)行渲染。
  2. 后端Golang Beego框架接入Keycloak。使用前端傳過(guò)來(lái)的Authorization進(jìn)行鑒權(quán)。
  3. 區(qū)分普通用戶和管理員兩種角色。

KeyCloak搭建、配置

最方便的搭建方式當(dāng)然就是用Docker了。

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

  1. 登錄管理頁(yè)面。創(chuàng)建Realme:demo

    我的理解一個(gè)Realm對(duì)應(yīng)一個(gè)應(yīng)用。

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

    client,用于和keycloak進(jìn)行通信。

    前端無(wú)需特別配置。使用Access Type為public的(即不需要ClientSecret)默認(rèn)配置即可。

    配置 Valid Redirect URIs 允許登錄成功后的重定向地址。測(cè)試時(shí)可以使用*。來(lái)允許任意地址。

  3. 接下來(lái)再建立后端使用的Clientdemo-back

    1. 配置Access Type為confidential。在Credentials中Tab記錄下生成的Secret。供后續(xù)使用。
    2. 為了能夠有權(quán)限查詢用戶的角色信息,首先開(kāi)啟Service Accounts。在新出現(xiàn)的Service Account Roles Tab中,增加Client Roles。我這里沒(méi)有做測(cè)試,把能增加的權(quán)限都加進(jìn)去了??赡苤恍枰黾?code>realm-management的query-users權(quán)限即可(未測(cè)試)。
  4. 新建Roles。

    普通用戶角色:demo_user_role

    管理用角色:demo_admin_role

  5. 創(chuàng)建用戶,本文不涉及用戶注冊(cè)的操作, 就直接在后臺(tái)創(chuàng)建兩個(gè)用戶再分別分配上角色就好了。demo-user,demo-admin

Vue接入

VUE的接入文章還是挺多的。這里簡(jiǎn)略過(guò)一下。

  1. 安裝導(dǎo)入vue-keycloak-js

  2. main.js中全局引入

    Vue.use(keycloak, {
      init: {
        onLoad: process.env.VUE_APP_KEYCLOAK_ONLOAD,
        checkLoginIframe: false // 防止登陸后重復(fù)刷新
      },
      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庫(kù)。我使用的是v8版本,目前已經(jīng)有v9了。我這里是手動(dòng)維護(hù)了一個(gè)JWT token 用于和keycloak進(jìn)行通信,后續(xù)可能有更簡(jiǎn)單的方案。

https://github.com/Nerzal/gocloak

初始化Client

因?yàn)槭褂玫氖?code>confidential的訪問(wèn)模式,我們需要登錄demo-backclient到keycloak。并且維護(hù)登錄狀態(tài)。

初始化代碼如下:

聲明用戶類(lèi)型常量,維護(hù)client需要的相關(guān)變量并提供刷新(登錄)函數(shù)。

models/user.go

type UserType int
const (
    ApplicationAdminType UserType = iota
    SuperAdminType                //超級(jí)管理員
    UnAuthorizedUserType          //未授權(quán)用戶
)

var (
    userId    string
    client    = gocloak.NewClient(conf.AppConfig.KeycloakUrl)
    clientJWT *gocloak.JWT
    // retrospecTokenResult存放了clientJWT的過(guò)期時(shí)間(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()
    ...

路由鑒權(quán)

為api接口增加鑒權(quán),獲取Authorization Header中的AccessToken,并發(fā)送給Keycloak,獲取用戶的基本信息,主要是Sub(即用戶id)。

再遍歷User的Role信息,確定用戶角色。

filter/auth.go

// 初始化,添加路由鑒權(quán)
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)
    
    //....
}

// 校驗(yàn)函數(shù)
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) + " 未查詢到使用授權(quán)信息。"},
            false, false)
        return
    }
    ctx.Input.SetData("UserType", userType)

    ctx.Input.SetData("UserId", *UserInfo.Sub)
    
    // 具體業(yè)務(wù)代碼,如獲取用戶名、根據(jù)用戶角色進(jìn)行不同的鑒權(quán)處理。
    // ....
}


具體實(shí)現(xiàn)

獲取用戶信息和獲取用戶角色的實(shí)現(xiàn)如下。代碼可根據(jù)業(yè)務(wù)進(jìn)行調(diào)整。

models/user.go

// 獲取用戶基礎(chǔ)信息
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
    //判斷是否過(guò)期
    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
}

維護(hù)client的JWT Token的任務(wù),我直接寫(xiě)到獲取用戶角色這里了。我這里測(cè)試,獲取用戶基礎(chǔ)信息的話,是不需要client的Access Token的。

后記

目前的實(shí)現(xiàn)是能滿足我的業(yè)務(wù)需求呢,但keycloak的強(qiáng)大之處,我可能還遠(yuǎn)遠(yuǎn)沒(méi)有用上。

希望能提供一些幫助 hhhh。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容