1. 程式人生 > >Go語言專案整合CAS單點登入

Go語言專案整合CAS單點登入

網上有高手開源了一個網盤專案:藍眼雲盤,我一看還行,版權也很寬鬆,是MIT,就用到了專案裡面去。

有個問題就是我們專案採用了CAS作為單點登入,而這個藍眼雲盤有自己的一套登入機制。需要改造一下,將單點登入也整合到雲盤中來。

藍眼雲盤專案伺服器端是用GO語言開發的,前端則用了VUE.JS框架,這兩樣我都沒接觸過。而且成品的前端還用了webpack進行打包,很難看清。webpack也沒接觸過。一片空白啊。

趕鴨子上架,花了約2周的時間,儘管還不完美,但終於像點樣子了。

思路或步驟記錄如下:

一、要有一個CAS for Go語言的客戶端,這屬於準備工作

這個客戶端做什麼用呢?接管和控制使用者請求,發現未經過CAS認證就轉向CAS。
這個CAS客戶端當然也是golang開發的。
原始碼請狠狠點選

這裡

下載,編譯以後,可以執行_examples/cas_chi.go(命令列方式下,go run cas_chi.go),就能看到這個客戶端的效果了:在瀏覽器輸入 localhost:9999,會先跳到CAS的登入頁。

但是!官方程式碼有個坑,就是登入以後,會出現重定向次數過多的錯誤。有人修復了這個坑。修改過的_examples/cas_chi.go程式碼如下:

package main

import (
	"bytes"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"net/url"

	"github.com/go-chi/chi"
	"github.com/shenshouer/cas"
) var casURL = "http://192.168.0.22:8080/cas2/" //單點登入地址 type templateBinding struct { Username string Attributes cas.UserAttributes } func main() { url, _ := url.Parse(casURL) client := cas.NewClient(&cas.Options{URL: url}) root := chi.NewRouter() root.Use(client.Handler) //這句為新增程式碼 server :=
&http.Server{ Addr: ":9999", Handler: client.Handle(root), } root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/html") tmpl, err := template.New("index.html").Parse(index_html) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, error_500, err) return } binding := &templateBinding{ Username: cas.Username(r), Attributes: cas.Attributes(r), } html := new(bytes.Buffer) if err := tmpl.Execute(html, binding); err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, error_500, err) return } html.WriteTo(w) }) //if err := http.ListenAndServe(":9999", root); err != nil { //遮蔽原先這句程式碼 if err := server.ListenAndServe(); err != nil {//改為這句 log.Fatal(err) //fmt.Println("error!") } //} } const index_html = `<!DOCTYPE html> <html> <head> <title>Welcome {{.Username}}</title> </head> <body> <h1>Welcome {{.Username}} <a href="/logout">Logout</a></h1> <p>Your attributes are:</p> <ul>{{range $key, $values := .Attributes}} <li>{{$len := len $values}}{{$key}}:{{if gt $len 1}} <ul>{{range $values}} <li>{{.}}</li>{{end}} </ul> {{else}} {{index $values 0}}{{end}}</li>{{end}} </ul> </body> </html> ` const error_500 = `<!DOCTYPE html> <html> <head> <title>Error 500</title> </head> <body> <h1>Error 500</h1> <p>%v</p> </body> </html> `

二、參照範例_examples/cas_chi.go 的原理,修改雲盤的main函式

golang的專案必有一個main包,main包裡必有一個main函式。雲盤初始化之際,執行main函式,就是植入CAS客戶端之時。

思路是這樣子的:

雲盤初始化的時候,改用CAS客戶端接管和監聽服務請求。

具體如何處理請求呢?
CAS客戶端有個最大的特點,就是如果尚未進行認證,就一定會先轉向單點登入;認證過,才會到達我們自己寫的這些程式碼。這一切都由CAS客戶端控制。

好,現在到達我們程式碼部分,每個請求過來的時候,先判斷一下是否需要進行自動登入。如果不需要,就按照雲盤設定的路由規則執行,否則轉向自動登入頁面。

**如何判斷是否需要進行自動登入呢?**演算法如下:
是靜態檔案嗎?,如JS,CSS,圖片之類,是的話,肯定不需要登入。
否則檢查cookie,如果cookie有相關資訊,則已經登入過,也不需要再登入。
一系列判斷之後,才輸出自動登入頁面。輸出的時候,會將CAS客戶端獲取到的登入賬號一併輸出。

修改過的雲盤main函式程式碼如下:
//main.go

func main() {

	//將執行時引數裝填到config中去。
	rest.PrepareConfigs()
	context := rest.NewContext()
	defer context.Destroy()

	//http.Handle("/", context.Router)//遮蔽了原先的路由

	dotPort := fmt.Sprintf(":%v", rest.CONFIG.ServerPort)

	info := fmt.Sprintf("App started at http://localhost%v", dotPort)
	rest.LogInfo(info)

	fmt.Println("網盤執行中:http://localhost%v,請勿關閉",dotPort)

	rest.CasDog(dotPort,context)//人來落閘放狗,CAS客戶端接管了服務監聽

/*	err := http.ListenAndServe(dotPort, nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}*/
}

“CAS狗”檔案:
rest/cas_chi.go

package rest

import (
	"bytes"
	"fmt"
	"html/template"
	"github.com/go-chi/chi"
	"github.com/shenshouer/cas"
	"log"
	"net/http"
	"net/url"
	"strings"
)

type templateBinding struct {
	Account   string
}

