微信号:phodal-weixin

介绍:在这我会分享我最近的一些编程与生活相关的心得、体会,文章的内容均是原创.我也会推荐一些好玩、有趣的代码.

构建基于 Auth0 的 Serverless 的用户授权体系

2017-11-09 20:58 Markdownless 的

在多次尝试了使用 Amazon Cognito 前端授权无果,我转而使用和其它教程类似的 Auth0 授权登录。虽然 Amazon 提供了一个用于 Cognito 授权的前端组件,但是它仍然不是很成熟。在浏览器端,好像用得不是很普遍,而 Auth0 则是一个更通用的方案。

Auth0 是一家“身份验证即服务”提供商,旨在为开发人员提供简单易用的身份管理服务。为了保持灵活性和可扩展性,Auth0 身份管理平台允许开发人员在身份验证和授权管道中增加自定义代码。

最后的代码见:auth0-frontend

代码的执行逻辑如下所示:

  • 由前端使用 Auth0 的 lock.js 调出授权框,进行用户授权

  • 用户可以选择使用第三方授权服务登录,如 Google、GitHub

  • 用户登录完后,会获取一个 Auth0 的 Token,通过该 Token 去请求数据

  • 后台接到数据后,先验证 Token 是否有效的,然后返回相应的结果

因此,对于我们而言,我们需要做这么一些事:

  • 创建一个 Serverless 服务

  • 创建一个验证 Token 的 Lambda 函数

  • 注册 Auth0 账户

  • 绑定 Auth0 的 GitHub 授权

这里我们采用的是 Serverless Framework 的官方示例 Demo。稍有不同的是,代码中对静态文件和 S3 部分进行了一些优化——官方的 DEMO,无法直接部署到 S3 上。

Serverless Auth0 前端代码

在这次的教程里,代码分为两部分:前端和后台。

这里的前端代码,是一个纯前端的代码。

先让我们看看授权部分:

 
           
  1. const lock = new Auth0Lock(AUTH0_CLIENT_ID, AUTH0_DOMAIN);

  2. ...

  3. lock.show((err, profile, token) => {

  4.    if (err) {

  5.      console.error('Something went wrong: ', err);

  6.    } else {

  7.      localStorage.setItem('userToken', token);

  8.      localStorage.setItem('profile', JSON.stringify(profile));

  9.      ...

  10.    }

  11.  });


首先,我们创建了一个 Auth0Lock 对象,并在参数中转入了对应的 ID 和 Auth0 域名。然后使用 lock.show 方法将调出 Auth0 的登录页面,当用户登录成功的时候,就会从后台取到 token 和 profile,然后我们在上面的代码中保存用户的 token 和 profile 到 localstorage 中。

然后在发送 fetch 请求的时候,我们会带上这个 Token:

 
           
  1. const token = localStorage.getItem('userToken');

  2. if (!token) {

  3.  return false;

  4. }

  5. const getdata = fetch(PRIVATE_ENDPOINT, {

  6.  headers: {

  7.    Authorization: `Bearer ${token}`,

  8.  },

  9.  method: 'GET',

  10.  cache: 'no-store',

  11. });

  12. getdata.then((response) => {

  13.  response.json().then((data) => {

  14.    console.log('Token:', data);

  15.  });

  16. });

主要的前端逻辑代码就是这么简单。

Serverless Auth0 后台代码

首先,先让我们看一眼 serverless.yml 配置。

serverless.yml 配置

 
           
  1. functions:

  2.  auth:

  3.    handler: handler.auth

  4.    environment:

  5.      AUTH0_ID: ${file(./config.yml):AUTH0_ID}

  6.      AUTH0_SECRET: ${file(./config.yml):AUTH0_SECRET}

  7.  publicEndpoint:

  8.    handler: handler.publicEndpoint

  9.    events:

  10.      - http:

  11.          path: api/public

  12.          method: get

  13.          integration: lambda

  14.          cors: true

  15.  privateEndpoint:

  16.    handler: handler.privateEndpoint

  17.    events:

  18.      - http:

  19.          path: api/private

  20.          method: get

  21.          integration: lambda

  22.          authorizer: auth # See custom authorizer docs here: http://bit.ly/2gXw9pO

  23.          cors:

  24.            origins:

  25.              - '*'

  26.            headers:

  27.              - Content-Type

  28.              - X-Amz-Date

  29.              - Authorization

  30.              - X-Api-Key

  31.              - X-Amz-Security-Token

