大家好,今天我們來講講OAuth的實作方式。OAuth有授權碼模式和密碼模式,通常情況下我們都是使用授權碼模式。應用首先會點選一個授權連結,然後攜帶應用ID和重定向位址,第三方認證伺服器在接收到對應請求後,如果驗證成功,會繼續跳轉到上一步傳過來的重定向位址,并攜帶一個code,應用可以根據code向第三方認證伺服器擷取通路token,到了這一步授權就完成了。後面可以根據token向第三方授權伺服器擷取對應的授權資訊。
我們以碼雲作為OAuth的示範,碼雲OAuth的驗證流程如下圖:
為了示範,我們需要先在碼雲上申請一個應用。
1、在 修改資料 -> 第三方應用,建立要接入碼雲的應用。
2、填寫應用相關資訊,勾選應用所需要的權限。其中: 回調位址是使用者授權後,碼雲回調到應用,并且回傳授權碼的位址。
3、建立成功後,會生成 Cliend ID 和 Client Secret。他們将會在上述OAuth2 認證基本流程用到。
應用建立成功後,就可以進入編碼了。我們以egg.js作為我們後端的實作。
1、安裝
$ mkdir oauth && cd oauth
$ npm init egg --type=ts
$ npm i
$ npm run dev
2、全局配置gitee參數
修改oauth/config/config.default.ts,在其中添加一個gitee的配置,并擴充到bizConfig。
const giteeOauthConfig = {
client_id: '510c2a54e0c942bc13b5ca8e88d4048f46af9a5535f',
client_secret: '2c64bcefcd37f10ff99f5cc84bc9e6739',
redirect_uri: 'http://localhost:7001/api/users/passport/gitee/callback',
authURL: 'https://gitee.com/oauth/token?grant_type=authorization_code',
giteeUserAPI: 'https://gitee.com/api/v5/user',
};
const bizConfig = {
giteeOauthConfig,
};
3、編寫controller方法
在oauth/app/controller目錄下添加一個user.ts,我們在其中實作兩個方法oauth,oauthByGitee,分别處理授權連結的跳轉與code換取token的操作。
import { Controller } from 'egg';
export default class UserController extends Controller {
public async oauth() {
}
public async oauthByGitee() {
}
}
同時我們在oauth/app/router.ts中添加對應對控制器路由
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
router.get('/', controller.home.index);
// 使用者子產品
router.get('/api/users/passport/gitee', controller.user.oauth);
router.get('/api/users/passport/gitee/callback', controller.user.oauthByGitee);
};
4、oauth方法實作
oauth方法中主要處理應用授權連結的跳轉,跳轉時我們需要傳遞應用ID與回調位址。
public async oauth() {
const { ctx, app } = this;
const { client_id, redirect_uri } = app.config.giteeOauthConfig;
const url = `https://gitee.com/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code`;
ctx.redirect(url);
}
我們從app.config.giteeOauthConfig擷取對應的配置,然後拼接成授權連結,并使用egg提供的ctx.redirect方法進行重定向跳轉。目前端通路授權接口時,服務端重定向到第三方授權伺服器頁面
使用者點選同意授權,第三方授權伺服器會根據傳過去的redirect_uri參數進行重定向跳轉,并攜帶一個code。我們在前面配置了redirect_uri,根據路由配置重定向後就進入到了oauthByGitee方法中。我們添加oauthByGitee的實作:
public async oauthByGitee() {
const { ctx } = this;
const { code } = ctx.query;
try {
const result = await ctx.service.user.loginByGitee(code);
ctx.body = result;
} catch (error) {
console.log(error);
}
}
oauthByGitee中通過code,擷取了通路token,服務端根據擷取到的token向碼雲擷取了對應的使用者資訊,對應的service代碼如下:
import { Service } from 'egg';
/**
* Test Service
*/
export default class User extends Service {
/**
* 通過使用者授權碼從碼雲擷取對應使用者資訊
* @param code 使用者授權碼
*/
public async getAccessToken(code:string) {
const { ctx, app } = this;
const { client_id, redirect_uri, client_secret, authURL } = app.config.giteeOauthConfig;
const access_token = await ctx.curl(authURL, {
method: 'POST',
contentType: 'json',
dataType: 'json',
data: {
code,
client_id,
redirect_uri,
client_secret,
},
});
return access_token;
}
public async getGiteeUserData(access_token: string) {
const { ctx, app } = this;
const { giteeUserAPI } = app.config.giteeOauthConfig;
const { data } = await ctx.curl(`${giteeUserAPI}?access_token=${access_token}`, {
dataType: 'json',
});
return data;
}
public async loginByGitee(code: string) {
// 擷取access_token
const access_token = await this.getAccessToken(code);
// 擷取使用者資訊
const user = await this.getGiteeUserData(access_token.data.access_token);
return user;
}
}
在loginByGitee,我們擷取了access_token,并根據access_token擷取了使用者資訊。此時oauth授權登入我們就完成了一半,也就是隻完成了授權,登入還沒實作。我們從第三方拿到了使用者資訊,此時我們就可以根據這些資訊來完成登入的操作。
5、前後端分離授權方式
目前市面上的應用基本是前後端分離的,通常我們前後端是分開部署的,此時就會有一個跨源通信的問題。前面我們已經根據第三方提供的資訊生成了自己的使用者token,那麼在前後端分離的情況下我們怎麼将token傳給前端呢?其實浏覽器已經給我們提供了方法,我們可以通過postMessage來實作跨源通信。
前端通過window.open打開授權連結,使用者同意授權後,跳轉到回調頁面。後端拿到token後自己渲染一個頁面提示授權成功,渲染的頁面邏輯:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>授權成功</title>
</head>
<body>
<h1>授權 成功</h1>
<h2>兩秒後關閉</h2>
</body>
<script>
window.onload=function(){
setTimeout(()=>{
window.opener.postMessage('{{token}}','http://127.0.0.1:5173/')
window.close()
},2000)
}
</script>
</html>
這時候前端通過addEventListener就可以監聽到對應事件,并接收對應參數了,這樣就授權成功了。