OAuth 2とSpring Security


couchy符号化では、firebaseに基づいてGoogle Outhを開発することをお勧めしますが、一般的にはfirebaseで使用しない場合が多いので、firebaseなしでOAuthを使用する方法について理解しました.

  • OAuth 2.0
    :認証プロトコル
    開発中のサービスに代わって、すでに有名な大型サービス(グーグル、フェイスブックなど)を通じて認証を行い、ユーザー情報を取得する方式だ.

  • 基本用語
  • Resource Owner
    :サービスの利用者を指す
    これは,一般ユーザがサーバ間の通信であればサーバであってもよいことを意味する.
  • Client
    :ユーザーが使用するサービス
    サービスを開発している場合は、そのサービスがクライアントです.
  • Resource Server
    :OAuthを介してリソースを提供するサーバ
  • Authorization Server
    :OAuth使用時に認証・認可を担当するサーバ
    Authベースのソーシャル・ログインの有名な大手サービス(Googleなど)が所有するリソース・サーバとAutheroizationサーバを提供することでユーザ情報を認証し、ユーザ情報を取得し、開発中のサービスのデータベースに格納して使用するのがソーシャル・ログイン方式である.

  • 潮流
    たとえば、DevelopgとGithubを使用してワークフロー全体にログインします.

    ソース

  • Grant Type
    通常、Web上で使用されるOAuthは、認証コードを使用してシェーディングされる.これはトークンを受け取って検証する方式が一般的なWeb開発で最も多く使われていることを意味する.
    リファレンス

  • Authorization Code Grant
    :認証サーバからフロントエンドから認証コードを取得した場合は、この認証コードをバックエンドに渡し、バックエンドで認証コードを使用して認証サーバにaccessTokenを要求します.アクセスtokenとrefresh tokenを受信し、resource serverにユーザ情報を受信するように処理を要求すればよい.


  • Spring Security + OAuth2

  • build.gradle
    dependencies {
      implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    }

  • OAuthでのサービス登録
    :これは、ソーシャル・ログインのサービスによって異なります.適切な開発者サイトを参照してください.
  • Github : https://github.com/settings/developers
  • Google : https://console.cloud.google.com/home/dashboard
  • Naver : https://developers.naver.com/apps/#/register
  • Kakao : https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite

  • application.yml設定
    :oauthに関連するclient-id、client-secretなどはアプリケーションです.直接ymlにアップロードするのではなく、個別のymlを作成して処理します.
    を選択します.次の構文を追加して、ymlに個別のymlを含めるようにします.
    spring:
      profiles:
        include: oauth
    およびoauth情報のアプリケーション-oauth.ymlの追加
    他のOAuthサービスも以下のように作成されています.
    ただし、Spring SecurityではGoogleやGithubなどのグローバルサービスを認識できないため、さらに設定する必要があります.
    spring:
      security:
        oauth2:
          client:
            registration:
              github:
                client-id: 6c34d9a6903231c5a301
                client-secret: 비밀키
                scope: name,email,avatar_url
              # naver 와 같은 국내 한정 서비스들은 아래처럼 별도의 추가 설정들이 필요함.
              naver: 
                client-id: sCfhQHgPVQFFf8RTGjVe
                client-secret: 비밀키
                redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
                authorization_grant_type: authorization_code
                scope: name,email,profile_image
                client-name: Naver
            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

  • SecurityConfig設定
    :Spring Securityとして作成され、以下のように使用されます.
    @EnableWebSecurity // spring security 설정을 활성화시켜주는 어노테이션
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final OAuthService oAuthService;
    
        public SecurityConfig(OAuthService oAuthService) {
            this.oAuthService = oAuthService;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.oauth2Login() // OAuth2 로그인 설정 시작점
            .userInfoEndpoint() // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당
            .userService(oAuthService); // OAuth2 로그인 성공 시, 후작업을 진행할 UserService 인터페이스 구현체 등록
        }
    }

  • Entityの設定
    :OAuth 2リソースサーバから受信するデータに基づいてエンティティを作成します.
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Member {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String oauthId;
    
        private String name;
    
        private String email;
    
        private String imageUrl;
    
        @Enumerated(EnumType.STRING)
        private Role role;
        
        // 생성자, 기타 메소드들 생략
    }
    
    // OAuthService 로직 내에서 사용될 객체
    public class UserProfile {
        private final String oauthId;
        private final String name;
        private final String email;
        private final String imageUrl;
        
        // 생성자 등 생략..
    }
    
    public enum Role {
        GUEST("ROLE_GUEST"),
        USER("ROLE_USER");
    
        private final String key;
    
        Role(String key) {
            this.key = key;
        }
    
        public String getKey() {
             return key;
        }
    }

  • OAuthService
    :OAuthサーバから取得したユーザ情報を取得し、現在開発されているサービスのDBにユーザがいない場合は、保存時に既存メンバーの論理記述を返す.
    @Service
    @RequiredArgsConstructor
    public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
        private final MemberRepository memberRepository;
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            OAuth2UserService delegate = new DefaultOAuth2UserService();
            OAuth2User oAuth2User = delegate.loadUser(userRequest); // OAuth 서비스(github, google, naver)에서 가져온 유저 정보를 담고있음
    
            String registrationId = userRequest.getClientRegistration()
                                           .getRegistrationId(); // OAuth 서비스 이름(ex. github, naver, google)
            String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
                                                  .getUserInfoEndpoint().getUserNameAttributeName(); // OAuth 로그인 시 키(pk)가 되는 값 
            Map<String, Object> attributes = oAuth2User.getAttributes(); // OAuth 서비스의 유저 정보들
    
            UserProfile userProfile = OAuthAttributes.extract(registrationId, attributes); // registrationId에 따라 유저 정보를 통해 공통된 UserProfile 객체로 만들어 줌 
    
            Member member = saveOrUpdate(userProfile); // DB에 저장
    
            return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(member.getRoleKey())),
                attributes,
                userNameAttributeName);
        }
    
        private Member saveOrUpdate(UserProfile userProfile) {
            Member member = memberRepository.findByOauthId(userProfile.getOauthId())
                                        .map(m -> m.update(userProfile.getName(), userProfile.getEmail(), userProfile.getImageUrl())) // OAuth 서비스 사이트에서 유저 정보 변경이 있을 수 있기 때문에 우리 DB에도 update 
                                        .orElse(userProfile.toMember());
            return memberRepository.save(member);
        }
    }

  • OAuthAttributes enum
    :各OAuthサービスのenumマッチング
    public enum OAuthAttributes {
        GITHUB("github", (attributes) -> {
            return new UserProfile(
                    String.valueOf(attributes.get("id")),
                    (String) attributes.get("name"),
                    (String) attributes.get("email"),
                    (String) attributes.get("avatar_url")
            );
        }),
        GOOGLE("google", (attributes) -> {
            return new UserProfile(
                    String.valueOf(attributes.get("sub")),
                    (String) attributes.get("name"),
                    (String) attributes.get("email"),
                    (String) attributes.get("picture")
            );
        }),
        NAVER("naver", (attributes) -> {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
            return new UserProfile(
                    (String) response.get("id"),
                    (String) response.get("name"),
                    (String) response.get("email"),
                    (String) response.get("profile_image")
            );
        });
    
        private final String registrationId;
        private final Function<Map<String, Object>, UserProfile> of;
    
        OAuthAttributes(String registrationId, Function<Map<String, Object>, UserProfile> of) {
            this.registrationId = registrationId;
            this.of = of;
        }
    
        public static UserProfile extract(String registrationId, Map<String, Object> attributes) {
            return Arrays.stream(values())
                     .filter(provider -> registrationId.equals(provider.registrationId))
                     .findFirst()
                     .orElseThrow(IllegalArgumentException::new)
                     .of.apply(attributes);
        } 
    }

  • Firebase
    :Firebaseを使用して処理する場合、Firebaseを使用しないロジックよりもロジックの方が簡単かもしれませんが、Spring Security内でOuthログインの内部構造をどのように処理するかを詳しく知るのは難しいです.
    したがって,困難で複雑であっても,Outh登録処理ではなく直接Firebaseで実現することを試みるべきである.
    リファレンス