<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" lang="en">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script>
function authorizationCode(){
window.location = new URL('http://localhost:8081/oauth2/authorization/keycloak1');
}
function authorizationCodeWithPKCE(){
window.location = new URL('http://localhost:8081/oauth2/authorization/keycloakWithPKCE');
}
function implicit(){
window.location = new URL('http://localhost:8081/oauth2/authorization/keycloak2');
}
</script>
</head>
<body>
<div>Welcome</div>
<div sec:authorize="isAuthenticated()"><a th:href="@{/logout}">Logout</a></div>
<form sec:authorize="isAnonymous()" action="#">
<p><input type="button" onclick="authorizationCode()" value="AuthorizationCode Grant" />
<p><input type="button" onclick="authorizationCodeWithPKCE()" value="AuthorizationCode Grant with PKCE" />
<p><input type="button" onclick="implicit()" value="Implicit Grant" />
</form>
</body>
</html>
spring:
security:
oauth2:
client:
registration:
keycloak1:
# 권한 부여 코드 승인 방식
client-id: oauth2-client-app
client-secret: ANwKmYvCcxiQbjsi4jh1JHqUWikngjec
redirect-uri: http://localhost:8081/login/oauth2/code/keycloak # /login/oauth2/code 는 스프링 시큐리티에서 정해놓은 기본값
client-name: oauth2-client-app
authorization-grant-type: authorization_code
client-authentication-method: client_secret_basic
scope:
- openid
- profile
provider: keycloak
# PKCE 권한 부여 코드 승인 방식
# client-authentication-method : none으로 해주어야 한다.
keycloakWithPKCE:
client-id: oauth2-client-app2
client-secret: 4NNnaSZpHK0Bu4m1ZtTwVC2b7cByZZRk
redirect-uri: http://localhost:8081/login/oauth2/code/keycloak
client-name: oauth2-client-app2
authorization-grant-type: authorization_code
client-authentication-method: none
scope:
- openid
- profile
provider: keycloak
# 암묵적 승인 방식
# 현재 스프링 시큐리티 6 에서는 이 방식을 지원하지 않는다.
keycloak2:
client-id: oauth2-client-app3
redirect-uri: http://localhost:8081/home
client-name: oauth2-client-app3
authorization-grant-type: implicit
client-authentication-method: none
scope:
- openid
- profile
provider: keycloak
provider:
keycloak:
issuer-uri: http://localhost:8080/realms/oauth2
authorization-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/auth
token-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/token
user-info-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/userinfo
jwk-set-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/certs
user-name-attribute: preferred_username
/**
* {@link DefaultOAuth2AuthorizationRequestResolver}의 주요 코드를 대부분 그대로 가져오고, 필요한 부분만 커스텀
*/
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
private static final Consumer<OAuth2AuthorizationRequest.Builder> DEFAULT_PKCE_APPLIER =
OAuth2AuthorizationRequestCustomizers.withPkce();
private final DefaultOAuth2AuthorizationRequestResolver delegate;
private final AntPathRequestMatcher authorizationRequestMatcher;
public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, String baseUri) {
this.authorizationRequestMatcher = new AntPathRequestMatcher(baseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
this.delegate = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, baseUri);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
//PKCE 요청일 경우 위임 후 커스텀 값 추가
if (registrationId.equals("keycloakWithPKCE")) {
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = delegate.resolve(request);
return customResolve(oAuth2AuthorizationRequest);
}
//PKCE 방식이 아닌 경우 기존에 스프링 시큐리티가 진행하던 대로 하기 위해 그대로 위임한다.
return delegate.resolve(request);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
String registrationId = resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
//PKCE 요청일 경우 위임 후 커스텀 값 추가
if (registrationId.equals("keycloakWithPKCE")) {
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = delegate.resolve(request);
return customResolve(oAuth2AuthorizationRequest);
}
//PKCE 방식이 아닌 경우 기존에 스프링 시큐리티가 진행하던 대로 하기 위해 그대로 위임한다.
return delegate.resolve(request);
}
private OAuth2AuthorizationRequest customResolve(OAuth2AuthorizationRequest oAuth2AuthorizationRequest) {
//커스텀한 값도 추가 가능
Map<String, Object> map = new HashMap<>();
map.put("customName1", "customValue1");
map.put("customName2", "customValue2");
map.put("customName3", "customValue3");
//기존 요청 객체 정보 + 커스텀 속성 값
OAuth2AuthorizationRequest.Builder build = OAuth2AuthorizationRequest
.from(oAuth2AuthorizationRequest)
.additionalParameters(map);
DEFAULT_PKCE_APPLIER.accept(build); //PKCE 방식을 위한 파라미터 추가
return build.build();
}
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher.matcher(request)
.getVariables()
.get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class OAuth2ClientConfig {
private final ClientRegistrationRepository clientRegistrationRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(request -> request
.requestMatchers("/home").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(authLogin -> authLogin
.authorizationEndpoint(
authEndPoint -> authEndPoint.authorizationRequestResolver( //추가
new CustomOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization")
)
)
)
.logout(logout -> logout.logoutSuccessUrl("/home"))
;
return http.build();
}
}