使用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。