흔히 찾아볼 수 있는 대부분의 스프링 시큐리티 코드
국내외 가릴 것 없이 수많은 Spring Security(이하 시큐리티) 관련 글을 찾아보았지만, 그 어떤 곳에서도 JWT에 특성을 제대로 살린 시큐리티 코드를 보지 못했다.
블로그든, 유튜브든 어디서 시작되었는지 모를 코드를 똑같이 복사 붙여넣기 하고 있는 듯 하다.
이상하다고 생각하는 코드는 아래와 같다.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findById(Long.valueOf(username))
.orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND));
return new UserPrincipal(user);
}
}
(어떤 글이든 영상이든 가도 이 코드가 반드시 사용되고 있다)
UserDetailsService를 implements 해서 loadUserByUsername을 구현한 클래스이다.
이 클래스를 Filter에 DI하여 사용한다.
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = jwtUtil.getTokenFromRequest(req);
if (StringUtils.hasText(tokenValue)) {
tokenValue = jwtUtil.substringToken(tokenValue);
if (!jwtUtil.validateToken(tokenValue)) {
log.error("Token Error");
return;
}
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
try {
setAuthentication(info.getSubject());
} catch (Exception e) {
log.error(e.getMessage());
return;
}
}
filterChain.doFilter(req, res);
}
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
private Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
Filter에서 loadUserByUsername 메서드를 이용해 모든 request마다 DB에 들어가 유저를 조회한다.
그리고 조회한 User를 가지고 authentication 객체를 만들어 SpringContextHolder에 set을 해주는 식이다.
세부적인 구현 사항은 글마다 영상마다 다르지만, 결국 다 여기서 벗어나지 않는다.
무엇이 문제인가?
주니어 면접시 빠지지 않는 그 질문, JWT
굉장히 흔한 질문과 답변이다.
Q. JWT의 특징은 무엇인가요?
A. Stateless입니다.
이걸 답변하지 못하는 사람은 없을 것이다. (근데 그 다음 질문까지 답하는 사람은 의외로 많지 않다)
그 다음 예상 질문과 답변도 상상해보자.
Q. 그렇다면, stateless의 주체는 누구인가요?
A. 서버입니다.
서버가 stateless하다
서버가 stateless하다는 건, 서버가 상태를 갖지 않는다는 것이다.
이 상황에서는 서버가 JWT에 담긴 정보를 갖고 있지 않아도 괜찮다는 뜻이다.
간단히 말하면, '서버는 몰라요'다.
이 특징 덕에 JWT의 큰 장점 중의 하나가 '확장성'이다.
A서버에서 DB의 정보를 바탕으로 발행한 JWT가 있다고 가정하자.
이 때, 이 JWT를 B와 C서버에서 사용할 수 있다는 게 바로 확장성에 해당한다.
B와 C는 DB와 연결되어있지 않음에도 JWT의 정보를 믿고 쓸 수 있는 것이다.
문제점
다시 UserDetailsServiceImpl 클래스 코드를 보자.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findById(Long.valueOf(username))
.orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND));
return new UserPrincipal(user);
}
}
분명히 위 코드에서는 DB에 접근하고 있다.
따라서 이 로직으로 인해 모든 요청마다 DB에 접근할 것이고, 이는 JWT의 stateless 특성에 완전히 위배된다.
(JPA에서의 N+1과 근본적으로 다를 바 없는 문제라고 생각한다)
JWT의 특장점을 전혀 살리지 못한, 사실상 세션 방식이라고 볼 수 있는 코드인 것이다.
다음 글에서는 왜 이러한 코드가 생기게 되었는지, JWT와 Spring Security의 역사와 엮어 이야기해보려고 한다.
'Spring' 카테고리의 다른 글
[Spring] Gradle 경로 에러 (1) | 2024.09.11 |
---|---|
[Spring] 스프링 3.3에서 추가된 `pageSerializationMode = VIA_DTO` (0) | 2024.08.26 |
[Spring] @ModelAttribute와 @RequestBody의 데이터 바인딩 (0) | 2024.04.05 |
[Spring] 코틀린 쓸 때 @Valid 동작이 안된다면 (0) | 2024.01.18 |
[Spring] 스웨거 안되는 버전이 너무 많아요! (0) | 2023.12.31 |
댓글