728x90

- AccessToken을 재발급받기 위해 RefreshToken을 사용한다.
- AccessToken은 보통 5~10분 정도의 만료시간을 가지며, 만약 유출되었을 경우에도 10분 이후에는 유출된 토큰을 더 이상 사용 못하게 한다.
- 5~10분마다 재인증을 받을 경우 번거롭기 때문에 RefreshToken을 사용해, AccessToken이 만료되더라도 RefreshToken을 통해 재인증을 받지 않고 발급받을 수 있도록 한다.
- PasswordOAuth2AuthorizedClientProvider에서 OAuth2AuthorizedClient 정보가 있고, token이 만료되었으며, refresh 토큰이 만료되지 않았다면, RefreshTokenOAuth2AuthrizedClientProvider로 토큰 재 발급 처리를 위임한다.
- 인가 서버 입장에서는 사용자가 로그인 하고, 동의하는 과정을 거쳐야 하는데 이 과정 없이 토큰을 발급해 줄 수 있다.
- RefreshTokenOAuth2AuthrizedClientProvider에서 OAuth2AuthoriedClient 있고, RefreshToken이 있고, token이 만료되었을 경우 DefaultRefreshTokenTokenResponseClient에서 DefaultRefreshTokenGrantRequestEntityConverter를 통해 RestTemplate 요청을 하고 인가 서버에서 OAuth2AccessTokenResponse를 응답받는다.
Test
AppConfig
@Configuration
public class AppConfig {
@Bean
public DefaultOAuth2AuthorizedClientManager auth2AuthorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository clientRepository) {
OAuth2AuthorizedClientProvider auth2AuthorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
//해당 설정을 통해 AccessToken이 만료 시간을 빨리 만료되게끔 한다(내부적으로 해당 값을 빼는 코드가 있다)
.password(passwordGrantBuilder -> passwordGrantBuilder.clockSkew(Duration.ofSeconds(3600)))
.clientCredentials()
.refreshToken(refreshTokenGrantBuilder -> refreshTokenGrantBuilder.clockSkew(Duration.ofSeconds(3600)))
.build();
DefaultOAuth2AuthorizedClientManager oAuth2AuthorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, clientRepository);
oAuth2AuthorizedClientManager.setAuthorizedClientProvider(auth2AuthorizedClientProvider);
oAuth2AuthorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return oAuth2AuthorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return oAuth2AuthorizeRequest -> {
Map<String, Object> contextAttributes = new HashMap<>();
HttpServletRequest request = oAuth2AuthorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
String password = request.getParameter(OAuth2ParameterNames.PASSWORD);
if(StringUtils.hasText(username) && StringUtils.hasText(password)){
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
}
- AccessToken이 빨리 만료된것 처럼 느끼도록 clockSkew 값을 세팅한다
- 내부적으로 해당 값을 빼서, AccessToken 만료 시간을 비교하는 로직이 있다
- KeyCloak에서 AccessToken 만료시간을 설정할 수 있으며 기본 5분이다.
application.yml
server:
port: 8081
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: oauth2-client-app
client-name: oauth2-client-app
client-secret: xSlqD456gfAeLZO93BLbTwQys0NEc8KL
authorization-grant-type: password
scope:
- profile
- openid
client-authentication-method: client_secret_basic
provider: keycloak
# keycloak:
# client-id: oauth2-client-app
# client-secret: xSlqD456gfAeLZO93BLbTwQys0NEc8KL
# client-name: oauth2-client-app
# authorization-grant-type: client_credentials
# client-authentication-method: client_secret_basic
# provider: keycloak
provider:
keycloak:
authorization-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/auth
issuer-uri: http://localhost:8080/realms/oauth2
jwk-set-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/certs
token-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/token
user-info-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/userinfo
user-name-attribute: preferred_username
- password 방식으로 동작시킨다.
- password 방식 내부에서 RefreshToken이 존재, AccessToken 이 만료, authorizedClient가 존재하는지 확인하는 코드가 있다
- 위 조건 모두 만족시, 인가 서버로 부터 다시 인증요청을 하지 않고 AccessToken을 재발급받는다.
LoginController
@Controller
public class LoginController {
@Autowired
private DefaultOAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
@Autowired
private OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;
private Duration clockSkew = Duration.ofSeconds(3600);
private Clock clock = Clock.systemUTC();
@GetMapping("/oauth2Login")
public String oauth2Login(Model model, HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthorizeRequest auth2AuthorizeRequest =
OAuth2AuthorizeRequest.withClientRegistrationId("keycloak")
.principal(authentication)
.attribute(HttpServletRequest.class.getName(), request)
.attribute(HttpServletResponse.class.getName(), response)
.build();
OAuth2AuthorizationSuccessHandler successHandler = (authorizedClient, principal, attributes) -> {
oAuth2AuthorizedClientRepository
.saveAuthorizedClient(authorizedClient, principal,
(HttpServletRequest) attributes.get(HttpServletRequest.class.getName()),
(HttpServletResponse) attributes.get(HttpServletResponse.class.getName()));
System.out.println("authorizedClient = " + authorizedClient);
System.out.println("principal = " + principal);
System.out.println("attributes = " + attributes);
};
oAuth2AuthorizedClientManager.setAuthorizationSuccessHandler(successHandler);
OAuth2AuthorizedClient authorizedClient = oAuth2AuthorizedClientManager.authorize(
auth2AuthorizeRequest);//authorizedClient에 토큰이 있지만 바로 만료되어 있다
// 권한부여 타입을 변경하지 않고 실행
// if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken())
// && authorizedClient.getRefreshToken() != null) {
// //authorizedClient 존재하고, AccessToken이 만료 되어 있고, RefreshToken이 만료되지 않았을 경우
// oAuth2AuthorizedClientManager.authorize(auth2AuthorizeRequest);
// }
//권한부여 타입을 변경하고 실행
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken())
&& authorizedClient.getRefreshToken() != null) {
//아래 클래스에 대한 재정의가 필요하므로 재생성 한것이다.
ClientRegistration clientRegistration = ClientRegistration.withClientRegistration(
authorizedClient.getClientRegistration())
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.build();
OAuth2AuthorizedClient oAuth2AuthorizedClient = new OAuth2AuthorizedClient(
clientRegistration,
authorizedClient.getPrincipalName(),
authorizedClient.getAccessToken(),
authorizedClient.getRefreshToken()
);
OAuth2AuthorizeRequest auth2AuthorizeRequest2 =
OAuth2AuthorizeRequest
.withAuthorizedClient(oAuth2AuthorizedClient)
.principal(authentication)
.attribute(HttpServletRequest.class.getName(), request)
.attribute(HttpServletResponse.class.getName(), response)
.build();
oAuth2AuthorizedClientManager.authorize(auth2AuthorizeRequest2);
}
model.addAttribute("AccessToken", authorizedClient.getAccessToken().getTokenValue());
model.addAttribute("RefreshToken", authorizedClient.getRefreshToken().getTokenValue());
return "home";
}
private boolean hasTokenExpired(OAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
@GetMapping("/logout")
public String logout(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.logout(request, response, authentication);
return "redirect:/";
}
}
- 권한 부여 타입을 변경하지 않고 실행하는 방법
- 비교적 간단하며, 쉽게 AccessToken을 재발급받아 인증을 한다.
- 권한 부여 타입을 변경하고 실행하는 방법
- 권한 부여 타입을 변경하지 않고 실행하게 되면, 기본적으로 Password 방식으로 동작하기 때문에 Password 인증에 관한 동작을 하게 되는데, 코드에서 이를 RefreshToken 방식으로 변경할 수 있다.
- 코드가 길어지는 단점이 있다.(설정해야 하는 값들이 많아진다)
- 코드에서 강제로 authorizedClient.ClientRegistration.GrantType을 RefreshToken으로 변경하며, 변경된 authorizedClient를 통해 인증을 진행한다.
728x90
'스프링 시큐리티 OAuth2 > OAuth 2.0 Client - oauth2Client()' 카테고리의 다른 글
| @RegisteredOAuth2AuthorizedClient 이해 및 활용 (0) | 2023.02.24 |
|---|---|
| DefaultOAuth2AuthorizedClientManager -필터 기반으로 구현하기 (0) | 2023.02.23 |
| DefaultOAuth2AuthorizedClientManager - Client Credentials 권한 부여 구현하기 (0) | 2023.02.21 |
| DefaultOAuth2AuthorizedClientManager - Resource Owner Password 권한 부여 구현하기 (0) | 2023.02.20 |
| DefaultOAuth2AuthorizedClientManager (0) | 2023.01.29 |