오늘 수업 제법 재밌었는데, 다 포스팅을 하고 잘 수 있을련지 모르겠다. 떼잉
오늘의 과제는 Redis를 이용하여 로그아웃을 구현하는 것이였다.
아마 이 글을 보고 있는 사람이라면, 딱 나와 비슷한 레벨의 수준일 것이기에
코드를 올려놓고 리뷰하는 식으로 진행을 하려고 한다.
/// 임포트 한 것들 ////
import { Cache } from 'cache-manager';
import { CACHE_MANAGER, Inject } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
///////////////////////////////////////////
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) {}
// 생성자 주입, Redis는 임시저장해주는 곳으로 사용하기 떄문에 흔히 캐시라고 부르는 것이라
// 이름도 캐시로 만들어져있다. (결국은 캐시가 레디스고 레디스가 캐시인 건가 싶기도 하고)
@Mutation(() => String)
async logout(
//
@Context() context: any,
//
// 생성자로 컨텍스트 받아오기, 이렇게 해놔야 프론트에서 쏘는 것들을 잡아올 수 있다.
// rest api라면 req로 받아왔을 듯?
//
) {
const refresh = context.req.rawHeaders
.filter((ele) => {
return ele.match(/refresh/);
})[0]
.split('=')[1];
const access = context.req.rawHeaders
.filter((ele) => {
return ele.match(/Bearer/);
})[0]
.split(' ')[1];
// 위에 토큰을 가져오는 것은 사람마다 차이가 있을 것 같다.
// headers에도 담겨있고
// rawHeaders에도 담겨있었는데
// headers가 객체의 형식으로 되어있어서 가져오기는 더 쉬울 것 같았는데
// 그냥 한번 남들과 다르게 해볼라고 써봤다(?)
// rawHeaders는 배열의 형식을 가지고 있고
// 데이터를 주고 받을 때 순서대로 쌓아서 보내준다고 한다
try {
jwt.verify(access, 'myAccessKey');
jwt.verify(refresh, 'myRefreshKey');
} catch {
throw new UnauthorizedException();
// 이것은 인증되지 않음을 뜻하는 용어(?)다
// 인증이 안되면 너 인증 안댔다. 하고 아래 있는 코드를 실행하지 않고 에러뱉고 멈춘다.
}
// 과제 조건이 try catch문을 쓰라는 것이여서 썼다.
// jwt.verify는 알아서 토큰을 검증을 해주는 작업을 해준다.
// ...가드를 왜만들었나 싶기도 해서 뭔가 어....음....
//
// 결국 jwt.sign은 토큰을 만들어주는 것
// jwt.verify는 토큰을 인증하는 역할을 한다.
//
// 아무튼 앞에는 해당하는 값을 넣고 뒤에는 비밀번호를 넣으면 된다.
// 나는 액세스토큰의 비밀번호는 myAccessKey
// 리프레시토큰의 비밀번호는 myRefreshKey의 형식으로 되어있어서 저렇게 넣었다.
//
//
await this.cacheManager.set(access, 'accessToken', { ttl: 120 });
await this.cacheManager.set(refresh, 'refreshToken', { ttl: 120 });
//
// 요것은 Redis의 문법이다.
// 형태는 앞에는 key, 뒤에는 value 한칸 더는 객체의 형태로 ttl 데이터가 생존해있는 시간을 적어넣을 수 있다.
// 만약에 0으로 쓸 경우 서버를 내리기 전까지 유지가 된다.
// 일단 저건 120초라는 의미다. s나 h같은거 붙여볼까...?
//
return '로그아웃에 성공했습니다';
//
// 확인해봤는데, 레디스에 저장되어있는 시간동안 재인증이 불가능해진다.
// ....왜? verify의 구조를 조금 더 알아봐야할 것 같다.
//
}
throw new UnauthorizedException();
를 사용하면 발생하는 에러
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
// 여기는 검증을 하기 위하여 Redis를 불러올 것이기 때문에 캐시를 불러온다.
// 임포트는 똑같이 해주면 된다.
) {
super({
//
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'myAccessKey',
passReqToCallback: true,
//
// 이것을 사용하지 않으면, 토큰을 복호화했을 경우에 들어있는 값만 나온다.
// 만약 이것을 true로 해놓으면 일반적으로 컨텍스트 중 req가 포함되어있는 것을 받아볼 수 있다.
//
});
}
async validate(req, payload) {
// 위에 passReqTocallback을 설정해놨기때문에
// validate의 함수에 req 값을 받아올 수 있다.
const rawHeadersInAccessToken = req.rawHeaders
.filter((ele) => {
return ele.match(/Bearer/);
})[0]
.split(' ')[1];
// 이건 위에서 쓴 형식과 동일하다
//
const check = await this.cacheManager.get(rawHeadersInAccessToken);
//
// 로그아웃 당시 해당하는 액세스 토큰은 키가 되었고, 'accessToken은 값이 된 상태다.
// 그래서 로그아웃할 때 사용한 액세스 토큰이라면 값이 존재하여 true고
// 새로운 액세스 토큰으로 찾아오면 null이라는 값을 받게 된다.
//
if (check) throw new UnauthorizedException();
//
// 값이 존재하면 로그아웃 했던 것과 같은 토큰이기에 너 인증 안댓음! 에러 발송
// 아니면 코드는 아래로 흘러간다.
const user = await this.userRepository
.createQueryBuilder()
.where({ user_email: payload.user_email })
.getOne();
// 이것은 그냥 내 욕심에 넣어놓은 코드인데
// 나는 유저 id는 난수인 uuid로 생성되는 id고
// 실질적으로 쓰는 아이디는 email인데 쿼리문을 돌릴 때는 고유값인 PK를 가져와서
// 사용하는 것이 좋은 편이라
// 굳이 찾아와서 토큰 정보에 PK를 꽂아놓은 상태다.
return {
user_email: payload.user_email,
user_id: user.user_id,
};
// 아무튼 토큰정보에 이런거 담아서 넘겨줌 복호화하면 이메일이랑 id를 볼 수 있다.
}
설정파일 속으로 들어왔을 때, StrategyOptions에 있는 것들
잘 보면 passReqToCallback을 사용하지 않을 경우인 VerifyCallback에는
req라는 값이 없지만
딱봐도 쓰는 것 같은
VerifyCallbackWithRequest의 인터페이스 속에는 req 타입이 존재하는 것을 확인할 수 있다.
그리고 저기 노란색으로 대문자인 Request는 express에서 땡겨쓰는 것이라
우리가 생각하는 웹 상의 리퀘스트가 된다.
리프레시 토큰도 똑같이 만들어놔서 굳---이 안올려도 될 것 같아서 그냥 패스한다!
Redis 쓰러갈거다.