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