Skip to content

使用nest框架做服务端认证

认证(Authentication)

认证是确认某人或某物的身份或真实性的过程。在计算机领域,认证通常指的是确认用户身份以获得访问权限。

INFO

认证是大多数应用中的重要环节。客户端通过用户名密码认证,经过服务端认证成功,服务器签发JWT,后续客户端发送请求时在请求头中携带access_token,可以从一些受保护的路由中获取资源。

auth.controller

ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignInDto } from './dto/sign-in.dto';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  signIn(@Body() signInDto: SignInDto) {
    return this.authService.signIn(signInDto.name, signInDto.password);
  }
}

接收到客户端的请求,收集到提交的用户名密码,到authService中校验。

auth.service

ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private userService: UserService, private jwtService: JwtService) {}

  /**
   * 登录
   * @param username
   * @param pass
   * @returns
   */
  async signIn(username: string, pass: string): Promise<any> {
    const param = { name: username };
    const user = await this.userService.findOne(param);
    if (user !== null) {
      const valid = await bcrypt.compare(pass, user.password);
      if (!valid) throw new UnauthorizedException('密码错误');
    } else {
      throw new UnauthorizedException('用户不存在');
    }

    const payload = { id: user.id, name: user.name };
    return {
      access_token: await this.jwtService.signAsync(payload),
    };
  }
}

用户信息通过校验后jwtService签发token。jwtService是官方对jwt做的一个封装,在下面的场景中会经常用到它:

  • Authorization (用户通过token访问有权限控制的资源)
  • Single Sign On(在不同域名之前以很小的开销实现单点登录)

auth.module

ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      global: true,
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

这里把JwtModule注册成一个全局的模块,在其他地方使用的时候就不需要引入。可以看到签发的token会在60s后过期。

接下来封装一个auth.guard来是签发的token发挥作用。

auth.guard

ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { IS_PUBLIC_KEY } from './auth.decorator';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 验证token
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: jwtConstants.secret,
      });
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException('token expired');
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

可以看到上面的方法会从request.headers.authorization中提取出token来进行校验。

在controller中使用auth.guard

auth.controller

ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignInDto } from './dto/sign-in.dto';
import { AuthGuard } from './auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  signIn(@Body() signInDto: SignInDto) {
    return this.authService.signIn(signInDto.name, signInDto.password);
  }

  @UseGuards(AuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

这样GET /profile 就成了一个受保护的路由。使用curl来测试一下:

$ # GET /profile
$ curl http://localhost:3000/auth/profile
{"statusCode":401,"message":"Unauthorized"}

$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."}

$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/auth/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
{"sub":1,"username":"john","iat":...,"exp":...}

TIP

到此我们已经使用jwt控制了资源的访问,但是有个问题这个access_token过期时间很短,一分钟后就需要重新登录获取新的token,这样用户体验太差了,下一篇讨论如何让获得无感刷新token。

参考链接

nestjs authentication

Released under the MIT License.