Spring Security 연동 (5) (소셜 로그인 처리 - ①)
Spring Security 연동 (5) (소셜 로그인 처리 - ①)
Spring Security 연동 (4) (UserDetailsService, DTO, Controller) Spring Security 연동 (4) (UserDetailsService, DTO, Controller) Spring Security 연동 (3) (JPA 처리, Entity, Repository, Test) Spring Security 연동 (3) (JPA 처리, Entity, Repository,
soohykeee.tistory.com
앞서 OAuth 2.0 설정을 완료하였다. 이제 프로젝트와의 연동을 위해 디테일한 코드 작성을 해줘야한다.
앞서 로그인 페이지에서 OAuth를 이용하여 구글로 로그인이 성공적으로 되었다. 하지만 '/sample/member'에 접근 시 출력되는 정보들이 로그인한 유저의 정보를 제대로 가져오지 못했다. 이러한 점들을 고려해서 다음을 추가해주어야 한다.
- 소셜 로그인 처리 시에 사용자의 이메일 정보를 추출해야한다.
- 현재 DB와 연동해서 사용자 정보를 관리해야한다.
- 기존 방식으로도 로그인할 수 있어야 하고, 소셜 로그인으로도 동일하게 동작해야 한다.
OAuth2UserService
우선적으로 구글과 같은 서비스에서 로그인 처리가 끝난 결과를 가져오는 작업을 사용할 수 있는 환경을 구성해야한다. 이를 위해서는 실제 소셜 로그인 과정에서 동작하는 OAuth2UserService라는 것을 알아야한다.
org.springframework.security.oauth2.client.userinfo.OAuth2UserService 인터페이스는 UserDetailsService의 OAuth 버전이라고 생각하면 된다. 이를 구현하는 것은 OAuth의 인증 결과를 처리한다는 의미이다.

인터페이스를 직접 구현할 수도 있지만, 구현된 클래스 중 하나인 DefaultOAuth2UserService 클래스를 상속하여 사용하는 것이 좀더 편리하다.
DefaultOAuth2UserService 는 위에서 확인할 수 있듯이 OAuth2UserRequest와 OAuth2User 타입의 객체를 반환한다. 현재 프로젝트에서 로그인 시 사용하는 반환 타입이 아니기에, 이를 변환해주어 사용해야한다.
service 디렉토리 하위에 ClubOAuth2UserDetailsService 클래스를 생성해준다.

@Log4j2
@Service
public class ClubOAuth2UserDetailsService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("----------------------------------");
log.info("userRequest: " + userRequest);
String clientName = userRequest.getClientRegistration().getClientName();
log.info("clientName : " + clientName);
log.info(userRequest.getAdditionalParameters());
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("=====================================");
oAuth2User.getAttributes().forEach((k,v)->{
log.info(k + ":" + v);
});
return super.loadUser(userRequest);
}
}
oAuth2User를 사용하여 Attirubtes의 key, value 값을 가져오는 코드는, 현재 OAuth2UserRequest에서 어떤 서비스를 통해서 로그인이 되었는지 확인해주기 위해 추가해준 코드이다.
즉, 다시말하면 위의 코드는 메서드 내에서 OAuth로 연결한 클라이언트 이름과 이때 사용한 파라미터들을 출력하고, 이후에 처리 결과로 나오는 OAuth2User 객체의 내부에 어떤 값들이 있는지 확인하는 것이다. 해당 코드 작성 후, 다시한번 'sample/member'로 접근하여 Google 로그인 진행 시, 아래에 값들이 출력되는 것을 확인할 수 있다.

