1 设计

1.1 单点登录:gin+cas认证

01.CAS认证客户端实现
    a.背景
        在使用Golang对接CAS认证时,发现网上的资料大多使用Golang的CAS客户端包:gopkg.in/cas.v2。
        然而,在接入CAS服务器后,出现了不断跳转和重定向的问题,无法继续执行认证成功后的操作。
        为了解决该问题,基于CAS认证原理,自行实现了一个CAS认证客户端。
    b.定义响应结构体
        a.结构体定义
            首先,需要定义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"`
            }
    c.编写CAS认证逻辑
        a.核心逻辑
            // 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)
            }

            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
                }
                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
            }

            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
            }

            func getTicket(r *http.Request) string {
                return r.FormValue("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
                }
            }
    d.在Gin中间件中应用
        a.中间件应用
            将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
                }
            }
    e.总结
        这里只根据CAS原理实现了一个基本的CAS客户端认证流程,包括了请求检查、重定向处理、票据验证和用户信息解析,并通过Gin中间件集成到了Web应用程序中。