最近在学习神光大神的《Nest通关秘籍》,接下来的日子里,我将更新一系列的学习笔记。
感兴趣的能够重视我的专栏《Nest 通关秘籍》学习总结。
特别声明:本系列文章现已经过作者本人的答应。 咱们也不要想着白嫖,此笔记仅仅个人学习记录,不是非常完善,如想深入学习能够去购买原版小册,购买链接点击《传送门》。
学完了 mysql
、typeorm
、jwt/session
之后,今日咱们学习做个归纳实战事例:登录注册。
1. 创立数据库
CREATE SCHEMA login_test DEFAULT CHARACTER SET utf8mb4;
2. 创立项目
nest new login-and-register -p pnpm
3. 安装包
安装 typeorm相关的包:
npm install --save @nestjs/typeorm typeorm mysql2
然后在 AppModule 里引进 TypeOrmModule,传入 option:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'xxxxxx',
database: 'login_test',
synchronize: true,
logging: true,
entities: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
4. 创立 CRUD 模块
创立个 user 的 CRUD 模块:
nest g resource user
在AppModule中引进 在User 的 entity:
然后给 User 增加一些属性:
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 50,
comment: '用户名'
})
username: string;
@Column({
length:50,
comment: '暗码'
})
password: string;
@CreateDateColumn({
comment: '创立时刻'
})
createTime: Date;
@UpdateDateColumn({
comment: '更新时刻'
})
updateTime: Date;
}
- id 列是主键、自动递加。
- username 和 password 是用户名和暗码,类型是 VARCHAR(50)。
- createTime 是创立时刻,updateTime 是更新时刻。
- @CreateDateColumn 和 @UpdateDateColumn 都是 datetime 类型。@CreateDateColumn 会在第一次保存的时分设置一个时刻戳,之后一直不变。而 @UpdateDateColumn 则是每次更新都会修正这个时刻戳。
运行项目:
nest start --watch
能够看到打印了 create table 的建表 sql,数据库中也生成了对应的user表和字段。
在 UserModule 引进 TypeOrm.forFeature 动态模块,传入 User 的 entity。
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
这样模块内就能够注入 User 对应的 Repository 了:
然后就能够完成 User 的增删改查。
咱们在 UserController 里增加两个 handler:
import { Body, Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('login')
login(@Body() user: LoginDto) {
return user;
}
@Post('register')
register(@Body() user: RegisterDto) {
return user;
}
}
这儿的LoginDto
和RegisterDto
如下:
// login.dto.ts
export class LoginDto {
username: string;
password: string;
}
// register.dto.ts
export class RegisterDto {
username: string;
password: string;
}
然后咱们在apifox中测验一下:
能够看到,都恳求成功了。
接下来咱们来处理详细的逻辑。首要:
login 和 register 的处理不同:
- register 是把用户信息存到数据库里
- login 是依据 username 和 password 取匹配是否有这个 user
先来完成注册功能。
5. 注册
先在user.controller.ts
中修正register
:
@Post('register')
async register(@Body() user: RegisterDto) {
return await this.userService.register(user);
}
然后在user.service.ts
中增加一个register
办法:
import { Injectable, HttpException, Logger } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import * as crypto from 'crypto';
function md5(str) {
const hash = crypto.createHash('md5');
hash.update(str);
return hash.digest('hex');
}
@Injectable()
export class UserService {
private logger = new Logger();
@InjectRepository(User)
private userRepository: Repository<User>;
async register(user: RegisterDto) {
const foundUser = await this.userRepository.findOneBy({
username: user.username,
});
/**
* 校验用户是否已存在
*/
if (foundUser) {
throw new HttpException('用户已存在', 200);
}
const newUser = new User();
newUser.username = user.username;
newUser.password = md5(user.password);
try {
await this.userRepository.save(newUser);
return '注册成功';
} catch (e) {
this.logger.error(e, UserService);
return '注册失利';
}
}
}
先依据 username 查找下,假如找到了,说明用户已存在,抛一个 HttpException 让 exception filter 处理。
不然,创立 User 目标,调用 userRepository 的 save 办法保存。
password 需要加密,这儿运用 node 内置的 crypto 包来完成。
在apifox里边来测验一下:
能够看到,注册接口恳求成功,而且数据现已保存在数据库中,暗码现已被加密。
以上就是注册逻辑的完成。下面咱们来完成登录接口。
6. 登录
先在user.controller.ts
中修正login
:
@Post('login')
async login(@Body() user: LoginDto) {
const foundUser: LoginDto = await this.userService.login(user);
if (foundUser) {
return 'login success';
} else {
return 'login fail';
}
}
然后在user.service.ts
中增加一个login
办法:
async login(user: LoginDto) {
const foundUser = await this.userRepository.findOneBy({
username: user.username,
});
if (!foundUser) {
throw new HttpException('用户名不存在', 200);
}
if (foundUser.password !== md5(user.password)) {
throw new HttpException('暗码过错', 200);
}
return foundUser;
}
依据用户名查找用户,没找到就抛出用户不存在的 HttpException、找到可是暗码不对就抛出暗码过错的 HttpException。不然,回来找到的用户。
咱们来在apifox中测验一下登录接口:
1.账户过错
2.暗码过错
3.账户暗码都正确
能够看到,接口都回来了正确的成果。
登录成功今后,咱们要把用户信息放在 jwt 或许 session 中一份,这样后边再恳求就知道现已登录了。
7. jwt鉴权
安装 @nestjs/jwt 的包:
pnpm install @nestjs/jwt
在 AppModule 里引进 JwtModule:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { User } from './user/entities/user.entity';
@Module({
imports: [
...
JwtModule.register({
global: true,
secret: 'xiumubai',
signOptions: {
expiresIn: '7d',
},
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
global:true 声明为全局模块,这样就不必每个模块都引进它了,指定加密密钥,token 过期时刻。
在 UserController 里注入 JwtService:
import { Body, Controller, Post, Inject, Res } from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Inject(JwtService)
private jwtService: JwtService;
....
@Post('login')
async login(
@Body() user: LoginDto,
@Res({ passthrough: true }) res: Response,
) {
const foundUser: LoginDto = await this.userService.login(user);
if (foundUser) {
const token = await this.jwtService.signAsync({
user: {
id: foundUser.id,
username: foundUser.username,
},
});
res.setHeader('authorization', 'bearer ' + token);
return 'login success';
} else {
return 'login fail';
}
}
}
用apifox测验一下:
能够看到,token现已拿到了。在这个token中是携带着用户信息的,就是咱们id和username。
现在假如有两个接口,在恳求的时分需要登录,那咱们就需要前端把这个token传过来,然后解析里边的用户信息,看看是否正确。
下面咱们再写一个获取用户信息的接口getUserInfo
:
@Get('getUserInfo')
getUserInfo() {
return 'userinfo';
}
这个接口现在不需要登录就能够恳求。
现在咱们来增加个 Guard 来限制拜访:
nest g guard login --no-spec --flat
import {
CanActivate,
ExecutionContext,
Injectable,
Inject,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import { User } from './entities/user.entity';
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authorization = request.header('authorization') || '';
const bearer = authorization.split(' ');
if (!bearer || bearer.length < 2) {
throw new UnauthorizedException('登录 token 过错');
}
const token = bearer[1];
try {
const info = this.jwtService.verify(token);
(request as any).user = info.user;
return true;
} catch (e) {
throw new UnauthorizedException('登录 token 失效,请从头登录');
}
}
}
取出 authorization 的 header,验证 token 是否有用,token 有用回来 true,无效的话就回来 UnauthorizedException。
把这个 Guard 应用到 getUserInfo
:
import {
Body,
Controller,
Post,
Inject,
Res,
Get,
UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginGuard } from './login.guard';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
...
@Get('getUserInfo')
@UseGuards(LoginGuard)
getUserInfo() {
return 'userinfo';
}
}
接下来,在apifox中再次恳求getUserInfo
接口:
能够看到,这时分没有带token信心,鉴权失利。
当咱们携带一个正确的token再次恳求:
这次恳求成功了。
以上。咱们完成了登录注册的流程。
接下来,咱们需要对参数进行校验。
8. 参数校验
安装 class-validator 和 class-transformer 的包:
pnpm install class-validator class-transformer
然后给 /user/login
和 /user/register
接口增加 ValidationPipe:
import {
Body,
Controller,
Post,
Inject,
Res,
Get,
UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginGuard } from './login.guard';
import { ValidationPipe } from '@nestjs/common/pipes';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Inject(JwtService)
private jwtService: JwtService;
@Post('register')
register(@Body(ValidationPipe) user: RegisterDto) {
return this.userService.register(user);
}
@Post('login')
async login(
@Body(ValidationPipe) user: LoginDto,
@Res({ passthrough: true }) res: Response,
) {
const foundUser: LoginDto = await this.userService.login(user);
if (foundUser) {
const token = await this.jwtService.signAsync({
user: {
id: foundUser.id,
username: foundUser.username,
},
});
res.setHeader('authorization', 'bearer ' + token);
return 'login success';
} else {
return 'login fail';
}
}
@Get('getUserInfo')
@UseGuards(LoginGuard)
getUserInfo() {
return 'userinfo';
}
}
在 dto 里声明参数的束缚:
// register.dto.ts
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
/**
* 注册的时分,用户名暗码不能为空,长度为 6-30,并且限制了不能是特别字符。
*/
export class RegisterDto {
@IsString()
@IsNotEmpty()
@Length(6, 30)
@Matches(/^[a-zA-Z0-9#$%_-]+$/, {
message: '用户名只能是字母、数字或许 #、$、%、_、- 这些字符',
})
username: string;
@IsString()
@IsNotEmpty()
@Length(6, 30)
password: string;
}
// login.dto.ts
import { IsNotEmpty } from 'class-validator';
export class LoginDto {
id?: number;
@IsNotEmpty()
username: string;
@IsNotEmpty()
password: string;
}
在apifox中测验一下:
咱们下来测验注册接口。
1.测验用户名不合法
2.用户名为空
这儿命中了好几种规矩
3.用户名长度
其他情况咱们自行检测。
4.测验暗码为空
5.暗码长度不合法
接下来测验一下登录:
1.用户名为空
2.暗码为空
ValidationPipe 收效了。
至此,咱们就完成了了登录、注册和鉴权的完整功能,并且在后端增加了参数校验。
最终,你能够写一部分前端代码,来跑通登录注册前后端联调的进程。