Golang实践录:使用gin实现 cas 单点登录

本文介绍使用 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 循环的问题。

附录

参考:

go gin框架使用cas单点登录

go cas实现

基于Go语言实现单点登录系统(sso) 支持手机号码+验证码、邮箱+验证码、微信第三方授权三种方式注册 支持手机号码、用户名、邮箱号码、微信登 支持手机和邮箱找回密码 支持阿里云通信和互亿无线的短信验证码服务 Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值