[Spring Security6] 인증 서버 구축
페이스북, 구글, 깃허브 등 대기업들은 자신만의 OAuth2 인증 서버를 가지고 있다.
따라서 사용자의 구글 정보를 활용하는 서드 파티 애플리케이션을 개발할 때는 구글이 제공하는 Auth Server를 사용해 사용자의 정보에 안전하게 접근하거나 사용자를 인증할 수 있다.
만들어진 Auth Server를 사용하는건 쉽지만, 직접 Auth Server를 구축하는건 쉽지 않다.
애플리케이션을 개발할 때 MSA 기반으로 설계한다면 인증 서버를 다른 요소들과 독립적으로 구축해야 한다.
Keycloak, Spring Security OAuth, AWS Cognito, Okta 등 여러 가지 기술을 활용해 인증 서버를 구축할 수 있다.
각 기술들은 OAuth2, OpenID Connect 등의 프로토콜을 지원하고, 인증 관련 기능을 통합해서 제공한다.
스프링 시큐리티가 제공하는 OAuth2와 OpenID Connect를 사용하면 애플리케이션 내에서 인증 기능을 편하게 구현할 수 있다.
구글, 페이스북 등 소셜 계정을 사용해서 애플리케이션에 로그인하는 소셜 로그인 기능도 구현할 수 있고, 사용자의 구글 정보를 활용해서 애플리케이션 내부에서 활용할 수 있고, OpenID Connect를 사용해 인증된 사용자에 대한 기본 정보를 토큰 형식으로 받아올 수도 있다.
이렇듯 애플리케이션 내부에서 사용되는 인증을 구현하는건 스프링 시큐리티를 사용하면 편하지만, 인증과 권한 부여를 중앙 집중적으로 관리하는 인증 서버를 구축할 때는 Keycloak 등 다른 기술을 사용하면 편하다.
물론 인증 서버를 구축할 때도 스프링 시큐리티를 써도 되긴 하지만...
Keycloak 같은 솔루션을 사용하면 미리 만들어진 템플릿과 구축된 설정을 통해 빠르게 인증 서버를 구축할 수 있고, 관리하기 편하도록 백오피스를 제공하는 등 여러 편의 기능을 제공해 편하게 인증 서버를 구축할 수 있다.
Keycloak 등 인증 서버를 구축하는 기술으로 Auth Server를 구축했으면 spring-boot-starter-oauth2-resource-server 라이브러리를 사용해 스프링으로 Resource Server를 구축할 수 있다.
인증 관련 로직은 따로 구축된 인증 서버에게 전담시키고 스프링 애플리케이션에서는 인증 서버에서 받아온 사용자의 권한과 역할을 바탕으로 리소스를 전달하는 로직에만 집중할 수 있다.
public class KeycloakRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
if (realmAccess == null || realmAccess.isEmpty()) {
return new ArrayList<>();
}
Collection<GrantedAuthority> returnValue = ((List<String>) realmAccess.get("roles"))
.stream().map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return returnValue;
}
}
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Collections.singletonList("*"));
config.setExposedHeaders(Arrays.asList("Authorization"));
config.setMaxAge(3600L);
return config;
}
})).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/contact", "/register")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/myAccount").hasRole("USER")
.requestMatchers("/myBalance").hasAnyRole("USER", "ADMIN")
.requestMatchers("/myLoans").authenticated()
.requestMatchers("/myCards").hasRole("USER")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices", "/contact", "/register").permitAll())
.oauth2ResourceServer(oauth2ResourceServerCustomizer ->
oauth2ResourceServerCustomizer.jwt(jwtCustomizer -> jwtCustomizer.jwtAuthenticationConverter(jwtAuthenticationConverter)));
return http.build();
}
}
시큐리티 설정은 인증 서버를 사용하지 않을 때와 크게 다르지 않다.
설정 클래스에서 Keycloak 인증서버를 사용함을 명시해주고, http form 기반으로 인증하지 않음을 명시해주자.
KeycloakRoleConverter 클래스에서 JWT를 통해 사용자의 Roles를 추출하고 시큐리티의 GrantedAuthority 형식으로 변환한다.
oauth2ResourceServer 메서드로 해당 애플리케이션을 OAuth2 Resource Server로 설정한다.
Keycloak에서 발행된 JWT를 인증 방식으로 사용한다.
React로 프론트엔드 애플리케이션을 만들었다. 이 애플리케이션을 A 라고 하자.
Keycloak으로 Auth Server를 구축했다. 이 서버를 B 라고 하자.
SpringBoot로 Resource Server를 구축했다. 이 서버를 C 라고 하자.
사용자는 A를 통해 서비스에 액세스한다.
위와 같이 Authorization Code Grant Type 흐름으로 인증을 진행하는데, React는 자바스크립트라서 A에서 Client Secret을 안전하게 보관할 수 없다.
따라서 OAuth2에서는 PKCE를 사용해 프론트엔드 애플리케이션에서 클라이언트 시크릿을 안전하게 보관한다.
Proof Key for Code Exchange (PKCE) 는 OAuth의 Authorization Code 흐름을 보완하기 위해 도입됐고, Code Verifier와 Code Challenge를 통해 클라이언트 시크릿을 안전하게 보관한다.
Code Verifier : 클라이언트가 생성하는 무작위 문자열으로, 토큰 요청 시 사용된다.
Code Challenge : Code Verifier를 통해 생성한다. SHA-256 해시 알고리즘을 사용해 Code Verifier를 해싱한 후 인코딩한다.
1. 인증 서버에 Authorization Code를 요청할 때 Code Challenge를 함께 보낸다.
2. 인증이 완료됐으면 인증 서버는 Code Challenge를 저장하고 클라이언트에게 Authorization Code를 발급해준다.
3. 클라이언트는 Access Token을 요청할 때 Authorization Code와 Code Verifier를 함께 보낸다.
4. 인증 서버는 Code Verifier를 바탕으로 Code Challenge를 다시 계산하고 값이 일치하는 경우 Access Token을 발급한다.
이런 과정 덕분에 해커가 인증 코드를 가져가더라도 Access Token을 발급받을 수 없다.
Keycloak 같은 인증 서버 구축 기술은 PKCE를 기본적으로 제공하니 프론트엔드와 백엔드를 따로 개발하는 경우 사용하자.
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security] 인증 메커니즘 (0) | 2024.05.30 |
---|---|
[Spring Security] 초기화 과정 (0) | 2024.05.27 |
[Spring Security6] OAuth2와 OpenID Connect (0) | 2023.08.13 |
[Spring Security 6] 메서드 단위 권한 요청 (0) | 2023.08.12 |
[Spring Security6] Json Web Token (0) | 2023.08.11 |
댓글
이 글 공유하기
다른 글
-
[Spring Security] 인증 메커니즘
[Spring Security] 인증 메커니즘
2024.05.30 -
[Spring Security] 초기화 과정
[Spring Security] 초기화 과정
2024.05.27 -
[Spring Security6] OAuth2와 OpenID Connect
[Spring Security6] OAuth2와 OpenID Connect
2023.08.13 -
[Spring Security 6] 메서드 단위 권한 요청
[Spring Security 6] 메서드 단위 권한 요청
2023.08.12