您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~
前言
本文将从项目搭建到实现从零到一开发一个登录、注册、鉴权的简易版注册登录系统,主要功能和技术选型如下:
- 服务端框架———
Midway.js
; - 密码加密存储———
bcrypt.js
; - 数据库存储———
typeorm
、mysql
; - 登录鉴权———
jwt
;
准备工作
安装mysql环境、建好数据库和一张user表,Dbeaver
或vscode database
用于初始化表字段。
全流程
首先我们创建一个Midway
项目。
npm init midway@latest -y
初始化数据库环境
然后第一步先初始化项目数据库环境,连接mysql
,安装数据库相关依赖包。
npm i @midwayjs/typeorm@3 typeorm mysql2 --save
然后在configuration.ts
中引入typeorm
组件:
// configuration.ts
import { Configuration } from '@midwayjs/core';
import * as orm from '@midwayjs/typeorm';
import { join } from 'path';@Configuration({imports: [// ...orm // 加载 typeorm 组件],importConfigs: [join(__dirname, './config')]
})
export class MainConfiguration {}
然后在config
目录中配置数据库信息:
import { MidwayConfig } from '@midwayjs/core';
import { User } from '../entity/user.entity';export default {// use for cookie sign key, should change to your own and keep securitykeys: '1697424147281_6188',koa: {port: 7001,},typeorm: {dataSource: {default: {/*** 单数据库实例*/type: 'mysql',host: 'localhost',port: 3306,username: 'root',password: 'xxxxx',database: '数据库名',synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据logging: false,// 配置实体模型entities: [User],},},},
} as MidwayConfig;
最后我们还需要一个数据表实例,新建/entity/user.entity.ts
,代码如下:
import { Entity, Column, PrimaryColumn } from 'typeorm';@Entity('userInfo')
export class User {@PrimaryColumn()id: number;@Column()username: string;@Column()password: string;
}
至此关于数据库配置环境已经OK了,项目已经和数据库关联起来了。
登录注册接口
然后新建一个user.controller.ts
和user.service.ts
,controller用于中转服务,service用于存放业务逻辑代码。
user.controller.ts:
import { Inject, Controller, Post } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { UserService } from '../service/user.service';@Controller('/api')
export class APIController {@Inject()ctx: Context;@Inject()userService: UserService;@Post('/register')async register() {const params = this.ctx.request.body as {username: string;password: string;};const user = await this.userService.register(params);return { success: true, message: 'OK', data: user };}@Post('/login')async login() {const params = this.ctx.request.body as {username: string;password: string;};const user = await this.userService.login(params);return { success: true, message: 'OK', data: user };}
}
我们再把service
的雏形给写出来,代码如下:
user.service.ts
import { Provide, httpError, Inject, Context } from '@midwayjs/core';
import { User } from '../entity/user.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
const { v4: uuidv4 } = require('uuid');
import { JwtService } from '@midwayjs/jwt';@Provide()
export class UserService {@InjectEntityModel(User)userModal: Repository<User>;@Inject()jwtService: JwtService;@Inject()ctx: Context;async register(options: { username: string; password: string }) {const { username, password } = options;return {success: true,username,res: '注册成功',};}async login(options: { username: string; password: string }) {const { username, password } = options;return {accessToken: 'xxxx',};}
}
前面已经让项目和数据库关联了,现在需要让接口与数据表绑定起来,我们可以通过InjectEntityModel
在接口服务中注入表信息,来进行增删改查操作,有了操作数据库的能力,就可以开始开发主体逻辑了。
注册
user.service.ts:
/** @Author: 幻澄* @Date: 2023-10-16 10:42:27* @LastEditors: 幻澄* @LastEditTime: 2023-10-16 16:06:07* @FilePath: /midway-jwt/src/service/user.service.ts*/
import { Provide, httpError, Inject, Context } from '@midwayjs/core';
import { User } from '../entity/user.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
const { v4: uuidv4 } = require('uuid');
import { JwtService } from '@midwayjs/jwt';@Provide()
export class UserService {@InjectEntityModel(User)userModal: Repository<User>;@Inject()jwtService: JwtService;@Inject()ctx: Context;async register(options: { username: string; password: string }) {const { username, password } = options;const user = new User();const findRes = await this.userModal.findOne({where: {username,},});if (findRes) return new httpError.BadRequestError('用户已存在');user.id = uuidv4();user.username = username;user.password = password;const res = await this.userModal.save(user);return {success: true,username,res: '注册成功',};}async login(options: { username: string; password: string }) {const { username, password } = options;return {accessToken: 'xxxx',};}
}
注册接口的逻辑如下:
- 获取请求参数
username
、password
; - 去
user
表查重,重复则响应异常; - 生成ID,落库,响应用户信息;
我们通过ApiScout
来模拟请求:
可以看到,注册信息被插入到数据表中了:
相同的入参再调一次,会返回重复用户的异常信息:
这样功能实现了,但是有个安全问题————账号密码应该加密存储在数据表中,因此我们使用bcryptjs
来解决。
npm i bcryptjs --save
安装好之后我们将password
加密一下,改造后的代码如下:
import { Provide, httpError, Inject, Context } from '@midwayjs/core';
import { User } from '../entity/user.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
const { v4: uuidv4 } = require('uuid');
import { JwtService } from '@midwayjs/jwt';
import * as bcryptjs from 'bcryptjs';@Provide()
export class UserService {@InjectEntityModel(User)userModal: Repository<User>;@Inject()jwtService: JwtService;@Inject()ctx: Context;async register(options: { username: string; password: string }) {const { username, password } = options;const user = new User();const findRes = await this.userModal.findOne({where: {username,},});if (findRes) return new httpError.BadRequestError('用户已存在');user.id = uuidv4();user.username = username;user.password = bcryptjs.hashSync(password, 10);const res = await this.userModal.save(user);return {success: true,username,res: '注册成功',};}async login(options: { username: string; password: string }) {const { username, password } = options;return {accessToken: 'xxxx',};}
}
OK,至此,注册接口就开发好了。
登录
有了注册,登录就大差不差了,简易版代码如下:
import { Provide, httpError, Inject, Context } from '@midwayjs/core';
import { User } from '../entity/user.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
const { v4: uuidv4 } = require('uuid');
import { JwtService } from '@midwayjs/jwt';
import * as bcryptjs from 'bcryptjs';@Provide()
export class UserService {@InjectEntityModel(User)userModal: Repository<User>;@Inject()jwtService: JwtService;@Inject()ctx: Context;async register(options: { username: string; password: string }) {const { username, password } = options;const user = new User();const findRes = await this.userModal.findOne({where: {username,},});if (findRes) return new httpError.BadRequestError('用户已存在');user.id = uuidv4();user.username = username;user.password = bcryptjs.hashSync(password, 10);const res = await this.userModal.save(user);console.log(55, res);return {success: true,username,res: '注册成功',};}async login(options: { username: string; password: string }) {const { username, password } = options;const findRes = await this.userModal.findOne({where: {username,},});if (!findRes) return new httpError.BadRequestError('不存在该用户');const compareRes: boolean = bcryptjs.compareSync(password,findRes.password);if (!compareRes) return new httpError.BadRequestError('密码错误');return {success: true};}
}
登录接口主要做了这些事情:
- 获取请求带来的
username
、password
; - 到
user
表查用户名,不存在的话返回异常信息; - 通过
bcryptjs
将登陆的明文密码和注册落库的加密密码比较,如果密码错误,返回异常信息; - 登录完成;
JWT
接下来我们加入鉴权,完善整个登录系统业务流程。
首先安装依赖包:
npm i @midwayjs/jwt --save
然后在configuration.ts
中引入JWT
组件:
import { Configuration, IMidwayContainer } from '@midwayjs/core';
import { IMidwayContainer } from '@midwayjs/core';
import * as jwt from '@midwayjs/jwt';@Configuration({imports: [// ...jwt,],
})
export class MainConfiguration {// ...
}
然后在config
中加入JWT
加密配置信息:
// src/config/config.default.ts
export default {// ...jwt: {secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')expiresIn: '2d', // https://github.com/vercel/ms},
};
配置结束,接下来分两步走:
- 对于登录接口,产出token,返回给前端;
- 对于业务接口,依赖token,做中间件拦截判断鉴权;
先实现第一步,我们只需要在之前的login
接口中增加token
的逻辑即可。
user.service.ts:
export class UserService {@InjectEntityModel(User)userModal: Repository<User>;@Inject()jwtService: JwtService;@Inject()ctx: Context;async login(options: { username: string; password: string }) {const { username, password } = options;const findRes = await this.userModal.findOne({where: {username,},});if (!findRes) return new httpError.BadRequestError('不存在该用户');const compareRes: boolean = bcryptjs.compareSync(password,findRes.password);if (!compareRes) return new httpError.BadRequestError('密码错误');const token = this.jwtService.signSync({ username });return {accessToken: token,};}
}
当登录成功时,基于用户信息生成加密token,并返回给前端,前端保存在请求头的authorization
,接下来每次请求都带给后端。
然后我们封装一个jwt.middleware.ts
鉴权中间件,除了登录注册以外依赖个人账号相关的业务接口,都先走到中间件中,代码如下:
import { Inject, Middleware, httpError } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import { JwtService } from '@midwayjs/jwt';@Middleware()
export class JwtMiddleware {@Inject()jwtService: JwtService;resolve() {return async (ctx: Context, next: NextFunction) => {// 判断下有没有校验信息if (!ctx.headers['authorization']) {throw new httpError.UnauthorizedError();}// 从 header 上获取校验信息const parts = ctx.get('authorization').trim().split(' ');if (parts.length !== 2) {throw new httpError.UnauthorizedError();}const [scheme, token] = parts;if (/^Bearer$/i.test(scheme)) {//jwt.verify方法验证token是否有效await this.jwtService.verify(token, {complete: true,});await next();}};}// 配置忽略认证校验的路由地址public match(ctx: Context): boolean {const ignore = ['/api/login'].includes(ctx.path);return !ignore;}
}
然后在configuration.ts
中引入中间件:
import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info';
import { join } from 'path';
import * as orm from '@midwayjs/typeorm';
import * as jwt from '@midwayjs/jwt';
import { JwtMiddleware } from './middleware/jwt.middleware';@Configuration({imports: [koa,validate,{component: info,enabledEnvironment: ['local'],},orm,jwt,],importConfigs: [join(__dirname, './config')],
})
export class ContainerLifeCycle {@App()app: koa.Application;async onReady() {// add middlewarethis.app.useMiddleware([JwtMiddleware]);}
}
这样除了中间件内部白名单的接口以外,都会先走到JWT
中间件中。
简单测试一下,首先写一个/getShop
接口,不在jwt白名单中,首先前端不带token发一次注册请求:
符合预期,无法访问,被中间件拦下来了。然后我们调一下/login
接口,保存一下token
,再带着去请求一下/getShop
接口:
OK,至此,midway
搭建的登录注册鉴权功能完成。
写在后面
这篇文章对你有帮助的话,非常荣幸。
如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~