1. 首页
  2. 后端

Go CAS认证实现 gin+cas认证 单点登录

  Go CAS认证实现 gin+cas认证 单点登录

=========================

背景

在用golang对接cas认证时,发现网上的资料都是使用Golang的 cas 客户端的包:gopkg.in/cas.v2实现。经测试,在接入 cas 服务器后,不断跳转,重定向而不继续执行认证成功后的操作

为了解决该问题,基于cas认证原理,自己实现了一个cas认证客户端

定义响应结构体

首先,我们需要定义 CAS 认证成功后的响应结构体:

// model/cas.go
type CasServiceResponse struct {
    XMLName xml.Name `xml:"serviceResponse"`
    Data    struct {
        SFRZH      string `xml:"user"`
        Attributes struct {
            Uid      string `xml:"uid"`
            UserName string `xml:"userName"`
        } `xml:"attributes"`
    } `xml:"authenticationSuccess"`
}

编写 CAS 认证逻辑

接下来,我们编写 CAS 认证的核心逻辑:

// utils/cas.go
package utils

import (
    "encoding/xml"
    "errors"
    "fmt"
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
    "io"
    "net/http"
    "roomlive-go/global"
    "roomlive-go/model/cas"
    "roomlive-go/model/user"
    "strings"
)

func IsAuthentication(w http.ResponseWriter, r *http.Request, casServerUrl string) (bool, *cas.CasServiceResponse) {
    if !hasTicket(r) {
        redirectToCasServer(w, r, casServerUrl)
        return false, nil
    }
    localUrl := getLocalUrl(r)
    ok, err, res := validateTicket(localUrl, casServerUrl)
    global.SYSLOG.Debug("cas validateTicket", zap.Bool("ok", ok), zap.Error(err), zap.Any("res", res))
    if !ok {
        redirectToCasServer(w, r, casServerUrl)
        return false, nil
    }
    global.SYSLOG.Info("user authenticated", zap.String("sfrzh", res.Data.SFRZH))
    return true, res
}
func redirectToCasServer(w http.ResponseWriter, r *http.Request, casServerUrl string) {
    casServerUrl = casServerUrl + "/login?service=" + getLocalUrl(r)
    http.Redirect(w, r, casServerUrl, http.StatusFound)
}

/*
验证访问路径中的ticket是否有效
*/
func validateTicket(localUrl, casServerUrl string) (bool, error, *cas.CasServiceResponse) {
    casServerUrl = casServerUrl + "/serviceValidate?service=" + localUrl
    res, err := http.Get(casServerUrl)
    if err != nil {
        return false, err, nil
    }
    defer res.Body.Close()
    data, err := io.ReadAll(res.Body)
    if err != nil {
        return false, err, nil
    }
    casRes, err := ParseCasUserInfo(data)
    if err != nil {
        return false, err, nil
    }
    if casRes.Data.SFRZH == "" {
        return false, errors.New("authentication failed"), nil
    }
    //dataStr := string(data)
    //if !strings.Contains(dataStr, "cas:authenticationSuccess") {
    //  return false, err, nil
    //}
    return true, nil, casRes
}

/*
从请求中获取访问路径
*/
func getLocalUrl(r *http.Request) string {
    scheme := "http://"
    if r.TLS != nil {
        scheme = "https://"
    }
    url := strings.Join([]string{scheme, r.Host, r.RequestURI}, "")
    fmt.Printf("url: %v\n", url)
    slice := strings.Split(url, "?")
    if len(slice) > 1 {
        localUrl := slice[0]
        urlParamStr := ensureOneTicketParam(slice[1])
        url = localUrl + "?" + urlParamStr
    }
    return url
}

/*
处理并确保路径中只有一个ticket参数
*/
func ensureOneTicketParam(urlParams string) string {
    if len(urlParams) == 0 || !strings.Contains(urlParams, "ticket") {
        return urlParams
    }
    sep := "&"
    params := strings.Split(urlParams, sep)
    newParams := ""
    ticket := ""
    for _, value := range params {
        if strings.Contains(value, "ticket") {
            ticket = value
            continue
        }
        if len(newParams) == 0 {
            newParams = value
        } else {
            newParams = newParams + sep + value
        }
    }
    newParams = newParams + sep + ticket
    return newParams
}

/*
获取ticket
*/
func getTicket(r *http.Request) string {
    return r.FormValue("ticket")
}

/*
判断是否有ticket
*/
func hasTicket(r *http.Request) bool {
    t := getTicket(r)
    return len(t) != 0
}

func ParseCasUserInfo(data []byte) (*cas.CasServiceResponse, error) {
    var casResponse cas.CasServiceResponse
    if err := xml.Unmarshal(data, &casResponse); err != nil {
        return nil, err
    }
    return &casResponse, nil
}

func GetUser(c *gin.Context) (*user.User, error) {
    if res, exists := c.Get("casResponse"); !exists {
        return nil, errors.New("cas authentication failed")
    } else {
        casRes := res.(*cas.CasServiceResponse)
        waitUser := &user.User{
            UserName: casRes.Data.Attributes.UserName,
            SFRZH:    casRes.Data.SFRZH,
        }
        return waitUser, nil
    }
}

在 Gin 中间件中应用

将 CAS 认证逻辑应用到 Gin 的中间件中:

func CASMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        isAuth, casResponse := utils.IsAuthentication(c.Writer, c.Request, utils.CASServer)
        if !isAuth {
            c.Abort()
            return
        }
        c.Set("casResponse", casResponse)
        c.Next()
        return
    }
}

总结

这里只根据cas原理实现了一个基本的CAS客户端认证流程,包括了请求检查、重定向处理、票据验证和用户信息解析,并通过Gin中间件集成到了Web应用程序中。

原文链接: https://juejin.cn/post/7386916905007677455

文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17322.html

QR code