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를 응용하고자 이런 방식을 채택하였습니다.