Spring 공부를 하다보니 인증/인가에 관련해서 쿠키를 만들 일이 생기게 되었다. 쿠키의 개념만 어렴풋이 알고 있었어서, 자세한 전달 방식과 여러 옵션들에 대해 정리하며 글을 작성한다.
@PostMapping("/login")
public ResponseEntity<Object> login(@RequestBody LoginRequest request) {
String accessToken = authService.signin(request);
ResponseCookie cookie = ResponseCookie
.from("SESSION", accessToken)
.domain("localhost")
.path("/")
.httpOnly(true)
.secure(false)
.maxAge(Duration.ofDays(30))
.sameSite("Strict")
.build();
return ResponseEntity
.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.build();
}
먼저 쿠키를 생성하는 부분이다.
사용자 정보를 RequestBody로 받으면, 서비스계층을 통해 실제 존재하는 사용자인지 확인 후, accessToken값을 발급한다.
이 accessToken값을 레포지토리에 저장하고, 동시에 사용자에게 쿠키로 전달해주면 나중에 인증이 필요한 페이지에 접속할 때 체크를 할 수 있게 되는 것이다.
가장 중요하게 봐야하는 부분은,
ResponseCookie cookie = ResponseCookie
.from("SESSION", accessToken)
.domain("localhost")
.path("/")
.httpOnly(true)
.secure(false)
.maxAge(Duration.ofDays(30))
.sameSite("Strict")
.build();
이 부분이다. ResponseCookie라는 Spring 클래스를 통해 쿠키를 만드는데, 각 부분에 대해서 설명을 해보자면,
- from : 쿠키이름과 쿠키값을 지정한다.
- domain : 특정 도메인에서만 사용되도록 제한한다.
- path : 특정 서블릿에만 쿠키를 전달한다.
- httpOnly : 클라이언트 등을 통해 쿠키가 탈취되는 것을 방지한다. 즉, 브라우저에서 쿠키에 접근하는 것을 막는다.
- secure : https 요청으로만 쿠키를 주고받을 수 있게 만든다. 쿠키가 탈취 당하더라도 암호화가 되어있으므로 안전하다.
- maxAge : 쿠키의 만료기간을 설정한다.
- sameSite : 서드파티요청에 쿠키를 전달할지 설정한다. CSRF 관련 문제를 해결한다.
다음으로 이렇게 생성한 쿠키를 통해 인증/인가를 받을 수 있는 방법을 정리해보았다.
@GetMapping("/test")
public Long test(UserSession userSession) {
// 처리하고자 하는 내용
}
@RequiredArgsConstructor
public class AuthResolver implements HandlerMethodArgumentResolver {
private final SessionRepository sessionRepository;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(UserSession.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest == null) {
throw new Unauthorized();
}
Cookie[] cookies = servletRequest.getCookies();
if (cookies == null || cookies.length == 0) {
throw new Unauthorized();
}
String accessToken = cookies[0].getValue();
Session session = sessionRepository.findByAccessToken(accessToken)
.orElseThrow(Unauthorized::new);
return new UserSession(session.getUsers().getId());
}
}
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final SessionRepository sessionRepository;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthResolver(sessionRepository));
}
}
AuthResolver가 핵심인데, HandlerMethodArgumentResolver를 구현하고, supportsParameter()와 resolveArgument()를 오버라이딩해야한다.
- supportsParameter() : 어떤 메서드가 ArgumentResolver를 통하게 할 것인지 설정한다. 위 예제에서는 UserSession이라는 파라미터를 받아야만 진행한다.
- resolveArgument() : 위의 메서드를 통과하면 진행된다. NativeWebRequest를 통해 요청의 여러 정보들을 받아올 수 있고, 위 예제에서는 쿠키를 받아와 인증토큰값을 꺼낸다. 인증토큰값이 레포지토리에 저장된 값이면 통과시킨다.
위와 같은 방식으로 간단하게 토큰을 이용한 인증을 구현할 수 있다. 즉, 로그인 메서드 등에서 토큰을 발급하고, 인증이 필요한 메서드에 대해서는 ArgumentResolver를 통하게 하는 것이다. 더 공부해서 jwt나 SpringSecurity에 관한 내용도 적어보겠다.