diff --git "a/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.en.md" "b/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.en.md" index bd67b21..d6d1efd 100644 --- "a/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.en.md" +++ "b/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.en.md" @@ -1,108 +1,129 @@ --- -title: Web应用接入OAuth2 +title: Web OAuth 2.0 authorization date: 2023-12-27 19:41:20 tags: ["Web", "OAuth2", "Google", "Apple", "Facebook"] series: ["前端开发"] -category: ["blog","前端"] +category: ["blog", "前端"] featured: true - --- +Accessing third-party authorization login from Google, Apple, and Facebook was actually done several years ago. Coincidentally, there is a project that needs to be done again recently. I found some modifications to the interaction details, and the document is quite hidden. Therefore, I have compiled and summarized the document information for reference by my team colleagues later. + +To briefly introduce, OAuth (Open Authorization) is a token based authorization protocol. That is "Sign In with xxx". Mainstream platforms offer this authentication service, which allows us to authenticate and authorize login to our applications through third-party authentication providers. Enterprise level applications generally use this service, making it convenient for resource owners (RO) to use resource information on resource servers (RS). For more information, refer to the link below. + +## Documentation + +Introducing the OAuth2: -好几年前其实就做过这类需求了,刚好最近有一个项目又要接入 Google\Apple\Facebook 三方授权登录的需求,做的时候发现交互细节的修改,文档藏的比较深,所以整理汇总了下文档资料,方便后面给团队同事参考用。 -简单介绍下,OAuth(Open Authorization) 是一个基于令牌的授权协议。即 “Sign In with xxx”。主流平台都有提供的这项认证服务,可以通过第三方认证提供商进行身份验证和授权登录我们的应用,企业级应用一般都会上这个,方便资源拥有者(RO)使用在资源服务器(RS)的资源信息。更多了解参考下方链接。 +[理解 OAuth 2.0 - 阮一峰的网络日志](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) + +Sign In with Apple: -## 官方文档 -介绍 OAuth2 标准: -[理解OAuth 2.0 - 阮一峰的网络日志](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) -Sign In with Apple: [https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) + [https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) -Sign In with Google: + +Sign In with Google: + [https://developers.google.cn/identity/protocols/oauth2/javascript-implicit-flow?hl=zh-cn](https://developers.google.cn/identity/protocols/oauth2/javascript-implicit-flow?hl=zh-cn) -Sign In with Facebook: + +Sign In with Facebook: + [https://developers.facebook.com/docs/facebook-login/](https://developers.facebook.com/docs/facebook-login/) + JS SDK [https://developers.facebook.com/docs/javascript/reference/v18.0](https://developers.facebook.com/docs/javascript/reference/v18.0) -手动构建登录流程,不走SDK [https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog) + +手动构建登录流程,不走 SDK [https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog) + [使用 Facebook 登录功能和现有应用登录系统集成](https://developers.facebook.com/docs/facebook-login/guides/advanced/existing-system) + [https://developers.facebook.com/docs/facebook-login/guides/access-tokens](https://developers.facebook.com/docs/facebook-login/guides/access-tokens) 获取长期口令 + ## OAuth 工作流程 + #### 角色: -- 业务系统(我们的应用 Application) - - 前端(客户端 client ) - - 后台(客户端 API) -- 用户(Resource Owner) -- 认证服务(Authorization server,Google、Apple、微信、QQ等第三方授权服务器) -- 资源服务器 (Resource server) +- 业务系统(我们的应用 Application) + - 前端(客户端 client) + - 后台(客户端 API) +- 用户(Resource Owner) +- 认证服务(Authorization server,Google、Apple、微信、QQ 等第三方授权服务器) +- 资源服务器 (Resource server) + #### 流程: - - 客户端点击登录,此时浏览器导向授权服务器认证页面,携带客户端标识和重定向URI。客户端向授权服务器请求授权, - - 授权服务器验证参数,然后用户未登录需先登录,并选择是否给该客户端授权。 - - 用户同意授权,授权服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个code授权码(可能是get,也可能是post方式)。 - - 前端客户端收到授权码后,会通知客户端后台API,通过 POST 请求,附上上次的一样的"重定向URI",向授权服务器申请令牌(access_token)。这一步是在客户端的后端API隐藏式完成的,对前端侧的用户不可见。 - - 授权服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)或更新令牌(refresh token)。 - - 客户端收到token后,传回给前端view并更新页面,用户登录成功。 - - 以上登录流程已经完成,需要补充的是:此时客户端可以使用访问令牌向资源服务器请求资源了(具体要看申请了那些权限,如果只是email 和name,就没啥用)。 +- 客户端点击登录,此时浏览器导向授权服务器认证页面,携带客户端标识和重定向 URI。客户端向授权服务器请求授权, +- 授权服务器验证参数,然后用户未登录需先登录,并选择是否给该客户端授权。 +- 用户同意授权,授权服务器将用户导向客户端事先指定的"重定向 URI"(redirection URI),同时附上一个 code 授权码(可能是 get,也可能是 post 方式)。 +- 前端客户端收到授权码后,会通知客户端后台 API,通过 POST 请求,附上上次的一样的"重定向 URI",向授权服务器申请令牌(access_token)。这一步是在客户端的后端 API 隐藏式完成的,对前端侧的用户不可见。 +- 授权服务器核对了授权码和重定向 URI,确认无误后,向客户端发送访问令牌(access token)或更新令牌(refresh token)。 +- 客户端收到 token 后,传回给前端 view 并更新页面,用户登录成功。 +- 以上登录流程已经完成,需要补充的是:此时客户端可以使用访问令牌向资源服务器请求资源了(具体要看申请了那些权限,如果只是 email 和 name,就没啥用)。 + #### 几种模式: + **授权码模式(是最复杂的,也是最安全的,上面流程就是这种,推荐使用)** - - 客户端请求验证,由 用户获取 code - - 客户端后端拿着 code,去请求 token - - 销毁 code,下发 token,而用户拿不到 token,客户端保存 - - 客户端使用 token 访问资源服务器的资源 - - 过期后使用 refresh_token 刷新, token 再次使用 +- 客户端请求验证,由 用户获取 code +- 客户端后端拿着 code,去请求 token +- 销毁 code,下发 token,而用户拿不到 token,客户端保存 +- 客户端使用 token 访问资源服务器的资源 +- 过期后使用 refresh_token 刷新,token 再次使用 **简化模式(**为 web 浏览器应用设计,不支持 refresh token**)** - - 用户请求网站,如:http://www.baidu.com - - 重定向到一个授权页面 - - 用户登录,并同意授权 - - 重定向到网站,并带上access_token如:www.baidu.com?access_token=123 - - 访问 资源服务器的资源 - +- 用户请求网站,如:http://www.baidu.com +- 重定向到一个授权页面 +- 用户登录,并同意授权 +- 重定向到网站,并带上 access_token 如:www.baidu.com?access_token=123 +- 访问 资源服务器的资源 #### 以 Gitee 网站为例,使用 GitHub 账号登录(授权码模式) + 1、点击登录,触发到 GitHub 的认证页面 2、允许授权,302 重定向到 Gitee 的 redirect_uri 页面 -3、页面 接受 code 参数后,请求后端,由后端再去获取GitHub的 accessToken,返回给前端 -4、再次 302 重定向到 Gitee 首页,并携带Cookie,完成用户登录成功状态。 +3、页面 接受 code 参数后,请求后端,由后端再去获取 GitHub 的 accessToken,返回给前端 +4、再次 302 重定向到 Gitee 首页,并携带 Cookie,完成用户登录成功状态。 ```html https://github.com/login/oauth/authorize?client_id=5a179b878a9f6ac42acd& -redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fgithub%2Fcallback& -response_type=code&scope=user& +redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fgithub%2Fcallback& response_type=code&scope=user& state=bc9c9fb74d0ad5745f891bc370b9de1cafb46e2a417793fa - ``` + 认证后,重定向 -![认证后重定向](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756156517-55c266f1-c8aa-4601-b2b2-b7035a7d6ba0.png#averageHue=%23363232&clientId=u1e30360e-97ca-4&from=paste&height=792&id=u9dd11888&originHeight=1188&originWidth=1624&originalType=binary&ratio=1.5&rotation=0&showTitle=true&size=224513&status=done&style=none&taskId=u01a3e0ad-3f6a-4667-9a5c-9e7afec5594&title=%E8%AE%A4%E8%AF%81%E5%90%8E%E9%87%8D%E5%AE%9A%E5%90%91&width=1082.6666666666667 "认证后重定向") +![认证后重定向](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756156517-55c266f1-c8aa-4601-b2b2-b7035a7d6ba0.png "认证后重定向") + ```html https://gitee.com/auth/github/callback?code=9c637527cce64b8a5736&state=bc9c9fb74d0ad5745f891bc370b9de1cafb46e2a417793fa ``` -获取GitHub的 accessToken -![获取GitHub的 accessToken](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756251104-6d3e2129-76fe-4619-9f69-1f901b1b69a6.png#averageHue=%23313131&clientId=u1e30360e-97ca-4&from=paste&height=778&id=ud62a00c9&originHeight=1167&originWidth=1604&originalType=binary&ratio=1.5&rotation=0&showTitle=true&size=216271&status=done&style=none&taskId=u2d21ba42-9456-4d96-ae08-1ec62c72f71&title=%E8%8E%B7%E5%8F%96GitHub%E7%9A%84%20accessToken&width=1069.3333333333333 "获取GitHub的 accessToken") + +获取 GitHub 的 accessToken +![获取 GitHub 的 accessToken](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756251104-6d3e2129-76fe-4619-9f69-1f901b1b69a6.png "获取GitHub的 accessToken") 重定向到首页 -![重定向到首页](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756570890-fcfa1b8c-2b94-4258-8d06-f60101329f5a.png#averageHue=%23d9ae57&clientId=u1e30360e-97ca-4&from=paste&height=800&id=u16650903&originHeight=1200&originWidth=1983&originalType=binary&ratio=1.5&rotation=0&showTitle=true&size=413240&status=done&style=none&taskId=u10797c3f-644f-49b9-b150-61011a0f15a&title=%E9%87%8D%E5%AE%9A%E5%90%91%E5%88%B0%E9%A6%96%E9%A1%B5&width=1322 "重定向到首页") -流程完成,下面分别介绍三种,我们应用用上的OAuth场景。 +![重定向到首页](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756570890-fcfa1b8c-2b94-4258-8d06-f60101329f5a.png "重定向到首页") +流程完成,下面分别介绍三种,我们应用用上的 OAuth 场景。 + ## 集成 Apple 授权登录 (Sign in with Apple) + 苹果官方文档: -[https://developer.apple.com/documentation/sign_in_with_apple](https://developer.apple.com/documentation/sign_in_with_apple) (前端js调用,发起授权) -[https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api)( REST API ,后端同学看的) +[https://developer.apple.com/documentation/sign_in_with_apple](https://developer.apple.com/documentation/sign_in_with_apple) (前端 js 调用,发起授权) +[https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api)(REST API,后端同学看的) 第三方文档参考: [https://zhuanlan.zhihu.com/p/632483498](https://zhuanlan.zhihu.com/p/632483498) [Sign in with Apple NODE,苹果第三方登录](https://segmentfault.com/a/1190000020786994#item-4-5) [What the Heck is Sign In with Apple?](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) -Sign in with Apple Tutorial: +Sign in with Apple Tutorial: 1. [Sign in with Apple, Part 1: Apps](https://sarunw.com/posts/sign-in-with-apple-1) 2. [Sign in with Apple, Part 2: Private Email Relay Service](https://sarunw.com/posts/sign-in-with-apple-2) 3. [Sign in with Apple, Part 3: Backend – Token verification](https://sarunw.com/posts/sign-in-with-apple-3) 4. [Sign in with Apple, Part 4: Web and Other Platforms](https://sarunw.com/posts/sign-in-with-apple-4) -**授权码模式(和 Google 不同的是,redirect-url 回传的code需要有一个post接口来接收):** -```html +**授权码模式(和 Google 不同的是,redirect-url 回传的 code 需要有一个 post 接口来接收):** + +``` app client 向 app server 请求 oauth url app client 收到 url ,点击事件触发访问 oauth url, 跳转到 apple id server @@ -114,29 +135,37 @@ app server 验证 authorization code 和 id_token 是在同一个请求下发的 app server 成功返回用户信息给 app client ``` -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703773909091-5843f905-22b0-4f51-a075-5ea98eb6ec04.png#averageHue=%23f3f3f3&clientId=u7f2dfee7-d8e6-4&from=paste&height=491&id=ucac3f60c&originHeight=491&originWidth=628&originalType=binary&ratio=1&rotation=0&showTitle=false&size=93721&status=done&style=none&taskId=u5ac69480-0a9e-4bc9-84cc-fc73fd1334b&title=&width=628) -#### 注册&配置 OAuth2应用 -参考官方文档即可。主要是为了 获取客户端ID和客户端密钥:注册成功后,OAuth2提供商将为您的应用程序分配一个客户端ID和客户端密钥。还有重定向 redirectURI,这些凭据将在后续的OAuth2交互中使用。 -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703754389135-2963205e-d366-44e6-931f-af6cc149a44f.png#averageHue=%23dedddd&clientId=u1e30360e-97ca-4&from=paste&height=475&id=u6d670983&originHeight=712&originWidth=804&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=180368&status=done&style=none&taskId=u0d1dd340-85fd-4b82-8fbc-0410946e589&title=&width=536) -当配置结束后,我们将获得我们所需的两个文件、三个ID、和一个URL连接,如下: -```jsx -redirectURI = 'https://xx.xxx.online/login/oauth-url' // 自己设置的重定向域名,可添加多个 -webClientId = 'com.xx.cn'; // 设置的client_id,一般是域名的反写 -teamId = 'xx'; // 10个字符的team_id -keyId = 'KOI23S78J6'; // 获取的10个字符的密钥标识符 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703773909091-5843f905-22b0-4f51-a075-5ea98eb6ec04.png) + +#### 注册&配置 OAuth2 应用 +参考官方文档即可。主要是为了 获取客户端 ID 和客户端密钥:注册成功后,OAuth2 提供商将为您的应用程序分配一个客户端 ID 和客户端密钥。还有重定向 redirectURI,这些凭据将在后续的 OAuth2 交互中使用。 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703754389135-2963205e-d366-44e6-931f-af6cc149a44f.png) + +当配置结束后,我们将获得我们所需的两个文件、三个 ID、和一个 URL 连接,如下: + +```jsx +redirectURI = "https://xx.xxx.online/login/oauth-url"; // 自己设置的重定向域名,可添加多个 +webClientId = "com.xx.cn"; // 设置的 client_id,一般是域名的反写 +teamId = "xx"; // 10 个字符的 team_id +keyId = "KOI23S78J6"; // 获取的 10 个字符的密钥标识符 ``` 设置登录徽标样式 + [https://appleid.apple.com/signinwithapple/button](https://appleid.apple.com/signinwithapple/button) -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775721626-23734410-a70c-4d7c-99c9-8aa6a88088bb.png#averageHue=%23fafafa&clientId=u7f2dfee7-d8e6-4&from=paste&height=483&id=u81212d96&originHeight=483&originWidth=571&originalType=binary&ratio=1&rotation=0&showTitle=false&size=44215&status=done&style=none&taskId=ud456e44c-8fc3-41c0-8cae-bb45a1a9d2c&title=&width=571) -#### Web 代码实现1( 这种是 OAuth 2.0 简化模式) -前端 SDK触发 + +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775721626-23734410-a70c-4d7c-99c9-8aa6a88088bb.png) + +#### Web 代码实现 1(这种是 OAuth 2.0 简化模式) + +前端 SDK 触发 + ```jsx -// 1\页面注入js sdk +// 1\页面注入 js sdk @@ -148,7 +177,7 @@ useEffect(() => { usePopup: true, redirectURI: redirectUrl, state: 'init00state', - responseType: 'code', // 有效值为code和id_token。 + responseType: 'code', // 有效值为 code 和 id_token。 }); }, [APPLEKey]); @@ -165,7 +194,7 @@ const data = await AppleID?.auth?.signIn({ ); console.log('APPLE signIn 的回调 datadatadata:', data); -// 4\处理响应、与自己的登录接口做交互,返回用户info,完成登录。 +// 4\处理响应、与自己的登录接口做交互,返回用户 info,完成登录。 if (data?.authorization?.code) { const authTokenLocal = jwt.decode(data?.authorization?.id_token) || {}; const oauthOpenId = get(authTokenLocal, 'sub'); @@ -177,7 +206,8 @@ if (data?.authorization?.code) { } ``` -APPLE 登录成功的回调 data, 拿着 id_token 和自己的 后端api 做绑定或登录,即完成业务。 +APPLE 登录成功的回调 data, 拿着 id_token 和自己的 后端 api 做绑定或登录,即完成业务。 + ```json { "authorization": { @@ -203,8 +233,11 @@ APPLE 登录成功的回调 data, 拿着 id_token 和自己的 后端api 做绑 } } ``` -#### Web 代码实现2( 这种是 授权模式) + +#### Web 代码实现 2(这种是 授权模式) + 手工触发 (授权模式) + ```jsx https://appleid.apple.com/auth/authorize?client_id=[CLIENT_ID]&redirect_uri=[REDIRECT_URI]&response_type=[RESPONSE_TYPE]&scope=[SCOPES]&response_mode=[RESPONSE_MODE]&state=[STATE] @@ -217,184 +250,193 @@ https://appleid.apple.com/auth/oauth2/v2/authorize ``` #### 登录流程 -用户点击 Apple 登录图标,会跳转到 Apple 登录网站,输入账号密码。首次登录会在任一 Apple设备弹出原生登录授权验证(双重验证),输入6位随机验证码 即可登录完成。 -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775181457-dc133d2b-a274-41e6-9e65-c841eda23224.png#averageHue=%23f1f1f1&clientId=u7f2dfee7-d8e6-4&from=paste&height=340&id=ue35bc92c&originHeight=340&originWidth=557&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23752&status=done&style=none&taskId=u0ab9e70f-cbf5-48b5-8c78-6ca6a1fced7&title=&width=557) -注意:首次登录会选择是否隐藏邮箱,选择隐藏将会使用apple提供的一个匿名邮箱而不是真实邮箱号。当选择信任浏览器后,之后在此浏览器中登录只需要输入账号、密码即可。 -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775215350-7bac5760-3198-46d4-8b7b-4eb9c1ef3bb1.png#averageHue=%23fcfcfc&clientId=u7f2dfee7-d8e6-4&from=paste&height=354&id=uedd78471&originHeight=354&originWidth=517&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30409&status=done&style=none&taskId=u6c046b5b-6f26-42bc-9a6a-c4211f0b0b5&title=&width=517) -在登录后用户可以随时在apple设备上取消apple id在该程序上的授权登录。mac上safari浏览器上可直接验证登录。也可以通过手机号等其他方式进行验证,apple设备如果开启双重认证,会使用双重验证,简单说就是当你首次使用Apple登录一个设备时,在输入Apple id和密码之后,还需要在其他已登录的Apple设备上确认授权,并输入已登录设备上提供的验证码进行验证。 + +用户点击 Apple 登录图标,会跳转到 Apple 登录网站,输入账号密码。首次登录会在任一 Apple 设备弹出原生登录授权验证 (双重验证),输入 6 位随机验证码 即可登录完成。 + +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775181457-dc133d2b-a274-41e6-9e65-c841eda23224.png#taskId=u0ab9e70f-cbf5-48b5-8c78-6ca6a1fced7&title=&width=557) + +注意:首次登录会选择是否隐藏邮箱,选择隐藏将会使用 apple 提供的一个匿名邮箱而不是真实邮箱号。当选择信任浏览器后,之后在此浏览器中登录只需要输入账号、密码即可。 + +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775215350-7bac5760-3198-46d4-8b7b-4eb9c1ef3bb1.png) + +在登录后用户可以随时在 apple 设备上取消 apple id 在该程序上的授权登录。mac 上 safari 浏览器上可直接验证登录。也可以通过手机号等其他方式进行验证,apple 设备如果开启双重认证,会使用双重验证,简单说就是当你首次使用 Apple 登录一个设备时,在输入 Apple id 和密码之后,还需要在其他已登录的 Apple 设备上确认授权,并输入已登录设备上提供的验证码进行验证。 + #### 处理授权响应(分两种模式) + 用户单击“使用 Apple 登录”按钮后,框架会将授权信息发送给 Apple。Apple 处理授权请求后: -如果是 usePopup 模式,是直接返回 data【简化模式】; -否则会将包含授权结果的 HTTP POST 请求发送到 中提供的 URL redirectURI【授权码模式】 。 -HTTP 正文包含 content-type 为 application/x-www-form-urlencoded 的结果参数。成功的响应包含以下参数: + 如果是 usePopup 模式,是直接返回 data【简化模式】; + 否则会将包含授权结果的 HTTP POST 请求发送到 中提供的 URL redirectURI【授权码模式】 。 + +HTTP 正文包含 content-type 为 application/x-www-form-urlencoded 的结果参数。成功的响应包含以下参数: ## 集成 Google OAuth2 #### 需要注意 -##### 一、 google 的 gsi/client 和 oauth2 的区别 + +##### 一、google 的 gsi/client 和 oauth2 的区别 + > Google Identity Services (GIS) 的 gsi/client 是用于实现用户登录的 JavaScript 库。它可以帮助开发者在网页中集成“使用 Google 账号登录”的功能。当用户点击登录按钮后,gsi/client 会弹出一个 Google 登录界面,用户可以选择使用他们的 Google 账号进行登录。这样,开发者就可以通过 gsi/client 获取用户的身份信息,比如用户的邮箱地址等。 -> OAuth2 则是通过授权code,来获取访问令牌,然后使用该访问令牌来调用 Google API 或其他受保护的资源。 +> OAuth2 则是通过授权 code,来获取访问令牌,然后使用该访问令牌来调用 Google API 或其他受保护的资源。 综上: -gsi/client 用于前端直接新开一个弹窗,实现帮用户用Google实现登录网页的功能。【简化模式】 -OAuth2 则是先获取访问令牌后,Google重定向页面 URL 得到后授权码code,拿这个授权码去接口去授权,适合在不能弹窗的场景下使用。【授权码模式】 +gsi/client 用于前端直接新开一个弹窗,实现帮用户用 Google 实现登录网页的功能。【简化模式】 +OAuth2 则是先获取访问令牌后,Google 重定向页面 URL 得到后授权码 code,拿这个授权码去接口去授权,适合在不能弹窗的场景下使用。【授权码模式】 + ##### 二、不允许使用的嵌入式用户代理中 -Google的授权页面,在 Google 的 OAuth 2.0 政策中 disallowed_useragent 要求不能被webview嵌套。解决办法在下面: + +Google 的授权页面,在 Google 的 OAuth 2.0 政策中 disallowed_useragent 要求不能被 webview 嵌套。解决办法在下面: + Android 开发者在 android.webkit.WebView 中打开授权请求时可能会遇到此错误消息。开发者应该改用 Android 库,例如适用于 Android 的 Google 登录或 OpenID 基金会的 AppAuth for Android。当 Android 应用通过嵌入式用户代理打开常规 Web 链接,且用户从您的网站转到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许在操作系统的默认链接处理程序(包括 Android App Links 处理程序或默认浏览器应用)中打开常规链接。Android 自定义标签页库也是一个受支持的选项。 + iOS 和 macOS 开发者在 WKWebView 中打开授权请求时可能会遇到此错误。开发者应该改用 iOS 库,例如适用于 iOS 的 Google 登录或 OpenID 基金会的 AppAuth for iOS。当 iOS 或 macOS 应用在嵌入式用户代理中打开常规 Web 链接,且用户从您的网站转到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许操作系统的默认链接处理程序(包括通用链接处理程序或默认浏览器应用)中打开常规链接。SFSafariViewController 库也是一个受支持的选项。 -#### Web 代码实现1(这种属于 授权码模式) +#### Web 代码实现 1(这种属于 授权码模式) + 授权码模式是最复杂的,也是最安全的 - - 客户端发起请求验证,由用户获取 code - - 客户端拿到 code,请求 token - - 销毁 code,下发 token,而用户拿不到 token,客户端保存 - - 客户端使用 token 访问资源 - - 过期后使用 refresh_token 刷新, token 再次使用 +- 客户端发起请求验证,由用户获取 code +- 客户端拿到 code,请求 token +- 销毁 code,下发 token,而用户拿不到 token,客户端保存 +- 客户端使用 token 访问资源 +- 过期后使用 refresh_token 刷新,token 再次使用 + +Create form to request - Create form to request ```jsx /* - * Create form to request access token from Google's OAuth 2.0 server. - */ + * Create form to request access token from Google's OAuth 2.0 server. + */ const oauthSignIn = (key) => { - // Google's OAuth 2.0 endpoint for requesting an access token - const oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth'; - - // Create
element to submit parameters to OAuth 2.0 endpoint. - const googleForm = document.createElement('form'); - googleForm.setAttribute('method', 'GET'); // Send as a GET request. - googleForm.setAttribute('action', oauth2Endpoint); - - // Parameters to pass to OAuth 2.0 endpoint. - const params = { - client_id:clientId, - redirect_uri: redirectUrl, - response_type: 'code', - scope: 'https://www.googleapis.com/auth/userinfo.profile', - include_granted_scopes: "true", - state: OAUTH_LOGIN_PLATFORM_TYPE.GOOGLE, - }; - - // Add form parameters as hidden input values. - // eslint-disable-next-line no-restricted-syntax - for (const p in params) { - const input = document.createElement("input"); - input.setAttribute("type", "hidden"); - input.setAttribute('name', p); - input.setAttribute('value', params[p]); - googleForm.appendChild(input); - } + // Google's OAuth 2.0 endpoint for requesting an access token + const oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + + // Create element to submit parameters to OAuth 2.0 endpoint. + const googleForm = document.createElement("form"); + googleForm.setAttribute("method", "GET"); // Send as a GET request. + googleForm.setAttribute("action", oauth2Endpoint); + + // Parameters to pass to OAuth 2.0 endpoint. + const params = { + client_id: clientId, + redirect_uri: redirectUrl, + response_type: "code", + scope: "https://www.googleapis.com/auth/userinfo.profile", + include_granted_scopes: "true", + state: OAUTH_LOGIN_PLATFORM_TYPE.GOOGLE, + }; - // Add form to page and submit it to open the OAuth 2.0 endpoint. - document.body.appendChild(googleForm); - googleForm.submit(); -}; + // Add form parameters as hidden input values. + // eslint-disable-next-line no-restricted-syntax + for (const p in params) { + const input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", p); + input.setAttribute("value", params[p]); + googleForm.appendChild(input); + } + // Add form to page and submit it to open the OAuth 2.0 endpoint. + document.body.appendChild(googleForm); + googleForm.submit(); +}; ``` + OAuth 2.0 服务器响应 -```markdown -// 正确响应:code交给自己的服务器api,完成后续操作。 +```markdown +// 正确响应:code 交给自己的服务器 api,完成后续操作。 http://oauth2.example.com/callback?state=G&code=4%2F0AfJohXmMnygj-k1dcfv8k8bObKZOkeg&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none // 错误响应: https://oauth2.example.com/callback#error=access_denied - ``` -#### Web 代码实现2(这种属于简化模式) +#### Web 代码实现 2(这种属于简化模式) + ```javascript -// 传统 web登录 +// 传统 web 登录 useEffect(() => { - initGoogleScript(); + initGoogleScript(); }, []); - - // eslint-disable-next-line no-unused-vars const initGoogleScript = () => { - // const script = document.createElement('script'); - // script.src = 'https://accounts.google.com/gsi/client'; - // document.head.append(script); - function handleCredentialResponse(response) { - const authTokenLocal = jwt.decode(response.credential) || {}; - console.log("GOOGLE 回调 authTokenLocal", response.credential,authTokenLocal); - const oauthOpenId = get(authTokenLocal, 'sub'); - // const email = get(authTokenLocal, 'email'); - // 如果是走前端自己组织的URL,才和APP相同的处理函数 - if (oauthOpenId) { - loginSuccess(res); + // const script = document.createElement('script'); + // script.src = 'https://accounts.google.com/gsi/client'; + // document.head.append(script); + function handleCredentialResponse(response) { + const authTokenLocal = jwt.decode(response.credential) || {}; + console.log("GOOGLE 回调 authTokenLocal", response.credential, authTokenLocal); + const oauthOpenId = get(authTokenLocal, "sub"); + // const email = get(authTokenLocal, 'email'); + // 如果是走前端自己组织的 URL,才和 APP 相同的处理函数 + if (oauthOpenId) { + loginSuccess(res); + } } - } - loadScript('https://accounts.google.com/gsi/client', () => { - console.log('loadScript....'); - try { - // eslint-disable-next-line no-undef - google.accounts.id.initialize({ - client_id: '643984392818-v2aad7hxx3r65h29imbq4d.apps.googleusercontent.com', // GOOGLE_CLIENT_ID, - callback: handleCredentialResponse, - }); - - // eslint-disable-next-line no-undef - google.accounts.id.renderButton( - document.getElementById('btn-google-login'), - { - icon: 'standard', - type: 'icon', - shape: 'circle', - theme: 'outline', - text: 'signin_with', - size: 'large', - }, // customization attributes - ); - // google.accounts.id.prompt(); // also display the One Tap dialog - } catch { - // - } - }); + loadScript("https://accounts.google.com/gsi/client", () => { + console.log("loadScript...."); + try { + // eslint-disable-next-line no-undef + google.accounts.id.initialize({ + client_id: "643984392818-v2aad7hxx3r65h29imbq4d.apps.googleusercontent.com", // GOOGLE_CLIENT_ID, + callback: handleCredentialResponse, + }); + + // eslint-disable-next-line no-undef + google.accounts.id.renderButton( + document.getElementById("btn-google-login"), + { + icon: "standard", + type: "icon", + shape: "circle", + theme: "outline", + text: "signin_with", + size: "large", + } // customization attributes + ); + // google.accounts.id.prompt(); // also display the One Tap dialog + } catch { + // + } + }); }; - - -// 动态加载Google js sdk 的 +// 动态加载 Google js sdk 的 export const loadScript = (url, callback) => { - const script = document.createElement('script'); - script.type = 'text/javascript'; - // script.async = 'async'; - script.async = true; - script.src = url; - document.body.appendChild(script); - if (script.readyState) { - // IE - script.onreadystatechange = () => { - if (script.readyState === 'complete' || script.readyState === 'loaded') { - script.onreadystatechange = null; - callback(); - } - }; - } else { - // 非IE - script.onload = () => { - callback(); - }; - } + const script = document.createElement("script"); + script.type = "text/javascript"; + // script.async = 'async'; + script.async = true; + script.src = url; + document.body.appendChild(script); + if (script.readyState) { + // IE + script.onreadystatechange = () => { + if (script.readyState === "complete" || script.readyState === "loaded") { + script.onreadystatechange = null; + callback(); + } + }; + } else { + // 非 IE + script.onload = () => { + callback(); + }; + } }; - // 登录成功桥接 app 的方法 -const loginSuccess = resp => { - console.log(' 登录成功桥接 app 的方法', resp); - const { token } = resp || {}; - if (token && window?.uc) { - window?.uc.loginSuccess(JSON.stringify(resp)); - } +const loginSuccess = (resp) => { + console.log(" 登录成功桥接 app 的方法", resp); + const { token } = resp || {}; + if (token && window?.uc) { + window?.uc.loginSuccess(JSON.stringify(resp)); + } }; - - // 下面交给 APP 桥接 loginSuccess 处理了 /* const { userInfo, refreshToken, token, tokenExpiryDate } = res; @@ -411,7 +453,9 @@ if (refreshToken) { window.localStorage.setItem('initApp', JSON.stringify(obj)); } */ ``` + jwt.decode(response.credential) 解析后的 + ```javascript { "iss": "https://accounts.google.com", @@ -431,9 +475,11 @@ jwt.decode(response.credential) 解析后的 "jti": "cc011e8dc96a4f73ba799a97f72399a3188af3c" } ``` + ## 集成 Facebook OAuth2 平台注册,获取应用设置 + ```javascript https://developers.facebook.com/apps/xx2992667/settings/basic/ 账号 @@ -441,7 +487,9 @@ https://developers.facebook.com/apps/xx2992667/settings/basic/ 口令: b1ce44exxxbca231 ``` -#### Web 代码实现1(这种属于简化模式) + +#### Web 代码实现 1(这种属于简化模式) + ```javascript @@ -465,7 +513,7 @@ FB.login(function(response) { } }); -// 也可以调用 FB.getLoginStatus检查登录状态 +// 也可以调用 FB.getLoginStatus 检查登录状态 FB.getLoginStatus(function(response) { if (response.status === 'connected') { @@ -501,7 +549,9 @@ FB.getLoginStatus(function(response) { ``` + authResponse + ```javascript { "authResponse": { @@ -516,10 +566,15 @@ authResponse } ``` -#### Web 代码实现2(这种属于 授权码模式) + +#### Web 代码实现 2(这种属于 授权码模式) + 在不使用 SDK 的情况下为网页或桌面应用实施基于浏览器的登录,可以使用浏览器重定向来构建自己的登录流程。 + [https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#exchangecode](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#exchangecode) + 当用户点击 Log in with Facebook 时,发出如下 HTTP POST 请求: + ``` // diglog 模式 https://www.facebook.com/v18.0/dialog/oauth? @@ -541,31 +596,32 @@ error_reason=user_denied &error_description=Permissions+error. ``` + 注意:redirect_uri 值 在应用面板中确认是否已为您的应用设置该网址。在应用面板左侧导航菜单的**产品**下点击 **Facebook 登录**,然后点击**设置**。在**客户端 OAuth 设置**部分验证**有效的 OAuth 重定向 URI。** -#### 登录支持 OpenID Connect (OIDC) 标准的授权代码流程和代码交换证明密钥 (PKCE)这种属于 授权码模式 +#### 登录支持 OpenID Connect(OIDC)标准的授权代码流程和代码交换证明密钥(PKCE)这种属于 授权码模式 + [**https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token**](https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token) + ```markdown https://www.facebook.com/v11.0/dialog/oauth? - client_id={app-id} // Replace with your application’s ID - &scope=openid - &response_type=code - &redirect_uri={"https://www.domain.com/login"} // Replace with your Redirect URI - &state={"state123abc"} // Replace with your State param - &code_challenge={"E91k-123k123-115X"} // Replace with your generated code_challenge - &code_challenge_method=S256 // Replace with the method used to generate the code_challenge - $nonce={"123"} // Replace with a randomly generated nonce value - - -// 返回值,拿着code 去这个接口换口令令牌 /oauth/access_token +client_id={app-id} // Replace with your application’s ID +&scope=openid +&response_type=code +&redirect_uri={"https://www.domain.com/login"} // Replace with your Redirect URI +&state={"state123abc"} // Replace with your State param +&code_challenge={"E91k-123k123-115X"} // Replace with your generated code_challenge +&code_challenge_method=S256 // Replace with the method used to generate the code_challenge +$nonce={"123"} // Replace with a randomly generated nonce value + +// 返回值,拿着 code 去这个接口换口令令牌 /oauth/access_token https://www.domain.com/login?state=state123abc&code={authorization-code} - - ``` ##### 使用短期口令交换长期口令 默认的用户和主页访问口令为短期口令,会在数小时后过期,可以使用短期口令交换长期口令。请向以下 OAuth 端点发出 HTTP GET 请求: + ``` GET https://graph.facebook.com/v18.0/oauth/access_token? @@ -581,17 +637,17 @@ GET } ``` - **另外还有以下可选参数:** -- response_type。确定重定向回应用时所包含的响应数据是网址参数形式还是网址片段形式。请参阅验证身份部分,选择应用应使用的参数类型。这些参数的类型可为以下其中一种: - - code。所包含的响应数据为网址参数形式,且包含 code 参数(每个登录请求独有的加密字符串)。如果未指定此参数,则此为默认行为。当服务器处理口令时,此行为尤为实用。 - - token。所包含的响应数据为网址片段形式,且包含访问口令。桌面应用必须为 response_type 使用此设置。当客户端处理口令时,此行为尤为实用。 - - code%20token。所包含的响应数据为网址片段形式,且包含访问口令和 code 参数。 - - granted_scopes。返回逗号分隔列表,其中包含用户在登录时授予应用的所有权限。可与其他 response_type 值合并。与 token 合并时,所包含的响应数据为网址片段形式;与其他值合并时,所包含的响应数据则为网址参数形式。 -- scope。逗号或空格分隔列表,其中包含要向应用用户请求的权限。 +- response_type。确定重定向回应用时所包含的响应数据是网址参数形式还是网址片段形式。请参阅验证身份部分,选择应用应使用的参数类型。这些参数的类型可为以下其中一种: + - code。所包含的响应数据为网址参数形式,且包含 code 参数(每个登录请求独有的加密字符串)。如果未指定此参数,则此为默认行为。当服务器处理口令时,此行为尤为实用。 + - token。所包含的响应数据为网址片段形式,且包含访问口令。桌面应用必须为 response_type 使用此设置。当客户端处理口令时,此行为尤为实用。 + - code%20token。所包含的响应数据为网址片段形式,且包含访问口令和 code 参数。 + - granted_scopes。返回逗号分隔列表,其中包含用户在登录时授予应用的所有权限。可与其他 response_type 值合并。与 token 合并时,所包含的响应数据为网址片段形式;与其他值合并时,所包含的响应数据则为网址参数形式。 +- scope。逗号或空格分隔列表,其中包含要向应用用户请求的权限。 + +#### 应用已有登录系统,结合 Facebook 登录 -#### 应用已有登录系统,结合Facebook登录 可能需要处理更复杂的情况 用户使用他们的电子邮箱和密码注册应用,但之后又想使用 Facebook 登录获取 Facebook 帐户的数据,以便向时间线发帖或用于在今后登录您的应用。 用户使用他们的电子邮箱和密码注册应用,但之后又单独选择通过 Facebook 登录。本指南假定用户最初提供的邮箱就是与用户的 Facebook 帐户关联的首选邮箱。 diff --git "a/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.md" "b/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.md" index bd67b21..38c9101 100644 --- "a/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.md" +++ "b/content/blog/Web\345\272\224\347\224\250\346\216\245\345\205\245OAuth2.md" @@ -1,108 +1,129 @@ --- -title: Web应用接入OAuth2 +title: Web 应用接入 OAuth2 date: 2023-12-27 19:41:20 tags: ["Web", "OAuth2", "Google", "Apple", "Facebook"] series: ["前端开发"] -category: ["blog","前端"] +category: ["blog", "前端"] featured: true - --- +接入 Google、Apple 和 Facebook 三方授权登录,好几年前其实就做过这类需求了,刚好最近有一个项目又要做,发现一些交互细节的修改,文档藏的比较深,所以整理汇总了下文档资料,方便后面给团队同事参考用。 -好几年前其实就做过这类需求了,刚好最近有一个项目又要接入 Google\Apple\Facebook 三方授权登录的需求,做的时候发现交互细节的修改,文档藏的比较深,所以整理汇总了下文档资料,方便后面给团队同事参考用。 -简单介绍下,OAuth(Open Authorization) 是一个基于令牌的授权协议。即 “Sign In with xxx”。主流平台都有提供的这项认证服务,可以通过第三方认证提供商进行身份验证和授权登录我们的应用,企业级应用一般都会上这个,方便资源拥有者(RO)使用在资源服务器(RS)的资源信息。更多了解参考下方链接。 +简单介绍下,OAuth(Open Authorization)是一个基于令牌的授权协议。即“Sign In with xxx”。主流平台都有提供的这项认证服务,可以通过第三方认证提供商进行身份验证和授权登录我们的应用,企业级应用一般都会上这个,方便资源拥有者(RO)使用在资源服务器(RS)的资源信息。更多了解参考下方链接。 ## 官方文档 + 介绍 OAuth2 标准: -[理解OAuth 2.0 - 阮一峰的网络日志](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) -Sign In with Apple: + +[理解 OAuth 2.0 - 阮一峰的网络日志](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) + +Sign In with Apple: + [https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) + [https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) -Sign In with Google: + +Sign In with Google: + [https://developers.google.cn/identity/protocols/oauth2/javascript-implicit-flow?hl=zh-cn](https://developers.google.cn/identity/protocols/oauth2/javascript-implicit-flow?hl=zh-cn) -Sign In with Facebook: + +Sign In with Facebook: + [https://developers.facebook.com/docs/facebook-login/](https://developers.facebook.com/docs/facebook-login/) + JS SDK [https://developers.facebook.com/docs/javascript/reference/v18.0](https://developers.facebook.com/docs/javascript/reference/v18.0) -手动构建登录流程,不走SDK [https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog) + +手动构建登录流程,不走 SDK [https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#logindialog) + [使用 Facebook 登录功能和现有应用登录系统集成](https://developers.facebook.com/docs/facebook-login/guides/advanced/existing-system) + [https://developers.facebook.com/docs/facebook-login/guides/access-tokens](https://developers.facebook.com/docs/facebook-login/guides/access-tokens) 获取长期口令 + ## OAuth 工作流程 + #### 角色: -- 业务系统(我们的应用 Application) - - 前端(客户端 client ) - - 后台(客户端 API) -- 用户(Resource Owner) -- 认证服务(Authorization server,Google、Apple、微信、QQ等第三方授权服务器) -- 资源服务器 (Resource server) +- 业务系统(我们的应用 Application) + - 前端(客户端 client) + - 后台(客户端 API) +- 用户(Resource Owner) +- 认证服务(Authorization server,Google、Apple、微信、QQ 等第三方授权服务器) +- 资源服务器 (Resource server) + #### 流程: - - 客户端点击登录,此时浏览器导向授权服务器认证页面,携带客户端标识和重定向URI。客户端向授权服务器请求授权, - - 授权服务器验证参数,然后用户未登录需先登录,并选择是否给该客户端授权。 - - 用户同意授权,授权服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个code授权码(可能是get,也可能是post方式)。 - - 前端客户端收到授权码后,会通知客户端后台API,通过 POST 请求,附上上次的一样的"重定向URI",向授权服务器申请令牌(access_token)。这一步是在客户端的后端API隐藏式完成的,对前端侧的用户不可见。 - - 授权服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)或更新令牌(refresh token)。 - - 客户端收到token后,传回给前端view并更新页面,用户登录成功。 - - 以上登录流程已经完成,需要补充的是:此时客户端可以使用访问令牌向资源服务器请求资源了(具体要看申请了那些权限,如果只是email 和name,就没啥用)。 +- 客户端点击登录,此时浏览器导向授权服务器认证页面,携带客户端标识和重定向 URI。客户端向授权服务器请求授权, +- 授权服务器验证参数,然后用户未登录需先登录,并选择是否给该客户端授权。 +- 用户同意授权,授权服务器将用户导向客户端事先指定的"重定向 URI"(redirection URI),同时附上一个 code 授权码(可能是 get,也可能是 post 方式)。 +- 前端客户端收到授权码后,会通知客户端后台 API,通过 POST 请求,附上上次的一样的"重定向 URI",向授权服务器申请令牌(access_token)。这一步是在客户端的后端 API 隐藏式完成的,对前端侧的用户不可见。 +- 授权服务器核对了授权码和重定向 URI,确认无误后,向客户端发送访问令牌(access token)或更新令牌(refresh token)。 +- 客户端收到 token 后,传回给前端 view 并更新页面,用户登录成功。 +- 以上登录流程已经完成,需要补充的是:此时客户端可以使用访问令牌向资源服务器请求资源了(具体要看申请了那些权限,如果只是 email 和 name,就没啥用)。 + #### 几种模式: + **授权码模式(是最复杂的,也是最安全的,上面流程就是这种,推荐使用)** - - 客户端请求验证,由 用户获取 code - - 客户端后端拿着 code,去请求 token - - 销毁 code,下发 token,而用户拿不到 token,客户端保存 - - 客户端使用 token 访问资源服务器的资源 - - 过期后使用 refresh_token 刷新, token 再次使用 +- 客户端请求验证,由 用户获取 code +- 客户端后端拿着 code,去请求 token +- 销毁 code,下发 token,而用户拿不到 token,客户端保存 +- 客户端使用 token 访问资源服务器的资源 +- 过期后使用 refresh_token 刷新,token 再次使用 **简化模式(**为 web 浏览器应用设计,不支持 refresh token**)** - - 用户请求网站,如:http://www.baidu.com - - 重定向到一个授权页面 - - 用户登录,并同意授权 - - 重定向到网站,并带上access_token如:www.baidu.com?access_token=123 - - 访问 资源服务器的资源 - +- 用户请求网站,如:http://www.baidu.com +- 重定向到一个授权页面 +- 用户登录,并同意授权 +- 重定向到网站,并带上 access_token 如:www.baidu.com?access_token=123 +- 访问 资源服务器的资源 #### 以 Gitee 网站为例,使用 GitHub 账号登录(授权码模式) + 1、点击登录,触发到 GitHub 的认证页面 2、允许授权,302 重定向到 Gitee 的 redirect_uri 页面 -3、页面 接受 code 参数后,请求后端,由后端再去获取GitHub的 accessToken,返回给前端 -4、再次 302 重定向到 Gitee 首页,并携带Cookie,完成用户登录成功状态。 +3、页面 接受 code 参数后,请求后端,由后端再去获取 GitHub 的 accessToken,返回给前端 +4、再次 302 重定向到 Gitee 首页,并携带 Cookie,完成用户登录成功状态。 ```html https://github.com/login/oauth/authorize?client_id=5a179b878a9f6ac42acd& -redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fgithub%2Fcallback& -response_type=code&scope=user& +redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fgithub%2Fcallback& response_type=code&scope=user& state=bc9c9fb74d0ad5745f891bc370b9de1cafb46e2a417793fa - ``` + 认证后,重定向 -![认证后重定向](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756156517-55c266f1-c8aa-4601-b2b2-b7035a7d6ba0.png#averageHue=%23363232&clientId=u1e30360e-97ca-4&from=paste&height=792&id=u9dd11888&originHeight=1188&originWidth=1624&originalType=binary&ratio=1.5&rotation=0&showTitle=true&size=224513&status=done&style=none&taskId=u01a3e0ad-3f6a-4667-9a5c-9e7afec5594&title=%E8%AE%A4%E8%AF%81%E5%90%8E%E9%87%8D%E5%AE%9A%E5%90%91&width=1082.6666666666667 "认证后重定向") +![认证后重定向](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756156517-55c266f1-c8aa-4601-b2b2-b7035a7d6ba0.png "认证后重定向") + ```html https://gitee.com/auth/github/callback?code=9c637527cce64b8a5736&state=bc9c9fb74d0ad5745f891bc370b9de1cafb46e2a417793fa ``` -获取GitHub的 accessToken -![获取GitHub的 accessToken](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756251104-6d3e2129-76fe-4619-9f69-1f901b1b69a6.png#averageHue=%23313131&clientId=u1e30360e-97ca-4&from=paste&height=778&id=ud62a00c9&originHeight=1167&originWidth=1604&originalType=binary&ratio=1.5&rotation=0&showTitle=true&size=216271&status=done&style=none&taskId=u2d21ba42-9456-4d96-ae08-1ec62c72f71&title=%E8%8E%B7%E5%8F%96GitHub%E7%9A%84%20accessToken&width=1069.3333333333333 "获取GitHub的 accessToken") + +获取 GitHub 的 accessToken +![获取 GitHub 的 accessToken](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756251104-6d3e2129-76fe-4619-9f69-1f901b1b69a6.png "获取GitHub的 accessToken") 重定向到首页 -![重定向到首页](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756570890-fcfa1b8c-2b94-4258-8d06-f60101329f5a.png#averageHue=%23d9ae57&clientId=u1e30360e-97ca-4&from=paste&height=800&id=u16650903&originHeight=1200&originWidth=1983&originalType=binary&ratio=1.5&rotation=0&showTitle=true&size=413240&status=done&style=none&taskId=u10797c3f-644f-49b9-b150-61011a0f15a&title=%E9%87%8D%E5%AE%9A%E5%90%91%E5%88%B0%E9%A6%96%E9%A1%B5&width=1322 "重定向到首页") -流程完成,下面分别介绍三种,我们应用用上的OAuth场景。 +![重定向到首页](https://cdn.nlark.com/yuque/0/2023/png/203859/1703756570890-fcfa1b8c-2b94-4258-8d06-f60101329f5a.png "重定向到首页") +流程完成,下面分别介绍三种,我们应用用上的 OAuth 场景。 + ## 集成 Apple 授权登录 (Sign in with Apple) + 苹果官方文档: -[https://developer.apple.com/documentation/sign_in_with_apple](https://developer.apple.com/documentation/sign_in_with_apple) (前端js调用,发起授权) -[https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api)( REST API ,后端同学看的) +[https://developer.apple.com/documentation/sign_in_with_apple](https://developer.apple.com/documentation/sign_in_with_apple) (前端 js 调用,发起授权) +[https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api)(REST API,后端同学看的) 第三方文档参考: [https://zhuanlan.zhihu.com/p/632483498](https://zhuanlan.zhihu.com/p/632483498) [Sign in with Apple NODE,苹果第三方登录](https://segmentfault.com/a/1190000020786994#item-4-5) [What the Heck is Sign In with Apple?](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple) -Sign in with Apple Tutorial: +Sign in with Apple Tutorial: 1. [Sign in with Apple, Part 1: Apps](https://sarunw.com/posts/sign-in-with-apple-1) 2. [Sign in with Apple, Part 2: Private Email Relay Service](https://sarunw.com/posts/sign-in-with-apple-2) 3. [Sign in with Apple, Part 3: Backend – Token verification](https://sarunw.com/posts/sign-in-with-apple-3) 4. [Sign in with Apple, Part 4: Web and Other Platforms](https://sarunw.com/posts/sign-in-with-apple-4) -**授权码模式(和 Google 不同的是,redirect-url 回传的code需要有一个post接口来接收):** -```html +**授权码模式(和 Google 不同的是,redirect-url 回传的 code 需要有一个 post 接口来接收):** + +``` app client 向 app server 请求 oauth url app client 收到 url ,点击事件触发访问 oauth url, 跳转到 apple id server @@ -114,29 +135,37 @@ app server 验证 authorization code 和 id_token 是在同一个请求下发的 app server 成功返回用户信息给 app client ``` -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703773909091-5843f905-22b0-4f51-a075-5ea98eb6ec04.png#averageHue=%23f3f3f3&clientId=u7f2dfee7-d8e6-4&from=paste&height=491&id=ucac3f60c&originHeight=491&originWidth=628&originalType=binary&ratio=1&rotation=0&showTitle=false&size=93721&status=done&style=none&taskId=u5ac69480-0a9e-4bc9-84cc-fc73fd1334b&title=&width=628) -#### 注册&配置 OAuth2应用 -参考官方文档即可。主要是为了 获取客户端ID和客户端密钥:注册成功后,OAuth2提供商将为您的应用程序分配一个客户端ID和客户端密钥。还有重定向 redirectURI,这些凭据将在后续的OAuth2交互中使用。 -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703754389135-2963205e-d366-44e6-931f-af6cc149a44f.png#averageHue=%23dedddd&clientId=u1e30360e-97ca-4&from=paste&height=475&id=u6d670983&originHeight=712&originWidth=804&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=180368&status=done&style=none&taskId=u0d1dd340-85fd-4b82-8fbc-0410946e589&title=&width=536) -当配置结束后,我们将获得我们所需的两个文件、三个ID、和一个URL连接,如下: -```jsx -redirectURI = 'https://xx.xxx.online/login/oauth-url' // 自己设置的重定向域名,可添加多个 -webClientId = 'com.xx.cn'; // 设置的client_id,一般是域名的反写 -teamId = 'xx'; // 10个字符的team_id -keyId = 'KOI23S78J6'; // 获取的10个字符的密钥标识符 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703773909091-5843f905-22b0-4f51-a075-5ea98eb6ec04.png) + +#### 注册&配置 OAuth2 应用 +参考官方文档即可。主要是为了 获取客户端 ID 和客户端密钥:注册成功后,OAuth2 提供商将为您的应用程序分配一个客户端 ID 和客户端密钥。还有重定向 redirectURI,这些凭据将在后续的 OAuth2 交互中使用。 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703754389135-2963205e-d366-44e6-931f-af6cc149a44f.png) + +当配置结束后,我们将获得我们所需的两个文件、三个 ID、和一个 URL 连接,如下: + +```jsx +redirectURI = "https://xx.xxx.online/login/oauth-url"; // 自己设置的重定向域名,可添加多个 +webClientId = "com.xx.cn"; // 设置的 client_id,一般是域名的反写 +teamId = "xx"; // 10 个字符的 team_id +keyId = "KOI23S78J6"; // 获取的 10 个字符的密钥标识符 ``` 设置登录徽标样式 + [https://appleid.apple.com/signinwithapple/button](https://appleid.apple.com/signinwithapple/button) -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775721626-23734410-a70c-4d7c-99c9-8aa6a88088bb.png#averageHue=%23fafafa&clientId=u7f2dfee7-d8e6-4&from=paste&height=483&id=u81212d96&originHeight=483&originWidth=571&originalType=binary&ratio=1&rotation=0&showTitle=false&size=44215&status=done&style=none&taskId=ud456e44c-8fc3-41c0-8cae-bb45a1a9d2c&title=&width=571) -#### Web 代码实现1( 这种是 OAuth 2.0 简化模式) -前端 SDK触发 + +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775721626-23734410-a70c-4d7c-99c9-8aa6a88088bb.png) + +#### Web 代码实现 1(这种是 OAuth 2.0 简化模式) + +前端 SDK 触发 + ```jsx -// 1\页面注入js sdk +// 1\页面注入 js sdk @@ -148,7 +177,7 @@ useEffect(() => { usePopup: true, redirectURI: redirectUrl, state: 'init00state', - responseType: 'code', // 有效值为code和id_token。 + responseType: 'code', // 有效值为 code 和 id_token。 }); }, [APPLEKey]); @@ -165,7 +194,7 @@ const data = await AppleID?.auth?.signIn({ ); console.log('APPLE signIn 的回调 datadatadata:', data); -// 4\处理响应、与自己的登录接口做交互,返回用户info,完成登录。 +// 4\处理响应、与自己的登录接口做交互,返回用户 info,完成登录。 if (data?.authorization?.code) { const authTokenLocal = jwt.decode(data?.authorization?.id_token) || {}; const oauthOpenId = get(authTokenLocal, 'sub'); @@ -177,7 +206,8 @@ if (data?.authorization?.code) { } ``` -APPLE 登录成功的回调 data, 拿着 id_token 和自己的 后端api 做绑定或登录,即完成业务。 +APPLE 登录成功的回调 data, 拿着 id_token 和自己的 后端 api 做绑定或登录,即完成业务。 + ```json { "authorization": { @@ -203,8 +233,11 @@ APPLE 登录成功的回调 data, 拿着 id_token 和自己的 后端api 做绑 } } ``` -#### Web 代码实现2( 这种是 授权模式) + +#### Web 代码实现 2(这种是 授权模式) + 手工触发 (授权模式) + ```jsx https://appleid.apple.com/auth/authorize?client_id=[CLIENT_ID]&redirect_uri=[REDIRECT_URI]&response_type=[RESPONSE_TYPE]&scope=[SCOPES]&response_mode=[RESPONSE_MODE]&state=[STATE] @@ -217,184 +250,193 @@ https://appleid.apple.com/auth/oauth2/v2/authorize ``` #### 登录流程 -用户点击 Apple 登录图标,会跳转到 Apple 登录网站,输入账号密码。首次登录会在任一 Apple设备弹出原生登录授权验证(双重验证),输入6位随机验证码 即可登录完成。 -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775181457-dc133d2b-a274-41e6-9e65-c841eda23224.png#averageHue=%23f1f1f1&clientId=u7f2dfee7-d8e6-4&from=paste&height=340&id=ue35bc92c&originHeight=340&originWidth=557&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23752&status=done&style=none&taskId=u0ab9e70f-cbf5-48b5-8c78-6ca6a1fced7&title=&width=557) -注意:首次登录会选择是否隐藏邮箱,选择隐藏将会使用apple提供的一个匿名邮箱而不是真实邮箱号。当选择信任浏览器后,之后在此浏览器中登录只需要输入账号、密码即可。 -![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775215350-7bac5760-3198-46d4-8b7b-4eb9c1ef3bb1.png#averageHue=%23fcfcfc&clientId=u7f2dfee7-d8e6-4&from=paste&height=354&id=uedd78471&originHeight=354&originWidth=517&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30409&status=done&style=none&taskId=u6c046b5b-6f26-42bc-9a6a-c4211f0b0b5&title=&width=517) -在登录后用户可以随时在apple设备上取消apple id在该程序上的授权登录。mac上safari浏览器上可直接验证登录。也可以通过手机号等其他方式进行验证,apple设备如果开启双重认证,会使用双重验证,简单说就是当你首次使用Apple登录一个设备时,在输入Apple id和密码之后,还需要在其他已登录的Apple设备上确认授权,并输入已登录设备上提供的验证码进行验证。 + +用户点击 Apple 登录图标,会跳转到 Apple 登录网站,输入账号密码。首次登录会在任一 Apple 设备弹出原生登录授权验证 (双重验证),输入 6 位随机验证码 即可登录完成。 + +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775181457-dc133d2b-a274-41e6-9e65-c841eda23224.png#taskId=u0ab9e70f-cbf5-48b5-8c78-6ca6a1fced7&title=&width=557) + +注意:首次登录会选择是否隐藏邮箱,选择隐藏将会使用 apple 提供的一个匿名邮箱而不是真实邮箱号。当选择信任浏览器后,之后在此浏览器中登录只需要输入账号、密码即可。 + +![image.png](https://cdn.nlark.com/yuque/0/2023/png/203859/1703775215350-7bac5760-3198-46d4-8b7b-4eb9c1ef3bb1.png) + +在登录后用户可以随时在 apple 设备上取消 apple id 在该程序上的授权登录。mac 上 safari 浏览器上可直接验证登录。也可以通过手机号等其他方式进行验证,apple 设备如果开启双重认证,会使用双重验证,简单说就是当你首次使用 Apple 登录一个设备时,在输入 Apple id 和密码之后,还需要在其他已登录的 Apple 设备上确认授权,并输入已登录设备上提供的验证码进行验证。 + #### 处理授权响应(分两种模式) + 用户单击“使用 Apple 登录”按钮后,框架会将授权信息发送给 Apple。Apple 处理授权请求后: -如果是 usePopup 模式,是直接返回 data【简化模式】; -否则会将包含授权结果的 HTTP POST 请求发送到 中提供的 URL redirectURI【授权码模式】 。 -HTTP 正文包含 content-type 为 application/x-www-form-urlencoded 的结果参数。成功的响应包含以下参数: + 如果是 usePopup 模式,是直接返回 data【简化模式】; + 否则会将包含授权结果的 HTTP POST 请求发送到 中提供的 URL redirectURI【授权码模式】 。 + +HTTP 正文包含 content-type 为 application/x-www-form-urlencoded 的结果参数。成功的响应包含以下参数: ## 集成 Google OAuth2 #### 需要注意 -##### 一、 google 的 gsi/client 和 oauth2 的区别 + +##### 一、google 的 gsi/client 和 oauth2 的区别 + > Google Identity Services (GIS) 的 gsi/client 是用于实现用户登录的 JavaScript 库。它可以帮助开发者在网页中集成“使用 Google 账号登录”的功能。当用户点击登录按钮后,gsi/client 会弹出一个 Google 登录界面,用户可以选择使用他们的 Google 账号进行登录。这样,开发者就可以通过 gsi/client 获取用户的身份信息,比如用户的邮箱地址等。 -> OAuth2 则是通过授权code,来获取访问令牌,然后使用该访问令牌来调用 Google API 或其他受保护的资源。 +> OAuth2 则是通过授权 code,来获取访问令牌,然后使用该访问令牌来调用 Google API 或其他受保护的资源。 综上: -gsi/client 用于前端直接新开一个弹窗,实现帮用户用Google实现登录网页的功能。【简化模式】 -OAuth2 则是先获取访问令牌后,Google重定向页面 URL 得到后授权码code,拿这个授权码去接口去授权,适合在不能弹窗的场景下使用。【授权码模式】 +gsi/client 用于前端直接新开一个弹窗,实现帮用户用 Google 实现登录网页的功能。【简化模式】 +OAuth2 则是先获取访问令牌后,Google 重定向页面 URL 得到后授权码 code,拿这个授权码去接口去授权,适合在不能弹窗的场景下使用。【授权码模式】 + ##### 二、不允许使用的嵌入式用户代理中 -Google的授权页面,在 Google 的 OAuth 2.0 政策中 disallowed_useragent 要求不能被webview嵌套。解决办法在下面: + +Google 的授权页面,在 Google 的 OAuth 2.0 政策中 disallowed_useragent 要求不能被 webview 嵌套。解决办法在下面: + Android 开发者在 android.webkit.WebView 中打开授权请求时可能会遇到此错误消息。开发者应该改用 Android 库,例如适用于 Android 的 Google 登录或 OpenID 基金会的 AppAuth for Android。当 Android 应用通过嵌入式用户代理打开常规 Web 链接,且用户从您的网站转到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许在操作系统的默认链接处理程序(包括 Android App Links 处理程序或默认浏览器应用)中打开常规链接。Android 自定义标签页库也是一个受支持的选项。 + iOS 和 macOS 开发者在 WKWebView 中打开授权请求时可能会遇到此错误。开发者应该改用 iOS 库,例如适用于 iOS 的 Google 登录或 OpenID 基金会的 AppAuth for iOS。当 iOS 或 macOS 应用在嵌入式用户代理中打开常规 Web 链接,且用户从您的网站转到 Google 的 OAuth 2.0 授权端点时,Web 开发者可能会遇到此错误。开发者应允许操作系统的默认链接处理程序(包括通用链接处理程序或默认浏览器应用)中打开常规链接。SFSafariViewController 库也是一个受支持的选项。 -#### Web 代码实现1(这种属于 授权码模式) +#### Web 代码实现 1(这种属于 授权码模式) + 授权码模式是最复杂的,也是最安全的 - - 客户端发起请求验证,由用户获取 code - - 客户端拿到 code,请求 token - - 销毁 code,下发 token,而用户拿不到 token,客户端保存 - - 客户端使用 token 访问资源 - - 过期后使用 refresh_token 刷新, token 再次使用 +- 客户端发起请求验证,由用户获取 code +- 客户端拿到 code,请求 token +- 销毁 code,下发 token,而用户拿不到 token,客户端保存 +- 客户端使用 token 访问资源 +- 过期后使用 refresh_token 刷新,token 再次使用 + +Create form to request - Create form to request ```jsx /* - * Create form to request access token from Google's OAuth 2.0 server. - */ + * Create form to request access token from Google's OAuth 2.0 server. + */ const oauthSignIn = (key) => { - // Google's OAuth 2.0 endpoint for requesting an access token - const oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth'; - - // Create element to submit parameters to OAuth 2.0 endpoint. - const googleForm = document.createElement('form'); - googleForm.setAttribute('method', 'GET'); // Send as a GET request. - googleForm.setAttribute('action', oauth2Endpoint); - - // Parameters to pass to OAuth 2.0 endpoint. - const params = { - client_id:clientId, - redirect_uri: redirectUrl, - response_type: 'code', - scope: 'https://www.googleapis.com/auth/userinfo.profile', - include_granted_scopes: "true", - state: OAUTH_LOGIN_PLATFORM_TYPE.GOOGLE, - }; - - // Add form parameters as hidden input values. - // eslint-disable-next-line no-restricted-syntax - for (const p in params) { - const input = document.createElement("input"); - input.setAttribute("type", "hidden"); - input.setAttribute('name', p); - input.setAttribute('value', params[p]); - googleForm.appendChild(input); - } + // Google's OAuth 2.0 endpoint for requesting an access token + const oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + + // Create element to submit parameters to OAuth 2.0 endpoint. + const googleForm = document.createElement("form"); + googleForm.setAttribute("method", "GET"); // Send as a GET request. + googleForm.setAttribute("action", oauth2Endpoint); + + // Parameters to pass to OAuth 2.0 endpoint. + const params = { + client_id: clientId, + redirect_uri: redirectUrl, + response_type: "code", + scope: "https://www.googleapis.com/auth/userinfo.profile", + include_granted_scopes: "true", + state: OAUTH_LOGIN_PLATFORM_TYPE.GOOGLE, + }; - // Add form to page and submit it to open the OAuth 2.0 endpoint. - document.body.appendChild(googleForm); - googleForm.submit(); -}; + // Add form parameters as hidden input values. + // eslint-disable-next-line no-restricted-syntax + for (const p in params) { + const input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", p); + input.setAttribute("value", params[p]); + googleForm.appendChild(input); + } + // Add form to page and submit it to open the OAuth 2.0 endpoint. + document.body.appendChild(googleForm); + googleForm.submit(); +}; ``` + OAuth 2.0 服务器响应 -```markdown -// 正确响应:code交给自己的服务器api,完成后续操作。 +```markdown +// 正确响应:code 交给自己的服务器 api,完成后续操作。 http://oauth2.example.com/callback?state=G&code=4%2F0AfJohXmMnygj-k1dcfv8k8bObKZOkeg&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none // 错误响应: https://oauth2.example.com/callback#error=access_denied - ``` -#### Web 代码实现2(这种属于简化模式) +#### Web 代码实现 2(这种属于简化模式) + ```javascript -// 传统 web登录 +// 传统 web 登录 useEffect(() => { - initGoogleScript(); + initGoogleScript(); }, []); - - // eslint-disable-next-line no-unused-vars const initGoogleScript = () => { - // const script = document.createElement('script'); - // script.src = 'https://accounts.google.com/gsi/client'; - // document.head.append(script); - function handleCredentialResponse(response) { - const authTokenLocal = jwt.decode(response.credential) || {}; - console.log("GOOGLE 回调 authTokenLocal", response.credential,authTokenLocal); - const oauthOpenId = get(authTokenLocal, 'sub'); - // const email = get(authTokenLocal, 'email'); - // 如果是走前端自己组织的URL,才和APP相同的处理函数 - if (oauthOpenId) { - loginSuccess(res); + // const script = document.createElement('script'); + // script.src = 'https://accounts.google.com/gsi/client'; + // document.head.append(script); + function handleCredentialResponse(response) { + const authTokenLocal = jwt.decode(response.credential) || {}; + console.log("GOOGLE 回调 authTokenLocal", response.credential, authTokenLocal); + const oauthOpenId = get(authTokenLocal, "sub"); + // const email = get(authTokenLocal, 'email'); + // 如果是走前端自己组织的 URL,才和 APP 相同的处理函数 + if (oauthOpenId) { + loginSuccess(res); + } } - } - loadScript('https://accounts.google.com/gsi/client', () => { - console.log('loadScript....'); - try { - // eslint-disable-next-line no-undef - google.accounts.id.initialize({ - client_id: '643984392818-v2aad7hxx3r65h29imbq4d.apps.googleusercontent.com', // GOOGLE_CLIENT_ID, - callback: handleCredentialResponse, - }); - - // eslint-disable-next-line no-undef - google.accounts.id.renderButton( - document.getElementById('btn-google-login'), - { - icon: 'standard', - type: 'icon', - shape: 'circle', - theme: 'outline', - text: 'signin_with', - size: 'large', - }, // customization attributes - ); - // google.accounts.id.prompt(); // also display the One Tap dialog - } catch { - // - } - }); + loadScript("https://accounts.google.com/gsi/client", () => { + console.log("loadScript...."); + try { + // eslint-disable-next-line no-undef + google.accounts.id.initialize({ + client_id: "643984392818-v2aad7hxx3r65h29imbq4d.apps.googleusercontent.com", // GOOGLE_CLIENT_ID, + callback: handleCredentialResponse, + }); + + // eslint-disable-next-line no-undef + google.accounts.id.renderButton( + document.getElementById("btn-google-login"), + { + icon: "standard", + type: "icon", + shape: "circle", + theme: "outline", + text: "signin_with", + size: "large", + } // customization attributes + ); + // google.accounts.id.prompt(); // also display the One Tap dialog + } catch { + // + } + }); }; - - -// 动态加载Google js sdk 的 +// 动态加载 Google js sdk 的 export const loadScript = (url, callback) => { - const script = document.createElement('script'); - script.type = 'text/javascript'; - // script.async = 'async'; - script.async = true; - script.src = url; - document.body.appendChild(script); - if (script.readyState) { - // IE - script.onreadystatechange = () => { - if (script.readyState === 'complete' || script.readyState === 'loaded') { - script.onreadystatechange = null; - callback(); - } - }; - } else { - // 非IE - script.onload = () => { - callback(); - }; - } + const script = document.createElement("script"); + script.type = "text/javascript"; + // script.async = 'async'; + script.async = true; + script.src = url; + document.body.appendChild(script); + if (script.readyState) { + // IE + script.onreadystatechange = () => { + if (script.readyState === "complete" || script.readyState === "loaded") { + script.onreadystatechange = null; + callback(); + } + }; + } else { + // 非 IE + script.onload = () => { + callback(); + }; + } }; - // 登录成功桥接 app 的方法 -const loginSuccess = resp => { - console.log(' 登录成功桥接 app 的方法', resp); - const { token } = resp || {}; - if (token && window?.uc) { - window?.uc.loginSuccess(JSON.stringify(resp)); - } +const loginSuccess = (resp) => { + console.log(" 登录成功桥接 app 的方法", resp); + const { token } = resp || {}; + if (token && window?.uc) { + window?.uc.loginSuccess(JSON.stringify(resp)); + } }; - - // 下面交给 APP 桥接 loginSuccess 处理了 /* const { userInfo, refreshToken, token, tokenExpiryDate } = res; @@ -411,7 +453,9 @@ if (refreshToken) { window.localStorage.setItem('initApp', JSON.stringify(obj)); } */ ``` + jwt.decode(response.credential) 解析后的 + ```javascript { "iss": "https://accounts.google.com", @@ -431,9 +475,11 @@ jwt.decode(response.credential) 解析后的 "jti": "cc011e8dc96a4f73ba799a97f72399a3188af3c" } ``` + ## 集成 Facebook OAuth2 平台注册,获取应用设置 + ```javascript https://developers.facebook.com/apps/xx2992667/settings/basic/ 账号 @@ -441,7 +487,9 @@ https://developers.facebook.com/apps/xx2992667/settings/basic/ 口令: b1ce44exxxbca231 ``` -#### Web 代码实现1(这种属于简化模式) + +#### Web 代码实现 1(这种属于简化模式) + ```javascript @@ -465,7 +513,7 @@ FB.login(function(response) { } }); -// 也可以调用 FB.getLoginStatus检查登录状态 +// 也可以调用 FB.getLoginStatus 检查登录状态 FB.getLoginStatus(function(response) { if (response.status === 'connected') { @@ -501,7 +549,9 @@ FB.getLoginStatus(function(response) { ``` + authResponse + ```javascript { "authResponse": { @@ -516,10 +566,15 @@ authResponse } ``` -#### Web 代码实现2(这种属于 授权码模式) + +#### Web 代码实现 2(这种属于 授权码模式) + 在不使用 SDK 的情况下为网页或桌面应用实施基于浏览器的登录,可以使用浏览器重定向来构建自己的登录流程。 + [https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#exchangecode](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow#exchangecode) + 当用户点击 Log in with Facebook 时,发出如下 HTTP POST 请求: + ``` // diglog 模式 https://www.facebook.com/v18.0/dialog/oauth? @@ -541,31 +596,32 @@ error_reason=user_denied &error_description=Permissions+error. ``` + 注意:redirect_uri 值 在应用面板中确认是否已为您的应用设置该网址。在应用面板左侧导航菜单的**产品**下点击 **Facebook 登录**,然后点击**设置**。在**客户端 OAuth 设置**部分验证**有效的 OAuth 重定向 URI。** -#### 登录支持 OpenID Connect (OIDC) 标准的授权代码流程和代码交换证明密钥 (PKCE)这种属于 授权码模式 +#### 登录支持 OpenID Connect(OIDC)标准的授权代码流程和代码交换证明密钥(PKCE)这种属于 授权码模式 + [**https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token**](https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token) + ```markdown https://www.facebook.com/v11.0/dialog/oauth? - client_id={app-id} // Replace with your application’s ID - &scope=openid - &response_type=code - &redirect_uri={"https://www.domain.com/login"} // Replace with your Redirect URI - &state={"state123abc"} // Replace with your State param - &code_challenge={"E91k-123k123-115X"} // Replace with your generated code_challenge - &code_challenge_method=S256 // Replace with the method used to generate the code_challenge - $nonce={"123"} // Replace with a randomly generated nonce value - - -// 返回值,拿着code 去这个接口换口令令牌 /oauth/access_token +client_id={app-id} // Replace with your application’s ID +&scope=openid +&response_type=code +&redirect_uri={"https://www.domain.com/login"} // Replace with your Redirect URI +&state={"state123abc"} // Replace with your State param +&code_challenge={"E91k-123k123-115X"} // Replace with your generated code_challenge +&code_challenge_method=S256 // Replace with the method used to generate the code_challenge +$nonce={"123"} // Replace with a randomly generated nonce value + +// 返回值,拿着 code 去这个接口换口令令牌 /oauth/access_token https://www.domain.com/login?state=state123abc&code={authorization-code} - - ``` ##### 使用短期口令交换长期口令 默认的用户和主页访问口令为短期口令,会在数小时后过期,可以使用短期口令交换长期口令。请向以下 OAuth 端点发出 HTTP GET 请求: + ``` GET https://graph.facebook.com/v18.0/oauth/access_token? @@ -581,17 +637,17 @@ GET } ``` - **另外还有以下可选参数:** -- response_type。确定重定向回应用时所包含的响应数据是网址参数形式还是网址片段形式。请参阅验证身份部分,选择应用应使用的参数类型。这些参数的类型可为以下其中一种: - - code。所包含的响应数据为网址参数形式,且包含 code 参数(每个登录请求独有的加密字符串)。如果未指定此参数,则此为默认行为。当服务器处理口令时,此行为尤为实用。 - - token。所包含的响应数据为网址片段形式,且包含访问口令。桌面应用必须为 response_type 使用此设置。当客户端处理口令时,此行为尤为实用。 - - code%20token。所包含的响应数据为网址片段形式,且包含访问口令和 code 参数。 - - granted_scopes。返回逗号分隔列表,其中包含用户在登录时授予应用的所有权限。可与其他 response_type 值合并。与 token 合并时,所包含的响应数据为网址片段形式;与其他值合并时,所包含的响应数据则为网址参数形式。 -- scope。逗号或空格分隔列表,其中包含要向应用用户请求的权限。 +- response_type。确定重定向回应用时所包含的响应数据是网址参数形式还是网址片段形式。请参阅验证身份部分,选择应用应使用的参数类型。这些参数的类型可为以下其中一种: + - code。所包含的响应数据为网址参数形式,且包含 code 参数(每个登录请求独有的加密字符串)。如果未指定此参数,则此为默认行为。当服务器处理口令时,此行为尤为实用。 + - token。所包含的响应数据为网址片段形式,且包含访问口令。桌面应用必须为 response_type 使用此设置。当客户端处理口令时,此行为尤为实用。 + - code%20token。所包含的响应数据为网址片段形式,且包含访问口令和 code 参数。 + - granted_scopes。返回逗号分隔列表,其中包含用户在登录时授予应用的所有权限。可与其他 response_type 值合并。与 token 合并时,所包含的响应数据为网址片段形式;与其他值合并时,所包含的响应数据则为网址参数形式。 +- scope。逗号或空格分隔列表,其中包含要向应用用户请求的权限。 + +#### 应用已有登录系统,结合 Facebook 登录 -#### 应用已有登录系统,结合Facebook登录 可能需要处理更复杂的情况 用户使用他们的电子邮箱和密码注册应用,但之后又想使用 Facebook 登录获取 Facebook 帐户的数据,以便向时间线发帖或用于在今后登录您的应用。 用户使用他们的电子邮箱和密码注册应用,但之后又单独选择通过 Facebook 登录。本指南假定用户最初提供的邮箱就是与用户的 Facebook 帐户关联的首选邮箱。 diff --git "a/content/blog/\346\210\221\347\232\204\345\211\215\347\253\257\347\274\226\347\240\201\350\247\204\350\214\203.en.md" "b/content/blog/\346\210\221\347\232\204\345\211\215\347\253\257\347\274\226\347\240\201\350\247\204\350\214\203.en.md" new file mode 100644 index 0000000..1c7c7c7 --- /dev/null +++ "b/content/blog/\346\210\221\347\232\204\345\211\215\347\253\257\347\274\226\347\240\201\350\247\204\350\214\203.en.md" @@ -0,0 +1,199 @@ +--- +title: my coding standard +date: 2019-09-21 17:31:20 +tags: ["前端", "编码规范"] +series: ["前端开发"] +category: ["blog", "前端"] +featured: true +--- + +Record some daily coding standards for my development, such as naming conventions, code styles, submission standards, etc., update them periodically. + +## 命名规范 + +- 变量名,函数名 小驼峰【命名法 camel Case】: numberOfPeople 第一个单词的首字母小写;第二个单词开始每个单词的的首字母大写 + +- 组件名 大驼峰【命名法 Camel Case】: NumberOfPeople 每一个单词的首字母都大写 + +- css 样式名 中横线【命名法 kabab case】: number-of-people 单词小写用(-)中横线分隔 + +- 常量名,graphql query 与 mutation 变量名:蛇底式大写【命名法 upper snake case】: NUMBER*OF_PEOPLE 复合词或短语中的各个单词之间:下划线(*)分隔并且没有空格 + +- 禁用小写加下划线:number_of_people + +例: + +```javascript +handleButtonClick +onChange={this.handleCardChange} +handleSubmit +hideDialog +handleNextStep +onConfirm = handleConfirmClick +onCancel +handleDialogClose +handleClickNextButton +``` + +| 命名方式 | 应用范围 | 备注 | +| ---------------- | ---------------------------------------- | ----------------------------------- | +| camel Case | 变量名,函数名 | | +| Camel Case | 组件名,枚举名 | 枚举:SaveType = { BUILD: 'BUILD' } | +| kabab case | css 样式名 | | +| upper snake case | 常量名,graphql query 与 mutation 变量名 | | +| snake case | 禁止使用 | | + +#### 应用范围 + +| 结构 | 应用范围 | 备注 | +| ------------------------------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 动 + 宾 (+ 副词) | 函数名,graphql mutation 名称 | | +| 名词 (定语 + 名词) | 组件名,类名 | | +| 形容词 (名词 + 形容词)
动词被动形态
(be+xxx) | 状态变量 | 控制对话框是否显示:dialogVisible

