[Java]OAuth 2.0 구글 설정

정석환·2025년 4월 29일
post-thumbnail

사전 설명

Oauth 2.0에 앞서서 우리는 시큐리티 필터체인의 구조를 볼 것이다.

시큐리티 필터 체인의 구조는 cors 설정, 및 csrf 설정, formlogin, 인증 권한설정, oauthlogin 설정, 추가 필터 설정 순으로 설정하며

필자는 이에 따라 총 4개의 글을 적을 것이다.
1편 OAuth 2.0 구글 설정
2편 SuccessHandler
3편 JWT 필터
4편 액세스 토큰 갱신 및 프론트 api 연결
에 대해서 글을 적을 것이다.
이를 다 따라 했을 경우에는 기본적인 OauthLogin 및 jwt토큰 설정이 완료가 될 것이다.

사전 설정

공식 사이트 설정 방법

https://developers.google.com/identity/protocols/oauth2/service-account?hl=ko#creatinganaccount

위의 사이트를 들어가면 google에서 공식적으로 어떻게 설정하는지 설명이 되어 있다. 이가 어렵다면 아래와 같이 따라하자

https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts?hl=ko&supportedpurview=project&allowsmanagementprojects=true
먼저 위의 링크에 들어가서 프로젝트를 만들어준다.
프로젝트 만들기를 생성을 누른 후에
원하는 프로젝트 이름을 생성한다.

만들고 나면 아래와 같이 알림이 뜨는데 프로젝트 만들기를 누르면

이러한 페이지로 오게 된다.
다 되었다면 중간에 검색으로 Oauth 동의 화면을 검색 하면 된다.


동의화면으로 오면 시작하기를 눌러주고

프로젝트 구성

앱정보에서는 원하는 앱 이름, 이메일을 넣어주면 되고
대상은 외부 를 선택을 해주자,
연락처 정보는 자기가 연락 받고싶은 이메일을 넣으면 되고
완료 해주고 만들면 된다.

Oauth 개요


여기서 이제 클라이언트 만들기를 진행 하면 되는데

위에서 우리는 웹을 설정 할거기에 웹, 이름은 자기가 원하는 프로젝트,
그다음에 승인된 JavaScript원본 이부분은 넘어가면되고
승인된 리디렉션 URI를 적어주는데 필자는 현재 도메인이 없이 그냥 할 것이기 때문에 http://localhost:8080/를 적어 주었고.
도메인이 있다면 도메인 주소로, 아니면 자기가 사용할 포트 주소로 적어주면 된다.

login/oauth2/code/google

그리고 생성 하면 되는데 주의 해야하는 것이 있다.
절대로 클라이언트 ID랑 비밀번호는 유출이 되면 안된다.

클라이언트 ID는 공개돼도 비교적 위험이 적지만,클라이언트 Secret은 비밀번호처럼 취급해야 하며
→ 이게 유출되면 제3자가 당신의 앱을 가장해서 OAuth 인증 요청을 보낼 수 있다.
→ 심할 경우, 구글 API 사용량 초과, 요금 폭탄, 보안 사고가 발생할 수 있으니 꼭 주의하자.

잊으면 안되니 json파일로 다운로드 해서 잘 보관 해두자.

설정이 잘되었다면 위의 사진과 같이 잘 설정이 되었을 것이다.
이렇게 까지 잘 따라 왔다면 사전 설정은 끝이다.

Java 적용법

설명 전 확인 사항

필자는 일단 초심자가 뭐부터 만들어야 할지 이해가 힘들다는 점에 따라 먼저 구성하는 요소들 부터 만들었다.
OAuth2.0로그인의 흐름이 어떻게 작동하는지 확인하고 싶다면 아래의 SecurityConfig 부터 위로 올라오면서 보면 된다.

yml 설정

    security:
      oauth2:
        client:
          registration:
            google:
              client-id: id키
              client-secret: secret키
              scope:
                - email
                - profile

위에서 받은 id랑 secret 을 넣어준 상태로 위의 코드를 spring 의 하위 레벨에 넣어주면 된다.

폴더 구성

폴더 구성은 이렇게 되어있으며 위와 디렉토리에 위의 클래스 들을 만들어 줄 것이다.

