๐Ÿ’ทGoogle, Naver, Kakao ๋กœ๊ทธ์ธ API๋กœ ์ถ”๊ฐ€ํ•˜๊ธฐ

gdhiยท2023๋…„ 12์›” 21์ผ
post-thumbnail

๐Ÿ“–๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ API


๐Ÿ“ŒOAuth

๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ๋ถ€ํ„ฐ ์ง„ํ–‰ ํ›„

Google Cloud API ์ ‘์†

๐Ÿ‘‰ ์ฝ˜์†” ํด๋ฆญ

๐Ÿ‘‰ ํ”„๋กœ์ ํŠธ ์„ ํƒ > ์šฐ์ธก ์ƒ๋‹จ ์ƒˆ ํ”„๋กœ์ ํŠธ ํด๋ฆญ


๐Ÿ‘‰ ๋งŒ๋“ค๊ธฐ ํด๋ฆญ


๐Ÿ‘‰ OAuth ํด๋ผ์ด์–ธํŠธ ID ๋งŒ๋“ค๊ธฐ


๐Ÿ‘‰ ์•ฑ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ์—ฐ๋ฝ์ฒ˜ ์ž‘์„ฑ ํ›„ ์ €์žฅ ํ›„ ๊ณ„์† x 3 ๐Ÿ‘‰ ๋Œ€์‹œ ๋ณด๋“œ

๐Ÿ‘‰ ์—ฌ๊ธฐ๊นŒ์ง€ ๋™์˜ํ™”๋ฉด ๋งŒ๋“  ๊ฒƒ ์ด์ œ ์ง„์งœ๋กœ ๋กœ๊ทธ์ธ API๋ฅผ ์ ์šฉํ•ด๋ณด์ž



๐Ÿ“ŒOAuth ์ƒ์„ฑ

๐Ÿ‘‰ ๋งŒ๋“ค๊ธฐ

๐Ÿ‘‰ ์•ž์œผ๋กœ ID ์™€ PW๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค

๐Ÿ‘‰ ๊นŒ๋จน์œผ๋ฉด ์ด๊ฑฐ ๋ˆ„๋ฅด๋ฉด ๋‹ค์‹œ ๋‚˜์˜ด



๐Ÿ“Œapplication-oauth ํ”„๋กœํผํ‹ฐ ์ƒ์„ฑ

spring.security.oauth2.client.registration.google.client-id= # ํด๋ผ์ด์–ธํŠธ ID
spring.security.oauth2.client.registration.google.client-secret= # ํด๋ผ์ด์–ธํŠธ PW
spring.security.oauth2.client.registration.google.scope=profile, email

๐Ÿ‘‰ [๋„์–ด์“ฐ๊ธฐ ํ•˜์ง€ ๋ง ๊ฒƒ !] ๋ฐฉ๊ธˆ ๋ฐ›์€ ID, PW๋ฅผ ๋„ฃ์–ด์ฃผ๊ณ 


๐Ÿ“application.properties ์—์„œ import ํ•ด์ฃผ๊ธฐ

๋งจ ์•„๋ž˜์— spring.profiles.include=oauth
์ถ”๊ฐ€
๐Ÿ‘‰ ํŒŒ์ผ ์ด๋ฆ„์— oauth ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์•Œ์•„์„œ ์‹คํ–‰ํ•จ. ์šฐ๋ฆฐ application-oauth ๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฏ€๋กœ ์ด๊ฑธ๋กœ ์‹คํ–‰.



๐Ÿ“ŒUser ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ๋กœ๊ทธ์ธ๊ณผ ๋ณ„๊ฐœ๋กœ ํ…Œ์ด๋ธ” ์ƒ์„ฑ

package com.shop.entity;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name; // ๋‹‰๋„ค์ž„์ด ์ž๋™์œผ๋กœ ๋“ฑ๋ก ๋จ

    @Column(nullable = false)
    private String email; // ์ด๋ฉ”์ผ์ด ์ž๋™์œผ๋กœ ๋“ฑ๋ก ๋จ

    private String picture; // ํ”„์‚ฌ๊ฐ€ ์ž๋™์œผ๋กœ ๋“ฑ๋ก ๋จ
    private String role = "ROLE_USER";

    public User(String name, String email, String picture) {
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;

        return this;
    }
}



