[S-HOOK] 소셜 로그인을 고려하다
소셜 로그인에 대해서 생각하게 된 이유
s-hook 프로젝트의 추가 기능을 구현해야 한다.
구현을 완료한 기능
- 구간의 시작점과 구간의 길이를 지정하여서 내가 좋아하는 파트로 등록한다.
- 내가 등록한 구간이 노래에 등록된 구간중 몇 번째로 많이 등록된 구간인지 알려준다.
- 등록한 구간을 들을 수 있는 유투브 링크를 제공한다.
위에 기능들이 작동하는 데에는 회원이라는 개념은 필요하지 않았다.
구현할 기능
- 한 노래의 파트에 내가 제일 좋아하는 파트를 1번만 등록할 수 있다.
회원이 필요한 시점은 언제일까?
기능이 클라이언트의 데이터에 의존성을 가질 때 회원이 필요하다고 생각한다.
- 클라이언트의 데이터를 보여줄 때
- 클라이언트의 데이터의 상태에 따라 기능이 다르게 작동해야 할 때
우리가 구현해야할 위의 기능은 클라이언트의 노래에 대한 파트 등록 여부 데이터가 필요하다고 판단했다.
회원이라는 개념이 필요하다.
현재 우리 프로젝트 진행에 필요한 회원 가입은 뭘까?
클라이언트가 직접 가입을 해도 회원 가입, 개발자가 회원 데이터를 직접 추가해줘도 회원 가입이다.
지금 당장 회원 가입을 위한 모든 서비스와 정책이 완료될 필요는 없다는 것이다.
우리는 회원이라는 개념이 필요한 것이지 가입에서 로그인까지 이어지는 모든 프로세스가 필요한 것이 아니다.
클라이언트가 할 수 있는 가장 간단한 로그인은 뭘까?
물론 그저 Mock 회원을 통하여 기능의 여부만 파악할 수 있다.
하지만 내가 부릴 수 있는 최소오오오한의 욕심이 뭔지에 대해 생각하면 바로 떠오른 것이 소셜 로그인이였다.
소셜 로그인이 뭘까?
소셜 로그인은 Oauth2.0을 사용해 제공되는 로그인이다.
Oauth2.0는 리소스 서버에서 제공하는 데이터에 대한 접근 권한을 리소스 소유자에 위임받을 수 있는 표준 프로토콜이다
쉽게 설명하면 아래와 같다.
- 스플릿이 구글에 가입한다.
- 스플릿의 정보는 구글 리소스 서버에 저장된다.
- 스플릿은 리소스의 소유자이며 접근 권한을 가지고 있다.
- 스플릿이 s-hook 서비스에서 구글 소셜 로그인을 한다.
- 구글 리소스 서버는 스플릿이 s-hook에게 리소스 접근 권한을 부여하는 것으로 판단한다.
- 구글 리소스 서버는 s-hook에게 스플릿의 리소스를 제공한다.
내가 생각하는 장점
- 유저의 로그인 과정을 간단하게 하여 UX를 향상시킨다.
- 로그인이라는 진입 장벽을 최대한 낮출 수 있다.
내가 생각하는 단점
- 리소스 서버에 의존성을 가지게 된다.
- 리소스 서버에 장애가 생길 경우 서비스까지 영향이 미친다.
- 리소스 서버에 데이터가 있는 유저만 이용이 가능하다.
적절한 리소스 서버는 어디일까?
모든 리소스 서버에서 위에 장점은 제공한다고 생각한다.
그렇다면 단점이 가장 부각되지 않을 리소스 서버를 선택하는 것이 좋을 것이다.
리소스 서버를 제공하는 곳에 거의 모든 사람이 회원으로 가입되어 있고 장애가 발생한 확률이 적은 곳
딱 생각난 곳은 구글 이였다.
( 이미 많은 사람이 경험해봤을 것이고 나도 수차례 경험해보았다. )
구글 소셜 로그인 맛보기로 구현해보기
Spring 을 이용해서 구글 로그인를 통해 토큰을 발급받을 수 있는 간단한 코드이다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.14'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'split'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
1. application.properties 설정
- clientId, clientSecret 은 외부에 노출되면 안되는 값이다.
- 만약 해당 코드를 Github를 통해 관리할 경우 작성한 application.properties 또한 커밋내용에 절대 추가하면 안된다.
- Github Submodule을 사용하여 해당 내용을 노출시키지 않고 관리할 수 있다.
google.clientId={ 발급 받은 클라이언트 ID }
google.redirectUri={ 설정한 리다이렉트 URI }
google.clientSecret={ 발급 받은 클라이언트 SECRET }
2. GoogleLoginProperties 클래스
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
@Getter
@Component
public class GoogleLoginProperties {
@Value("${google.clientId}")
private String clientId;
@Value("${google.clientSecret}")
private String clientSecret;
@Value("${google.redirectUri}")
private String redirectUri;
}
3. GoogleTokenResponse 클래스
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
public class GoogleTokenResponse {
private String access_token;
private Integer expires_in;
private String scope;
private String token_type;
private String id_token;
}
4. GoogleLoginController 클래스
import java.util.HashMap;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
@Controller
public class GoogleLoginController {
private static final String GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
private final GoogleLoginProperties properties;
public GoogleLoginController(final GoogleLoginProperties properties) {
this.properties = properties;
}
@GetMapping("/login")
public String googleLogin() {
final String url = "https://accounts.google.com/o/oauth2/v2/auth?"
+ "client_id=" + properties.getClientId()
+ "&redirect_uri=" + properties.getRedirectUri()
+ "&response_type=code"
+ "&scope=profile";
return "redirect:" + url;
} //구글 로그인 화면으로 리다이렉트 해주는 메서드
@GetMapping("/{구글에 설정한 리다이렉트 URI}")
@ResponseBody
public String showCode(@RequestParam(value = "code") String code) {
return getToken(code);
} //리다이렉트 URI에 존재하는 코드로 토큰을 발급받아 화면에 띄워주는 메서드
private String getToken(final String code) {
final RestTemplate restTemplate = new RestTemplate();
final ResponseEntity<GoogleTokenResponse> response = restTemplate.postForEntity(
GOOGLE_TOKEN_URL,
getParamsForTokenRequest(code),
GoogleTokenResponse.class
);
if (response.getStatusCodeValue() == HttpStatus.OK.value()) {
return response.getBody().getAccess_token();
}
return "FAIL TO GET TOKEN";
} //로그인 성공시 반환받은 코드를 통해 POST 요청으로 토큰을 발급 받는 메서드
private HashMap<String, String> getParamsForTokenRequest(final String code) {
final HashMap<String, String> params = new HashMap<>();
params.put("code", code);
params.put("client_id", properties.getClientId());
params.put("client_secret", properties.getClientSecret());
params.put("redirect_uri", properties.getRedirectUri());
params.put("grant_type", "authorization_code");
return params;
} //토큰 발급 요청에 필요한 파라미터를 담은 Map을 반환해주는 메서드
}