서비스에서 사용자 개인정보와 인증에 대한 책임을 지지 않고 신뢰할 만한 타사 플랫폼에 위임
https://kauth.kakao.com/oauth/authorize : response_type, client_id, redirect_uri
Redirect URI : code
https://kauth.kakao.com/oauth/token : code
Redirect URI : token_type, access_token, expires_in, refresh_token, refresh_token_expires_in, scope
https://kapi.kakao.com/v2/user/me : access_token
{
"id": 123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
"profile": {
"nickname": "홍길동",
...
},
"name":"홍길동",
...
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
},
"for_partner": {
"uuid": "${UUID}"
}
}
a href="/oauth2/authorization/[naver || kakao || google]"
의 버튼을 통해 로그인 창 요청작동하는 몇가지 oauth2 코드를 아래에 작성하겠지만 REST API 개발 중이므로 spring security oauth2 client를 통한 로그인 처리 관련 코드는 작성하지 않을 예정
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
spring:
security:
oauth2:
client:
registration:
kakao:
client-id:
client-secret:
redirect-uri:
client-name: Kakao
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
scope:
- profile_nickname
naver:
client-id:
client-secret:
redirect-uri:
client-name: Naver
authorization-grant-type: authorization_code
scope:
- name
google:
client-id:
client-secret:
scope:
- email
- profile
provider: # kakao, naver만 추가로 작성
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
naver:
authorization_uri: https://nid.naver.com/oauth2.0/authorize
token_uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user_name_attribute: response
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
.oauth2Login(oauth2Login ->
// oauth2Login.loginPage("/login/oauth2")
oauth2Login.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.userService(customOAuth2UserService))
);
return http.build();
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. OAuth2 로그인 유저 정보를 가져옴
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("getAttributes : {}", oAuth2User.getAttributes());
// 2. provider : kakao, naver, google
String provider = userRequest.getClientRegistration().getRegistrationId();
log.info("provider : {}", provider);
// 3. 필요한 정보를 provider에 따라 다르게 mapping
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(provider, oAuth2User.getAttributes());
log.info("oAuth2UserInfo : {}", oAuth2UserInfo.toString());
// 4. oAuth2UserInfo가 저장되어 있는지 유저 정보 확인
// 없으면 DB 저장 후 해당 유저를 저장
// 있으면 해당 유저를 저장
User user = userRepository.findByProviderAndPassword(oAuth2UserInfo.getProvider(), oAuth2UserInfo.getPassword())
.orElseGet(() -> userRepository.save(oAuth2UserInfo.toEntity()));
log.info("user : {}", user.toString());
// 5. UserDetails와 OAuth2User를 다중 상속한 CustomUserDetails
return new CustomUserDetails(user, oAuth2User.getAttributes());
}
}
getAttributes :
{
sub=,
name=,
given_name=,
family_name=,
picture=,
email=,
email_verified=true,
locale=ko,
hd=
}
provider : google
getAttributes :
{
id=,
connected_at=2024-03-09T11:19:11Z,
properties={
nickname=
},
kakao_account={
profile_nickname_needs_agreement=false,
profile={nickname=}
}
}
provider : kakao
getAttributes :
{
resultcode=00,
message=success,
response={
id=,
nickname=,
name=
}
}
provider : naver
Builder
@Getter
@ToString
public class OAuth2UserInfo {
private String id;
private String password;
private String email;
private String nickname;
private String provider;
public static OAuth2UserInfo of(String provider, Map<String, Object> attributes) {
switch (provider) {
case "google":
return ofGoogle(attributes);
case "kakao":
return ofKakao(attributes);
case "naver":
return ofNaver(attributes);
default:
throw new RuntimeException();
}
}
private static OAuth2UserInfo ofGoogle(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.provider("google")
.id("google_" + (String) attributes.get("sub"))
.password((String) attributes.get("sub"))
.nickname((String) attributes.get("name"))
.email((String) attributes.get("email"))
.build();
}
private static OAuth2UserInfo ofKakao(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.provider("kakao")
.id("kakao_" + attributes.get("id").toString())
.password(attributes.get("id").toString())
.nickname((String) ((Map) attributes.get("properties")).get("nickname"))
.build();
}
private static OAuth2UserInfo ofNaver(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.provider("naver")
.id("naver_" + (String) ((Map) attributes.get("response")).get("id"))
.password((String) ((Map) attributes.get("response")).get("id"))
.nickname((String) ((Map) attributes.get("response")).get("name"))
.build();
}
public User toEntity() {
return User.builder()
.id(id)
.password(password)
.provider(provider)
.nickname(nickname)
.email(email)
.userRole(UserRole.MEMBER)
.build();
}
}
@Builder
public class CustomUserDetails implements UserDetails, OAuth2User {
private User user;
public CustomUserDetails(User user) {
this.user = user;
}
public CustomUserDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
@Override
public List<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getUserRole().name()));
return authorities;
}
// get Password 메서드
@Override
public String getPassword() {
return user.getPassword();
}
// get Username 메서드 (생성한 User은 id 사용)
@Override
public String getUsername() {
return user.getId();
}
// 계정이 만료 되었는지 (true: 만료X)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겼는지 (true: 잠기지 않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호가 만료되었는지 (true: 만료X)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정이 활성화(사용가능)인지 (true: 활성화)
@Override
public boolean isEnabled() {
return true;
}
// OAuth2User
private Map<String, Object> attributes;
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return user.getNickname();
}
}
다음 글에서는 프론트에서 인가 코드를 보낸다는 가정 하에 백에서 토큰과 사용자 정보를 요청하고 jwt 로그인 처리하는 과정을 다루겠습니다.
참고