DB에 저장
위에서 콘솔에서 보였듯이 email이 출력되는 것을 확인할 수 있다. 이를 이용해서 해당 이메일을 DB에 추가해주면 된다. 하지만 여기서 추가로 생각해줘야 하는 것은 패스워드나 사용자의 이름의 경우이다. 만약 임의로 패스워드를 지정해서 DB에 저장하는 경우에는 나중에 문제가 생길 수 있다. 따라서 해당 문제는 조금 뒤에 해결해 줄 것이다.
@Log4j2
@Service
@RequiredArgsConstructor
public class ClubOAuth2UserDetailsService extends DefaultOAuth2UserService {
private final ClubMemberRepository repository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("----------------------------------");
log.info("userRequest: " + userRequest);
String clientName = userRequest.getClientRegistration().getClientName();
log.info("clientName : " + clientName);
log.info(userRequest.getAdditionalParameters());
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("=====================================");
oAuth2User.getAttributes().forEach((k, v) -> {
log.info(k + ":" + v);
});
String email = null;
if (clientName.equals("Google")) {
email = oAuth2User.getAttribute("email");
}
log.info("-------EMAIL : " + email);
ClubMember clubMember = saveSocialMember(email);
return oAuth2User;
}
private ClubMember saveSocialMember(String email) {
Optional<ClubMember> result = repository.findByEmail(email, true);
if (result.isPresent()) {
return result.get();
}
ClubMember clubMember = ClubMember.builder()
.email(email)
.name(email)
.password(passwordEncoder.encode("1111"))
.fromSocial(true)
.build();
clubMember.addMemberRole(ClubMemberRole.USER);
repository.save(clubMember);
return clubMember;
}
}
앞서 말했던 비밀번호와 사용자이름의 경우, 비밀번호는 임의로 1111로 설정해두고, 사용자의 이름은 똑같이 이메일로 설정한다. 현재 ClubMember DB에서 PK가 email 이므로, result.isPresent()를 통해 동일한 이메일이 이미 존재하는지 확인하고, 만일 존재한다면 새로운 정보를 추가해주는 것이 아니라, get을 통해 값을 가져오도록 해준다.
위와 같이 코드를 작성해준 후, 로그인을 진행하면 ClubMember DB에 값이 저장되는 것을 확인할 수 있다.

DB에 정상적으로 저장이 되지만 해당 Controller에는 여전히 이메일 주소가 아닌 사용자의 번호로 출력되는 것을 확인할 수 있고, user role 또한 정상적으로 출력이 되지 않는 것을 확인할 수 있다. 이는, 앞서 만들어진 Controller와 front 단에서 ClubAuthMemberDTO 타입을 사용하기에 소셜 로그인하는 경우 null 값이 들어가 제대로 정보가 출력이 되지 않는 것이다. 따라서 해당 부분도 수정이 필요하다.

@Log4j2
@Getter
@Setter
@ToString
public class ClubAuthMemberDTO extends User implements OAuth2User {
private String email;
private String password;
private String name;
private boolean fromSocial;
private Map<String, Object> attr;
public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.email = username;
this.password = password;
this.fromSocial = fromSocial;
}
public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities, Map<String,Object> attr) {
this(username, password, fromSocial, authorities);
this.attr = attr;
}
@Override
public Map<String, Object> getAttributes() {
return this.attr;
}
}
@Log4j2
@Service
@RequiredArgsConstructor
public class ClubOAuth2UserDetailsService extends DefaultOAuth2UserService {
private final ClubMemberRepository repository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("----------------------------------");
log.info("userRequest: " + userRequest);
String clientName = userRequest.getClientRegistration().getClientName();
log.info("clientName : " + clientName);
log.info(userRequest.getAdditionalParameters());
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("=====================================");
oAuth2User.getAttributes().forEach((k, v) -> {
log.info(k + ":" + v);
});
String email = null;
if (clientName.equals("Google")) {
email = oAuth2User.getAttribute("email");
}
log.info("-------EMAIL : " + email);
ClubMember member = saveSocialMember(email);
ClubAuthMemberDTO clubAuthMember = new ClubAuthMemberDTO(
member.getEmail(),
member.getPassword(),
true,
member.getRoleSet().stream().map(
role -> new SimpleGrantedAuthority("ROLE_" + role.name())
).collect(Collectors.toList()),
oAuth2User.getAttributes()
);
clubAuthMember.setName(member.getName());
return clubAuthMember;
}
private ClubMember saveSocialMember(String email) {
// ... 생략
}
}
loadUser()에서 가장 달라지는 점은 다음과 같다.
- saveSocialMember()한 결과로 나오는 ClubMember를 이용해서 ClubAuthMemberDTO를 구성
- OAuth2User의 모든 데이터는 ClubAuthMemberDTO의 내부로 전달해서 필요한 순간에 사용할 수 있도록 구성
위의 코드 작성 후, 실행 시 Google로 로그인 하여도 정상적으로 email과 role이 출력되는 것을 볼 수 있다.

