OAuth - OAuth를 활용한 세션 로그인 구현

TopOfTheHead·2026년 5월 24일

Spring OAuth

목록 보기
7/12

세션 로그인 / 회원가입
권한부여승인코드

사전 설정

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bblog
    username: ${userDbId}
    password: ${userDbPw}
  jpa:
    hibernate:
      ddl-auto: create
  security:
    oauth2:
      client:
        registration:
          google:
            client-name: Google
            client-id: ${googleClientId}
            client-secret: ${googleClientSecret}
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            scope:
              - email
              - profile
          kakao:
            client-name: Kakao
            client-id: ${kakaoClientId}
            client-secret: ${kakaoClientSecret}
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_post
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            scope:
              - profile_nickname
              - profile_image
          naver:
            client-name: Naver
            client-id: ${naverClientId}
            client-secret: ${naverClientSecret}
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_post
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            scope:
              - id
              - nickname
              - email
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            user-name-attribute: id
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize 
            user-name-attribute: response
            token-uri: https://nid.naver.com/oauth2.0/token 
            user-info-uri: https://openapi.naver.com/v1/nid/me
  • Config 정의
@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity){
        return httpSecurity
                .csrf(csrf -> csrf.disable())
                .cors(cors -> cors.disable())
                .oauth2Login(Customizer.withDefaults())
                .authorizeHttpRequests(
                        auth -> auth.requestMatchers("/login", "/sign-up")
                                .anonymous()
                                .requestMatchers("/user/**")
                                .hasAnyAuthority("USER","ADMIN")
                                .requestMatchers("/admin/**")
                                .hasAuthority("ADMIN")
                                .anyRequest()
                                .denyAll()
                )
                .build();
    }
}

로그인 기능 활용하기

  • Entity Class 정의하기
    OAuth 2.0 구현 시 보통 이메일식별용 아이디로서 데이터유일한 식별자 역할을 수행
    중복회원을 방지하는 용도로 사용

    카카오사업자로 등록되지 않으면 이메일 계정을 요청할 수 없으므로 서브ID로서 ID + @kakao.com을 설정하여 유일하게 식별하도록 설정
@Column(unique = true, nullable = false)
private String email;

Entity 설계 시 다음처럼 설정

provider 필드OAuth Provider( = kakao, google, ... )를 지정하는 필드

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String nickname;
    private String role = "USER";
    @Column(unique = true, nullable = false)
    private String email;
    private String provider;
    @Builder
    public Member(String name, String nickname, String email, String provider) {
        this.name = name;
        this.nickname = nickname;
        this.email = email;
        this.provider = provider;
    }
}
  • DTO 역할의 OAuth2User 정의
    OAuth2UserService( = UserServiceDetails )에 의해 사용되는 UserDetails 구현체를 정의 시 OAuth2User구현
    OAuth2User

    OAuth Provider에서 제공되는 사용자정보에 해당하는 attrobutes 필드를 정의
    OAuth2UserService에 의해 사용자 정보를 포함하여 생성
@Getter
public class CurretUser implements OAuth2User {
    private String email;
    private String name;
    private Map<String,Object> attributes;
    @Setter
    private String role;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(
                new SimpleGrantedAuthority("ROLE_" + this.role.name())
        );
    }
    @Builder
    public CurretUser(String email, String name, Map<String, Object> attributes){
        this.email = email;
        this.name = name;
        this.attributes = attributes;
    }
}
  • Repository Layer 정의
@Repository
public interface MemberRepository extends JpaRepository<Member,Long> {
    Optional<Member> findByEmail(String email);
    default Member findByEmailOrThrow(String email){
        return findByEmail(email).orElseThrow(()->new RuntimeException("이메일에 해당하는 계정을 찾지 못했습니다."));
    }
}

카카오사업자로 등록되지 않으면 이메일 계정을 요청할 수 없으므로 서브ID로서 ID + @kakao.com을 설정하여 유일하게 식별하도록 설정

  • OAuth Provider자원 응답 구조에 대한 양식을 정의 및 커스텀 OAuth2User 구현체를 반환하는 팩토리 패턴 클래스 정의
    。각 OAuth ProviderAPI 문서응답 구조 를 보고 설계
    google / kakao / naver사용자 정보 요청 시 반환하는 사용자정보json 구조가 다르므로

    OAuth2UserService에 의해 사용자 정보를 포함하여 생성된 Oauth2User 구현체팩토리 패턴 클래스를 통해 각 OAuth Provider응답구조에 대응하는 로직을 각각 정의 후 커스텀 OAuth2User 구현체를 생성해서 반환하도록 설정
