목차
- [이론] 스프링 시큐리티 1
- [OAuth] 스프링 시큐리티 OAuth
- [실습] 스프링 시큐리티 OAuth2 Login 1
- [실습] 스프링 시큐리티 OAuth2 Login 2
- [추가] 스프링 시큐리티 OAuth 로그인 처리 방법 1
- [추가] 스프링 시큐리티 OAuth 로그인 처리 방법 2
앞서 OAuth2 로그인에 대해서 알아보았다
이번에는 Front와 통신을 위한 OAuth2 로그인 후 JWT 발행하는 법을 알아볼 것이다
Security Config
앞서 일반 로그인에서 filter에 successHandler를 설정해준 것처럼 이번에도 successHandler를 통해서 JWT발행을 할 것이다
기억이 나지 않는다면 스프링 시큐리티 JWT 처리를 보고오자
이번에는 직접 filter를 만드는게 아니라 security가 제공해주는 filter에 등록을 할 것이다
아래와 같이 설정을 해주자
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login()
.userInfoEndpoint()
.userService(customOauth2UserService)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler);
return http.build();
}
JwtService 변경 내용
successHandler 구현 이전에 약간의 변경내용이 있다
토큰을 한번에 관리하는 DTO클래스를 만들었다
@Service
public class JwtService {
//추가
public TokenMapping createToken(String email) {
return TokenMapping.builder()
.accessToken(createAccessToken(email))
.refreshToken(createRefreshToken())
.build();
}
}
@Getter
public class TokenMapping {
private final String accessToken;
private final String refreshToken;
@Builder
public TokenMapping(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
SuccessHandler
이전과 크게 다른게 없다
다른 점이라고는 front와 통신을 위해 redirect_url에 queryParameter로 token을 전달해 주는 것이 달라진 점이다
스프링 시큐리티 JWT 처리와 비교해서 봐보도록 해보자
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String TOKEN = "token";
private static final String REFRESH_TOKEN = "refreshToken";
private static final String REDIRECT_URL = "http://localhost:3000/login/redirect"
private final JwtService jwtService;
private final UserRepository userRepository;
@Override
@Transactional
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
TokenMapping tokenMapping = saveUser(authentication);
getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(tokenMapping));
}
private TokenMapping saveUser(Authentication authentication) {
CustomUser customUser = (CustomUser) authentication.getPrincipal();
String email = customUser.getEmail();
TokenMapping token = jwtService.createToken(email);
userRepository.findUserByEmail(email).get()
.updateRefreshToken(token.getRefreshToken());
return token;
}
private String getRedirectUrl(TokenMapping token) {
return UriComponentsBuilder.fromUriString(REDIRECT_URL)
.queryParam(TOKEN, token.getAccessToken())
.queryParam(REFRESH_TOKEN, token.getRefreshToken())
.build().toUriString();
}
}
이렇게 하면 Front에서 Redirect_URL을 받아 처리를 해주면 된다
front에 대해서 잘 알지는 못하지만 간단하게 react를 가지고 구현을 해보았다 react코드에 대해서는 이게 맞다라고 말은 못하지만 원하는대로 구현은 됐으니 일단 이것으로 만족한다
front에서 http://localhost:8080/oauth2/authorization/naver
처럼 서버로 호출을 하면 서버가 처리 후 위 설정 한 redirect_url로 보내주고 해당 url을 가진 router로 전달이 되게된다
/login/redirect router
import React, { useEffect } from "react";
import { useLocation, } from "react-router-dom";
function Redirect() {
const location = useLocation(); // location.search = ?token=asdfasdf&refreshToken=asdf
const params=new URLSearchParams(location.search); // params = token:asdfasdf, refreshToken=asdf key-value 형태로 저장
const token=params.get('token');
const refreshToken=params.get('refreshToken');
useEffect(() => {
// token 저장
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refreshToken)
})
// redirect
window.location.href = '/loginhome'
return (
<></>
)
}
export default Redirect;
이렇게 하면 작동은 하지만 한가지 조금 더 나아가 보자
추가
더보기
기본적으로 JWT를 사용을 한 이유는 사용자 정보를 Session에 저장하지 않고 인증을 하기 위해서이다
서버를 여러개 두는 경우 각 서버의 Session이 담고 있는 정보의 차이에서 발생하는 단점도 있고 session에서 인증 정보를 보는 것이아닌 단순 token이 유효한지만 확인해서 인증을 하는 단순함에서도 있는데
OAuth2Filter 내부를 보게되면 private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();의 코드를 볼 수 있다
Spring Security Docs에서는 OAuth2AuthorizationRequest는 인증에 대한 응답을 연관시키고 검증하는데 사용된다고 적혀있다
기본 구현을 사용하게 되면 OAuth2AuthorizationRequestRedirectFilter에서 만들어져 session에 저장이 OAuth2LoginAuthenticationFilter에서 사용이 된다
여기서 문제는 session에 저장을 하게 된다는 점이다
이 부분에서 session을 사용하지 않고 다른 방식인 cookie를 통해서 저장과 로드과정을 구현할 예정이다
기본 세팅
Cookie를 사용할 간단한 utility class를 만들어 보았다
public class CookieUtils {
public static Optional<Cookie> getCookie(HttpServletRequest request, String name){
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return Optional.of(cookie);
}
}
}
return Optional.empty();
}
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie: cookies) {
if (cookie.getName().equals(name)) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
}
public static String serialize(Object object) {
return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));
}
public static <T> T deserialize(Cookie cookie, Class<T> clazz) {
return clazz.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
}
}
이후 우리가 할 일은 authorizationRequestRepository에 대해서 변경을 해주는 것이다
변경을 하는 것은 다음과 같다
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login()
.authorizationEndpoint()
//우리가 만든 authorizationRequestRepository를 설정해 준다
.authorizationRequestRepository(customAuthorizationRequestRepository)
.and()
.userInfoEndpoint()
.userService(customOauth2UserService)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler);
return http.build();
}
authorizationRequestRepository
@Repository
public class CustomAuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
private static final int cookieExpireSeconds = 60*60;
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
.map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
.orElse(null);
}
//OAuth2AuthorizationRequestRedirectFilter에서 사용
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
return;
}
CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds);
String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds);
}
}
//OAuth2LoginAuthenticationFilter에서 사용
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
return this.loadAuthorizationRequest(request);
}
//OAuth2AuthenticationSuccessHandler에서 사용
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
}
}
각 함수의 역할은 authentication을 쿠키에 담아서 넘겨주고 받아오고 로그인 성공 시 쿠키를 제거하는 역할을 한다
여기서 추가로 하나더 다른 점은 기존에는 서버에서 redirect_url정보를 담고 있었다면 이제는 front에서 queryString을 통해서 원하는 redirct_url을 전달을 해주는 것이다
이를 통해 front와 서버간 결합도를 낮춰주는 효과를 볼 수 있다
OAuth2AuthenticationSuccessHandler
우리가 만든 authorizationRequestRepository를 사용하게 되면 successHandler는 다음과 같이 변경하면 된다
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String TOKEN = "token";
private static final String REFRESH_TOKEN = "refreshToken";
private final JwtService jwtService;
private final UserRepository userRepository;
private final CustomAuthorizationRequestRepository customAuthorizationRequestRepository;
@Override
@Transactional
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String targetUrl = determineTargetUrl(request, response, authentication);
TokenMapping tokenMapping = saveUser(authentication);
getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(targetUrl, tokenMapping));
}
//이 부분이 추가되면 된다
@Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue);
//이미 OAuth2LoginAuthenticationFilter에서 authentication을 꺼내왔고 위에서 redirectUrl을 받아왔으므로 쿠키의 값은 제거하면 된다
clearAuthenticationAttributes(request, response);
return redirectUri.orElse(getDefaultTargetUrl());
}
private TokenMapping saveUser(Authentication authentication) {
CustomUser customUser = (CustomUser) authentication.getPrincipal();
String email = customUser.getEmail();
TokenMapping token = jwtService.createToken(email);
userRepository.findUserByEmail(email).get()
.updateRefreshToken(token.getRefreshToken());
return token;
}
private String getRedirectUrl(String targetUrl, TokenMapping token) {
return UriComponentsBuilder.fromUriString(targetUrl)
.queryParam(TOKEN, token.getAccessToken())
.queryParam(REFRESH_TOKEN, token.getRefreshToken())
.build().toUriString();
}
protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
customAuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
}
front에서는 http://localhost:8080/oauth2/authorization/naver?redirect\_uri=http://localhost:3000/login/redirect로 보내면 된다
마무리
이렇게 Spring Security의 formLogin, Json을 이용한 front와 통신에서의 login, OAuth2Login, JWT발행까지 security를 사용하여 로그인의 전반적인 과정에 대해서 알아보았다
별다른 강의 없이 docs와 책 한권만을 가지고 도전한 결과인데 끝을 맺게 되어서 다행이라 생각한다
코드 구현을 하나하나 확인하면서 어떻게 구현이 되었는지 오류가 나는 이유가 무엇이지를 알게 되고 Spring의 기능과 구현의 분리에 의해서 내가 원하는 것을 추가하고 변경하는 것에 직접적으로 사용한 적이 별로 없었던 나였지만 몇가지를 적용을 해보면서 다시한번 감탄하고 가는 작은 프로젝트였던 것 같다
이후 내가 공부하면서 본 Spring Security 내부 동작에 대해서 작성을 할 예정인데 조금이나마 보는사람들이 도움이 되었으면 좋겠다
지금까지 작성한 모든 코드는 아래 깃헙에 있다
[GitHub - Juhongseok/SpringSecurityLogin: 스프링 시큐리티를 이용한 로그인 프로젝트
스프링 시큐리티를 이용한 로그인 프로젝트. Contribute to Juhongseok/SpringSecurityLogin development by creating an account on GitHub.
github.com](https://github.com/Juhongseok/SpringSecurityLogin)
'공부기록 > Spring Security' 카테고리의 다른 글
[추가] 스프링 시큐리티 OAuth 로그인 처리 방법 2 (0) | 2023.02.22 |
---|---|
[추가] 스프링 시큐리티 OAuth 로그인 처리 방법 1 (2) | 2023.02.22 |
[실습] 스프링 시큐리티 OAuth2 Login 1 (0) | 2023.02.21 |
[이론] 스프링 시큐리티 OAuth (0) | 2023.02.20 |
[실습] 스프링 시큐리티 JWT 처리 (0) | 2023.02.20 |