요즘 사이트중에서 소셜로그인을 지원하지 않는 사이트는 거의 없다. 특히 구글의 경우 구현이 쉬워서인지 범용성이 좋아서인지 없는 사이트를 찾아보는게 더 힘들정도이다.
이런 소셜로그인 서비스를 구현할 때 사용하는 인증규악 프로토콜을 OAuth라 부른다.
2006년 Twitter와 Google이 정의한 개방형 Authorization 표준이며, API 허가(Authorize)를 목적으로 JSON(JavaScript Object Notation) 형식으로 개발된 인증프로토콜이다.
다른 웹 서비스를 이용할 때 로그인 자격 증명과 개인정보 전송 없이도 접근 또는 권한부여를 하는 프로토콜이다.
리소스 서버는 백엔드서버이고, Authentication Server는 구글, 애플, 카카오같은 로그인을 처리해주는 사이트이다.
원리는 다음과 같다.
물론 이는 가장 기본적인 방법으로 더 다양한 로그인 인증방식이 있다.
백엔드가 로그인과 인증을 전부 전담하는 방식, 둘다 인증을 하는 방식, 프론트가 인증하고 백엔드는 토큰만 발행하는 방식등 다양하다
가장 기본적인 웹 Oauth2 인증방식을 구현해보았다.
한가지 유의할점은 Google의 경우 자주 사용해서 스프링에 이미 콜백 주소, 인증 주소가 다 저장되어 있어서 간단하게 세팅해도 되지만, 카카오, 네이버같은 경우는 조금 더 복잡하다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: 154711632031-e6a1lsonnrt68haimdnlou81n254rdug.apps.googleusercontent.com
client-secret: 시크릿키가 나옴
scope:
- email
- profile
이렇게 구글 API에서 받아온 ID와 시크릿을 넣어서 tml 파일을 세팅하면 된다.
클라이언트 ID, Secret 발급 방법은 이 글을 참고하자
이전에 하던것처럼, Security를 세팅해주면 끝이다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); // csrf 비활성화
// http
// .formLogin().disable()
// .httpBasic().disable();
http
.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manger/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().authenticated()
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.sessionManagement()
.maximumSessions(1);
http
.formLogin()
.loginProcessingUrl("/loginProc")
.and()
.oauth2Login()
.successHandler(new OAuthSuccessHandler(memberRepository))
.userInfoEndpoint()// 후처리 시작
.userService(principalOAuth2UserService); // 서비스에서 후처리함
}
예제코드에서는 폼로그인과 OAuth2가 모두 활성화 되어있지만, 불편하면 form쪽을 disable해도 된다.
SuccessHAndler는 로그인 성공 시 후처리할 핸들러를 담고 있다.
UserInfoEndPoint는 로그인 정보 전송 후 후처리를 시작하는 부분이다. 여기서 UserSerevice메서드를 통해 Principal Service를 지정해주면, 폼 로그인과 동일한 방식으로 흘러가게 된다.
package Focus_Zandi.version1.web.config.auth;
import Focus_Zandi.version1.domain.Member;
import Focus_Zandi.version1.domain.MemberDetails;
import Focus_Zandi.version1.web.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class PrincipalOAuth2UserService extends DefaultOAuth2UserService {
//후처리함수
//구글로부터 받은 userRequest에 대한 후처리
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final MemberRepository memberRepository;
//구글로그인 버튼 클릭하면 -> 구글로그인 창 -> 로그인 완료 -> code return (OAuth 라이브러리가 받음) -> AccessToken 요청
//userRequest wjdqh -> 회원프로필을 받음(loadUser함수) -> 구글로부터 회원 프로필을 받아옴
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
//회원가입용 정보
Member userEntity = createUser(userRequest, oAuth2User);
return new PrincipalDetails(userEntity, oAuth2User.getAttributes());
}
//자동 회원가입 진행 로직
private Member createUser(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
String provider = userRequest.getClientRegistration().getClientId();
String providerId = oAuth2User.getAttribute("sub");
String name = oAuth2User.getAttribute("name");
String username = provider + "_" + providerId;
String password = passwordEncoder.encode("CommonPassword");
String email = oAuth2User.getAttribute("email");
MemberDetails memberDetails = new MemberDetails();
Member memberEntity = memberRepository.findByUsername(username);
if(memberEntity == null) {
System.out.println("최초 로그인");
memberEntity = Member.builder()
.username(username)
.userToken(providerId)
.password(password)
.name(name)
.email(email)
.memberDetails(memberDetails)
.build();
memberRepository.save(memberEntity);
} else {
System.out.println("이미 가입된 사용자");
}
return memberEntity;
}
}
구글로그인 버튼 클릭
구글로그인 창 나오고 로그인 실행
구글 Authorization Server에서 Code를 반환
Oauth 라이브러리가 이를 받아서 AccessToken을 요청
회원 프로필을 받아옴 (미리 지정한 범위내의 프로필을 받음)
loadUser가 호출되면서 유저를 찾아오고 회원가입 혹은 가입 생략을 진행
가입 혹은 로그인 완료 후 memberEntity를 PrincipalDetails에 담아서 반환함
이를 세션에 담아서 사용자 인증 완료
API로 SPA랑 통신할떄, 즉 안드로이드, IOS, REACT 같은 어플리케이션 일단 프론트와 백이 분리된 상태로 작동한다. 따라서 이렇게 백에서 모든걸 전담하는 방식으로는 제대로 작동하지 않는다.
위에서 설명한것처럼 OAuth 처리방식은 여러가지가 있는데
첫번째 서버쪽에서 처리하는 방식은 Code 방식이라고 하고
두번째 클라이언트쪽에서 처리하는 방식을 Credential 방식이라고 한다. 따라서 이럴때는 클라이언트 처리방식은 Credential 방식을 사용해야한다.
즉, 클라이언트가 인증서버에서 인증까지 다 받아오고 백엔드는 그 정보를 받아서 jwt만 발급해주면 된다.
이렇게 해야하는걸 모르고 단순히 IOS에서 Web으로 리다이렉트해서 거기서 하면 안되는건가? 라고 생각하고 그쪽으로만 계속 접근을 해서 프로젝트에서 제대로 사용하지 못했었다.
간단하게 정리하자면
프론트에서 인증, 인가를 받아 사용자 정보를 받아오고,
백엔드에서는 단순히 /join 을 통해 프론트에서 넘겨준 데이터를 저장하고 JWT 발급기로서의 역할만 하면 되는것이다.
따라서 JWT 사용법을 학습해야한다.