使用 Authing + Lambda 替代 AWS Cognito

Amazon Web Services(AWS) 雖然做爲市場份額全球第一的雲計算廠商,其產品也不是天衣無縫的,Cognito (AWS 的身份認證解決方案)及其附帶的中文文檔就是一個反面教材,其難用程度使人髮指。固然,除了不易用以外,還有訪問速度緩慢,不適用於中國市場等問題存在。前端

而國產的 Authing 能夠解決使用 Cognito 的諸多問題,先看一下 Authing 的介紹:node

Authing 是一個 身份認證服務商,其提供了企業級身份認證和管理解決方案,客戶分佈教育、IoT、互聯網和電商等多個行業。

Lambda 是一個由 AWS 提供的 Function-as-a-Service (FaaS) 平臺 。Lambda 和 AWS 生態結合的很是緊密,接入 Lambda 後,開發者可使用 AWS 生態內的全部資源。好比,咱們能夠建立一個 Lambda 函數,讓用戶經過 Cognito 登陸(固然這篇文章是讓用戶使用 Authing 登陸),而後再調用另一個能夠上傳文件到 S3(AWS 的存儲服務) 的 Lambda 函數。git

這類平臺(如今多被稱爲 Serverless,無服務器架構)的一個好處是可讓開發者無需擔憂基礎設施,專心業務研發。github

FaaS 或者說 Serverless 平臺正在逐漸得到市場關注,由於這種類型的平臺可讓開發者不用再關注基礎設施。"What is serverless" 這篇文章詳細的講解了什麼是「無服務器計算」和「無服務器計算」的好處,推薦讀一下。web

這篇文章的主要目的是介紹如何使用 Authing + Lambda 替代 AWS Cogito,點擊這裏體驗最終 demo。數據庫

此外,Authing 遵循 OIDC 規範,因此本篇文章將使用 OIDC 來作認證,若是你還不瞭解什麼是 OIDC,請查看這篇文章npm

首先確認下用戶的操做流程

  1. 打開頁面:sample.authing.cn/aws/
  2. 點擊 Login 進行登陸,此時跳轉到 Authing 的登陸頁面(應用的二級域名);
  3. 輸入帳號密碼進行登陸,若沒有帳號密碼則先進行註冊;
  4. 登陸成功後返回第一步打開的頁面,並顯示登陸用戶的頭像;
  5. 此時用戶能夠看到從 AWS Lambda 請求回來的 Private 信息;

最終效果以下圖所示:json

建立一個 Authing 應用

若是你尚未註冊 Authing,那麼請點擊這裏進行註冊,註冊完成後,按如下步驟建立一個 Authing 應用。後端

1. 建立應用
2. 填寫基本信息,應用類型選擇 Web 應用
3. 建立完成後會進入到應用主頁(空空如也)

建立 OIDC 應用

建立完應用後至關於你有了一個用戶池,接下來你能夠建立 OIDC 應用來受權其餘程序(你本身寫的或其餘第三方程序)訪問你的用戶池。api

若是你還不清楚什麼是 OIDC,請參考這篇文章

4. 點擊「第三方登陸」開始建立 OIDC 應用
5. 選擇「OIDC 應用」選項卡,並點擊「建立 OIDC 應用」
6. 填寫應用名和認證地址,並勾選 id_token token

這裏要說明一下,建立 OIDC 應用時的認證地址將由 Authing 生成一個二級域名(支持 HTTPS),且不能重複,回調 URL 填寫你本身的回調地址便可,在這裏我用的是 authing.cn,注意,OIDC 協議中不容許回調 URL 爲 localhost,請使用代理工具進行調試。

7. 點擊確認,就能夠看到咱們有了第一個基於 OIDC 協議的受權應用

建立完成後你能夠訪問 lambda.authing.cn ,此時會看到報了一個錯,別懼怕,這是由於咱們發起的受權連接不正確。

8. 訪問 lambda.authing.cn 時報的錯

發起正確受權請求的方式請繼續往下看。

發起受權請求

和絕大多數的 OAuth 應用差很少,OIDC 的受權連接也須要拼接(若是你開發過微信應用,應該會很容易理解),Authing OIDC 應用的受權連接符合標準規範,具體格式爲:

lambda.authing.cn/oauth/oidc/… profile&response_type=<OIDC 模式,分爲好幾種>&state=<一個隨機字符串,用來防範 CSRF 攻擊>