尽可能不要用 is+ 形容词结构,如 isSelected, 用 selected 就可以了.

is+ 名词结构可以使用,如 isEnterprise | + +### 名词示例 + +**camel Case**: numberOfPeople
**Camel Case**: NumberOfPeople
**kabab case**: number-of-people
**snake case**: number_of_people
**upper snake case**: NUMBER_OF_PEOPLE + +## Merge Request Checklist + +- 检查是否和目标分支有冲突 + +- 检查是否修复所有 dn test 的问题 + +- 检查目录、文件名拼写 + +- 检查 graphql 文件名拼写,Query 首字母大写的 CamelCase,Mutation 首字母小写的 camelCase + +- 检查 conf 中的命名是否符合规范 + +## 标准的项目研发流程包括以下几个阶段: + +``` +* 评审阶段 + * 需求评审 + * 交互评审 + * 视觉评审 +* 开发阶段 + * 原型开发 + * 用户交互事件响应 + * 接入Mock数据 + * 后台接口数据对接 +* 联调阶段 + * 预发联调 + * 整个业务串联测试流程 +* 测试阶段 + * 埋点开发及验证 + * 自测用例覆盖 + * 交付提测 + * bug修复 +* 发布上线 +``` + +## 写作的基本准则(优化) + +``` +基本上写作的基本准则的每一部分都能应用在代码上: + ● 让段落成为文章的基本结构:每一段对应一个主题。 + ● 去掉无用的单词。 . + ● 使用主动语态。 + ● 避免一连串松散的句子。 + ● 将相关的词语放在一起。 + ● 陈述句用主动语态。 + ● 平行的概念用平行的结构。 + 这些对应的 用在前端的代码风格上 + 1. 让函数成为代码的基本单元。每个函数做一件事。 + 2. 去掉无用的代码 + 3. 使用主动语态 + 4. 避免一连串松散结构的代码逻辑 + 5. 把相关的变量、函数放在一起。 + 6. 表达式和陈述语句中使用主动语态。 + 7. 用并行的代码表达并行的概念。 +``` + +## Bug 分类 + +- fixed: 注明问题出现的原因 +- Won't fix:非问题,不需要修复的 bug,需置为“Won't fix” +- Later:遗留 bug,需要将 bug 置为“Later” +- Worksforme:log 无效,且无法再复现的 bug,需置为“Worksforme”,由测试同学继续跟踪复现 +- Duplicate:重复 bug 需要设置为“Duplicate”,并关联初始 bug,由测试同学跟踪初始 bug 解决状态 +- invaild:无效的 bug, +- By Design:逻辑设计如此,不需要修复的 bug,需置为“By Design”。 + +## Git 分支 + +存在三种短期分支
            功能分支(feature branch)
            补丁分支(hotfix branch)
            预发分支(release branch) + +更多见:https://markyun.github.io/blog/branch/ + +## Git Commit type + +bug fix - 组件 bug 修复;
