Spring Security OAuth2 - gae-jang-mo/app GitHub Wiki
๊ธฐ๋ณธ์ ์ผ๋ก OAuth2
๋ฐฉ์์๋ 4๊ฐ์ง ๋ฐฉ์์ด ์์ต๋๋ค.
- Authorization Code Grant
- Implicit Grant
- Resource Owner Password Credentials Grant
- Client Credentials Grant
๊ทธ์ค ์ฒซ๋ฒ์งธ์ธ Authorization Code Grant ๋ฐฉ์์ด ์ ์ผ ๋ง์ด ์ฌ์ฉ๋ฉ๋๋ค.

-
(A)
Resource Owner(์ฌ์ฉ์)
๋User-Agent(๋ธ๋ผ์ฐ์ )
๋ฅผ ํตํดClient(Application)
์๊ฒ domain.com/oauth2/authorization/github ๊ฒฝ๋ก๋ก ์์ฒญ์ ๋ณด๋ ๋๋ค. -
(A)
Client
๋ ์ ๊ฒฝ๋ก๋ก ๋ค์ด์จ ์์ฒญ์ ๋ํด OAuth2 ์ธ์ฆ ๋ฐฉ์ ์์ฒญ์์ ํ์ธํ๊ณAuthorization Server(๊ถํ ์๋ฒ, OAuth Provider ์๋ฒ : Github OAuth App Server)
์๊ฒ ์ ๊ทผํ ์ ์๋ ๊ฒฝ๋ก๋ฅผ Location ํด๋์ ๋ด์ ์๋ตํฉ๋๋ค. ์ด๋ Client๋ Location ์ ๋ณด๋ก authorization-endpoint (https://github.com/login/oauth/authorize)์ ์ฟผ๋ฆฌ ์คํธ๋ง์ผ๋ก Client Identifier (client-id), Redirection-URI(domain.com/login/oauth2/code/github) ๋ฑ(scope, state...)์ ๋ด์์ค๋๋ค. -
(A) 302 ๋ฆฌ๋ค์ด๋ ์ ์๋ต์ ๋ฐ์
User-Agent
๋ Location ๊ฒฝ๋ก์ ์ํดAuthorization Server
์๊ฒ ์์ฒญ์ ๋ณด๋ด๊ณ ์ด ๊ถํ ์๋ฒ๋ ์์์Client
๊ฐ ๋ด์๋์ client-id๋ฅผ ํ์ธํ์ฌ ํด๋น oauth app์ ๊ถํ ์น์ธ ํ์ด์ง๋ก ์ด๋์์ผ์ค๋๋ค. (oauth2 provider์ ๋ก๊ทธ์ธ ๋์ด ์์ง ์๋ค๋ฉด ๋ก๊ทธ์ธ์ ๋จผ์ ํ๋ผ๋ ํ์ด์ง๋ก ์ด๋์ํต๋๋ค.) -
(B)
User-Agent
์ ์น์ธํ์ด์ง๊ฐ ๋์ฐ๊ณResource Owner
๋ ๊ถํ์ ๋ถ์ฌํ๊ฑฐ๋ ๊ฑฐ์ ํฉ๋๋ค. ๊ถํ ์น์ธ(๋๋ ๊ฑฐ์ ) ์ ๋ณด๋ฅผAuthorization Server
์ ๋ณด๋ ๋๋ค. -
(C) ๋ง์ฝ
Resource Owner
๊ฐ ๊ถํ ์น์ธํ๋ค๋ฉดAuthorization Server
๋ token(code)์ ๋ฐํํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณUser-Agent
๋ฅผ ์ด์ ์ ์ ๋ฌ ๋ฐ์ Redirect-URI ๊ฒฝ๋ก(domain.com/login/oauth2/code/github)๋ก ๋ฆฌ๋ค์ด๋ ํธ ์ํต๋๋ค. ์ด๋ ๋ฆฌ๋ค์ด๋ ํธ ๊ฒฝ๋ก์ ๋ฐ๊ธํ ํ ํฐ๊ณผ ๋๋ถ์ด ์ด์ ์Client
๊ฐ ์ ๋ฌํ ์ฌ๋ฌ ์ํ ๊ฐ์ ๊ฐ์ด ๋ด์์ค๋๋ค. -
(D)
User-Agent
๋Client
๋ก ๋ฆฌ๋ค์ด๋ ํธ๋ ๊ฒ์ด๊ณClient
๋ ๋ค์Authorization Server
์๊ฒ Token-URI(https://github.com/login/oauth/access_token)๊ฒฝ๋ก์ Post ์์ฒญ์ผ๋ก Access Token์ ๋ฌ๋ผ๋ ์์ฒญ์ ๋ณด๋ ๋๋ค. ์ด๋ Access Token์ ๋ฐ๊ธํ๊ธฐ์ํ ์ธ์ฆ์ ์ํด ์ด์ ์ ๋ฐ์ token(code)๊ณผ ๋๋ถ์ดAuthorization Server
์ ๋ณด๋์๋ ์ํ๊ฐ(client-id, client-secret, redirect-uri)์ ๊ฐ์ด ๋ณด๋ ๋๋ค. -
(E)
Authorization Server
๋Client
๊ฐ ๋ณด๋ธ ๊ฐ์ ๊ฐ์ง๊ณ ํ๋นํ์ง๋ฅผ ํ์ธํ๊ณ ์ ํจํ ์ ๋ณด๊ฐ ํ์ธ๋์ ๊ฒฝ์ฐ Access Token์ ๋ฐํํ์ฌ (์ ํ์ ์ผ๋ก Refresh Token๋ ๊ฐ์ด ๋ฐํํ๋ค.)Client
๋ก ์๋ตํฉ๋๋ค.(C)์์ ๋ฐํํ token๊ณผ (E)์์ ๋ฐํํ access token์ ๋ค๋ฅธ ๊ฒ์ ๋๋ค.
-
์ถ๊ฐ๋ก
Client
๊ฐ Access Token์ ๋ฐ๊ธ ๋ฐ์ผ๋ฉด ํด๋น ํ ํฐ์ ์ด์ฉํ์ฌResource Server
์ user-Info-endpoint(https://api.github.com/user/{user-id}) ๊ฒฝ๋ก๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์์ต๋๋ค.
์ฌ์ค์ Spring Security์์ OAuth2์์ ํ์ํ ์ค์ ์ ๋๋ถ๋ถ ํด์ฃผ๊ณ ์์ต๋๋ค.
package org.springframework.security.config.oauth2.client;
public enum CommonOAuth2Provider {
...
GITHUB {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
builder.scope("read:user");
builder.authorizationUri("https://github.com/login/oauth/authorize");
builder.tokenUri("https://github.com/login/oauth/access_token");
builder.userInfoUri("https://api.github.com/user");
builder.userNameAttributeName("id");
builder.clientName("GitHub");
return builder;
}
},
...
}
์ด๋ฏธ Spring Security๊ฐ ์ CommonOAuth2Provider
์ฒ๋ผ OAuth ์ฐ๋์ ํ์ํ (github oauth์)๊ธฐ๋ณธ ์ค์ ์ ๋ณด๋ฅผ ๋ค ๋ง๋ค์ด๋์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋๋ก ์ ๊ณตํ๊ณ ์์ต๋๋ค.
Authorization Code Grant Flow์์ ์ด์ผ๊ธฐํ Authorization Server
๋ก ์ ๊ทผํ๊ธฐ ์ํ authorization-endpoint, Access Token์ ๋ฐ๊ธ๋ฐ๊ธฐ ์ํ uri, ์ธ๊ฐ๋ฅผ ๋ฐ๊ณ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ๊ธฐ์ํ userInfo-endpoint ๋ฑ CommonOAuth2Provider.GITHUB
์์ ์ ์๋ ๊ฐ์ผ๋ก ์๋ ์ค์ ๋ ๊ฒ์
๋๋ค.
์ถ๊ฐ๋ก ์ฒ์ ์ฌ์ฉ์(Resource Owner
)๊ฐ oauth ์ธ์ฆ์ ํ๊ธฐ์ํ ์๋์ ์๋ฐ์ (A)์ผ๋ก ์ ๊ทผํ๋ ๊ฒฝ๋ก domain.com/oauth2/authorization/github ๋ํ Spring Security๊ฐ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ ์ค์ ๊ฐ์
๋๋ค.
๊ทธ๋์ ์ฐ๋ฆฌ๋ ์๋์ฒ๋ผ ๋๊ฐ์ง(client-id / client-secret)๋ง ์ค์ ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
# application.yml
spring:
security:
oauth2:
client:
registration:
github:
client-id: <ํด๋ผ์ด์ธํธ ID>
client-secret: <ํด๋ผ์ด์ธํธ SECRET>
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login(); // ๊ธฐ๋ณธ oauth login ์ ์ฉ
}
}
.oauth2Login()
์ ์ ์ฉํจ์ผ๋ก์จ Spring Security ์ ์๋ก์ด ํํฐ๊ฐ ๋๊ฐ๊ฐ ์ถ๊ฐ๋ฉ๋๋ค.
์ด ํํฐ๊ฐ OAuth2 ์ ์ฉ์ ์ํ ์ค์ ๊ณผ ํต์ ์ ๋ด๋นํ๊ฒ ๋ฉ๋๋ค.
class org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter
class org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter
์ Authorization Code Grant Flow ์์ (A), (B), (C) ๊น์ง User-Agent(๋ธ๋ผ์ฐ์ )
์ Authorization Server
๊ฐ์ ํต์ ์ด์๊ณ (D), (E)๊ฐ Client
์ Authorization / Resource Server
๊ฐ์ ํต์ ์
๋๋ค.
๋ธ๋ผ์ฐ์ ๋ฅผ ํตํ ํต์ ์ ๊ทธ๋ ๋ค ์ณ๋ ์๋ฒ๊ฐ(Client
- Authorization Server
) ํต์ ์ ์ด๋ป๊ฒ ์ด๋ค์ง๊น?
package org.springframework.security.oauth2.client.endpoint;
// Access Token ๋ฐ๊ธ ๊ณผ์
public class DefaultAuthorizationCodeTokenResponseClient {
...
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
...
// post request ์์ฑ
// public T convert(S s) {
// ...
// // ๊ธฐ์กด์ Spring Security๊ฐ ์ค์ ํ token uri ๊ฐ์ ๋ถ๋ฌ์ uri ๊ฒฝ๋ก๋ก ์ค์
// URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
// .build()
// .toUri();
// return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
// }
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response;
try {
// RestOperation์ ์ด์ฉํ ํต์ ์ ์งํ
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
OAuth2AccessTokenResponse tokenResponse = response.getBody();
...
}
...
}
์ ์ฝ๋์ ๋์์๋ค์ํผ this.requestEntityConverter.convert(authorizationCodeGrantRequest);
๋ฅผ ํตํด Http Request๋ฅผ Access Token์ ๋ฐ๊ธ๋ฐ๊ธฐ ์ํ ์์ฒญ ๊ท์ฝ์ ๋ง๊ฒ ์์ฑํ๊ณ this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
๋ก ํต์ ์ ํ๊ฒ ๋ฉ๋๋ค.
SecurityConfig
Spring Security ์ค์ ์์ .oauth2Login()
๋ง์ผ๋ก ์ค์ ํด๋จ๊ธฐ ๋๋ฌธ์ Default๋ก ์ค์ ๋ ํด๋์คDefaultAuthorizationCodeTokenResponseClient
์ ์ํด oauth ์ธ์ฆ - ์ธ๊ฐ๊ฐ ์ฒ๋ฆฌ๋ฉ๋๋ค.
package org.springframework.security.oauth2.client.userinfo;
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
...
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
...
// convert() ๋ก request ์์ฒญ ์์ฑ
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<Map<String, Object>> response;
try {
// Authorization Server์ ํต์
response = this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
} catch (OAuth2AuthorizationException ex) {
...
}
Map<String, Object> userAttributes = response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
...
}
Access Token์ ๋ฐ์์ค๋ ๋ฐฉ์๊ณผ ๋น์ทํ๊ฒ DefaultOAuth2UserService
์์ user-info-endpoint ๊ฒฝ๋ก๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์์ต๋๋ค.
package org.springframework.security.oauth2.client.userinfo;
public class OAuth2UserRequestEntityConverter {
...
@Override
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
...
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// ๊ธฐ์กด์ Spring Security๊ฐ ์ค์ ํ user-info-endpoint์ ๊ฐ์ ๋ถ๋ฌ์ uri ๊ฒฝ๋ก ์ค์
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
.build()
.toUri();
...
return request;
}
...
}
Client
- Authorization Server
ํต์ ๋ด๋น ํด๋์ค
์์์ ์ค๋ช
ํ Access Token ๋ฐ๊ธ ๋ก์ง, ์ฌ์ฉ์ ์ ๋ณด ์กฐํ ๋ก์ง์ ํธ์ถํ๋ ํด๋์ค๋ค.
package org.springframework.security.oauth2.client.authentication;
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
OAuth2AccessTokenResponse accessTokenResponse;
try {
OAuth2AuthorizationExchangeValidator.validate(
authorizationCodeAuthentication.getAuthorizationExchange());
// access token ๋ฐ๊ธ
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// ์์์ ๋ฐ์์จ accessToken ์ถ์ถ
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
// ์ธ๊ฐ๋ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ด with accessToken
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
...
// ์์์ ๊ฐ์ ธ์จ ์ ๋ณด ์ ์ฅ
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
accessTokenResponse.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
...
}
์์์ ์ ์ฅ๋OAuth2LoginAuthenticationToken authenticationResult
๋ ์ถํ ์ฌ๋ฌ ๊ถํ ๊ฐ๊ณผ ํจ๊ป OAuth2AuthenticationToken
ํด๋์ค๋ฅผ ์์ฑํ๊ณ SecurityContextHolder์ ์ ์ฅ๋ฉ๋๋ค.
SecurityContextHolder์ ๋ฑ๋ก๋ ์ฌ๋ฌ ์ปจํ ์คํธ๋ค์ oauth ์ธ์ฆ - ์ธ๊ฐ ํ๋ฆ์ ๋ง๋ ์๋ช ์ฃผ๊ธฐ๋ก ๊ด๋ฆฌ๋ฉ๋๋ค.
package com.gaejangmo.apiserver;
@ResController
public class Controller {
@GetMapping("/access_token")
public String index(OAuth2AuthenticationToken authenticationToken) {
// SecurityContextHolder์ ์ ์ฅ๋ ์ฌ์ฉ์ ์ ๋ณด ์ฌ์ฉ
log.info("authenticationToken {}", authenticationToken);
return null;
}
}
๊ทธ๋์ ์์ ๊ฐ์ด ์ฐ๋ฆฌ๋ ์ปจํธ๋กค๋ฌ์์ oauth ์ธ๊ฐ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ OAuth2AuthenticationToken
๊ฐ์ฒด๋ฅผ ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.