若須要查看詳細的參數,請點擊這裏查看。

例如:

lambda.authing.cn/oauth/oidc/…

爲了簡單起見,這裏咱們的 response_type 設置爲「id_token token」,這樣不須要使用「code」換取 token,token 會直接附帶到回調地址中。

9. 若是你的受權連接正確,應該能夠看到上圖這樣的登陸窗口

若是你的受權連接正確,應該能夠看到上圖這樣的登陸窗口,同時這個窗口也是你的終端用戶所使用的窗口,他們都將從這裏登陸而後回調到你配置好的回調 URL 中。

你能夠試着註冊一個帳號而後進行登陸,登陸完成後能夠在控制檯中觀察到登陸情況。

10. 註冊成功
11. 登陸以後的受權頁面
12. 控制檯中觀察到的用戶數據

在你登陸成功後應該會看到回調到了你填寫 URL 中,而且附帶了不少參數,接下來咱們會闡述如何使用這些參數。

獲取用戶信息

回調到在控制檯中配置的 redirect_uri 中後,將附帶如下信息:

{
	"id_token": "JWT_TOKEN",
	"access_token": "JWT_TOKEN",
	"expires_in": "3600",
	"token_type": "Bearer",
	"state": "jacket",
	"session_state": "644d7b324ba61d517fdedd28b5b6e365d78f2a8178f2ee742474d5b57a99eb3f"
}
複製代碼

能夠看到其中包含了 access_token id_token,其中 access_token 能夠幫助你從 Authing 後端獲取用戶信息,而 id_token 中包含了基本的信息,若是你要獲取用戶的頭像,那麼是須要經過 access_token 獲取的。

咱們先看一個 id_token 的例子:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJzdWIiOiI1Y2MyYTg1MTFiYmFmMDRmOTNjZTQ4OWYiLCJub25jZSI6IjE4MzEyODkiLCJzaWQiOiI5MzkwZDA1ZC01ZTM3LTQ3ZWUtODJjNi1jNTQ1ZjA2ODhhMDAiLCJhdF9oYXNoIjoiNmxZMGRXajZYUTY0aExWdHAtR2tEdyIsInNfaGFzaCI6IlZVOU5QYV9JQ0VTSEdxRmxUZ3A2LUEiLCJhdWQiOiI1Y2MyYjU0OGQxNGM3NDJkYjg5M2JhNTUiLCJleHAiOjE1NTYzNjY0ODksImlhdCI6MTU1NjM2Mjg4OSwiaXNzIjoiaHR0cHM6Ly9vYXV0aC5hdXRoaW5nLmNuL29hdXRoL29pZGMifQ.Qc_OMqMf6_wwzW2SsEgEtiaGr3ZY1FWHnRrMU2M7LADGlNpq_pvPrFxAVsR2j-BFr1y48M-Trvq6yAu4_ZOUBHPtIIpoQ5W2bnABytUV693ZcwNlf9CCiLc-k0LG3o1U-BmiH3L6NAV7aKGsfVHS8toiNbVDuimPVdYJsRrF2C1jj1meM1K8FBVwqozXm6YtB--u3sqY4IszHnd5PMEWguLsOkpZJIh7xWeYPpVQ5WKfx0cA8rB_T2puSCbeaUVhgIwNADy06qBqXhUOiA4gdcNbHtx7tvGZMxzMC3rdjpXoZk89Duh3O5tHlMtaBlidJGYavUSjVl7potESecSlBg

使用 jwt.io 解析後將獲得以下結果:

{
  "sub": "5cc2a8511bbaf04f93ce489f",
  "nonce": "1831289",
  "sid": "9390d05d-5e37-47ee-82c6-c545f0688a00",
  "at_hash": "6lY0dWj6XQ64hLVtp-GkDw",
  "s_hash": "VU9NPa_ICESHGqFlTgp6-A",
  "aud": "5cc2b548d14c742db893ba55",
  "exp": 1556366489,
  "iat": 1556362889,
  "iss": "https://oauth.authing.cn/oauth/oidc"
}
複製代碼

其中包含了簽發時間(iat)、過時時間(exp)等字段,能夠用來判斷用戶有沒有被認證過,在 OIDC 的規範中,JWT 使用 OIDC 應用的 secret 簽發,須要開發者在後端驗證(這一步咱們將會在 Lambda 中執行)後繼續執行開發者自己的業務流程。

