Spring Security OAuth Login(Naver, Kakao) 구현

young·2024년 6월 26일
0

Spring Security

목록 보기
5/7
post-thumbnail

OAuth 2.0 로그인

  • 사용자의 정보를 안전하게 인증하고 권한을 부여하기 위하여 사용

build.gradle 필수 의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.mysql:mysql-connector-j'
}

application.yml 설정

${ } 사용하여 환경변수로 지정!

spring:
  security:
    oauth2:
      client:
        registration:
          naver:
            client-name: naver
            client-id: ${NAVER_CLIENT_ID}
            client-secret: ${NAVER_CLIENT_SECRET}
            redirect-uri: ${NAVER_REDIRECT_URI}
            authorization-grant-type: authorization_code
            scope:
              - name
              - email
          kakao:
            client-id: ${KAKAO_CLIENT_ID}
            client-secret: ${KAKAO_CLIENT_SECRET}
            scope:
              - profile_nickname
              - account_email
            client-name: Kakao
            authorization-grant-type: authorization_code
            redirect-uri: ${KAKAO_REDIRECT_URI}
            client-authentication-method: client_secret_post
        provider:
          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
          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

PrincipalDetails

  • Spring Security의 UserDeatails와 OAuth2User 인터페이스 구현
    -> OAuth / 일반 사용자 모두 처리 가능
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private User user;
    private Map<String,Object> attributes;

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

    //Oauth 로그인을 할때 사용하는 생성자
    public PrincipalDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add((GrantedAuthority) () -> user.getRole().getAuthority());
        return authorities;
    }

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

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

    @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()
: 사용자 권한(Role) 반환
getAttributes()
: OAuth2 로그인 시 제공되는 사용자 정보 처리

PrincipalOauth2UserService

  • OAuth2.0 로그인이 이루어지는 곳
  • 클라이언트에서 사용자 정보를 받아와서 데이터베이스에서 사용자 정보를 조회 및 저장
@Service
@Slf4j
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        log.info("getClientRegistration : " + userRequest.getClientRegistration());
        log.info("getAccessToken : " + userRequest.getAccessToken().getTokenValue());

        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info("getAttributes : " + oAuth2User.getAttributes());

        OAuth2Response oAuth2UserInfo = null;

        if (userRequest.getClientRegistration().getRegistrationId().equals("kakao")) {
            System.out.println("카카오 로그인 요청");
            oAuth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
        } else if (userRequest.getClientRegistration().getRegistrationId().equals("naver")) {
            System.out.println("네이버 로그인 요청");
            oAuth2UserInfo = new NaverUserInfo((Map<String, Object>) oAuth2User.getAttributes().get("response"));
        } else {
            throw new OAuth2AuthenticationException("Unsupported provider");
        }


        String provider = oAuth2UserInfo.getProvider();
        String providerId = oAuth2UserInfo.getProviderId();
        String name = oAuth2UserInfo.getName();
        String email = oAuth2UserInfo.getEmail();
        Role role = Role.USER;

        User userEntity = userRepository.findByEmail(email);
       
        if(userEntity == null) {
            System.out.println("Oauth인이 최초입니다.");
            userEntity = User.builder()
                    .name(name)
                    .email(email)
                    .nickname(name)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();

            userRepository.save(userEntity);
        } else {
            System.out.println("로그인을 이미 한 적이 있습니다.");
        }

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

loadUser() 메서드

  • registrationId : 로그인 제공자 구분 / 각각의 사용자 정보 매핑

OAuth2Response

  • 서로 다른 플랫폼(Kakao, Naver..등)에서 공통적으로 사용자 정보를 얻기 위한 인터페이스
public interface OAuth2Response {
    String getProvider();
    String getProviderId();
    String getEmail();
    String getName();
}
  • Naver에서 반환된 사용자 정보 매핑
    단일 response 객체에서 데이터 추출
public class NaverUserInfo implements OAuth2Response {

    private Map<String, Object> attributes; //getAttributes()

    public NaverUserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }
    @Override
    public String getProviderId() {
        return (String)attributes.get("id");
    }

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

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

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

KakaoUserInfo

  • Kakao에서 반환된 사용자 정보 매핑
    kakao_account 와 properties에서 데이터 추출
public class KakaoUserInfo implements OAuth2Response{
    private Map<String, Object> attributes; //getAttributes()

    public KakaoUserInfo(Map<String, Object> attributes) {
        if (attributes == null) {
            throw new IllegalArgumentException("Attributes map cannot be null");
        }
        this.attributes = attributes;
    }
    @Override
    public String getProvider() {
        return "kakao";
    }

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

    @Override
    public String getEmail() {
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
        return String.valueOf(kakaoAccount.get("email"));
    }

    @Override
    public String getName() {
        Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
        if (properties != null) {
            return String.valueOf(properties.get("nickname"));
        }
        return null;
    }
}
profile
ฅʕ•̫͡•ʔฅ

0개의 댓글