사용하는 것
nest
redis
저번에 한 것
가입시 db에 유저정보를 넣어줬다.
jwt access token을 쿠키에 넣어줬다.
이번에 할것
프로젝트에 redis를 설정한다.
jwt refresh token을 생성한다.
jwt refresh token을 검증한다.
왜 redis를 썻는가.
UserEntity에 이전에 설정한 postgresql에 refresh토큰 col을 적용한다면 코드작성이 복잡해지며, 투머치하다고 생각한다.
따로 테이블을 만드는 것 역시 투머치하다고 생각한다.
postgresql엔 상대적으로 복잡한 쿼리나 모델이 필요한 데이터를 넣어줄 것이다.
단순 key:value로만 넣어줄 것이므로 redis가 적당하다고 생각한다.
하지만 이 경우 데이터가 손상되지 않게 주의해야하고 백업이 필요하다.
redis docker
docker pull redis
터미널에서 redis이미지를 받아온다.
redis:
image: redis
command: redis-server --requirepass 레디스비밀번호 --port 6379
container_name:
hostname:
labels:
- "name=redis"
- "mode=standalone"
volumes:
- ./redis/data:/data
docker compose를 작성해준다.
app.module작성
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { redisConfig } from './config/redis.config';
@Module({
imports: [
CacheModule.registerAsync(redisConfig),
],
})
export class AppModule {}
cachemodule에 redis설정을 넣어준다.
export const redisConfig: CacheModuleAsyncOptions = {
isGlobal: true,
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
const store = await redisStore({
socket: {
host: ,
port: ,
},
password: ,
});
return {
store: () => store,
};
},
inject: [ConfigService],
};
redis설정은 자유롭게.
환경변수 적용을 위해 AsyncOption을 registerAsync에 넣어줬다.
jwt refresh token module
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { JwtRefreshStrategy } from './jwt-refresh.strategy';
import { JwtRefreshService } from './jwt-refresh.service';
import { UsersModule } from 'src/users/users.module';
@Module({
imports: [
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
return {
signOptions: { expiresIn: '30d' },
secret: configService.get<string>('JWT_REFRESH_SECRET'),
};
},
inject: [ConfigService],
}),
],
providers: [JwtRefreshStrategy, JwtRefreshService],
exports: [JwtRefreshService],
})
export class JwtRefreshModule {}
accesstoken과 동일하게 module에서 jwt refresh token관련 정보를 config해준다.
strategy작성
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtRefreshService } from './jwt-refresh.service';
import { PayLoadType } from '../type';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
Strategy,
'jwt-refresh',
) {
constructor(
configService: ConfigService,
private jwtRefreshService: JwtRefreshService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 클라이언트는 authorization: Bearer blabla 형식 헤더를 보낸다.
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
passReqToCallback: true, // 추출되고 디코딩된 토큰이 아닌 쌩 토큰을 이용해야한다.
});
}
async validate(req: Request, payload: PayLoadType) {
const authorization = req.headers?.authorization;
if (!authorization || authorization.split(' ').length < 2) {
throw new HttpException('인증 실패', HttpStatus.UNAUTHORIZED);
}
const refreshToken = authorization.split(' ')[1];
if (!this.jwtRefreshService.matchRefreshToken(payload.key, refreshToken)) {
throw new HttpException('인증 실패', HttpStatus.UNAUTHORIZED);
}
return payload;
}
}
리프레시 토큰의 검증은, 요청보낸 유저의 쿠키의 리프레시토큰과 db(여기선 redis)의 유저에 대응하는 리프레시토큰이 맞는지 추가로 검증한다.
passReqToCallback: true로 설정해 날것의 req를 받아서 raw refreshtoken을 추출해야한다.
service 작성
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class JwtRefreshService {
constructor(private jwtService: JwtService) {}
login(payload: PayLoadType) {
return {
refreshToken: this.jwtService.sign(payload),
};
}
}
JwtRefreshService에 refresh토큰 검증과 생성로직작성
import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PayLoadType } from '../type';
import { CACHE_MANAGER, CacheStore } from '@nestjs/cache-manager';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class JwtRefreshService {
constructor(
private jwtService: JwtService,
@Inject(CACHE_MANAGER) private cacheManager: CacheStore,
) {}
login(payload: PayLoadType) {
return {
refreshToken: this.jwtService.sign(payload),
};
}
async setRefreshToken(key: string, refreshToken: string) {
const hashedToken = await bcrypt.hash(refreshToken, 5);
return await this.cacheManager.set<string>(key, hashedToken);
}
async matchRefreshToken(key: string, refreshToken: string): Promise<boolean> {
const hashedToken = await this.cacheManager.get<string>(key);
return await bcrypt.compare(refreshToken, hashedToken);
}
// decodeRefreshToken(refreshToken: string) {
// return this.jwtService.decode(refreshToken);
// }
}
리프레시 토큰을 redis에 넣어준다. 따라서 보안을 강화하기 위해 bcrypt로 리프레시 토큰을 추가로 암호화한다.
https://nayounsang1.tistory.com/71 해당 이슈로 인해 bcryptjs모듈 사용을 추천한다.
우린 app.module에 넣은 redis설정으로 인해, cacheManager은 자동으로 우리가 설정한 redis에 데이터를 집어넣고 가져온다.
jwtrefreshmodule에 리프레시토큰 관련 설정을 넣어줬다. 따라서 jwtrefreshservice에서 작성하면 우린 쉽게 jwt토큰을 생성할 수 있다. 구조상으로도 리프레시관련된 동작을 리프레시토큰서비스에 넣어줘 깔끔해진다.
가입시 리프레시토큰 발급 및 설정
@Get('callback')
@UseGuards(AuthGuard('github'))
async githubAuthCallback(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
): Promise<void> {
const user = { key: req.user as string };
const { accessToken } = this.jwtAccessService.login(user);
const { refreshToken } = this.jwtRefreshService.login(user);
res.cookie('access_token', accessToken);
res.cookie('refresh_token', refreshToken);
await this.jwtRefreshService.setRefreshToken(user.key, refreshToken);
res.redirect('foobar');
}
payload를 이용해 refreshToken을 생성한다.
cookie에 refreshToken을 넣어준다.
github login시 refreshToken을 redis에 넣어준다.
이제 우린 github로그인을 하면 유저정보를 가져와 db에 저장하고 access token과 refresh token을 쿠키에 받아올 수 있다.
'개발' 카테고리의 다른 글
내가 개발중인 nestjs 인증 structure (1) | 2024.02.27 |
---|---|
react-hook-form usecontroller 뜯어보기 (3) | 2024.02.18 |
github oauth2 클라이언트와 서버 구현해보기 -2 (1) | 2024.02.15 |
bcrypt invalid ELF header 에러 해결하기 (0) | 2024.02.14 |
github oauth2 클라이언트와 서버 구현해보기 -1 (0) | 2024.02.12 |