
개발하다보면 구현 해봐야 좋은 것들이 하나씩 늘어난다 대표적인 것 중 하나가 바로 소셜 로그인 ..! 구글,카카오,네이버 등 원리는 비슷하기 때문에 구글로그인만 구현해 보기로 !! 시간 여유가 있다면 모두 구현해보는게 좋다.
SpringBoot 2.버전은 이제 사용할 수 없고 3버전부터 Java17을 사용해야해서 기존에 Java8 쓰던 사람은 버전을 변경하자
소셜로그인 구현시 꼭 필요한 Spring Security + OAuth2를 환경설정에 추가하기 위해 build.gradle 파일에 다음코드를 작성한다.
//security
implementation 'org.springframework.boot:spring-boot-starter-security'
//oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
기존에 프로젝트 안에 존재하는 application.properties 파일 외에 구글 oauth2 정보를 담기위해 application-oauth.properties 파일을 생성하고 다음 코드를 작성한다.
(git에 업로드되지 않도록 .gitignore 파일에 파일 등록하기)
#oauth2
spring.security.oauth2.client.registration.google.client-id=구글 클라이언트 아이디
spring.security.oauth2.client.registration.google.client-secret= 구글 클라이언트 비밀번호
spring.security.oauth2.client.registration.google.scope=email,profile
구글 클라이언트 아이디 & 비밀번호 발급 방법은 이미 많은 블로그,사이트에 나와있기 때문에 생략했다.
위에서 작성한 google client 정보를 가져오기 위해 다음 코드를 작성한다.
#oauth2
spring.profiles.include=oauth
모두 완료했다면 환경설정은 끝 !
Spring으로 개발할 때 Controller, Service, DAO(Repository), DTO 등으로 나눠 서비스를 구현하는데 구글로그인을 구현할 때도 마찬가지라고 생각하면 된다.
구글 로그인 정보를 데이터베이스에 저장하려면 UserEntity 클래스를 먼저 생성한다. (저장하지 않는 경우 생략)
UserEntity.class
@Entity
@NoArgsConstructor
@Data //getter, setter를 선언하지 않고 사용할 수 있게 해주는 lombok 기능
@DynamicUpdate //변경 사항을 감지하고 자동 업데이트를 해주는 기능
@Table(name="user")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private int id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "password")
private String password;
@Enumerated(EnumType.STRING)
@Column(name = "role",nullable = true)
private Role role;
@Builder
public UserEntity(int id, String name, String email, String password, Role role) {
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.role = role;
}
public UserEntity update(String name) {
this.name = name;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
소셜로그인 후 OAuth2User의 Attribute를 담기 위한 DTO 클래스를 생성한다.
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String password;
private Role role;
@Builder
public OAuthAttributes(Map<String,Object> attributes,
String nameAttributeKey,
String name,
String email,
String password,
Role role){
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.password = password;
this.role = role;
}
public static OAuthAttributes of(String registrationId,
String nameAttributeName,
Map<String, Object> attributes) {
return ofGoogle(nameAttributeName, attributes);
}
private static OAuthAttributes ofGoogle(String nameAttributeName,
Map<String, Object> attributes) {
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.password(null)
.attributes(attributes)
.nameAttributeKey(nameAttributeName)
.build();
}
//처음 가입할 때 user 테이블에 데이터를 저장하기 위한 UserEntity 생성
public UserEntity toEntity() {
return UserEntity.builder()
.name(name)
.email(email)
.password(password)
.role(Role.USER)
.build();
}
}
맨 마지막 toEntity() 메서드에 데이터베이스와 매핑되는 UserEntity 타입의
구글로그인으로 가져온 사용자 정보를 활용하는 OAuthUserService 클래스를 생성한다.
@Service
@RequiredArgsConstructor
public class Oauth2UserServiceImpl implements OAuth2UserService<OAuth2UserRequest,OAuth2User> {
private final UserRepo userRepo;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userIDAttributeID = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
//로그인 정보를 OAuthAttribute 객체로 생성한다.
OAuthAttributes attributes = OAuthAttributes.
of(registrationId, userIDAttributeID, oAuth2User.getAttributes());
//attribute값을 넘겨 저장 또는 수정
UserEntity user = saveOrUpdate(attributes);
//프론트에서 사용할 사용자 이메일과 비밀번호 재설정을 확인하기 위한 속성값을 세션에 등록
httpSession.setAttribute("userID", user.getEmail());
if(user.getPassword() == null || user.getPassword().equals("")){
httpSession.setAttribute("emptypw","yes");
}
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
//DB에 회원 정보 저장 또는 수정
private UserEntity saveOrUpdate(OAuthAttributes attributes) {
UserEntity user = userRepo.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName()))
.orElse(attributes.toEntity());
return userRepo.save(user);
}
}
코드를 보면 OAuth2UserService 인터페이스를 implement하고 loadUser 메서드를 오버라이딩 한 걸 확인할 수 있다.
loadUser 메서드에서 가져온 데이터를 saveOrUpdate 메서드로 DB에 저장하거나 구글 클라이언트 정보가 변경되면 수정한다.
oauth login 성공 및 실패시 처리 조건, 사용자 정보 처리, 접근 페이지 등을 설정하기 위한 SecurityConfig 클래스를 생성한다.
** 이제 SpringBoot 2.x 버전을 사용할 수 없기 때문에 기존에 메서드 형식으로 작성하던 코드를 람다 표현식으로 작성해야한다.
❌ http.csrf().disable()
⭕ http.csrf(AbstractHttpConfigurer::disable)
@EnableWebSecurity
@Configuration
@AllArgsConstructor
public class SecurityConfig {
private final Oauth2UserServiceImpl oauth2UserServiceImpl;
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// .formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorizeRequest)-> authorizeRequest
// "/user/**"경로는 로그인을 해야 접근 가능
.requestMatchers("/user/**").authenticated()
//그 외 경로는 모두 접근 가능
anyRequest().permitAll())
//로그아웃 성공 후 이동할 경로
.logout(logout-> logout.logoutSuccessUrl("/"))
//구글로그인 후 사용자 정보를 보낼 클래스와 주소 지정
.oauth2Login((oauth2Login)->oauth2Login.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(oauth2UserServiceImpl)).defaultSuccessUrl("/google-login"))
;
return http.build();
}
}
구현 완료 ! 이제 로그인이 되는지 테스트 해 보자. 간단히 테스트 하는 방법은 두 가지가 있다 (본인은 jsp,jstl 사용)
화면에 구글로그인 버튼을 생성해 테스트하기
<c:when test="${user == null}">
<li>
<div id="google-btn" style="width: 50px;">
<a href="/oauth2/authorization/google">
<img src="resources/image/web_neutral_rd_na@3x.png" style="width: 100%;"/>
</a>
</div>
</li>
</c:when>
이전에 구글로그인이 완료되면 세션에 user값을 넣어주도록 설정했기 때문에 user == null이면 구글로그인 버튼을 표시하도록 작성했다.
이 때 href="/oauth2/authorization/google" 경로를 추가해야한다.
로그인이 정상적으로 완료되면 SecurityConfig 클래스에서 설정한 .defaultSuccessUrl("/google-login")) 코드의 경로로 이동하는 걸 확인할 수 있다.
컨트롤러에서 경로를 매핑해 테스트하기
화면 구현을 하지 않는 테스트 방법을 원한다면 컨트롤러에서 특정 경로에 접근시 /oauth2/authorization/google 페이지로 이동하도록 구현하면 된다.
@Controller
@AllArgsConstructor
public class HomeController {
@RequestMapping(value={"/loginTest"})
public String loginTest() {
log.info("로그인 테스트");
return "/oauth2/authorization/google";
}
}
localhost:8080/loginTest 로 접속하면 구글로그인 화면으로 이동하고 로그인 성공시 위와 같이 defaultSuccessUrl 설정 경로로 이동하는걸 확인할 수 있다.
이 과정도 모두 성공적으로 완료했다면 정말 구현 끝!!
하지만 매번 세션값을 가져오는 코드를 작성하면 중복 코드가 늘어나고 번거로워지는게 당연하다. 조금 더 간결한 코드 작성과 편리성을 위한 어노테이션을 생성하려면 다음 링크로 이동 ~!
구글로그인 검증하기
간단한 듯 하지만 초보에겐 간단하지 않은 구글로그인을 구현해봤는데 전체적인 구현 원리는 이해했지만 SecurityConfig, LoginUserArgumentResolver 같은 클래스의 모든 코드를 한 번에 이해하긴 어려웠다. 꾸준한 복습만이 답ㅠㅠ
바르지 않은 정보가 존재할 수 있습니다. 틀린 부분이나 개선하면 좋은 부분을 댓글로 알려주시면 커피 기프티콘 보내드리겠습니다 !! ☕