Spring Security OAuth2 Login은 따로 설정하지 않는다면,
인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 정보를 캡슐화한 객체를 세션에 저장한다.
JWT 기반 인증을 구현하고, 세션을 사용하지 않도록 Security 설정도 했으나
OAuth2 Login을 도입하고 쿠키를 확인해보니 난데없이 JSESSIONID가 생겨있어서 당황했다면 (내 얘기 🙋🏻♀️)은 본 포스팅을 참고하여 해결해보자.
본 포스팅에서는 인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 정보를 캡슐화한 객체 (OAuth2AuthorizationRequest 객체
)를
세션에서 저장 및 관리하지 않고
객체를 직렬화하여 암호화한 후 쿠키에 저장하고,
쿠키에 저장된 값을 복호화한 후 역직렬화하여 관리하도록 설정하는 코드만을 다룬다.
[Spring Security + JWT + OAuth 2.0을 사용한 Stateless Social Login 전체 코드]
💡
인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의
자신의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는
공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준
위키백과
Spring Security OAuth2 Login은 기본적으로,
AuthorizationRequestRepository 인터페이스의
구현체인 HttpSessionOauth2AuthorizationRequestRepository
를 사용하여
OAuth2AuthorizationRequest
를 세션에 저장한다.
OAuth 2.0 Authorization Code Grant Flow는 일반적으로 다음의 단계를 따른다.
인가 코드 받기
- client_id
: 인증 서버 식별자
- redirect_uri
: 인가 코드를 전달받을 URI
- response_type
: 응답 타입, code로 고정된 값
- scope
: 인증 서버로부터 사용자의 리소스를 받기 위해 사용자에게 동의 요청할 동의 항목 목록)
토큰 받기
사용자 로그인 처리
OAuth2AuthorizationRequest
는 인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 다음과 같은 정보를 캡슐화한 객체이다.
인증 서버로 인가 코드 발급을 요청을 보내기 위해 필요한 정보(이하 OAuth2AuthorizationRequest)를 관리하기 위한 인터페이스로, 기본 구현체는 HttpSessionOauth2AuthorizationRequestRepository이다.
💡 AutorizationRequestRepository는 인가 코드 발급 요청을 시작한 시점부터,
인가 코드를 받는 시점까지 OAuth2AuthorizationRequest를 저장한다.
loadAuthorizationRequest
인증 서버는 사용자의 동의 항목에 대한 동의 여부에 따라 인가 코드 또는 에러 응답을 생성한다.
loadAuthorizationRequest는 인가 코드 요청에 대한 응답을 받은 시점에 호출되어
AuthorizationRequestRepository에 저장된 OAuth2AuthorizationRequest를 가져오는데,
이는 인가 코드 발급 요청에 대한 응답을 처리하는데 사용된다.
saveAuthorizationRequest
인가 코드 발급 요청을 시작한 시점에 호출 되어, OAuth2AuthorizationRequest를 저장한다.
removeAuthorizationRequest
현재 인가 코드 발급 요청에 대한 응답을 받은 시점에 호출되어, AuthorizationRequestRepository에서 OAuth2AuthorizationRequest를 삭제한다.
AuthorizationRequestRepository 인터페이스에 대해 알아보았으니,
이제 Stateless한 소셜 로그인을 구현하기 위해 OAuth2AuthorizationRequest를 쿠키에 저장하는 구현 클래스를 만들어보자.
@Slf4j
@Component
public class HttpCookieOAuth2AuthorizedClientRepository
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_COOKIE_NAME = "OAUTH2_AUTHORIZATION_REQUEST";
public static final Duration OAUTH_COOKIE_EXPIRY = Duration.ofMinutes(5);
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return getCookie(request);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
HttpServletResponse response) {
if (isNull(authorizationRequest)) {
removeAuthorizationRequest(request, response);
return;
}
CookieUtil.addCookie(response, OAUTH2_COOKIE_NAME, encrypt(authorizationRequest), OAUTH_COOKIE_EXPIRY);
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
HttpServletResponse response) {
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = getCookie(request);
CookieUtil.removeCookie(request, response, OAUTH2_COOKIE_NAME);
return oAuth2AuthorizationRequest;
}
private OAuth2AuthorizationRequest getCookie(HttpServletRequest request) {
return CookieUtil.getCookie(request, OAUTH2_COOKIE_NAME).map(this::decrypt).orElse(null);
}
private String encrypt(OAuth2AuthorizationRequest authorizationRequest) {
byte[] bytes = SerializationUtils.serialize(authorizationRequest);
return Aes256.encrypt(bytes);
}
private OAuth2AuthorizationRequest decrypt(Cookie cookie) {
byte[] bytes = Aes256.decrypt(cookie.getValue().getBytes(StandardCharsets.UTF_8));
return (OAuth2AuthorizationRequest)SerializationUtils.deserialize(
bytes);
}
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CookieUtil {
public static Optional<Cookie> getCookie(HttpServletRequest request, String cookieName) {
return Optional.ofNullable(request.getCookies())
.flatMap(cookies -> Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.findFirst());
}
public static void addCookie(HttpServletResponse response, String cookieName, String cookieValue,
Duration maxAge) {
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setPath("/");
cookie.setHttpOnly(Boolean.TRUE);
cookie.setSecure(Boolean.TRUE);
cookie.setMaxAge((int)maxAge.toSeconds());
response.addCookie(cookie);
}
public static void removeCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
Optional.of(request.getCookies())
.ifPresent(cookies ->
Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.forEach(cookie -> {
cookie.setValue(EMPTY);
cookie.setPath("/");
cookie.setMaxAge(ZERO);
response.addCookie(cookie);
})
);
}
}
이처럼 인가 코드 발급을 요청을 보내기 위해 필요한 다음과 같은 정보를 캡슐화한 객체를 쿠키에서 저장하고 관리하도록 AuthorizationRequestRepository를 구현한 HttpCookieOAuth2AuthorizedClientRepository 클래스를 작성했다면,
이제 남은 것은 Spring Security 설정 파일에서 AuthorizationRequestRepository의 기본 구현체인 HttpSessionOauth2AuthorizationRequestRepository가 아닌,
HttpCookieOAuth2AuthorizedClientRepository를 사용하도록 설정해주는 것이다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
// ...
private final HttpCookieOAuth2AuthorizedClientRepository httpCookieOAuth2AuthorizedClientRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
// ...
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(config ->
config.authorizationRequestRepository(httpCookieOAuth2AuthorizedClientRepository))
// ...other config...
.build();
}
}