개발하다보면 구현 해봐야 좋은 것들이 하나씩 늘어난다 대표적인 것 중 하나가 바로 소셜 로그인 ..! 구글,카카오,네이버 등 원리는 비슷하기 때문에 구글로그인만 구현해 보기로 !! 시간 여유가 있다면 모두 구현해보는게 좋다.

SpringBoot 2.버전은 이제 사용할 수 없고 3버전부터 Java17을 사용해야해서 기존에 Java8 쓰던 사람은 버전을 변경하자

1. 환경설정 🌀

1-1) build.gradle

소셜로그인 구현시 꼭 필요한 Spring Security + OAuth2를 환경설정에 추가하기 위해 build.gradle 파일에 다음코드를 작성한다.

//security
implementation 'org.springframework.boot:spring-boot-starter-security'

//oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

1-2) application-oauth.properties

기존에 프로젝트 안에 존재하는 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

구글 클라이언트 아이디 & 비밀번호 발급 방법은 이미 많은 블로그,사이트에 나와있기 때문에 생략했다.

1-3) application.properties

위에서 작성한 google client 정보를 가져오기 위해 다음 코드를 작성한다.

#oauth2

spring.profiles.include=oauth

모두 완료했다면 환경설정은 끝 !

2. 구글로그인 구현 ⭐

Spring으로 개발할 때 Controller, Service, DAO(Repository), DTO 등으로 나눠 서비스를 구현하는데 구글로그인을 구현할 때도 마찬가지라고 생각하면 된다.

2-1) UserEntity.class

구글 로그인 정보를 데이터베이스에 저장하려면 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();
   }
}

2-2) OAuthAttributes.class (DTO)

소셜로그인 후 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 타입의

2-3) OAuthUserService.class

구글로그인으로 가져온 사용자 정보를 활용하는 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에 저장하거나 구글 클라이언트 정보가 변경되면 수정한다.

2-4) SecurityConfig.class

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();
    }
}



3. 로그인 테스트 🌟

구현 완료 ! 이제 로그인이 되는지 테스트 해 보자. 간단히 테스트 하는 방법은 두 가지가 있다 (본인은 jsp,jstl 사용)

3-1) Front Test

화면에 구글로그인 버튼을 생성해 테스트하기

 <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")) 코드의 경로로 이동하는 걸 확인할 수 있다.

3-2) Controller Test

컨트롤러에서 경로를 매핑해 테스트하기

화면 구현을 하지 않는 테스트 방법을 원한다면 컨트롤러에서 특정 경로에 접근시 /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 같은 클래스의 모든 코드를 한 번에 이해하긴 어려웠다. 꾸준한 복습만이 답ㅠㅠ
















바르지 않은 정보가 존재할 수 있습니다. 틀린 부분이나 개선하면 좋은 부분을 댓글로 알려주시면 커피 기프티콘 보내드리겠습니다 !! ☕

profile
곰발이지만 개발 잘 하고싶다 🐻🐾

0개의 댓글