func CasDog(port string,ctx *Context) {
	url, _ := url.Parse(CONFIG.Cas)
	client := cas.NewClient(&cas.Options{URL: url})

	root := chi.NewRouter()
	root.Use(client.Handler)

	server := &http.Server{
		Addr:    port,
		Handler: client.Handle(root),
	}

	root.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {//注意路由規則是“/*”,而不是"/"
	//否則靜態檔案等無法訪問
		if !autoLogin(ctx,w,r){//如果無須轉向自動登入頁面,就按照原先的路由規則執行
			ctx.Router.ServeHTTP(w,r)
		}
	})

	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}
func autoLogin(ctx *Context,w http.ResponseWriter,r *http.Request) bool {//自動登入
	path := r.URL.Path
	if strings.Index(path, "/autoLogin") >= 0 || isStaticFile(r){
		return false
	}

	cookie, _ := r.Cookie(COOKIE_AUTH_KEY)
	if cookie != nil{
		ar := strings.Split(cookie.Value,"|")
		if len(ar) > 1{
			username := ar[1]
			if username == cas.Username(r){//已經在雲盤中登入過了,有相關的cookie資訊
				return false
			}
		}
	}

	outputAutoLoginPage(w,r)
	return true
}
func isStaticFile(r *http.Request) bool{//是靜態資源嗎
	suffixs := [6]string{".js",".css",".png",".jpg",".gif",".html"}
	for _,sf := range suffixs{
		if strings.HasSuffix(strings.ToLower(r.URL.Path),sf){
			return true
		}
	}
	return false
}
func outputAutoLoginPage(w http.ResponseWriter, r *http.Request){//輸出自動登入頁面
	w.Header().Add("Content-Type", "text/html")

	tmpl, err := template.New("autoLogin.html").Parse(index_html)

	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, error_500, err)
		return
	}

	binding := &templateBinding{
		Account:   cas.Username(r),//在CAS中登入的賬號
	}

	html := new(bytes.Buffer)
	if err := tmpl.Execute(html, binding); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, error_500, err)
		return
	}

	html.WriteTo(w)
}

const index_html = `<!DOCTYPE html>
<html>
  <head>
    <title>Welcome {{.Account}}</title>
	<script src="/static/dist/jquery-1.8.3.min.js"></script>
	<script src="/static/dist/auto.js"></script>
  </head>
  <body>
	<div>login,please wait....</div>
  </body>
</html>
<script>
autoLogin('/api/user/autoLogin','name={{.Account}}');//賬號資訊
</script>
`

const error_500 = `<!DOCTYPE html>
<html>
  <head>
    <title>Error 500</title>
  </head>
  <body>
    <h1>Error 500</h1>
    <p>%v</p>
  </body>
</html>
`

三、自動登入
如果已經過CAS認證,那麼首次訪問雲盤,就要自動登入。
改動工作包括兩部分,一是伺服器端註冊一個自動登入路由,二是客戶端新增一個自動登入頁面。

1、自動登入路由
負責實現將CAS傳回來的賬號在雲盤中自動登入,如果不存在該賬號,還要先建立,再自動登入。
這個功能是在伺服器端寫一個函式,然後註冊為路由,開放給自動登陸頁面呼叫。

在user_controller.go中修改

//註冊一個路由
routeMap["/api/user/autoLogin"] = this.Wrap(this.AutoLogin, USER_ROLE_GUEST)
。。。
func (this *UserController) AutoLogin(writer http.ResponseWriter, request *http.Request) *WebResult {

	name := request.FormValue("name")
	user := this.userDao.FindByUserName(name)//新加的方法,用賬號名來獲取使用者資訊
	if user == nil {
		return this.autoCreate(name,writer,request)//自動建立
	} else {
		return this.loginImpl(user,writer,request)//登入實現
	}
}

以上詳細程式碼就不貼了,容易實現

2、自動登入頁面
這個頁面負責將賬號資訊提交給自動登入路由。

本來呢,從CAS轉回到雲盤,賬號資訊等都在伺服器端,為什麼不直接在伺服器端處理,還要將資訊輸出到頁面,然後又提交回伺服器?兜了這麼大的一個圈子。原因是不好改,原先雲盤的結構都是客戶端呼叫伺服器端的api,登入也是,所以暫時先這麼處理。

該頁面主體是在雲盤的main函式中輸出,外鏈的js放在/static/dist/auto.js

這裡要介紹一下藍眼雲盤的登入機制。它並不是完全依賴cookie的。貌似cookie,只是它的後端使用;而前端則完全依賴於local storage。所以在自動登入成功後,需要將登入資訊寫入local storage。

const index_html = `<!DOCTYPE html>
<html>
  <head>
    <title>Welcome {{.Account}}</title>
	<script src="/static/dist/jquery-1.8.3.min.js"></script>
	<script src="/static/dist/auto.js"></script>
  </head>
  <body>
	<div>login,please wait....</div>
  </body>
</html>
<script>
autoLogin('/api/user/autoLogin','name={{.Account}}');
</script>
`

/static/dist/auto.js

function autoLogin(url,data){
	$.ajax({
		type: 'post',
		url: url,
		contentType: "application/x-www-form-urlencoded; charset=utf-8",
		dataType: "json",
		data:data,
		timeout: 30000,
		success: function (msg) {
			msg.data.isLogin = true;
			let json = JSON.stringify(msg.data);
			let key = "user";
			window.localStorage.removeItem(key);
			window.localStorage.setItem(key,json);//將資訊寫入local storage
			location.href = "/";
		},
		error: function (err) {
			alert(err.responseText);
		}
	});
}

四、遺留問題
單點登出沒解決。就是別的系統已經登出了,但云盤這裡沒反應,仍然處於登入狀態。這在別的系統退出並切換賬號登入時,就發現兩邊賬號不一致。本來大家都是張三,現在一個是張三,一個是李四。