소셜 로그인은 사용자에게 간편한 로그인 방법을 제공하고, 개발자는 사용자 인증을 쉽게 처리할 수 있는 방법입니다. 이번 글에서는 OAuth2.0와 Spring Security를 활용해 구글 소셜 로그인을 구현하는 방법을 공유하려고 합니다!
그 전에 Spring Security와 OAuth2.0에 대한 숙지가 필요하다면 아래 글을 참고해주세요!
⭐ 폴더 구조
프로젝트는 크게 config, controller, helper, service 폴더로 구성되어 있으며, 소셜 로그인을 처리하는 기능을 담당하고 있습니다. 각 폴더는 다음과 같은 역할을 가지고 있습니다.
- Config 폴더: Spring Security 설정을 통해 보안 설정을 관리.
- Controller 폴더: 클라이언트로부터의 요청을 처리하고 서비스를 호출.
- Helper 폴더: 로그인 타입과 관련된 상수 및 변환 로직을 제공.
- Service 폴더: 소셜 로그인 로직을 처리하는 비즈니스 로직을 담고 있음.
구글, 네이버, 카카오와 같은 SNS 로그인 기능을 제공하며, 각 SNS 로그인 프로세스는 별도의 서비스 클래스로 구현되어 있습니다.
⭐ 코드 설명
✅ Config 폴더
☑️ SecurityConfig.java 파일
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/auth/google", "/auth/kakao", "/auth/naver").permitAll() // 소셜 로그인 엔드포인트는 인증 없이 접근 가능
.anyRequest().authenticated() // 그 외의 요청은 인증이 필요
)
.formLogin(formLogin ->
formLogin
.loginPage("/login") // 커스텀 로그인 페이지 설정
.permitAll() // 로그인 페이지는 누구나 접근 가능
)
.csrf(csrf -> csrf.disable()); // 개발 시 CSRF 보호를 비활성화
return http.build();
}
}
이 클래스는 인증이 필요한 요청과 그렇지 않은 요청을 구분하고, 커스텀 로그인 페이지를 설정합니다.
csrf.disable(): CSRF 보호를 비활성화한 것은 개발 과정에서의 편의를 위해 사용된 것으로, 배포 시에는 주의가 필요합니다.
애플리케이션의 보안을 설정하고, 인증 및 권한 부여 로직을 쉽게 구현하기 위해 Spring Security를 사용했습니다.
✅ Controller 폴더
☑️ OauthController.java 파일
@RestController
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping(value = "/auth")
@Slf4j
public class OauthController {
private final OauthService oauthService;
@GetMapping(value = "/{socialLoginType}")
public void socialLoginType(@PathVariable(name = "socialLoginType") SocialLoginType socialLoginType) {
log.info(">> 사용자로부터 SNS 로그인 요청을 받음 :: {} Social Login", socialLoginType);
oauthService.request(socialLoginType);
}
@GetMapping(value = "/{socialLoginType}/callback")
public String callback(@PathVariable(name = "socialLoginType") SocialLoginType socialLoginType,
@RequestParam(name = "code") String code) {
log.info(">> 소셜 로그인 API 서버로부터 받은 code :: {}", code);
return oauthService.requestAccessToken(socialLoginType, code);
}
}
socialLoginType 메서드 : 소셜 로그인 요청 처리
> 사용자가 특정 소셜 로그인 타입(구글, 카카오, 네이버)으로 로그인 요청을 보내면 해당 요청을 OauthService로 넘겨 처리합니다.
callback 메서드 :
> 각 SNS 서버로부터 받은 인증 코드를 바탕으로 OauthService를 통해 액세스 토큰을 요청합니다.
✅ Helper 폴더 - Constants 폴더
☑️ SocialLoginType.java 파일 (Enum)
public enum SocialLoginType {
GOOGLE,
KAKAO,
NAVER
}
각 SNS 서비스의 종류를 Enum으로 정의해, 이후 로그인 처리 로직에서 구분하여 사용할 수 있도록 합니다.
✅ Helper 폴더 - Converter 폴더
@Configuration
public class SocialLoginTypeConverter implements Converter<String, SocialLoginType> {
@Override
public SocialLoginType convert(String s) {
return SocialLoginType.valueOf(s.toUpperCase());
}
}
URL 파라미터로 전달된 문자열을 소문자든 대문자든 무관하게 Enum 타입으로 변환하여 처리합니다.
✅ Service 폴더
☑️ OAuthService.java. 파일
@Service
@RequiredArgsConstructor
public class OauthService {
private final List<SocialOauth> socialOauthList;
private final HttpServletResponse response;
public void request(SocialLoginType socialLoginType) {
SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType);
String redirectURL = socialOauth.getOauthRedirectURL();
try {
response.sendRedirect(redirectURL);
} catch (IOException e) {
e.printStackTrace();
}
}
public String requestAccessToken(SocialLoginType socialLoginType, String code) {
SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType);
return socialOauth.requestAccessToken(code);
}
private SocialOauth findSocialOauthByType(SocialLoginType socialLoginType) {
return socialOauthList.stream()
.filter(x -> x.type() == socialLoginType)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("알 수 없는 SocialLoginType 입니다."));
}
}
request 메서드 : 소셜 로그인 요청
> 각 소셜 로그인 타입에 따라 OAuth URL로 리디렉션하여 인증 요청을 수행합니다.
requestAccessToken 메서드 : 액세스 토큰 요청
> 인증 코드를 사용해 각 소셜 로그인 서버로부터 액세스 토큰을 받아오는 기능을 담당합니다.
✅ Service 폴더 - Social 폴더
☑️ GoogleOauth.java 파일
@Component
@RequiredArgsConstructor
public class GoogleOauth implements SocialOauth {
@Value("${sns.google.url}")
private String GOOGLE_SNS_BASE_URL;
@Value("${sns.google.client.id}")
private String GOOGLE_SNS_CLIENT_ID;
@Value("${sns.google.callback.url}")
private String GOOGLE_SNS_CALLBACK_URL;
@Value("${sns.google.client.secret}")
private String GOOGLE_SNS_CLIENT_SECRET;
@Value("${sns.google.token.url}")
private String GOOGLE_SNS_TOKEN_BASE_URL;
@Override
public String getOauthRedirectURL() {
Map<String, Object> params = new HashMap<>();
params.put("scope", "profile");
params.put("response_type", "code");
params.put("client_id", GOOGLE_SNS_CLIENT_ID);
params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
String parameterString = params.entrySet().stream()
.map(x -> x.getKey() + "=" + x.getValue())
.collect(Collectors.joining("&"));
return GOOGLE_SNS_BASE_URL + "?" + parameterString;
}
@Override
public String requestAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> params = new HashMap<>();
params.put("code", code);
params.put("client_id", GOOGLE_SNS_CLIENT_ID);
params.put("client_secret", GOOGLE_SNS_CLIENT_SECRET);
params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
params.put("grant_type", "authorization_code");
ResponseEntity<String> responseEntity = restTemplate.postForEntity(GOOGLE_SNS_TOKEN_BASE_URL, params, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
return responseEntity.getBody();
}
return "구글 로그인 요청 처리 실패";
}
}
getOauthRedirectURL 메서드 : OAuth URL 생성
> 구글 OAuth 인증 페이지로 리디렉션할 URL을 생성합니다.
@Value 애노테이션을 통해 외부 설정 파일(application.properties)에서 환경 변수를 주입받을 수 있습니다.
Stream API의 Collectors.joining 메서드를 통해 맵의 키-값 쌍을 하나의 문자열로 연결하는 데 사용했습니다.
requestAccessToken 메서드 : 액세스 토큰 요청
> 구글 서버에 인증 코드를 보내고, 액세스 토큰을 요청하여 받아옵니다.
RestTemplate은 구글 OAuth 서버와 통신하여 토큰을 주고받는 데 사용됩니다.
☑️ SocialOauth.java (Interface)
public interface SocialOauth {
String getOauthRedirectURL();
String requestAccessToken(String code);
default SocialLoginType type() {
if (this instanceof GoogleOauth) {
return SocialLoginType.GOOGLE;
} else if (this instanceof NaverOauth) {
return SocialLoginType.NAVER;
} else if (this instanceof KakaoOauth) {
return SocialLoginType.KAKAO;
} else {
return null;
}
}
}
각 소셜 로그인 클래스들이 공통적으로 사용할 메서드(getOauthRedirectURL, requestAccessToken)를 정의한 인터페이스입니다.
☑️ KakaoOauth.java 파일
KakaoOauth와 NaverOauth 파일은 아직 구체적인 로직은 작성되지 않은 상태이지만, 전체적인 틀을 소개해드리도록 하겠습니다.
두 클래스 모두 SocialOauth 인터페이스를 구현하여, 공통적으로 필요한 메서드인 getOauthRedirectURL()과 requestAccessToken()을 오버라이드하고 있습니다
@Component
public class KakaoOauth implements SocialOauth {
@Override
public String getOauthRedirectURL() {
return "";
}
@Override
public String requestAccessToken(String code) {
return null;
}
}
카카오 OAuth를 사용한 로그인 기능을 구현하는 서비스 클래스입니다. 카카오로부터 인증 코드를 받아오고, 해당 코드를 사용하여 액세스 토큰을 요청하는 역할을 해야 하지만, 현재 기능은 아직 구현되지 않았습니다.
- @Component 애노테이션: Spring에서 KakaoOauth 클래스가 자동으로 빈으로 등록될 수 있도록 합니다. 즉, 이 클래스는 Spring 컨텍스트에서 자동으로 관리되며 다른 곳에서 주입될 수 있습니다.
- getOauthRedirectURL(): 이 메서드는 사용자가 카카오 로그인 페이지로 리디렉션될 URL을 반환해야 합니다. 그러나 현재 빈 문자열을 반환하고 있어 아직 구현되지 않았습니다.
- requestAccessToken(String code): 이 메서드는 카카오 서버에서 인증 코드를 받아와 액세스 토큰을 요청하는 기능을 해야 합니다. 그러나 현재 null을 반환하고 있어 역시 구현이 되지 않은 상태입니다.
☑️ NaverOauth.java 파일
@Component
public class NaverOauth implements SocialOauth {
@Override
public String getOauthRedirectURL() {
return "";
}
@Override
public String requestAccessToken(String code) {
return null;
}
}
네이버 OAuth 인증을 처리하는 서비스로, 네이버 로그인 요청을 받고 리디렉션 URL을 생성한 뒤 네이버 서버에 인증 코드를 통해 액세스 토큰을 요청하는 기능을 담당해야 합니다. 하지만 카카오와 마찬가지로 아직 구체적인 로직은 구현되지 않은 상태입니다.
따라서, 이 두 클래스는 해당 소셜 로그인 서비스와의 연동을 위한 로직이 추가되면, 현재의 구글 OAuth 처리 로직과 유사한 방식으로 작동할 수 있을 것입니다.
✅ resources 폴더 - application.properties 파일
sns.google.url=https://accounts.google.com/o/oauth2/v2/auth
sns.google.client.id=<본인이 GCP에서 발급받은 클라이언트 ID>
sns.google.client.secret=<본인이 GCP에서 발급받은 비밀번호>
sns.google.callback.url=http://localhost:8080/auth/google/callback
sns.google.token.url=https://oauth2.googleapis.com/token
이를 위해 GCP에서 프로젝트를 등록해서 클라이언트 ID와 비밀번호를 받아와야 합니다. 구글 OAuth 인증을 위해 GCP에서 이를 발급받는 과정을 다루는 블로그 글은 무수히 많고, 간단하니 다른 글을 참고해주시길 바랍니다! (그냥 클릭만 하면 되는 수준입니다.)
OAuth 2.0 인증 과정에서는 클라이언트 애플리케이션(즉, 사용자가 개발한 애플리케이션)이 구글과 같은 OAuth 제공자(GCP)와 통신하기 위해 인증을 받아야 합니다. 이때 client ID와 client secret은 다음과 같은 역할을 합니다.
- Client ID: 애플리케이션을 구글 OAuth 서버에 식별하는 고유한 값입니다. 구글은 이 값을 통해 해당 요청이 인증된 애플리케이션으로부터 왔는지 확인합니다.
- Client Secret: 애플리케이션의 비밀 키로, 서버 간의 통신에서 애플리케이션의 신뢰성을 증명하는 데 사용됩니다. 클라이언트와 구글 서버 간의 통신이 안전하고 인증된 것임을 확인하기 위해 필수적입니다.
OAuth 2.0 인증 절차에서 client ID와 client secret이 사용되는 주요 단계는 다음과 같습니다.
- 사용자가 로그인 요청: 애플리케이션에서 사용자가 구글 로그인을 요청하면, 애플리케이션은 client ID를 포함한 요청을 구글 서버로 보냅니다.
- 사용자 인증 및 코드 발급: 사용자가 구글 인증 페이지에서 로그인을 완료하면, 구글은 애플리케이션으로 인증 코드를 반환합니다. 이때 애플리케이션은 해당 코드를 사용해 액세스 토큰을 요청합니다.
- 액세스 토큰 요청: 애플리케이션은 반환받은 인증 코드와 함께 client ID와 client secret을 구글 서버에 보내며, 이 요청이 구글 서버로부터 신뢰받은 요청임을 증명합니다. 구글 서버는 이 정보를 확인하고, 올바른 정보가 전달되었을 경우 액세스 토큰을 반환합니다.
application.properties 파일에 client ID와 client secret을 입력해 두는 이유는, 애플리케이션이 실행될 때 해당 정보가 필요할 때마다 이를 쉽게 참조할 수 있도록 설정하는 것입니다.
중요한 자격 증명 정보는 코드에 직접 하드코딩되지 않고 환경 설정 파일에 저장하여 관리할 수 있습니다. 이 설정 파일은 외부에 노출되지 않도록 환경에 따라 보안 조치가 필요합니다.
따라서 이를 통해 GCP에서 발급받은 client ID와 client secret은 구글 OAuth 인증을 통해 안전하게 사용자 인증을 처리할 수 있게 됩니다.
⭐ 전체 흐름 설명
- 사용자가 특정 소셜 로그인 페이지로 이동하면, OauthController에서 해당 SNS의 인증 요청을 처리하고 SNS 로그인 페이지로 리디렉션합니다.
- 사용자가 인증을 완료하면 SNS 서버에서 콜백 URL을 호출하고, 이때 인증 코드를 포함해 전송합니다.
- 애플리케이션은 OauthService를 통해 인증 코드를 SNS 서버에 보내고, 액세스 토큰을 받아 처리합니다.
⭐ 테스트
이제 Spring Boot 서버를 구동한 후, 구글 소셜 로그인을 테스트할 수 있습니다. 다음 단계를 통해 요청을 보내고 결과를 확인할 수 있습니다.
먼저, Spring Boot 서버가 정상적으로 실행되고 있는지 확인합니다. 저는 현재 로컬에서 개발 중이어서 localhost:8080로 실행되고 있습니다.
크롬과 같은 웹 브라우저를 사용해서 테스트 한다면 아래 링크에서 확인할 수 있습니다.
http://localhost:8080/auth/google
혹은 Postman과 같은 API 도구를 사용한다면 아래와 같이 요청을 보냅니다.
GET http://localhost:8080/auth/google
요청을 보내면 콘솔 로그에서 다음과 같은 메시지를 확인할 수 있습니다.
요청이 정상적으로 처리되면, 아래 사진과 같이 서버가 구글 OAuth 로그인 페이지로 리디렉션됩니다.
즉, 사용자가 구글 로그인 페이지로 이동하여 인증을 진행할 수 있습니다. 구글에서 인증이 완료되면, 콜백 URL로 코드가 반환되며, 이후 이를 활용해 액세스 토큰을 요청할 수 있습니다.
만약 위와 같은 페이지가 뜨지 않고 아래와 같은 현상이 나타난다면 아래 글을 읽어주시길 바랍니다!
📌 참고