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应用程序中。