nestjs入门学习总结(五):实现用户登录注册功能
实现用户注册
- 我们先使用命令创建两个模块,分别是用户模块和授权模块
nest g resource auth
nest g resource user
- 编写用户实体
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';
@Entity('tb_user')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'varchar',
length: 50,
unique: true,
comment: '用户名',
})
username: string;
@Column({
type: 'varchar',
length: 50,
nullable: true,
comment: '昵称',
})
nickname: string;
@Column({
type: 'datetime',
nullable: true,
comment: '生日',
})
birthday: Date;
@Exclude()
@Column({
type: 'varchar',
length: 100,
comment: '密码',
})
password: string;
@Column({
type: 'varchar',
length: 50,
nullable: true,
comment: '邮箱',
})
email: string;
@Column({
name: 'created_at',
type: 'datetime',
// default: () => 'NOW()',
default: () => 'CURRENT_TIMESTAMP',
comment: '创建时间',
})
createdAt: Date;
@Column({
name: 'updated_at',
type: 'datetime',
// default: () => 'NOW()',
default: () => 'CURRENT_TIMESTAMP',
comment: '更新时间',
})
updatedAt: Date;
}
- 编写authService和userService
// auth.service.ts
/**
* 用户注册
* @param createUserDto
* @returns
*/
async register(createUserDto: CreateUserDto) {
const { username, password } = createUserDto;
const existUser = await this.userService.findByUsername(username);
if (existUser) {
throw new BadRequestException('注册用户已存在');
}
const user = {
...createUserDto,
password: encryptPwd(password), // 保存加密后的密码
};
return await this.userService.create(user);
}
// user.service.ts
/**
* 根据用户名查询用户信息
* @param username
* @returns
*/
async findByUsername(username: string) {
return await this.userRepository.findOne({
where: { username },
});
}
/**
* 创建新用户
*/
async create(user: CreateUserDto) {
const { username } = user;
await this.userRepository.save(user);
return await this.userRepository.findOne({
where: { username },
});
}
- 密码加密处理
用户注册密码,需要对密码加密后再保存,这里我们使用bcryptjs这个库来对密码进行加密处理
bcryptjs 是 nodejs 中比较出色的一款处理加盐加密的包。
所谓加盐,就是在加密的基础上再加点“佐料”。这个“佐料”是系统随机生成的一个随机值,并且以随机的方式混在加密之后的密码中。
由于“佐料”是系统随机生成的,相同的原始密码在加入“佐料”之后,都会生成不同的字符串。
这样就大大的增加了破解的难度。
/**
* 加密处理 - 同步方法
* bcryptjs.hashSync(data, salt)
* - data 要加密的数据
* - slat 用于哈希密码的盐。如果指定为数字,则将使用指定的轮数生成盐并将其使用。推荐 10
*/
const hashPassword = bcryptjs.hashSync(password, 10)
/**
* 校验 - 使用同步方法
* bcryptjs.compareSync(data, encrypted)
* - data 要比较的数据, 使用登录时传递过来的密码
* - encrypted 要比较的数据, 使用从数据库中查询出来的加密过的密码
*/
const isOk = bcryptjs.compareSync(password, encryptPassword)
安装使用
yarn add bcryptjs
// 引入 bcryptjs
const bcryptjs = require('bcryptjs')
/**
* 密码加密
* @param password 注册密码
* @returns 加密后的密码
*/
export const encryptPwd = (password) => {
return bcryptjs.hashSync(password, 10);
};
- 编写authController方法路由
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return this.authService.register(createUserDto);
}
- 发起api请求
发起post注册请求 http://localhost:3000/auth/register
请求参数
{
"username": "kerrywu",
"password": "123456"
}
返回结果
{
"code": 0,
"msg": "success",
"data": {
"id": "d20e01eb-3f71-4b63-87f2-78296fadf40b",
"username": "kerrywu17",
"nickname": null,
"birthday": null,
"email": null,
"createdAt": "2023-07-07T18:37:49.000Z",
"updatedAt": "2023-07-07T18:37:49.000Z"
}
}
请求结果过滤,不返回用户密码
- 对需要过滤的字段添加@Exclude()装饰器
import { Exclude } from 'class-transformer';
@Exclude()
@Column({
type: 'varchar',
length: 100,
comment: '密码',
})
password: string;
- 在对应请求的地方标记使用ClassSerializerInterceptor,此时该请求返回的数据中就不会包含password字段
import {
Controller,
Post,
Body,
UseInterceptors,
ClassSerializerInterceptor,
} from '@nestjs/common';
// 绑定拦截器ClassSerializerInterceptor,返回结果过滤掉password
@UseInterceptors(ClassSerializerInterceptor)
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return this.authService.register(createUserDto);
}
3. 以上方式对执行save方法返回不生效,可以findOne查询后再返回
/**
* 创建新用户
*/
async create(user: CreateUserDto) {
const { username } = user;
await this.userRepository.save(user);
return await this.userRepository.findOne({
where: { username },
});
}
实现用户登录
- 编写authService,实现登录逻辑,返回token
// auth.service.ts
/**
* 用户登录
* @param createUserDto
* @returns
*/
async login(loginUserDto: LoginUserDto) {
const existUser = await this.validateUser(loginUserDto);
const token = this.createToken(existUser);
return {
userId: existUser.id,
token,
};
}
/**
* 校验登录用户
* @param user
* @returns
*/
async validateUser(user) {
const { username, password } = user;
const existUser = await this.userService.findByUsername(username);
if (!existUser) {
throw new BadRequestException('用户不存在');
}
const { password: encryptPwd } = existUser;
const isOk = comparePwd(password, encryptPwd);
if (!isOk) {
throw new BadRequestException('登录密码错误');
}
return existUser;
}
/**
* 创建token
* @param user
* @returns
*/
createToken(user) {
const payload = {
id: user.id,
username: user.username,
};
return this.jwtService.sign(payload);
}
- 编写authController,实现登录请求方法
@Post('login')
async login(@Body() loginUserDto: LoginUserDto) {
return this.authService.login(loginUserDto);
}
- 发起post登录请求
http://localhost:3000/auth/login
请求参数
{
"username": "kerrywu9",
"password": "123456"
}
返回结果
{
"code": 0,
"msg": "success",
"data": {
"userId": "4824c8bd-b540-476a-98bf-3fa71d2382d6",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjQ4MjRjOGJkLWI1NDAtNDc2YS05OGJmLTNmYTcxZDIzODJkNiIsInVzZXJuYW1lIjoia2Vycnl3dTkiLCJpYXQiOjE2ODg3NTQwNTksImV4cCI6MTY4ODc2ODQ1OX0.eQsBJBLQZnzmFupQQIfEqCQM8Vt2JKrRhoM7yhPk36w"
}
}
token发放、接口鉴权
我们使用jwt来生成token,生成是一串字符串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjQ4MjRjOGJkLWI1NDAtNDc2YS05OGJmLTNmYTcxZDIzODJkNiIsInVzZXJuYW1lIjoia2Vycnl3dTkiLCJpYXQiOjE2ODg3NTQwNTksImV4cCI6MTY4ODc2ODQ1OX0.eQsBJBLQZnzmFupQQIfEqCQM8Vt2JKrRhoM7yhPk36w
JWT 由三个部分组成,它们通过点号(.)分隔:
- 头部(Header):描述令牌的元数据和签名算法。
- 载荷(Payload):包含声明信息,例如用户身份、权限等。
- 签名(Signature):用于验证令牌的完整性和真实性。
JWT 验证流程
-
接收到 JWT 后,首先将其拆分为头部、载荷和签名三个部分。
-
验证签名:使用事先共享的密钥和签名算法对头部和载荷进行签名验证,确保令牌未被篡改。
-
检查有效期:检查载荷中的声明,例如过期时间(exp)和生效时间(nbf),确保令牌在有效时间范围内。
-
可选的其他验证:根据需要,可能还会验证其他声明,如发行者(iss)、受众(aud)等。
一旦 JWT 通过验证,可以信任其内容,并根据其中的声明执行相应的操作。常见的用途包括用户身份验证、授权访问资源和传递用户信息等。需要注意的是,JWT 的安全性依赖于密钥的保护和正确的实现。同时,由于 JWT 本身包含了用户信息,因此在传输过程中需要采取适当的安全措施,如使用 HTTPS 来保护通信。
jwt生成token
- 安装nest提供的jwt包
yarn add @nestjs/jwt
- 注册JwtModule
在模块中要使用jwt,我们需要先注册JwtModule
// auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
// 注册JwtModule
// const jwtModule = JwtModule.register({
// secret: jwtConstants.secret,
// signOptions: { expiresIn: '60s' }, // 单位:120ms/60s/4h/7d
// });
// 异步方式注册JwtModule
const jwtModule = JwtModule.registerAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '4h' }, // 设置token过期时间,单位:120ms/60s/4h/7d
};
},
});
@Module({
imports: [jwtModule],
providers: [AuthService],
exports: [jwtModule],
controllers: [AuthController],
})
export class AuthModule {}
- 使用jwtService.sign方法生成token
// auth.service.ts
import { JwtService } from '@nestjs/jwt';
constructor(
// 在authService类构造函数中注入JwtService
private readonly jwtService: JwtService,
) {}
/**
* 创建token
* @param user
* @returns
*/
createToken(user) {
const payload = {
id: user.id,
username: user.username,
};
return this.jwtService.sign(payload);
}
接口鉴权
用户登录成功后返回给前端jwt生成的token,前端请求需要鉴权的接口时候携带该token,后端对token进行鉴权验证
Passport是最流行的 node.js 身份验证库,实现token验证,nest提供了对Passport的包装@nestjs/passport模块
- 安装库及对应的ts类型
yarn add @nestjs/passport passport-jwt
yarn add -D @types/passport-jwt @types/passport
- 实现jwt策略
对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递
// jwt.strategy.ts
import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
// import { jwtConstants } from './constants';
/**
* 实现jwt策略
* 对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递
*/
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
// 提供从请求中提取 JWT 的方法。
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// 选择默认的 false 设置,它将确保 JWT 没有过期的责任委托给 Passport 模块。这意味着,如果我们的路由提供了一个过期的 JWT ,请求将被拒绝,并发送 401 Unauthorized 的响应。
ignoreExpiration: false,
// 密钥,不要暴露出去
secretOrKey: configService.get('JWT_SECRET'),
// secretOrKey: jwtConstants.secret,
} as StrategyOptions);
}
// payload {
// id: '4d8b498e-b743-46e4-bdf8-91c25212441b',
// username: 'kerrywu',
// iat: 1688724765,
// exp: 1688739165
// }
async validate(payload: any) {
return { userId: payload.id, username: payload.username };
}
}
- 导入PassportModule模块、注入JwtStrategy
// auth.module.ts
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [jwtModule, PassportModule, UserModule],
providers: [AuthService, JwtStrategy],
exports: [jwtModule],
controllers: [AuthController],
})
- 最后在需要的controller方法中绑定jwt授权守卫
// link.controller.ts
import {
Post,
Body,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@UseGuards(AuthGuard('jwt'))
@Post()
create(@Body() createLinkDto: CreateLinkDto, @Req() req) {
const { userId } = req.user;
return this.linkService.create(userId, createLinkDto);
}
验证通过后,从@Req() req里面我们就可以拿到jwt的用户信息
// 从@Req() req里面可以拿到jwt的用户信息
// user: {
// userId: '4d8b498e-b743-46e4-bdf8-91c25212441b',
// username: 'kerrywu'
// }
报错解决
error TS2559: Type 'string' has no properties in common with type 'FindOneOptions
42 return await this.userRepository.findOne(id);
说是版本问题
https://www.ithao.net/question/192418
您使用的是最新版本的 typeorm 吗?然后将其降级为 typeorm@0.2 因为 @nestjs/typeorm@8.0 可能还不支持最新的。您可以在此处阅读 typeorm@0.3 的更改:https://github.com/typeorm/typeorm/releases/tag/0.3.0
项目源码
代码已经上传到github中,欢迎大家star,持续更新,如有任何问题可以联系我v:sky201208(注明来意)
https://github.com/fozero/cloud-collect-nestjs
参考阅读
- nestjs文档 https://docs.nestjs.com/ 、https://docs.nestjs.cn/
- typeorm文档 https://typeorm.io/ 、https://typeorm.bootcss.com/
关于我&前端&node进阶交流学习群
大家好,我是阿健Kerry,一个有趣且乐于分享的人,前小鹏汽车、货拉拉高级前端工程师,长期专注前端开发,如果你对前端&Node.js 学习进阶感兴趣的话(后续有计划也可以),可以关注我,加我微信【sky201208】,拉你进交流群一起交流、学习共同进步,群内氛围特别好,定期会组织技术分享~
文章出处:https://www.cnblogs.com/fozero
声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。