4 분 소요

OAuth

우리의 서비스가 사용자가 이용하는 다른 플랫폼의 사용자 정보에 접근하기 위해 해당 플랫폼으로부터 접근 권한을 위임받을 수 있게하는
표준 프로토콜을 말한다. OAuth를 이용하기 위해 Resource server에 Client, Redirect URI를 등록하는 작업이 필요하다.
등록 후에는 Client ID, Client Secret을 발급받고 이를 활용해 액세스 토큰을 얻을 수 있다.

build.gradle

Spring Security, OAuth2 사용을 위해 의존성을 추가해주자.

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"


SecurityConfig

Spring Security 설정 파일이다. OAuth2 사용을 위해 http.oauth2Login(withDefaults())를 추가해주자.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http
              .csrf((csrf) -> csrf.disable());

      http
              .authorizeHttpRequests((authorizeHttpRequests) ->
                      authorizeHttpRequests
                              .requestMatchers("/login", "/signup").permitAll()
                              .anyRequest().authenticated()
              )
              .formLogin(withDefaults())
              .oauth2Login(withDefaults())
      return http.build();
  }
  ...
}


PrincipalDetails

현재 사용자의 정보를 가져올 수 있도록 하는 PrincipalDetails 클래스를 정의해주자. 이 때 OAuth2를 사용하지 않는다면
UserDetails만 구현해도 되지만, OAuth2를 사용할 것이므로 OAuth2User 또한 구현해줘야 한다. getAuthorities(),
getAttributes(), … isEnabled()까지 모두 구현해주자.

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

  private User user;
  private Oauth2UserInfo oAuth2UserInfo;

  public PrincipalDetails(User user) {
      this.user = user;
  }

  public PrincipalDetails(User user, Oauth2UserInfo oAuth2UserInfo) {
      this.user = user;
      this.oAuth2UserInfo = oAuth2UserInfo;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
      return user.getRoles().stream()
              .map(SimpleGrantedAuthority::new)
              .collect(Collectors.toList());
  }

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

  @Override
  public String getPassword() {
      return user.getPassword();
  }

  @Override
  public String getUsername() {
      return user.getEmail();
  }

  @Override
  public String getName() {
      return oAuth2UserInfo.getProviderId();
  }

  @Override
  public boolean isAccountNonExpired() {
      return true;
  }

  @Override
  public boolean isAccountNonLocked() {
      return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
      return true;
  }

  @Override
  public boolean isEnabled() {
      return true;
  }
}
  • getAuthorities() : 권한 정보 반환
  • getAttributes() : OAuth 사용자 정보 반환

Oauth2UserInfo : 사용자 정보

OAuth2 사용자 정보를 반환하는 메소드를 정의해둔 인터페이스다. 구글, 카카오 등 여러 플랫폼에서의 사용자 정보를 같은 메소드로
얻을 수 있도록 정의주자. provider는 제공자명(google, kakao 등)을, providerId는 제공자에 대한 식별자를 의미한다.

public interface Oauth2UserInfo {
  public Map<String, Object> getAttributes();
  String getProviderId();
  String getProvider();
  String getEmail();
  String getName();
}


GoogleUserInfo : 구글 사용자 정보

public class GoogleUserInfo implements Oauth2UserInfo {
  private Map<String, Object> attributes;

  public GoogleUserInfo(Map<String, Object> attributes) {
      this.attributes = attributes;
  }

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

  @Override
  public String getProviderId() {
      return attributes.get("sub").toString();
  }

  @Override
  public String getProvider() {
      return "google";
  }

  @Override
  public String getEmail() {
      return attributes.get("email").toString();
  }

  @Override
  public String getName() {
      return attributes.get("name").toString();
  }
}


KakaoUserInfo : 카카오 사용자 정보

public class KakaoUserInfo implements Oauth2UserInfo {
  private Map<String, Object> attributes;
  private Map<String, Object> attributesAccount;
  private Map<String, Object> attributesProfile;

  public KakaoUserInfo(Map<String, Object> attributes) {
      this.attributes = attributes;
      this.attributesAccount = (Map<String, Object>) attributes.get("kakao_account");
      this.attributesProfile = (Map<String, Object>) attributesAccount.get("profile");
  }

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