breaking change - 不兼容的改动;
new feature - 新功能 + +**提交其他 Commit 类型前缀 主要如下:**
feat: 新特性\功能
fix: 缺陷修复\bug
docs: 文档相关
style: 样式修改、错别字修改、代码的格式化改动,代码逻辑并未产生任何变化
refactor: 重构或其他方面的优化
perf: 性能提升
test: 增加测试
chore: 业务无关修改,如:发版、构建工具链修改等
scope 可选,作用域标识,指明你需改的代码所属作用域
subject 真实 Commit 描述,能说明白即可,字数不用太多 + +## Git 日常操作 + +``` +git diff 查看修改内容 + +git bisect 二分查找法 定位问题 + +git clone git@github.com:UED/test.git + +git fetch origin //取得远程更新,这里可以看做是准备要取了 + +git merge origin/master //把更新的内容合并到本地分支/master + + +git remote -v //查看当前项目远程连接的是哪个仓库地址。 + +git push -u origin master // 将本地的项目提交到远程仓库中。 + +git push -u origin master -f // 强制提交 + +git commit --amend 修改上一次 commit + +抛弃本地所有的修改,回到远程仓库的状态。 +git fetch --all && git reset --hard origin/master + + git clone 地址 clone + git branch 查看分支 + git branch daily/0.0.1 创建分支 + + # 切换分支,格式为 daily/x.y.z + git checkout daily/0.0.3 + # 提交代码 + git pull + git add * + git commit -m 'feat: 完成了某个新功能' + + # 将代码push到gitlab daily环境 + git push origin daily/0.0.3 + + # 打publish tag将代码发布到CDN + --tag 创建一个里程碑 + git tag publish/0.0.3 + git push origin publish/0.0.3 +``` + +## **代码中的注释前缀和类型** + +TODO: 有功能待实现。此时需要对将要实现的功能进行简单说明。 + +FIXME: 该处代码运行正常,但可能由于时间赶或者其他原因,需要修正。此时需要对如何修正进行简单说明。 + +HACK: 为修正某些问题而写的不太好或者使用了某些诡异手段的代码。此时需要对思路或诡异手段进行描述。 + +``` +# 标题行:50个字符以内,描述主要变更内容 + # + # 主体内容:更详细的说明文本,建议72个字符以内。 需要描述的信息包括: + # + # * 为什么这个变更是必须的? 它可能是用来修复一个bug,增加一个feature,提升性能、可靠性、稳定性等等 + # * 他如何解决这个问题? 具体描述解决问题的步骤 + # * 是否存在副作用、风险? + # + # 尾部:如果需要的化可以添加一个链接到issue地址或者其它文档,或者关闭某个issue。 +``` diff --git "a/content/blog/\346\210\221\347\232\204\345\211\215\347\253\257\347\274\226\347\240\201\350\247\204\350\214\203.md" "b/content/blog/\346\210\221\347\232\204\345\211\215\347\253\257\347\274\226\347\240\201\350\247\204\350\214\203.md" new file mode 100644 index 0000000..557e748 --- /dev/null +++ "b/content/blog/\346\210\221\347\232\204\345\211\215\347\253\257\347\274\226\347\240\201\350\247\204\350\214\203.md" @@ -0,0 +1,199 @@ +--- +title: 我的前端编码规范 +date: 2019-09-21 17:31:20 +tags: ["前端", "编码规范"] +series: ["前端开发"] +category: ["blog", "前端"] +featured: true +--- + +记录一些前端开发日常的编码规范,如命名规范、代码风格、提交规范等,不定期更新。 + +## 命名规范 + +- 变量名,函数名 小驼峰【命名法 camel Case】: numberOfPeople 第一个单词的首字母小写;第二个单词开始每个单词的的首字母大写 + +- 组件名 大驼峰【命名法 Camel Case】: NumberOfPeople 每一个单词的首字母都大写 + +- css 样式名 中横线【命名法 kabab case】: number-of-people 单词小写用(-)中横线分隔 + +- 常量名,graphql query 与 mutation 变量名:蛇底式大写【命名法 upper snake case】: NUMBER*OF_PEOPLE 复合词或短语中的各个单词之间:下划线(*)分隔并且没有空格 + +- 禁用小写加下划线:number_of_people + +例: + +```javascript +handleButtonClick +onChange={this.handleCardChange} +handleSubmit +hideDialog +handleNextStep +onConfirm = handleConfirmClick +onCancel +handleDialogClose +handleClickNextButton +``` + +| 命名方式 | 应用范围 | 备注 | +| ---------------- | ---------------------------------------- | ----------------------------------- | +| camel Case | 变量名,函数名 | | +| Camel Case | 组件名,枚举名 | 枚举:SaveType = { BUILD: 'BUILD' } | +| kabab case | css 样式名 | | +| upper snake case | 常量名,graphql query 与 mutation 变量名 | | +| snake case | 禁止使用 | | + +#### 应用范围 + +| 结构 | 应用范围 | 备注 | +| ------------------------------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 动 + 宾 (+ 副词) | 函数名,graphql mutation 名称 | | +| 名词 (定语 + 名词) | 组件名,类名 | | +| 形容词 (名词 + 形容词)
动词被动形态
(be+xxx) | 状态变量 | 控制对话框是否显示:dialogVisible

