📌 개요
예전에 jwt를 활용하여 로그인을 구현한 적이 있다. jwt를 활용하면 서버를 stateless하게 유지할 수 있고, 부하도 줄일 수 있다. 이번 개인 프로젝트는 MVP 출시가 목적이라 엄청난 서버 부하가 예상 되지 않고, 서버 증설도 아직 계획은 없어서 세션을 활용하여 로그인을 구현해보기로 했다. 확실히 jwt보다 쉽다.
✅ OAuth2 플로우
네이버를 예시로 OAuth2의 플로우를 살펴보자. 네이버를 활용해 우리 서비스를 이용하고 싶으면 어떻게 해야할까? 회원의 네이버 id,pw를 서비스가 가지고 있으면 된다. 하지만 신뢰 문제나 보안 문제가 있기 때문에 다른 방법을 택한다. 회원이 직접 서비스를 통해 네이버에 로그인을 하면, 네이버(인증 서버)가 서비스에 accessToken을 넘겨준다. accessToken이 있다면 네이버의 리소스 서버를 통해 계정 정보를 받을 수가 있다. 좀 더 구체적으로 그림을 보면 다음과 같다.

1. 유저가 로그인 요청을 한다. 요청 URL은 '/oauth2/authorization/naver'이다.
2. OAuth2AuthorizationRequestRedirectFilter가 요청을 가로채어 네이버 로그인 페이지를 보여준다.
3. 로그인을 성공하면 지정된 redirect페이지로 이동한다. 이 경우 /login/oauth2/code/naver이다. 이때 페이지 URL에 인가코드가 남는다.
4. 다시 요청을 보내면 OAuth2LoginAuthenticationFilter가 인가코드를 OAuth2LoginAuthenticationProvider에게 넘긴다.
5. Provider는 인가코드를 인증서버에게 넘기고 accessToken을 받아온다.
6. Provider는 OAuth2UserService을 활용해 accessToken을 리소스 서버에 넘기고 유저 정보를 가져온다.
✅ 스프링 시큐리티
Spring Security를 사용하게 되면 1~5를 Spring Security가 구현해주고, 6번의 OAuth2UserService만 구현하면 된다.
1️⃣ build.gradle