๐Ÿ“ŒSessionUser (Dto) ์ƒ์„ฑ

๊ธฐ๋ณธ ์ ์œผ๋กœ ๋‹ค๋ฅธ ๊ฒƒ๋“ค๊ณ  ์—ฐ๋™์ด ๋˜์•ผ ํ•˜๊ธฐ๋•Œ๋ฌธ์— Dto ๊ฐ€ ์žˆ์–ด์•ผ ํ•จ

package com.shop.dto;

import com.shop.entity.User;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;

@Getter
@Setter
@NoArgsConstructor
// ์ง๋ ฌํ™” : ์ž๋ฐ” ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฐ”์ดํŠธ ์ŠคํŠธ๋ฆผ ํ˜•ํƒœ๋กœ ์—ฐ์† ์ ์ธ ํฌ๋ฉง ๋ณ€ํ™˜ ๊ธฐ์ˆ 
// ๐Ÿ‘‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ค„ ์„ธ์›Œ์„œ ์ญ‰ ๋„ฃ์Œ (๋งˆํฌ ์ธํ„ฐํŽ˜์ด์Šค) / ์ž๋ฐ” Object Data ๐Ÿ‘‰ Byte Stream
// ์—ญ์ง๋ ฌํ™” : Byte Stream ๐Ÿ‘‰ ์ž๋ฐ” Object Data
public class SessionUser implements Serializable {

    private String name;

    private String email;

    private String picture;

    public SessionUser(User user) {
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

}



๐Ÿ“ŒUserRepository ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.repository;

import com.shop.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    
    // ์ด๋ฉ”์ผ ์ฒดํฌ
    Optional<User> findByEmail(String email);
    
}



๐Ÿ“ŒOAuthAttributes ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.config;

import com.shop.entity.User;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Map;

@Getter
@Setter
@ToString
public class OAuthAttributes {

    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey,
                           String name, String email, String picture) {
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    public OAuthAttributes(){}

    public static OAuthAttributes ofGoogle(String userNameAttributeName,
                                            Map<String, Object> attributes){

        return new OAuthAttributes(attributes,userNameAttributeName,
                (String) attributes.get("name"),
                (String) attributes.get("email"),
                (String) attributes.get("picture"));

    }

    public User toEntity(){

        return new User(name, email, picture);

    }
}



๐Ÿ“Œpom.xml ์˜์กด์„ฑ ์ถ”๊ฐ€

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>



๐Ÿ“ŒCustomOAuth2UserService ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.config;

import com.shop.dto.SessionUser;
import com.shop.entity.User;
import com.shop.repository.UserRepository;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collections;

@Service
public class CustomOAuth2UserService implements
        OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest)
        throws OAuth2AuthenticationException{

        OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);

        // String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();

        String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.ofGoogle(userNameAttributeName,
                 oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);

        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                 attributes.getAttributes(), attributes.getNameAttributeKey());

    }

    private User saveOrUpdate(OAuthAttributes attributes){

        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity
                        .update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());

        return userRepository.save(user);

    }

}



๐Ÿ“ŒSecurityConfig ํด๋ž˜์Šค ์ˆ˜์ •

...
    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;
...

 // ๋กœ๊ทธ์•„์›ƒ
                .logout(logout -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // ๋กœ๊ทธ์•„์›ƒ ํŽ˜์ด์ง€
                        .logoutSuccessUrl("/")) // ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต ํŽ˜์ด์ง€ ๐Ÿ‘‰ "/"
                // ๊ตฌ๊ธ€ ์ธ์ฆ
                .oauth2Login(oauth2Login -> oauth2Login
                        .defaultSuccessUrl("/")
                        .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
                        .userService(customOAuth2UserService))
                );

        // ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์—๋Ÿฌ ํ•ธ๋“ค๋ง
        ...

๐Ÿ‘‰ ์ด ๊ฒƒ๊นŒ์ง€ ํ•˜๊ณ  ์‹คํ–‰ํ•ด๋ณธ๋‹ค. ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค



๐Ÿ“ŒmemberLoginForm.html ์ˆ˜์ •

