# 用户注册登录

给大家画了个简易的注册登录流程图,我们就按照这个流程图来进行我们下面的操作

# 短信发送 API

在给客户开发一个信息发送功能的时候,需要涉及到短信的发送,短信发送一般不同的厂商提供的接口不同,处理方式也不太一样,之前用的一个厂商的,提供了一个封装类就很容易发送短息,因此都是基于HTTP协议做的一个数据发送而已,接触阿里云的短信服务器后,发现阿里云还增加了非常多的参数,其中包括一些秘钥和签名的内容。短信发送由于比较敏感原因,大多数应用场景是验证码或者一些固定的信息提醒,因此厂商都要求客户按预定的模板来发送,这样限制了短信的应用场景,只能根据业务进行消息定制了。本篇随笔主要介绍阿里云的短信服务的发送处理。

# 创建阿里云账号

为了访问短信服务,您需要有一个阿里云账号。如果没有,可首先按照如下步骤创建阿里云账号:

# 访问阿里云 官方网站,单击页面上的 免费注册 按钮。

按照屏幕提示完成注册流程并进行实名认证,短信服务只支持实名认证用户使用。为了更好地使用阿里云服务,建议尽快完成实名认证,否则部分阿里云服务将无法使用。具体实名认证流程,请参考 这里 (opens new window)。

# 获取阿里云访问密钥

为了使用短信发送API-node.js-SDK,您必须申请阿里云的访问密钥

阿里云访问秘钥是阿里云为用户使用 API(非控制台)来访问其云资源设计的“安全口令”。您可以用它来签名 API 请求内容以通过服务端的安全验证。

该访问秘钥成对(AccessKeyId 与 AccessKeySecret)生成和使用。每个阿里云用户可以创建多对访问秘钥,且可随时启用(Active)、禁用(Inactive)或者删除已经生成的访问秘钥对。

您可以通过阿里云控制台的 秘钥管理页面 (opens new window) 创建、管理所有的访问秘钥对,且保证它处于“启用”状态。由于访问秘钥是阿里云对 API 请求进行安全验证的关键因子,请妥善保管你的访问秘钥。如果某些秘钥对出现泄漏风险,建议及时删除该秘钥对并生成新的替代秘钥对。

在控制台完成模板与签名的申请,获得调用接口必备的参数

短信签名

根据用户属性来创建符合自身属性的签名信息。企业用户需要上传相关企业资质证明,个人用户需要上传证明个人身份的证明。

短信签名需要审核通过后才可以使用。

短信模板

短信模板,即具体发送的短信内容。

短信模板可以支持验证码、短信通知、推广短信、国际/港澳台消息四种模式。验证码和短信通知,通过变量替换实现个性短信定制。推广短信不支持在模板中添加变量。国际/港澳台消息只能使用国际/港澳台短信模版发送短信。

短信模板需要审核通过后才可以使用。

为了成功发送一条短信通知,您至少需要完成以下步骤

入参列表

参数名称 类型 是否必填 样例取值 参数说明
PhoneNumbers String Y 15000000000 短信接收号码。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式,发送国际/港澳台消息时,接收号码格式为:国际区号+号码,如“85200000000”
SignName String Y 云通信 短信签名
TemplateCode String Y SMS_0000 短信模板ID,发送国际/港澳台消息时,请使用国际/港澳台短信模版
TemplateParam String N {“code”:”1234”,”product”:”ytx”} 短信模板变量替换JSON串,友情提示:如果JSON中需要带换行符,请参照标准的JSON协议要求。
OutId String N abcdefgh 外部流水扩展字段

出参列表

出参名称 出参类型 样例取值 参数说明
RequestId String 8906582E-6722 请求ID
Code String OK 状态码-返回OK代表请求成功,其他错误码详见错误码列表
Message String 请求成功 状态码的描述
BizId String 134523^4351232 发送回执ID,可根据该ID查询具体的发送状态

# 安装插件

这是阿里官方提供的一个 阿里云开发者工具套件(SDK)。让我们不用复杂编程即可访问云服务器、云数据库RDS、云监控等多个阿里云服务。

$ npm install @alicloud/pop-core --save

# 配置参数

