[스프링 시큐리티 OAuth] - 구글 OAuth2 로그인 완전 정복 🩷 ps. 이것만 읽으면 구현은 한다.

코린이서현이·2024년 6월 10일
0
post-thumbnail

들어가면서...

몇주째 하고 있는데 ^^ 즐거워요.. ^^

OAuth2를 이용한 로그인

한 번 정복하고 나면 로그인구현이 즐거워진다는 OAuth-Client2!!
OAuth-Client2의 동작 원리와 사용하기 위한 방법들에 대해서 배워보자! 

OAuth2 사용의 이점

OAuth2는
1. 코드를 발급 받고..
2. 코드를 이용해서 토큰을 발급받고..
3. 토큰을 이용해서 사용자 정보를 받아오는 과정을

단순하게 만들어준다!!

즉, OAuth 서버에서 사용자 정보를 가져와준다

OAuth2 로그인 사용 준비

① OAuth 로그인을 사용할 수 있도록 OAuth 로그인을 활성화한다.
② OAuth2 로그인에 쓰이는 클라이언트 정보를 등록해야한다.

이 두번째를 담당해주는 객체가 바로 OAuth2ClientAutoConfiguration!!

OAuth2ClientAutoConfiguration 이란??

  • OAuth2ClientAutoConfiguration은 Spring Boot에서 OAuth 2.0 클라이언트와 관련된 설정을 자동으로 구성해준다.
  • OAuth 2.0 클라이언트 정보를 기반으로 클라이언트를 등록하고, 토큰 관리 및 보안 설정을 자동으로 구성한다.
  • 이를 통해 개발자는 별도의 복잡한 설정 없이도 OAuth 2.0 클라이언트 기능을 손쉽게 사용할 수 있습니다.

1) OAuth 로그인을 사용할 수 있도록 OAuth 로그인을 활성화

HttpSecurity 설정을 통해서 oauth2Login을 활성화한다.

👉 이렇게 하면 ClientRegistrationRepository 빈을 등록해서 직접 클라이언트 정보를 등록해야한다.

2) OAuth2 로그인에 쓰이는 클라이언트 정보를 등록

① Java Config 클래스를 이용해 ClientRegistrationRepository을 빈으로 등록해 동적으로 클라이언트 정보를 등록할 수 있다.

이외에도 ② application.yml 또는 application.properties 파일을 이용방법, ③ 환경 변수를 이용한 설정 방법이 있다.

나는 위에서 HttpSecurity 설정을 이용했기 때문에 첫번째 방법만 사용할 수 있다.

등록해야하는 클라이언트 정보

- clientId
- clientSecret
- redirectUri
- 인증 페이지로 리디렉션하기 위한 URL
- 액세스 토큰을 받을 수 있는 URL을 설정
- 사용자 이름(또는 ID)으로 사용할 속성의 이름을 설정
- 클라이언트의 이름 설정 

어떤 방법을 이용하던 위의 정보를 모두 제공해야한다.

OAuth2 로그인 과정

① 사용자 정보를 가져온다.
② 가져온 사용자 정보를 이용해서 후처리를 한다.

시큐리티 OAuth2을 이용하면 OAuth 서버에서 사용자 정보를 받아왔다고 해보자

🤔 그러면 우리는 생각해봐야할 부분이 있다!

① 사용자 정보를 리턴 받는다면 그 위치는 어디일까?
② 사용자 정보는 어떤 객체에 담겨올까?
③ 회원가입을 진행하고 세션에 넣을 객체가 필요하다!!

정답 공개 🛎️

① 사용자 정보를 리턴 받는다면 그 위치는 DefaultOAuth2UserService 클래스의 loadUser 메서드
② 사용자 정보는 OAuth2UserRequest 타입에 담겨온다.
③ OAuth2 로그인을 하면 loadUser 메서드가 호출되기 때문에 이 메서드에서 회원가입을 진행하고, OAuth2User 객체를 반환하면 이 객체가 Authentication에 담긴다.

public OAuth2User loadUser(OAuth2UserRequest userRequest) 
		throws OAuth2AuthenticationException {
	...
    return "Authentication에 담길 OAuth2User타입"
}

Authentication의 내부 객체로 올 수 있는 건 UserDetails 타입이라고 했잖아요..? @_@

사실 아직은 배우지 않았지만 Authentication의 내부 객체로는
1. 폼 로그인을 통해 생성되는 UsernamePasswordAuthenticationToken
2. OAuth2 인증을 통해 생성된 인증 토큰 OAuth2AuthenticationToken
3. JWT (JSON Web Token)를 사용하는 인증 토큰 JwtAuthenticationToken 이다.