配置中定义了三个 lambda 函数:

  • auth 函数,用于对用户传过来的 Token 进行校验

  • publicEndpoint 函数,一个公开的 API 结点

  • privateEndpoint 函数,一个需授权才能访问的 API,即它将调用 auth 函数,根据授权结果来返回相应的内容。

更详细的资料,可以访问官方的文档:使用 API Gateway 自定义授权方。

auth 函数的代码如下所示:

 
           
  1. const jwt = require('jsonwebtoken');

  2. ...

  3. const jwt = require('jsonwebtoken');

  4.  if (event.authorizationToken) {

  5.    // remove "bearer " from token

  6.    const token = event.authorizationToken.substring(7);

  7.    const options = {

  8.      audience: AUTH0_CLIENT_ID,

  9.    };

  10.    jwt.verify(token, AUTH0_CLIENT_SECRET, options, (err, decoded) => {

  11.      if (err) {

  12.        cb('Unauthorized');

  13.      } else {

  14.        cb(null, generatePolicy(decoded.sub, 'Allow', event.methodArn));

  15.      }

  16.    });

  17.  } else {

  18.    cb('Unauthorized');

  19.  }

  20. };


代码中的主要函数是 jwt.verify,它将根据 AUTH0 的 ID 和密钥来校验 token 是否是有效的。同时,还引用了一个名为 generatePolicy 的方法:

 
           
  1. const generatePolicy = (principalId, effect, resource) => {

  2.  const authResponse = {};

  3.  authResponse.principalId = principalId;

  4.  if (effect && resource) {

  5.    const policyDocument = {};

  6.    policyDocument.Version = '2012-10-17';

  7.    policyDocument.Statement = [];

  8.    const statementOne = {};

  9.    statementOne.Action = 'execute-api:Invoke';

  10.    statementOne.Effect = effect;

  11.    statementOne.Resource = resource;

  12.    policyDocument.Statement[0] = statementOne;

  13.    authResponse.policyDocument = policyDocument;

  14.  }

  15.  return authResponse;

  16. };

这个方法用于生成一个 IAM 的策略,这个策略的生成规则建议参考官方文档,以上的内容和 AWS 的官方 DEMO 是一致的。随后,再根据生成的是 'Allow' 或者 'Deny' 来判断,该用户是否拥有权限。如果用户拥有权限的,那么就会继续往下执行:

 
           
  1. module.exports.privateEndpoint = (event, context, cb) => {

  2.  cb(null, { message: 'Only logged in users can see this' });

  3. };

说了这么多,还是让我们跑跑代码吧。

配置及部署

在这一个步骤里我们要做这么几件事:

  • 注册、获取 Auth0 的账号

  • 部署 Lambda 函数,获取后台 API 地址

  • 根据上一步生成的地址,修改前端代码中的地址

因此在开始之前,需要先申请一个 Auth0 的账号,然后在 config.yml 中,添加 auth0 的 id 和密钥。

然后执行部署:

···

 
           
  1. ........................................................................

  2. Serverless: Stack update finished...

  3. Service Information

  4. service: auth0-frontend

  5. stage: dev

  6. region: us-east-1

  7. stack: auth0-frontend-dev

  8. api keys:

  9.  None

  10. endpoints:

  11.  GET - https://fy0qtq1r8c.execute-api.us-east-1.amazonaws.com/dev/api/public

  12.  GET - https://fy0qtq1r8c.execute-api.us-east-1.amazonaws.com/dev/api/private

  13. functions:

  14.  auth: auth0-frontend-dev-auth

  15.  publicEndpoint: auth0-frontend-dev-publicEndpoint

  16.  privateEndpoint: auth0-frontend-dev-privateEndpoint

