이 글에서는 페이스북 OAuth2.0 소셜 로그인 기능을 구현하는 방법에 대해서 기록해보고자 합니다.
이 글에서 사용한 프로젝트 스택은 Springboot 2.6.6 버전, Maven 빌드를 사용했습니다. 기본 로그인은 Spring Security를 사용하여 구현해 놓은 상태입니다.
OAuth2.0 클라이언트 라이브러리 추가하기
OAuth2.0 기능을 사용할 수 있도록 스프링에서 제공하는 OAuth2.0 클라이언트 라이브러리를 pom.xml에 추가해줍니다.
<!-- OAuth2 클라이언트 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
페이스북 로그인 구현하기
# 페이스북 개발자 센터 가입하기
저는 페이스북을 사용하지 않는데 소셜 로그인 기능 구현을 위해서 회원가입을 진행했습니다. 허허
https://developers.facebook.com/?locale=ko_KR
Meta for Developers
꿈의 아틀리에 창조 BUCK의 크리에이터와 개발자로부터 Meta Spark를 사용하여 DIOR Beauty를 위한 AR 경험을 설계 및 빌드하는 과정에 대한 비하인드 스토리를 들어보세요. 이제 고급 액세스에 대한 비
developers.facebook.com
먼저 개발자 센터에 들어가서 본인의 페이스북 아이디로 로그인을 해줍니다.
# 개발자 센터 설정하기
My Apps > 앱 만들기에 들어가서 본인의 앱을 하나 만들어줍니다.
앱이 생성되고 들어가 보면 왼쪽 사이드에서 Facebook 로그인이라는 메뉴가 있습니다. 설정이 아래와 같이 되어있는지 확인해 줍니다.
설정이 확인되었으면 이제 아래 빠른 시작을 눌러서 웹을 선택하고 본인 사이트 URL을 입력해 줍니다.
저는 로컬 환경에서 진행할 것이기 때문에 http://localhost:8080으로 설정해 두겠습니다. 본인의 배포된 사이트가 있다면 그 주소를 입력해 주시면 됩니다. 저장을 누르고 계속하기를 하고, 다음도 계속 넘겨줍니다.
# 페이스북 로그인 버튼 추가하기
저는 JSP를 사용해서 구현했기 때문에 아래와 코드를 작성해 줬습니다.
코드는 그냥 기본 버튼을 사용해도 되고, 본인이 원하는 버튼을 사용해도 상관없습니다.
"/oauth2/authorization/facebook" 경로만 잘 설정해 주면 됩니다.
<!-- Facebook Oauth 소셜로그인 -->
<div class="login__facebook">
<button onclick="javascript:location.href='/oauth2/authorization/facebook'" >
<i class="fab fa-facebook-square"></i>
<span>Facebook으로 로그인</span>
</button>
</div>
<button>의 onclick 옵션에 javascript의 location.href 기능으로 바로 정해진 페이스북 OAuth2 로그인 요청 페이지로 이동하도록 설정해 줍니다.
"/oauth2/authorization/facebook"은 내가 설정한 적도 없는데 저렇게 써도 되는 건가라는 궁금증이 있을 수도 있습니다. 우리가 제일 처음에 스프링이 제공하는 OAuth2.0 클라이언트 라이브러리를 추가해 준 이유입니다! yml을 설정하러 갑니다.
# application.yml OAuth2 설정하기
설정해줘야 하는 부분은 spring.security.oauth2.client.registration에 facebook에 대한 설정을 해줘야 합니다. 코드로 한번 살펴보겠습니다.
spring:
security:
oauth2:
client:
registration:
facebook:
client-id: {페이스북 개발자 센터의 앱 ID}
client-secret: {페이스북 개발자 센터의 앱 시크릿 코드}
scope:
- public_profile
- email
페이스북 개발자 센터의 앱 ID와 시크릿 코드는 개발자 센터에 들어가서 설정의 기본 설정으로 들어가면 확인하실 수 있습니다.
다음은 scope에 대해서 궁금하실 텐데 이 부분은 페이스북으로부터 사용자의 어떤 데이터를 받을지에 대한 범위를 지정할 수 있습니다.
개인정보는 민감한 부분이기 때문에 우리가 얻고 싶은 데이터를 얻는 것이 아니고 OAuth2.0를 제공하는 기업마다 다르기 때문에 문서를 참고해야 합니다. 우리는 email 정보와 프로필 정보를 가지고 있는 public_profile을 범위로 설정할 것입니다.
이렇게 yml 설정을 마치면 앞서 jsp에서 작성했던 경로인 "/oauth2/authorization/{registration에 등록한 기업명}"을 이제 사용할 수 있게 됩니다.
그리고 추가적으로 이번에는 다루지 않을 내용이지만 카카오, 네이버 로그인 기능 구현에는 요구되는 사항이라서 얘기하고 넘어가겠습니다. 페이스북, 구글은 Spring OAuth2.0 Client에서 제공되는 provider에 대한 yml 설정을 따로 해주지 않아도 됩니다. 우리나라에서는 빈번하게 사용되는 카카오, 네이버의 경우에는 우리가 직접 provider로 등록해서 사용해야 합니다. 다음에 네이버 소셜 로그인 기능 구현에 대한 게시글을 정리할 때 자세히 적어보겠습니다.
# SecurityConfig 클래스 수정하기
이름은 설정하기 나름이겠지만 SpringSecurity를 사용하여 로그인 기능을 구현할 때 사용했던 WebSecurityConfigurerAdapter를 상속받은 클래스를 의미합니다. 저는 이름을 SecurityConfig라고 지정했습니다.
아래는 기존의 SecurityConfig의 코드입니다.
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**")
.authenticated()
.anyRequest()
.permitAll()
.and()
.formLogin()
.loginPage("/auth/signin")
.loginProcessingUrl("/auth/signin")
.defaultSuccessUrl("/");
}
}
configure() 메소드에 설정된 대로 SpringSecurity가 작동합니다. 위 코드는 OAuth2.0 보다는 SpringSecurity에 가깝기 때문에 자세히 설명하진 않겠습니다.
간단하게 말하자면 ["/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**"]에 해당하는 URL 요청이 들어온다면 인증이 필요합니다. 그 외의 요청이 들어오면 모두 허용합니다.
그리고 로그인 인증 요청이 들어오면 로그인 페이지(/auth/signin)로 이동하고, 로그인 처리는 SpringSecurity에 위임합니다. 로그인이 성공한다면 홈(/)으로 이동한다는 의미의 코드입니다. 여기에 이제 OAuth2 처리에 대한 부분도 추가해 줍니다.
@RequiredArgsConstructor // OAuth2DetailService를 DI 받기 위한 Lombok 어노테이션
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final OAuth2DetailService oAuth2DetailService; // DI를 받아올 Service
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**")
.authenticated()
.anyRequest()
.permitAll()
.and()
.formLogin()
.loginPage("/auth/signin")
.loginProcessingUrl("/auth/signin")
.defaultSuccessUrl("/")
.and()
.oauth2Login() // OAuth2 로그인 요청이 들어오면 처리해준다.
.userInfoEndpoint() // 요청이 끝나면 인증코드가 아닌 회원정보를 받겠다는 코드
.userService(oAuth2DetailService); // 해당 로직을 실행시키고, 데이터를 받을 서비스를 지정한다.
}
}
새롭게 추가된 부분에는 주석을 달아놨습니다. 먼저 의문이 드는 코드가 한 두 개가 아닙니다. 순서대로 확인해 봅시다.
OAuth2DetailService
: 아직 구현하지 않은 클래스입니다. OAuth2 요청이 왔을 때, 그 응답 데이터를 받아서 로직을 처리하는 역할을 합니다.
.oauth2Login()
: OAuth2 로그인 요청이 왔을 때 처리해 줍니다. 우리는 스프링에서 제공하는 OAuth2 Client 라이브러리를 사용하고 있기 때문에 이렇게 쉽게 처리가 가능하지만 원래는 인가 코드를 받아서 AccessToken을 요청한 후 돌려받은 AccessToken을 가지고 사용자 데이터를 요청해야 합니다.
.userInfoEndpoint()
: 위 요청이 끝나면 인증코드가 아닌 회원정보를 받겠다는 코드입니다.
.userService(oAuth2DetailService)
: oAuth2DetailService에서 OAuth2에 대한 요청에 대한 데이터를 받아서 로직을 처리하도록 합니다. 이때 userService에 들어갈 수 있는 타입은 DefaultOAuth2UserService입니다.
# OAuth2DetailService 생성하기
위에서 말했듯이 OAuth2 요청이 왔을 때 그 응답 데이터를 받아서 로직을 처리하는 역할을 하는 클래스입니다.
타입은 DefaultOAuth2UserService여야 하기 때문에 상속을 받아서 구현합니다.
@Service
public class OAuth2DetailService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
return null;
}
}
loadUser의 파라미터를 보면 OAuth2UserRequest 타입의 변수가 있는데, 여기에 OAuth2 로그인을 요청한 사용자의 정보가 담겨서 온다. System.out.println()으로 oAuth2User를 출력해 보면 OAuth2 요청 버튼을 눌렀을 때, 정상적으로 사용자 정보가 들어오는 것을 확인할 수 있다.
# OAuth2DetailService 로그인 로직 구현
이제 받은 페이스북 사용자 정보를 가지고 로그인 기능을 구현해 볼 것이다. 가장 먼저 우리가 받아야 하는 정보는 username, password, email, name이다.
@RequiredArgsConstructor // 생성자로 DI를 받아오기 위한 Lombok 어노테이션
@Service
public class OAuth2DetailService extends DefaultOAuth2UserService {
private final UserRepository userRepository; // 회원을 DB에 넣기위해 DI를 받아옴
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> userInfo = oAuth2User.getAttributes();
// facebook 사용자임을 구분하기 위해서 아이디 앞에 facebook_을 붙임
String username = "facebook_" + (String) userInfo.get("id");
// 암호는 임의의 UUID를 만들어 인코딩을 진행
String password = new BCryptPasswordEncoder().encode(UUID.randomUUID().toString());
// 받아온 데이터에서 email과 name에 해당되는 내용을 저장
String email = (String) userInfo.get("email");
String name = (String) userInfo.get("name");
User userEntity = userRepository.findByUsername(username);
// OAuth2 요청할 때마다 계속 INSERT되는 것을 막기위한 로직, 기존 회원이 존재하면 회원가입 로직 X
if (userEntity == null) {
// OAuth2 요청을 한 사용자가 기존 회원이 아닐 때, 새로운 User를 생성
User user = User.builder()
.username(username)
.password(password)
.email(email)
.name(name)
.role("ROLE_USER")
.build();
// UserDetails 타입으로 반환해야 Session에 저장하고 할 수 있다.
// 그래서 PrincipalUserDetail에서 OAuth2User를 상속받게하여 loadUser에서 사용할 수 있도록 해준다.
return new PrincipalUserDetails(userRepository.save(user), oAuth2User.getAttributes());
}
return new PrincipalUserDetails(userEntity, oAuth2User.getAttributes());
}
}
요청받은 사용자 데이터에는 attributes라는 필드가 있고, 그 안에 우리가 필요로 하는 데이터들이 들어있습니다. 그렇기 때문에 Map<> 타입으로 우리가 필요한 데이터를 저장해 줍니다. 이때 attribute를 가져오기 위해서 getAttributes() 메소드를 사용합니다.
Map<>의 value 타입은 Object이기 때문에 userInfo.get()을 통해서 가져온 데이터는 각각에 맞는 데이터 형태로 형 변환을 해줘야 합니다. 이때 (String)을 주로 사용했습니다.
username, password, email, name을 attribute에서 가져와서 값을 저장한 뒤 user 데이터베이스에 넣기 위해서 UserRepositroy의 save() 메소드를 사용합니다.
근데 그 아래 return 되는 PrincipalUserDetailssms 무엇인지 궁금할 것이다. PrincipalUserDetails는 SpringSecurity 로그인 기능을 구현하기 위해서 만들어놨던 클래스이다. SpringSecurity에서 세션 정보를 저장하기 위해서는 UserDetails 타입으로 반환되어야 하는데, 그래서 PrincipalUserDetails를 이용해서 반환을 해준 것이다.
하지만 한 가지 걸리는 점이 있다. 세션을 위해 UserDetails 타입으로 반환해 준 것은 좋다. 근데 loadUser() 메소드의 return 타입을 보면 OAuth2User 타입이다. 즉, UserDetails 타입이면서 OAuth2User 타입도 되어야 하기 때문에 PrincipalUserDetails 코드를 조금 수정해야 한다.
// OAuth2User 타입으로 만들기 위해서 OAuth2User를 상속받음
@Data
public class PrincipalUserDetails implements UserDetails, OAuth2User {
private User user;
// OAuth2 사용자 데이터의 attribute를 저장하기 위한 변수
private Map<String, Object> attributes;
public PrincipalUserDetails(User user) {
this.user = user;
}
// OAuth2 유저 구분을 위한 생성자 오버로딩
public PrincipalUserDetails(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 (String) attributes.get("name");
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(() -> {
return user.getRole();
});
return collectors;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
기존 코드가 아닌 부분에는 주석을 달아놨다. 이렇게 OAuth2User 인터페이스를 상속받게 되면 PrincipalUserDetails 클래스는 UserDetails 타입이면서 OAuth2User 타입으로 사용할 수 있게 된다.
# 로그인 버튼 눌러서 확인해 보기
모든 기능 구현이 끝났기 때문에 만들어둔 로그인 버튼을 눌러서 로그인이 정상적으로 되는지 확인해 보자.
버튼을 눌러보면 아래와 같은 페이스북 로그인 페이지로 넘어가게 된다.
아이디와 비밀번호를 입력해서 로그인을 진행해 보면 로그인이 완료되는 것을 확인할 수 있다.
각자 진행하고 있는 프로젝트의 구조가 다르기 때문에 기능적으로 필요한 부분들만 얻어가셨으면 좋을 거 같다. 다음에는 카카오, 네이버, 구글 로그인에 대해서도 한번 다뤄볼 생각이다.
'🧑🏻💻 Dev > SpringBoot' 카테고리의 다른 글
[Spring] application.yml 설정값 가져오기 (0) | 2023.04.07 |
---|---|
[Spring] ExceptionHandler가 작동안 되는 오류 (컴포넌트 스캔) (0) | 2023.04.03 |
[Spring] 프로젝트 실행 시 Please Sign in 페이지 해결 (0) | 2023.02.19 |
[Spring] Mac OS : Web server failed to start. Port 8080 was already in use 문제 해결하기 (0) | 2023.02.19 |
[SpringBoot] mysql 연동 후 data.sql 적용하기 (0) | 2023.01.23 |