再來看看 access_token 的例子:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJqdGkiOiJza0p-bTNaYmZsTjVxVGEzR2J2YlMiLCJzdWIiOiI1Y2MyYTg1MTFiYmFmMDRmOTNjZTQ4OWYiLCJpc3MiOiJodHRwczovL29hdXRoLmF1dGhpbmcuY24vb2F1dGgvb2lkYyIsImlhdCI6MTU1NjM2Mjg4OSwiZXhwIjoxNTU2MzY2NDg5LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIiwiYXVkIjoiNWNjMmI1NDhkMTRjNzQyZGI4OTNiYTU1In0.Uf3YK4D9HL-G71hkA4cWt5kitDo5rNgwVA9Vqlv4RjAILNDTylYWtkacKJpLcOSS81ivaNpDVNYYzBSoyN-eMH80VhArPUre74F9SHdonA-IVFVPT0DHRtOAJI9kqDW4tgTXhZeZMUm-MCjVjR-q8XrayXaqrC5Hu5W3D1N-K_jZOlwxzIBf51nuC4NMvSI_wPpYj2WPzGxFwpfTCEbnhj5RO0CcThRpC3EdmpbtcJqStd7AZQhkLyTb1TQLHJOel8DSxLnLnoIU0rZXsodK6EjE_oqRLagetNXF1cKfRmnGFaAKZKqgvHc527S_CVkgXIwcHBRmDeqo93CCId_hmQ

使用 jwt.io 解析後將獲得以下結果:

{
  "jti": "skJ~m3ZbflN5qTa3GbvbS",
  "sub": "5cc2a8511bbaf04f93ce489f",
  "iss": "https://oauth.authing.cn/oauth/oidc",
  "iat": 1556362889,
  "exp": 1556366489,
  "scope": "openid profile",
  "aud": "5cc2b548d14c742db893ba55"
}
複製代碼

能夠看到 access_token 相比 id_token 是少了不少信息的,這裏有一段英文的介紹,該介紹講解了 access_token 和 id_token 的區別:

ID Tokens vs Access Tokens. The ID Token is a security token granted by the OpenID Provider that contains information about an End-User. This information tells your client application that the user is authenticated, and can also give you information like their username or locale.You can pass an ID Token around different components of your client, and these components can use the ID Token to confirm that the user is authenticated and also to retrieve information about them.Access tokens, on the other hand, are not intended to carry information about the user. They simply allow access to certain defined server resources. More discussion about when to use access tokens can be found in Validating Access Tokens.

簡單來說,id_token 告訴你用戶被驗證過了,而 access_token 是一個你能夠訪問資源服務器(這裏就是 Authing) 的一個憑證。

同時也能夠看到,idtoken 包含的信息較少,若是想獲取更多信息,須要使用 access_token 來獲取。獲取方式也很是簡單,只須要往如下連接發送 GET 請求而且附帶 access_token 便可,如:

$ curl users.authing.cn/oauth/oidc/…

能夠獲取到 id 等信息,獲取到 id 以後你能夠將 id 存儲到你本身的數據庫中以完成本身的實際業務。

{
    "sub":"5cc2a8511bbaf04f93ce489f",
    "nickname":"",
    "picture":"https://usercontents.authing.cn/authing-avatar.png"
}
複製代碼

上面的 JSON 是一個使用 access_token 換取用戶數據後的返回結果。

好了,如今咱們已經獲取到 Token 了,接下來咱們須要在 Lambda 中驗證這個 Token 的合法性並在前端顯示不一樣的信息。

編寫 Lambda 函數

編寫 Lambda 函數推薦使用 Serverless 這個 CLI,AWS 控制檯中的函數編寫堪稱讓人痛不欲生。

同時,你能夠到這裏查看完整代碼。

Lambda 在這篇文章中主要用來作三件事:

  1. 對 id_token 進行認證,以獲取用戶是否被認證過;
  2. 提供一個 Public API,此 API 能夠直接被訪問;
  3. 提供一個 Private API,此 API 須要通過認證後被訪問;

對 id_token 進行認證

認證 id_token 首先須要知道 OIDC 應用的 secret,此值能夠在 Authing 控制檯查看 OIDC 應用的詳情中找到:

請務必保管好此值,避免向任何人泄漏

