ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링부트 시큐리티 03 - OAuth (구글/페이스북/네이버 로그인)
    스프링/시큐리티 2021. 9. 8. 13:47

    유투버 '데어프로그래밍'님 강의 참조

     

    01 - 구글 API 

     

    • 아래 주소를 통해 API 생성을 하자

    https://console.cloud.google.com/projectselector2/apis/dashboard?pli=1&supportedpurview=project&project=&folder=&organizationId=

     

    Google Cloud Platform

    하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

    accounts.google.com

    프로젝트 생성 → 오스 외부 사용 동의(간단히 프로필 작성) →  사용자 인증 정보 → 사용자 인증 정보 만들기 → 오스 클라이언트 ID 만들기 → 승인된 리다이렉션 URI 주소 세팅(http://localhost:8080/login/oauth2/code/google)

     

    *카카오 로그인처럼 직접 모든걸 구현하면 주소 변경이 가능하나 어스 라이브러리를 쓸 경우 승인 URL 주소는 고정
    *또한 컨트롤러 세팅도 필요 없음 (라이브러리가 처리함)

     

     

     

    02 - 라이브러리 설치 

    • 프로젝트 초기 세팅시에 OAuth를 체크하지 않았기 때문에 직접 세팅해야한다.
      (스프링 프로젝트를 세로 만들어서 OAuth2 Client를 클릭하여 코드를 가져오면 편하다(메이븐 업데이트 필수)
    *pom.xml
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    • yml 추가
      security:
        oauth2:
          client:
            registration:
             google:
               client-id: 684759709364-1enpl7paf0kkbkad6svt54cu46acdih1.apps.googleusercontent.com
               client-secret: i38cGfvb9NnD9BU-7gd5zlRL
               scope:
                - email
                - profile

     

     

    03 - 구글 로그인을 위한 설정

    • 버튼 추가 (loginForm.html)
    <a href="/oauth2/authorization/google">Google Login</a>

    → 주소 무조건 고정!

     

    • 구글 맵핑 (SecurityConfig에 어스 관련 추가)
    .and()
    .oauth2Login()
    .loginPage("/loginForm");

    굿!

     

     

     

    • 계정을 선택하고 나면 후처리가되도록 해야 한다. (아직까지는 403에러를 띄움) 
    • 스프링부트에서 카카오로그인에서 배웠던 다른 플랫폼 로그인 프로세스를 기억하자
      인증 -> 엑세스토큰(권한) -> 사용자 정보 받을 수 있음 -> 그 정보를 토대로 회원가입 + 로그인
      어스 라이브러리가 지원하는 구글/페이스북은 토큰+정보를 한방에 받을 수 있음
    • SecurityConfig 후처리 코드 삽입
    .userInfoEndpoint()
    .userService(principalOauth2UserService);
    • OAuth용 userService 만들기
    @Service
    public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    	@Override
    	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    		System.out.println("userRequest: "+userRequest);
    		return super.loadUser(userRequest);
    	}
    }

    굿!

    • 계정을 클릭하고 넘어가면 후처리가 잘 되서 나온다(userRequest가 토큰,사용자정보 등 이미 다 들고 있다)

     

     

     

    04 - Authentication 조금 더 보기

    • 'userRequest'가 하는 일은 구글 로그인이 완료되면 OAuth-clinet 라이브러리를 통해 code를 리턴하고 AccessToken까지 요청을 받은 내용을 담고 있다 
    • 이제 이 userRequest에 담긴 정보로 회원 정보를 받아야 하는데 이때 사용되는 함수가 'loardUser' → 이 함수가 구글로부터 받게 됨
    • Authentication에 접근을 할때에는 직접 'Authentication'을 선언하거나 '@Authentication'을 하여 이 어노테이션이 가지고 있는 기능을 다운캐스팅하여 사용 가능 하다. (다운캐스팅하여 PrincipalDetails/UserDetails 를 쓸 수 있다)
    • 일반적으로 로그인을 하면 UserDetails 타입이 Authentication에 들어가게되고 구글로 로그인을하면 OAuth라이브러리가 들고있는 OAuth2User 타입이 들어가게 된다. 

     

    정리를 해보면...

    1. 시큐리티 세션이 존재하고 그 안에는 들어갈 수 있는 타입은 'Authentication' 객체 뿐 → 시큐리티를 통해 로그인이 정상 진행이 되면 'Authentication'이 생김
    2. Authentication에 담긴 내용을 컨트롤러를 통해서 꺼내서 사용 가능
    3. Authentication 객체 안에 들어갈 수 있는 2개의 타입은 'UserDetails'/'OAuth2User'
    4. 일반적인 로그인을 하게 되면 auth 패키지 이하 프로세스를 타고 UserDetails이 생성되고, 구글시에는 oauth패키지 프로세스를 타여 OAuth2User가 생성이 되고 로그인 방식에 따라 둘 중 하나가 Authentication에 쏙 들어감
    5. 정상 로그인 이후 이제 이 세션들을 꺼내서 쓸 수 있어야 하는데 일반 로그인(UserDetails) 과 타플랫폼 로그인(OAuth2User)이 지향하는 객체가 다르기때문에 코드를 하나로 하여 꺼내 쓸 수가 없는 불편함이 있다. 

     

    • 즉 하나로 묶으면 된다. PrincipalDetails 는 이미 UserDetails를 상속 받았기 때문에 OAuth2User도 PrincipalDetails 로 묶으면 가장 간단 하다
    • PrincipalDetails로 다 묶이게되고 PrincipalDetails/OAuth2User 둘다 user객체를 쓸 수 있게 되는 것
    @Data
    public class PrincipalDetails implements UserDetails, OAuth2User {
    
    	private Map<String, Object> attributes;
    	...
    	//OAuth 로그인
        	public PrincipalDetails(User user, Map<String, Object> attributes) {
    		this.user = user;
    		this.attributes = attributes;
    	}
    	...
        
        
        	@Override
    	public Map<String, Object> getAttributes() {
    		return attributes;
    	}
    
    	@Override
    	public String getName() {
    		return null;
    	}
    }

     

    • PrincipalOauth2UserService에서 회원가입 후 처리
    @Service
    public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    	
    	@Autowired
    	private BCryptPasswordEncoder bCryptPasswordEncoder;
    	
    	@Autowired
    	private UserRepository userRepository;
    	
    	@Override
    	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    		OAuth2User oauth2User = super.loadUser(userRequest);
    		System.out.println("getAttributes: " +oauth2User.getAttributes());
    		String provider = userRequest.getClientRegistration().getRegistrationId();
    		String providerId = oauth2User.getAttribute("sub");
    		String username = provider+"_"+providerId;
    		String password = bCryptPasswordEncoder.encode("7777");
    		String email = oauth2User.getAttribute("email");
    		String role = "ROLE_USER";
    		
    		User userEntity = userRepository.findByUsername(username);
    		if (userEntity == null) {
    			System.out.println("구글 로그인이 진행 됩니다!");
    			userEntity = User.builder()
    					.username(username)
    					.password(password)
    					.email(email)
    					.role(role)
    					.provider(provider)
    					.providerId(providerId)
    					.build();
    			userRepository.save(userEntity);
    		} else {
    			System.out.println("이미 회원가입이 되어있으십니다.");
    		}
    		return new PrincipalDetails(userEntity, oauth2User.getAttributes());
    	}
    }
    • IndexController에서 정보를 받도록 세팅
    	@GetMapping("/user")
    	public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
    		System.out.println("principalDetails: " + principalDetails.getUser());
    		return "user";
    	}
    • 현재까지 세팅으로 인해 일반/OAuth 로그인 둘다 PrincipalDetails가 가능하게 된다!
    • 즉 아래 두개의 서비스는 결국 PrincipalDetails를 리턴하기 위함! 또한 이 서비스들의 함수가 종료됨과 동시에 @AuthenticationPrincipal 어노테이션이 생성 되는 것 

     

     

     

     

     

    ※ 테스트

    일반적인 로그인

    구글 로그인

    굿!

     

     

     

     

     

     

    05 - 페이스북 로그인

    • 구글과 같이 아래 사이트로 들어가 API를 설정 해 준다.

    https://developers.facebook.com/?locale=ko_KR 

     

    Facebook for Developers

    iOS 14에 대비한 파트너 준비 사항: Facebook 광고에 영향을 미칠 Apple iOS 14 요구 사항에 대해 자세히 알아보세요. FACEBOOK으로 빌드하기 Facebook의 추천 플랫폼으로 고객과 소통하고 효율을 높여보세요

    developers.facebook.com

    내 앱 → 앱만들기 → 없음 → 세팅 후 Facebook 로그인 → 웹 → 사이트 URL → 설정(기본설정) → 앱ID/시크릿코드 확인 → Facebook로그인 메뉴에 설정 →  URI 리디렉션 확인

     

    • yml 추가
    facebook:
    client-id: 248961027152275
    client-secret: 3d52d4c7f3fbd0a3dae14136a7b60308
    scope:
    - email
    - public_profile

     

    • Facebook 로그인 버튼 추가 (loginForm.html)
    <a href="/oauth2/authorization/facebook">Facebook Login</a>

    • 진행은 잘되나 구글처럼 id를 넘겨주는 부분이 없다. 아래처럼 딱 3가지 정보만 받을 수 있다. 그래서 providerId부분이 null값이 된다. 이를 해결 하자 

     

    • 값을 따로 받아주는 인터페이스 만들기

    public interface OAuth2UserInfo {
    	String getProviderId();
    	String getProvider();
    	String getEmail();
    	String getName();
    }

     

     

    • 이 인터페이스를 활용하여 각각의 플랫폼이 요구하는 다른 데이터들을 받을 클래스

    @Data
    public class FacebookUserInfo implements OAuth2UserInfo {
    	
    	private Map<String, Object> attributes;
    	
    	public FacebookUserInfo(Map<String, Object> attributes) {
    		this.attributes = attributes; 
    	}
    
    	@Override
    	public String getProviderId() {
    		return (String)attributes.get("id");
    	}
    
    	@Override
    	public String getProvider() {
    		return "facebook";
    	}
    
    	@Override
    	public String getEmail() {
    		return (String)attributes.get("email");
    	}
    
    	@Override
    	public String getName() {
    		return (String)attributes.get("name");
    	}
    }

    *Google용도 내용이 같다

    • PrincipalOauth2UserService에서 데이터를 각 플래폼에 따라 다르게 적용하도록 코드 수정
    @Service
    public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    	..
        	@Override
    	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    		OAuth2User oauth2User = super.loadUser(userRequest);
    		System.out.println("getAttributes: " +oauth2User.getAttributes());
    		
    		OAuth2UserInfo oAuth2UserInfo = null;
    		if(userRequest.getClientRegistration().getRegistrationId().equals("google")) {
    			System.out.println("Google Login Request");
    			oAuth2UserInfo = new GoogleUserInfo(oauth2User.getAttributes());
    		} else if (userRequest.getClientRegistration().getRegistrationId().equals("facebook")) {
    			oAuth2UserInfo = new FacebookUserInfo(oauth2User.getAttributes());
    			System.out.println("FaceBook Login Request");
    		} else {
    			System.out.println("Only Google/Facebook");
    		}
    		
    		String provider = oAuth2UserInfo.getProvider();
    		String providerId = oAuth2UserInfo.getProviderId();
    		String username = provider+"_"+providerId;
    		String password = bCryptPasswordEncoder.encode("7777");
    		String email = oAuth2UserInfo.getEmail();
    		String role = "ROLE_USER";

    굿!

    → 이제 provider로 타플랫폼인지 나의 웹사이트에서 한건지 구별하면 된다. 

     

     

     

    ※ 짚고 넘어가야 할 팁

    • 스프링에서는 구글/페이스북/트위터 정도를 provider로 제공을 하지만 카카오/네이버 같은 세계적으로 사용하지않는것은 지원하지 않는다. 그 이유는 각 플랫폼마다 OAuth를 사용시에 getAttributes로 뿌려주는 정보가 제각기이기 때문이다 (예: 구글-sub, 페이스북-id 등)

     

     

     

     

     

    06 - 네이버 로그인 진행

    • 네이버는 기본적으로 provider기반이 아니다. 직접 만들어 줘야한다.
    • 구글과 같이 아래 사이트로 들어가 API를 설정 해 준다.

    https://developers.naver.com/main/

     

    NAVER Developers

    네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

    developers.naver.com

    로그인 → 어플리케이션 → 어플리케이션 등록 → 네아로 → 회원이름/이메일주소 필수 → 환경 추가(PC웹) → 서비스 URL → Callback URL(redirect-uri)

    • yml 세팅
      security:
        oauth2:
          client:
            registration:
            ..
             naver:
               client-id: 8lU5yLj2dzUy3tg7kQBh
               client-secret: ynxfgF8LCm
               scope:
               - name
               - email
               authorization-grant-type: authorization_code
               redirect-uri: http://localhost:8080/login/oauth2/code/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.naer.com/v1/nid/me
               user-name-attribute: response #회원정보를 JSON으로 받는데 response라는 키값으로 네이버가 리턴해줌

     

    • loginForm.html에 버튼 추가
    <a href="/oauth2/authorization/naver">Naver Login</a>

    → 네이버 로그인 동의는 뜨지만 동의 이후는 널포인트 에러가 뜬다. 그 이유는 response키값안에 getAttributes의 내용들(아래)이 들어가기 때문에 response를 명확하게 코딩 해줘야 한다.

    getAttributes: {resultcode=00, message=success, 
    response={id=qKvqNGSJAzSQNhxPfbaNR_iRRW2t-NwxbvK6r6tC6zQ, 
    email=sehwan_8@naver.com, name=차세환}}

     

    • PrincipalOauth2UserService에 똑같이 이 정보를 받을 수 있도록 코드 추가
    } else if (userRequest.getClientRegistration().getRegistrationId().equals("naver")) {
    oAuth2UserInfo = new NaverUserInfo((Map)oauth2User.getAttributes().get("response"));
    System.out.println("Naver Login Request");
    • 똑같이 NaverUserInfo 생성

    @Data
    public class NaverUserInfo implements OAuth2UserInfo {
    	
    	private Map<String, Object> attributes;
    	
    	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");
    	}
    }
    • 네이버는 response에 정보가 담겨있기때문에 서비스에서 타 플랫폼과 다르게 getAttributes에서 get을 써서 response 내용을 따로 받고 이것을 NaverUserInfo로 넘기고 NaverUserInfo에서 attributes로 response안의 내용을 각각 찾을 수 있게 되는 것!

    굿!

     

Designed by Tistory.