이룸 프로젝트

[이룸] 240302 JwtAuthorizationFilter.java

쿠마냥 2024. 3. 7. 15:35
  1. 클래스명의 이유
    • JwtAuthorizationFilter : jwt 토큰으로 Authorization(인가)을 하는 Filter 클래스.
    FIlter 인터페이스 → **Generic Filter Bean** 추상 클래스 → OncePerRequestFIlter 추상 클래스 → JwtAuthorizaionFilter 클래스!
    • **OncePerRequestFilter**란? 각 HTTP 요청에 대해 필터가 정확히 한 번만 실행되도록 보장하는 필터로, 상속받은 후 doFilterInternal 메소드를 오버라이드하여 필요한 로직을 구현할 수 있음.

[의존성 주입]

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 {
    // access token 추출     
    String tokenValue = jwtUtil.getTokenFromRequest(req, JwtUtil.AUTHORIZATION_HEADER);

    // 만약 /api/token(access token 재발급 URI)로 요청이 들어온다면
    if(req.getRequestURI().equals("/api/token")){
        // refresh token 추출
        tokenValue = jwtUtil.getTokenFromRequest(req, JwtUtil.REFRESH_TOKEN_HEADER);

        // refresh token이 없을 경우 403 반환
        if(!StringUtils.hasText(tokenValue)){
            res.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }

        // refresh token이 있을 경우 bearer prefix 제거
        tokenValue = jwtUtil.substringToken(tokenValue);

        // 토큰 유효성 검사 후, true 반환되면 다음 필터로 전달  
        jwtUtil.validateToken(tokenValue);
        filterChain.doFilter(req, res);
        return;
    }

    // access token이 존재할 경우 같은 방식으로 유효성 검사를 거치고 통과하지 못하면 401 반환
    if (StringUtils.hasText(tokenValue)) {
        tokenValue = jwtUtil.substringToken(tokenValue);
        log.info("tokenValue : " + tokenValue);

        if (!jwtUtil.validateToken(tokenValue)) {
            log.error("Invalid JWT Token");
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // 인증이 끝나면 토큰에서 사용자 정보를 뽑아내 claims에 집어넣기 
        Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

        try {
            // 추출한 사용자 정보에서 subject를 뽑아내 인증 객체 생성
            setAuthentication(info.getSubject());
        } catch (Exception e) {
            log.error("Authentication failed: {}", e.getMessage());
            return;
        }
    }

    filterChain.doFilter(req, res);
}

 

  • 이룸은 refresh token을 사용하므로 토큰 재발급 컨트롤러를 만들었는데, 이 doFIlter를 통과하지 못해서 계속 500에러가 나는 문제가 발생했었음. if(req.getRequestURI().equals("/api/token")){…} 로 해결.
public void setAuthentication(String username) {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    Authentication authentication = createAuthentication(username);
    context.setAuthentication(authentication);

    SecurityContextHolder.setContext(context);
}
  • 사용자 이름 subject를 받아 인증을 진행하는 메서드.
  • SecurityContext 객체를 생성→ 이 객체는 인증 정보(Authentication 객체)를 담는 바구니 역할을 함. → **createAuthentication(username)**로 인증 객체 생성 -> context.setAuthentication(authentication); 해서 authentication을 담기.
  • 마지막으로, 현재 스레드의 **SecurityContext**를 새로 설정한 **SecurityContext**로 교체. 이제 스프링 시큐리티가 로그인한 사용자의 정보를 가져다 쓸 수 있게 되었음!
private Authentication createAuthentication(String username) {
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
  • **loadUserByUsername()** 을 호출하여 사용자 이름(우리 서비스에서는 이메일)에 해당하는 userDetails 객체를 불러옴. → **UserDetails**는 사용자의 정보(예: 사용자 이름, 비밀번호, 권한 등)를 담고 있음. → **UsernamePasswordAuthenticationToken**은 Authentication 인터페이스의 구현체로, 사용자 이름과 비밀번호를 기반으로 한 인증 정보를 나타냄. → 생성된 Authentication 객체는 이후 **SecurityContext**에 설정됨.


  • 아쉽고 아쉬운 점
  • 앞에서 authentication = 너 누구야?, authroization = 너 뭐 돼? 라고 설명드렸는데, 사실 우리 이룸에는 authorize 할 만한 것이 없습니다. user와 admin으로 분리해 놓기는 했지만 이를 활용하지 않기 때문입니다. 또한 autorization filter에 있는 메서드들 모두 authentication과 관련이 더 높아 필요 없는 필터라고 볼 수 있습니다. 항해의 많은 프로젝트들이 authroization filter 클래스를 가지고 있지 않은 것도 같은 이유입니다.
  • 그러나 저희가 초기에 분명 admin에 대한 내용을 구현하려 했었고, 이를 위한 authorization filter는 필요합니다. 또한 어디까지가 authenticate이고 authroizae인지 무 자르듯 자르기도 어렵다는 생각이 듭니다. 모든 인증 과정은 인가에 필요하고, 어떻게 구현할 것인가는 개발자의 몫일 테니까요.
  • 시간적 여유가 더 있다면, 현재의 모든 메서드들을 authentication으로 옮기고 admin과 user를 구분하는 (제대로 된) authroization filter를 구현해 보고 싶습니다. 결국 필터는 하나밖에 필요하지 않았다는 소식을 전해드리며… 스프링 시큐리티 코드 리뷰를 마치겠습니다.
댓글수0