Project/토이 프로젝트

[Cinemagram] 소셜 로그인 (구글) - (13)

Lea Hwang 2023. 1. 29. 15:59

일반 로그인뿐만 아니라 OAuth2 로그인을 추가로 구현 후 통합 할 예정입니다.

 

관련 포스팅의 순서는 다음과 같습니다.

  • 소셜 로그인 (구글) - (13)
  • [Refactoring] 여러 소셜 로그인을 위한 공통영역 분리
  • 소셜 로그인 (구글, 네이버) - (14)

 

 

이번 포스팅에서는 구글 소셜 로그인 구현에 집중해 보겠습니다.

 

 


 

 

 

시프링 시큐리티 설정

build.gradle > dependencies 스프링 시큐리티 관련 의존성 추가

// Oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  • spring-boot-starter-oauth2-client
    • 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현시 필요한 의존성
    • spring-security-oauth2-client와 spring-security-oauth2-jose를 기본으로 관리해 줌

 

 

OAuth2 전용 설정 파일 생성

application-oauth.yml

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: [client-id]
            client-secret: [client-secret]
            scope: profile,email

 

위 파일을 기존 파일에서도 사용할 수 있도록 application.yml에  include 시키겠습니다. 

# dev-profile
spring:
  profiles:
    include: oauth

 

 

보안을 위한 .gitignore등록

구글 로그인 시 사용되는 client-id, client-secret가 외부에 노출될 경우 언제든 개인정보를 가져갈 수 있습니다. 

보안을 위해 깃허브에는 application-oauth.yml파일이 올라가는 것을 방지하기 위해 .gitignore에 다음의 코드를 추가합니다.

application-oauth.yml

 

추후 커밋 했을 시 커밋 파일 목록에 해당 파일이 나오지 않으면 성공입니다.

 

 

 

SecurityConfig

@RequiredArgsConstructor
@EnableWebSecurity 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder encode() {
        return new BCryptPasswordEncoder();
    }

    private final CustomOAuth2DetailsService customOAuth2DetailsService;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http)

        http.csrf().disable();

        http.authorizeRequests()
            .antMatchers("/","/user/**", "/image/**", "/follow/**","/comment/**","/api/**").authenticated()
            .anyRequest().permitAll()
            .and()
                .formLogin()
                .loginPage("/auth/signin") // GET
                .loginProcessingUrl("/auth/signin") // POST
                .defaultSuccessUrl("/")
            .and()
                .logout()
                .logoutSuccessUrl("/")
            .and()
                .oauth2Login() 
                .userInfoEndpoint() 
                .userService(customOAuth2DetailsService);

    }

}

1. @EnableWebSecurity

Spring Security 설정들을 활성화시켜주는 어노테이션

 

2. authorizeRequests

  • URL별 권한 관리를 설정하는 옵션의 시작
  • authorizeRequests가 선언되어야 antMatchers 옵션을 사용할 수 있음

3. antMatchers

  • 권한 관리 대상을 지정하는 옵션
  • URL, HTTP 메서드별로 관리가 가능
  • .antMatchers("/","/user/**", "/image/**", "/follow/**","/comment/**","/api/**").authenticated()
    • ( ) 안으로 들어오면 우선전으로 인증이 되어야 전체 열람 권한을 줌 (로그인)
  • .anyRequest().permitAll()
    • 설정된 값들 이외의 URL로 들어오면 바로 전체 열람 권한을 줌

4. logout().logoutSuccessUrl("/")

로그아웃 기능 설정의 진입으로 로그아웃 성공 시 / 주소로 이동

 

 

 

 

새롭게 추가된 부분 

.and()
    .oauth2Login() 
    .userInfoEndpoint() 
    .userService(customOAuth2DetailsService);
  • .oauth2Login()
    • OAuth2 로그인 기능에 대한 여러 설정의 진입점
    • 일반적인 로그인 뿐만아니라 OAuth2 로그인도 할 것임을 명시
  • .userInfoEndpoint()
    • OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정을 담당
    • OAuth2 로그인을 성공 시 응답으로 회원정보 바로 보내 달라!
  • .userService(customOAuth2DeailsService)
    • 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체*를 등록
    • 그 구현체가 customOAuth2DeailsService
      • 후속 조치로 해당 소셜로그인으로 받은 회원정보의 응답을 처리하는 곳

 

 

customOAuth2DeailsService

해당 소셜 로그인 이후 가져온 사용자의 정보들을 기반으로 가입, 정보 수정, 세션 저장 등의 기능을 지원합니다. 

 

@RequiredArgsConstructor
@Service
public class CustomOAuth2DetailsService extends DefaultOAuth2UserService { 

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        
        Map<String, Object> userInfo = oAuth2User.getAttributes();
        
        String username = "google_"+(String) userInfo.get("sub");
        String password = bCryptPasswordEncoder.encode(UUID.randomUUID().toString());
        String email = (String) userInfo.get("email");
        String name = (String) userInfo.get("name");

        User userEntity = userRepository.findByUsername(username);

        if(userEntity == null){ 
            User user = User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .name(name)
                    .build();

            return new CustomUserDetails(userRepository.save(user)); 
        }else { 
            return new CustomUserDetails(userEntity);
        }
    }
}

 

