JWT(JSON Web Token)의 구조와 생성

bin-lee·2022년 1월 15일
0

JWT

JWT(JSON Web Token)은 JSON 형식으로 사용자에 대한 정보를 저장하는 웹 토큰이다. 디지털 서명이 되어 있으므로 안전하고 신뢰할 수 있다. 정보를 안전하게 전달할 때, 유저의 권한을 체크할 때 사용하는 유용한 모듈이다.

보통 로그인을 할 때 많이 사용된다. 로그인한 고유 유저를 위한 JWT 토큰을 생성하여 안전하게 로그인하고 또 권한을 확인하는 용도다.


JWT의 구조


Header, Payload, Signiture 세 파트가 합쳐진 구조를 띈다.

  1. Header
    토큰에 대한 메타 데이터를 포함하고 있다. 메타 데이터란 타입, 해싱 알고리즘, SHA256 등을 뜻한다.

  2. Payload
    실제적으로 담긴 데이터다. 유저 정보, 만료 기간 등이 담겨 있다. 필요한 데이터만 넣는 것이 중요하나, 보안의 위험 때문에 중요한 데이터 삽입은 지양하는 게 좋다.

  3. Signiture
    토큰이 보낸 사람에 의해 시크릿키로 서명되었으며 조작되지 않았음을 확인하는 데 사용된다. 위 사진에서 볼 수 있듯이 base64로 인코딩된 HeaderPayload + 서명 알고리즘 + 시크릿키(혹은 퍼블릭키)가 조합되어 생성된다.

이 세 세그먼트가 모여서 하나의 JWT 토큰이 된다.


JWT의 흐름


JWT 설치

Nest 환경에서 JWT의 인증 처리를 더 간편하게 사용하기 위해 Passport 모듈도 같이 설치해 준다. 네 개 모두 설치.

npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

npm install --save @nestjs/passport passport
npm install --save-dev @types/passport-local

JWT 사용 - 로그인 과정

(1) JWT 모듈 등록

새로 생성한 auth.module에서 사용하기 때문에 imports에 JwtModule을 넣어 준다.

JwtModule.register({
  secret: `${process.env.JWT_SECRET}`,
  signOptions: { expiresIn: '1y' },
})

secret은 노출되면 안 되는 시크릿 키라서 환경 변수로 따로 저장했다. 이렇게 환경 변수를 이용할 때는 꼭 ConfigModule.forRoot()를 imports 해 줘야 한다. imports 없이 사용하면 찾을 수 없어서 오류가 발생한다.

signOptionsexpiresIn는 토큰의 유효 기간이다. 나는 우선 1년으로 지정했다.


(2) passport 모듈 등록

다음으로는 auth.module.ts에 passport 모듈을 등록한다. 마찬가지로 imports 해 주면 된다.

PassportModule.register({ defaultStrategy: 'jwt', session: false })

session을 사용하지 않을 거라서 false로 두었다.


(3) jwtService 주입

추후 서비스단에서 토큰을 생성하려면 jwtService가 필요하다. jwtService는 별도의 서비스단을 만들지 않고, 사용할 서비스단에 바로 주입하는 방식으로 사용 가능하다. 로그인 서비스를 구현할 Auth.service.ts 파일에 jwtService를 주입했다.

@Injectable()
export class AuthService {
  constructor(
    private readonly userRepository: UserRepository,
    private jwtService: JwtService,
  ) {}

(4) 토큰 생성

로그인에 필요한 유효성 검사를 거쳐 로그인에 성공하면 유저 토큰이 생성되도록 한다.

  const payload = { id: id, sub: user._id };
  return {
    token: this.jwtService.sign(payload),
  };

토큰의 payload에는 유니크값인 id와 토큰의 제목(sub)으로 user._id 고유값을 담았다. sign메서드로 payload를 담아서 토큰을 생성한다. 로그인이 성공적으로 완료되면 클라이언트는 이 토큰을 보관한다.


JWT 사용 - 인증 과정

(1) 유효한 토큰인지 확인

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly userRepository: UserRepository) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: `${process.env.JWT_SECRET}`,
      ignoreExpiration: false,
    });
  }

