우리는 지난시간에 OAuth2LoginConfigurer 에 어떻게 설정이 되는지 살펴보는 와중에 OAuth2AuthorizationRequestRedirectFilter 객체가 만들어지는것을 확인했다 우리는 진짜 로그인을 하러 이제 들어갈것이다 시큐리티는 Filter 로 이루어진 프레임워크이기 떄문에 중요한 로직들은 전부 이 ~~Filter 로 끝이 나게 됩니다 그러면 천천히 들어가보겠습니다 이 부분은 로그인 인증과 더불어서 승인코드를 발급받아서 시큐리티 안으로 들고 들어오는 역활을 하게 됩니다
승인코드
사실 승인코드라는 이름은 정식명칭이 아닌것으로 알고 있다 임시코드라고 하는 분도 있고 코드라고 말하는 사람도 있다 이 승인코드는 Authorization code grant 방식에서 로그인을 하게 되면 인가서버는 코드를 하나 return 되는데 이 코드를 가지고 인가서버에서 access_token 을 발급받을때 이 코드를 쓰게 됩니다
로그인 페이지 주소
1
2
http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/auth?response_type=code&client_id=Spring-Oauth2-Authorizaion-client&scope=email%20profile&state=KvR7XkC1EIAIjMCYxLr4Ljs_gzTuprTn5_tHWMrljY4%3D&redirect_uri=http://localhost:8081/login/oauth2/code/keycloak
로그인 페이지 주소를 보게 되면 엄청 복잡한 로그인 주소가 쓰여 있는것을 볼 수 있습나다 이 로그인 주소는 어떻게 만들어지고 요청이 이루어지는지 알아보는데 그 핵심에 OAuth2AuthorizationRequestRedirectFilter 가 존재합니다
OAuth2AuthorizationRequestRedirectFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
...
@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;
}
}
...
try {
filterChain.doFilter(request, response);
}
...
catch (Exception ex) {
...
}
}
}
필터는 한가지 특징이 무엇이냐면 무조건 doFilterInternal 를 재정의 해야 한다 이때 이 위주로 무조건 들어오기에 여기에 주석을 잡고 진행을 하겠습니다
그러면 우리는 이제 로그인을 해야 합니다 로그인을 하자마자 OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
호출이 들어온것을 확인 할 수 있다
DefaultOAuth2AuthorizationRequestResolver
1
2
3
4
5
6
7
8
9
10
11
12
13
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
}
String registrationId = resolveRegistrationId(request);
이 부분에서 registrationId 를 분리하게 되는데 filter 특정상 모든 요청이 거쳐가는것임으로 2번 정도 재호출될것이다 첫번째는 보통 파비콘이 호출됨으로 그 부분은 pass 하고 두번째 부분에서 registrationId 를 분리해해서 registrationId 값은 keycloak 을 가져오게 된다 String redirectUriAction = getAction(request, "login");
이 부분에서 다시 돌아올 수 있는 페이지를 객체로 만드는데 기본값은 login 이 된다
그리고 다음코드에서 resolve 를 호출하게 되는데
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(DEFAULT_STATE_GENERATOR.generateKey());
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
에서 저장된 ClientRegistration 을 가져오게 된다 이 값의 특징은 key 값으로 저장이 되고 key 값으로 불러올 수 있는데 여기서 key 값이 왜 keyClock 가 된것이냐면
앞에 application.properties spring.security.oauth2.client.registration.keycloak.clientId
저장을 했을때 client 부분과 provider 부분이 모두 keycloak 이 중간에 들어가게 되고 시큐리티는 이를 key 값으로 읽고 이를 저장하고 불러올 수 있게 repository 에 저장을 하게 된다
왜 이렇게 저장되는지 기억이 안난다면 https://time-kimdongy1000.github.io/posts/Spring-Security22/ 이 장에서 다루었으니 확인을 해보면 될것이다
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
이 부분에서 이제 인가서버에 무엇을 요청할지 저장을 하게 되는데 이 builder 값을 살펴보면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
builder = {OAuth2AuthorizationRequest$Builder}
authorizationUri = null
authorizationGrantType = {AuthorizationGrantType}
value = "authorization_code"
responseType = {OAuth2AuthorizationResponseType}
value = "code"
clientId = null
redirectUri = null
scopes = null
state = null
additionalParameters = {LinkedHashMap@8109} size = 0
parametersConsumer = {OAuth2AuthorizationRequest$Builder$lambda}
attributes = {LinkedHashMap} size = 1
"registration_id" -> "keycloak"
authorizationRequestUri = null
authorizationRequestUriFunction = {OAuth2AuthorizationRequest$Builder$lambda}
uriBuilderFactory = {DefaultUriBuilderFactory}
대략 authorizationGrantType 방식은 authorization_code 방식이고 우리가 return 을 받을 데이터는 code 데이터가 된다 그리고 이런 정보를 조합해서 String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
를 만들어 내는데
이 값을 살펴보면 http://localhost:8081/login/oauth2/code/keycloak 된다 이 부분은 우리가 제일 처음 keyClock 를 연동하고 admin 페이지에서 redirect url 을 설정한것을 기억할것이다 다시 사진을 살펴보면
이 부분에서 우리는 정확하게 이 Valid redirect URI 를 설정했다 이 부분은 로그인 성공후 승인코드를 발급받고 나서 인가서버가 어디로 redirect 하는지 선언하게 된다 참고로 인가서버에서 선언한 redirect url 과 시큐리티가 선언한 redirect url 이 다르면 에러가 발생한다 그럼 시큐리티는 승인코드를 가져와서 http://localhost:8081/login/oauth2/code/keycloak 주소를 호출시키는 것이다
1
2
3
4
5
builder.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(DEFAULT_STATE_GENERATOR.generateKey());
이때 이 builder 값은
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
builder = {OAuth2AuthorizationRequest$Builder}
authorizationUri = "http://localhost:8080/realms/Srping-Oauth2-Authorizaion-Project/protocol/openid-connect/auth"
authorizationGrantType = {AuthorizationGrantType}
value = "authorization_code"
responseType = {OAuth2AuthorizationResponseType}
value = "code"
clientId = "Spring-Oauth2-Authorizaion-client"
redirectUri = "http://localhost:8081/login/oauth2/code/keycloak"
scopes = {Collections$UnmodifiableSet} size = 2
0 = "email"
1 = "profile"
state = "KvR7XkC1EIAIjMCYxLr4Ljs_gzTuprTn5_tHWMrljY4="
additionalParameters = {LinkedHashMap} size = 0
parametersConsumer = {OAuth2AuthorizationRequest$Builder$lambda}
attributes = {LinkedHashMap} size = 1
authorizationRequestUri = null
authorizationRequestUriFunction = {OAuth2AuthorizationRequest$Builder$lambda}
uriBuilderFactory = {DefaultUriBuilderFactory}
이런 정보가 담길것이다 요청하는 주소는 authorizationUri 이 될것이고 authorizationGrantType authorization_code 방식이고 retun 받고자 하는 데이터는 code 가 되는것이고 이때 시큐리티가 원하는 정보는 email profile 가 되는것이고 그리고 state 가 있는데 이는 약간의 설명을 붙이자면
state
클라이언트가 임의의 값을 만들어서 code 요청에 담아서 보내고 인가서버는 이 값을 저장하고 있다가 다음 요청인 token 요청이 들어올때 이 클라이언트가 다시 한번 state 값을 보내게 되는데 이 값이 code 요청때 값하고 비교해서 일치하면 access_token 이 발급이 되고 그렇지 않으면 거절이 되는 메커니즘을 가지게 됩니다 즉 지금 현재 요청하는 클라이언트가 동일한 클라이언트에서 access_token 을 요청하는지 검증을 하게 됩니다
1
2
3
4
5
6
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
authorizationRequest 에 위해서 진행한 요청정보가 만들어지게 되고 시큐리티는 이 정보를 redirect 하게 됩니다 이 부분까지 시큐리티가 어떻게 로그인 페이지 요청을 만들어서 redirect 시키는지에 대해서 알아보았습니다 다음시간에는 진짜 로그인 을 통해서 승인코드 발급에 대해서 알아보겠습니다