Spring Security 연동 (5) (소셜 로그인 처리 - ①)
Spring Security 연동 (5) (소셜 로그인 처리 - ①)
Spring Security 연동 (4) (UserDetailsService, DTO, Controller) Spring Security 연동 (4) (UserDetailsService, DTO, Controller) Spring Security 연동 (3) (JPA 처리, Entity, Repository, Test) Spring Security 연동 (3) (JPA 처리, Entity, Repository,
soohykeee.tistory.com
앞서 OAuth 2.0 설정을 완료하였다. 이제 프로젝트와의 연동을 위해 디테일한 코드 작성을 해줘야한다.
앞서 로그인 페이지에서 OAuth를 이용하여 구글로 로그인이 성공적으로 되었다. 하지만 '/sample/member'에 접근 시 출력되는 정보들이 로그인한 유저의 정보를 제대로 가져오지 못했다. 이러한 점들을 고려해서 다음을 추가해주어야 한다.
- 소셜 로그인 처리 시에 사용자의 이메일 정보를 추출해야한다.
- 현재 DB와 연동해서 사용자 정보를 관리해야한다.
- 기존 방식으로도 로그인할 수 있어야 하고, 소셜 로그인으로도 동일하게 동작해야 한다.
OAuth2UserService
우선적으로 구글과 같은 서비스에서 로그인 처리가 끝난 결과를 가져오는 작업을 사용할 수 있는 환경을 구성해야한다. 이를 위해서는 실제 소셜 로그인 과정에서 동작하는 OAuth2UserService라는 것을 알아야한다.
org.springframework.security.oauth2.client.userinfo.OAuth2UserService 인터페이스는 UserDetailsService의 OAuth 버전이라고 생각하면 된다. 이를 구현하는 것은 OAuth의 인증 결과를 처리한다는 의미이다.

인터페이스를 직접 구현할 수도 있지만, 구현된 클래스 중 하나인 DefaultOAuth2UserService 클래스를 상속하여 사용하는 것이 좀더 편리하다.
DefaultOAuth2UserService 는 위에서 확인할 수 있듯이 OAuth2UserRequest와 OAuth2User 타입의 객체를 반환한다. 현재 프로젝트에서 로그인 시 사용하는 반환 타입이 아니기에, 이를 변환해주어 사용해야한다.
service 디렉토리 하위에 ClubOAuth2UserDetailsService 클래스를 생성해준다.

@Log4j2
@Service
public class ClubOAuth2UserDetailsService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("----------------------------------");
log.info("userRequest: " + userRequest);
String clientName = userRequest.getClientRegistration().getClientName();
log.info("clientName : " + clientName);
log.info(userRequest.getAdditionalParameters());
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("=====================================");
oAuth2User.getAttributes().forEach((k,v)->{
log.info(k + ":" + v);
});
return super.loadUser(userRequest);
}
}
oAuth2User를 사용하여 Attirubtes의 key, value 값을 가져오는 코드는, 현재 OAuth2UserRequest에서 어떤 서비스를 통해서 로그인이 되었는지 확인해주기 위해 추가해준 코드이다.
즉, 다시말하면 위의 코드는 메서드 내에서 OAuth로 연결한 클라이언트 이름과 이때 사용한 파라미터들을 출력하고, 이후에 처리 결과로 나오는 OAuth2User 객체의 내부에 어떤 값들이 있는지 확인하는 것이다. 해당 코드 작성 후, 다시한번 'sample/member'로 접근하여 Google 로그인 진행 시, 아래에 값들이 출력되는 것을 확인할 수 있다.