먼저 build.gradle에 spring security와 oauth2 의존관계를 설정해준다.
2️⃣ Security Config
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final OAuth2UserService oAuth2UserService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf(AbstractHttpConfigurer::disable);
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
httpSecurity.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
//1.userInfoEndpointConfig
httpSecurity.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint((userInfoEndpointConfig) ->
userInfoEndpointConfig.userService(oAuth2UserService)));
//2.세션 고정 보호
httpSecurity.sessionManagement((session) -> session
.sessionFixation(SessionFixationConfigurer::newSession));
return httpSecurity.build();
}
}
먼저 Config파일로 Security 설정을 해주어야한다. 로그인에 직접 관련있는 부분은 다음 2가지이다.
1. userInfoEndpointConfig
사용자 정보를 받아올 객체를 등록하는 곳이다. 즉, 소셜로그인 플로우의 6번 과정을 수행할 객체를 등록한다. 나는 oAuth2UserService를 등록했다. (oAuth2UserService의 구현 내용은 뒤에 적었다.) 이렇게 하면 1번의 로그인 요청시 1~5번을 security가 자동으로 수행하고 등록한 oAuth2UserService의 loadUser 메소드가 자동으로 실행된다.
2.세션 고정 보호
이걸 지금 하지 않아도 소셜로그인은 구할 수 있다. 하지만 세션 방식으로 로그인을 구현할 시 세션 고정에 의해 보안에 취약점이 생긴다. 이를 방지하기 위한 설정이다.
3️⃣ OAuth2Service
@Service
@Transactional
@RequiredArgsConstructor
@Log4j2
public class OAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
private final RoleRepository roleRepository;
private final HasRoleRepository hasRoleRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException{
// 1.accessToken을 user정보로 교환
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if(registrationId.equals("naver")){
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else {
return null;
}
Member findMember = memberRepository.findByProviderAndProviderId(oAuth2Response.getProvider(),oAuth2Response.getProviderId()).orElse(null);
PrincipalDetails principalDetails = null;
//2.회원가입 또는 로그인
if (findMember == null) {
Member newMember =Member.builder()
.email(oAuth2Response.getEmail())
.nickname(oAuth2Response.getName())
.name(oAuth2Response.getName())
.provider(oAuth2Response.getProvider())
.providerId(oAuth2Response.getProviderId())
.build();
memberRepository.save(newMember);
Role role = roleRepository.findByRoleType(USER).orElse(null);
HasRole hasRole = HasRole.builder().member(newMember).role(role).build();
log.info("hasRole={}", hasRole.toString());
log.info("getRole={}", hasRole.getRole());
hasRoleRepository.save(hasRole);
principalDetails = PrincipalDetails.builder()
.attributes(oAuth2User.getAttributes())
.roleTypes(List.of(hasRole.getRole().getRoleType()))
.member(newMember)
.build();
}
else {
findMember.changeEmail(oAuth2Response.getEmail());
findMember.changeName(oAuth2Response.getName());
List<HasRole> hasRoles = hasRoleRepository.findByMemberId(findMember.getId());
List<RoleType> roleTypes = new ArrayList<>();
for(HasRole hasRole : hasRoles){
roleTypes.add(hasRole.getRole().getRoleType());
}
principalDetails = PrincipalDetails.builder().attributes(oAuth2User.getAttributes()).roleTypes(roleTypes).member(findMember).build();
}
//3.반환
return principalDetails;
}
}
OAuth2UserService에서 유저정보를 얻으면 된다.
1. loadUser 메소드
Config 파일에 userInfoEndPoint에 oAuth2UserService에 등록했다. 그렇다면 accessToken을 받은 직후 loadUser가 바로 실행된다. loadUser의 OAuth2UserRequest에 accessToken이 들어있다. 이후 반환값인 OAuth2User에 email, name등 유저의 정보가 담기게 된다.
2.회원가입 or 로그인
중요한건 loadUser로 유저정보를 가져오는 것이고, 이후 부터는 본인 서비스에 맞게 회원가입이나 로그인 로직을 작성하면 된다.
3.반환
나는 OAuth2User를 상속받는 principalDetails를 따로 만들어서 return했다. 이때 OAuth2User가 return 되는 순간 세션에 정보가 담기게 된다. 즉 Http Session에 Security Context 객체를 저장하고, Security Context는 Authentication에 User정보를 담게 된다. 실제 브라우저를 확인하면 쿠키에 JESSIONID가 담긴것을 볼 수 있다.
*OAuth2UserService에서 사용한 NaverResponse
public class NaverResponse implements OAuth2Response{
private final Map<String, Object> attribute;
public NaverResponse(Map<String, Object> attribute) {
this.attribute = (Map<String, Object>) attribute.get("response");
}
@Override
public Provider getProvider() {
return Provider.NAVER;
}
@Override
public String getProviderId() {
return attribute.get("id").toString();
}
@Override
public String getEmail() {
return attribute.get("email").toString();
}
@Override
public String getName() {
return attribute.get("name").toString();
}
}
✅ 참고자료
https://www.youtube.com/watch?v=CY_QrS26Euk&list=PLJkjrxxiBSFBGk0b931ZkCVlNUo7sFisu&index=10
'개발' 카테고리의 다른 글
| React에 MVVM 아키텍처 적용하기 (1) | 2025.07.25 |
|---|---|
| WebRTC에 필요한 서버 4가지 (0) | 2025.04.28 |
| CORS 에러가 발생하는 과정 (0) | 2025.01.03 |
| Builder 패턴을 도입한 이유 (0) | 2024.12.23 |
| React Native WebView로 PortOne 결제 API 연동하기 (0) | 2024.12.04 |