
SecurityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
.....
.oauth2Login(oauth2Login -> oauth2Login
.loginPage("/login")
.successHandler(successHandler)
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.userService(customOAuth2UserService)
)
);
return http.build();
}
- 사용자 인증이 필요하면 자동으로
/login 으로 가게한다.
- 나 같은 경우 소셜 로그인 말고 일반 로그인도 할 수 있게 구현해놔서
.successHandler(successHandler)를 통해 소셜로그인이 성공하면 jwt 의 accessToken 과 refreshToken 을 발급 받고 홈 화면으로 리다이렉트 할 수 있게 구현했다.
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"))
.nickname((String) attributes.get("name") + "_google")
.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")
.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"))
.nickname((String) response.get("name") + "_naver")
.email((String) response.get("email"))
.build();
}
public Member toEntity() {
return Member.builder()
.email(email)
.password(password)
.provider(provider)
.nickname(nickname)
.memberRole(MemberRole.MEMBER)
.neighborhoodVerification(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 {
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("getAttributes : {}", oAuth2User.getAttributes());
String provider = userRequest.getClientRegistration().getRegistrationId();
log.info("provider : {}", provider);
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(provider, oAuth2User.getAttributes());
log.info("oAuth2UserInfo : {}", oAuth2UserInfo.toString());
Member member = memberRepository.findByEmail(oAuth2UserInfo.getEmail())
.orElseGet(() -> memberRepository.save(oAuth2UserInfo.toEntity()));
log.info("user : {}", member.toString());
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()));
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getId().toString();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
private Map<String, Object> attributes;
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return member.getNickname();
}
}
- 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;
private static final int REFRESH_TOKEN_EXPIRATION = 60 * 60 * 24 * 7;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
log.info("OAuth2 로그인 성공: {}", authentication.getPrincipal());
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String email = extractEmail(oAuth2User);
Member member = authenticationService.findMemberByEmail(email);
Authentication newAuthentication = new UsernamePasswordAuthenticationToken(
member.getEmail(),
member.getPassword(),
authentication.getAuthorities()
);
TokenDto tokenDto = tokenService.generateTokenDto(newAuthentication);
redisTokenService.setStringValue(String.valueOf(member.getId()), tokenDto.getRefreshToken(),
(long) REFRESH_TOKEN_EXPIRATION);
CookieUtils.addCookie(response, "accessToken", tokenDto.getAccessToken(), ACCESS_TOKEN_EXPIRATION);
CookieUtils.addCookie(response, "refreshToken", tokenDto.getRefreshToken(), REFRESH_TOKEN_EXPIRATION);
String targetUrl = "/";
log.info("리다이렉트 URL: {}", targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
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 토큰을 생성 및 저장하며, 이후 리다이렉트 설정까지 수행