DB에 저장
위에서 콘솔에서 보였듯이 email이 출력되는 것을 확인할 수 있다. 이를 이용해서 해당 이메일을 DB에 추가해주면 된다. 하지만 여기서 추가로 생각해줘야 하는 것은 패스워드나 사용자의 이름의 경우이다. 만약 임의로 패스워드를 지정해서 DB에 저장하는 경우에는 나중에 문제가 생길 수 있다. 따라서 해당 문제는 조금 뒤에 해결해 줄 것이다.
@Log4j2
@Service
@RequiredArgsConstructor
public class ClubOAuth2UserDetailsService extends DefaultOAuth2UserService {
private final ClubMemberRepository repository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("----------------------------------");
log.info("userRequest: " + userRequest);
String clientName = userRequest.getClientRegistration().getClientName();
log.info("clientName : " + clientName);
log.info(userRequest.getAdditionalParameters());
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("=====================================");
oAuth2User.getAttributes().forEach((k, v) -> {
log.info(k + ":" + v);
});
String email = null;
if (clientName.equals("Google")) {
email = oAuth2User.getAttribute("email");
}
log.info("-------EMAIL : " + email);
ClubMember clubMember = saveSocialMember(email);
return oAuth2User;
}
private ClubMember saveSocialMember(String email) {
Optional<ClubMember> result = repository.findByEmail(email, true);
if (result.isPresent()) {
return result.get();
}
ClubMember clubMember = ClubMember.builder()
.email(email)
.name(email)
.password(passwordEncoder.encode("1111"))
.fromSocial(true)
.build();
clubMember.addMemberRole(ClubMemberRole.USER);
repository.save(clubMember);
return clubMember;
}
}
앞서 말했던 비밀번호와 사용자이름의 경우, 비밀번호는 임의로 1111로 설정해두고, 사용자의 이름은 똑같이 이메일로 설정한다. 현재 ClubMember DB에서 PK가 email 이므로, result.isPresent()를 통해 동일한 이메일이 이미 존재하는지 확인하고, 만일 존재한다면 새로운 정보를 추가해주는 것이 아니라, get을 통해 값을 가져오도록 해준다.
위와 같이 코드를 작성해준 후, 로그인을 진행하면 ClubMember DB에 값이 저장되는 것을 확인할 수 있다.

DB에 정상적으로 저장이 되지만 해당 Controller에는 여전히 이메일 주소가 아닌 사용자의 번호로 출력되는 것을 확인할 수 있고, user role 또한 정상적으로 출력이 되지 않는 것을 확인할 수 있다. 이는, 앞서 만들어진 Controller와 front 단에서 ClubAuthMemberDTO 타입을 사용하기에 소셜 로그인하는 경우 null 값이 들어가 제대로 정보가 출력이 되지 않는 것이다. 따라서 해당 부분도 수정이 필요하다.

@Log4j2
@Getter
@Setter
@ToString
public class ClubAuthMemberDTO extends User implements OAuth2User {
private String email;
private String password;
private String name;
private boolean fromSocial;
private Map<String, Object> attr;
public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.email = username;
this.password = password;
this.fromSocial = fromSocial;
}
public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities, Map<String,Object> attr) {
this(username, password, fromSocial, authorities);
this.attr = attr;
}
@Override
public Map<String, Object> getAttributes() {
return this.attr;
}
}
@Log4j2
@Service
@RequiredArgsConstructor
public class ClubOAuth2UserDetailsService extends DefaultOAuth2UserService {
private final ClubMemberRepository repository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("----------------------------------");
log.info("userRequest: " + userRequest);
String clientName = userRequest.getClientRegistration().getClientName();
log.info("clientName : " + clientName);
log.info(userRequest.getAdditionalParameters());
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("=====================================");
oAuth2User.getAttributes().forEach((k, v) -> {
log.info(k + ":" + v);
});
String email = null;
if (clientName.equals("Google")) {
email = oAuth2User.getAttribute("email");
}
log.info("-------EMAIL : " + email);
ClubMember member = saveSocialMember(email);
ClubAuthMemberDTO clubAuthMember = new ClubAuthMemberDTO(
member.getEmail(),
member.getPassword(),
true,
member.getRoleSet().stream().map(
role -> new SimpleGrantedAuthority("ROLE_" + role.name())
).collect(Collectors.toList()),
oAuth2User.getAttributes()
);
clubAuthMember.setName(member.getName());
return clubAuthMember;
}
private ClubMember saveSocialMember(String email) {
// ... 생략
}
}
loadUser()에서 가장 달라지는 점은 다음과 같다.
- saveSocialMember()한 결과로 나오는 ClubMember를 이용해서 ClubAuthMemberDTO를 구성
- OAuth2User의 모든 데이터는 ClubAuthMemberDTO의 내부로 전달해서 필요한 순간에 사용할 수 있도록 구성
위의 코드 작성 후, 실행 시 Google로 로그인 하여도 정상적으로 email과 role이 출력되는 것을 볼 수 있다.
