使用 Authing + Lambda 替代 AWS Cognito

Amazon Web Services(AWS) 虽然做为市场份额全球第一的云计算厂商,其产品也不是天衣无缝的,Cognito (AWS 的身份认证解决方案)及其附带的中文文档就是一个反面教材,其难用程度使人发指。固然,除了不易用以外,还有访问速度缓慢,不适用于中国市场等问题存在。javascript

而国产的 Authing 能够解决使用 Cognito 的诸多问题,先看一下 Authing 的介绍:前端

Authing 是一个身份认证服务商,其提供了企业级身份认证和管理解决方案,客户分布教育、IoT、互联网和电商等多个行业。

Lambda 是一个由 AWS 提供的 Function-as-a-Service (FaaS) 平台 。Lambda 和 AWS 生态结合的很是紧密,接入 Lambda 后,开发者可使用 AWS 生态内的全部资源。好比,咱们能够建立一个 Lambda 函数,让用户经过 Cognito 登陆(固然这篇文章是让用户使用 Authing 登陆),而后再调用另一个能够上传文件到 S3(AWS 的存储服务) 的 Lambda 函数。java

这类平台(如今多被称为 Serverless,无服务器架构)的一个好处是可让开发者无需担忧基础设施,专心业务研发。node

FaaS 或者说 Serverless 平台正在逐渐得到市场关注,由于这种类型的平台可让开发者不用再关注基础设施。"What is serverless" 这篇文章详细的讲解了什么是「无服务器计算」和「无服务器计算」的好处,推荐读一下。git

这篇文章的主要目的是介绍如何使用 Authing + Lambda 替代 AWS Cogito,点击这里体验最终 demo。github

https://s2.ax1x.com/2019/04/30/E82txA.jpg

此外,Authing 遵循 OIDC 规范,因此本篇文章将使用 OIDC 来作认证,若是你还不了解什么是 OIDC,请查看这篇文章。web

首先确认下用户的操做流程:shell

  1. 打开页面:https://sample.authing.cn/aws/
  2. 点击 Login 进行登陆,此时跳转到 Authing 的登陆页面(应用的二级域名);
  3. 输入帐号密码进行登陆,若没有帐号密码请先进行注册;
  4. 登陆成功后返回第一步打开的页面,并显示登陆用户的头像;
  5. 此时用户能够看到从 AWS Lambda 请求回来的 Private 信息;

最终效果以下图所示:数据库

https://s2.ax1x.com/2019/04/30/E822Mn.jpg

点击这里体验 DEMOnpm

建立一个 Authing 应用

若是你尚未注册 Authing,那么请点击这里进行注册,注册完成后,按如下步骤建立一个 Authing 应用。

  1. 建立应用

E82Hz9.jpg

  1. 填写基本信息,应用类型选择 Web 应用

https://s2.ax1x.com/2019/04/30/E8RCzd.jpg

  1. 建立完成后会进入到应用主页(空空如也)

https://s2.ax1x.com/2019/04/30/E8RkLt.jpg

建立 OIDC 应用

建立完应用后至关于你有了一个用户池,接下来你能够建立 OIDC 应用来受权其余程序(你本身写的或其余第三方程序)访问你的用户池。

若是你还不清楚什么是 OIDC,请参考这篇文章

  1. 点击「第三方登陆」开始建立 OIDC 应用

E82Oqx.png

  1. 选择「OIDC 应用」选项卡,并点击「建立 OIDC 应用」

https://s2.ax1x.com/2019/04/30/E8RGwV.jpg

  1. 填写应用名和认证地址,并勾选 id_token token

https://s2.ax1x.com/2019/04/30/E8RJoT.jpg

这里要说明一下,建立 OIDC 应用时的认证地址将由 Authing 生成一个二级域名(支持 HTTPS),且不能重复,回调 URL 填写你本身的回调地址便可,在这里我用的是 https://authing.cn,注意,OIDC 协议中不容许回调 URL 为 localhost,请使用代理工具进行调试。

点击确认,就能够看到咱们有了第一个基于 OIDC 协议的受权应用
E82qMR.jpg

建立完成后你能够访问 lambda.authing.cn ,此时会看到报了一个错,别惧怕,这是由于咱们发起的受权连接不正确。
E82Ls1.png

发起正确受权请求的方式请继续往下看。

发起受权请求

和绝大多数的 OAuth 应用差很少,OIDC 的受权连接也须要拼接(若是你开发过微信应用,应该会很容易理解),Authing OIDC 应用的受权连接符合标准规范,具体格式为:

https://lambda.authing.cn/oau...;应用 ID>&redirect_uri=<回调 URL,必须和平台配置彻底同样>&scope=openid profile&response_type=<OIDC 模式,分为好几种>&state=<一个随机字符串,用来防范 CSRF 攻击>

若须要查看详细的参数,请点击这里查看

例如:

https://lambda.authing.cn/oau...://authing.cn&scope=openid profile&response_type=id_token token&state=jacket

为了简单起见,这里咱们的 response_type 设置为「id_token token」,这样不须要使用「code」换取 token,token 会直接附带到回调地址中。

https://s2.ax1x.com/2019/04/30/E8RTTf.jpg

若是你的受权连接正确,应该能够看到上图这样的登陆窗口,同时这个窗口也是你的终端用户所使用的窗口,他们都将从这里登陆而后回调到你配置好的回调 URL 中。

你能够试着注册一个帐号而后进行登陆,登陆完成后能够在控制台中观察到登陆情况。

  1. 注册成功

https://s2.ax1x.com/2019/04/30/E8WS00.jpg

  1. 登陆以后的受权页面

https://s2.ax1x.com/2019/04/30/E8WihF.jpg

  1. 控制台中观察到的用户数据

https://s2.ax1x.com/2019/04/30/E8WEc9.jpg

在你登陆成功后应该会看到回调到了你填写 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 https://users.authing.cn/oauth/oidc/user/userinfo?access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQy...balabala...verylong...

能够获取到 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 应用的详情中找到:

https://s2.ax1x.com/2019/04/30/E8WMtO.jpg

请务必保管好此值,避免向任何人泄漏

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

部署完成后会获得三个连接,这三个连接分别是上述代码的三个函数。

https://s2.ax1x.com/2019/04/30/E8WREV.jpg

红框中的路由是在 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!

最后,在咱们的前端补充上相关信息,在点击登陆后应该能够看到以下信息:

https://s2.ax1x.com/2019/04/30/E822Mn.jpg

线上体验地址:https://sample.authing.cn/aws/

Enjoy!
Authing.cn - 领先的身份认证云

https://s2.ax1x.com/2019/04/30/E8W4CF.md.png

相关文章
相关标签/搜索