⌨️ 로그인 구현
이번 세션 로그인을 구현하며 가장 많이 공부가 된 파트, 그리고 고민을 한 부분이다.
1. 로그인 로직 고민
세션 로그인 자체는 쉽게 기간의 문제 상 쉽게 접근했다.
로그인 시 세션에 로그인한 유저의 정보를 저장하고 필요한 곳에서 가져다 쓰는 간단한 방식으로 구현을 완료했다.
그 와중 해당 방식에서의 많은 중복과 불편함이 발생했기에, Spring Security처럼 필터를 두어 로그인을 처리하기로 했다.
1.1. LoginCheckFilter
@Slf4j
public class LoginCheckFilter implements Filter {
// TODO 로그인이 필요 없는 URL 추가
private static final String[] whiteList = { "/", "/signup", "/login", "/logout", "/signupForm", "/walking-path",
"/walking-path/*", "/reviews","/reviews/list/*", "/comments","/comments/list/*", "/images/*", "/ex_images/*", "/style/**", "/js/**",
"/images/**", "/visitor/delete","/visitorslist", "/visitorslist/**", "/insertVisitors", "/insertVisitorsForm"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String requestURI = httpServletRequest.getRequestURI();
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
try {
log.info("인증 필터 시작 : {}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 로직 실행 : {}", requestURI);
HttpSession session = httpServletRequest.getSession(false);
if (session == null || session.getAttribute(SessionConst.LONGIN_USERS) == null) {
log.info("미인증 사용자 요청 : {}", requestURI);
// 로그인으로 보내기
httpServletResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 필터 종료 : {}", requestURI);
}
}
/**
* whiteList는 체크 안하는 목록
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
로직 자체는 어렵지 않다, 로그인을 체크하지 않아도 되는 white list를 만들어 필터에서 체크하여
로그인이 필요하다면 현재 위치의 URI를 가지고 로그인으로 가서 로그인 후 다시 요청이 발생한 위치로 돌아오는 로직이다.
여기서 SessionConst.LOGIN_USERS는 확장성을 위해 별도의 인터페이스로 빼서 사용을 하였다. 저 부분이 특정 변수로 들어간다면, 모든 부분을 다 수정해야 하기 때문이다.
즉 session.getAttribute("auth") 였다가 session.getAttribute("Authorization") 으로 값이 바뀌면 전부 바꿔 주어야 한다.
1.2. SessionConst
public interface SessionConst {
String LONGIN_USERS = "auth";
}
인터페이스로 상수만 구현해 놨으니, 해당 부분만 수정한다면 이 변수를 쓰는 많은 곳에서의 재작성을 방지할 수 있고, 또 SessionConst에 다른 메서드가 필요하다면 메서드를 제공하고, 필요한 위치에서 구현체를 만들어 사용할 수 있다.
1.3. Config
@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
이렇게 구현한 필터를 WebMvcConfigurer를 상속한 WebConfig에 등록해주어야 한다.
이 포스트는 Configurer나 Filter에 대한 설명을 하는 게 아니므로 간단히 넘어가도록 하자.
filter에서 url 패턴을 검사하지 않고 내부에 white list를 만든 것은,
Configurer 가 필요이상으로 길게 구현되는 것을 방지하기 위해서이다
2. 인가 과정
위에서 로그인을 통해 UserDTO의 인증을 거쳤다.
이어서 인가를 구현하기 위해 @Login 어노테이션을 구현하여 활용하였다.
2.1. @Login
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
어노테이션을 직접 구현해서 사용하는 것은 처음인데 굉장히 편해서 좋았다.
우리 서비스에서는 역할에 따른 인가는 없지만, 사용자 검증은 필요해서 매번 세션의 값과 일치를 체크하는 과정을 각 핸들러에 작성하자니 너무 중복이 심했다.
이를 어노테이션을 활용하여 @AuthenticationPrincipal처럼 활용하였다.
2.1. LoginUsersArgResolver
@Slf4j
public class LoginUsersArgResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = UsersDTO.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory){
log.info("resolveArg 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if(session == null){
return null;
}
return session.getAttribute(SessionConst.LONGIN_USERS);
}
}
HandlerMethodArgumentResolver를 Override 하여 구현하면 된다.
로직 자체는 어렵지 않고 Resolver를 구현하는 방식만 조금 공부하면 누구나 쉽게 구현할 수 있다.
중요한 것은 이러한 것이 필요한 지를 판단하고 활용하는 인지 능력이라고 생각한다.
로직은 @Login 어노테이션이 걸린 UsersDTO에 로그인한 유저가 있다면 그 값을 session에서 가져와서 넣어주는 것과 같다.
이를 통해 각 핸들러에서 session을 조회할 필요 없이 변수에 @Login UsersDTO를 통해 로그인 한 유저의 정보를 편하게 가져다 사용할 수 있다.
3. 마무리
구현했던 부분을 혼자 오래 기억하기 위해 기록하는 것이라 가독성이 떨어지고, 코드가 타인이 보기에는 이해가 힘들 수 있을 것 같다.
아무래도 내 생각이나 고민을 남에게 설명하며 그것에 대한 해결책을 구현한 것을 제시하는 것이 쉽지는 않은 것 같지만, 꾸준히 함으로써 이러한 역량도 키우는 것을 중요시 생각하기 때문에 꾸준히 연습해 봐야겠다
4. 피드백
- 회원가입/로그인에 대해 세션을 활용하고 이를 확장성 있게 코드를 짜고, 재사용성을 고려하여 짜는 방식에 대해 많은 공부가 되었다.
- 앞으로도 이러한 부분들을 고려하며 코드를 짜는 것이 중요할 것 같다는 생각을 함
*(이러한 부분 : 재사용성, 확장성, 중복제거 등 코드의 품질)