尽可能不要用 is+ 形容词结构,如 isSelected, 用 selected 就可以了.

is+ 名词结构可以使用,如 isEnterprise | + +### 名词示例 + +**camel Case**: numberOfPeople
**Camel Case**: NumberOfPeople
**kabab case**: number-of-people
**snake case**: number_of_people
**upper snake case**: NUMBER_OF_PEOPLE + +## Merge Request Checklist + +- 检查是否和目标分支有冲突 + +- 检查是否修复所有 dn test 的问题 + +- 检查目录、文件名拼写 + +- 检查 graphql 文件名拼写,Query 首字母大写的 CamelCase,Mutation 首字母小写的 camelCase + +- 检查 conf 中的命名是否符合规范 + +## 标准的项目研发流程包括以下几个阶段: + +``` +* 评审阶段 + * 需求评审 + * 交互评审 + * 视觉评审 +* 开发阶段 + * 原型开发 + * 用户交互事件响应 + * 接入Mock数据 + * 后台接口数据对接 +* 联调阶段 + * 预发联调 + * 整个业务串联测试流程 +* 测试阶段 + * 埋点开发及验证 + * 自测用例覆盖 + * 交付提测 + * bug修复 +* 发布上线 +``` + +## 写作的基本准则(优化) + +``` +基本上写作的基本准则的每一部分都能应用在代码上: + ● 让段落成为文章的基本结构:每一段对应一个主题。 + ● 去掉无用的单词。 . + ● 使用主动语态。 + ● 避免一连串松散的句子。 + ● 将相关的词语放在一起。 + ● 陈述句用主动语态。 + ● 平行的概念用平行的结构。 + 这些对应的 用在前端的代码风格上 + 1. 让函数成为代码的基本单元。每个函数做一件事。 + 2. 去掉无用的代码 + 3. 使用主动语态 + 4. 避免一连串松散结构的代码逻辑 + 5. 把相关的变量、函数放在一起。 + 6. 表达式和陈述语句中使用主动语态。 + 7. 用并行的代码表达并行的概念。 +``` + +## Bug 分类 + +- fixed: 注明问题出现的原因 +- Won't fix:非问题,不需要修复的 bug,需置为“Won't fix” +- Later:遗留 bug,需要将 bug 置为“Later” +- Worksforme:log 无效,且无法再复现的 bug,需置为“Worksforme”,由测试同学继续跟踪复现 +- Duplicate:重复 bug 需要设置为“Duplicate”,并关联初始 bug,由测试同学跟踪初始 bug 解决状态 +- invaild:无效的 bug, +- By Design:逻辑设计如此,不需要修复的 bug,需置为“By Design”。 + +## Git 分支 + +存在三种短期分支
            功能分支(feature branch)
            补丁分支(hotfix branch)
            预发分支(release branch) + +更多见:https://markyun.github.io/blog/branch/ + +## Git Commit type + +bug fix - 组件 bug 修复;