1. extends DefaultOAuth2UserService

  • extends 한 이유는 SecurityConfig 파일에서 요구한 타입을 맞추기 위한 한 방법으로 선택한 것

 

2. 소셜 로그인을 통해 어떤 회원정보들이 넘어왔는지 확인

  • System.out.println(oAuth2User.getAttributes());

 

3. 구글 로그인으로 가입한 적이 없는 사용자는 새롭게 생성 후 DB에 넣어줘야 함 (필수값만 세팅)

String username = "google_"+(String) userInfo.get("sub");
String password = bCryptPasswordEncoder.encode(UUID.randomUUID().toString());
String email = (String) userInfo.get("email");
String name = (String) userInfo.get("name");


    

 

 

CustomUserDetails에 OAuth2 로그인 한 사용자도 바로 세션에 넣을 수 있도록 하는 게 추후 유지보수하기 쉽습니다. 그러려면 기존 CustomUserDetails에 추가할 것들이 있습니다. 

CustomUserDetails 

@Data
public class CustomUserDetails implements UserDetails, OAuth2User {

    private User user;
    private Map<String, Object> attributes;

    // 일반 시큐리티 로그인 시 사용 (생성자)
    public CustomUserDetails(User user) {
        this.user = user;
    }

    // OAuth2.0 로그인 시 사용 (생성자)
    public CustomUserDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    public User getUser() {
        return user;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(()-> "ROLE_"+user.getRole().toString());
        return collection;
    }

    @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;
    }


    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
}
  • public class CustomUserDetails implements UserDetails, OAuth2User 추가
  • OAuth2.0 로그인 생성자, 구글 인증 후 응답받는 회원정보, 이름을 @Override해줍니다. 

 

 

확인

에러 문구: The dependencies of some of the beans in the application context form a cycle:

원인 : DI의 사이클 문제

 

 

해결 : 

어노테이션을 보고 IoC로 등록하는데 비밀번호의 경우 CustomOAuth2DetailsService 보다 SecurityConfig가 늦게 떠서 발생한 문제입니다. 

왜? SecurityConfig에 BCryptPasswordEncoder을 빈으로 등록함

 

 

 

다시 확인

첫 화면입니다. 예쁘게 꾸미고 싶었는데 이게 한계더군요...

 

구글로 로그인하기 버튼을 클릭합니다.

그럼 바로 http://localhost:8080/image/feed로 진입합니다. 

 

 

 

나의 페이지 profile로 이동하면 제 구글 닉네임이 뜨는 것을 확인할 수 있습니다. 

 

 

그럼 회원변경 페이지로 들어가서 나의 구글 정보 중 어떤 것들을 가져왔는지 확인해 보겠습니다.

 

맨 위에 있는 id는 UUID로 만들었습니다. (거의) 절대 중복될 수 없는 수의 조합이죠, 이름과 이메일은 실제 저의 구글 닉네임과 메일입니다.

 

 

여기까지 회원가입과 로그인을 동시에 해보았습니다. 

 

그럼 다른 기능들도 다 잘 작동하는지 확인해 보겠습니다. 

 

 

 

 

[에러]

회원정보 변경페이지에서 profile페이지로 가는 아이콘 클릭 시 에러

 

에러 문구

DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "null"]

 

 

상황 파악 및 해결

update.html에 갈 때 일반적으로 로그인한 유저의 세션 정보만 들고 갔기 때문입니다. 이제부턴 소셜로그인 한 유저의 정보도 들고 가야 하므로 해당 Controller에 코드 한 줄을 추가했습니다. 

@GetMapping("/user/{id}/update")
public String update(@PathVariable int id, @AuthenticationPrincipal CustomUserDetails customUserDetails, Model model) {
    model.addAttribute("sessionUser", customUserDetails.getUser());
    model.addAttribute("socialSessionUser", customUserDetails.getAttributes());
    return "user/update";
}

 

 

 

 

 

 

 

여기까지 구글 로그인을 적용해 보았고 구글 소셜 로그인으로 push 했습니다. ← 삭제함*

다음 포스팅은 네이버 로그인을 바로 적용하기 전에 리팩토링을 진행해서 계속해서 소셜 로그인을 추가 변경 삭제할 때 조금 더 편리하게 코드를 수정해 보겠습니다. 

 

 

 

❗❗ application-oauth.yml은 공유되면 안 되는 id와 secret이 기재되어 있습니다. 
따라서 .gitignore에 추가해야 하는데요. 처음에는 해당 gitignore가 정상 작동되지 않아서 해당 파일이 git에 올라갔었습니다. 

OH MY GOD...


그래서 깃의 캐시를 삭제하고 다시 올렸습니다. 해당 방법이 궁금하시면 포스팅을 눌러주세요👏

 

 

 

 

 

 

+ 추가

커밋 히스토리를 확인하다 Credential 정보가 포함된 것을 확인했습니다.

git rebase -i옵션을 이용해서 관련 히스토리를 삭제했습니다.

git 특정 커밋 삭제에 관심있으시면 이 포스팅을 참고바랍니다😎

 

 

+ 추가

구글 소셜 로그인으로 push 했습니다.

 


 

 

참고 :

스프링 부트와 AWS로 혼자 구현하는 웹 서비스

인프런 스프링부트 시큐리티 & JWT 강의