从零开始开发聊天APP (后端篇二)

107 阅读4分钟

登录密码

上一章可以看到新增用户成功后遗留的问题

  1. 返回的数据中有密码
  2. 密码是明文没有加密

话不多说上代码

安装密码加密、密码验证及jwt相关依赖
yarn add bcryptjs @nestjs/jwt passport @nestjs/passport passport-jwt passport-local
yarn add @types/passport @types/passport-jwt @types/passport-local -D

user/entities/user.entity.ts

添加
...
+ import * as bcrypt from 'bcryptjs';
  
password: {  
    type: String,  
    required: [true, '请输入密码'],  
    minlength: [6, '密码最小长度6个字符'],  
  +  select: false,  
},   
  
+ // 密码加密  
+    userSchema.pre('save', async function (next) {  
+        if (!this.isModified('password')) {  
+            next();  
+        }  
+    this.password = await bcrypt.hashSync(this.password);     

这样查询方法就不会查出密码了

新增auth模块

专门用来处理登录的验证

   nest g res auth
   选择REST API 回车

密码验证这里使用了@nestjs/passport的策略方法 新增 auth/local.strategy.ts

import { compareSync } from 'bcryptjs';
import { PassportStrategy } from '@nestjs/passport';
import { IStrategyOptions, Strategy } from 'passport-local';
import { UserType } from 'src/user/entities/user.entity';
import { HttpException, HttpStatus } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';  
import { Model } from 'mongoose';

export class LocalStorage extends PassportStrategy(Strategy) {
  constructor(
   @InjectModel('Users') private readonly userModel: Model<UserType>,
  ) {
   
    super({
      usernameField: 'username',
      passwordField: 'password',
    } as IStrategyOptions);
  }

  async validate(username: string, password: string) {
    const user = await this.userModel  
    .findOne({  
        username,  
    })  
    .select('+password');  

    if (!user) {  
        throw new HttpException('用户名不正确!', HttpStatus.BAD_REQUEST);  
    }  

    if (!compareSync(password, user.password)) {  
        throw new HttpException('密码错误!', HttpStatus.BAD_REQUEST);  
    }

    return user;
  }
}


auth.module.ts 
注入密码校验策略及表实体
import { Module } from '@nestjs/common';  
import { AuthService } from './auth.service';  
import { AuthController } from './auth.controller'; 
+ import { LocalStorage } from './local.strategy';  
+ import { UserService } from '../user/user.service';
+ import { MongooseModule } from '@nestjs/mongoose';  
+ import userSchema from '../user/entities/user.entity'
  
@Module({  
+ imports: [MongooseModule.forFeature([{ name: 'Users', schema: userSchema }])],  
controllers: [AuthController],  
+ providers: [AuthService,UserService, LocalStorage],  
})  
export class AuthModule {}

新增验证守卫
common下创建守卫文件夹并指令创建local-auth.guard.ts文件
nest g guard common/guard/local-auth

添加如下代码
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

将此守卫添加至auth.controller的登录方法中并使用守卫

import { Body, Controller, Post, UseGuards } from '@nestjs/common';  
import { AuthService } from './auth.service';  
import { CreateAuthDto } from './dto/create-auth.dto';  
+ import { LocalAuthGuard } from '../common/guard/local-auth/local-auth.guard';  
  
@Controller('auth')  
export class AuthController {  
    constructor(private readonly authService: AuthService) {}  
  
    + @Post('login')  
    + @UseGuards(LocalAuthGuard)  
    create(@Body() createAuthDto: CreateAuthDto) {  
      +  return this.authService.login(createAuthDto);  
    }  
}

此时登录密码验证就完成了。

注:新增需要自行排除返回的数据password字段

request_05.png

接下来添加jwt验证

JWT

首先先添加username查询方法 user/user.service.ts

...
async findByUsername(username: string) {  
    return this.userModel.findOne({  
        username,  
    }); 
}
...

auth/auth.module.ts 导入jwt模块

import { Module } from '@nestjs/common';  
import { AuthService } from './auth.service';  
import { AuthController } from './auth.controller'; 
import { LocalStorage } from './local.strategy';  
import { UserService } from '../user/user.service';  
+ import { JwtModule } from '@nestjs/jwt';  
+ import { ConfigService } from '@nestjs/config';  
import { MongooseModule } from '@nestjs/mongoose';  
import userSchema from '../user/entities/user.entity';
  
+ const jwtModule = JwtModule.registerAsync({  
+    inject: [ConfigService],  
+    useFactory: async (configService: ConfigService) => {  
+    return {  
+        secret: configService.get('SECRET', 'my-chat-app'),  
+        signOptions: {  
+            expiresIn: '4h',  
+            },  
+        };  
+    },  
+ });  
  
@Module({  
+ imports: [MongooseModule.forFeature([{ name: 'Users', schema: userSchema }]), jwtModule],  
controllers: [AuthController],  
providers: [AuthService, UserService, LocalStorage],  
})  
export class AuthModule {}

auth/auth.service.ts 添加jwt生成及解密方法

...
constructor(  
    private readonly userService: UserService,  
   + private jwtService: JwtService,  
) {}  
  
+ createToken(user: Partial<User>) {  
+    return this.jwtService.sign(user);  
+ }  
  
+ async decodeToken(token: string) {  
+    return this.jwtService.decode(token.replace('Bearer ', ''));  
+ }

async login(user: Partial<CreateAuthDto>) {  
    const userInfo = await this.userService.findByUsername(user.username);  
    + const token = this.createToken({  
    +    _id: userInfo.id,  
    +    username: userInfo.username,  
    });  
    // if(userInfo.status !== 1){  
    // throw new HttpException('账号已被禁用',500)  
    // }  
    + return { token, user: userInfo };  
    }  
}
...

测试下token获取成功

request_06.png

验证token

在auth模块下同样创建jwt.strategy.ts jwt验证策略 策略代码如下

import { ConfigService } from '@nestjs/config';  
import { UnauthorizedException } from '@nestjs/common';  
import { PassportStrategy } from '@nestjs/passport';  
import { StrategyOptions, Strategy, ExtractJwt } from 'passport-jwt';  
import { InjectModel } from '@nestjs/mongoose';  
import { Model } from 'mongoose';  
import { UserType } from '../user/entities/user.entity';
  
export class JwtStrategy extends PassportStrategy(Strategy) {  
    constructor(  
    @InjectModel('Users') private readonly userModel: Model<UserType>,  
    private readonly configService: ConfigService,  
    ) {  
    super({  
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),  
        secretOrKey: configService.get('SECRET'),  
        passReqToCallback: true,  
        ignoreExpiration: false,  
        } as StrategyOptions);  
    }  

    async validate(req: Request, payload: User) {  
        const existUser = await this.userModel.findOne({  
            _id: payload._id, 
        });  

        if (!existUser) {  
            throw new UnauthorizedException('token不正确');  
        }  
            return existUser;  
        }  
}

