๐ ํ์ฌ ์ํฉ ๋ฐ ๋ฐฐ๊ฒฝ ์ค๋ช
- ํ์ฌ JWT ๊ธฐ๋ฐ ์์ ๋ก๊ทธ์ธ์ ๊ตฌํ ์ค์ด๋ฉฐ, ํด๋ผ์ด์ธํธ๋ React Native๋ก, ์๋ฒ๋ Spring Boot๋ก ๊ฐ๋ฐํ๊ณ ์๋ค.
- ์์ ๋ก๊ทธ์ธ ๋ฒํผ ํด๋ฆญ ์ ์ธ๋ถ ์์ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ฐ๊ฒฐ๋์ด ์ธ์ฆ์ ๋ง์น ํ, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
โถ OAuth ๋ก๊ทธ์ธ์ ์ธ์ฆ ํ๋ฆ
- ์ฌ์ฉ์๊ฐ ํด๋ผ์ด์ธํธ๋ฅผ ํตํด ์์ ๋ก๊ทธ์ธ์ ์๋ํ๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ ์ธ์ฆ ์๋ฒ๋ก ๋ฆฌ๋ค์ด๋ ์ ํ์ฌ ์ฌ์ฉ์ ์ธ์ฆ์ ์์ฒญํ๋ค.
- ์ฌ์ฉ์๊ฐ ์ธ์ฆ์ ์๋ฃํ๋ฉด, ์ธ์ฆ ์๋ฒ๋ Authorization Code๋ฅผ ํด๋ผ์ด์ธํธ์ ์ ๋ฌํ๋ค.
- ํด๋ผ์ด์ธํธ๋ Authorization Code๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ ์๋ฒ์ Access Token์ ์์ฒญํ๊ณ ๋ฐ๊ธ๋ฐ๋๋ค.
- Access Token์ ์ฌ์ฉํด ๋ฆฌ์์ค ์๋ฒ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์์ฒญํ๋ค.
- ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ด์ฉํด ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ๋ค.
์ธ์ฆ ์๋ฒ: ์ฌ์ฉ์ ์ธ์ฆ์ ํ์ธํ๊ณ , ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด Access Token์ ๋ฐ๊ธ
๋ฆฌ์์ค ์๋ฒ: ์ธ์ฆ ์๋ฒ์์ ๋ฐ๊ธํ Access Token์ ๊ฒ์ฆํ์ฌ, ๋ณดํธ๋ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณต
๐ฝ ๊ณผ์
์ธ์ฆ ์๋ฒ ๋ฐ ๋ฆฌ์์ค ์๋ฒ๋ ๋ชจ๋ OAuth ์๋ฒ์ด๋ค. ์ธ์ฆ ์๋ฒ์์ ์ฌ์ฉ์์ ์๊ฒฉ ์ฆ๋ช ์ ํ์ธํ ๋ค Authorization Code๋ฅผ ๋ฐ๊ธํ๋ค. ์ด Authorization Code๋ ์ผํ์ฉ์ผ๋ก, Access Token์ ๋ฐ๊ธ๋ฐ๊ธฐ ์ํด ์ฌ์ฉ๋๋ค. ์ดํ ๋ฆฌ์์ค ์๋ฒ๋ Access Token์ ๊ฒ์ฆํ๊ณ , ์ค์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ค.
โถ Spring Security์ OAuth ๋ก๊ทธ์ธ
ํ์ง๋ง Spring Security๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๊ณผ์ ์ค 2~5๋ฒ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด ์ค๋ค. ๋ฐ๋ผ์ ์๋ฒ๋ ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ด์ฉํด ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ง ํ๋ฉด ๋๋ค.
- ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ์ด์ง์์ ์์ ๋ก๊ทธ์ธ ๋ฒํผ์ ํด๋ฆญํ๋ค.
- Spring Security๊ฐ ์ด๋ฅผ ๊ฐ๋ก์ฑ๊ณ , ๋ด๋ถ์ ์ผ๋ก ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ค. (์ ๊ณผ์ ์ค 2~5๋ฒ)
- ํผ ๋ก๊ทธ์ธ์ ๊ฒฝ์ฐ UsernamePasswordAuthenticationFilter๊ฐ, ์์ ๋ก๊ทธ์ธ์ ๊ฒฝ์ฐ OAuth2LoginAuthenticationFilter๊ฐ ์ธ์ฆ์ ๋ด๋นํ๋ค.
- UserDetailsService ๋๋ OAuth2UserService๋ฅผ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๊ณ , ์๊ฒฉ ์ฆ๋ช
์ด ์ ํจํ์ง ํ์ธํ๋ค.
- ์ฐ๋ฆฌ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ OAuth2UserService์ ๊ตฌํ์ฒด์ธ DefaultOAuth2UserService๋ฅผ ์ฌ์ฉํ๋ค.
- ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด ์ฌ์ฉ์ ์ ๋ณด๋ SecurityContextHolder์ ์ ์ฅ๋๊ณ , AuthenticationSuccessHandler๋ฅผ ํตํด ์ดํ์ ์ฒ๋ฆฌ๊ฐ ์ด๋ฃจ์ด์ง๋ค.
- ์ธ์ฆ์ด ์คํจํ๋ฉด AuthenticationFailureHandler๋ฅผ ํตํด ์คํจ์ ๋ํ ์ฒ๋ฆฌ(์: ์ค๋ฅ ๋ฉ์์ง, ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ)๊ฐ ์ด๋ฃจ์ด์ง๋ค.
SecurityContextHolder: Spring Security์์ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ๋ฐ ์ฌ์ฉ๋๋ ํด๋์ค์ด๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐ์์ ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์ฝ๊ฒ ์กฐํํ ์ ์๋๋ก, SecurityContext๋ฅผ ๋ณด๊ดํ๋ ์ญํ ์ ํ๋ค.
โถ ๊ตฌํ ์ฝ๋
- DefaultOAuth2UserService๋ฅผ ๊ตฌํํ์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฒ๋ฆฌํ๋ ์๋น์ค ํด๋์ค๋ฅผ ์์ฑํ๊ณ , ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก AuthenticationSuccessHandler์ AuthenticationFailureHandler๋ฅผ ํตํด ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ๋ค.
๐ฝ OAuth ์ธ์ฆ ์์ฒญ URL
http://localhost:8080/v1/oauth2/authorization/${provider}
๐ฝ DefaultOAuth2UserService์ ๊ตฌํ์ฒด์ธ CustomOAuth2UserService
/**
* OAuth2 ์ฌ์ฉ์ ์ ๋ณด ๋ก๋ฉ ๋ฐ ๊ถํ ์ค์ ์ ์ํ ์ปค์คํ
์๋น์ค ํด๋์ค์
๋๋ค.
*/
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
// ๊ถํ ์ฒ๋ฆฌ
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority(RolesType.ROLE_USER.name()));
// OAuth2 ๋ก๊ทธ์ธ ์ ๊ธฐ๋ณธ ํค(PK)๊ฐ ๋๋ ๊ฐ
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
Map<String, Object> attributes = oAuth2User.getAttributes();
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // naver, kakao, apple ๊ตฌ๋ถ
OAuth2UserInfo oAuth2UserInfo = getOAuth2UserInfo(registrationId, attributes);
return new CustomOAuth2User(
authorities,
attributes,
userNameAttributeName,
oAuth2UserInfo.getId(),
oAuth2UserInfo.getProvider(),
oAuth2UserInfo.getName(),
oAuth2UserInfo.getImageUrl()
);
}
private OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
return switch (registrationId) {
case "naver" -> new NaverUserInfo(attributes);
case "kakao" -> new KakaoUserInfo(attributes);
case "apple" -> new AppleUserInfo(attributes);
default -> throw new IllegalArgumentException("Unknown registrationId: " + registrationId);
};
}
}
๐ฝ AuthenticationSuccessHandler์ ๊ตฌํ์ฒด์ธ CustomOAuth2SuccessHandler
/**
* CustomOAuth2UserService ์ธ์ฆ ์ฑ๊ณต ์ ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ ํด๋์ค์
๋๋ค.
*/
@Component
@RequiredArgsConstructor
public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final MemberService memberService;
private final JwtService jwtService;
/**
* OAuth2 ํ์๊ฐ์
๋ฐ ๋ก๊ทธ์ธ
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal();
// ๊ถํ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
GrantedAuthority authority = customOAuth2User.getAuthorities().stream().findFirst()
.orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER_ROLES));
RolesType role = RolesType.valueOf(authority.getAuthority());
// CustomOAuth2User๋ฅผ MemberLoginDto๋ก ๋ณํ
MemberLoginDto memberLoginDto = MemberLoginDto.builder()
.oauthId(customOAuth2User.getOauthId())
.provider(customOAuth2User.getProvider())
.name(customOAuth2User.getName())
.profileImageUrl(customOAuth2User.getProfileImageUrl())
.role(role)
.build();
// ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ฐ JWT ํ ํฐ ์์ฑ
JwtTokenDto jwtTokens = memberService.login(memberLoginDto);
// JWT ํ ํฐ์ ์๋ต ํค๋์ ์ถ๊ฐ
jwtService.setAccessTokenToHeader(response, jwtTokens.getAccessToken());
jwtService.setRefreshTokenToHeader(response, jwtTokens.getRefreshToken());
}
}
๐ฝ AuthenticationFailureHandler์ ๊ตฌํ์ฒด์ธ CustomOAuth2FailureHandler
/**
* CustomOAuth2UserService ์ธ์ฆ ์คํจ ์ ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ ํด๋์ค์
๋๋ค.
*/
@Component
public class CustomOAuth2FailureHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Authentication failed");
errorResponse.put("message", exception.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
๐ฝ Spring Security ์ค์ ํด๋์ค์ธ SecurityConfig์ filterChain์ ๋ค์ด๊ฐ ๋ฉ์๋
/**
* Spring Security ๊ธฐ๋ณธ ์ค์ ๋ฉ์๋
*/
private void defaultSecuritySetting(HttpSecurity http) throws Exception {
http
...
// OAuth2 ๋ก๊ทธ์ธ ์ค์
.oauth2Login(oauth -> oauth
.successHandler(customOAuth2SuccessHandler)
.failureHandler(customOAuth2FailureHandler)
.userInfoEndpoint(userinfo -> userinfo
.userService(customOAuth2UserService))
);
}
๐จ ๋ฌธ์ ์ํฉ
๊ธฐํ ๋จ๊ณ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฅ๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์๋น์ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋๋๊ธฐ๋ก ํ๋ค๋ ๋ด์ฉ์ ์ ๋ฌ๋ฐ์๋ค. ๊ทธ๋ฌ๋ ํ์ฌ ๊ตฌํ๋ ๋ฐฉ์์์๋ ์์ ๋ก๊ทธ์ธ์ผ๋ก ์ฌ์ฉ์ ์ธ์ฆ๋ง ์ํ๋์ด ์๋ฒ์ ์ ์ฅ๋๊ณ , ํด๋ผ์ด์ธํธ ์ฑ์์ ๋ก๊ทธ์ธ ์ด์ ์ ์ ๋ณด๋ฅผ ์ ๋ฌํ ๊ฒฝ๋ก๊ฐ ์๋ค. ๋ฐ๋ผ์ ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฅ๋ ๋ฒ์ ๋๋ ์๋น์ ๋ฒ์ ์ค ์ด๋ค ๊ฒ์ ์ฌ์ฉํ๋์ง๋ฅผ ํ๋ณํ ์ ์๋ ์ํฉ์ด๋ค.
โ๏ธ ์์ธ ๋ถ์
- ์์ ์ค๋ช ํ OAuth ๋ก๊ทธ์ธ ์ธ์ฆ ํ๋ฆ์ ์ดํด๋ณด๋ฉด, ๋ค์ํ ๋ก์ง๋ค์ด ์คํ๋๋ฉฐ ๊ทธ ๊ณผ์ ์์ ์ฌ๋ฌ ๋ฒ ๋ฆฌ๋ค์ด๋ ํธ๊ฐ ๋ฐ์ํ์ฌ ๋ฐํ๋๋ ํ๋ผ๋ฏธํฐ ๊ฐ์ด ๊ณ์ ๋ณ๊ฒฝ๋๋ค. ์ด๋ก ์ธํด ํด๋ผ์ด์ธํธ ์ฑ์์ ํน์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ์กํ๋๋ผ๋, ์ธ์ฆ ํ์๋ ํด๋น ๊ฐ๋ค์ด ์์ค๋์ด ์ ๋ฌ๋์ง ์๋๋ค.
- ๋ํ, ๊ธฐ๋ณธ์ ์ผ๋ก DefaultOAuth2UserService๋ ์์ ๋ก๊ทธ์ธ ํ ์ฌ์ฉ์ ์ธ์ฆ๊ณผ ์ฌ์ฉ์ ์ ๋ณด ๋ก๋ฉ๋ง์ ๋ด๋นํ๋ฉฐ, ๋ก๊ทธ์ธ ์ด์ ์ ํด๋ผ์ด์ธํธ์์ ์ ๋ฌ๋ ์ ๋ณด๋ฅผ ์ ์งํ๊ณ ์ ๋ฌํ๋ ๋ก์ง์ ํฌํจ๋์ด ์์ง ์๋ค.
๐จ ํด๊ฒฐ ๋ฐฉ๋ฒ
OAuth ๊ณต์ ๋ฌธ์๋ฅผ ์ดํด๋ณด๋, state ํ๋ผ๋ฏธํฐ๋ฅผ ํตํด์ OAuth ๋ก๊ทธ์ธ ์ด์ ์ ๊ฐ์ ์๋ฒ๋ก ์ ๋ฌํ ์ ์๋ค๊ณ ํ๋ค. ๋ณธ๋ ๋ชฉ์ ์ CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง์ ์์ฒญ์ ์ ํจ์ฑ ํ์ธ์ ์ํด ์ฌ์ฉ๋์ง๋ง, ํ์ฌ ์ฐ๋ฆฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ํน์ ๊ถํ ์ ๋ณด๋ฅผ ์ ๋ฌํ๊ธฐ ์ํด ์ฌ์ฉํด ๋ดค๋ค.
๐ฝ ์๊ฐํ ๋ฐฉ๋ฒ
๐ฝ ๊ณ ๋ คํด์ผ ํ ๋ถ๋ถ
- state ๊ฐ์ ๋ฐ๋ฅธ ๊ถํ ์ฒ๋ฆฌ ๋ก์ง์ ์ถ๊ฐํด์ผ ํ๋ค.
- ๋ํ, ์ค์ํ ๋ถ๋ถ์ OAuth2AuthorizationRequestResolver๋ฅผ ๊ตฌํํด์ผ ํ๋ค๋ ๊ฒ์ด๋ค.
- OAuth2AuthorizationRequestResolver๋ OAuth2 ์ธ์ฆ ์์ฒญ์ ์ปค์คํฐ๋ง์ด์งํ ์ ์๊ฒ ํด ์ค๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก OAuth2 ์ธ์ฆ ์์ฒญ์๋ ํด๋ผ์ด์ธํธ ID, ๋ฆฌ๋ค์ด๋ ํธ URI, scope ๋ฑ์ด ํฌํจ๋๋๋ฐ, ์ถ๊ฐ์ ์ธ ํ๋ผ๋ฏธํฐ๊ฐ ํ์ํ ๊ฒฝ์ฐ ์ด๋ฅผ ์ค์ ํ ์ ์๋๋ก ๋์์ค๋ค.
โถ ๊ตฌํ ์ฝ๋
๐ฝ OAuth ์ธ์ฆ ์์ฒญ URL
http://localhost:8080/v1/oauth2/authorization/${provider}?state=${์ญํ }
๐ฝ DefaultOAuth2UserService์ ๊ตฌํ์ฒด์ธ CustomOAuth2UserService
- state ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ์ถํ์ฌ ๊ถํ ์ฒ๋ฆฌํ๋ ๋ก์ง ์ถ๊ฐ
- state ๊ฐ์ ๋ฐ๋ผ ๊ถํ์ ๊ฒฐ์ ํ๋ ๋ฉ์๋ ์ถ๊ฐ
/**
* OAuth2 ์ฌ์ฉ์ ์ ๋ณด ๋ก๋ฉ ๋ฐ ๊ถํ ์ค์ ์ ์ํ ์ปค์คํ
์๋น์ค ํด๋์ค์
๋๋ค.
*/
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
// ๊ถํ ์ฒ๋ฆฌ
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String state = request.getParameter("state");
Set<SimpleGrantedAuthority> authorities = determineAuthorities(state);
// ๊ฐ ์ถ๋ ฅ
System.out.println("state = " + state);
// OAuth2 ๋ก๊ทธ์ธ ์ ๊ธฐ๋ณธ ํค(PK)๊ฐ ๋๋ ๊ฐ
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
Map<String, Object> attributes = oAuth2User.getAttributes();
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // naver, kakao, apple ๊ตฌ๋ถ
OAuth2UserInfo oAuth2UserInfo = getOAuth2UserInfo(registrationId, attributes);
return new CustomOAuth2User(
authorities,
attributes,
userNameAttributeName,
oAuth2UserInfo.getId(),
oAuth2UserInfo.getProvider(),
oAuth2UserInfo.getName(),
oAuth2UserInfo.getImageUrl()
);
}
/**
* state ๊ฐ์ ๋ฐ๋ผ ๊ถํ์ ๊ฒฐ์
*/
private Set<SimpleGrantedAuthority> determineAuthorities(String state) {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
if ("USER".equalsIgnoreCase(state)) {
authorities.add(new SimpleGrantedAuthority(RolesType.ROLE_USER.name()));
} else if ("STORE_OWNER".equalsIgnoreCase(state)) {
authorities.add(new SimpleGrantedAuthority(RolesType.ROLE_STORE_OWNER.name()));
} else {
throw new MemberException(MemberErrorCode.BAD_REQUEST_STATE_PARAMETER);
}
return authorities;
}
private OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
return switch (registrationId) {
case "naver" -> new NaverUserInfo(attributes);
case "kakao" -> new KakaoUserInfo(attributes);
case "apple" -> new AppleUserInfo(attributes);
default -> throw new IllegalArgumentException("Unknown registrationId: " + registrationId);
};
}
}
๐ฝ OAuth2AuthorizationRequestResolver์ ๊ตฌํ์ฒด์ธ CustomAuthorizationRequestResolver
/**
* OAuth2 ์ธ์ฆ ์์ฒญ์ ๋ํด state ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๊ณ ์ปค์คํฐ๋ง์ด์งํ๋ Resolver ํด๋์ค์
๋๋ค.
*/
@Component
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;
public CustomAuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
this.defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
}
/**
* ๊ธฐ๋ณธ Resolver๋ฅผ ํตํด ์์ฒญ ์ฒ๋ฆฌ
*/
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request);
return customizeAuthorizationRequest(request, authorizationRequest);
}
/**
* ๊ธฐ๋ณธ Resolver๋ฅผ ํตํด ์์ฒญ ์ฒ๋ฆฌ
*/
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId);
return customizeAuthorizationRequest(request, authorizationRequest);
}
/**
* OAuth2 ์ธ์ฆ ์์ฒญ์ state ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๋ ๋ฉ์๋
*/
private OAuth2AuthorizationRequest customizeAuthorizationRequest(HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest) {
if (authorizationRequest == null) {
return null;
}
String state = request.getParameter("state");
if (!StringUtils.hasText(state)) {
throw new MemberException(MemberErrorCode.BAD_REQUEST_STATE_PARAMETER);
}
return OAuth2AuthorizationRequest.from(authorizationRequest)
.state(state)
.build();
}
}
๐ฝ Spring Security ์ค์ ํด๋์ค์ธ SecurityConfig์ filterChain์ ๋ค์ด๊ฐ ๋ฉ์๋์ Resolver ์ถ๊ฐ
/**
* Spring Security ๊ธฐ๋ณธ ์ค์ ๋ฉ์๋
*/
private void defaultSecuritySetting(HttpSecurity http) throws Exception {
http
...
// OAuth2 ๋ก๊ทธ์ธ ์ค์
.oauth2Login(oauth -> oauth
.successHandler(customOAuth2SuccessHandler)
.failureHandler(customOAuth2FailureHandler)
.userInfoEndpoint(userinfo -> userinfo
.userService(customOAuth2UserService))
// resolver ์ถ๊ฐ
.authorizationEndpoint(endpoint -> endpoint
.authorizationRequestResolver(customAuthorizationRequestResolver()))
);
}
๐ ๊ฒฐ๊ณผ ๊ด์ฐฐ
๐ฝ OAuth ์ธ์ฆ ์์ฒญ URL
http://localhost:8080/v1/oauth2/authorization/naver?state=USER
๐ฝ ๊ฒฐ๊ณผ
- state ๊ฐ์ด ์ ์์ ์ผ๋ก ์ ๋ฌ๋๋ค!
- FilterChain๋ ์ ์์ ์ผ๋ก ์๋ํ๋ค.
- ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ํ ์ ์์ ์ผ๋ก ์ด๋ฃจ์ด์ง๋ฉฐ, ์ฟผ๋ฆฌ๋ ์ ์คํ๋๋ค.
๐ก ๊ณ ์ฐฐ
1. state ๊ฐ์ ํตํด ํน์ ๊ฐ์ ์ ๋ฌํ๋ ๊ฒ ์ ์ ํ๊ฐ?
- state์ ๋ณธ๋ ๋ชฉ์
- state์ ๋ณธ๋ ๋ชฉ์ ์ CSRF ๋ฐฉ์ง ๋ฐ ์์ฒญ์ ์ ํจ์ฑ ๊ฒ์ฆ์ด๋ค. ์ด๋ฅผ ์ฌ์ฉ์ ์ญํ ์ด๋ ํน์ ๋น์ฆ๋์ค ๋ก์ง์ ์ ๋ฌํ๋ ์ฉ๋๋ก ์ฌ์ฉํ๋ค๋ฉด, CSRF ๋ฐฉ์ง ์ธ์ ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ํฌํจํ๊ฒ ๋๋ฏ๋ก ๋ณธ๋ ๋ชฉ์ ์ ๋ถํฉํ์ง ์์ ์ ์๋ค. ์ด๋ฌํ ์ฌ์ฉ์ ๋ณด์์ ์ธ ์ํ๊ณผ ๊ด๋ฆฌ์ ๋ณต์ก์ฑ์ ์ด๋ํ ์ ์๋ค.
- state ๋ณ์ง์ ์ํ์ฑ
- state ๊ฐ์ด ์ธ์ฆ ๊ณผ์ ์์ ์ค๊ฐ์ ๋ณ์ง๋๊ฑฐ๋ ์กฐ์๋ ๊ฒฝ์ฐ, ํ์๊ฐ์ ์ด๋ ๋ก๊ทธ์ธ ์ ์ ์์ ์ธ ๊ถํ ์ฒ๋ฆฌ๊ฐ ๋์ง ์์ ์ ์๋ค.
2. ๊ทธ๋ฆฌ๊ณ ์ฌ์ค React Native์์๋ ๋ฆฌ๋ค์ด๋ ํธ๋ฅผ ํตํด ์ ๋ณด๋ฅผ ๋ฐ๋ ๊ฒ์ด ์ด๋ ต๋ค.
- ์น์์์ OAuth ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
- DefaultOAuth2UserService๋ ์ฃผ๋ก ์น์ด๋ Spring MVC ํ๊ฒฝ์์ ์ฌ์ฉ๋๋ค. ์ด๋ฌํ ํ๊ฒฝ์์๋ ์๋ฒ๊ฐ ์ธ์ฆ ํ๋ฆ์ ์ ์ดํ๊ณ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ฉํ๋ฉฐ ์ธ์ ์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ DefaultOAuth2UserService๊ฐ ์ ํฉํ๋ค.
- React Native์์์ OAuth ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
- ๋ฐ๋ฉด, React Native์ ๊ฐ์ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์๋ ์์ ๋ก๊ทธ์ธ์ ๊ตฌํํ ๋ ๋ณดํต ํด๋ผ์ด์ธํธ ์ธก์์ ์ง์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๊ณ , ์๋ฒ์ ํต์ ํ์ฌ ํ ํฐ์ ์ ๋ฌํ๊ณ ๊ฒ์ฆํ๋ ๋ฐฉ์์ด ๋ ์ผ๋ฐ์ ์ด๋ค.
๊ทธ๋์ ์ฐ๋ฆฌ ํ๋ก์ ํธ์์๋ ํด๋น ๋ก์ง์ ๊ฐ๊ณ , ํด๋ผ์ด์ธํธ ์ธก์์ ์ง์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ผ๋ก ์๋ก ๊ตฌํํ๋ค...