자바 스프링 부트 17일차 /b01Security 카카오 OAuth2 소셜 로그인 연동

2026. 4. 30. 21:11대우개발원 수업 내용/spring boot, framework

반응형

카카오 OAuth2 소셜 로그인 연동 및 사용자 맞춤형 인증 시스템 구축

1. 카카오 API 설정 및 OAuth2 클라이언트 환경 구축

  • 플랫폼 등록: Kakao Developers에서 애플리케이션을 생성하고, REST API 키 발급 및 리다이렉트 URI(.../code/kakao)를 등록하여 인증 기반을 마련함
  • 환경 설정: application.properties에 카카오 전용 Authorization/Token/UserInfo URI를 설정하고, 클라이언트 ID와 Secret을 입력하여 스프링 부트와 카카오 서버를 연결함
  • 의존성 및 활성화: spring-security-oauth2-client 라이브러리를 추가하고, CustomSecurityConfig에서 oauth2Login 기능을 활성화함

2. CustomOAuth2UserService를 이용한 소셜 데이터 파싱

  • 사용자 로딩 커스텀: DefaultOAuth2UserService를 상속받아 loadUser 메서드를 재정의하고, 카카오에서 넘어오는 복잡한 JSON 구조(kakao_account)에서 이메일 정보를 추출함
  • 로그 분석: paramMap에 담긴 소셜 속성들을 전수 조사하여, 시스템 내 사용자를 식별하기 위한 핵심 키 값을 확보함

3. 일반/소셜 통합 인증 객체(MemberSecurityDTO) 확장

  • 멀티 인터페이스 구현: 기존 User 클래스 상속에 OAuth2User 인터페이스 구현을 추가하여, 하나의 DTO로 일반 DB 로그인과 소셜 로그인을 모두 처리할 수 있게 통합함
  • 속성 저장소(props): 소셜 서비스가 제공하는 닉네임, 프로필 등 다양한 추가 정보를 보관하기 위해 Map 타입의 필드를 정의하고 이를 세션에 유지함

4. 소셜 회원 자동 가입 및 DB 연동 로직

  • 이메일 기반 검증: 소셜 로그인 시 전달받은 이메일이 DB에 있는지 확인하여, 신규 방문자일 경우 social=true 속성과 임시 비밀번호("1111")를 부여해 자동으로 회원가입을 진행함
  • 계정 통합 지원: 이미 가입된 회원은 기존 정보를 불러오고, 신규 회원은 DB에 영속화하여 이후 게시판 작성자(Writer) 등에 식별자로 활용되도록 설계함

5. 로그인 성공 핸들러(CustomSocialLoginSuccessHandler)를 통한 보안 강화

  • 비밀번호 변경 유도: 소셜 가입 초기 비밀번호가 "1111"인 사용자를 감지하여, 로그인 성공 후 메인 페이지 대신 비밀번호 수정 페이지(/member/modify)로 강제 리다이렉트 시킴
  • 맞춤형 경로 제어: 이미 비밀번호를 변경한 안전한 사용자는 게시판 목록으로 정상 안내하며, AuthenticationSuccessHandler를 빈으로 등록하여 유연한 인증 사후 처리를 구현함

[요약]

카카오 OAuth2 소셜 로그인 연동 및 사용자 맞춤형 인증 시스템 구축

1. 카카오 Developers API 연동 및 OAuth2 클라이언트 자동 설정 적용

2. CustomOAuth2UserService 구현을 통한 카카오 계정 이메일 추출 및 데이터 분석

3. OAuth2User 인터페이스 확장을 통한 일반/소셜 통합 인증 DTO 설계

4. 소셜 로그인 시 신규 회원 자동 가입 및 기존 계정 연동 프로세스 구축

5. 성공 핸들러를 이용한 초기 비밀번호 사용자 대상 비밀번호 변경(Modify) 강제 유도


 

카카오 API 연동해서 로그인하기

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

에 접속 후

로그인 앱 > 앱 생성 > SpinrgSecurity라는 이름의 앱 만들기

앱이 생성된것을 확인 > 비지니스 인

앱 > 플랫폼 키 > REST API 키 수정 > 

 

카카오 로그인 리다이렉트 URI
카카오 로그인을 사용할 때 필요한 OAuth 리다이렉트 URI를 등록합니다.
http://localhost:8080/login/oauth2/code/kakao 입력 후 저장

 