将策略注入到user.module中

...
    + import { JwtStrategy } from 'src/auth/jwt.strategy';
    ...
  - providers: [UserService],
  + providers: [UserService,JwtStrategy],
  ...

同样创建jwt验证守卫

nest g guard common/guard/jwt-auth

import { ExecutionContext, Injectable } from '@nestjs/common';  
import { Reflector } from '@nestjs/core';  
import { AuthGuard } from '@nestjs/passport';  
import { Observable } from 'rxjs';  
import { IS_PUBLIC_KEY } from 'src/common/decorator/public.decorator';  // 需创建此装饰器
  
@Injectable()  
export class JwtAuthGuard extends AuthGuard('jwt') {  
    constructor(private reflector: Reflector) {  
    super();  
}  
  
canActivate(  
    context: ExecutionContext,  
    ): boolean | Promise<boolean> | Observable<boolean> {  
        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
        context.getHandler(),  
        context.getClass(),  
        ]);  
    if (isPublic) return true;  
    return super.canActivate(context);  
    }  
}

可以看到上边守卫代码中添加了不需要jwt验证的装饰器所以需要自己创建一个装饰器 nest g decorator common/decorator/public

代码很简单就是设置一个属性方便控制器使用
import { SetMetadata } from '@nestjs/common';  
  
export const IS_PUBLIC_KEY = 'isPublic';  
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

别忘了在app.module中使用刚刚创建的jwt-auth守卫

...
+ import { JwtAuthGuard } from './common/guard/jwt-auth/jwt-auth.guard';
...
providers: [
    AppService,
   + {
   +   provide: APP_GUARD,
   +   useClass: JwtAuthGuard,
   + },
    {
      provide:APP_FILTER,
      useClass: HttpExceptionFilter
    },{
      provide: APP_INTERCEPTOR,
      useClass: ResponseInterceptor
    }
  ],
  ...

测试一下可以看到所有的接口都因为没有token而返回401

request_07.png

因为某些接口可能不需要登录就可以访问所以,刚刚设置的装饰器就用上了

将登录和注册的方法加上这个装饰器
如:
auth/auth.controller.ts
...
+ import { Public } from '../common/decorator/public/public.decorator';
...
@Post('login')  
+ @Public()  
@UseGuards(LocalAuthGuard)  
create(@Body() createAuthDto: CreateAuthDto) {  
    return this.authService.login(createAuthDto);  
}

这样jwt的验证就完成了,这样项目的雏形就差不多了

注:数据库字符长度限制请视情况自行设置这里就不写出来了

接下来该业务代码