회의 결과 이번 프로젝트에서는 Spring Security를 쓰지 않기로 했다.
이유는 로그인이 주된 기능이 아니기 때문에, 로그인 기능의 비중을 줄이기 위해서이다.
지금까지 써보지 않은 Session 식별자를 쿠키에 저장하는 방식을 쓸 것이다.
- Session 식별자 vs Token: Session(세션)과 Token(토큰)의 차이는? (velog.io)
Session 기반 로그인을 위해 추가한 클래스들은 아래와 같다.
1. WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")//아래 경로 빼고 모든 경로 체크
.excludePathPatterns("/", "/api/signup", "/api/login", "/api/logout", "/api/user-info/**",
// 실험용으로 빼놓는 자리
"/css/**", "/*.ico", "/error");
}
}
위 클래스는 해당 프로젝트의 전반적인 설정을 담당한다.
- addArgumetResolvers(): HandlerMethodArgumentResolver를 통해 로그인한 사용자 정보를 매개변수로 받는 기능을 등록, 프로젝트 전체에서 일관된 방식으로 로그인 정보를 처리할 수 있도록 해줌.
- addInterceptors(): 요청을 가로채는 인터셉터를 추가해줌. LogInterceptor은 모든 요청에 대해 로깅을 처리,
LoginCheckInterceptor은 모든 요청에 대해 로그인을 체크해줌. 둘 다 excludePathPatterns에 포함된 경로는 체크하지 않는다.
2. LoginMemberArgumentResolver
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = User.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
위 클래스는 supportsParameter메서드를 통해 매개변수가 특정 조건을 만족하는지 확인, resolveArgument를 통해 실제 매개변수를 해결해준다.
- supportsParameter() : @Login 어노테이션이 붙은 매개변수이면서, 해당 매개변수의 타입이 User 클래스나 User 클래스를 상속받은 클래스인지를 확인한다.
- resolveArgument 메서드: HttpServletRequest를 통해 현재 요청에 대한 정보를 가져온다.
그다음, 세션에서 로그인한 사용자 정보(SessionConst.LOGIN_MEMBER)를 가져온다.
만약 세션이 없거나 로그인한 사용자 정보가 없다면 null을 반환한다.
이렇게 함으로써, 컨트롤러 메서드에서 @Login 어노테이션이 붙은 매개변수로 사용하면 로그인한 사용자 정보를 간편하게 받아올 수 있다.
3. @Login
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
위에서 말한 매개변수를 나타냄, 즉 로그인한 사용자를 뜻함
4. LogInterceptor
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
//@RequestMapping: HandlerMethod
//정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;//호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
복잡해 보이지만 단지 로그를 남기는 클래스이다. 로깅 처리를 위해 HandlerInterceptor를 구현한다.
preHandle(), postHandle(), afterCompletion()모두 요청을 처리하기 전에 로그를 남기는 역할을 한다.
5. LoginCheckInterceptor
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/api/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
HandlerInterceptor를 구현하여 로그인을 체크한다.
- preHandle(): 요청이 들어오면 로그인 상태를 체크하고, 로그인되지 않은 경우 로그인 페이지로 리다이렉트한다.
6. SessionConst
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
세션에서 사용되는 상수를 정의한다. 여기서는 로그인한 사용자를 나타내는 상수를 정의한다.
그 결과 여러 클래스에서 일관된 방식으로 세션을 사용할 수 있다.
7. LoginController
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping(value = "/api")
public class LoginController {
private final LoginService loginService;
/*
BindingResult bindingResult: 유효성 검사 후 발생한 바인딩 오류를 포함하는 객체.
@RequestParam(defaultValue = "/") String redirectURL: 로그인 후 리다이렉트할 URL을 받음. 이 값이 없을 경우 기본적으로 "/"로 리다이렉트.
HttpServletRequest request: 현재 HTTP 요청에 대한 정보를 담고 있는 객체. 세션을 생성하거나 사용자 정보를 저장하기 위해 사용됨.
@RequestBody @Valid LoginRequestDto loginRequestDto: 프론트에서 보내줘야함.
*/
@PostMapping("/login")
public String login(@RequestBody @Valid LoginRequestDto loginRequestDto, BindingResult bindingResult,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
User loginUser = loginService.login(loginRequestDto.getUserName(), loginRequestDto.getPassword());
if (loginUser == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginUser);
return "redirect:/";
}
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
}
위에서 만든 세션 기능을 통해 만든 로그인, 로그아웃 컨트롤러 코드이다.
- login(): 서비스코드를 통과해 만일 유효한 로그인 요청이라면, 세션에 로그인 정보가 보관된다.
- logout(): 세션이 이미 존재하는 경우 세션을 무효화하며 해당 세션 정보를 삭제한다.
이후 로그인한 정보를 사용하고 싶다면, HttpSession session 을 가져와서 이용하면 된다.
예시로 컨트롤러 메서드 하나를 보이면 아래와 같다.
@GetMapping("/recipes")
public List<GetAllRecipesResponseDto> getRecipes(HttpSession session) {
return recipeService.getAllRecipes(session);
}
아래는 세션에서 사용자 정보를 가져오는 방법이다.
private User getUserFromSession(HttpSession session) {
// 세션에서 사용자 정보 가져오기
User loginUser = (User) session.getAttribute(SessionConst.LOGIN_MEMBER);
// 만약 세션에 사용자 정보가 없다면 로그인하지 않은 상태이므로 적절히 처리
if (loginUser == null) {
throw new IllegalArgumentException("로그인이 필요합니다.");
}
return loginUser;
}
이렇게 로그인 기능이 완성되었다. 확실히 Security에 비해 보안 취약점이 있지만, 복잡성은 감소한 방식이다.
'프로젝트 > AI명종원' 카테고리의 다른 글
GPT-4: 재료->음식->레시피 기능 구현 (1) | 2024.05.17 |
---|---|
GPT-3.5: 재료->음식->레시피 기능 구현 (0) | 2024.05.16 |
Roboflow Java API (0) | 2024.05.10 |
Roboflow 이미지 전처리 + 모델 훈련 (0) | 2024.05.10 |
Roboflow 선택 과정 + 이미지 수집(크롤링) (0) | 2024.05.10 |