카카오 로그인 일반 > 사용설정 상태 on

카카오로그인 동의항목 > 이메일 카카오 계정 설정 동의


연동 후

4개 코드에 코드를 추가하거나 수정

application.properties

더보기
spring.application.name=b01

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/webdbplus
spring.datasource.username=webuser
spring.datasource.password=1234

logging.level.org.springframework=info
logging.level.com.example=debug
logging.level.org.springframework.security=trace 

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=C:\\upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB
file.upload.path=C:\\upload

spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me

spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.redirect_uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.client-id=3f488b73cfd44597f835e8f95f316be1


spring.security.oauth2.client.registration.kakao.client-secret=8HZmeEgPJjGdO2K0JAMP2Kj9uqDVGoHL
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email

build.gradle

implementation 'org.springframework.security:spring-security-oauth2-client'

CustomSecurityConfig

더보기
package com.example.b01.config;

import com.example.b01.security.CustomUserDetailsService;

import com.example.b01.security.handler.Custom403Handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

import static org.springframework.security.config.Customizer.withDefaults;

@Log4j2
@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true)
public class    CustomSecurityConfig {
    private final DataSource dataSource;
    private final CustomUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        log.info("------------configure-------------------");
//        http.formLogin(withDefaults());
        http.formLogin( httpSecurityFormLoginConfigurer -> {
            httpSecurityFormLoginConfigurer.loginPage("/member/login");
        });
        http.csrf(httpSecurityCsrfConfigurer -> {
            httpSecurityCsrfConfigurer.disable();
        });
        http.rememberMe(httpSecurityRememberMeConfigurer -> {
            httpSecurityRememberMeConfigurer.key("12345678") //remember-me 쿠키를 암호화/검증할 때 사용하는 키
                    .tokenRepository(persistentTokenRepository()) // remember-me 토큰을 DB에 저장하는 방식
                    .userDetailsService(userDetailsService) // 자동 로그인 시 사용자 정보를 조회할 서비스
                    .tokenValiditySeconds(60*60*24*30); // 자동 로그인 유지 시간 설정. 30일
        });
        http.exceptionHandling( httpSecurityExceptionHandlingConfigurer -> {
            httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler());
        });
        http.oauth2Login( httpSecurityOauth2LoginConfigurer -> {
            httpSecurityOauth2LoginConfigurer.loginPage("/member/login");
        });


        return http.build();
    }
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        log.info("------------web configure-------------------");
        return (web) -> web.ignoring().requestMatchers(PathRequest.
                toStaticResources().atCommonLocations());
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
        repo.setDataSource(dataSource);
        return repo;
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new Custom403Handler();
    }

}

login.html

더보기
<div>
    <a href="/oauth2/authorization/kakao">KAKAO</a>
</div>

CustomOAuth2UserService 클래스를 생성

코드는

스프링 시큐리티의 기본 소셜 사용자 로딩 기능을 상속받아, 
소셜 로그인 시도 시 전달되는 요청 정보를 가로채고 로그로 확인할 수 있도록 커스터마이징한 서비스 로직임.

더보기
  1. 소셜 서비스 확장: DefaultOAuth2UserService를 상속받아 소셜 로그인 처리 프로세스에
    개발자가 개입할 수 있는 구조를 마련함.
  2. 요청 데이터 수집: loadUser 메서드의 파라미터인 OAuth2UserRequest를 통해
    현재 어떤 소셜 서비스(카카오, 구글 등)를 통해 인증이 들어오는지 정보를 확보함.
  3. 디버깅 및 모니터링: log.info를 사용하여 소셜 서버에서 넘어오는 액세스 토큰 및 클라이언트 설정 정보
    등을 로그로 출력하여 연동 상태를 점검함.
  4. 기본 로직 유지: 별도의 데이터 변환 없이 super.loadUser(userRequest)를 반환함으로써,
    시큐리티가 제공하는 표준적인 소셜 사용자 정보 로드 기능을 그대로 수행하도록 함.
package com.example.b01.security;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.parameters.P;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Map;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        log.info("userRequest.......");
        log.info(userRequest);
      	return super.loadUser(userRequest);
    }
}

 

실행해서 카카오 로그인 후 로그를 확인하면

userRequest.......가 보임


CustomSecurityConfig 코드 수정

