OAuth는 인터넷 사용자가 비밀번호를 제공하지 않고도, 다른 웹사이트의 리소스에 접근할 수 있도록 허용하는 개방형 표준입니다. OAuth를 통해 사용자는 안전하게 특정 권한을 가진 서비스나 애플리케이션에 제한된 자원 접근을 승인할 수 있습니다.
OAuth는 일반적으로 두 가지 버전, 즉 OAuth 1.0과 OAuth 2.0으로 나뉩니다.
OAuth 1.0
OAuth 1.0은 2010년에 발표된 최초의 OAuth 표준입니다.
이 표준의 주요 특징은 다음과 같습니다:
- 서명된 요청
- OAuth 1.0은 클라이언트가 서버에 요청을 보낼 때마다 요청을 서명하는 방식을 사용합니다. 이를 위해 클라이언트는 소비자 키(consumer key)와 소비자 비밀(consumer secret)을 사용합니다.
- 보안
- 서명된 요청은 요청이 변경되지 않았음을 보증하고, 중간자 공격을 방지합니다. 이는 HTTPS가 없는 환경에서도 보안을 제공합니다.
- 복잡한 구현
- 서명 과정은 구현을 복잡하게 만들며, 개발자들이 OAuth 1.0을 구현하는 데 어려움을 겪게 합니다.
- 서명 과정은 구현을 복잡하게 만들며, 개발자들이 OAuth 1.0을 구현하는 데 어려움을 겪게 합니다.
OAuth 2.0
OAuth 2.0은 OAuth 1.0의 복잡성을 해결하고 확장성을 개선하기 위해 2012년에 발표된 새로운 버전입니다.
OAuth 2.0의 주요 특징은 다음과 같습니다:
- 보다 간단한 프로세스
- OAuth 2.0은 클라이언트 인증과 토큰 발급 프로세스를 간소화했습니다. 이는 구현을 용이하게 하고, 개발자들의 부담을 줄여줍니다.
- 다양한 인증 흐름
- OAuth 2.0은 다양한 인증 흐름(예: Authorization Code, Implicit, Password Credentials, Client Credentials)을 제공하여 다양한 시나리오와 애플리케이션 유형에 맞게 적용할 수 있습니다.
- 보안 고려사항
- OAuth 2.0은 HTTPS를 통한 보안 통신을 권장합니다. OAuth 1.0에서 제공하는 요청 서명 메커니즘은 제거되었으며, 대신 토큰을 안전하게 전송하고 관리하는 것에 중점을 둡니다.
- 확장성
- OAuth 2.0은 다양한 유형의 애플리케이션을 지원하도록 설계되었으며, 새로운 흐름이나 토큰 유형을 쉽게 추가할 수 있습니다.
글로벌하게 사용되며 검색 결과가 많이 나오는 구글을 OAuth의 실습 플랫폼으로 정하였습니다.
스펙은 아래와 같습니다.
- spring boot: 3.1.5
- java: 17
실습을 진행해 보겠습니다.
Gradle에 의존성 추가하기
dependencies {
// ...
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// OAuth2 Client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// 타임리프
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// ...
}
Google Cloud Platform 셋팅하는 방법
Google Cloud Platform(GCP)에서 OAuth 2.0 클라이언트 ID를 생성하는 방법은 다음과 같습니다:
- Google Cloud Platform 콘솔 방문
- [Google Cloud Console](https://console.cloud.google.com/)에 접속합니다.
- 프로젝트 생성 또는 선택
- 새 프로젝트를 만들거나 기존 프로젝트를 선택합니다. 새 프로젝트를 만들기 위해서는 '프로젝트 만들기' 버튼을 클릭하고 필요한 정보를 입력합니다.
- API 및 서비스 대시보드로 이동
- GCP 콘솔의 사이드바에서 'API 및 서비스' > '대시보드'로 이동합니다.
- OAuth 동의 화면 구성
- 'OAuth 동의 화면'을 선택하고, 필요한 정보를 입력합니다. 이 단계에서는 애플리케이션 이름, 사용자 지원 이메일, 동의 화면에 표시될 정보 등을 설정합니다. '저장' 버튼을 클릭하여 변경 사항을 저장합니다.
- API 사용 설정
- 필요한 API(예: Google+ API, Gmail API 등)를 활성화합니다. 'API 및 서비스' > '라이브러리'에서 필요한 API를 검색하고 '사용' 버튼을 클릭합니다.
- OAuth 2.0 클라이언트 ID 생성
- 'API 및 서비스' > '사용자 인증 정보'로 이동한 후, '사용자 인증 정보 만들기' 버튼을 클릭하고 'OAuth 클라이언트 ID'를 선택합니다.
- 애플리케이션 유형 선택
- 애플리케이션의 유형을 선택합니다(예: 웹 애플리케이션, iOS, Android 등).
- 애플리케이션의 유형을 선택합니다(예: 웹 애플리케이션, iOS, Android 등).
- 리디렉션 URI 설정
- 애플리케이션의 로그인 후 사용자를 리디렉션할 URI를 입력합니다. 이 URI는 애플리케이션의 설정 파일에도 동일하게 명시되어야 합니다.
- 애플리케이션의 로그인 후 사용자를 리디렉션할 URI를 입력합니다. 이 URI는 애플리케이션의 설정 파일에도 동일하게 명시되어야 합니다.
- 클라이언트 ID 및 비밀 생성
- 폼을 완성하고 '생성' 버튼을 클릭하면 클라이언트 ID와 클라이언트 비밀이 생성됩니다. 이 정보를 안전하게 보관하고 애플리케이션의 설정 파일에 사용합니다.
- 폼을 완성하고 '생성' 버튼을 클릭하면 클라이언트 ID와 클라이언트 비밀이 생성됩니다. 이 정보를 안전하게 보관하고 애플리케이션의 설정 파일에 사용합니다.
- 사용자 인증 정보 다운로드
- 생성된 사용자 인증 정보를 JSON 파일로 다운로드하여 프로젝트에 안전하게 저장할 수 있습니다.
이 과정을 통해 Google Cloud Platform에서 OAuth 2.0 클라이언트 ID를 생성하고, 이를 애플리케이션에 사용하여 Google 서비스에 대한 OAuth 인증을 구현할 수 있습니다.
애플리케이션 설정하기
yml 파일에 아래 설정을 추가합니다.
# Google OAuth 설정
spring:
security:
oauth2:
client:
registration:
google:
client-id: #{CLIENT_ID}
client-secret: #{CLIENT_SECRET}
scope: profile,email
redirect-uri: http://localhost:8080/login/oauth2/code/google
thymeleaf:
prefix: classpath:/templates/
suffix: .html
main:
allow-bean-definition-overriding: true
프론트는 타임리프로 구현했습니다.
redirect-uri는 아래와 같은 특징을 가집니다.
redirect-uri는 일반적으로 OAuth 2.0 공급자가 사용자를 리디렉션하는 콜백 URL입니다.
이 URL은 인증 공급자로부터 인증 코드를 받는 데 사용됩니다. Spring Security는 이 코드를 받고, 그것을 사용해서 사용자를 인증한 후, CustomAuthenticationSuccessHandler를 호출합니다.
따라서, redirect-uri 경로에 대한 명시적인 컨트롤러 메서드가 없더라도, Spring Security가 OAuth 2.0 로그인 프로세스를 처리하고, 로그인 성공 시 CustomAuthenticationSuccessHandler를 통해 추가적인 처리를 수행하게 됩니다.
SecurityConfiguration 설정하기
@Configuration
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange((auths) ->
auths
.pathMatchers("/", "/login/oauth2/code/google").permitAll() // 공개 경로 설정
.anyExchange().authenticated()
)
// .formLogin((formLogin) ->
// formLogin.loginPage("/login")
// )
.logout((logout) ->
logout.logoutUrl("/logout") // 로그아웃을 처리할 URL
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
)
.oauth2Login(oauth2Login ->
oauth2Login.authenticationSuccessHandler(new CustomAuthenticationSuccessHandler())
)
.build()
;
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository(
ClientRegistration googleClientRegistration){
return new InMemoryClientRegistrationRepository(googleClientRegistration);
}
@Bean("googleClientRegistration")
ClientRegistration googleClientRegistration(
@Value("${spring.security.oauth2.client.registration.google.client-id}") String clientId,
@Value("${spring.security.oauth2.client.registration.google.client-secret}") String clientSecret,
@Value("${spring.security.oauth2.client.registration.google.scope}") String scope,
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}") String redirectUri) {
return ClientRegistration.withRegistrationId("google")
.clientId(clientId)
.clientSecret(clientSecret)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(redirectUri)
.scope(List.of(scope.split(",")))
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.clientName("Google")
.build();
}
}
public class CustomAuthenticationSuccessHandler extends RedirectServerAuthenticationSuccessHandler {
public CustomAuthenticationSuccessHandler() {
super("/main"); // 성공 시 리디렉션할 URL 지정
}
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange exchange, Authentication authentication) {
return super.onAuthenticationSuccess(exchange, authentication);
}
}
public class CustomLogoutSuccessHandler implements ServerLogoutSuccessHandler {
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
URI uri = URI.create("/");
// 쿠키 삭제
ResponseCookie cookie = ResponseCookie.from("JSESSIONID", "")
.path("/") // 쿠키의 경로 설정 (필요에 따라 변경)
.maxAge(0) // 쿠키 만료 시간을 0으로 설정하여 쿠키 삭제
.build();
exchange.getExchange().getResponse().addCookie(cookie);
// 리디렉션 설정
exchange.getExchange().getResponse().setStatusCode(HttpStatus.SEE_OTHER);
exchange.getExchange().getResponse().getHeaders().setLocation(uri);
return exchange.getExchange().getResponse().setComplete();
}
}
redirect-uri는 모두 허용 설정을 해야 합니다.
컨트롤러
WebFlux를 사용한 예시입니다.
@Controller
public class OAuthViewController {
@GetMapping("/")
public Mono<String> index(){
return Mono.just("index");
}
@GetMapping("/main")
public Mono<Rendering> renderView() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> (OAuth2AuthenticationToken) securityContext.getAuthentication())
.map(token -> {
final Map<String, Object> attr = token.getPrincipal().getAttributes();
return Rendering.view("main")
.modelAttribute("name", attr.get(OAuthAttr.name.name()))
.modelAttribute("email", attr.get(OAuthAttr.email.name()))
.build();
});
}
}
View page
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OAuth 2.0 로그인</title>
</head>
<body>
<a href="/oauth2/authorization/google">Google Login</a><br>
</body>
</html>
main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Main Page</title>
</head>
<body>
로그인 사용자 정보 <br>
이름 >> <span th:text="${name}"></span> <br>
email >> <span th:text="${email}"></span>
<br>
<br>
<a href="/logout">logout</a><br>
</body>
</html>
실습 순서는 아래와 같습니다.
- 스프링 앱을 기동합니다.
- localhost:8080/ 로 접속합니다.
- 화면의 구글 로그인을 클릭하여 OAuth 로그인을 합니다.
- localhost:8080/main 으로 접속하여 화면에 표시된 email, name이 올바른지 확인합니다.
- 화면에 로그아웃을 클릭하여 로그아웃합니다.
참고
'[개발] Info > 용어' 카테고리의 다른 글
CQRS(Command Query Responsibility Segregation) (0) | 2023.11.30 |
---|---|
CDC(Change Data Capture) (0) | 2023.11.29 |
SSL (0) | 2023.11.17 |
OSI 7 Layer (0) | 2023.10.24 |
gRPC (0) | 2023.10.22 |