OAuth2 授权机制详解
OAuth2 是什么
假设用户在二狗商城中点击"使用 GitHub 登录"。
二狗商城不能要求用户直接输入 GitHub 的账号和密码,否则用户的 GitHub 密码就会暴露给二狗商城。正确流程是:
#mermaid-svg-vRQJwPuoAG1Tu7St{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vRQJwPuoAG1Tu7St .error-icon{fill:#552222;}#mermaid-svg-vRQJwPuoAG1Tu7St .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vRQJwPuoAG1Tu7St .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vRQJwPuoAG1Tu7St .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vRQJwPuoAG1Tu7St .marker.cross{stroke:#333333;}#mermaid-svg-vRQJwPuoAG1Tu7St svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vRQJwPuoAG1Tu7St p{margin:0;}#mermaid-svg-vRQJwPuoAG1Tu7St .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St .cluster-label text{fill:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St .cluster-label span{color:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St .cluster-label span p{background-color:transparent;}#mermaid-svg-vRQJwPuoAG1Tu7St .label text,#mermaid-svg-vRQJwPuoAG1Tu7St span{fill:#333;color:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St .node rect,#mermaid-svg-vRQJwPuoAG1Tu7St .node circle,#mermaid-svg-vRQJwPuoAG1Tu7St .node ellipse,#mermaid-svg-vRQJwPuoAG1Tu7St .node polygon,#mermaid-svg-vRQJwPuoAG1Tu7St .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vRQJwPuoAG1Tu7St .rough-node .label text,#mermaid-svg-vRQJwPuoAG1Tu7St .node .label text,#mermaid-svg-vRQJwPuoAG1Tu7St .image-shape .label,#mermaid-svg-vRQJwPuoAG1Tu7St .icon-shape .label{text-anchor:middle;}#mermaid-svg-vRQJwPuoAG1Tu7St .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vRQJwPuoAG1Tu7St .rough-node .label,#mermaid-svg-vRQJwPuoAG1Tu7St .node .label,#mermaid-svg-vRQJwPuoAG1Tu7St .image-shape .label,#mermaid-svg-vRQJwPuoAG1Tu7St .icon-shape .label{text-align:center;}#mermaid-svg-vRQJwPuoAG1Tu7St .node.clickable{cursor:pointer;}#mermaid-svg-vRQJwPuoAG1Tu7St .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vRQJwPuoAG1Tu7St .arrowheadPath{fill:#333333;}#mermaid-svg-vRQJwPuoAG1Tu7St .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vRQJwPuoAG1Tu7St .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vRQJwPuoAG1Tu7St .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vRQJwPuoAG1Tu7St .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vRQJwPuoAG1Tu7St .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vRQJwPuoAG1Tu7St .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vRQJwPuoAG1Tu7St .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vRQJwPuoAG1Tu7St .cluster text{fill:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St .cluster span{color:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vRQJwPuoAG1Tu7St .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vRQJwPuoAG1Tu7St rect.text{fill:none;stroke-width:0;}#mermaid-svg-vRQJwPuoAG1Tu7St .icon-shape,#mermaid-svg-vRQJwPuoAG1Tu7St .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vRQJwPuoAG1Tu7St .icon-shape p,#mermaid-svg-vRQJwPuoAG1Tu7St .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vRQJwPuoAG1Tu7St .icon-shape .label rect,#mermaid-svg-vRQJwPuoAG1Tu7St .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vRQJwPuoAG1Tu7St .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vRQJwPuoAG1Tu7St .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vRQJwPuoAG1Tu7St :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户访问二狗商城
跳转到 GitHub
用户登录并同意授权
GitHub 返回一次性 Code
二狗商城后端用 Code 换 Access Token
使用 Access Token 调用 GitHub API
这里要区分两个概念:
- 认证 Authentication:确认当前用户是谁。
- 授权 Authorization:确认某个客户端可以访问哪些资源。
OAuth2 主要解决授权。很多"微信登录""GitHub 登录",实际上是先通过 OAuth2 获得 Access Token,再调用用户信息接口获得外部用户 ID,最后在本系统中建立本地登录状态。
OAuth2 中有四个角色:
- Resource Owner:资源所有者,通常就是用户。
- Client:希望访问用户资源的客户端,例如二狗商城。
- Authorization Server:授权服务器,负责用户登录、授权确认以及签发 Code 和 Token。
- Resource Server:资源服务器,保存受保护资源,并负责校验 Access Token。
以 GitHub 登录为例,用户是 Resource Owner,二狗商城是 Client,GitHub 授权服务是 Authorization Server,GitHub 用户信息和仓库 API 是 Resource Server。
Authorization Code 授权码模式
授权码模式是 OAuth2 中最常用、最重要的流程。客户端接入授权服务器前,需要先在授权服务器注册信息:
client_id:客户端的公开标识,表示是谁发起授权。client_secret:客户端密钥,用来证明请求来自合法客户端后端。redirect_uri:授权结束后的回调地址。scope:客户端允许申请的权限范围。
client_id 可以出现在浏览器地址中,不属于秘密;client_secret 不能放在浏览器、前端代码、URL 或 App 安装包中,因为这些环境都可能被用户查看。
用户发起授权时,Client 将浏览器重定向到 Authorization Endpoint:
text
https://sso.example.com/authorize
?response_type=code
&client_id=order-system
&redirect_uri=https://order.example.com/callback
&scope=openid profile
&state=abc123
&code_challenge=xxxx
&code_challenge_method=S256
各参数含义如下:
response_type=code:希望授权服务器返回授权码。client_id:哪个客户端发起授权请求。redirect_uri:授权完成后回调到哪个地址。scope:客户端申请访问哪些资源。state:关联授权请求和回调,主要防止 CSRF。code_challenge:由code_verifier计算出的 PKCE 挑战值。code_challenge_method=S256:使用 SHA-256 计算挑战值。
scope 表示客户端申请的权限,例如 profile 表示读取基础资料,email 表示读取邮箱,order.read 表示读取订单。最终签发的权限不能超过客户端允许申请的权限、用户同意的权限以及用户实际拥有的权限。
授权服务器收到请求后,会检查 client_id、redirect_uri 和 scope 是否合法。用户登录并同意授权后,授权服务器生成一个短期、一次性的 Code,并将浏览器重定向回 Client:
text
https://order.example.com/callback
?code=code-xyz
&state=abc123
Client 首先比较返回的 state 是否与之前保存的一致。校验通过后,由 Client 后端使用 Code 请求 Token Endpoint:
这个
state是 Client 生成的
http
POST /oauth2/token HTTP/1.1
Host: sso.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=code-xyz
&client_id=order-system
&redirect_uri=https://order.example.com/callback
&code_verifier=原始随机值
有安全后端的 Client 还可以提交 client_secret。
授权服务器会检查:
- Code 是否存在、是否过期、是否已经使用。
- Code 是否属于当前
client_id。 redirect_uri是否与授权请求时一致。code_verifier是否能匹配之前的code_challenge。- 客户端身份是否可信。
校验成功后,Code 立即失效,授权服务器返回:
json
{
"access_token": "access-xxx",
"refresh_token": "refresh-yyy",
"id_token": "id-zzz",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "openid profile"
}
Client 再使用 Access Token 访问 Resource Server:
http
GET /api/userinfo HTTP/1.1
Host: api.example.com
Authorization: Bearer access-xxx
完整流程如下:
Resource Server Authorization Server Client 用户/浏览器 Resource Server Authorization Server Client 用户/浏览器 #mermaid-svg-ZObpbCpycIlIoFqO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZObpbCpycIlIoFqO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZObpbCpycIlIoFqO .error-icon{fill:#552222;}#mermaid-svg-ZObpbCpycIlIoFqO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZObpbCpycIlIoFqO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZObpbCpycIlIoFqO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZObpbCpycIlIoFqO .marker.cross{stroke:#333333;}#mermaid-svg-ZObpbCpycIlIoFqO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZObpbCpycIlIoFqO p{margin:0;}#mermaid-svg-ZObpbCpycIlIoFqO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZObpbCpycIlIoFqO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ZObpbCpycIlIoFqO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZObpbCpycIlIoFqO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ZObpbCpycIlIoFqO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ZObpbCpycIlIoFqO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ZObpbCpycIlIoFqO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ZObpbCpycIlIoFqO .sequenceNumber{fill:white;}#mermaid-svg-ZObpbCpycIlIoFqO #sequencenumber{fill:#333;}#mermaid-svg-ZObpbCpycIlIoFqO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ZObpbCpycIlIoFqO .messageText{fill:#333;stroke:none;}#mermaid-svg-ZObpbCpycIlIoFqO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZObpbCpycIlIoFqO .labelText,#mermaid-svg-ZObpbCpycIlIoFqO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ZObpbCpycIlIoFqO .loopText,#mermaid-svg-ZObpbCpycIlIoFqO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ZObpbCpycIlIoFqO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZObpbCpycIlIoFqO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ZObpbCpycIlIoFqO .noteText,#mermaid-svg-ZObpbCpycIlIoFqO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ZObpbCpycIlIoFqO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZObpbCpycIlIoFqO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZObpbCpycIlIoFqO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZObpbCpycIlIoFqO .actorPopupMenu{position:absolute;}#mermaid-svg-ZObpbCpycIlIoFqO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ZObpbCpycIlIoFqO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZObpbCpycIlIoFqO .actor-man circle,#mermaid-svg-ZObpbCpycIlIoFqO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ZObpbCpycIlIoFqO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 发起授权或第三方登录2. 生成 state 和 code_verifier3. 计算 code_challenge4. 重定向到 /authorize5. 携带 client_id、scope、state、code_challenge6. 校验客户端参数7. 展示登录和授权页面8. 用户登录并同意授权9. 返回 code 和原始 state10. 浏览器访问 callback11. 校验 state12. 后端使用 code + code_verifier 换 Token13. 校验 Code、客户端和 PKCE14. 返回 Access Token 等15. 携带 Access Token 访问资源16. 校验 Token 和 scope17. 返回受保护资源
为什么要使用 Code
授权服务器完成授权时,正在和浏览器通信。如果直接把 Access Token 放进回调 URL:
text
https://order.example.com/callback?access_token=xxx
Token 可能进入浏览器历史、Nginx 日志、Referer、监控系统或第三方脚本。
因此授权码模式把流程拆成两步:
- 浏览器只负责传递短期、一次性的 Code。
- Client 后端再使用 Code 向授权服务器换取真正的 Token。
真正的 Token 通过 Client 后端和授权服务器之间的后端通信返回,不经过浏览器 URL。
为什么通常放在 URL?因为授权完成后,授权服务器需要让浏览器自动跳回 Client,而 HTTP 302 重定向只能通过 Location 指定下一个访问地址,所以通常把 code 拼在回调 URL 上。若只放在认证服务器的响应体中,浏览器不会自动把这段响应体转发给 Client。当然也可以使用 form_post,让浏览器通过 POST 请求体提交 code,只是 URL 回调更简单、通用
state 和 PKCE
state 和 PKCE 解决的不是同一个问题。
state 由 Client 在发起授权前生成,并保存在当前用户的 Session 或安全 Cookie 中。授权服务器回调时将它原样返回,Client 比较前后是否一致。
假设攻击者伪造一个回调地址诱导用户访问,如果回调中的 state 与用户浏览器中保存的不一致,或者用户根本没有发起过这次授权,Client 就会拒绝处理。因此,state 可以防止攻击者伪造授权回调,也能避免多次授权请求之间发生串线。
PKCE 用于防止 Authorization Code 被截获。Client 先生成一个随机的 code_verifier,自己保存,然后计算:
java
code_challenge = Base64Url(SHA256(code_verifier))
发起授权时只发送 code_challenge,授权服务器会将它与 Code 绑定;换 Token 时,Client 必须提交原始的 code_verifier。攻击者即使截获了 Code,因为没有 code_verifier,也无法通过校验并换取 Access Token。
state:证明这次回调对应当前用户之前发起的授权请求。- PKCE:证明兑换 Code 的 Client,就是最初发起授权请求的 Client。
Access Token、Refresh Token 和 ID Token
Access Token 是访问 Resource Server 的凭证:
http
Authorization: Bearer <AccessToken>
如果 Access Token 是 JWT,可能包含:
json
{
"sub": "10001",
"scope": "order.read",
"iss": "https://sso.example.com",
"aud": "order-service",
"exp": 1785493800
}
sub:当前用户或主体是谁。scope:允许访问哪些资源。iss:由哪个授权服务器签发。aud:Token 签发给哪个 Resource Server。exp:Token 的过期时间。
授权服务器负责决定并签发 scope,Gateway 或 Resource Server 根据 scope 执行真正的接口鉴权。
如果你的系统使用的 RBAC 三级鉴权,Gateway 可以根据 Token 中的
userId查询 Redis 或权限服务,获取用户的角色和权限后完成接口鉴权;也可以在登录时将权限写入 Token 的permissions或scope中,由 Gateway 直接读取并鉴权。前者权限变更能较快生效,后者性能更好,但 Token 过期前可能存在权限数据不及时的问题。第三方 OAuth2 中的
scope更强调用户授权某个 Client 可以访问本平台的哪些资源;内部系统的 Gateway 鉴权更强调当前用户在本系统中具备哪些角色和接口权限。两者可以使用相似的字符串表示,但含义并不完全相同。
Access Token 也可以是不透明随机字符串。但这种 Token 本身看不出用户和权限,Resource Server 需要查询共享存储,或者调用授权服务器提供的 Token Introspection 接口确认是否有效。
Refresh Token 用于 Access Token 过期后申请新的 Access Token。它的有效期一般更长,只能发送给 Authorization Server,不能直接访问普通业务接口。
ID Token 属于 OIDC,主要告诉 Client"当前完成登录的用户是谁"。因为 OAuth2 本身只是授权,没办法完成认证
三种 Token 的区别:
- Access Token:给 Resource Server 看,用于访问 API。
- Refresh Token:给 Authorization Server 看,用于刷新 Access Token。
- ID Token:给 Client 看,用于确认登录用户是谁。
不要使用 ID Token 代替 Access Token 调用业务接口。
OAuth2、OIDC 与第三方登录
OAuth2 主要解决"客户端可以访问哪些资源",OIDC 建立在 OAuth2 之上,补充身份认证能力,解决"当前登录用户是谁"。
OIDC 通常增加:
- scope 中添加
openid。 - ID Token。
- UserInfo Endpoint。
nonce。- 标准用户身份字段。
很多"微信登录""GitHub 登录"虽然经常被称为 OAuth2 登录,但实际过程通常是:
#mermaid-svg-WvbQ9XzwaDiE6sPy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WvbQ9XzwaDiE6sPy .error-icon{fill:#552222;}#mermaid-svg-WvbQ9XzwaDiE6sPy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WvbQ9XzwaDiE6sPy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .marker.cross{stroke:#333333;}#mermaid-svg-WvbQ9XzwaDiE6sPy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WvbQ9XzwaDiE6sPy p{margin:0;}#mermaid-svg-WvbQ9XzwaDiE6sPy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .cluster-label text{fill:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .cluster-label span{color:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .cluster-label span p{background-color:transparent;}#mermaid-svg-WvbQ9XzwaDiE6sPy .label text,#mermaid-svg-WvbQ9XzwaDiE6sPy span{fill:#333;color:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .node rect,#mermaid-svg-WvbQ9XzwaDiE6sPy .node circle,#mermaid-svg-WvbQ9XzwaDiE6sPy .node ellipse,#mermaid-svg-WvbQ9XzwaDiE6sPy .node polygon,#mermaid-svg-WvbQ9XzwaDiE6sPy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .rough-node .label text,#mermaid-svg-WvbQ9XzwaDiE6sPy .node .label text,#mermaid-svg-WvbQ9XzwaDiE6sPy .image-shape .label,#mermaid-svg-WvbQ9XzwaDiE6sPy .icon-shape .label{text-anchor:middle;}#mermaid-svg-WvbQ9XzwaDiE6sPy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .rough-node .label,#mermaid-svg-WvbQ9XzwaDiE6sPy .node .label,#mermaid-svg-WvbQ9XzwaDiE6sPy .image-shape .label,#mermaid-svg-WvbQ9XzwaDiE6sPy .icon-shape .label{text-align:center;}#mermaid-svg-WvbQ9XzwaDiE6sPy .node.clickable{cursor:pointer;}#mermaid-svg-WvbQ9XzwaDiE6sPy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .arrowheadPath{fill:#333333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WvbQ9XzwaDiE6sPy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WvbQ9XzwaDiE6sPy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WvbQ9XzwaDiE6sPy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WvbQ9XzwaDiE6sPy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .cluster text{fill:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy .cluster span{color:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WvbQ9XzwaDiE6sPy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WvbQ9XzwaDiE6sPy rect.text{fill:none;stroke-width:0;}#mermaid-svg-WvbQ9XzwaDiE6sPy .icon-shape,#mermaid-svg-WvbQ9XzwaDiE6sPy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WvbQ9XzwaDiE6sPy .icon-shape p,#mermaid-svg-WvbQ9XzwaDiE6sPy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WvbQ9XzwaDiE6sPy .icon-shape .label rect,#mermaid-svg-WvbQ9XzwaDiE6sPy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WvbQ9XzwaDiE6sPy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WvbQ9XzwaDiE6sPy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WvbQ9XzwaDiE6sPy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户选择 GitHub 登录
通过 OAuth2 获得 GitHub Access Token
调用 GitHub 用户信息接口
获得 githubId
是否绑定本地账号
找到本地用户
创建账号或绑定手机号
创建本系统 Session 或 JWT
GitHub Access Token 只能用于访问 GitHub,不能直接作为二狗商城自己的请求凭证。二狗商城确认用户身份后,需要建立本地 Session,或者由自己的认证中心签发本系统的 Access Token,供后续访问二狗商城接口使用。
两个 Access Token 来源不同,不要混了
OAuth2 负责获得授权结果,并不规定业务系统必须使用 Session 还是 JWT。