소셜 로그인 인증 과정에서 외부 서비스로부터 전달된 사용자 프로필 정보를 로드하고,

그 구조를 로그로 상세히 확인하는 전처리 로직임.

더보기
  1. 클라이언트 식별: userRequest에서 ClientRegistration 정보를 추출하여 현재 로그인을 시도한
    서비스(카카오, 구글 등)가 무엇인지 clientName으로 확인함.
  2. 소셜 사용자 데이터 로드: 부모 클래스의 super.loadUser(userRequest)를 호출하여
    외부 인증 서버로부터 사용자 기본 정보가 담긴 OAuth2User 객체를 가져옴.
  3. 속성 정보 맵 변환: 소셜 서비스마다 제각각인 사용자 데이터(이메일, 닉네임, 프로필 사진 등)를
    키-값 형태의 Map 구조인 paramMap으로 추출함.
  4. 데이터 구조 전수 조사: forEach 반복문을 통해 소셜 서버가 보내준 모든 속성값들을 로그에 출력함으로써,
    실제 서비스 운영 시 어떤 키 값을 파싱해야 할지 분석하는 기초 작업을 수행함.
        log.info("OAuth2User--------------------------------------------");
        // 클라이언트 등록 정보를 가져옴
        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        // 클라이언트 이름을 가져옴. kakao Cacao
        String clientName = clientRegistration.getClientName();
        log.info("NAME : " + clientName);
        // OAuth2 사용자 정보를 로드
        OAuth2User oAuth2User = super.loadUser(userRequest);
        // 사용자 속성 정보를 맵 형태로 가져옴
        Map<String, Object> paramMap = oAuth2User.getAttributes();
        // 사용자 속성 정보 출력
        paramMap.forEach((k, v) -> {
            log.info("----------------------------------------------");
            log.info(k + ":" + v);
        });
        // 최종적으로 oAuth2User 객체 반환
        return oAuth2User;
//        log.info(userRequest);
//        return super.loadUser(userRequest);
    }

 

실행하면

이렇게 로그가 뜨게된다


CustomSecurityConfig

email 정보도 출력

소셜 서비스로부터 전달받은 사용자 정보 뭉치에서 특정 서비스(카카오)에 맞는 이메일 정보를 추출해내는 데이터 파싱 로직임.

더보기
  1. 클라이언트별 분기 처리: switch 문을 사용하여 로그인 요청을 보낸 소셜 서비스(clientName)가
    "kakao"인지 확인하고, 해당 서비스 전용 추출 메서드를 호출함.
  2. 계층형 구조 데이터 접근: 카카오가 제공하는 JSON 데이터 구조에 맞춰 kakao_account 키로 먼저 접근한 뒤,
    이를 다시 LinkedHashMap으로 형변환하여 실제 값에 접근함.
  3. 이메일 정보 획득: 최종적으로 맵 내부에서 email 키에 해당하는 문자열 값을 추출하여,
    시스템 내 사용자를 식별하거나 자동 가입시키는 용도로 활용함.
  4. 로그를 통한 데이터 검증: 추출 과정의 중간 데이터와 최종적으로 얻은 이메일 주소를 로그로 출력하여,
    소셜 API의 응답 규격이 변경되었는지 혹은 데이터가 정상인지 모니터링함.
           String email = null;
            switch (clientName) {
                case "kakao":
                    email = getKakaoEmail(paramMap);
                    break;
            }
            log.info("===============================");
            log.info(email);
            log.info("-------------------------------");
            return oAuth2User;
//        return super.loadUser(userRequest);
    }
    private String getKakaoEmail(Map<String, Object> paramMap) {
        log.info("KAKAO---------------------------------------------------");
        Object value = paramMap.get("kakao_account");
        log.info(value);
        LinkedHashMap accountMap = (LinkedHashMap) value;
        String email = (String) accountMap.get("email");
        log.info("email........." + email);
        return email;
    }

실행하면

이메일 정보도 나오게 된다


이메일을 체크해서 가입이 되어 있지 않은 회원이면 회원가입을 시키고 social는 true가 된다.

현재 일반회원으로 가입되어 있는 사람은 social이 이미 fasle이다

더보기

이상한 점을 보완하여 3가지 케이스로 정리해 보겠습니다.

1. 신규 방문자 (가입되지 않은 이메일)

  • 동작: 자동으로 회원가입을 진행합니다.
  • 상태: social = true로 저장합니다.
  • 결과: 로그인 성공.

2. 기존 소셜 회원 (이미 가입된 이메일 && social = true)

  • 동작: 추가 가입 없이 바로 로그인 처리합니다.
  • 결과: 로그인 성공.

3. 기존 일반 회원 (이미 가입된 이메일 && social = false) ⬅️ 이 부분이 중요!

  • 문제점: 동일한 이메일로 이미 아이디/패스워드 로그인을 사용하는 사용자가 소셜 로그인을 시도한 상황입니다.
  • 처리 방법:
    • 방법 A (연동): 기존 계정에 소셜 정보를 업데이트하고 social = true로 변경해 줍니다. (권장)
    • 방법 B (차단): "이미 일반 계정으로 가입된 이메일입니다. 일반 로그인을 이용해 주세요."라고 안내하며 로그인을 막습니다.

즉 = 무차별적인 공격을 방지(마구잡이로 회원가입하는것을 방지)

 

MemberRepository

코드에 아래 구문을 추가

더보기
@EntityGraph(attributePaths = "roleSet")
Optional<Member> findByEmail(String email);

MemberSecurityDTO

코드에 소셜 로그인 정보를 담는 필드를 추가하고

일반 로그인뿐만 아니라 소셜 로그인 정보까지 통합해서 처리할 수 있도록 OAuth2User 인터페이스를 구현한 확장 DTO 로직임.

더보기
  1. 멀티 로그인 인터페이스 구현: 기존의 User 클래스 상속 외에 OAuth2User 인터페이스를 추가로 구현하여,
    일반 로그인과 소셜 로그인 사용자 정보를 하나의 객체로 통합 관리함.
  2. 소셜 속성 저장소 추가: 카카오나 구글 등 소셜 서비스로부터 전달받은 모든 상세 데이터를 키-값 쌍 형태로
    보관하기 위해 props 필드(Map 타입)를 정의함.
  3. OAuth2 필수 메서드 오버라이드: getAttributes() 메서드가 props를 반환하게 하고,
    getName()이 사용자의 식별자인 mid를 반환하도록 설정하여 소셜 인증 규격을 충족함.
  4. 데이터 일관성 유지: 일반 DB 로그인 시에는 기본 필드들을 사용하고,
    소셜 로그인 시에는 추가된 props 필드를 활용함으로써 시스템 전반에서 동일한 사용자 객체 타입을 사용할 수 있게 함
package com.example.b01.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.parameters.P;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
@Getter
@Setter
@ToString

public class MemberSecurityDTO extends User implements OAuth2User {
    private String mid;
    private String mpw;
    private String email;
    private boolean del;
    private boolean social;
    private java.util.Map<String, Object> props;

    public MemberSecurityDTO(String username, String password, String email,
                             boolean del, boolean social,
                             Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.mid = username;
        this.mpw = password;
        this.email = email;
        this.del = del;
        this.social = social;
    }

    @Override
    public java.util.Map<String, Object> getAttributes() {
        return this.getProps();
    }
    @Override
    public String getName() {
        return this.mid;
    }
}

CustomOAuth2UserService

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

코드를 추가

소셜 로그인 시 전달받은 이메일 정보를 바탕으로 기존 회원은 정보를 불러오고,

신규 회원은 자동으로 DB에 등록하여 인증 객체를 생성하는 로직임.

더보기
  1. 이메일 기반 회원 확인: memberRepository.findByEmail을 호출하여 소셜 서비스로부터 받은 이메일이
    이미 우리 시스템 DB에 존재하는지 확인함.
  2. 신규 소셜 회원 자동 등록: DB에 정보가 없는 경우, 이메일을 아이디(mid)로 설정하고
    임시 비밀번호와 social=true 속성을 부여하여 새로운 Member 엔티티를 생성하고 저장함.
  3. 권한 및 속성 매핑: 신규 또는 기존 회원 모두에게 ROLE_USER 등의 권한을 부여하고,
    소셜 서비스에서 넘겨준 추가 정보(params)를 props 필드에 저장함.
  4. 통합 DTO 반환: 최종적으로 일반 로그인과 동일한 규격의 MemberSecurityDTO 객체를 생성하여 반환함으로써,
    이후 보안 프로세스가 일관되게 작동하도록 지원
