15身份认证:使用Serverle实现登录注册功能
文章目录
从今天开始,我们将进入 Serverless 应用的场景案例篇。这一讲我将带你实现 Serverless 中的身份认证。
我们平时用过那么多的网站和 App,很多都需要登录,登录的过程就是身份认证的过程。以电商网站为例,如果你想下单,先要注册账号,然后输入用户名(可能手机号或邮箱)、密码登录。之后你在一段时间内再访问该电商网站,都不用再登录,只有在连续长时间不登录的情况下(比如一个月),才用登录。这样一来,当你长时间不输入密码,很可能换了电脑或手机后,就忘记密码了。
身份认证几乎是每个系统必备能力,所以很多同学开发应用时,实现的第一功能就是登录注册。然而当把应用迁移到 Serverless 架构时,很多同学就犯难了,基于 Serverless 的身份认证功能应该怎么实现呢?
为了让你深入理解 Serverless 架构中的身份认证实现原理,我准备了今天的内容,这一讲我会先带你了解一些身份认证技术方案,然后带你从零到一实现一个 Serverless 的登录注册应用。
话不多说,我们进入今天的学习。
身份认证的技术方案
要实现应用中的身份认证,你首先要详细了解身份认证的技术方案,以及该方案怎么在 Serverless 架构中使用,因为有的技术方案可能不适合 Serverless。
Cookie-Session
早期互联网主要以 Web 为主,客户端是浏览器,所以 Cookie-Session 方式是早期最常用的身份认证方式,直到现在很多 Web 网站依然使用这种方式。其认证流程是:
用户在浏览器中输入账号密码登录;
服务端验证通过后,将用户信息保存在 Session 中并生成一个 Session ID;
然后服务端将 Session ID 放在 HTTP 响应头的 cookie 字段中;
浏览器收到 HTTP 响应后,将 cookie 保存在浏览器中,cookie 内容就是之前登录时生成的 Session ID;
用户再访问网站时,浏览器请求头就会自动带上 cookie 信息;
服务端接收到请求后,从 cookie 获取到 Session ID,然后根据 Session ID 解析出用户信息。
Cookie-Session 身份认证流程
这种方案存在两个主要问题:
服务端的 Session ID 是直接存储在内存中的,在分布式系统中无法共享登录状态;
cookie 是浏览器的功能,手机 App 等客户端并不支持 cookie,所以该方案不适用于非浏览器的应用。
第一个问题也是 Cookie-Session 方案应用于 Serverless 架构的主要问题,因为 Serverless 应用是无状态的,内存中的数据用完即销毁,多个请求间无法共享 Session。解决该问题也比较容易, 就是用一个共享存储来保存 Session 信息,最常见的就是 Redis,因为 Redis 是一个内存数据库,读写速度很快。
于是 Cookie-Session 的身份认证方案就发生了变化:
基于共享存储的 Cookie-Session 身份认证流程
与早期方案不同,用户登录时,该方案会把用户信息保存在 Redis 中,而不是内存中,然后服务端依然会将 Session ID 返回给浏览器,浏览器将其保存在 cookie 中。而之后非登录的请求,浏览器依然会将包含 Session ID 的 cookie 放在请求头中发送给服务端,服务端拿到 Session ID 后,从 Redis 中查询出用户信息。这样就可以解决分布式、无状态的系统中用户登录状态共享问题。
不过这个方案依旧无法解决非浏览器场景的身份认证问题,所以 JWT 方案诞生了。
JWT
JWT 是(JSON Web Token)的简称,其原理是:
服务端认证通过后,根据用户信息生成一个 token 返回给客户端;
客户端将 token 存储在 cookie 或 localStorage 中;
之后客户端每次请求都需要带上 token,通常是将 token 放在 HTTP 请求头的 Authorization 字段中;
服务端接收到 token 后,验证 token 的合法性,并从 token 中解析出用户信息。
JWT 身份认证流程
token 是个比较长字符串,格式为 Header.Payload.Signature ,由 . 分隔为三部分。下面是一个实际的 token 示例:
|
|
可能有同学会担忧: token 是根据用户信息生成的,这样会不会泄露用户信息呢?其实不用担心,因为生成 token 的加密算法是不可逆的,并且 token 也可以设置过期时间,所以 token 字符串本身不会泄露用户信息。基于 JWT ,客户端可以使用自己特有的存储来保存 token,不依赖 cookie,所以 JWT 可以适用于任意客户端。并且使用 JWT 进行身份认证,服务端就不用存储用户信息了,这样服务端就是无状态的。因此 JWT 这种身份认证方案,也非常适合 Serverless 应用。
接下来,我就基于 JWT ,带你从 0 到 1实现一个登录注册应用。
从 0 到 1 实现一个登录注册应用
为了方便,我们将基于 Express.js 框架进行开发。关于该应用的所有代码你可以在 Github 上查看:Serverless Authorization。
应用初始化
首先安装 express、body-parser 和 @webserverless/fc-express 等依赖:
|
|
@webserverless/fc-express 的作用是将函数计算的 HTTP 或 API 网关触发器参数转换为 Express.js 框架的参数,这样你就可以很方便在函数计算中使用 Express.js 了。
然后我们初始化一个 template.yaml 模板,该模板定义了 auth-app 这个函数,函数触发器为 HTTP 触发器,支持 GET 和 POST 请求:
|
|
接下来在 index.js 中编写初始化代码,如下所示:
|
|
这段代码主要实现两个功能:
定义了 / 路由,该路由返回了 Hello Serverless! 字符串,我们之后可以用它来测试代码是否正常运行;
使用 @webserverless/fc-express 将函数计算的请求转发给 Express.js 应用,@webserverless/fc-express 可以将函数参数转换为 Express.js 的路由参数。
然后通过 fun deploy 部署应用:
|
|
部署成功后,我们就可以获取到函数计算提供的测试 HTTP Endpoint,然后就可以通过 curl 命令进行测试应用是否正常运行:
|
|
如果你和我一样返回了上述 JSON 字符串,就说明应用正常运行了,接下来我们就可以继续实现注册功能了。
实现注册功能
注册的逻辑是:先获取用户输入的用户名和密码,然后判断用户是否存在,如果不存在就将其存入表格存储数据库。
这里我们使用的数据库是表格存储。 可能你使用的比较多的是 MySQL,之所以选用表格存储而不是 MySQL,是因为表格存储可以直接通过 Restful API 进行读写,并且弹性可扩展,更适合 Serverless 应用。使用表格存储时,你要先创建一个表格存储实例,然后创建一个 user 表。为了方便,我也给你提供了一个创建 user 表的脚本:create-table。
接下来继续编写代码。由于要使用表格存储,所以首先需要安装 tablestore 依赖,然后在 index.js 中初始化表格存储 client:
|
|
// index.js // … const TableStore = require(’tablestore’);
// 初始化 TableStore client
const client = new TableStore.Client({
accessKeyId: ‘
|
|
// 定义 /register 路由,处理注册请求 app.post(’/register’, async (req, res) => { // 从请求体中获取用户信息 const name = req.body.name; const password = req.body.password; const age = req.body.age; // 判断用户是否已经存在 const { row } = await client.getRow({ tableName: “user”, primaryKey: [{ name }] }); if (row.primaryKey) { // 如果用户已存在,则直接返回 return res.json({ success: false, message: ‘用户已存在’ }); } // 创建用户,将用户信息写入到表格存储中 await client.putRow({ tableName: “user”, condition: new TableStore.Condition(TableStore.RowExistenceExpectation.EXPECT_NOT_EXIST, null), primaryKey: [{ name }], attributeColumns: [{ password }, { age }] }); // 返回创建成功 return res.send({ success: true, }); });
|
|
$ curl https://1457216987974698.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/serverless/auth/login
-d “name=jack&password=123456&age=18”
-X POST
{“success”:true}
$ curl https://1457216987974698.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/serverless/auth/login
-d “name=jack&password=123456&age=18”
-X POST
{“success”:false,“message”:“用户已存在”}
|
|
$ npm install jsonwebtoken -S
|
|
// index.js // … const jwt = require(‘jsonwebtoken’) // 设置密钥,非常重要,不能泄露 const SECRET = ’token_secret_xd2dasf19df=’ // … // 定义 /login 路由,用来实现登录功能 app.post(’/login’, async (req, res) => { // 从请求体中获取用户名和密码 const name = req.body.name; const password = req.body.password; // 根据用户名查询用户信息 const { row } = await client.getRow({ tableName: ‘user’, primaryKey: [{ name }] }) // 如果查询结果为空,则直接返回用户不存在 if (!row.primaryKey) { return res.json({ success: false, message: ‘用户不存在’ }) } // 从查询结果中构造用户信息 const user = { name }; row.attributes.forEach(item => user[item.columnName] = item.columnValue); // 判断密码是否正确 if (password !== user.password) { return res.json({ success: false, message: ‘密码错误’ }) } user.password = ‘****’; /
- 生成 token
- jwt.sign() 接受两个参数,一个是传入的对象,一个是自定义的密钥 */ const token = jwt.sign(user, SECRET) return res.json({ success: true, data: { token } }) });
|
|
curl https://1457216987974698.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/serverless/auth-app/login
-d “name=jack&password=123456” \
-X POST
{“success”:true,“data”:{“token”:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiamFjayIsImFnZSI6IjE4IiwicGFzc3dvcmQiOiIqKioqKioiLCJpYXQiOjE2MTA5MDY5MTJ9.qzNZarWbpDUA8-SO6nLd4ffEUR1IVOWKGXiocHV7MkU”}}
使用错误的密码登录
$ curl https://1457216987974698.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/serverless/auth-app/login
-d “name=jack&password=1234561”
-X POST
{“success”:false,“message”:“密码错误”}
|
|
Authorization: Bearer token
|
|
// 定义 /user 路由,获取当前登录的用户信息 app.get(’/user’, (req, res) => { // 从 HTTP 请求头中获取 token 信息 const token = req .headers .authorization .split(’ ‘) .pop(); try { // 验证 token 并解析出用户信息 const user = jwt.verify(token, SECRET); return res.json({ success: true, data: user }) } catch (error) { return res.json({ success: false, data: ‘身份认证失败’ }) } });
|
|
curl https://1457216987974698.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/serverless/auth-app/user
-H “Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiamFjayIsImFnZSI6IjE4IiwicGFzc3dvcmQiOiIqKioqKioiLCJpYXQiOjE2MTA5MDY5MTJ9.qzNZarWbpDUA8-SO6nLd4ffEUR1IVOWKGXiocHV7MkU”
{“success”:true,“data”:{“name”:“jack”,“age”:“18”,“password”:"******",“iat”:1610905944}}
使用错误的 token 进行身份认证
$ curl https://1457216987974698.cn-shanghai.fc.aliyuncs.com/2016-08-15/proxy/serverless/auth-app/user -H “Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiamFjayIsImFnZSI6IjE4IiwicGFzc3dvcmQiOiIqKioqKioiLCJpYXQiOjE2MTA5MDY5MTJ9.qzNZarWbpDUA8-SO6nLd4ffEUR1IVOWKGXiocHV7Mk” {“success”:false,“data”:“身份认证失败”}
|
|
文章作者
上次更新 2025-01-03