将生成的 API Gateway 的地方放入到 client/dist/app.js 文件中:

再执行:

 
           
  1. $ serverless client deploy

以部署我们的静态文件。

 
           
  1. Serverless: Deploying client to stage "dev" in region "us-east-1"...

  2. Serverless: Creating bucket auth.wdsm.io...

  3. Serverless: Configuring website bucket auth.wdsm.io...

  4. Serverless: Configuring policy for bucket auth.wdsm.io...

  5. Serverless: Configuring CORS policy for bucket auth.wdsm.io...

  6. Serverless: Uploading file app.css to bucket auth.wdsm.io...

  7. Serverless: If successful this should be deployed at: https://s3.amazonaws.com/auth.wdsm.io/app.css

  8. Serverless: Uploading file app.js to bucket auth.wdsm.io...

  9. Serverless: If successful this should be deployed at: https://s3.amazonaws.com/auth.wdsm.io/app.js

  10. Serverless: Uploading file index.html to bucket auth.wdsm.io...

  11. Serverless: If successful this should be deployed at: https://s3.amazonaws.com/auth.wdsm.io/index.html

然后打开 https://s3.amazonaws.com/auth.wdsm.io/index.html 就可以尝试授权。

不过,在那之间,我们需要填写对应平台的授权信息:


Auth GitHub

接着,点击上面的 GitHub 『!』号,会提示我们填写对应的授权信息。

打开我们的 GitHub ,申请一个新的 OAuth 应用,地址:https://github.com/settings/applications/new

详细的信息见:https://auth0.com/docs/github-clientid。

如我的配置是:

 
           
  1. Homepage URL: https://phodal.auth0.com

  2. Authorization callback URL  https://phodal.auth0.com/login/callback

完成后,把生成的 GitHub ID 和 Client Secret 填入。点击 Save,Auth0 就会自动帮我们测试。

接着,再到我们的页面上尝试使用 GitHub 登录,还是报了个错:

 
           
  1. app.js:26 Something went wrong:  Error: error: invalid origin: https://s3.amazonaws.com

  2.    at new LoginError (lock-9.0.min.js:9)

  3.    at lock-9.0.min.js:9

  4.    at onMessage (lock-9.0.min.js:10)

漏掉了在 Auth0 的设置页的 Allowed Callback URL 和 Allowed Origins 上加上用于登录的地址,用于允许跨域请求了。在这里,我的地址是:

 
           
  1. https://s3.amazonaws.com/auth.wdsm.io/index.html


CORS 配置


然后,再测试一下登录:

Auth0 测试登录

漂亮,我们登录成功了。

清理

  • 删除 Auth0 的应用

  • 删除 GitHub 的应用

  • 清空 Bucket: serverless client remove

  • 清空 Lambda: serverless remove

结论

AWS 官方的 Congito 支持的第三方应用有限,在这个时候 Auth0 成了一个更好的选择。除了 GitHub,Auth0 还集成了微博、人人等等的国内的平台。

当然授权,作为一个基础的服务,几乎是每个应用的重要功能,也是核心的功能。对于大数中大型公司来说,几乎不太可能采用这样的方案。

 
phodal 更多文章 《全栈应用开发:精益实践》亚马逊五折特惠 在这家不加班的 IT 公司里,我们是这样加班的 Serverless 应用开发指南:serverless 的 hello, world 通过使用结构化数据 JSON-LD,我为网站带来了更多的流量 这些改成中文名的前端框架,你还能认识几个?
猜您喜欢 浅谈redis超时(二) 数人云|值得掏空身体来听的应用容器化实践Meetup 用深度链接优化用户增长策略 下一代分布式消息系统Apache Kafka Emacs是最好的编辑器~~