์์ฆ์ OAuth2๋ฅผ ์ฌ์ฉํ์ฌ ์์ ๋ก๊ทธ์ธ์ ํตํด ๊ฐ์ ์ด ๊ฐ๋ฅํ ์๋น์ค๋ค์ด ๊ต์ฅํ ๋ง๋ค.
์คํ๋ง ์ํ๋ฆฌํฐ์์๋ OAuth2๋ฅผ ์ด์ฉํ ์์ ๋ก๊ทธ์ธ ๋ฐฉ์์ ์ง์ํ๋๋ฐ, ํ์๋ ๋งจ ์ฒ์ ์ด๋ฅผ ์ฌ์ฉํ ๋ ๋๋ฌด ์ด๋ ค์ ์๋ค.
์ง๊ธ๋ถํฐ ์ํ๋ฆฌํฐ์์ OAuht2๊ฐ ์ด๋ป๊ฒ ๋์ํ๋์ง ์์๋ณด์.
์์กด์ฑ ๊ด๋ฆฌ
์ฐ์ OAuth2๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด OAuth2-client ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด์ผ ํ๋ค.
build.gradle ์ ๋ค์์ ์ถ๊ฐํ์.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
oauth2Login() ์ ์๋๋ฐฉ์
WebSecurityConfigurerAdapter ๋ฅผ ์์๋ฐ์ Security Config ํ์ผ์์ oauth2 ๋ก๊ทธ์ธ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํด์ค๋ค.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login();
}
}
oauth2Login()์ ์์๊ฐ์ด ํ์ฑํ ์์ผ์ค์ผ๋ก์จ ์ฌ์ฉํ ์ ์๋๋ฐ ์ด์ ํ๋ฒ ์ฝ๋๋ฅผ ์ดํด๋ณด์.
OAuth2LoginConfigurer
oauth2Login()์ ํ์ฑํ ์ํค๋ฉด OAuth2LoginConfigurer๊ฐ ํ์ฑํ๋๋ค.
AbstractAuthenticationFilterConfigurer๋ฅผ ์์๋ฐ์ผ๋ฉฐ, Filter๋ก๋ OAuth2LoginAuthenticationFilter๋ฅผ ์ฌ์ฉํ๋ค.
AbstractAuthenticationFilterConfigurer๋ฅผ ์์๋ฐ์ Configurer ์ค์๋ Form๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ฅผ ํ ๋ ์ฌ์ฉํ๋ FormLoginConfigurer๋ ์๋ค. (formLogin()์ผ๋ก ํ์ฑํ ์ํฌ ์ ์๋ค.)
์์ฒญ์ ์ฒ๋ฆฌ ํ๋ฆ
๋๋ฉ์ธ/oauth2/authorization/{์๋ฌด๊ฑฐ๋} ๋ฅผ ์ ๋ ฅํ์ฌ ์์ฒญ์ ๋ณด๋๋ค๊ณ ๊ฐ์ ํ์. (ํ์๋ /oauth2/authorization/dd ๋ก ์์ฒญ์ ๋ณด๋๋ค.)
๋๋ฒ๊น ์ ํด๋ณด์!
์ฒ์์ผ๋ก OAuth2๊ด๋ จ๋ ํํฐ๊ฐ ์ ์ฉ๋๋ ๊ฒ์ด OAuth2AuthorizationRequestRedirectFilter์ด๋ค.
OAuth2AuthorizationRequestRedirectFilter์ ์ฝ๋๋ฅผ ์ดํด๋ณด์
OAuth2AuthorizationRequestRedirectFilter
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
/**
* The default base {@code URI} used for authorization requests.
*/
public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();
private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();
private RequestCache requestCache = new HttpSessionRequestCache();
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
}
catch (Exception ex) {
this.unsuccessfulRedirectForAuthorization(request, response, ex);
return;
}
...
}
}
์ค์ํ ๋ถ๋ถ์ ๋ค์๊ณผ ๊ฐ๋ค.
/oauth2/authorization์ผ๋ก ๋ค์ด์ค๋ ์์ฒญ์ ๋ํด์ ์ด ํํฐ๊ฐ ์๋ํ๋ค๋๊ฒ์ ์ ์ ์๋ค.
ํ์๋ http://localhost:8080//oauth2/authorization/dd๋ก ์์ฒญ์ ๋ณด๋๊ณ , ๋ฐ๋ผ์ OAuth2AuthorizationRequestRedirectFilter์ doFilterInternal()์ด ์๋ํ๋ค.
์ฌ๊ธฐ์ this.authorizationRequestResolver๋ OAuth2AuthorizationRequestResolver๊ฐ์ฒด์ด๋ฉฐ, ์ด๋ ์คํ๋๋ OAuth2AuthorizationRequestResolver๋ DefaultOAuth2AuthorizationRequestResolver ์ด๋ค. (์ฌ์ค ์ด๊ฑฐ ํ๋๋ฐ์ ๊ตฌํ์ฒด๊ฐ ์๋ค.)
์ฐ์ DefaultOAuth2AuthorizationRequestResolver์ ์ค์ํ ์ฝ๋๋ฅผ ์ดํด๋ณด๋๋ก ํ์.
DefaultOAuth2AuthorizationRequestResolver
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
private static final char PATH_DELIMITER = '/';
private final ClientRegistrationRepository clientRegistrationRepository;
private final AntPathRequestMatcher authorizationRequestMatcher;
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 96);
...
public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository,
String authorizationRequestBaseUri) {
...
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizationRequestMatcher = new AntPathRequestMatcher(
authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId,
String redirectUriAction) {
if (registrationId == null) {
return null;
}
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
}
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration, attributes);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
...
}
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher.matcher(request).getVariables()
.get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
}
์ฐ์ ์์ฑ์๋ฅผ ์ดํด๋ณด์
(์ ์๋ณด์ด๋ฏ๋ก, ์ฌ์ง์ ํด๋ฆญํ์ฌ ํ๋ํด์ ๋ณด์.)
- authorizationRequestBaseUri
authorizationRequestBaseUri๋ OAuth2AuthorizationRequestRedirectFilter์ DEFAULT_AUTHORIZATION_REQUEST_BASE_URI (=/oauth2/authorization)๋ฅผ ๋ฐ์์ค๋ฉฐ, ์ด๋ฅผ ์ฌ์ฉํด
authorizationRequestMatcher๋ฅผ ์ธํ ํ๋ค.
(authorizationRequestMatcher = /oauth2/authorization/{registrationId})
- ClientRegistrationRepository
ClientRegistrationRepository๋ ์ต์ด ์ ํ๋ฆฌ์ผ์ด์ ์คํ ์ propertiesํ์ผ ํน์ yml ํ์ผ์ ์กด์ฌํ๋ oauth2์ ์ค์ ์ ํตํด registrationId๋ฅผ ClientRegistrationRepository์ ์ ์ฅ์์ผ ๋๊ณ , ๊ทธ๊ฒ์ ์ฃผ์ ํ๋ค.
[์ฐธ๊ณ ]
google ๋ฑ๊ณผ ๊ฐ์ด ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ์๋น์ค์ ๊ฒฝ์ฐ์๋ registration์ผ๋ก ๋ฑ๋กํ๊ธฐ ์ํด์๋
์๋์ ๊ฐ์ด client-id๋ง ๊ธฐ๋ณธ์ ์ผ๋ก ์ค์ ํ๋ฉด ๋ฑ๋ก๋๋ค(client-secret ๋ฑ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ ์ถ๊ฐํด์ฃผ์ด์ผ ํจ)
๋ง์ฝ ์ํ๋ฆฌํฐ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ์๋น์ค๊ฐ ์๋ ๊ฒฝ์ฐ(์๋ฅผ ๋ค์ด ์นด์นด์ค) ๋ค์๊ณผ ๊ฐ์ด ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค.
์ถ๊ฐ๋ก registration๋ฅผ ๋ฑ๋กํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด 5๊ฐ์ ์ค์ ์ ํ์์ ์ผ๋ก ํ์ํ๋ฉฐ, ์ถ๊ฐ ์ ๋ณด(token-uri ๋ฑ)๊ฐ ํ์ํ ๊ฒฝ์ฐ ์์ kakao ์์์ ๊ฐ์ด ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค. (์๋์์ ๋ณด์ฌ์ง๋ 5๊ฐ์ง ์ค์ ์ ํ๋๋ผ๋ ๋น ์ง๊ฒฝ์ฐ ์์ธ๊ฐ ๋ฐ์ํจ.)
์ฐธ๊ณ - ์ํ๋ฆฌํฐ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ oauth2 client
package org.springframework.security.config.oauth2.client;
public enum CommonOAuth2Provider {
GOOGLE {
...
},
GITHUB {
...
},
FACEBOOK {
...
}
},
OKTA {
...
};
private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
...
}
Google, Github, Facebook, Okta๊ฐ ์๋ค.
์์ฒญ์ด ๋ค์ด์จ ๊ฒฝ์ฐ
ํ์๋ /oauth2/authorization/dd ๋ก ์์ฒญ์ ๋ณด๋๋ค๊ณ ํ์๋ค.
์์ฒญ์ด ๋ค์ด์ค๋ฉด ์์์ ์ดํด๋ณธ ๋ฐ์ ๊ฐ์ด OAuth2AuthorizationRequestRedirectFilter๊ฐ ์๋ํ์ฌ DefaultOAuth2AuthorizationRequestResolver์ resolve๋ฅผ ํธ์ถํ๋๋ฐ, ์ด์ resolve ์ฝ๋๋ฅผ ์ดํด๋ณด์.
this.resolveRegistrationId()๋ฅผ ํธ์ถํ๋ค.
resolveRegistrationId() ๋ this.authorizationRequestMatcher์ matches๋ฅผ ํธ์ถํ๋ค.
์๊น ์ดํด๋ดค๋ฏ์ด, authorizationRequestMatcher์๋ /oauth2/authorization/{registrationId}๊ฐ ๋ค์ด์์ผ๋ฉฐ, ๊ฒฐ๊ตญ ๋ค์ด์จ ์์ฒญ์ด /oauth2/authorization/{registrationId} ์ธ ๊ฒฝ์ฐ if๋ฌธ์ ์กฐ๊ฑด์ ํด๋น๋๋ฏ๋ก ๋ค์ ์ฝ๋๊ฐ ์คํ๋๋ค.
this.authorizationRequestMatcher.matcher(request).getVariables()
.get(REGISTRATION_ID_URI_VARIABLE_NAME);
์์ ์ฝ๋์ ์คํ ๊ฒฐ๊ณผ๋ก /oauth2/authorization/{registrationId} ์์ registrationId๋ง ์ถ์ถ๋๊ณ ์ด๋ฅผ ๋ฐํํ๋ค.
๋ค์ resolve๋ก ๋์๊ฐ๋ฉด, registrationId๋ฅผ ์ถ์ถ ์ดํ ๋ง์ง๋ง์ ์๋ ์ฝ๋๊ฐ ์คํ๋๋ ๊ฒ์ ์ ์ ์๋ค.
return resolve(request, registrationId, redirectUriAction);
์ด์ resolve(request, registrationId, redirectUriAction); ์ฝ๋๋ฅผ ์ดํด๋ณด์.
clientRegistrationRepository์๋ ์ฐ๋ฆฌ๊ฐ propertiesํ์ผ ํน์ yml ํ์ผ์์ ์ค์ ํ registration ์ ๋ณด๊ฐ ๋ค์ด์์ผ๋ฉฐ, ์ด๋ ์(resolve(HttpServletRequest request))์์ ์ถ์ถํ registrationId ๋ฅผ ํตํด ์ฐพ์์์, ๋ง์ฝ ๋ฑ๋ก๋ registration์ด ์๋ค๋ฉด ๋ก์ง์ ์งํํ๊ณ ์๋ค๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
ํ์๋ ๋ฑ๋กํ์ง ์์ dd๋ก ์์ฒญ์ ๋ณด๋๊ธฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ์ฌ ํ๊ธฐ๋ ๊ฒ์ ์ ์ ์๋ค.
์ฑ๊ณต์ ์ธ ์์ฒญ ์ ํ๋ฆ
๊ทธ๋ผ ์ด์ Google์ ํตํด ๋ก๊ทธ์ธ์ ์งํํด ๋ณด๋๋ก ํ์. (/oauth2/authorization/google)
(Application.properties ํ์ผ์, ์๋์ ๊ฐ์ด ๋ฑ๋กํด ์ฃผ์๊ธฐ ๋๋ฌธ์ google๋ก ์ฌ์ฉ ๊ฐ๋ฅํ๋ค)
์์์ ํ์ธํ ๊ฒ๊ณผ ๊ฐ์ด OAuth2AuthorizationRequestRedirectFilter๋ฅผ ํต๊ณผํ๋ค. ์ดํ ๊ณผ์ ์ ์ดํด๋ณด์.
OAuth2LoginAuthenticationProvider์ authenticate()๊ฐ ์คํ๋๋ค.
OAuth2LoginAuthenticationProvider์ authenticate() ์์๋ userService.loadUser()๊ฐ ์คํ๋๋ค.
์ด๋ ์คํ๋๋ userService๋ DefaultOAuth2UserService์ด๋ค.
(์ด๋ฅผ ๋ฐ๊ฟ์ฃผ๊ณ ์ถ๋ค๋ฉด config ํ์ผ์์ oauth2Login().userInfoEndpoint().userService(OAuth2UserService<OAuth2UserRequest, OAuth2User> ๊ตฌํ์ฒด)๋ฅผ ํตํด userService๋ฅผ ์ค์ ํด์ฃผ๋ฉด ๋๋ค.
(๋งจ ์๋์์ ์์ธํ ์ค๋ช ํ๊ฒ ๋ค.)
DefaultOAuth2UserService
DefaultOAuth2UserService์ loadUser()๋ฅผ ์ดํด๋ณด์.
์ค๊ฐ ๊ณผ์ ์ ๋ณต์กํ๋ ์๋ตํ๊ณ , ๊ฒฐ๊ตญ DefaultOAuth2User๊ฐ ๋ฆฌํด๋๋ ๊ฒ์ ์ ์ ์๋ค.
DefaultOAuth2User๋ ๋ค์๊ณผ ๊ฐ์ ํ๋๋ค๋ก ๊ตฌ์ฑ๋์ด ์๋ค.
(authorities๋ ๊ถํ, attribute๋ ๋ก๊ทธ์ธ API์์ ๋ฐํํ๋ ๊ฐ(JSON)์ ๋ด๊ณ ์๋ ๊ฐ, nameAttributeKey๋ ์์ง ์ ๋ชจ๋ฅด๊ฒ ๋ค..ใ ์ฝ๊ฐ ํฅ๋ก๋ ์ฑ ๋ณด๋ฉด OAuth2 ๋ก๊ทธ์ธ ์งํ ์ ํค๊ฐ ๋๋ ํ๋๊ฐ, Primary Key์ ๊ฐ์ ์๋ฏธ๋ผ ๋์์๋๋ฐ, ๋๋์ ์ดํด๊ฐ ๋์ง๋ง ์ ๋ชจ๋ฅด๊ฒ ๋ด..)
SecurityContextHolder์ Authentication ์ ์ฅ
๊ทธ๋ ๊ฒ ๊ณ์ํด์ ๋ก๊ทธ์ธ์ ์งํํ๋ฉด, AbstractAuthenticationProcessingFilter์ ๋๋ฌํ๋๋ฐ, ๋ง์ฝ ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ค๋ฉด successfulAuthentication์ด ์๋ํ๋ค.
successfulAuthentication์์ SecurityContextHolder์ SecurityContext์ Authentication์ ์ ์ฅํด์ค context๋ฅผ set ํด์ฃผ๋๊ฒ์ ์ ์ ์๋ค. ์ฆ ์ฌ๊ธฐ์ ๊ถํ ์ค์ ์ด ๋๋๊ฒ์ด๋ค.
์ดํ successHandler์ onAuthenticationSuccess๋ฅผ ํธ์ถํด ์ฑ๊ณต ์ฒ๋ฆฌ๋ฅผ ํ๋๊ฒ์ ์ ์ ์๋ค.
OAuth2UserService<OAuth2UserRequest, OAuth2User> ์ปค์คํ ํ๊ธฐ
์๊น ์์์ ์ค๋ช ํ๋ OAuth2LoginAuthenticationProvider ์ authenticate์์ DefaultOAuth2UserService์ ๋์ ์์ ์ด ์ปค์คํ ํ userService๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์์๋ณด์.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//== ์์
๋ก๊ทธ์ธ ์ค์ ==//
//.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
์์๊ฐ์ด ์ค์ ํ์ฌ OAuth2UserService๋ฅผ ์ปค์คํ ํ์ฌ ์ฌ์ฉํ ์ ์๋ค. ์ด๋ฅผ ํตํด ์ํ๋ฆฌํฐ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ์ง ์๋ Kakao ๋ฑ์ ๋ก๊ทธ์ธ๋ ์ฒ๋ฆฌํ ์ ์๋ค.
์ฐธ๊ณ ๋ก OAuth2LoginConfigurer ์ init()์์
์์ ๊ฐ์ด getOAuth2UserService()๋ฅผ ์คํํด ๊ฐ์ ธ์จ oauth2UserService๋ฅผ Provider์๊ฒ ์ง์ ํด์ฃผ๋๊ฒ์ ์ ์ ์๋ค.
getOAuth2UserService()์ ๋ณด๋ฉด userInfoEndpointConfig์ ์ ์ฉํ userService๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ๋๋ฌธ์, userInfoEndpoint().userService(customOAuth2UserService); ๋ฅผ ํตํด userService๋ฅผ ์ค์ ํด ์ค ์ ์๋ค.
(๋ง์ฝ ์ค์ ์ ์ํด์ค๋ค๋ฉด DefaultOAuth2UserService๋ฅผ ์ฌ์ฉํ๋ค.)
์๋ฅผ ์ด์ฉํ ์นด์นด์ค ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ ์๋ ํฌ์คํ ์ ์ฐธ๊ณ ํ๋๋ก ํ์.
ํ๋ฆ ์ ๋ฆฌ
Http ์์ฒญ [/oauth2/authorization/{์๋ฌด๊ฑฐ๋}] ->
OAuth2AuthorizationRequestRedirectFilter ์๋ ->
DefaultOAuth2AuthorizationRequestResolver ์๋ registrationId ๋ฐ์์ด ->
Application.properties์ ๋ฑ๋ก๋ registrationId ๋ก ๋ณด๋ด์ง ์์ฒญ์ด๋ผ๋ฉด
OAuth2LoginAuthenticationProvider์ authenticate() ์คํ ->
userService(DefaultOAuth2UserService)์ loadUser() ์คํ->
DefaultOAuth2User ๋ฆฌํด ->
AbstractAuthenticationProcessingFilter์ ๋๋ฌ ->
AbstractAuthenticationProcessingFilter.successfulAuthentication() ์๋->
SecurityContextHolder ์์ Authentication ์ ์ฅ ->
successHandler.onAuthenticationSuccess() ํธ์ถ