概要
好多时候在系统开发中懒得实现用户模块的登录/注册功能。直接使用第三方授权用户数据作为数据基础,以达到快速创建用户和登录操作。对用户来说可以更快捷的使用系统,对开发者来说可以直接免掉一部分重复工作,双赢!
开始
实现之前,请确认有上线的微信小程序(不上线没法生成小程序二维码),记录好你小程序的SerectKey
功能实现需要以下几个接口,请先研读说明:
- 小程序接口调用凭据 - 接口调用凭证 / 获取接口调用凭据 (qq.com)
- 小程序登录 - 小程序登录 / 小程序登录 (qq.com)
- 获取小程序二维码 - 小程序码与小程序链接 / 小程序码 / 获取不限制的小程序码 (qq.com)
一般情况上面几个接口够用了,只需要用户的 openid / unionid / 小程序码
1.创建项目
基础创建一个nestjs 项目,具体就不说了(Documentation | NestJS - A progressive Node.js framework)
$ npm i -g @nestjs/cli
$ nest new easy_wechat_auth
2.初始化模块
$ nest g mo auth
$ nest g co auth --no-spec
$ nest g s auth --no-spec
3.引入依赖
开发该功能,我们需要用到以下几个依赖:
HttpModule进行网络请求 HTTP module | NestJS - A progressive Node.js framework
$ npm i --save @nestjs/axios axios
CacheModule进行数据缓存 Caching | NestJS - A progressive Node.js framework
$ npm install @nestjs/cache-manager cache-manager
使用缓存的时候,你喜欢用redis 需要多加依赖方可使用
3.1 系统引入依赖 app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
// 依赖
import { CacheModule } from '@nestjs/cache-manager';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [
CacheModule.register({
isGlobal: true,
}),
HttpModule.register({
timeout: 60e3,
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
4.接口实现
在开发小程序接口之前,需要获取接口调用凭据:
......
private async getAccessToken() {
const APPID_CACHE_KEY = `${this.appId}:token`;
// 读取缓存
let accessToken: string | null =
await this.cacheManager.get(APPID_CACHE_KEY);
if (!accessToken) {
// 获取access_token GET https://api.weixin.qq.com/cgi-bin/token
const { status, data } = await this.httpService
.get('https://api.weixin.qq.com/cgi-bin/token', {
params: {
grant_type: 'client_credential',
appid: this.appId,
secret: this.secret,
},
})
.toPromise();
if (status === 200 && data?.access_token) {
await this.cacheManager.set(
APPID_CACHE_KEY,
data.access_token,
(data.expires_in - 60) * 1e3,
);
}
// 抛出异常
if (data?.errcode) {
throw new BadRequestException(`[${data?.errcode}] ${data?.errmsg}`);
}
accessToken = data?.access_token;
}
this.logger.debug('accessToken', accessToken);
return accessToken;
}
......
实现整个扫码流程需要以下几个接口
- 获取小程序二维码
getQrCode -
Controller
......
@Get('getQrCode')
async getQrCode() {
return await this.authService.getQrCode();
}
......
-
Service
......
/**
* 获取小程序二维码
* @returns
*/
async getQrCode() {
const APPID_CACHE_KEY = `${this.appId}:scene`;
// 构建二维码数据
const accessToken = await this.getAccessToken();
// POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
const { headers, data } = await this.httpService
.post(
`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`,
{
page: '',
scene: 'a=1',
},
{
responseType: 'arraybuffer',
},
)
.toPromise();
// 截取请求响应类型,是错误还是成功
const contentType = headers['content-type'];
if (contentType.includes('application/json')) {
throw new BadRequestException(`[${data?.errcode}] ${data?.errmsg}`);
} else {
const scene = uuidv4();
// 记录缓存等待扫码
await this.cacheManager.set(
APPID_CACHE_KEY + `${scene}`,
{
status: 0, // 0 等待 1 扫码 2 登录
},
5 * 60 * 1e3,
);
this.logger.debug('scene', scene);
return {
scene,
qrcode: `data:image/png;base64,${Buffer.from(data as string).toString('base64')}`,
};
}
}
......
- 检查二维码状态
getCodeStatus/:sceneController
......
@Get('getCodeStatus/:scene')
async getCodeStatus(@Param('scene') scene: string) {
return await this.authService.getCodeStatus(scene);
}
......
-
Service
......
/**
* 获取码状态
* @param scene
* @returns
*/
async getCodeStatus(scene: string) {
const APPID_CACHE_KEY = `${this.appId}:scene`;
// 这里不多做了,直接读取缓存数据吧
return await this.cacheManager.get(APPID_CACHE_KEY + `${scene}`);
}
......
- 扫码确认登录操作
appletLogin/:scene -
Controller
......
@Post('appletLogin/:scene')
async appletLogin(@Param('scene') scene: string, @Body() data: any) {
return await this.authService.appletLogin(scene, data);
}
......
-
Service
......
/**
*
* @param scene
* @param { code }
* @returns
*/
async appletLogin(scene: string, { code }) {
// GET https://api.weixin.qq.com/sns/jscode2session
const accessToken = await this.getAccessToken();
const { data } = await this.httpService
.get(
`https://api.weixin.qq.com/sns/jscode2session?access_token=${accessToken}`,
{
params: {
appid: this.appId,
secret: this.secret,
js_code: code,
grant_type: 'authorization_code',
},
},
)
.toPromise();
// 获取缓存
const APPID_CACHE_KEY = `${this.appId}:scene`;
const qrStatus = await this.getCodeStatus(scene);
if (qrStatus) {
// TODO 这里如果是有用户模块,需要进行用户的比对处理增加或者修改操作,并生成好登录token数据
// 更新扫码状态,标记登录成功
await this.cacheManager.set(
APPID_CACHE_KEY + `${scene}`,
{
status: 2,
// TODO 写入token等数据
...data,
},
5 * 60 * 1e3,
);
}
return data;
}
......
5.扫码操作
生成好小程序码之后,小程序侧扫码进入需要获取二维码携带的登录凭据 scene
Page({
onLoad (query) {
// scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene
const scene = decodeURIComponent(query.scene)
}
})
通过以上onload读取到登录的 scene 凭据,在登录确认时携带 scene 进行登录
- 登录按钮
<button bind:tap="onAuthTap">一键登录</button>
- 登录触发事件
onAuthTap() {
// 缓存读取scene
const scene = wx.getStorageSync('scene') || Date.now()
//微信登录coded
wx.login({
success: ({ code }) => {
console.log('code', code);
// 提交登录操作
wx.request({
method: 'POST',
url: `http://127.0.0.1:3000/auth/appletLogin/${scene}`,
data: {
code,
},
success: (res) => {
// TODO 检查登录结果和写入token等数据
console.log('success', res);
}
})
}
})
}
最后
下面是我简单描绘的一个图,比较简单的概括了下流程
比较基础的功能实现,主要是了解下具体的实现流程
各端的能力:
| 端点 | 能力 |
|---|---|
| Web | 获取二维码信息及其状态确认登录 |
| 小程序 | 扫小程序码提取scene确认登录操作 |
| Api | 提供小程序码接口和确认scene登录状态 |