인스타그램 소셜 로그인 구현하기

알파로그·2023년 5월 3일
1

Spring Boot

목록 보기
44/57
post-custom-banner

⚠️ 페이스북 소셜로그인 구현이 선행되어있어야 한다.

🔗 페이스북 소셜로그인 구현하기

🔗 Meta for Developers 공식 홈페이지


✏️ 앱 세팅

📍 제품 추가

  • 제품 추가 → Instagram Basic Display → 페이지 하단의 새 앱 만들기 → 표시 이름 입력
  • 클라이언트 OAuth 설정
    • 유효한 OAuth 리다이렉션 URL
https://localhost/login/oauth2/code/instagram

  • 콜백 승인 취소
https://localhost/instaMember/cancelCallbackUrlByInstagram

  • 데이터 삭제 요청
https://localhost/instaMember/needToDeleteByInstagram

  • 정보 입력 후 변경내용 저장

📍 사용자 토큰 생성기

  • 인스타그램은 개발단계에서는 실제 인스타그램 계정으로 로그인 할 수 없어서 테스트용 계정을 생성후 로그인해야한다.
  • 좌측 매뉴 → Instagram Basic Display → 기본표시 → 페이지 중간에 사용자 토큰 생성기 → add or remove instagram testers 선택
    • 하단의 instagra 테스터 추가 → 본인의 인스타그램 계정 입력
    • 생성된 인스타 계정 클릭 → 프로필 수정 → 좌측 매뉴의 앱 및 웹사이트 → 테스터 초대 → 수락
      • 삭제하고싶을 때도 이곳에서 하면된다.
  • 매타 개발자 페이지 → Instagram Basic Display → 기본표시
    • 앱 ID 와 시크릿 코드 복사

✏️ 프로젝트 환경설정

📍 application yml

security:
    oauth2:
      client:
        registration:

					instagram:
					  provider: instagram
						clientId: 'CLIENT ID'
            client-secret: 'secret'
					  scope: user_profile,user_media
					  client-name: Instagram
					  authorization-grant-type: authorization_code
					  redirect-uri: '리다이렉트 url'
					  client-authentication-method: client_secret_post

				provider:

					instagram:
            authorization-uri: https://api.instagram.com/oauth/authorize
            token-uri: https://api.instagram.com/oauth/access_token
            user-info-uri: https://graph.instagram.com/me?fields=id,username&access_token={access-token}
            user-name-attribute: username

✏️ 코드 구현

  • 인스타그램은 oauth2 를 지원하지 않기 때문에 지금까지 사용한 oauth2 라이브러리를 사용할 수 없다.
    • 따라서 기존 라이브러리 코드를 복사해 insatagram 용 로직을 별도로 추가해줘야한다.
  • 참고

📍 CustomOAuth2AccessTokenResponseClient

  • 기존 OAuth2AccessTokenResponseClient 객체를 수정하기 위해 복사해 새로운 객체를 생성 후 필요한 로직만 추가해줬다.
package com.ll.gramgram.base.security;

import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import static org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType.BEARER;

@Component
public class CustomOAuth2AccessTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {

   ...

    @Override
    public OAuth2AccessTokenResponse getTokenResponse(
            OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");

				// 인스타 토큰값을 구하기 위한 if 문 추가
        if (authorizationCodeGrantRequest.getClientRegistration().getClientName().equals("Instagram"))
            return getInstagramTokenResponse(authorizationCodeGrantRequest);

        RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
        ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);

        return response.getBody();
    }

	  ...

		// 토큰 받는 로직 추가
    public OAuth2AccessTokenResponse getInstagramTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
        RestTemplate restTemplate = new RestTemplate();
        RequestEntity<?> request = this.requestEntityConverter.convert(authorizationGrantRequest);
        ResponseEntity<InstagramAccessToken> response = restTemplate.exchange(request, InstagramAccessToken.class);

        InstagramAccessToken body = response.getBody();

        Map<String, Object> additionalParameters = new HashMap<>();
        additionalParameters.put("access_token", body.getAccessToken());
        additionalParameters.put("user_id", body.getUserId());

        return OAuth2AccessTokenResponse
                .withToken(body.getAccessToken())
                .tokenType(BEARER)
                .additionalParameters(additionalParameters)
                .build();
    }
}

📍 SecurityConfig

  • oauth2Login 에 tokenEndpoint 을 추가한다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

		// 새로 만든 객체를 DI 한다.
    private final CustomOAuth2AccessTokenResponseClient oAuth2AccessTokenResponseClient;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .formLogin(
                        formLogin -> formLogin
                                .loginPage("/usr/member/login")
                )
                .oauth2Login(
                        oauth2Login -> oauth2Login
                                .loginPage("/usr/member/login")
																// 추가된 로직 - 주입받은 객체를 변수로 준다.
                                .tokenEndpoint(t -> t
                                        .accessTokenResponseClient(oAuth2AccessTokenResponseClient)
                                )
                )
                .logout(
                        logout -> logout
                                .logoutUrl("/usr/member/logout")
                );

        return http.build();
    } 

📍 CustomOAuth2UserService

🔗 기존 CustomOAuth2UserService 코드

  • 기존 코드에서 if 문을 추가해 공급자가 INSTAGRAM 일 때 발생하는 로직을 추가해준다.
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    String providerTypeCode = userRequest.getClientRegistration().getRegistrationId().toUpperCase();

		// if 문 추가
    if (providerTypeCode.equals("INSTAGRAM")) {
        if (rq.isLogout()) {
            throw new OAuth2AuthenticationException("로그인 후 이용해주세요.");
        }

        String userInfoUri = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();
        userInfoUri = userInfoUri.replace("{access-token}", userRequest.getAccessToken().getTokenValue());
        RestTemplate restTemplate = new RestTemplate();
        HttpEntity<String> entity = new HttpEntity<>(new HttpHeaders());
        ResponseEntity<Map> response = restTemplate.exchange(userInfoUri, HttpMethod.GET, entity, Map.class);

        Map<String, String> userAttributes = response.getBody();

        String gender = rq.getSessionAttr("connectByApi__gender", "W");
        rq.removeSessionAttr("connectByApi__gender");

        instaMemberService.connect(rq.getMember(), gender, userAttributes.get("id"), userAttributes.get("username"), userRequest.getAccessToken().getTokenValue());

        Member member = rq.getMember();
        return new CustomOAuth2User(member.getUsername(), member.getPassword(), member.getGrantedAuthorities());
    }

    OAuth2User oAuth2User = super.loadUser(userRequest);

    String oauthId = switch (providerTypeCode) {
        case "NAVER" -> ((Map<String, String>) oAuth2User.getAttributes().get("response")).get("id");
        default -> oAuth2User.getName();
    };

    String username = providerTypeCode + "__%s".formatted(oauthId);

    Member member = memberService.whenSocialLogin(providerTypeCode, username).getData();

    return new CustomOAuth2User(member.getUsername(), member.getPassword(), member.getGrantedAuthorities());
}

📍 웹 링크 구현

profile
잘못된 내용 PR 환영
post-custom-banner

0개의 댓글