만약 로그인을 직접 구현하면 다음을 전부 구현해야 한다.
1) 로그인시 보안, 회원가입시 이메일 or 전화번호 인증 절차 비밀번호 찾기, 변경 회원정보 변경 등
이 번거로운 절차 없이 SNS 계정만 있으면 바로 로그인할 수 있게 할 수 있다.
2) OAuth 로그인 구현시 앞선 목록의 것들을 모두 구글, 네이버 등에 맡기면 되니 서비스 개발에 집중할 수 있다!
스프링부트1.5에서 OAuth2.0 연동방식이 많이 달라졌다.
스프링 부트 2.0에서는 Spring Security Oatuh2 Clinet 라이브러리 사용.
accessToken 을 얻을 수 있다.
client
Resource Server
Resource Owner
client id
client secret
redirect URL
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-client', version: '2.5.4'
먼저 구글 서비스에 신규 서비스를 생성, 여기서 만들어질 발급된 인증정보는 다음 3가지이다.
- clientId : 클라이언트 ID
- clientSecret : 클라이언트 보안 비밀번호
- redirect URL
구글 클라우드 플랫폼 주소(google cloud platform)로 이동
1) 프로젝트 생성
2) API 및 서비스 대시보드 이동
사용자 인증 정보 > 사용자 인증 정보 만들기
3) OAuth 클라이언트 ID 만들기 > 동의 화면 구성 버튼 클릭
4) OAuth 동의 화면 입력
5) OAuth 클라이언트 ID 만들기
6) 승인된 리다이렉션 URL
- 승인된 리다이렉션 URL은 서비스에서 파라미터로 인증 정보를 주었을 때 인증시 성공하면 구글에서 리다이렉트할 URL임.
- 스프링 부트 2 버전의 시큐리티에서는 기본적으로
{도메인}/login/oauth2/code/{소셜서비스코드}
로 리다이렉트 URL을 지원하고 있음.- 사용자가 별도로 리다이렉트 URL을 지원하는 Controller를 만들 필요가 없음. 시큐리티에서 이미 구현해 놓은 상태임.
- 현재는 개발 단계이므로
http://localhost:8080/login/oauth2/code/google
로만 등록하자.- AWS 서버에 배포하게 되면 localhost 말고 추가로 주소를 추가해야하며, 이건 추후 단계에서 진행하면 됨.
application.yml 등록
security:
oauth2:
client:
registration:
google:
client-id: 클라이언트 ID
client-secret: 클라이언트 보안 비밀번호
scope:
- email
- profile
Security Config 추가
.oauth2Login()
.loginPage("/loginForm")
.userInfoEndpoint()
.userService(principalOauthUserService)
oauth2Login()
: OAuth 2 로그인 기능에 대한 여러 설정의 진입점
userInfoEndpoint()
: OAuth 2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
userService()
: 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록, DefaultOAuth2UserService를 상속한 커스터마이징한 OAuth2UserService를 등록해야 한다.
SecurityConfig
private final OAuth2UserService oAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/todo/**").permitAll()
.antMatchers("/users/**").permitAll()
.anyRequest().authenticated(); // 그외는 인증을 해야 한다.
http.oauth2Login()
.userInfoEndpoint()
.userService(oAuth2UserService);
}
UserController
// http://localhost:8080/users/oauth 을 입력을 해야 json으로 토큰을 볼 수 있어
@GetMapping("/oauth")
public OAuth2AuthenticationToken oauthToken(OAuth2AuthenticationToken token) {
return token;
}
OAuth2UserInfo
public interface OAuth2UserInfo {
String getProviderId();
String getProvider();
String getEmail();
String getName();
}
FacebookUserInfo
@Data
public class FacebookUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes; // oauth2User.getAttributes()
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");
}
}
GoogleUserInfo
@Data
public class GoogleUserInfo implements OAuth2UserInfo{
private Map<String, Object> attributes; // oauth2User.getAttributes()
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public String getProviderId() {
return (String) attributes.get("sub");
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getEmail() {
return (String) attributes.get("email");
}
@Override
public String getName() {
return (String) attributes.get("name");
}
}
OAuth2UserService
@Slf4j
@Service
@RequiredArgsConstructor
public class OAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("userRequest: {}", userRequest);
ClientRegistration clientRegistration = userRequest.getClientRegistration();
log.info("clientRegistration :{}", clientRegistration);
OAuth2User oAuth2User = super.loadUser(userRequest);
oAuth2User.getAuthorities().forEach((k) -> {
log.info("k: {}", k);
});
// oauth 회원가입 강제 등록
OAuth2UserInfo oAuth2UserInfo = null;
if (clientRegistration.getRegistrationId().equals("google")) {
log.info("구글 로그인 요청");
oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
log.info("oAuth2UserInfo: {}",oAuth2UserInfo);
} else if(clientRegistration.getRegistrationId().equals("facebook")){
log.info("페이스북 로그인 요청");
oAuth2UserInfo = new FacebookUserInfo(oAuth2User.getAttributes());
log.info("oAuth2UserInfo: {}",oAuth2UserInfo);
}else{
log.info("우리는 구글과 페이스북만 지원합니다.");
}
String email = oAuth2UserInfo.getEmail();
String name = oAuth2UserInfo.getName();
log.info("email: {}", email);
log.info("name: {}", name);
Optional<User> optionalUser = userRepository.findByEmail(email);
User user = null;
if (optionalUser.isPresent()) {
log.info("로그인을 이미 했음, 자동회원가입이 되어있다.");
} else {
user = User.builder()
.name(name)
.email(email)
// password ecode 처리는 controller에 처리하는 게 나을 것 같음.
.password("githere")
.build();
userRepository.save(user);
// return new PrincipalDetails(user, oAuth2User.getAttributes()); // TODO: NPE 발생
}
return oAuth2User;
}
}
registrationId/clientName : 현재 로그인 진행 중인 서비스를 구분하는 코드 ex) 'Google'
Attributes : OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
이런 정보로 반환을 하면 인증이 된 것이다.