接下来将这些参数放到配置 (opens new window)文件中:

config.aliyun = {
    accessKeyId: 'xxxxxxxxxxxxxxxx',   // 秘钥
    accessKeySecret: 'xxxxxxxxxxxx',   // 秘钥
    endpoint: 'https://dysmsapi.aliyuncs.com',
    apiVersion: '2017-05-25',
    sendSms: {
      RegionId: 'cn-hangzhou',
      SignName: 'xxxx',   // 短信签名
      TemplateCode: 'xxxxxx', // 短信模板ID
    },
};

# 注册

有了上面的信息介绍,我们大概了解了短信消息发送的处理规则了。接下来我们就来基于腾讯的 SDK 来发送短信,并完成注册。

阿里的 SDK 发送短信为我们提供了一个demo,我们来简单的改造一下这个方法来供我们使用:

public sendSms(PhoneNumbers: string) {
    const { ctx, app } = this;
    const { accessKeyId, accessKeySecret, endpoint, apiVersion, sendSms } = app.config.aliyun;
    const { RegionId, SignName, TemplateCode } = sendSms;

    const client = new Core({
      accessKeyId,
      accessKeySecret,
      endpoint,
      apiVersion,
    });

    // 生成验证码
    const sendCode = ctx.helper.smsCode();

    const params = {
      RegionId,
      PhoneNumbers,
      SignName,
      TemplateCode,
      TemplateParam: JSON.stringify({ code: sendCode }),
    };

    // 请求方式 POST
    const requestOption = {
      method: 'POST',
    };
}

在上方实例化了 SDK 这个对象, 使用 client 这个实例调用 SendSms (opens new window) 方法传递参数以完成短信发送:

 return new Promise(async (resolve, _reject) => {
  // 调用方法完成短信发送
  await client.request('SendSms', params, requestOption).then(async (result: any) => {
    // 把生成的验证码存到redis中
    await ctx.service.redis.set(PhoneNumbers, sendCode, 60);
    return resolve(result);
  }).catch((ex: any) => {
    resolve(ex.data);
  });
});

操作数据库

我们把关于数据库的操作全部放到 service 里来进行

async register(data: IRegisterData) {
    const { ctx } = this;
    const { code, name, phone, password } = data;
    // 根据号码在redis中取出验证码
    const r_code = await ctx.service.redis.get(phone);
    // 判断存取的验证码和用户输入的是否一致
    if (Number(code) === Number(r_code)) {
      // 把用户信息写入数据库
      return await this.database.create({ name, phone, password });
    }
}

// TypeScript 中的接口,之前已为大家做过介绍哦
interface IRegisterData {
  code: string;  // 验证码
  name: string;	 // 名称
  phone: string;	// 手机号
  password: string;	// 密码
}

定义类型

可以将 GraphQL 的类型系统分为标量类型(Scalar Types,标量类型)和其他高级数据类型,标量类型即可以表示最细粒度数据结构的数据类型,可以和 JavaScript 的原始类型对应。GraphQL 规范目前规定支持的标量类型有:

  • Int :整数,对应 JavaScript 的 Number
  • Float :浮点数,对应 JavaScript 的 Number
  • String :字符串,对应 JavaScript 的 String
  • Boolean :布尔值,对应 JavaScript 的 Boolean
  • ID :ID 值,是一个序列化后值唯一的字符串,可以视作对应 ES 2015 新增的 Symbol
  • 还有一些高级类型,具体大家可以去查询相关文档。我们来定义在我们这个注册登录功能的所需要的一些类型。

类型GraphQL schema 定义。我们根据用户表结构、短信出参列表以及注册所需参数来定义 schema。

# 用户
type User {     // User: 一般是根据数据库表结构
  # 唯一索引
  id: ID
  # 手机号
  phone: String
  # 昵称
  name: String
  # 头像
  avatar: String
}

# 短信
type SendSms {	// SendSms: 可以参照阿里云短信的返回参数
 # 状态码的描述
 Message: String
 # 请求ID
 RequestId: String
 # 发送回执ID
 BizId: String
 # 状态码
 Code: String
}