jwt.strategy.ts를 생성하고 다른 곳에서도 인증 과정을 사용할 수 있게끔 @Injectable() 데코레이터를 달아 준다. userRepository를 주입받는 걸 확인할 수 있는데, 인증 요청이 날아온 토큰의 payload 데이터와 DB의 데이터가 상응하는지 확인하고 가져오기 위함이다.

super 안의 과정은 토큰이 Bearer 타입으로 넘어오는 걸 가져와서 시크릿 키로 유효한지 확인하는 것이다. 이때의 시크릿 키는 토큰 생성 시에 사용한 시크릿 키와 그 값이 동일해야 한다. 생성과 인증으로 용도만 다를 뿐이지 시크릿 키 자체가 달라지는 게 아니다.


(2) 데이터베이스에서 값 가지고 오기

위의 과정으로 유효한 토큰이라는 게 확인되면 validate 메서드에서 데이터베이스 검증 과정을 거쳐 boolean 타입으로 리턴한다.

  async validate(payload: Payload) {
    const user = await this.userRepository.findUserByIdWithoutPwd(payload.sub);

    if (user) {
      return user; // request.user안에 user가 들어감
    } else {
      throw new UnauthorizedException('접근 오류입니다');
    }
  }

payload에는 유저의 id 값이 id로, _id 값이 sub로 저장되어 있다. 둘 다 유니크하기 때문에 어떤 값을 이용하든 단 한 명의 유저를 뽑아낼 수 있다. 데이터베이스와 연결된 userRepository에서 payload의 sub를 이용해 유저를 찾은 후, 유저가 존재하면 그 정보를 user 객체에 담는다. 객체를 리턴해 주면 request 객체 안에서 user를 사용할 수 있게 된다.


(3) 모듈에 담기

위에서 만든 클래스 JwtStrategy를 사용하기 위해 providers에 담아 준다. 다른 곳에서도 사용해야 하므로 exports도 해 준다.

@Module({
  imports: [
    ConfigModule.forRoot(),
    // jwt 모듈 등록
    JwtModule.register({
      secret: `${process.env.JWT_SECRET}`,
      signOptions: { expiresIn: '1y' },
    }),
    // passport 모듈 등록
    PassportModule.register({ defaultStrategy: 'jwt', session: false }),

    // 두 모듈이 서로를 import하고 있기 때문에 순환 모듈 문제를 아래처럼 해결
    forwardRef(() => UserModule),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService, JwtStrategy],
})
export class AuthModule {}

(4) user 객체 사용하기

나는 현재 로그인된 유저가 누구인지 알아보기 위해 user.controller.ts 파일에서 로그인 유저 인증을 필요로 했다. 현재 로그인된 유저는 request 객체에 담겼다고 (2)에서 말했는데, 이 유저를 사용하기 위해 @UseGuards(JwtAuthGuard)를 이용한다.

가드는 인증 미들웨어로, 지정된 경로로 통과할 수 있는 사람과 허용되지 않는 사람을 서버에 알려 준다. 또한 가드가 실행되면 JwtStrategy를 자동으로 실행시켜 인증이 진행되게 도와준다.

  @ApiOperation({ summary: '현재 유저 가지고 오기' })
  @UseGuards(JwtAuthGuard)
  @Get()
  getCurrentUser(@CurrentUser() user) {
    return user.readOnlyUser;
  }

나는 커스텀 데코레이터를 이용해서 @Req() reqreq.user를 얻지 않고 바로 user라는 파라미터를 가져왔다. @CurrentUser()가 커스텀 데코레이터다.

profile
🚀 오늘 배운 건 오늘 적자

0개의 댓글