-
스프링부트 시큐리티 03 - OAuth (구글/페이스북/네이버 로그인)스프링/시큐리티 2021. 9. 8. 13:47
유투버 '데어프로그래밍'님 강의 참조
01 - 구글 API
- 아래 주소를 통해 API 생성을 하자
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 타입이 들어가게 된다.
정리를 해보면...
- 시큐리티 세션이 존재하고 그 안에는 들어갈 수 있는 타입은 'Authentication' 객체 뿐 → 시큐리티를 통해 로그인이 정상 진행이 되면 'Authentication'이 생김
- Authentication에 담긴 내용을 컨트롤러를 통해서 꺼내서 사용 가능
- Authentication 객체 안에 들어갈 수 있는 2개의 타입은 'UserDetails'/'OAuth2User'
- 일반적인 로그인을 하게 되면 auth 패키지 이하 프로세스를 타고 UserDetails이 생성되고, 구글시에는 oauth패키지 프로세스를 타여 OAuth2User가 생성이 되고 로그인 방식에 따라 둘 중 하나가 Authentication에 쏙 들어감
- 정상 로그인 이후 이제 이 세션들을 꺼내서 쓸 수 있어야 하는데 일반 로그인(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안의 내용을 각각 찾을 수 있게 되는 것!
굿! '스프링 > 시큐리티' 카테고리의 다른 글
스프링부트 시큐리티 04 - 최종 JWT 서버 구축 (0) 2021.09.11 스프링부트 시큐리티 04 - JWT 토큰 세팅까지의 개념 (1) 2021.09.09 스프링부트 시큐리티 02 - 시큐리티 기본(회원가입/로그인/권한처리) (0) 2021.09.07 스프링부트 시큐리티 01 - 환경설정 (0) 2021.09.07