provider Enum 클래스

public enum Provider {
    GOOGLE
}

처음에는 Enum클래스를 만들어 줄 것이다.
Provider이라는 것은 어떤 제공자의 Oauth2.0을 사용 했는지를 뜻하며
이는 엔티티의 관리를 위해서 member에 필드로 넣어주기 위해서 만들었다.
필자는 Google만 하려고 이리하였으나 GOOGLE,NAVER,KAKAO 등 더 넣어도 된다.

Role Enum 클래스

public enum Role {
    MEMBER,ADMIN
}

Role이라는 것은 멤버의 권한을 설정을 해주기 위해서 만들었다.

엔티티 클래스 설정

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @Column(name = "memberId")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 50)
    private String email;

    @Column(nullable = false, unique = true, length = 20)
    private String nickname;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private Provider provider;

	@Enumerated(EnumType.STRING)
    private Role role = Role.MEMBER;

    @Column(nullable = false)
    private LocalDateTime createdAt;
	
    @Builder
    public Member(String email, String nickName, Provider provider, LocalDateTime createdAt) {
        this.email = email;
        this.nickname = nickName;
        this.provider = provider;
        this.createdAt = createdAt;
    }

}

필자는 정말 기본적인 것들만 설정을 해줄 것이다.
주 키(id), email,nickName,provicer,createdAt만 설정해주고, role은 기본 가입시에 member로 초기값만 설정해준다.
Builder 패턴을 만들어준다.(나중에 Member 객체를 만들기 위해서 필요)

Repository

public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
}

리포지토리는 너무 간단하다.
email을 받아서 Optional타입의 Member을 반환 받는다.
이는 나중에 로그인 후 이메일을 통해 가입되어 있는 Member이라면 찾은 member을 반환 받고 아니라면 새로운 Member객체를 만들어 주기 위해서 사용한다.

CustomUserPrincipal

@Getter
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
//사용자 정보를 조회하는 용도의 클래스
public class CustomUserPrincipal implements OAuth2User, UserDetails {

    @Setter
    private Long id;

    private String name;
    private String email;

    @Setter
    private Role role;

    @Setter
    private boolean isBlocked;

    private Map<String, Object> attributes;

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }

    @Override
    public String getPassword() {
        return "";
    }

    @Override
    public String getUsername() {
        return name;
    }

    @Override
    public String getName() {
        return name;
    }


    public static CustomUserPrincipal from(Member member, Map<String, Object> attributes) {
        CustomUserPrincipal customUserPrincipal = new CustomUserPrincipal();
        customUserPrincipal.id = member.getId();
        customUserPrincipal.email = member.getEmail();
        customUserPrincipal.role = member.getRole();
        customUserPrincipal.name = member.getNickname();
        customUserPrincipal.attributes = attributes;
        return customUserPrincipal;

    }

}

이 클래스는 OAuth2 로그인 후 전달받은 사용자 정보를 담고 있으며,
Spring Security의 OAuth2User 인터페이스를 구현하여 인증된 사용자의 정보를 표현하는 역할을 한다.
추후에 유저 정보도 가져와야 하기 때문에 UserDetail또한 구현 해야한다.
getPassword의 경우 Oauth로그인이기 때문에 필요없다.

OAuthAttributes

@Getter
@AllArgsConstructor
public class OAuthAttributes {
    private String name;
    private String email;
    private String provider;
    private String providerId;
	
    //registrationId가 google이라면 google에 맞는 dto를 생성한다.
    public static OAuthAttributes of(String registrationId, Map<String, Object> attributes) {
        if (registrationId.equals("google")) {
            return ofGoogle(attributes);
        }
        throw new IllegalArgumentException("Unknown provider: " + registrationId);
    }
	
    // registrationId가 "google"인 경우 Google 사용자 정보를 처리하는 메서드
    // DefaultOAuth2UserService의 loadUser 메서드를 통해 가져온 OAuth2 사용자 정보를 
    // OAuthAttributes DTO로 매핑하여 저장하기 위해 사용된다.
    private static OAuthAttributes ofGoogle(Map<String, Object> attributes) {
        return new OAuthAttributes(
                (String) attributes.get("name"),
                (String) attributes.get("email"),
                "GOOGLE",
                (String) attributes.get("sub")
        );
    }
}

