스프링 시큐리티 OAUTH 2.0 카카오 로그인

큰모래·2023년 4월 12일
0

OAUTH란?

OAuth는 인증 및 권한 부여 프로토콜로서, 웹, 모바일 어플리케이션에서 다른 어플리케이션의 API를 안전하게 사용하기 위한 방법을 제공한다. OAuth는 기본적으로 사용자가 자신의 인증 정보를 다른 어플리케이션에 직접 제공하는 것이 아닌, 인증 서버를 통해 인증을 처리한다. 이 때, 사용자는 인증 서버에 로그인하여 인증을 완료하고, 인증 서버는 어플리케이션에게 인증된 사용자 정보를 제공한다.


OAuth 2.0

OAuth 1.0에서 복잡하게 얽혀있던 인증과 권한 처리에 대해 인증은 인증 서버에서, 권한은 자원 서버에서 처리하도록 설계됨. OAuth 2.0 에서는 Access Token의 유효 기간을 설정하여 보안성을 높였으며, Refresh Token을 제공하여 Access Token의 갱신을 용이하게 한다.


OAuth 2.0 주요 역할

  • Resource Owner
    • 자원에 대한 접근을 허가해 줄 수 있는 주체 (페이스북 사용자)
  • Resource Server
    • 자원을 호스팅하는 서버 (페이스북 서버)
  • Client
    • Resource Server에서 제공하는 자원을 사용하는 어플리케이션
      (3rd party 어플리케이션)
  • Authorization Server
    • 사용자의 동의를 받아서 권한을 부여하는 서버
    • Access Token 발급

OAuth2.0을 사용하면 로그인 및 회원가입과 관련된 보안, 비밀번호 찾기, 회원인증 등의 보안과 관련된 기능은 Resource Server(네이버, 구글 등)에 맡기고 다른 서비스적인 기능에 집중할 수 있다.


스프링 시큐리티에서 간단한 Flow

  1. OAuth 2.0 을 통한 Resource Server 로그인 시도 및 사용자 정보 받기
  2. OAuth2UserService에서 loadUser 메서드 실행
  3. loadUser 메서드에서는 받은 사용자 정보를 통해 기존 회원을 매핑해주거나 해당 정보로 새로운 회원을 만들어줄 수 있다.
  4. 최종적으로 사용자 정보를 통해 생성한 User 객체 반환
  5. 반환한 User 객체 + 권한 정보를 담은 Authentication 객체가 SecurityContext에 저장된다.
  6. 사용자의 로그인 상태를 유지하기 위해서는 SecurityContext가 필요하며, 이는 인증 정보를 스레드 로컬에 저장하는 방식으로 동작한다.

카카오 로그인 구현해보기

구현에 사용한 파일들

UserOAuth2UserService

  • OAuth를 통해 로그인을 시도한 사용자의 정보를 가공하여 User 객체를 만들어 반환한다.

KakaoOAuth2UserInfo

  • Kakao OAuth2를 통해 받은 json 형식의 데이터를 내가 원하는 형식으로 반환한다.

CustomOAuth2User

  • UserOAuth2UserService에서 반환할 User 객체를 커스터마이징

application.properties


#oauth2 client for kakao
spring.security.oauth2.client.registration.kakao.client-id= Kakao Developer로 부터 받은 고유 Id
spring.security.oauth2.client.registration.kakao.scope= account_email, gender
spring.security.oauth2.client.registration.kakao.client-name= Kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type= authorization_code
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method= POST

#oauth2 provider for kakao
spring.security.oauth2.client.provider.kakao.authorization-uri= https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri= https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri= https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute= id

KakaoOAuth2UserInfo

