[Spring] OAuth2 로그인 구현하기 - 구글, 카카오
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
생성 후 반환한다. (PrincipalDetails
는 OAuth2User
를 구현했으므로 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 ID
와 Client 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
댓글남기기