  @Override
  public String getProviderId() {
      return attributes.get("id").toString();
  }

  @Override
  public String getProvider() {
      return "Kakao";
  }

  @Override
  public String getEmail() {
      return attributesAccount.get("email").toString();
  }

  @Override
  public String getName() {
      return attributesProfile.get("nickname").toString();
  }
}


User 엔티티 정의

사용자 엔티티 클래스로, OAuth2와 관련된 필드는 username(or email), roles, provider, providerId,
providerLoginId가 있다.

@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
public class User {

  @Id @GeneratedValue
  @Column(name = "user_id")
  private Long id;

  @NonNull @Length(max = 20)
  private String username;

  @NonNull
  private String password;

  @NonNull @Length(min = 7, max = 64)
  private String email;

  @NonNull
  @ElementCollection(fetch = EAGER)
  private List<String> roles = new ArrayList<>();

  private String provider;
  private String providerId;
  private String providerLoginId; //{provider}_{providerId}

  public User(String email, String provider, String providerId) {
      username = email.substring(0, email.indexOf('@')); //이메일에서 도메인을 제거한 ID
      providerLoginId = provider + "_" + providerId;
      this.email = email;
      this.provider = provider;
      this.providerId = providerId;
      roles.add("USER");
  }
}
  • roles : 갖고있는 권한들
  • provider : 사용자 정보 제공자명
  • providerId : 제공자명 식별자
  • providerLoginId : {provider}_{providerId} 형태로, 식별자 중복 방지용 (선택)
  • 생성자로 provider, providerId, providerLoginId를 받을 수 있게 만들어 OAuth2로 회원가입 가능하게 만들기

PrincipalOauth2UserService

OAuth2로 로그인할 때 loadUser()로 사용자 정보를 가져와야 한다. 첫 로그인 시 자동으로 회원가입을 하고 그렇지 않으면 DB에
저장된 ID/PW를 비교해서 로그인 후 사용자 정보를 얻는다. 그렇게 얻은 user와 OAuth2 사용자 정보 oAuth2UserInfo
PrincipalDetails 생성 후 반환한다. (PrincipalDetailsOAuth2User를 구현했으므로 OAuth2User로 반환 가능)

@Service
@RequiredArgsConstructor
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

  private final UserRepository userRepository;

  @Override
  public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    OAuth2User oAuth2User = super.loadUser(userRequest);

    Oauth2UserInfo oAuth2UserInfo = null;
    String provider = userRequest.getClientRegistration().getRegistrationId();

    //provider에 따라 oAuth2User 정보 얻음
    if (provider.equals("google")) {
        oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
    } else if (provider.equals("kakao")) {
        oAuth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
    }

    String providerId = oAuth2UserInfo.getProviderId();
    String loginId = provider + "_" + providerId;
    String email = oAuth2UserInfo.getEmail();

    Optional<User> optionalUser = userRepository.findByProviderLoginId(loginId);
    User user;

    if (optionalUser.isEmpty()) {
        //첫 로그인 -> 회원가입
        user = new User(email, provider, providerId);
        userRepository.save(user);
    } else {
        //DB에 저장된 정보와 비교해서 로그인 후 사용자 정보 얻음
        user = optionalUser.get();
    }

    return new PrincipalDetails(user, oAuth2UserInfo);
  }
}


yml 설정

OAuth2를 사용하기 위해 yml 파일까지 만들어주자!
각 플랫폼 Resource server에 Client로 등록하면서 얻은 Client IDClient Secret을 입력해주면 된다.
Client ID, Client Secret이 git으로 관리되지 않도록 gitignore로 꼭 처리해주자😄

# application.yml
spring:
  profiles:
    include: oauth

# application-oauth.yml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: {Client ID}
            client-secret: {Client Secret}
            scope:
              - email
          kakao:
            client-id: {Client ID}
            client-secret: {Client Secret}
            scope: account_email
            client-name: Kakao
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-authentication-method: client_secret_post
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id


References

카테고리:

업데이트:

댓글남기기