本文介绍使用 Golang 语言实现 cas 单点登录。
起因
新年伊始,上班第一天收到消息。原来那台用于部署内部网页工具的服务器因安全问题被停止使用,需更新服务器部署,但从中带出一个问题,那个应用服务程序必须使用登录平台做跳转,不能直接通过URL访问网页。
时间急任务不熟悉,所幸搜索发现有现成的方案。因同时做维护方面的事,共了几天时间完成。
分析
经多方请教,得知线上服务均已使用某平台统一认证、转发,使用同一 cas 服务器。而该应用服务程序只是漏网之鱼。
应用服务使用 bootstrap + Golang 实现,不是主流的那个前后端分离架构,其它部门现有的模块不能直接使用,而再实现前后端分离,又要花费人力时间,且自己也不熟悉。因此只能自己找方法。
经分析,要做的事是:在 gin 接口中,当请求页面时,先请求 cas 服务器,如认证通过,再继续后续逻辑。
使用cas.v2包实现
代码
Golang 有 cas 客户端的包:gopkg.in/cas.v2
,使用即可。
思路如下:
- 调用
gin.BasicAuth()
创建需认证的路由组,账号密码由配置文件读取(可多个,本文示例略去)。 - 在该路由组下指定相应的URL及响应函数。
- 响应函数从
gin.Context
获取账号,与配置文件的对比,如不同,认证不通过,返回。否则继续后面处理流程。
关键代码示例:
import (
"conf"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"gopkg.in/cas.v2"
)
// 说明:测试用的cas服务器,URL是带有login的,所用的cas客户端会自动加上,
// 故下面的URL不再使用
// conf.CasUrl="https://192.168.18.18:8180/cas/"
// cas检测及登录
func checkCas(ctx*gin.Context) {
if conf.CasUrl == "" {
return
}
u, _ := url.Parse(conf.CasUrl)
client := cas.NewClient(&cas.Options{URL: u})
h := client.HandleFunc(func(w http.ResponseWriter, r *http.Request) {
if !cas.IsAuthenticated(r) { // 没有授权,跳转之
// klog.Println("go to cas url ", conf.CasUrl)
client.RedirectToLogin(w, r)
return
}
// else {
// klog.Println("cas login ok")
// }
})
h.ServeHTTP(ctx.Writer, ctx.Request) // 必须要这句
}
func pageIndex(ctx *gin.Context) {
checkCas(ctx)
page := "index.html"
ctx.HTML(http.StatusOK, page, gin.H{
"Title": conf.AppName,
})
}
问题
经测试,发现接入生产环境的 cas 服务器后,观察gin 日志,不断跳转,即循环重定向而不继续执行认证成功后的操作。附录的文章评论也有类似问题。因无法解决,只好继续寻找客户端。
自实现
代码
幸好,无意间发现go cas实现这篇文章,里面给出了自行实现的cas
客户端代码,于是借鉴使用之。此处列出所有代码,方便与文章的代码对比。
import (
"io/ioutil"
"net/http"
"strings"
"webdemo/pkg/klog"
)
/*
判断当前访问是否已认证
*/
func IsAuthentication(w http.ResponseWriter, r *http.Request, casServerUrl string) bool {
if !hasTicket(r) {
klog.Println("cas debug has no ticket...")
redirectToCasServer(w, r, casServerUrl)
return false
}
localUrl := getLocalUrl(r)
klog.Println("cas debug IsAuthentication hasticket.... url: ", localUrl)
if !validateTicket(localUrl, casServerUrl) {
klog.Println("cas debug validateTicket no ok")
redirectToCasServer(w, r, casServerUrl)
return false
}
return true
}
/*
重定向到CAS认证中心
*/
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 {
casServerUrl = casServerUrl + "serviceValidate?service=" + localUrl
klog.Println("cas debug validateTicket casServerUrl.... ", casServerUrl)
res, err := http.Get(casServerUrl)
if err != nil {
klog.Println("cas debug validateTicket 111 BUT RETURN TRUEEEEEE", err.Error())
//return false
return true //
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
klog.Println("cas debug validateTicket 222 ", err.Error())
return false
}
dataStr := string(data)
if !strings.Contains(dataStr, "cas:authenticationSuccess") {
klog.Println("cas debug validateTicket 333 ", err.Error())
return false
}
klog.Println("cas debug 444 ", err.Error())
return true
}
/*
从请求中获取访问路径
*/
func getLocalUrl(r *http.Request) string {
scheme := "http://"
if r.TLS != nil || 1 == 0 { // 经测试,有时输入了https,还得到还是http,这里可强制之
scheme = "https://"
}
url := strings.Join([]string{scheme, r.Host, r.RequestURI}, "")
slice := strings.Split(url, "?")
//klog.Println("----- slice", slice)
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)
//klog.Println("----- hasTicket", t)
return len(t) != 0
}
问题
依然有循环重定向问题,但通过修改绕过。但使用其它 cas 服务器测试,又是正常的。着实不知何故。
测试
由于接入现有的cas
服务器,需使用https
协议,在gin
中使用该协议也比较简单,因证书问题,只能在现有nginx
服务器中做代理实现https
协议。
小结
目前未找到根本解决方法,只是绕过 cas 循环的问题。
附录
参考: