[스프링] - 소셜 로그인 구현하기(2) (카카오,네이버,구글)

CodeByHan·2024년 12월 8일

스프링

목록 보기
10/33

SecurityConfig

 @Bean
 public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 
             .....
       
                // OAuth2 로그인 설정 추가
                .oauth2Login(oauth2Login -> oauth2Login
                        .loginPage("/login")
                        .successHandler(successHandler)
                        // 로그인 성공 시 사용자 정보 처리
                        .userInfoEndpoint(userInfoEndpoint ->
                                userInfoEndpoint.userService(customOAuth2UserService)
                        )
                );

        return http.build();
    }
  • 사용자 인증이 필요하면 자동으로 /login 으로 가게한다.
  • 나 같은 경우 소셜 로그인 말고 일반 로그인도 할 수 있게 구현해놔서 .successHandler(successHandler)를 통해 소셜로그인이 성공하면 jwt 의 accessTokenrefreshToken 을 발급 받고 홈 화면으로 리다이렉트 할 수 있게 구현했다.

OAuth2UserInfo

@Builder
@Getter
@ToString
@SuppressWarnings("unchecked")
public class OAuth2UserInfo {

    private String id;
    private String password;
    private String email;
    private String nickname;
    private String provider;

    public static OAuth2UserInfo of(String provider, Map<String, Object> attributes) {
        return switch (provider) {
            case "google" -> ofGoogle(attributes);
            case "kakao" -> ofKakao(attributes);
            case "naver" -> ofNaver(attributes);
            default -> throw new UnsupportedProviderException("Unsupported provider: " + provider);
        };
    }

    private static OAuth2UserInfo ofGoogle(Map<String, Object> attributes) {
        return OAuth2UserInfo.builder()
                .provider("google")
                .id("google_" + (String) attributes.get("sub"))
                .password((String) attributes.get("sub")) // 구글은 ID를 password로 사용
                .nickname((String) attributes.get("name") + "_google") // 닉네임에 provider 추가
                .email((String) attributes.get("email"))
                .build();
    }

    private static OAuth2UserInfo ofKakao(Map<String, Object> attributes) {
        Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");

        String email = (String) kakaoAccount.get("email");
        return OAuth2UserInfo.builder()
                .provider("kakao")
                .id("kakao_" + attributes.get("id").toString())
                .password(attributes.get("id").toString())
                .nickname((String) properties.get("nickname") + "_kakao") // 닉네임에 provider 추가
                .email(email)
                .build();
    }

    private static OAuth2UserInfo ofNaver(Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");

        return OAuth2UserInfo.builder()
                .provider("naver")
                .id("naver_" + (String) response.get("id"))
                .password((String) response.get("id")) // 네이버는 ID를 password로 사용
                .nickname((String) response.get("name") + "_naver") // 닉네임에 provider 추가
                .email((String) response.get("email"))
                .build();
    }

    public Member toEntity() {
        return Member.builder()
                .email(email)
                .password(password) // 소셜 로그인에서 제공하는 ID를 password로 사용
                .provider(provider)
                .nickname(nickname)
                .memberRole(MemberRole.MEMBER) // 기본 역할을 MEMBER로 설정
                .neighborhoodVerification(false) // 기본값: false
                .build();
    }

}
  • OAuth2(소셜 로그인 사용자)별로 응답데이터가 다르기 때문에 정보를 꺼내서 네이버,카카오,구글 사용자에 맞게 사용할 수 있도록 구성했다.
  • 각자 맞는 데이터를 바탕으로 Member 엔티티를 생성할 수 있게 구현했다.

CustomOAuth2UserService

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 1. OAuth2 로그인 유저 정보를 가져옴
        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info("getAttributes : {}", oAuth2User.getAttributes());

        // 2. provider : kakao, naver, google
        String provider = userRequest.getClientRegistration().getRegistrationId();
        log.info("provider : {}", provider);

        // 3. 필요한 정보를 provider에 따라 다르게 mapping
        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(provider, oAuth2User.getAttributes());
        log.info("oAuth2UserInfo : {}", oAuth2UserInfo.toString());

        // 4. oAuth2UserInfo가 저장되어 있는지 유저 정보 확인
        //    없으면 DB 저장 후 해당 유저를 저장
        //    있으면 해당 유저를 저장
        Member member = memberRepository.findByEmail(oAuth2UserInfo.getEmail())
                .orElseGet(() -> memberRepository.save(oAuth2UserInfo.toEntity()));
        log.info("user : {}", member.toString());

        // 5. UserDetails와 OAuth2User를 다중 상속한 CustomUserDetails
        return new CustomUserDetails(member, oAuth2User.getAttributes());
    }

}
  • 카카오,네이버,구글에게 인증 요청을 하고 OAuth2UserInfo 를 매핑한다음에 해당 Member가 DB에 없으면 DB에 저장하는 역할을 한다.

CustomUserDetails

@Builder
public class CustomUserDetails implements UserDetails, OAuth2User {

    private Member member;


    public CustomUserDetails(Member member) {
        this.member = member;
    }


    public CustomUserDetails(Member member, Map<String, Object> attributes) {
        this.member = member;
        this.attributes = attributes;
    }

    @Override
    public List<GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_" + member.getMemberRole().name())); // member에서 memberRole을 사용
        return authorities;
    }

    // getPassword: member에서 password를 반환
    @Override
    public String getPassword() {
        return member.getPassword();
    }

    // getUsername: member에서 id를 사용
    @Override
    public String getUsername() {
        return member.getId().toString(); // member의 id를 반환
    }

    // 계정이 만료 되었는지 (true: 만료X)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠겼는지 (true: 잠기지 않음)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 비밀번호가 만료되었는지 (true: 만료X)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정이 활성화(사용가능)인지 (true: 활성화)
    @Override
    public boolean isEnabled() {
        return true;
    }

    // OAuth2User 관련 메서드
    private Map<String, Object> attributes;

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

    @Override
    public String getName() {
        return member.getNickname(); // member의 nickname 반환
    }

}
  • pring Security에서 사용자 인증과 권한 관리를 위한 핵심 역할

@Component
@RequiredArgsConstructor
@Log4j2
@SuppressWarnings("unchecked")
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final TokenService tokenService;
    private final RedisTokenService redisTokenService;
    private final AuthenticationService authenticationService;

    private static final int ACCESS_TOKEN_EXPIRATION = 60 * 60; // 1시간
    private static final int REFRESH_TOKEN_EXPIRATION = 60 * 60 * 24 * 7; // 7일

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        log.info("OAuth2 로그인 성공: {}", authentication.getPrincipal());

        // 1. OAuth2User 정보 가져오기
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

        // 2. 이메일 추출
        String email = extractEmail(oAuth2User);

        // 3. Member 조회 또는 생성
        Member member = authenticationService.findMemberByEmail(email);

        // 4. UsernamePasswordAuthenticationToken 생성
        Authentication newAuthentication = new UsernamePasswordAuthenticationToken(
                member.getEmail(),
                member.getPassword(),
                authentication.getAuthorities()
        );

        // 5. JWT 토큰 생성
        TokenDto tokenDto = tokenService.generateTokenDto(newAuthentication);

        // 6. Refresh Token Redis에 저장
        redisTokenService.setStringValue(String.valueOf(member.getId()), tokenDto.getRefreshToken(),
                (long) REFRESH_TOKEN_EXPIRATION);

        // 7. Access Token과 Refresh Token을 쿠키에 저장
        CookieUtils.addCookie(response, "accessToken", tokenDto.getAccessToken(), ACCESS_TOKEN_EXPIRATION);
        CookieUtils.addCookie(response, "refreshToken", tokenDto.getRefreshToken(), REFRESH_TOKEN_EXPIRATION);

        // 8. 홈 페이지로 리다이렉트 설정
        String targetUrl = "/";  // 홈 페이지 URL
        log.info("리다이렉트 URL: {}", targetUrl);

        // 9. 리다이렉트 처리
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

    /**
     * OAuth2 제공자에 따라 이메일을 추출하는 메서드
     */
    private String extractEmail(OAuth2User oAuth2User) {
        String email = extractEmailFromKakao(oAuth2User);
        if (email == null) {
            email = extractEmailFromNaver(oAuth2User);
        }
        if (email == null) {
            email = extractGenericEmail(oAuth2User);
        }
        if (email == null) {
            throw new MemberNotFoundException(MemberErrorMessage.MEMBER_NOT_FOUND_EMAIL.getMessage());
        }
        return email;
    }

    private String extractEmailFromKakao(OAuth2User oAuth2User) {
        Map<String, Object> kakaoAccount = (Map<String, Object>) oAuth2User.getAttribute("kakao_account");
        return kakaoAccount != null ? (String) kakaoAccount.get("email") : null;
    }

    private String extractEmailFromNaver(OAuth2User oAuth2User) {
        Map<String, Object> naverResponse = (Map<String, Object>) oAuth2User.getAttribute("response");
        return naverResponse != null ? (String) naverResponse.get("email") : null;
    }

    private String extractGenericEmail(OAuth2User oAuth2User) {
        return oAuth2User.getAttribute("email");
    }

}
  • OAuth2 소셜 로그인 성공 후 처리 과정을 담당
  • 로그인한 사용자의 정보를 확인하고 JWT 토큰을 생성 및 저장하며, 이후 리다이렉트 설정까지 수행
profile
노력은 배신하지 않아 🔥

0개의 댓글