[JWT] 프레임워크 별 JWT 인증 방식

Hojun Song ·2023년 10월 27일
0
post-thumbnail

10-27 Wanted Today I learned.

스프링을 처음 배울 때 당황했던 것은 토큰 인증 방식을 위한 filter나 intercepter로, 맨 처음 Express와 FastAPI를 접했던 나에게는 상당히 이질적인 존재였다.

스프링을 3개월 쓰다가, 다시 Node의 NestJS로 돌아오니, 역으로 다시 적응이 안되는 시간이 다가왔다. Nest는 다양한 전략을 사용할 수 있었고, 스프링 사용자라면 익숙한 인터셉터 방식으로도 구현이 가능하다. 다음은 두 프레임워크별로 jwt인증 방식을 간략하게 비교해보고자 한다. 바로 아래는 jjwt 라이브러리를 사용해서, 간단한 jwt토큰을 발급하는 스프링 부트의 예제이다.

@Service
class AuthService {

    companion object {
        private val users = listOf(
            User("1", "John"),
            User("2", "Maria")
        )
    }

    fun validateUser(userId: String): User? {
        return users.find { it.userId == userId }
    }

    fun login(userId: String): String? {
        val user = validateUser(userId) ?: return null

        return Jwts.builder()
            .setSubject(user.userId)
            .claim("username", user.username)
            .signWith(SignatureAlgorithm.HS256, System.getenv("JWT_SECRET"))
            .setExpiration(Date(System.currentTimeMillis() + 3600000))  // 1 hour
            .compact()
    }
}

class JwtFilter : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authorizationHeader = request.getHeader("Authorization")
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            val jwt = authorizationHeader.substring(7)
            val claims = Jwts.parser()
                .setSigningKey(System.getenv("JWT_SECRET"))
                .parseClaimsJws(jwt)
                .body
            val userId = claims.subject
            // ... set authentication in context ...
        }
        filterChain.doFilter(request, response)
    }
}

이렇게 filter로 처리하면, 토큰이 필요하지 않은 엔드포인트를 역으로 한곳에서 관리해서 처리했었다. 토큰에 특정 값을 저장하면, 컨트롤러에서 httpServeletRequest에 등록된 값을 가져올 수 있었다.

다음은 내가 만들었던 컨트롤러의 예시이다.

@ApiResponse(responseCode = "200", description = "모든 사용자의 정보를 성공적으로 반환합니다.")
    fun getAllUsers(req: HttpServletRequest, pageable: Pageable): ResponseEntity<Page<UserDto>> {
        val users = userService.getAllUsers(pageable)
        return ResponseEntity.ok(users.map { userService.toDto(it) })
    

어떤 유저인지 파악하려면, 토큰값에 담아둔 memberId를 컨트롤러에서 조회했다.

val memberId = req.getAttribute("memberId") as Long?

Nest의 경우에는 filter와 middleware가 아닌 Guard라는 개념을 배웠는데, 이 개념이 흥미롭다. 스프링의 경우, 나는 특정 엔드포인트를 아래와 같이 예외처리를 했다.

override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(jwtTokenInterceptor)
            .addPathPatterns("/**")
            .excludePathPatterns(
                "/swagger-ui/**",
                "/v3/**",
                "/api/user/nickname/guest",
                "/api/follow/guest/followings",
                "/api/follow/guest/followers",
                "/api/oauth/kakao/**",
                "/api/comment/guest/**",
                "/api/trip/nickname/**",
                "/api/trip/guest/**",
                "/api/trip/like/user/**",
                "/favicon.ico",
            )
    }

nest에서는 간단하게

@Controller('your-path')
@UseGuards(YourGuard)
export class YourController {
    // ...
}

이렇게 데코레이터를 붙여주기만 하면 된다.
Authguard에는 다음과 같이 토큰에 값을 설정할 수 있다.

import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const canActivate = (await super.canActivate(context)) as boolean;
    if (!canActivate) {
      return false;
    }

    const request = context.switchToHttp().getRequest();
    const user = await this.getUserFromToken(request);
    request.user = user;

    return true;
  }

  async getUserFromToken(request): Promise<any> {
    // Implement your logic to extract user information from the token.
    // For example:
    const token = request.headers.authorization.split(' ')[1];
    return { userId: token.sub };  // Assuming 'sub' contains the user ID.
  }
}

그럼 이렇게 스프링과 비슷하게 컨트롤러에서도 꺼내쓸 수 있다.

@Controller('your-path')
@UseGuards(JwtAuthGuard)
export class YourController {

  @Get('profile')
  getProfile(@Request() req) {
    const memberId = req.user.userId;  // Accessing the user ID from the request object.
    return { memberId };
  }

}

하지만 이 방식들이 옳은건지는 조금 더 고민을 해봐야겠다. 익숙치 않은 프레임워크로 개발을 하려고 하니, 새로운 것들이 적지 않아 당황스러울 때가 많다.

profile
A web backend developer, let's share information and problem solving!

0개의 댓글