본문 바로가기
Spring

[Spring] Stateless Spring Security - 1

by Nhahan 2024. 10. 5.

흔히 찾아볼 수 있는 대부분의 스프링 시큐리티 코드

Spring Security

 

국내외 가릴 것 없이 수많은 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에 연결되어있다.

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에 접근할 것이다.

(JPA에서의 N+1과 근본적으로 다를 바 없는 문제라고 생각한다)

 

JWT의 특장점을 전혀 살리지 못한, 사실상 세션 방식이라고 볼 수 있는 코드인 것이다.

 

 

다음 글에서는 왜 이러한 코드가 생기게 되었는지, JWT와 Spring Security의 역사와 엮어 이야기해보려고 한다.

 

 

 

댓글