UserDetails를 이용해 UsernamePasswordAuthenticationToken`를 생성하는 과정이 있었던 것...!!

하여튼 OAuth2UserAuthentication의 내부 객체로 들어갈 수 있다.

⭐⭐ 다시 정리 하자면 ⭐⭐

① OAuth 로그인 활성화 하기
② 클라이언트 정보 제공하기
③ 전달받은 유저정보를 가지고 회원가입과 로그인 세션을 위한 OAuth2User을 제공

을 해야한다.

직접 해보기

OAuth2로그인 활성화

MySecurityConfig 파일에서 SecurityFilterChain반환 메서드에서 설정

package com.jsh.securitystudy.config;

@Configuration
@EnableWebSecurity

public class MySecurityConfig {

    private final PrincipalOauth2UserService principalOauth2UserService;

    public MySecurityConfig(@Lazy PrincipalOauth2UserService principalOauth2UserService) {
        this.principalOauth2UserService = principalOauth2UserService;
    }

    @Bean //빈으로 등록하기
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeRequests((authorizeRequests) -> authorizeRequests
                                .requestMatchers("/user/**").authenticated()
                                .requestMatchers("/admin/**").hasRole("ADMIN")
                                .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER"))
                .formLogin(formLogin -> formLogin
                        .loginPage("/loginForm") 
                        .loginProcessingUrl("/login") 
                        .defaultSuccessUrl("/")) 
                //설정 시작------------------------ 
                .oauth2Login(oauth2Login -> oauth2Login
                        .loginPage("/loginForm")
                        .defaultSuccessUrl("/")
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
                                .userService(principalOauth2UserService)));
				//설정 완료------------------------

        return http.build();

    }
}

각 메서드 더 자세히 살펴보기

  • .oauth2Login(oauth2Login -> oauth2Login : OAuth2 로그인 설정을 시작합니다.
  • .loginPage("/loginForm") : 사용자 정의 로그인 페이지 URL을 지정합니다.
    .defaultSuccessUrl("/") : 로그인 성공 후 리디렉션될 기본 URL을 지정합니다.
  • .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint : 사용자 정보 엔드포인트 설정을 시작합니다. 이는 OAuth2 제공자로부터 사용자 정보를 가져오는 데 사용한다.
  • .userService(principalOauth2UserService)));: 사용자 정보를 가져오기 위해 사용할 OAuth2UserService를 지정합니다. 이 서비스 안에 loadUser메서드가 있는 것이다.

클라이언트 정보 제공

MySecurityConfig 파일에서 ClientRegistrationRepositor 타입 빈을 등록한다.

package com.jsh.securitystudy.config;


@Configuration
@EnableWebSecurity
public class MySecurityConfig {

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        ClientRegistration googleClientRegistration = ClientRegistration.withRegistrationId("google")
                .clientId("760459109498-1dpf66s2nefpk02uvg07479hnakltacs.apps.googleusercontent.com")
                .clientSecret("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")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("http://localhost:8000/security/login/oauth2/code/google") //"http://localhost:8000/security/login/oauth2/code/google"
                .scope("profile", "email")
                .authorizationUri("https://accounts.google.com/o/oauth2/auth")
                .tokenUri("https://oauth2.googleapis.com/token")
                .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
                .userNameAttributeName("sub")
                .clientName("Google")
                .build();

        return new InMemoryClientRegistrationRepository(googleClientRegistration);
    }

}
  • clientRegistrationRepository 메서드는 ClientRegistration 객체를 생성하고, 이를 InMemoryClientRegistrationRepository에 추가하여 빈으로 등록한다.
  • 이렇게 등록된 ClientRegistrationRepository 빈은 스프링 시큐리티 OAuth2 설정에서 사용한다.

각 메서드 자세히 살펴보기

  • ClientRegistration.withRegistrationId("google")
    ClientRegistration 객체를 생성하기 위한 withRegistrationId("google") 메서드를 호출, 여기서 registrationId는 이 클라이언트 등록 정보의 고유 식별자입니다. 보통 OAuth2 로그인 시 사용자가 선택할 수 있도록 식별하는 데 사용됩니다.

  • .clientId("your-client-id") : 구글 OAuth2 클라이언트의 클라이언트 ID를 설정

  • .clientSecret("your-client-secret") : 구글 OAuth2 클라이언트의 비밀번호 설정

  • .redirectUri("http://localhost:8000/security/login/oauth2/code/google") : OAuth2 인증이 완료된 후 사용자가 리디렉션될 URI 설정

  • .authorizationUri("https://accounts.google.com/o/oauth2/auth")
    " 로그인창을 띄우는 페이지 주소 설정

  • .tokenUri("https://oauth2.googleapis.com/token")
    인증 코드 교환을 통해 액세스 토큰을 받을 수 있는 URL을 설정

  • .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
    액세스 토큰을 사용하여 사용자 정보를 가져올 수 있는 URL을 설정

  • .userNameAttributeName("sub"):
    사용자 정보 응답에서 사용자 이름(또는 ID)으로 사용할 속성의 이름을 설정합니다. 구글의 경우, 일반적으로 sub

  • .clientName("Google") : 클라이언트의 이름을 설정

  • .build(); : 빌드!

  • return new InMemoryClientRegistrationRepository(googleClientRegistration); : 빈으로 등록하는 부분~!

로그인과 회원가입 진행

로그인과 회원가입을 하는 위치는?

위 OAuth 설정에서 보았듯이 PrincipalOauth2UserService을 설정했다.
PrincipalOauth2UserServiceDefaultOAuth2UserService을 상속받는 클래스이고, 이 내부에 있는 loadUser메서드가 OAuth2로그인 후에 사용자 유저정보를 넘겨받고 호출된다.

로그인을 하기 위해서는 OAuth2User타입의 객체를 리턴해야한다.

⭐⭐ PrincipalDetails수정⭐⭐

이게 무슨말이냐면 스프링 시큐리티를 사용해서 세션을 가지고 로그인을 진행하면
@AuthenticationPrincipal어노테이션을 통해 현재 로그인한 사용자의 정보를 넘겨받을 수 있단말이에요??

그런데 폼 로그인을 하면! Authentication의 내부 객체로 UserDetails를 사용하고.. 구글 로그인을 하면 내부 객체로 OAuth2User 타입을 가지고 있어서

결국 상황마다 전달받는 타입이 다르다.. 😨😨😨

따라서 UserDetails과 OAuth2User를 한 번에 다룰 수 있도록 PrincipalDetails수정이 필요하다.

다시 정리하자면,

① 구글로그인을 통해서 세션을 넣으려면 OAuth2User타입의 객체를 리턴해야한다.
② 그러나 폼로그인과, 구글 로그인 모두를 지원하는 나의 프로젝트에서는 통일된 타입이 필요하다.
③ 따라서 UserDetailsOAuth2User 다중상속한 PrincipalDetails을 리턴한다.
④ 이후 @AuthenticationPrincipal어노테이션을 통해 현재 로그인한 사용자의 정보를 넘겨받을 때 PrincipalDetails타입으로 전달받아 어떤 로그인을 구현했던 동일하게 사용할 수 있도록 한다.

package com.jsh.securitystudy.config.auth;


@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
	// 다중 상속
    private User user;
    private Map<String, Object> attrilbutes;


    //일반 로그인
    public PrincipalDetails(User user) {
        this.user = user;
    }

    //OAuth 로그인 사용
    public PrincipalDetails(User user,Map<String, Object> attrilbutes ) {
        this.user = user;
        this.attrilbutes = attrilbutes;
    }

    //OAuth2User 확장으로 인해 추가한 메서드
    @Override
    public Map<String, Object> getAttributes() {
        return attrilbutes;
    }


}

그러면 이후 어떤 로그인을 구현했더라도 동일하게 사용할 수 있다.

    @GetMapping("/user")
    public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
        return "principalDetails.getUser() = " + principalDetails.getUser();
    }

① 일반 폼 로그인

② 구글 폼 로그인

전달 받는 객체 : userRequest

회원가입 구현하기

① userRequest 를 통해 OAuth2User를 만들어준다.
② OAuth2User를 통해 프로젝트에서 쓰는 회원정보인 USer 객체를 만들어준다.
이때 어떤 서버를 사용했는지는 userRequest.getClientRegistration().getRegistrationId()를 사용한다.
③ 해당 회원정보가 이미 존재하는지 검사하고 없다면 회원가입 시킨다.

package com.jsh.securitystudy.config.oauth;

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserRepository userRepository;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public PrincipalOauth2UserService(@Lazy BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
    
 
 
	@Override // 구글로부터 받은 UserRequest 데이터에 대한 후처리되는 함수
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        //① userRequest 를 통해 OAuth2User를 만들어준다. 
        OAuth2User oAuth2User = super.loadUser(userRequest);
        System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
        //이 정보를 사용해 회원 가입을 진행한다.


        // [회원가입 진행]
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oAuth2User.getAttribute("sub"); //
        String username = oAuth2User.getAttribute("name") + "_" +  providerId; //
        String email = oAuth2User.getAttribute("email"); //
        String password = bCryptPasswordEncoder.encode("비밀번호");
        String role = "ROLE_USER";

        //이미 존재하는 회원인지 검사
        User userEntity = userRepository.findByUsername(username);

        if (userEntity == null) {
            //회원가입 진행하기
            userEntity = User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();

            userRepository.save(userEntity);
        }

        return new PrincipalDetails(userEntity,oAuth2User.getAttributes());

    }
}

로그인 구현

로그인은 세션에 넣는 것이다.
따라서 OAuth2 로그인 시 넣을 수 있는 OAuth2User 타입 객체를 리턴하면 정상로그인!!

return new PrincipalDetails(userEntity,oAuth2User.getAttributes());

정상작동 확인하기

폼 로그인, 구글로그인 모두 정상적으로 회원가입 완료!!

폼 로그인, 구글로그인 모두 정상적으로 현재 세선 정보 반환

마무리하면서...

💦💦 진짜 오래걸리구... 하고 나니까 너무 간단해서 웃기다..^^

글이 미친듯이 길어졌지만 누구라도 잘 이해ㅐ할 수 있도록 정말 자세히 써봤다.. ^^
미래의 나야 언제든지 구현해조.. ㅜㅜ 

깃허브 주소 : https://github.com/jinseohyun1228/Spring_Security_Study

profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글