第三方登录是应用开发中的常用功能,通过第三方登录,可以更加容易的吸引用户来到我们的应用中。现在,很多网站都提供了第三方登录的功能,在他们的官网中,都提供了如何接入第三方登录的文档。
大多数网站提供的第三方登录都遵循OAuth协议,虽然大多数网站的细节处理都是不一致的,甚至会基于OAuth协议进行扩展,但大体上其流程是一定的。
Oauth2
引入
场景:快递员进入小区,被门禁系统拦截。
-
如果用户把自己的密码,告诉快递员,他就拥有了与用户同样的权限,危险较大,用户如果想取消他进入小区的权力,需要把自己的密码也得跟着改了,还得通知其他的快递员。
-
有没有一种办法,让快递员能够自由进入小区,又不必知道小区居民的密码。
授权机制的设计:
-
第一步,门禁系统的auth增加一个按钮,叫做"获取授权",快递员需要首先按这个按钮,去申请授权。
-
第二步,他按下按钮以后,屋主的手机就会跳出对话框:有人正在要求授权,系统还会显示该快递员的姓名、工号和所属的快递公司。
-
屋主确认请求属实,就点击按钮,告诉门禁系统,同意给予他进入小区的授权。
-
第三步,门禁系统得到的确认以后,向快递员显示一个进入小区的令牌(access token),只在短期内(比如七天)有效。
-
第四步,快递员向门禁系统输入令牌,进入小区。
为什么不是远程为快递员开门,而要为他单独生成一个令牌?这是因为快递员可能每天都会来送货,第二天他还可以复用这个令牌,另外,有的小区有多重门禁,快递员可以使用同一个令牌通过它们。
Oauth2概念
- OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问用户存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享用户数据的所有内容。
- OAuth在全世界得到广泛应用,目前的版本是2.0版。
- OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。
Oauth2的角色
OAuth2协议包含几种角色:Client(客户端)、Resource Owner(资源拥有者)、Authorization Server(认证服务器)、Resource Server(资源服务器)。
-
Resource owner 资源拥有者(用户):
能够授予对受保护资源的访问权限的实体。当资源所有者是一个人时,它被称为最终用户
-
Resource Server 资源服务器:
托管受保护资源的服务器,能够使用访问令牌接受和响应受保护资源请求。
-
Client 客户端:
代表资源所有者并经其授权发出受保护资源请求的应用程序。
-
Authorization server 授权服务器:
服务器在成功验证资源所有者并获得授权后向客户端颁发访问令牌。授权服务器和资源服务器之间的交互超出了本规范的范围。授权服务器可以是与资源服务器相同的服务器,也可以是单独的实体。 单个授权服务器可以发布多个资源服务器接受的访问令牌。
白话来讲:
- Client 客户端,即我们的应用就是一个Client。
- Resource Owner 资源所有者,即用户。
- Authorization Server 授权服务器,即提供第三方登录服务的服务器,如Github,Google。
- Resource Server 拥有资源信息的服务器,通常和授权服务器属于同一应用。
根据上图的信息,可以知道OAuth2的基本流程为:
- 客户端请求(引导)用户授权。
- 用户同意授权,并返回一个凭证(code)
- 客户端通过第二步的凭证(code)向授权服务器请求授权
- 授权服务器验证凭证(code)通过后,同意授权,并返回一个资源访问的凭证(Access Token)。
- 客户端通过第四步的凭证(Access Token)向资源服务器请求相关资源。
- 资源服务器验证凭证(Access Token)通过后,将客户端请求的资源返回。
Oauth2 的四种认证模式
- 授权码模式:常见的第三方平台登录功能基本都是使用这种模式。
- 简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),一般如果网站是纯静态页面则可以采用这种方式。
- 密码形式:密码模式是用户把用户名密码直接告诉客户端,客户端使用说这些信息向授权服务器申请令牌(token)。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。
授权码模式
这种方式是最常用的流程,安全性也最高。
简化模式 (隐式授权模式)
有些 Web 应用是纯前端应用,没有后端,这时就不能用上面的方式了,必须将令牌储存在前端。
RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)。
密码形式
如果高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用去申请令牌,这种方式称为"密码式"(password)。
客户端模式
有的应用可能没有前端页面,就是一个后台。
在这个过程中好像没有用户什么事了,客户端模式给出的令牌,就是针对第三方应用的,而不是针对用户的。
go web结合Github做auth认证(官方文档)
-
先到官网上注册一个application:https://github.com/settings/developers
只有callback地址比较重要,其他的不影响认证流程: -
得到cliendt_id和client_Secret,cliendt_id是GitHub用来标识APP的,client_secret获取access_token时用到。
-
流程1:客户端请求(引导)用户授权,允许用户点击Login with Github使用Github进行登录
package main import ( "fmt" "io" "log" "net/http" "net/url" "strings" ) var ( clientID = "" clientSecrets = "" githubAuthApi = "https://github.com/login/oauth/authorize" githubTokenApi = "https://github.com/login/oauth/access_token" ) func main() { http.HandleFunc("/", IndexHandler) http.HandleFunc("/login", LoginHandler) http.HandleFunc("/callback", CallbackHandler) if err := http.ListenAndServe("localhost:8080", nil); err != nil { log.Fatalln("could not start server:", err.Error()) } } func IndexHandler(w http.ResponseWriter, r *http.Request) { templates := `<html><body><a href="/login">Login with Github</a></body></html>` if _, err := fmt.Fprintln(w, templates); err != nil { log.Fatalln("error:", err.Error()) } } func LoginHandler(w http.ResponseWriter, r *http.Request) { // redirect到github query := url.Values{} query.Set("client_id", clientID) u, err := url.ParseRequestURI(githubAuthApi) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // 拼接get请求 u.RawQuery = query.Encode() http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) }
-
流程2:用户进行github的权限校验,允许授权,github会跳转到既定的地址,并且将code传入
-
流程3:客户端通过第二步的凭证(code)向授权服务器请求授权,流程4:授权服务器验证凭证(code)通过后,同意授权,并返回一个资源访问的凭证(Access Token)。
func CallbackHandler(w http.ResponseWriter, r *http.Request) { // 接收github回调 code := r.FormValue("code") formData := url.Values{} formData.Set("client_id", clientID) formData.Set("client_secret", clientSecrets) formData.Set("code", code) request, err := http.NewRequest(http.MethodPost, githubTokenApi, strings.NewReader(formData.Encode())) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } request.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(request) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // 关闭body defer func(Body io.ReadCloser) { err = Body.Close() if err != nil { log.Println(err) } }(resp.Body) content, err := io.ReadAll(resp.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = w.Write(content) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }
-
使用access_token获取用户信息:
// 添加头信息 Authorization: Bearer OAUTH-TOKEN // 用户接口 GET https://api.github.com/user
单点登录(SSO):简化用户认证体验
单点登录(Single Sign-On,简称SSO) 是一种身份验证和授权机制,旨在让用户只需一次登录,就能够访问多个相关但独立的系统或应用。
在传统的身份验证模型中,用户需要为每个系统提供独立的凭证(通常是用户名和密码),这不仅繁琐而且容易导致安全隐患。SSO解决了这一问题,通过一次登录,用户获取了对多个系统的访问权限,而无需在每个系统中重新输入凭证。这样的集中式身份验证模型极大地提升了用户体验,减轻了用户的记忆负担,同时也有助于提高整体系统的安全性。
**SSO的概念基于用户身份的共享,通过在用户登录时颁发令牌(Token)的方式,**实现用户在不同系统之间的无缝切换。这个令牌包含有关用户身份和权限的信息,服务提供者可以通过验证这个令牌来确认用户的身份,而无需用户提供额外的凭证。
总的来说,SSO的核心理念是通过一次登录,实现对多个系统的授权,从而提高用户体验、减少密码管理的负担,并增强整体系统的安全性。
流程说明——以淘宝和天猫为例
淘宝和天猫是两个典型的电商平台,它们采用了单点登录(SSO)机制,让用户能够方便快捷地在这两个平台间切换而无需重新登录。用户首次登录淘宝,完成登录后,可以在不再输入账号和密码的情况下访问天猫。这种SSO机制使用户在淘宝和天猫之间实现无缝切换,提升了用户体验。
- 首次登录: 用户首次访问淘宝网站。在登录页面,用户输入阿里巴巴的统一账户(例如阿里巴巴账号和密码)。
- SSO认证: 阿里巴巴的SSO系统对用户进行身份验证,验证通过后生成一个令牌(Token)。
- 令牌颁发: 身份验证成功后,SSO系统颁发一个令牌给用户。
- 令牌使用: 用户使用该令牌访问淘宝网站。这个令牌包含有关用户身份的信息。
- 无需重新登录访问天猫: 用户在淘宝登录后,无需重新输入账号和密码,即可直接访问天猫网站。令牌的有效性使用户能够在淘宝和天猫之间实现无缝切换。
单点登录的两种实现方式
基于Cookie实现单点登录——存在跨域问题
Cookie-Based SSO是指使用Cookie来实现单点登录功能的一种方式。
其原理是,在用户第一次登录系统时,系统会为用户颁发一个令牌(Token)。这个令牌包含了用户身份信息和过期时间等元数据,并在服务器端保存副本。然后,系统将这个令牌放入响应的Cookie中返回给客户端浏览器(设置SetCookie),并在后续的每个请求中都携带这个Cookie。当用户访问其他应用系统时,这些系统会验证Cookie中的令牌信息,如果令牌有效,则允许用户访问系统资源。
优点:
- 实现简单:Cookie-Based SSO实现起来比较简单,不需要大量的代码。仅需要在用户登录时颁发令牌,并在每个请求中验证Cookie即可。
- 可扩展性好:可以很容易地添加新的应用系统,只需要验证Cookie中的令牌即可。
缺点
- 安全性低:Cookie-Based SSO是基于Cookie实现的,Cookie可能会被盗用或者伪造。如果攻击者获取了有效的Cookie,则可以冒充用户身份,并访问被授权的系统资源。
- 用户体验差:用户第一次登录时需要输入用户名和密码,并为每个系统都颁发一个Cookie。这样会增加用户的操作量,并且在使用多个浏览器或清理Cookie时可能会造成登录状态失效的问题。
- 难以处理跨域问题:Cookie-Based SSO只适用于同一域名下的应用程序,对于不同域名之间的系统无法实现单点登录。(一个公司的多个部门的服务自然存在多个域名(域))
基于token实现单点登录
Token-Based SSO是指使用Token来实现单点登录功能的一种方式。其原理是,用户首先在认证服务器上进行身份验证,如果验证成功,则认证服务器会颁发一个Token。然后,这个Token会被发送到客户端浏览器,并通过HTTP请求携带在请求头中或者以参数的形式传递给其他应用系统。当用户访问其他应用系统时,这些系统会向认证服务器验证Token,如果Token有效,则允许用户访问系统资源。
优点:
- 安全性高:Token-Based SSO使用Token来实现用户身份认证,Token本身是无法被伪造的,并且可以通过加密和签名等手段进一步加强安全性,可以有效防止Cookie劫持和伪造攻击。
- 用户体验好:用户只需要进行一次身份验证,并获得一个Token即可访问所有被授权的系统资源,不需要多次输入用户名和密码。同时,用户可以在任何时间清除应用系统保存的Token,以保护自身安全。
- 易于处理跨域问题:Token-Based SSO不依赖Cookie,因此可以很容易地处理跨域问题。
缺点:
- 实现复杂:Token-Based SSO需要在认证服务器上实现用户身份验证、令牌颁发和Token验证等逻辑。这些逻辑可能比较复杂,需要一定的技术能力。
- 维护成本高:认证服务器需要长期维护和管理,才能保证系统的正常运行。另外,如果有多个应用系统需要接入SSO,还需要在每个系统中添加相应的Token验证逻辑,增加了维护成本。
单点登录原理——用户在认证后获得的会话或令牌在多个系统中共享
相比于单系统登录,SSO需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。
间接授权通过令牌实现,SSO认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。
下面对上图简要描述:
-
用户初次访问或未携带 Token:
- 应用1检测到用户初次访问或未携带 Token。
- 应用1发现缺少 Token。
- 应用1返回登录地址给浏览器,并触发浏览器跳转到该登录地址。
-
用户登录认证:
- 用户向 SSO 认证中心进行登录。
- SSO 认证中心验证用户信息成功后,生成 Token 和过期时间。
- 将 Token 和过期时间返回给浏览器。
-
Token 存储与传递:
- 前端将 Token 存储在浏览器的 Cookie 中,域名设置为 test.com。
-
再次访问应用1时:
- 用户再次请求应用1,并携带 Token 在请求头中。
-
本地缓存中获取用户信息:
- 应用1从本地缓存中通过 Token 获取用户信息。
-
获取不到用户信息时,向 SSO 获取:
- 若本地缓存中未找到用户信息,应用1通过 Token 向 SSO 认证中心获取用户信息。
-
SSO 验证 Token:
- SSO 认证中心验证 Token 成功后,将用户信息和 Token 过期时间返回给应用1。
-
本地缓存用户信息:
- 应用1将用户信息、Token 和过期时间缓存在本地。
-
进一步鉴权和资源返回:
- 应用1进行进一步鉴权,验证成功后返回受保护资源。
-
使用相同 Token 再次获取资源:
- 用户下一次使用相同 Token 请求资源。
-
本地缓存中获取用户信息再次鉴权:
- 应用1通过 Token 从本地获取用户信息。
-
鉴权成功后返回资源:
- 获取到用户信息后再次进行鉴权,成功后返回受保护资源。
-
用户向虚拟电厂平台发起请求:
- 用户从 Cookie 中获取 Token 并携带访问虚拟电厂平台。
-
虚拟电厂平台获取用户信息:
- 虚拟电厂平台通过 Token 从本地缓存中获取用户信息。
-
获取不到用户信息时,向 SSO 获取:
- 若本地缓存中未找到用户信息,虚拟电厂平台通过 Token 向 SSO 认证中心获取用户信息和 Token 过期时间。
-
SSO 验证 Token:
- SSO 认证中心验证 Token 成功后,将用户信息和 Token 过期时间返回给虚拟电厂平台。
-
虚拟电厂平台本地缓存用户信息:
- 虚拟电厂平台将用户信息、Token 和过期时间缓存在本地。
-
虚拟电厂平台进一步鉴权和资源返回:
- 虚拟电厂平台进行进一步鉴权,验证成功后返回受保护资源。
-
用户登出:
- 用户向 SSO 认证中心进行登出。
-
Token 销毁和消息队列通知:
- SSO 认证中心对 Token 进行验证,验证成功后销毁 Token。
- SSO 认证中心通过消息队列向各个子系统发送通知,销毁局部 Token。
前端需要做的逻辑:
- 创建单独的登录页面:
- 前端需要设计并创建一个单独的登录页面,用于用户进行身份验证。这个页面可以由SSO认证中心提供接口。
- 跳转到登录页面:
- 当用户访问各个系统时返回需要登录提示时,前端需要触发浏览器跳转到登录页面。可以通过重定向或打开新的登录窗口的方式实现。
- 处理登录成功后的 Token:
- 在登录页面完成用户身份验证后,前端需要将从服务器获取的 Token 存储在浏览器的 Cookie 中,同时设置域名为 test.com。
- 在请求中携带 Token:
- 在向各个系统发起请求时,前端需要从浏览器的 Cookie 中获取 Token,并将 Token 放置在请求头中,以便进行身份验证。
- 处理各系统的登出操作:
- 前端需要在各个系统中实现登出功能。当用户点击登出按钮时,前端应该向 SSO 认证中心发起登出请求,实现单点登出。
- 处理各系统的更新 Token 操作:
- 如果各个系统需要更新 Token,前端应该向 SSO 认证中心发起更新 Token 的请求,确保用户的身份信息保持最新。
- 与SSO进行通信:
- 前端需要处理与 SSO 认证中心的通信。这包括处理登录请求、登出请求以及更新 Token 的请求。使用适当的前端框架或库,例如 Axios 或 Fetch API,可以更方便地进行这些操作。
SSO和Oauth2的区别
- OAuth 2.0 是一个广泛使用的授权框架,用于为第三方应用程序提供对受保护资源的访问权限。
- SSO 提供了无缝的登录体验,用户在访问多个系统时无需多次登录;而 OAuth2.0 可能需要用户在某些情况下进行授权操作。
- SSO 主要适用于企业内部系统的整合;OAuth2.0 更广泛地应用于第三方应用与平台的集成。
- OAuth2.0 在授权模式和范围上具有更大的灵活性。
FQA
Oauth2和SSO生产环境中遇到的问题
- 当用户点击页面上的”一键登录/注册",会redirect到鉴权中心的路径并且携带一个callback路径:/api/v1/callback
- 鉴权中心签发授权码code给/api/v1/callback
- callback里面获取access_token再设置Location: /index.html#?access_token=accesstoken
- 前端vue拦截到请求再向login发送post请求,携带access_token
- 后端login接口使用access_token向鉴权中心请求用户信息并且查询自己user表,没有则注册,否则设置session返回。
疑问:
- access_token好像用过一次就没什么用了。因为本站有自己的user表,通过session的方式鉴权。
- 流程是不是可以简化为第一步直接设置callback为/api/v1/login,在后端的login函数中直接拿code获取token,再拿token获取userinfo,然后判断是否存在,最后设置session,并且Location=index.html
解答:
上述操作流程是一个典型的OAuth 2.0授权码流程,结合了单点登录的概念。
关于access_token的使用: access_token确实是用来获取资源服务器上的受保护资源的,比如用户信息。在场景中,一旦使用access_token从鉴权中心获取了用户信息,并在你的系统中创建或更新了用户会话,access_token的直接作用就不大了。但是,access_token通常有一定的有效期,在这个有效期内,它可以被用来多次获取资源,这取决于具体实现和安全要求。
关于流程简化: 这是将授权码流程的前后端交互进一步后端化。可以在后端处理授权码的交换,获取access_token和用户信息,然后创建用户会话,并重定向用户到首页。这样做的好处是减少了前端的复杂性,并且可能更安全,因为access_token不需要在前后端之间传递。
简化后的流程可能如下:
- 用户点击“一键登录/注册”,被重定向到鉴权中心,并携带一个回调路径:/api/v1/login。
- 鉴权中心签发授权码code给/api/v1/login。
- 后端的/api/v1/login接口使用code向鉴权中心请求access_token。
- 使用access_token从鉴权中心获取用户信息。
- 后端查询自己的user表,没有则注册,有则更新会话。
- 设置会话后,后端重定向用户到index.html。
这样的流程更加集中和安全,因为所有的敏感操作都在后端完成,减少了前端可能的安全风险。不过,这样做也有可能增加后端的负载,因为所有的授权操作都需要通过后端来完成。此外,确保重定向和会话管理的安全性也非常重要。
为了实现SSO,你需要确保:
- 鉴权中心是一个中央认证服务,所有相关的应用都通过它进行认证。
- 用户在鉴权中心登录后,鉴权中心能够提供一个令牌(如access_token或其他形式的令牌),其他应用可以使用这个令牌来确认用户的登录状态。
- 所有应用都能够处理这个令牌,并据此建立自己的会话或其他形式的用户状态。
- 如果你的系统中有多个应用需要实现SSO,你需要确保所有应用都能够识别鉴权中心发出的令牌,并且能够正确处理用户的登录状态。这通常涉及到跨域认证和会话管理的问题,可能需要使用额外的技术或协议,如JSON Web Tokens(JWT)、SAML(Security Assertion Markup Language)等来实现。
单点和oauth2的区别是不是系统有没有独立的鉴权系统? 比我的系统其实有自己的鉴权系统,所以拿到中心的access_token获取用户信息后就没什么用了? 而真正的SSO系统,没有自己的鉴权中心,获取到access_token后,每次请求要前端携带access_token,在后端中间件里向鉴权中心请求用户信息,然后设置上下文!
单点登录(SSO)和OAuth 2.0 的区别并不仅仅在于系统是否有独立的鉴权系统,而是它们各自解决的问题和应用场景不同。
单点登录(SSO)是一种用户身份验证过程,允许用户使用一组登录凭据(如用户名和密码)访问多个应用程序。SSO 的目标是简化用户的登录体验,使得用户只需登录一次就可以访问所有相关联的系统和服务。
在 SSO 系统中,通常存在一个中央的身份提供者(IdP),所有的应用程序(服务提供者,SP)都依赖这个中央服务来进行用户的认证。用户在任何一个应用上的登录状态都会被中央身份提供者所记录,当用户尝试访问其他应用时,可以无需再次输入凭据。
OAuth 2.0 是一个授权框架,它允许第三方应用代表用户访问其在资源服务器上的信息,而无需用户分享其登录凭据。OAuth 2.0 更多地关注于授权而非认证,尽管它可以被用作认证的一部分。
在 OAuth 2.0 流程中,access_token 是用来访问资源服务器上受保护资源的凭证。一旦第三方应用获得了 access_token,它就可以在 access_token 的有效期内多次访问资源服务器上的资源。
在你的系统中,你使用 OAuth 2.0 流程获取用户信息,并在自己的系统中创建会话。这里的关键点是,你的系统有自己的用户会话管理机制,一旦用户在你的系统中被认证并创建了会话,access_token 的作用就变得有限了,因为你不需要再次验证用户的身份。
在一个“真正的”SSO系统中,access_token 或其他形式的令牌可能会被用来在多个应用之间共享认证状态。这意味着,用户在一个应用中登录后,其他应用可以通过验证令牌来识别用户的登录状态,而不需要用户重新登录。
如果你的系统需要与其他应用共享登录状态,那么你可能需要实现一个更为集成的SSO解决方案,其中你的系统作为一个服务提供者,依赖一个中央的身份提供者来管理用户的登录状态。如果你的系统是唯一的应用,那么你的OAuth 2.0实现已经足够用来管理用户的认证和授权。
参考文献
- [1] 江南一点雨.做微服务绕不过的 OAuth2,松哥也来和大家扯一扯.
- [2] 我就是个菜鸟2021.oauth2基本概念
- [3] Java鱼仔.Oauth2是个什么东西?