Go語言專案整合CAS單點登入
版權宣告:本文為博主原屙文章,喜歡你就擔走。https://blog.csdn.net/leftfist/article/details/84630320
網上有高手開源了一個網盤專案:ofollow,noindex" target="_blank">藍眼雲盤 ,我一看還行,版權也很寬鬆,是IT%E8%AE%B8%E5%8F%AF%E8%AF%81/6671281?fr=aladdin" rel="nofollow,noindex" target="_blank">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 { Usernamestring 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 為藍本,可以移植到藍眼雲盤了。
golang的專案必有一個main包,main包裡必有一個main函式。雲盤初始化之際,執行main函式,就是植入CAS客戶端之時。
思路是這樣子的:
雲盤初始化的時候,改用CAS客戶端接管和監聽服務請求。
具體如何處理請求呢?
CAS客戶端有個最大的特點,就是如果尚未進行認證,就一定會先轉向單點登入;認證過,才會到達我們自己寫的這些程式碼。
好,現在到達我們程式碼部分,每個請求過來的時候,先判斷一下是否需要進行自動登入。如果不需要,就按照雲盤設定的路由規則執行,否則轉向自動登入頁面。自動登入頁面,顧名思義,會呼叫自動登入路由進行雲盤登入。
如何判斷是否需要進行自動登入呢?
是靜態檔案嗎?,如JS,CSS,圖片之類,是的話,肯定不需要登入。
否則檢查cookie,如果cookie有相關資訊,則已經登入過,也不需要再登入。
一系列判斷之後,才輸出自動登入頁面。輸出的時候,會將從CAS客戶端獲取到的登入賬號一併輸出。
如前所述,在登入頁面,會用ajax採取POST的方式到路由:api/user/autoLogin。這個路由是俺新開發的,有如下功效:
根據提交上來的賬號名稱,到資料庫中讀取,如果能獲取,登入順利通過;如果沒有,則建立一個,然後登入通過。(可以發現,這裡面有些安全性問題,怎麼保證提交上來的賬號就是單點登入認證過的哪個賬號?)
修改過的雲盤main函式程式碼如下:
//main.gofunc 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 { Accountstring } 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){ 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), } 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> `
//自動登入路由
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)//登入實現 } }
以上詳細程式碼就不貼了,容易實現
三、自動登入頁面
這裡要介紹一下藍眼雲盤的登入機制。它並不是完全依賴cookie的。貌似cookie,只是它的後端使用;而前端則完全依賴於local storage。所以在自動登入成功後,需要將登入資訊寫入local storage。
如前所述,自動登入頁面是後端輸出的,除了引用的js
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); } }); }
四、遺留問題
單點登出沒解決。就是別的系統已經登出了,但云盤這裡沒反應,仍然處於登入狀態。這在別的系統退出並切換賬號登入時,就發現兩邊賬號不一致。本來大家都是張三,現在一個是張三,一個是李四。