이 부분은 provider에 따라 DTO를 다르게 생성한다.
왜 이렇게 설계했냐면, Kakao, Naver, Google 등 각 사이트마다 사용자 정보를 제공하는 방식이 다르기 때문에,
각 provider에 맞는 별도의 처리가 필요하기 때문이다.

CustomOAuth2UserService


@Service
@Slf4j
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {

        // 제공자 정보가 들어 있다.
        String registrationId = userRequest.getClientRegistration().getRegistrationId();

        // loadUser 에는 사용자 정보, 고유 식별자, 권한 정보를 포함하고 있다..
        OAuth2User oAuth2User = super.loadUser(userRequest);

        // 고유 id, 이름, 사진, 이메일, 지역 등에 대한 정보가 들어 있다.
        Map<String, Object> attributes = oAuth2User.getAttributes();

        // 제공자 정보, 및 id에 대한 정보를 dto로 변환한다.
        OAuthAttributes oAuthAttributes = OAuthAttributes.of(registrationId, attributes);

        // 기존 id가 없다면 새로운 Member등록을 하고 , 기존 id가 있다면 Email을 반환한다.
        Member member = saveOrUpdate(oAuthAttributes);

        return new CustomUserPrincipal(member, attributes);
    }
	
    // 기존 회원가입 정보가 없다면 가입을 시켜주고 있다면 기존의 Email을 사용하여 Member을 가져온다.
    private Member saveOrUpdate(OAuthAttributes attributes) {
    
        Member member = Member.builder()
                .email(attributes.getEmail())
                .nickname(attributes.getName())
                .provider(Provider.valueOf(attributes.getProvider()))
                .createdAt(LocalDateTime.now())
                .build();
       
        return memberRepository.findByEmail(attributes.getEmail())
                .orElseGet(() -> memberRepository.save(member));
    }

}

제공자 정보를 확인한 후, loadUser를 통해 사용자 정보를 가져온다.
그 다음, member 등록에 필요한 attributes만 추출하여 제공자 정보와 함께 DTO로 변환한다.
이렇게 변환된 정보를 기반으로 회원 가입 또는 로그인 처리를 진행한다.

SecurityConfig

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
        		// CSRF 토큰 검사를 비활성화한다.
                .csrf(csrf -> csrf.disable())
                // index, 로그인 및 OAuth 관련 경로는 인증 없이 접근 가능하다.
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
                        .anyRequest().authenticated()
                )
                // OAuth2 로그인 시 사용자 정보를 customOAuth2UserService로 처리한다.
                .oauth2Login(oauth2 -> oauth2
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(customOAuth2UserService)
                        )
                )
                // 로그인 성공 시 무조건 index으로 리디렉션
                .defaultSuccessUrl("/") 
)
                .build();
    }
}

가장 중요한 점은, SecurityConfig에서 .userService()에 OAuth2UserService 구현체를 등록하면
Spring Security가 OAuth2 로그인 처리 과정 중 자동으로 해당 구현체의 loadUser() 메서드를 실행한다는 것이다.

현재는 defaultSuccessUrl("/")을 사용하고 있지만, 다음 편에서는 SuccessHandler를 활용하여 로그인 성공 이후의 동작을 세밀하게 제어할 예정이다다.

흐름 정리

OAuth2 로그인 시 내부 흐름은 다음과 같다:

SecurityFilterChain -> CustomOauthUserService -> LoadUser 메서드 -> saveOrUpdate메서드 -> CustomUserPrincipal객체 생성(SecurityContext저장)

이 설정을 통해 OAuth2.0 Google 로그인을 위한 기본적인 인증/인가 흐름이 구성된다.
이후에는 사용자의 정보를 세션 또는 토큰(JWT 등)으로 관리하거나, 프론트엔드와의 인증 상태 연동, 리디렉션 처리 등 후속 작업을 이어가면 된다.

profile
자바,스프링 백엔드 개발자를 꿈꾸는 초보아빠

0개의 댓글