private MemberSecurityDTO generateDTO(String email, Map<String, Object> params) {
        // 이메일을 기준으로 데이터베이스에서 회원 정보를 조회
        Optional<Member> result = memberRepository.findByEmail(email);
        // 데이터베이스에 해당 이메일을 가진 사용자가 없다면
        if (result.isEmpty()) {
            // 새로운 회원 추가(mid는 이메일 주소, 패스워드는 "1111"로 설정)
            Member member = Member.builder()
                    .mid(email) // 회원 ID로 이메일 사용
                    .mpw(passwordEncoder.encode("1111")) // 기본 패스워드 설정 (암호화)
                    .email(email) // 이메일 설정
                    .social(true) // 소셜 로그인 사용자로 설정
                    .build();
            // 기본 권한(ROLE_USER)부여
            member.addRole(MemberRole.USER);
            // 회원 정보 저장
            memberRepository.save(member);
//            MemberSecurityDTO 생성 및 반환
            MemberSecurityDTO memberSecurityDTO =
                    new MemberSecurityDTO(email, "1111", email, false, true,
                            Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
            // 추가 송성 설정
            memberSecurityDTO.setProps(params);
            return memberSecurityDTO;
            // 기존 회원 정보 가져오기 (수정된 블록)
        } else {
            Member member = result.get(); // Optional에서 객체 추출
            MemberSecurityDTO memberSecurityDTO =
                    new MemberSecurityDTO(
                            member.getMid(), // 회원 ID
                            member.getMpw(), // 암호화된 비밀번호
                            member.getEmail(), // 이메일
                            member.isDel(), // 삭제 여부
                            member.isSocial(), // 소셜 로그인 여부
                            member.getRoleSet().stream()
                                    .map(memberRole -> 
                                            new SimpleGrantedAuthority("ROLE_"
                                                    + memberRole.name())) // 권한 매핑
                                    .collect(Collectors.toList()) // 리스트로 변환
                    );
            memberSecurityDTO.setProps(params);
            return memberSecurityDTO;
        }

실행해서 저장되어있는 쿠키를 삭제하고 새로고침 > 카카오로 로그인

테이블이 social 부분에 true 맴버가 추가되고

이제는 맴버이기 떄문에 board/register가 접근 가능하고 Writer에 email로 등록이 된다

> 소셜 로그인 된 계정은 카카오 연동 로그인은 가능하지만 이메일 패스워드를 입력해서 로그인은 불가능하다.


handler 패키지 안에

CustomSocialLoginSuccessHandler 클래스 생성

소셜 로그인 성공 직후 사용자의 상태를 확인하여,

비밀번호 변경이 필요한 경우 특정 페이지로 강제 이동시키는 사용자 정의 핸들러 로직임.

더보기

 

  1. 인증 객체 정보 추출: 로그인에 성공한 사용자의 정보가 담긴 Authentication 객체에서
    MemberSecurityDTO를 꺼내 사용자의 소셜 가입 여부와 암호화된 비밀번호를 확인함.
  2. 비밀번호 변경 조건 검사: 사용자가 소셜 로그인(isSocial) 상태이면서 현재 비밀번호가 초기값인
    "1111"과 일치하는지 passwordEncoder.matches를 통해 검증함.
  3. 조건부 리다이렉트 실행: 초기 비밀번호를 그대로 사용 중인 위험 상태라면
    비밀번호 수정 페이지(/member/modify)로 강제 이동시켜 보안 강화를 유도함.
  4. 일반 로그인 프로세스 처리: 비밀번호가 이미 변경되었거나 일반적인 로그인 성공 상황이라면
    서비스의 메인 화면인 게시판 목록(/board/list)으로 사용자를 안내함.
package com.example.b01.security.handler;

import com.example.b01.security.dto.MemberSecurityDTO;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import java.io.IOException;
@Log4j2
@RequiredArgsConstructor

public class CustomSocialLoginSuccessHandler implements AuthenticationSuccessHandler {
    private final PasswordEncoder passwordEncoder;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("------------------------------------");
        log.info("CustomLoginSuccessHandler onAuthenticationSuccess.............");
        log.info(authentication.getPrincipal());
        MemberSecurityDTO memberSecurityDTO = (MemberSecurityDTO)  authentication.getPrincipal();
        String encodedPw = memberSecurityDTO.getMpw();
        // 소셜 로그인이고 회원의 패스워드가 1111이라면
        if(memberSecurityDTO.isSocial() && passwordEncoder.matches("1111", encodedPw)) {
            log.info("Should Change Password");
            log.info("Redirect to Member Modify");
            response.sendRedirect("/member/modify");
            return;
        } else {
            response.sendRedirect("/board/list");
        }
    }
}

CustomSecurityConfig 에 아래 코드 추가

사용자 정의 소셜 로그인 성공 핸들러를 스프링 빈(Bean)으로 등록하여,

시큐리티 설정에서 사용할 수 있도록 준비하는 설정 로직임.

더보기
  1. 객체 생성 및 의존성 주입: CustomSocialLoginSuccessHandler 객체를 생성하면서, 비밀번호 일치 여부 확인에 필요한 passwordEncoder()를 인자로 전달하여 주입함.
  2. 스프링 컨테이너 관리: @Bean 어노테이션을 통해 해당 메서드에서 반환되는 객체를 스프링 컨테이너에 등록하며, 이를 통해 애플리케이션 전역에서 싱글톤으로 관리되게 함.
  3. 시큐리티 설정 연동: 이렇게 정의된 빈은 SecurityConfig의 http.oauth2Login() 설정 부분에서 .successHandler(authenticationSuccessHandler())와 같이 호출되어 실제 인증 성공 시점의 동작을 제어하게 됨.
  4. 유연한 핸들러 교체: 로그인 성공 후의 복잡한 비즈니스 로직(비밀번호 변경 유도 등)을 별도의 클래스로 분리하고, 이를 빈으로 관리함으로써 유지보수와 코드 가독성을 높임.
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
    return new CustomSocialLoginSuccessHandler(passwordEncoder());
}

