Project

[Project] Lv_4 스케줄 프로젝트(심화)

kimyongjun0129 2025. 5. 21. 23:50

 


 

요구사항

설명

  • Cookie/Session을 활용해 로그인 기능을 구현합니다. → 2주차 Servlet Filter 실습 참고!
  • ✅ 필터를 활용해 인증 처리를 할 수 있습니다.
  • ✅ @Configuration 을 활용해 필터를 등록할 수 있습니다.

조건

  • ✅ 이메일과 비밀번호를 활용해 로그인 기능을 구현합니다.
  • ✅ 회원가입, 로그인 요청은 인증 처리에서 제외합니다.

예외처리

  • ✅ 로그인 시 이메일과 비밀번호가 일치하지 않을 경우 HTTP Status code 401을 반환합니다.

 


 

요구 구현

LoginController

더보기
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    @PostMapping("/login")
    ResponseEntity<String> login(
            @Valid @ModelAttribute LoginRequestDto requestDto,
            HttpServletRequest request
    ) {
        loginService.login(requestDto, request);
        return new ResponseEntity<>("로그인 성공", HttpStatus.OK);
    }
}
  • 로그인을 처리하기 위한 Controller를 만들었습니다.
  • 추후에 로그아웃 기능도 구현하기 위해 Controller를 분리하였습니다.

 

 

LoginService

더보기
@Service
@RequiredArgsConstructor
public class LoginService {

    private final UserRepository userRepository;

    public void login(LoginRequestDto requestDto, HttpServletRequest request) {
        // 유저 이메일 검증
        User user = userRepository.findByEmailOrElseThrow(requestDto.getEmail());

        // 비밀 번호 검증
        if (!user.getPassword().equals(requestDto.getPassword())) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "This password is incorrect.");
        }

        HttpSession session = request.getSession();

        session.setAttribute("login-userId", user.getId());
    }
}
  • 이메일 검증을 통해 DB에 저장된 `User`를 가져옵니다.
  • 가져온 `User`의 비밀번호가 현재 로그인을 시도하는 유저의 비밀번호와 일치하는지 검증합니다.
  • 검증을 마친 후, HttpServletRequest 안에 있는 session이 없으면 새로 생성하고 있으면, 기존 세션을 그대로 반환합니다.
    • session 안에 `login-userId : user.getId()` 값을 넣어줍니다. (이 값을 통해 `인가` 진행)

 

 

LoginRequestDto

더보기
@Getter
@AllArgsConstructor
public class LoginRequestDto {
    @NotNull
    @Email
    private String email;
    @NotNull
    private String password;
}

 


 

WebConfig

더보기
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loginFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        // Filter 등록
        filterFilterRegistrationBean.setFilter(new LoginFilter());
        // Filter 순서 설정
        filterFilterRegistrationBean.setOrder(1);
        // 전체 URL 에 Filter 적용
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean userAuthorizationFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        // Filter 등록
        filterFilterRegistrationBean.setFilter(new UserAuthorizationFilter());
        // Filter 순서 설정
        filterFilterRegistrationBean.setOrder(2);
        // 전체 URL 에 Filter 적용
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
}
  • 로그인 검증에 필요한 `LoginFilter`와 사용자 인가에 필요한 `UserAuthorizationFilter` 각 각 2개를 등록, 순서 설정, URL에 적용 후 Spring Container에 반환합니다. (빈으로 등록)

 

 

LoginFilter

더보기
@Slf4j
public class LoginFilter implements Filter {

    private static final String[] WHITE_LIST = {"/api/users", "/api/auth/*"};

    @Override
    public void doFilter(
            ServletRequest servletRequest,
            ServletResponse servletResponse,
            FilterChain filterChain
    ) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        String requestURL = httpRequest.getRequestURI();

        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

        // WHITE LIST에 포함되지 않은 경우 실행
        if(!isWhiteList(requestURL)) {
            HttpSession session = httpRequest.getSession(false);

            if(session == null || session.getAttribute("login-userId") == null) {
                throw new AuthenticationException("로그인을 먼저 시도해주세요.");
            }

            log.info("로그인에 성공했습니다.");
        }

        filterChain.doFilter(httpRequest, httpResponse);
    }

    public static boolean isWhiteList(String requestURI) {
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}
  • WHITE_LIST
    • `/api/users` : `/api/users/1`과 같은 값은 포함하지 않는다. (정확히 `/api/users`와 동일한 경로여야 한다.)
    • `/api/auth/*` : `/api/auth/login`등 `/auth`를 지나는 모든 경로가 포함된다.
  • 현재 WHITE_LIST를 제외한 나머지 경로에 접근을 시도할 때, 로그인한 사용자인지 판단하는 `Filter`입니다.

 

 

UserAuthorizationFilter

더보기
@Slf4j
public class UserAuthorizationFilter implements Filter {

    @Override
    public void doFilter(
            ServletRequest servletRequest,
            ServletResponse servletResponse,
            FilterChain filterChain
    ) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

        String requestURL = httpRequest.getRequestURI();

        if(!isWhiteList(requestURL)) {
            HttpSession session = httpRequest.getSession(false);
            Long loginUserId = (Long) session.getAttribute("login-userId");

            String[] split = requestURL.split("/");
            String userIdStr = split[split.length-1];
            Long userId = Long.parseLong(userIdStr);

            if (!loginUserId.equals(userId)) throw new AuthenticationException("다른 사용자의 리소스입니다.");

        }

        filterChain.doFilter(httpRequest, httpResponse);
    }
}
  • 앞서 `LoginFilter`에서 `session 값이 null`인지 판단했으므로 넣지 않았습니다.
    • Filter는 체인처럼 연결되어 존재하는 모든 Filter가 실행된 후에 예외가 없을 시, Controller가 실행됩니다.
    • `WebConfig` 클래스에서 Filter의 순서를 지정했기 때문에 `LoginFilter` 실행 후 `UserAuthorizationFilter`가 실행됩니다.
  • 보통은 인가하는 과정을 각 Service에서 매번 진행하지만, 이번 프로젝트는 소규모이기도 하고 인가가 필요한 로직은 모두 같은 형태를 띄므로 위에서와 같이 Filter로 처리하였습니다.
  • `@PathVariable`로 넘겨 받은 값은 해당 리소스에 등록된 userId와 동일하므로 위에서와 같이 경로에서 userId를 추출한 다음 session에 저장된 `userId` (즉, 로그인한 user의 id)와 비교하여 같지 않은 경우 예외를 발생하였습니다.

 

 


 

 

회고

이번에 인가를 하는 과정에서 Filter를 사용하여 한 번에 처리를 해줬지만 만약, `userId`를 다르게 받아오는 경우가 생기면 이 방법을 사용할 수 없습니다. 이 방법은 위 구조에 완전히 의존적이므로 실무에서는 인가가 필요한 모든 Service에서 확인을 해야합니다. Filter를 응용하고자 이런 방식을 채택하였습니다.