...
    <a href="/oauth2/authorization/google" >๊ตฌ๊ธ€ ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธ</a>
</div>

</html>



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์ฐฝ์ด ๋œจ๊ฒŒ ๋œ๋‹ค. ๋กœ๊ทธ์ธ์„ ํ•ด๋ณด๋ฉด ROLE_USER ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ฃผ๋ฌธ๋งŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ‘‰ DB์—๋Š” ๋‹ด์„ ์ˆ˜ ์žˆ์ง€๋งŒ USER๋กœ View์—์„œ ๋ณด์—ฌ์ง€๋Š” ๋กœ์ง์€ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ๋กœ ๋ณด์ด์ง€๋Š” ์•Š๋Š”๋‹ค.

๐Ÿ‘‰ ๐Ÿ”ฅTodo









๐Ÿ“–๋„ค์ด๋ฒ„, ์นด์นด์˜ค ์ถ”๊ฐ€


๐Ÿ“Œ๋„ค์ด๋ฒ„

๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ API
๐Ÿ‘‰ ๋กœ๊ทธ์ธ ์ง„ํ–‰


๐Ÿ‘‰ Client ID/PW๋ฅผ ์ด์šฉํ•ด ๊ตฌ๊ธ€๊ณผ ๋˜‘๊ฐ™์ด ํ•ด์ฃผ๋ฉด ๋œ๋‹ค



๐Ÿ“Œ์นด์นด์˜ค

์นด์นด์˜ค ๋กœ๊ทธ์ธ API
๐Ÿ‘‰ ๋กœ๊ทธ์ธ ์ง„ํ–‰



๐Ÿ‘‰ ์‹ ์ฒญํ•˜๊ณ  ๋น„์ฆˆ๋‹ˆ์Šค ์•ฑ ๊ฐ€์ž… ํ•˜๋ฉด ๋œ๋‹ค. (์„ค๋ช…๋Œ€๋กœ ํ•˜๋ฉด ๋จ)

๐Ÿ‘‰ ์„ค์ • ๊ฐ€๋Šฅํ•ด ์ง

๐Ÿ‘‰ Client ID

๐Ÿ‘‰ Clinet PW



๐Ÿ“Œ์ฝ”๋“œ์— ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ตฌ๊ธ€๊ณผ ๋‹ค๋ฅด๊ฒŒ ์กฐ๊ธˆ ๊นŒ๋‹ค๋กญ๋‹ค
configํŒจํ‚ค์ง€ ๊ด€๋ จ ํด๋ž˜์Šค, memberLonginForm.html, application-oauth.properties ์ˆ˜์ •์„ ํ•ด์ค€๋‹ค


๐Ÿ“CustomOAuth2UserService ํด๋ž˜์Šค ์ˆ˜์ •

package com.shop.config;

import com.shop.dto.SessionUser;
import com.shop.entity.User;
import com.shop.repository.UserRepository;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException{
        OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);

        String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();

        String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName
                , oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);
        httpSession.setAttribute("user",new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))
                , attributes.getAttributes()
                , attributes.getNameAttributeKey()
        );
    }

    private User saveOrUpdate(OAuthAttributes attributes){
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());
        return userRepository.save(user);
    }
}



๐Ÿ“OAuthAttributes ํด๋ž˜์Šค ์ˆ˜์ •

package com.shop.config;

import com.shop.entity.User;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Map;

