OAuth2协议学习 作者: nbboy 时间: 2021-06-12 分类: Golang 评论 > OAuth2专注为Web应用程序,桌面程序,移动设备,或者其他设备,提供认证流程,是一种第三方认证的业界标准。 ### OAuth2简介 OAuth2在平时接入第三方API经常用到,比如Github用户授权获取仓库信息,Google API用户授权获取其资源,一般在接入第三方开放平台时,都会涉及到OAuth2的开发。不过接入第三方平台时我们不需要了解Server侧的实现原理,只需要专注Client端的请求参数和响应参数。本着知识应该完整性的想法,总结了OAuth2的一些流程和实现原理,并使用Golang搭建一个简单的OAuth2 Auth Server,配合Resource Server进行用户授权信息的限制访问需求。 ### OAuth2支持的4个流程 OAuth2支持4个流程,分别是Authorization Code,Implicit,Resource Owner Password Credentials,Client Credentials,其分别对应Web应用,SPA应用,电子设备,移动端几个常用场景。我比较关注的是Authorization Code模式,所以只对这种模式进行说明,其他模式也雷同,具体可以看RFC文档。 ###### Authorization Code 这种模式适合用在Web网站上,在用户侧有一个游览器,RFC的图非常好,借用来说明下: ```c +----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token) ``` Resource Owner就是用户,User Agent其实就是游览器,Client就是后端的程序。 A)第一步,用户发起OAuth请求之后,前台页面会重定向到Auth Server,如果用户没有登录过,则显示登录Auth Server的界面,然后显示用户授权的信息和确认按钮。 B)用户看到授权信息后,授权Client获取Auth Server的资源 C)Auth Server授权成功后,就会带着AuthCode重定向到A步骤设置的页面地址 D)携带AuthCode和回调地址参数,向Auth Server发起获取Access Token请求 E)Auth Server认证后,重定向到D设置的回调函数页面,并且携带AccessToken 经过这几步后,AccessToken已经获取,后面资源的获取都是通过携带AccessToken去通过认证的,也可以给AccessToken设置有效时间。当时间过期,可以通过RefreshToken去再次获取AccessToken,而RefreshToken就是在E步骤同时返回的。 ### Authorization Code 3大组件 ###### Client Manager 客户端的管理,比如登记,注销,在配置Client的时候需要提供Identifier和Secret信息,在请求Auth Server时,也需要把Client Identifier发给它。如果对于需要接入很多Client的Auth Server则需要一个统一地方管理这些Client信息,这就是Client Manager的职责。 ###### Token Manager 这个组件值得是Token的生命周期管理和存储管理,比如Token多少时间失效?或者Token存储在Redis还是Mysql? ###### Scope Auth Server生成的Token可以有Scope,它的意义都是由Auth Server定义,其实可以理解为一种权限,比如如果获得的Access Token只有Access 权限,则不能对资源进行写入,需要Write权限才可以。 ### 搭建OAuth2 Auth Server 我这里使用的是社区提供的[OAuth2](https://github.com/go-oauth2/oauth2),这个库对Client Manager和Token Manager做了抽象,使用也非常简单。可以看其提供的examples,但是这个库文档不是很全面,可以说没有文档。根据RFC说的流程,顺着examples来看下关键流程。 ```go var ( config = oauth2.Config{ ClientID: "888888", ClientSecret: "666666", Scopes: []string{"all"}, RedirectURL: "http://localhost:8888/oauth2", Endpoint: oauth2.Endpoint{ AuthURL: "http://localhost:9999/authorize", TokenURL: "http://localhost:9999/token", }, } ) //Step(A):redirect to authorize url func homePage(w http.ResponseWriter, r *http.Request) { log.Println("Step(A): redirect handle") u := config.AuthCodeURL("xyz") log.Println(u) http.Redirect(w, r, u, http.StatusFound) } //Step(C): get access token from auth server func authorize(w http.ResponseWriter, r *http.Request) { log.Println("Step(C): get access token from auth server") r.ParseForm() state := r.Form.Get("state") if state != "xyz" { http.Error(w, "State invalid", http.StatusBadRequest) return } code := r.Form.Get("code") if code == "" { http.Error(w, "Code not found", http.StatusBadRequest) return } token, err := config.Exchange(context.Background(), code) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } e := json.NewEncoder(w) e.SetIndent("", " ") e.Encode(*token) } ``` 设置了oauth2一些配置,比如客户端ID和密钥,scope,回调URL等等信息。其中authorize是回调执行函数,homePage页面函数直接跳转到Auth Server。 Auth Server最重要的方法是如下两个,路径/authorize用来生成authorize code,并且跳转到Step(C),而路径/token用来生成token,它是又客户端调用config.Exchange方法发起访问的。在例子中,我都打印出了跳转的方法名,具体可以跟踪一下,跳来跳去不是太明显,需要根据RFC那张图来理解。 ```go //HandleAuthorizeRequest //Step(B): generate authorize code, and redirect to client's callback url http.HandleFunc("/authorize", func(resp http.ResponseWriter, req *http.Request) { log.Println("Step(B): authorize handle") s, _ := sessionStore.Get(req, "LoginUser") if _, ok := s.Values["LoginId"]; !ok { if req.Form == nil { req.ParseForm() } log.Println("QueryString :", req.Form) encodedForm, _ := json.Marshal(req.Form) s.Values["ReturnUri"] = encodedForm err := s.Save(req, resp) if err != nil { log.Printf("sessionStore save error: %v", err) } resp.Header().Set("Location", "/login") resp.WriteHeader(http.StatusFound) return } if form, ok := s.Values["ReturnUri"]; ok { json.Unmarshal(form.([]byte), &req.Form) log.Println("Get Form String:", req.Form) } err := srv.HandleAuthorizeRequest(resp, req) if err != nil { log.Println("authHandler error") http.Error(resp, err.Error(), http.StatusBadRequest) } }) //HandleTokenRequest //Step(D): auth server generate token by auth code, and redirect to client's callback url http.HandleFunc("/token", func(resp http.ResponseWriter, req *http.Request) { log.Println("Step(D): auth server generate access token") err := srv.HandleTokenRequest(resp, req) if err != nil { http.Error(resp, err.Error(), http.StatusInternalServerError) } }) ``` ### 总结 OAuth2是一个获取第三方资源的认证协议,他解决了想共享资源给第三方,但是又不想造成安全问题的两个问题。这篇文章只了解了Authorization Code这种模式,在这种模式下,需要客户端获取AuthorizeCode后,再去换取AccessToken。而且在获取AccessToken后,可以给其设置一个有效时间,在失效后,需要根据RefreshToken来获取新的AccessToken。 ### 参考文档 [Examples](https://github.com/x-debug/go-examples/tree/master/oauth2-example) [OAuth2 RFC](https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1) [OAuth2的简单介绍,可以作为RFC的补充,里面的图不错](https://itnext.io/an-oauth-2-0-introduction-for-beginners-6e386b19f7a9) [OAuth2一些资源](https://oauth.net/2/) [Golang Client Implement](https://github.com/golang/oauth2)