breaking change - 不兼容的改动;
new feature - 新功能 + +**提交其他 Commit 类型前缀 主要如下:**
feat: 新特性\功能
fix: 缺陷修复\bug
docs: 文档相关
style: 样式修改、错别字修改、代码的格式化改动,代码逻辑并未产生任何变化
refactor: 重构或其他方面的优化
perf: 性能提升
test: 增加测试
chore: 业务无关修改,如:发版、构建工具链修改等
scope 可选,作用域标识,指明你需改的代码所属作用域
subject 真实 Commit 描述,能说明白即可,字数不用太多 + +## Git 日常操作 + +``` +git diff 查看修改内容 + +git bisect 二分查找法 定位问题 + +git clone git@github.com:UED/test.git + +git fetch origin //取得远程更新,这里可以看做是准备要取了 + +git merge origin/master //把更新的内容合并到本地分支/master + + +git remote -v //查看当前项目远程连接的是哪个仓库地址。 + +git push -u origin master // 将本地的项目提交到远程仓库中。 + +git push -u origin master -f // 强制提交 + +git commit --amend 修改上一次 commit + +抛弃本地所有的修改,回到远程仓库的状态。 +git fetch --all && git reset --hard origin/master + + git clone 地址 clone + git branch 查看分支 + git branch daily/0.0.1 创建分支 + + # 切换分支,格式为 daily/x.y.z + git checkout daily/0.0.3 + # 提交代码 + git pull + git add * + git commit -m 'feat: 完成了某个新功能' + + # 将代码push到gitlab daily环境 + git push origin daily/0.0.3 + + # 打publish tag将代码发布到CDN + --tag 创建一个里程碑 + git tag publish/0.0.3 + git push origin publish/0.0.3 +``` + +## **代码中的注释前缀和类型** + +TODO: 有功能待实现。此时需要对将要实现的功能进行简单说明。 + +FIXME: 该处代码运行正常,但可能由于时间赶或者其他原因,需要修正。此时需要对如何修正进行简单说明。 + +HACK: 为修正某些问题而写的不太好或者使用了某些诡异手段的代码。此时需要对思路或诡异手段进行描述。 + +``` +# 标题行:50个字符以内,描述主要变更内容 + # + # 主体内容:更详细的说明文本,建议72个字符以内。 需要描述的信息包括: + # + # * 为什么这个变更是必须的? 它可能是用来修复一个bug,增加一个feature,提升性能、可靠性、稳定性等等 + # * 他如何解决这个问题? 具体描述解决问题的步骤 + # * 是否存在副作用、风险? + # + # 尾部:如果需要的化可以添加一个链接到issue地址或者其它文档,或者关闭某个issue。 +```