코드 수정

기존의 매핑되었던 부분을 주석처리 하고 변경

핸들러에서 경로를 처리하고 있기 때문

스프링 시큐리티 설정에서 OAuth2 소셜 로그인 기능을 활성화하고, 로그인 성공 시 실행될 커스텀 핸들러를 연결하는 구성 로직임.

더보기
  1. 소셜 로그인 기능 활성화: http.oauth2Login() 메서드를 호출하여 구글, 카카오 등 외부 서비스와의 인증 연동을 위한 보안 필터 체인을 가동함.
  2. 커스텀 성공 핸들러 연결: 소셜 인증이 완료된 후 실행될 로직(비밀번호 변경 유도 등)이 담긴 authenticationSuccessHandler()를 설정하여 인증 성공 시의 동작을 제어함.
  3. 람다식 기반 설정: httpSecurityOauth2LoginConfigurer를 인자로 받는 람다 표현식을 사용하여, 소셜 로그인과 관련된 세부 옵션들을 가독성 있게 정의함.
  4. 로그인 페이지 커스터마이징(선택): 주석 처리된 loginPage 설정을 통해 소셜 로그인 버튼이 포함된 별도의 커스텀 로그인 화면 경로를 지정하거나 기본 제공 화면을 사용하도록 선택할 수 있음.
        http.oauth2Login( httpSecurityOauth2LoginConfigurer -> {
//            httpSecurityOauth2LoginConfigurer.loginPage("/member/login");
            httpSecurityOauth2LoginConfigurer.successHandler(authenticationSuccessHandler());
        });

실행해서 소셜로그인으로 가입하면

 

modify로 /member/modify로 연결되는데 member modify.html은 아직 없어서 오류남

 


소셜로그인을 하면 비밀번호가 고정으로 1111로 되어있으니 비밀변호를 변경할 수 있게 만든다

MemberRepository

코드에 아래 구문을 추가

더보기
@Modifying
@Transactional
@Query("update Member m set m.mpw = :mpw where m.mid = :mid")
void updatePassword(@Param("mpw") String password, @Param("mid") String mid);

MemberRepositoryTests

코드에 내 소셜로그인을 하는 이메일을 넣어서 테스트

더보기
@Commit
@Test
public void testUpdate() {
    String mid ="tkddus4158@gmail.com"; // 소셜로그인으로 추가된 사용자로 현재 DB에 존재하는 이메일
    String mpw = passwordEncoder.encode("54321"); // 1111이라는 비밀번호에서 54321로 변경
    memberRepository.updatePassword(mpw, mid);
}

테스트 코드를 실행해보면

정상적으로 바뀌어서

다시 프로젝트를 실행해 보면

http://localhost:8080/board/list로 연결되는것을 확인