在开发AWS Web应用的时候,用户管理是非常常见的功能。下面就介绍一下如何使用serverless框架开发AWS后台管理用户注册,登录,使用token保护特殊页面等功能。随后会介绍如何在前台使用React集成这些API。

serverless+React开发之用户管理
创建DynamoDB中的表
首先在serverless.yml中定义一个DynamoDB中的表:
yaml
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.userTableName}
AttributeDefinitions:
- AttributeName: username
AttributeType: S
KeySchema:
- AttributeName: username
KeyType: HASH
BillingMode: PAY_PER_REQUEST
custom:
userTableName: demo-users
同时还需要定义操作数据库的权限:
yaml
provider:
name: aws
region: eu-west-1
runtime: nodejs14.x
lambdaHashingVersion: 20201221
environment:
userTableName: ${self:custom.userTableName}
profile: default
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.userTableName}"
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.userTableName
其实,在profile => environment中定义的所有环境变量在部署Lambda时,serverless框架都会在Lambda中增加相应的定义:

serverless+React开发之用户管理
定义API Key
下面通过使用API Key来保护自己的API:
yaml
provider:
name: aws
region: eu-west-1
runtime: nodejs14.x
lambdaHashingVersion: 20201221
profile: default
apiGateway:
apiKeys:
- value: YOUR_API_KEY # let cloudformation name the key (recommended when setting api key value)
description: Demo Api key description
usagePlan:
quota:
limit: 5000
offset: 2
period: MONTH
throttle:
burstLimit: 200
rateLimit: 100
声明Lambda以及对应的APIGateway endpoints
需要注意的是,所有的API都定义为私有的,必须提供API Key才能访问这些API。这样就能保护API不被滥用。
添加相应的functions
yaml
functions:
registerLambda:
handler: api/user/register.handler
memorySize: 256
events:
- http:
path: register
method: POST
cors: true
private: true
loginLambda:
handler: api/user/login.handler
memorySize: 256
events:
- http:
path: login
method: POST
cors: true
private: true
validateLambda:
handler: api/user/validate.handler
memorySize: 256
events:
- http:
path: validate
method: POST
cors: true
private: true
register功能
首先安装依赖库:
bash
npm i bcryptjs jsonwebtoken
添加文件:api/user/register.js (和user相关的DynamoDB查询等操作应单独封装,这里就不做了。):
Javascript
const bcrypt = require('bcryptjs');
const Responses = require('../common/Responses');
const Dynamo = require('../common/dynamo/Dynamo');
const tableName = process.env.userTableName;
exports.handler = async event => {
const user = JSON.parse(event.body);
if (!user.username || !user.email || !user.password) {
return Responses.httpResponse(401, {
message: 'All fields are required'
})
}
// check if user already exist
const queryParams = {
KeyConditionExpression: 'username = :username',
ExpressionAttributeValues: { ':username': user.username},
TableName: tableName
};
const existingUsers = await Dynamo.query(queryParams).catch(err => {
return Responses.httpResponse(500, {
message: 'error while querying user from DynamoDB'
});
})
if(existingUsers && existingUsers.length>0 && existingUsers[0].username) {
console.log("User already exist.");
return Responses.httpResponse(400, {message: "User already exist."});
}
let params = {
TableName: tableName,
Item: {
"username": user.username,
"password": bcrypt.hashSync(user.password.trim(), 10),
"email": user.email
}
};
const newUser = await Dynamo.insert(params).catch(err => {
console.log('error in dynamo insert', err);
return null;
});
console.log(newUser);
if (!newUser) {
return Responses.httpResponse(400, { message: 'Failed to insert data' });
}
return Responses.httpResponse(200, { newUser });
};
login功能
添加文件:api/user/login.js:
Javascript
const bcrypt = require('bcryptjs');
const Responses = require('../common/Responses');
const auth = require('../common/auth/auth');
const Dynamo = require('../common/dynamo/Dynamo');
const tableName = process.env.userTableName;
exports.handler = async event => {
const user = JSON.parse(event.body);
if (!user.username || !user.password) {
return Responses.httpResponse(401, {
message: 'Both username & password are required'
})
}
const queryParams = {
KeyConditionExpression: 'username = :username',
ExpressionAttributeValues: { ':username': user.username},
TableName: tableName
};
const existingUsers = await Dynamo.query(queryParams).catch(err => {
return Responses.httpResponse(500, {
message: 'error while querying user from DynamoDB'
});
})
if(!existingUsers || existingUsers.length==0 || !existingUsers[0].username) {
return Responses.httpResponse(400, {message: "User doesn't exist."});
}
if (!bcrypt.compareSync(user.password, existingUsers[0].password)) {
return Responses.httpResponse(403, { message: 'password is incorrect'});
}
const userInfo = {
username: existingUsers[0].username,
email: existingUsers[0].email
}
const token = auth.generateToken(userInfo)
const response = {
userInfo,
token: token
}
return Responses.httpResponse(200, response);
};
validate功能
添加文件:api/user/validate.js:
Javascript
const bcrypt = require('bcryptjs');
const auth = require('../common/auth/auth');
const Responses = require('../common/Responses');
exports.handler = async event => {
const user = JSON.parse(event.body);
if (!user || !user.username || !user.token) {
return Responses.httpResponse(401, {
verified: false,
message: 'both username & token are needed in the request body.'
})
}
const validation = auth.validateToken(user.username, user.token);
if (!validation.verified) {
return Responses.httpResponse(401, validation);
}
return Responses.httpResponse(200, {
verified: true,
message: 'success',
user: user
})
};
auth.js
api/common/auth/auth.js:
Javascript
const jwt = require('jsonwebtoken');
const generateToken = userInfo => {
if (!userInfo) {
return null;
}
return jwt.sign(userInfo, process.env.JWT_SECRET, {
expiresIn: '2h'
})
}
const validateToken = (username, token) => {
return jwt.verify(token, process.env.JWT_SECRET, (error, response) => {
if (error) {
return {
verified: false,
message: 'invalid token'
}
}
if (response.username !== username) {
return {
verified: false,
message: 'invalid user'
}
}
return {
verified: true,
message: 'verifed'
}
})
}
module.exports.generateToken = generateToken;
module.exports.validateToken = validateToken;
使用Postman测试这些API
注意需要在header中添加:
json
x-api-key: YOUR_API_KEY
测试register

serverless+React开发之用户管理
测试login

serverless+React开发之用户管理
测试validate

serverless+React开发之用户管理