Micro Service의 앞단 Gateway에서 모든 호출에 대한 인증/인가를 처리한다. 먼저 JWT기반 인증에 대한 설정을 한다.
- passwort jwt 설정
JWT 처리를 위한 패키지 설치
passport를 통해 JWT를 관리한다.
- userId/password 기반은 passport-local을 사용
- JWT 체크 passport-jwt 사용
- 추가적으로 http security를 위해 express middleware인 helmet 적용
$> yarn add @nestjs/jwt @nestjs/passport passport passport-local passport-jwt helmet
$> yarn add -D @types/passport-local @types/passport-jwt @types/express
Passport Local 적용
- libs/domain/src/lib/auth/strategies 폴더 생성
- local.strategy.ts 파일 생성
// local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException({
error: 'Incorrect username and password'
});
}
return user;
}
}
Username/Password 체크하기
libs/domain/src/lib/auth/ 폴더 생성하고, auth.service.ts 파일을 생성한다.
- validateUser: LocalStrategy에서 호출한다. username/password 로그인 유효성을 login 호출 이전에 체크한다.
- login: validate user인 경우 사용자 정보를 통한 webtoken 생성
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '../entities/user/user.model';
import { UserService } from '../entities/user/user.service';
import { bcryptCompare } from '../utilties/bcrypt.util';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) { }
async validateUser(username: string, pass: string): Promise<any> {
const user: any = await this.userService.findOne(username) || {};
if (user && pass === user.password) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(loginUser: User): Promise<any> {
const user = await this.userService.findOne(loginUser.username);
if (user) {
const payload = { username: user.username, sub: user.id, email: user.email, role: user.role };
return {
access_token: this.jwtService.sign(payload),
};
} else {
throw new UnauthorizedException({
error: 'There is no user'
});
}
}
}
토큰 생성확인은 https://jwt.io/ 에서 할 수 있다.
libs/domain/src/lib/auth/auth.module.ts 파일을 생성하고, config.json파일에 AUTH 프로퍼티를 추가한다.
- JwtModule을 등록한다.
- secret은 반드시 별도의 환경설정 파일에서 관리한다.
- AuthService도 등록한다.
- User 정보를 read하기 위해 EntitiesModule도 imports 에 설정한다.
// apps/gateway/api/src/environments/config.json
{
"HTTP_PORT": 8000,
"AUTH": {
"SECRET": "iot_secret_auth",
"EXPIRED_ON": "1d"
},
...
}
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { GatewayConfiguration, loadConfigJson } from '@rnm/shared';
import { EntitiesModule } from '../entities/entity.module';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
const config: GatewayConfiguration = loadConfigJson();
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: config.AUTH?.SECRET,
signOptions: { expiresIn: config.AUTH?.EXPIRED_ON },
}),
EntitiesModule
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule { }
Passport JWT 적용
로그인이 성공하면 jsonwebtoken 을 생성하고, 이후 request(요청)에 대해 JWT를 체크하는 환경설정을 한다.
- libs/domain/src/lib/auth/strategies/jwt.strategy.ts 파일 생성
- request header의 Bearer Token 체크 => Cookie 사용으로 변경 (master branch소스 참조)
- 확인하는 secret 설정
- AuthModule에 등록한다.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';
import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
// jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// Cookie를 사용한다
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
return request?.cookies?.LOGIN_TOKEN;
}]),
ignoreExpiration: false,
secretOrKey: config?.AUTH?.SECRET
});
}
async validate(user: any): Promise<any> {
// return { id: payload.sub, username: payload.username };
return user;
}
}
Password 암호화
암호화 모듈 설치
$> yarn add bcrypt
$> yarn add -D @types/bcrypt
암호화 유틸리티를 생성한다. libs/domain/src/lib/utilties/bcrypt.util.ts 파일 생성
- 사용자 생성시 패스워드
- 입력 패스워드를 DB의 암호화된 패스워드와 비교한다.
import * as bcrypt from 'bcrypt';
// 사용자의 패스워드 암호화
export const bcryptHash = (plainText: string, saltOrRounds = 10): Promise<string> => {
return bcrypt.hash(plainText, saltOrRounds);
}
// 입력 패스워드와 DB 패스워드 비교
export const bcryptCompare = (plainText: string, hashedMessage: string): Promise<boolean> => {
return bcrypt.compare(plainText, hashedMessage);
}
libs/domain/src/lib/auth/auth.service.ts 의 validateUser에서 암호화된 password를 체크토록 수정한다.
// auth.service.ts
import { bcryptCompare } from '../utilties/bcrypt.util';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) { }
async validateUser(username: string, pass: string): Promise<any> {
const user: any = await this.userService.findOne(username) || {};
// 암호화된 패스워드를 입력 패스워드와 같은지 비교
const isMatch = await bcryptCompare(pass, user.password);
if (user && isMatch) {
const { password, ...result } = user;
return result;
}
return null;
}
...
}
로그인 하기
- apps/gateway/api/src/app/auth/auth.controller.ts 파일을 신규 생성.
- apps/gateway/api/src/app/app.module.ts 설정
- "auth/login" API에 대해 static server에 exclude 설정
- AuthModule imports에 설정
- AuthController 등록
// auth.controller.ts
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { AuthService, LocalAuthGuard, User } from '@rnm/domain';
@Controller()
export class AuthController {
constructor(
private readonly authService: AuthService
) { }
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Body() user: User): Promise<Response> {
return this.authService.login(user);
}
}
// app.module.ts
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, 'public'),
exclude: [
'/auth/*',
'/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
'/dashboard*', '/configuration*', '/back-office*'
],
}),
// ORM
TypeOrmModule.forRoot({
...ormConfigService.getTypeOrmConfig(),
entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
}),
EntitiesModule,
// MicroService
DashboardModule,
// Auth
AuthModule
],
controllers: [
AppController,
AuthController,
UserController
],
providers: [GatewayApiAppService]
})
export class AppModule { }
UseGuard에서 username/password는 LocalAuthGuard를 등록한다. 이를 위해 libs/domain/src/lib/auth/guards/local-auth.guard.ts 파일 생성한다.
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { JwtAuthGuard, User, UserService } from '@rnm/domain';
@Controller('api/gateway/user')
export class UserController {
constructor(
private readonly service: UserService
) { }
@UseGuards(JwtAuthGuard)
@Post()
create(@Body() data: User): Promise<User> {
return this.service.create(data);
}
@UseGuards(JwtAuthGuard)
@Put(':id')
updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
return this.service.updateOne(id, data);
}
@UseGuards(JwtAuthGuard)
@Get()
findAll(): Promise<User[]> {
return this.service.findAll();
}
@UseGuards(JwtAuthGuard)
@Get(':username')
findOne(@Param('username') username: string): Promise<User | undefined> {
return this.service.findOne(username);
}
@UseGuards(JwtAuthGuard)
@Delete(':id')
deleteOne(@Param('id') id: string): Promise<any> {
return this.service.deleteOne(id);
}
}
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') { }
User 생성하는 apps/gateway/api/src/app/user/user.controller.ts 에도 @UseGuards 를 JWT 토큰 체크하는 Guard로 등록한다. 이를 위하여 libs/domain/src/lib/auth/guards/jwt-auth.guard.ts 파일을 생성한다.
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any, context: any, status?: any) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
그리고 apps/gateway/api/src/app/user/user.controller.ts 에 @UseGuards를 "JwtAuthGuard"로 등록한다.
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { JwtAuthGuard, User, UserService } from '@rnm/domain';
@Controller('api/gateway/user')
export class UserController {
constructor(
private readonly service: UserService
) { }
@UseGuards(JwtAuthGuard)
@Post()
create(@Body() data: User): Promise<User> {
return this.service.create(data);
}
@UseGuards(JwtAuthGuard)
@Put(':id')
updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
return this.service.updateOne(id, data);
}
@UseGuards(JwtAuthGuard)
@Get()
findAll(): Promise<User[]> {
return this.service.findAll();
}
@UseGuards(JwtAuthGuard)
@Get(':username')
findOne(@Param('username') username: string): Promise<User | undefined> {
return this.service.findOne(username);
}
@UseGuards(JwtAuthGuard)
@Delete(':id')
deleteOne(@Param('id') id: string): Promise<any> {
return this.service.deleteOne(id);
}
}
Postman으로 테스트 한다.
소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-6
주의: 소스가 계속 업데이트되고 있기에 master branch를 참조해도 된다.
<참조>
- NestJS에 passport 기반 JWT 적용하기
https://docs.nestjs.kr/security/authentication
- passport local 환경설정
http://www.passportjs.org/packages/passport-local/
- passport-jwt 환경설정
https://www.passportjs.org/packages/passport-jwt/
- password 암호화
https://wanago.io/2020/05/25/api-nestjs-authenticating-users-bcrypt-passport-jwt-cookies/
'React > Architecture' 카테고리의 다른 글
[MS-8] NestJS Auth/Role 기능 Gateway에 추가하기 (0) | 2021.09.30 |
---|---|
[MS-7] Refresh Token 설정 (0) | 2021.09.30 |
[MS-5] NestJS에 TypeORM 사용하기 (0) | 2021.09.27 |
[MS-4] Gateway에 Prisma ORM 사용하기 (0) | 2021.09.24 |
[MS-3] Gateway와 Micro Service간 디버깅 환경 구축 (0) | 2021.09.23 |