스프링부트

OncePerReqeustFilter를 사용하여 로그인 구현하기 (Security X)

하이자바 2025. 4. 1. 23:11

이 구현은 시큐리티 의존성을 사용하지 않고 필터를 사용하여 Http 리퀘스트를 통해 구현하고자 한다.

먼저 Filter는 기본적인 Filter와 OncePerRequestFilter가 있다.

이러한 필터는 HTTP 요청 응답 전후에 로직을 제어할 수 있다.

+ 이렇게 요청, 응답 제어를 하는 역할로는 인터셉터, AOP가 있다. 각각은 처리 시점이 다르다는 차이점이 있다.

 

Filter와 OncePerRequestFilter의 차이점으로는 단 한 번의 요청을 받는 지의 차이점이 있다.

로그인을 구현할 때는 단 한 번의 요청에 대하여 필터를 적용하기 위해 OncePerRequestFilter를 사용하고자 한다.

 

1. OncePerRequestFilter를 상속받는 클래스 생성 후 메소드 오버라이드를 통해 필터 로직을 작성한다.

public class CustomAuthFilter extends OncePerRequestFilter 

 

2. 로직은 다음과 같이 작성하였다.

로그인 컨트롤러를 만든 후 로그인 성공 시 유저라는 세션 정보를 만든다. 

그 세션 정보에 따라서 로그인 여부를 파악하고 API에 접근할 수 있도록 한다.

현재 로직은 /api 경로로 접근하는 경우 로그인없이 접근할 수 있다.

지금은 기능들이 동작하는 지 테스트 하고자 하기 때문에 /login,/register와 같이 로그인 없이 접근가능한 부분에 대해서 작성하지 않고

/api로 두었지만 프로젝트가 완성이 되면 /api/login, /api/register는 로그인없이 접근할 수 있도록 수정해야 한다.

그 외 게시판 조회와 같은 내용도 필요 시 추가하면 좋을 것 같다.

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String requestURI = request.getRequestURI();
    String requestMethod = request.getMethod();

    log.info("Method : {}, URI : {}", requestMethod, requestURI);

    if (requestURI.startsWith("/api")) {
        filterChain.doFilter(request, response);
        return;
    }

    HttpSession session = request.getSession(false);
    if(session == null || session.getAttribute("user") == null) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"message\": \"로그인이 필요합니다.\"}");
        return;
    }

    filterChain.doFilter(request,response);
}

 

3. 로그인 시 세션 정보 저장

현재는 메모리에 세션을 저장하고 있다. 하지만 서버 재시작 시 세션이 없어지기 때문에 세션 저장소를 따로 만들어 사용하는 것이 좋다고 생각한다.

@PostMapping("/login")
public ResponseEntity<Void> loginUser(
        HttpServletRequest request,
        @Valid @RequestBody LoginReqDTO loginReqDTO
){
    UserDTO user = userService.login(loginReqDTO.getEmail(), loginReqDTO.getPassword());
    HttpSession session = request.getSession();
    session.setAttribute("user", user);
    return ResponseEntity.status(HttpStatus.OK).build();
}

 

추가적으로 이와 같이 시큐리티필터체인을 사용하지 않고, 로그인 기능을 구현한다면 글 쓰기, 수정, 삭제와 같이 권한이 필요한 기능들에는

DTO를 통해 사용자 정보를 받는 것보다 세션을 통해 받는 것이 좋다. 왜냐하면 프론트에서 유저 정보를 바꿔서 DTO에 담아 보내게 된다면

내 계정이 아닌 다른 유저의 계정으로 CRUD가 가능해지게 된다. 

아래의 코드와 같이 UserDTO를 만들고 세션 정보를 저장하는 객체를 생성 후 Getter를 통해 필요한 유저 번호, 이메일, 이름 등을 사용하도록 하는 것이 좋다.

HttpSession session = request.getSession(false);
UserDTO loginUser = (UserDTO) session.getAttribute("user");

 

+ 시큐리티는 인증된 사용자의 정보를 자동으로 가져올 수 있게 해주는 @AuthenticationPrincipal 어노테이션이 있기 때문에

다음과 같이 작성하지 않는다.

+ 시큐리티를 사용하지 않기 때문에 비클립트 암호화 기능은 아래 의존성을 추가하여 가져와야 한다.

implementation 'at.favre.lib:bcrypt:0.10.2'

 

그리고 다음과 같이 컴포넌트 등록 후 시큐리티의 패스워드인코드와 동일하게 주입하여 사용하면 된다.

@Component
public class PasswordEncoder {

    public String encode(String rawPassword) {
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
    }

    public boolean matches(String rawPassword, String encodedPassword) {
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified;
    }
}