@Getter
@Setter
@ToString
public class OAuthAttributes {
    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name,
                           String email, String picture) {
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    public OAuthAttributes() {
    }

    // ํ•ด๋‹น ๋กœ๊ทธ์ธ์ธ ์„œ๋น„์Šค๊ฐ€ kakao์ธ์ง€ google์ธ์ง€ ๊ตฌ๋ถ„ํ•˜์—ฌ, ์•Œ๋งž๊ฒŒ ๋งคํ•‘์„ ํ•ด์ฃผ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
    // ์—ฌ๊ธฐ์„œ registrationId๋Š” OAuth2 ๋กœ๊ทธ์ธ์„ ์ฒ˜๋ฆฌํ•œ ์„œ๋น„์Šค ๋ช…("google","kakao","naver"..)์ด ๋˜๊ณ ,
    // userNameAttributeName์€ ํ•ด๋‹น ์„œ๋น„์Šค์˜ map์˜ ํ‚ค๊ฐ’์ด ๋˜๋Š” ๊ฐ’์ด๋ฉ๋‹ˆ๋‹ค. {google="sub", kakao="id", naver="response"}
    public static OAuthAttributes of(String registrationId, String userNameAttributeName,
                                     Map<String, Object> attributes) {
        if (registrationId.equals("kakao")) {
            return ofKakao(userNameAttributeName, attributes);
        } else if (registrationId.equals("naver")) {
            return ofNaver(userNameAttributeName,attributes);
        }
        return ofGoogle(userNameAttributeName, attributes);
    }

    private static OAuthAttributes ofKakao(String userNameAttributeName,
                                           Map<String, Object> attributes) {
        Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account");  // ์นด์นด์˜ค๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ์—์„œ ๊ณ„์ • ์ •๋ณด๊ฐ€ ๋‹ด๊ธด kakao_account ๊ฐ’์„ ๊บผ๋‚ธ๋‹ค.
        Map<String, Object> profile = (Map<String, Object>) kakao_account.get("profile");   // ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ profile(nickname, image_url.. ๋“ฑ) ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ’์„ ๊บผ๋‚ธ๋‹ค.

        return new OAuthAttributes(attributes,
                userNameAttributeName,
                (String) profile.get("nickname"),
                (String) kakao_account.get("email"),
                (String) profile.get("profile_image_url"));
    }

    private static OAuthAttributes ofNaver(String userNameAttributeName,
                                           Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");    // ๋„ค์ด๋ฒ„์—์„œ ๋ฐ›์€ ๋ฐ์ดํ„ฐ์—์„œ ํ”„๋กœํ•„ ์ •๋ณด๋‹ค ๋‹ด๊ธด response ๊ฐ’์„ ๊บผ๋‚ธ๋‹ค.

        return new OAuthAttributes(attributes,
                userNameAttributeName,
                (String) response.get("name"),
                (String) response.get("email"),
                (String) response.get("profile_image"));
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName,
                                            Map<String, Object> attributes) {

        return new OAuthAttributes(attributes,
                userNameAttributeName,
                (String) attributes.get("name"),
                (String) attributes.get("email"),
                (String) attributes.get("picture"));
    }

    public User toEntity() {
        return new User(name, email, picture);
    }
}



๐Ÿ“application-oauth.properties ์ˆ˜์ •

#Google
spring.security.oauth2.client.registration.google.client-id=
spring.security.oauth2.client.registration.google.client-secret=
spring.security.oauth2.client.registration.google.scope=profile,email

#Naver
# registration
spring.security.oauth2.client.registration.naver.client-id=
spring.security.oauth2.client.registration.naver.client-secret=
spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver

# provider
spring.security.oauth2.client.provider.naver.authorization_uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token_uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response

# KAKAO
spring.security.oauth2.client.registration.kakao.client-id=
spring.security.oauth2.client.registration.kakao.client-secret=
spring.security.oauth2.client.registration.kakao.scope=profile_nickname, account_email, profile_image
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={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post

# provider
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

๐Ÿ‘‰ ๋ณธ์ธ Client ID, PW ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค



๐Ÿ“memberLonginForm.html ์ˆ˜์ •

...
    <a href="/oauth2/authorization/google">๊ตฌ๊ธ€ ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธ</a>
    <a href="/oauth2/authorization/naver">๋„ค์ด๋ฒ„ ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธ</a>
    <a href="/oauth2/authorization/kakao">์นด์นด์˜ค ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธ</a>
</div>

</html>



๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ ์„ฑ๊ณต

๐Ÿ‘‰ ์‚ฌ์—…์ž ๋“ฑ๋ก์„ ์•ˆํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์ „ํ•˜๊ฒŒ 2์ฐจ ์ธ์ฆ๊นŒ์ง€ ๋ฐ›๋„๋ก ๋˜์žˆ์Œ

๐Ÿ‘‰ ์„ฑ๊ณต

0๊ฐœ์˜ ๋Œ“๊ธ€