# 注册
input Register {  // Register: 注册所需要的数据
  # 昵称
  name: String!
  # 手机号
  phone: String!
  # 密码
  password: String!
  # 验证码
  code: Int!
}

目前为止,我们只讨论过将例如枚举和字符串等标量值作为参数传递给字段,但是你也能很容易地传递复杂对象。这在变更(mutation)中特别有用,因为有时候你需要传递一整个对象作为新建对象。输入对象看上去和常规对象一模一样,除了关键字是 input 。

处理逻辑

编写完 schema 之后,graphql 知道有哪些数据了,这个时候我们就要用到 connector 调用 egg 的 service。

// 短信发送
public async sendSms(PhoneNumbers: string) {
    const { ctx } = this;
    return await ctx.service.utils.sendSms(PhoneNumbers);
}

// 注册
public async register(data: IRegisterData) {
    const { ctx } = this;
    return await ctx.service.user.register(data);
}

进行处理

大家还记得在启动一个最简单的 graphql api 服务器一篇时为大家画的一个简易流程图吗,resolver 调用对应的 connector 进行处理。

export default {
  Mutation: {
    // 短信发送
    async sendSms(_root: any, { PhoneNumbers }, { connector }) {
      return await connector.utils.sendSms(PhoneNumbers);
    },

    // 注册
    async register(_root: any, { data }, { connector }) {
      return await connector.user.register(data);
    },
  },
};

查询和变更类型(Types Mutation And Query)

GraphQL 的一个查询请求被称为一份 query 文档(query document),即 GraphQL 服务能够解析验证并执行的一串请求字符串。query 由操作(Operation)和片段(Fragments)组成。一个 query 可以包含多个操作和片段。只有包含操作的 query 才会被 GraphQL 服务执行。但是不包含操作,只有片段的 query 也会被 GraphQL 服务解析验证,这样一份片段就可以在多个 query 文档内使用。

只包含一个操作的 query 可以不带操作名称或者使用简写形式(即 query 关键字加操作名)。query 包含多个操作时,所有操作都必须带上名称。

GraphQL 规范支持两种操作:

  • Query :仅获取数据(fetch)的只读请求
  • Mutation :获取数据后还有写操作的请求

简单来讲, Query 就是获取数据的基本查询;Mutation 支持对数据的增、删、改等操作。

注册肯定是要写入数据库的,发送短信也不仅仅是只读的请求,所以在这里我选择了 Mutation。

type Mutation {
  sendSms(PhoneNumbers: String!) : SendSms
  register(data: Register!): User
}

完成注册

我们把 GraphQL 服务运行起来

然后这时候手机上就来了条短信

紧接着我们去完成注册

至此我们的注册就算是告一段落了,接下来我们来看登录。

# 登录

按照我们的注册流程,我们再来利用登陆这个功能熟悉一下 graphql 的工作流程。

# 操作数据库

我们先在 service (opens new window) 里操作数据库,接收两个参数,登录成功后我们会生成一个 uuid 返回给前端来当做 token使用。

  public async fetchByNamePassword(phone: string, password: string) {
    const { ctx } = this;
    // 生成一个唯一标识
    const uuid = ctx.helper.uuidv1();
   	// 查询当前用户是否存在
    const user = await this.database.findOne({
      where: {
        phone,
        password,
      },
    });

    if (!user) return null;
    const result = JSON.stringify(user);
    // 存到redis中
    await ctx.service.redis.set(uuid, result, 3600 * 24);
    return uuid;
  }

# 定义类型

我们定义一个输入类型,你可以理解为这是前端传递的数据。

input Authentication {
  # 手机号
  phone: String!
  # 密码
  password: String!
}

# 处理逻辑

connector 调用 egg 的 service。

async fetchByNamePassword(phone: string, password: string) {
    const { ctx } = this;
    return await ctx.service.user.fetchByNamePassword(phone, password);
}

# 进行处理

resolver 调用对应的 connector 进行处理。

export default {
  Query: {
    async login(_root: any, { data }, { connector }) {
      const { phone, password } = data;
      return await connector.user.fetchByNamePassword(phone, password);
    },
  },
};

# 查询类型(Query Types)

Query 对应 读

type Query {
  login(data: Authentication): String!
}

完成登录

阅读全文