passport, jwt
로그인 인증을 위해 passport 라이브러리를 사용한다. 인증이 확인되면 헤더의 인증토큰을 위해 jwt를 발행한다. 요청에서 유효한 jwt인지 확인한다.
passport 패키지를 설치한다..
npm install --save-dev @types/passport-local
npm install --save @nestjs/passport passport passport-local
auth 모듈과 서비스를 생성한다.
nest g module auth
nest g service auth
UserService에 유저를 확인하는 메서드 하나를 만들어준다.
// user.service.ts
export type User = any;
@Injectable()
export class UserService {
private readonly users = [
{
userId: 1,
username: 'john',
password: 'j',
},
{
userId: 2,
username: 'maria',
password: 'm',
}
];
async findOne(
username: string
): Promise<User | undefined> {
console.log('findOne');
return this.users.find(user => user.username === username);
}
}
UserService를 export 해줘서 다른 모듈에서 사용할 수 있게 해준다.
// user.module.ts
@Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
AuthService에 validateUser()를 생성하고 유저 확인 로직을 넣어준다. 이 메서드는 Passport local strategy 에서 호출할 것이다.
실제 어플리케이션에서 비밀번호는 bcrypt 같은 라이브러리를 이용해 해싱된 값으로 처리해야한다.
// auth.service.ts
import { UserService } from 'src/user/user.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
) {}
async validateUser(
username: string,
pass: string
): Promise<any> {
console.log('AuthService validateUser()');
const user = await this.userService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
AuthService를 export 해준다.
// auth.module.ts
@Module({
imports: [UserModule],
providers: [AuthService],
})
export class AuthModule {}
local.strategy.ts 파일을 만들고 Passport local strategy 를 구현한다.
super()는 Passport local strategy에서 옵션 개체를 사용자 지정하기 위해 사용된다. 따로 지정할 것이 없으면 그냥 둔다.
ex) username이 아닌 email 을 검사하고 싶다면 super({ usernameField: 'email' }) 을 추가한다.
// local.strategy.ts
import { AuthService } from './auth.service';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { Injectable, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
console.log(user);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
AuthModule에 PassportModule을 import하고 providers 에 LocalStrategy를 등록한다.
// auth.module.ts
import { LocalStrategy } from './local.strategy';
import { UserModule } from './../user/user.module';
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [UserModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
AppController 에 가드를 적용시킨다.
// app.controller.ts
import { Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@UseGuards(AuthGuard('local')) // @nestjs/passport
@Post('auth/login')
async login(
@Req() req
) {
return req.user;
}
}
body에 username과 password를 제대로 적고 해당 경로로 로그인을 시도하면 user 정보가 출력된다.
다만, 전략이름을 직접 전달하는 것보다 클래스를 만들어 주는 것을 권유하고 있다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport/dist';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
// @UseGuards(AuthGuard('local')) // @nestjs/passport
@UseGuards(LocalAuthGuard) // 위처럼 전략을 직접 적지 않고 클래스로 따로 빼내놓는 것이 좋음
@Post('auth/login')
async login(
@Req() req
) {
return req.user;
}
각각 콘솔을 찍어보면 어떤 흐름으로 되는지 알 수 있다.
http://localhost:3333/auth/login -> 가드 : LocalStrategy ->
LocalStrategy의 validate() 메서드 내부 authService.validateUser()
-> username, password 검사 -> 일치하는 유저 있으면 유저 정보 리턴
JWT를 발행하기 위해 패키지를 설치한다.
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt
AuthService에 메서드를 추가한다.
// auth.service.ts
import { UserService } from 'src/user/user.service';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(
username: string,
pass: string
): Promise<any> {
console.log('AuthService validateUser()');
const user = await this.userService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
// jwtService
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
// jwt 발행
access_token: this.jwtService.sign(payload),
}
}
}
AuthModule에 의존성을 주입한다.
// auth.module.ts
import { LocalStrategy } from './local.strategy';
import { UserModule } from './../user/user.module';
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: 'secret key',
signOptions: {
expiresIn: '60s', // 토큰 만료기간
},
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
AppController의 로그인 메서드를 AuthService의 login()을 리턴한다.
// app.controller.ts
import { AuthService } from './auth/auth.service';
import { Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LocalAuthGuard } from './auth/local-auth.guard';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly authService: AuthService,
) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(
@Req() req
) {
return this.authService.login(req.user);
}
}
API 를 실행하면 토큰이 발급된다.
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG4iLCJzdWIiOjEsImlhdCI6MTY3NTk1NzM0NSwiZXhwIjoxNjc1OTU3NDA1fQ.o--wEa0TPLreHXEhCLTguKtiixDSgwmsacyrOrPHFu0"
}
jwt.strategy.ts 파일을 만들고 passport-jwt 전략을 구현한다.
생성자 super()를 통해 옵션을 넣어준다.
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // jwt 추출방법 : Authorization 헤더의 Bearer 토큰
ignoreExpiration: false, //true : Passport에 토큰 검증을 위임하지 않고 직접 검증, false : Passport에 검증 위임
secretOrKey: 'secret key', // 토큰 대칭 비밀키
});
}
async validate(payload: any) {
console.log('JwtStrategy validate()');
return { userId: payload.sub, username: payload.username };
}
}
AuthModule에 Jwt전략을 추가한다.
// auth.module.ts
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: 'secret key',
signOptions: {
expiresIn: '60s', // 토큰 만료기간
},
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy], // new
exports: [AuthService],
})
export class AuthModule {}
jwt가드 클래스를 만든다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
AppController에 유저 정보를 가져오는 메서드를 만들고 jwt가드를 등록해준다.
// app.controller.ts
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { AuthService } from './auth/auth.service';
import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LocalAuthGuard } from './auth/local-auth.guard';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly authService: AuthService,
) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(
@Req() req
) {
console.log('AppController login');
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('user-info')
getUserInfo(
@Req() req
) {
return this.userService.findOne(req.user['username']);
}
}
username과 password를 통해 등록된 유저를 확인하는 로그인 후 발행된 토큰을 이용해서 유저 정보를 조회해본다.
유저 정보와 함께 토큰을 주기 위해 AuthService와 AppController를 수정한다.
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(
username: string,
pass: string
): Promise<any> {
console.log('AuthService validateUser()');
const user = await this.userService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
const accessToken = await this.jwtService.sign(result); // 토큰 발생
result['token'] = accessToken; // 토큰 추가
return result;
}
return null;
}
}
// app.controller.ts
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(
@Req() req
) {
return req.user;
}
@UseGuards(JwtAuthGuard)
@Get('user-info')
getUserInfo(
@Req() req
) {
return this.userService.findOne(req.user['username']);
}
}
auth/login 으로 요청하면 유저 정보와 토큰을 응답해준다.
이 토큰을 가지고 user-info 를 요청한다.
헤더에 Authorization : Bearer '토큰값' 을 넣어야 한다.
{
"userId": 2,
"username": "maria",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsInVzZXJuYW1lIjoibWFyaWEiLCJpYXQiOjE2NzU5NjAzNTYsImV4cCI6MTY3NTk2MDQxNn0.iL3eNYlngFV6ODdcDW09__2dek3ssyfq8rlwR2_P3wc"
}
{
"userId": 2,
"username": "maria",
"password": "m"
}
만약 유효하지 않은 토큰이라면 오류가 뜬다.
{
"statusCode": 401,
"message": "Unauthorized"
}