public class MemberDetailsFactory {
    public static CurretUser memberDetails(String provider, OAuth2User oAuth2User){
        Map<String,Object> attributes = oAuth2User.getAttributes();
        return switch (provider.toUpperCase()) {
            case "GOOGLE" -> CurretUser.builder()
                    .email(attributes.get("email").toString())
                    .name(attributes.get("name").toString())
                    .attributes(attributes)
                    .build();
            case "KAKAO" -> {
                Map<String, Object> properties = (Map<String, Object>) attributes.get("properties"); //
                yield CurretUser.builder()
			// "id"의 경우 "properties"와 동일 레벨에 존재
                        .email(attributes.get("id").toString() + "@kakao.com") // 별칭을 이메일로서 동적으로 지정
                        .name(properties.get("nickname").toString())
                        .attributes(attributes)
                        .build();
            }
            case "NAVER" -> {
                Map<String, Object> properties = (Map<String, Object>) attributes.get("response");
                yield CurretUser.builder()
                        .email(properties.get("email").toString())
                        .name(properties.get("nickname").toString())
                        .attributes(attributes)
                        .build();
            }
            default -> throw new IllegalArgumentException("지원하지 않는 제공자입니다. :" + provider);
        };
    }
}
  • Service Layer 정의 및 회원가입 로직 작성
    OAuth를 통한 로그인 시 해당 백엔드 서버회원가입을 자동으로 수행하여 DB에 저장하도록 설정

    。이떄, OAuth를 통한 로그인매번 호출되므로, 동일 계정으로 로그인중복 회원가입되는 문제가 발생
    최초 로그인일때만 회원가입을 수행하고, 아닌 경우 DB에 존재하는 사용자정보를 기반으로 Role을 설정한 커스텀 OAuth2User 생성 및 반환하도록 설정

    。동일한 이메일이 존재하는 경우 다른 Provider이메일인지 확인하는 로직을 작성
    ex) naverwjdtn747@naver.com이 등록된 상태에서 googlewjdtn747@naver.com로 로그인 시 차단
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OAuthService extends DefaultOAuth2UserService {
    private final MemberRepository memberRepository;
    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // ~ 권한부여승인코드 상 1 ~ 6번이 자동으로 수행
        //
        // 권한부여승인코드 7 ~ 8번
        // OAuth2UserRequest의 Access Token, ClientRegistration을 기반으로
        // OAuth Provider에게 요청 및 반환받은 사용자정보를 기반으로 OAuth2User 구현체 생성
        OAuth2User oAuth2User = super.loadUser(userRequest);
        // OAuth Provider 정보 가져오기 ( kakao, ... )
        String providerId = userRequest.getClientRegistration().getRegistrationId();
        // 각 OAuth Provider의 각각 다른 응답구조에 대응하면서
        // 커스텀 OAuth2User 구현체를 생성하여 반환하는 Factory 메서드 사용
        CurretUser curretUser = MemberDetailsFactory.memberDetails(providerId, oAuth2User);
        //
        // 회원가입 분기점
        // 각 상호작용 후 currentUser의 Role을 설정해야한다.
        //
        String role;
        if (isNoExistingMember(curretUser.getEmail())) {
            // 가입된 계정이 없는 경우 회원가입
            role = saveMemberByCurrentUserAndReturnRole(providerId, curretUser);
        } else {
            // 동일한 이메일이 존재하는 경우 다른 Provider의 이메일인지 확인
            // ex) naver의 wjdtn747@naver.com이 등록된 상태에서
            // google의 wjdtn747@naver.com로 로그인 시 차단
            role = ValidateSameProviderAndReturnRole(providerId, curretUser.getEmail());
        }
        // 커스텀 OAuth2User에 Role 설정 및 로그인 완료
        curretUser.setRole(role);
        return curretUser;
    }
    public boolean isNoExistingMember(String email){
        return memberRepository.findByEmail(email).isEmpty();
    }
    public String ValidateSameProviderAndReturnRole(String currentProviderId, String email){
        Member founded = memberRepository.findByEmailOrThrow(email);
        if(founded.getProvider().equals(currentProviderId)){
            // 동일한 Provider인 경우 Role 반환
          return founded.getRole();
        } else {
            // 다른 Provider인데 동일 이메일인 경우 예외 발생
            throw new RuntimeException("중복이메일 입니다.");
        }
    }
    public String saveMemberByCurrentUserAndReturnRole(String providerId, CurretUser curretUser){
        Member saved = memberRepository.save(
                Member.builder()
                        .name(curretUser.getName())
                        .email(curretUser.getEmail())
                        .provider(providerId)
                        .nickname("별명")
                        .build()
        );
        return saved.getRole();
    }
}

▶ 반환된 OAuth2User 구현체 ( = UserDetails )는 이후 Authentication 구현체Principal에 포함되어 SecurityContextHolder의 현재 스레드SecurityContext에 저장됨


Provider : googlewjdtn747@naver.com이 등록된 상태에서 Provider : naverwjdtn747@naver.com회원가입중복이메일 처리됨

profile
공부기록 블로그

0개의 댓글