serverless和React的组合开发之用户管理 - 后台


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

serverless+React开发之用户管理
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开发之用户管理
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开发之用户管理
serverless+React开发之用户管理

测试login

serverless+React开发之用户管理
serverless+React开发之用户管理

测试validate

serverless+React开发之用户管理
serverless+React开发之用户管理


文章作者: 逻思
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 逻思 !