Kakao OAuth2로부터 받는 json 파일 형식

  • 프로필, 이름, 이메일, 나이, 생일, 성별, 휴대번호 등의 정보를 받을 수 있다.
  • 파일을 확인해보면 위의 주요 정보들은 kakao_account라는 map 형식의 필드안에 있는 구조를 파악할 수 있다.
{
    "id":123456789,
    "connected_at": "2023-04-12T00:00:28Z",
    "kakao_account": { 
      	//프로필
        "profile_nickname_needs_agreement": false,
        "profile_image_needs_agreement	": false,
        "profile": {
            "nickname": "홍길동",
            "thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
            "profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
            "is_default_image":false
        },
      
      	//이름
        "name_needs_agreement":false, 
        "name":"홍길동",
      
      	//이메일
        "email_needs_agreement":false, 
        "is_email_valid": true,   
        "is_email_verified": true,
        "email": "sample@sample.com",
      
      	//나이
        "age_range_needs_agreement":false,
        "age_range":"20~29",
      
      	//생일
        "birthyear_needs_agreement": false,
        "birthyear": "2002",
        "birthday_needs_agreement":false,
        "birthday":"1130",
        "birthday_type":"SOLAR",

      	//성별
        "gender_needs_agreement":false,
        "gender":"female",

      	//휴대번호
        "phone_number_needs_agreement": false,
        "phone_number": "+82 010-1234-5678",   
        
        "ci_needs_agreement": false,
        "ci": "${CI}",
        "ci_authenticated_at": "2019-03-11T11:25:22Z",
    },
    "properties":{
        "${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
        ...
    }
}

소스 코드

  • 생성자 매개변수로 attributes(위의 OAuth2 데이터 중 kakao_account 부분) 정보를 받아 필드 값에 주입한다.
  • getEmail을 통해 kakao_account의 이메일 정보를 반환받는다.
public class KakaoOAuth2UserInfo {

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

    private Map<String, Object> attributes;

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

}

CustomOAuth2User

  • UserOAuth2UserService에서 반환할 User 클래스를 커스터마이즈한 클래스
  • 딱히, 특별한 건 없다...
@Getter
public class CustomOAuth2User extends User implements OAuth2User {

    public CustomOAuth2User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

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

    @Override
    public String getName() {
        return null;
    }
}


UserOAuth2UserService

소스 코드

  • providerTypeCodeattributeid를 조합해 만든 username을 통해 기존에 존재하는 회원인지 찾고, 존재하면 기존 객체 반환, 존재하지 않다면 create한다.
@Service
@RequiredArgsConstructor
@Transactional
public class UserOAuth2UserService extends DefaultOAuth2UserService {

    private final UserService userService;
    private final UserRepository userRepository;


    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        KakaoOAuth2UserInfo kakaoOAuth2UserInfo = new KakaoOAuth2UserInfo((Map<String, Object>) oAuth2User.getAttribute("kakao_account"));
        String name = oAuth2User.getName();
        String providerTypeCode = userRequest.getClientRegistration().getRegistrationId().toUpperCase();
        String username = providerTypeCode + name;

        SiteUser siteUser;

        Optional<SiteUser> optionalSiteUser = userRepository.findByUsername(username);
        if (optionalSiteUser.isEmpty()) {
            siteUser = userService.create(username, "", kakaoOAuth2UserInfo.getEmail());
        }

        else {
            siteUser = optionalSiteUser.get();
        }

        String role = siteUser.getUserRole().getValue();
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role);
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(grantedAuthority);


        return new CustomOAuth2User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }
}

OAuth2UserRequest 객체 (userRequest)

userRequest에는 위의 3개의 파라미터 정보를 가지고 있다.

  • clientRegistration : client id, secret 등의 정보 저장
  • accessToken : Authorization Server로 부터 받은 accessToken 저장
  • additionalParameters : 추가 요청한 사용자의 정보가 저장
super.loadUser(userRequest)

이러한 정보를 담고있는 userRequestDefaultOAuth2UserServiceloadUser 메서드를 통해 OAuth2User 객체로 반환한다.

SimpleGrantedAuthority

GrantedAuthority 인터페이스를 구현한 클래스이며, 인증된 사용자의 권한을 나타내는 클래스이다.

생성자 매개변수로 String 타입의 role (ex : USER)을 넣어주어 생성하면 권한 정보를 나타내게 된다.

또한, 스프링 시큐리티에서는 인증된 사용자의 권한 정보를 Collection<? extends GrantedAuthority> 형태로 저장하기 때문에, 생성한 SimpleGrantedAuthority를 따로 만든 컬렉션에 저장해 인증된 사용자에게 하나 이상의 권한을 부여할 수 있다.


db에 들어온 데이터


후기

전체적인 흐름을 먼저 이해하려 노력했고 간단하게 구현을 해봤다.
하지만, 여기서 더 나아가 구체적인 동작 과정과 흐름에 대해 이해하려면 더 많은 시간과 노력을 들여 공부를 해야겠다는 생각이 자동으로 들어버렸다...

profile
큰모래

0개의 댓글