id_token 在簽發時的簽名是此 secret ,所以在 JavaScript 中能夠直接使用 jsonwebtoken 這個庫來驗證 id_token 的合法性(詳情請參考:驗證 Token 合法性)。

在控制檯中安裝 jsonwebtoken:

$ npm install jsonwebtoken --save複製代碼

P.S. 在 lambda 中引入包後會一塊兒打包上傳到 AWS Lambda 運行時中。

const jwt = require('jsonwebtoken');

// Policy helper function 
// 這是 AWS 提供的模版代碼,這裏不須要作修改
const generatePolicy = (principalId, effect, resource) => {
  const authResponse = {};
  authResponse.principalId = principalId;
  if (effect && resource) {
    const policyDocument = {};
    policyDocument.Version = '2012-10-17';
    policyDocument.Statement = [];
    const statementOne = {};
    statementOne.Action = 'execute-api:Invoke';
    statementOne.Effect = effect;
    statementOne.Resource = resource;
    policyDocument.Statement[0] = statementOne;
    authResponse.policyDocument = policyDocument;
  }
  return authResponse;
};

// Reusable Authorizer function, set on `authorizer` field in serverless.yml
module.exports.auth = async (event, context, cb) => {
  if (event.authorizationToken) {
    // remove "bearer " from token
    const token = event.authorizationToken.substring(7);

    try {
        let decoded = jwt.verify(token, 'YOUR_OIDC_APP_SECRET'),
          expired = (Date.parse(new Date()) / 1000) > decoded.exp;
        if (expired) {
          cb('Unauthorized, Login information has expired.');
        }else {
          cb(null, generatePolicy('user', 'Allow', event.methodArn));
        }
      } catch (error) {
        cb('Unauthorized');
      }
  } else {
    cb('Unauthorized');
  }
};
複製代碼

公共 API

// Public API
module.exports.publicEndpoint = (event, context, cb) => {
  cb(null, { message: 'Welcome to our Public API!' });
};
複製代碼

私有 API

// Private API
module.exports.privateEndpoint = (event, context, cb) => {
  cb(null, { message: 'Only logged in users can see this' });
};
複製代碼

serverless.yml

service: serverless-authorizer

provider: 
 name: aws
 runtime: nodejs8.10

functions:
  auth:
    handler: handler.auth
  getUserInfo:
    handler: handler.getUserInfo
    events:
      - http:
          path: api/userInfo
          method: get
          integration: lambda
          cors: true    
  publicEndpoint:
    handler: handler.publicEndpoint
    events:
      - http:
          path: api/public
          method: get
          integration: lambda
          cors: true
  privateEndpoint:
    handler: handler.privateEndpoint
    events:
      - http:
          path: api/private
          method: get
          integration: lambda
          authorizer: auth # See custom authorizer docs here: http://bit.ly/2gXw9pO
          cors: true複製代碼

此文件可用來配置須要鑑權的路由,如上面代碼中的 privateEndpoint,配置了 authorizer 爲 auth 函數。

點擊此處查看完整代碼。

測試 Lambda

寫完了代碼以後咱們須要進行測試。

Lambda 支持直接在本地測試,可使用以下命令:

$ sls invoke local -f auth --data '{"authorizationToken": "Bearer <id_token>"}'複製代碼

若是本地測試返回了以下信息則表示驗證成功:

{
    "principalId": "user"
}複製代碼

部署 Lambda

$ serverless deploy複製代碼

部署完成後會獲得三個連接,這三個連接分別是上述代碼的三個函數。

紅框中的路由是在 serverless.yml 中定義好的,能夠直接映射到函數中

使用 curl 或 postman 將 OIDC 登陸後的 id_token 攜帶到 header 的 Authorization 中便可查看結果,如:

$ curl --header "Authorization: <id_token>" <endpoint>複製代碼

上述三個路由的結果應該爲:

curl <endpoint/dev/api/public> - Should work! Public!
curl <endpoint/dev/api/private> - Should not work
curl --header "Authorization: <id_token>" <endpoint/dev/api/private> - Should work! Authorized!複製代碼

最後,在咱們的前端補充上相關信息,在點擊登陸後應該能夠看到以下信息:

線上體驗地址:sample.authing.cn/aws/

Enjoy!

Authing.cn - 領先的身份認證雲

相關文章
相關標籤/搜索