그동안 프로젝트를 여러번 시작하면서, 가장 중요하다고 생각하는 기능은 로그인 및 로그아웃 기능이였다.(필수이기 때문)
때문에, 포스팅 코드를 복붙하기만 한다면 기초 구현은 가능한 정도로 코드를 만들어 추후 프로젝트 신규 진행 시 유용하게 사용하고 싶은 마음에 포스팅 하게 되었다.
(카카오톡 계정 하나만 있으면 로그인, 로그아웃 가능한 서비스 완성!)
---
1️⃣ 사용 기술 스택
- Spring Boot 3.5.3
- Java 17
- Spring Security 6.x
- Thymeleaf
- Gradle
- Kakao Developers (REST API)
---
2️⃣ 목표
- 카카오 로그인 버튼 클릭 → Kakao 인증 → 사용자 정보 DB 및 세션 저장
- 로그인한 유저만 관리자 페이지(`/admin/index`) 접근 가능
- 로그아웃 시 카카오 로그아웃 + 세션/인증정보 모두 초기화
---
3️⃣ 🔧 의존성 추가 (`build.gradle`)
- 디펜던시는 아래정도 추가해 주었다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
4️⃣ 🔐 application.yml 설정
- console path 설정 필요
- kakao 의 client-id, client-secret 은 developers 의 앱을 참고하여 개인이 설정하면 된다.
server:
port: 12341
spring:
h2:
console:
enabled: true # H2 웹 콘솔을 사용하겠다는 의미
path: /test # 콘솔의 경로
settings:
web-allow-others: true
jpa:
show_sql: true
hibernate:
ddl-auto: none
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.H2Dialect
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:~/test
username: sa
password:
security:
oauth2:
client:
registration:
kakao:
client-id: [REST_API_KEY] # REST API key
client-secret: [ADMIN_KEY] # Admin key
authorization-grant-type: authorization_code
redirect-uri: http://localhost:12341/login/oauth2/code/kakao
client-authentication-method: client_secret_post
scope: profile_nickname,profile_image
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
kakao:
authorize_url: https://kauth.kakao.com/oauth/authorize
redirect_url: http://localhost:12341
🔑 Kakao Developers에서 애플리케이션 등록 후 Redirect URI는
http://localhost:12341/login/oauth2/code/kakao 로 설정해 주세요.
oauth 가 신기한게,, 로그인 그냥 href 로 저 URL 을 입력했더니 알아서 인증이 된다.
메인 화면에서는 /oauth2/autorization/kakao 로 입력해줘야한다.
5️⃣ Security 설정 (SecurityConfig.java)
- 사용자 정보 관련
- 로그인 성공 관련
- 로그인 실패 관련
- 권한 및 접근 제어 관련
기본 설정을 적용해볼 수 있는 자바 설정을 기록했다.
import lombok.RequiredArgsConstructor;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler;
private final CustomAuthExceptionHandler customAuthExceptionHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// H2 콘솔 사용을 위해 CSRF 비활성화
.csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화 (H2 콘솔 사용을 위해 필요)
.headers(headers ->
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 프레임 사용 허용
.authorizeHttpRequests(
authorize -> authorize
.requestMatchers("/admin/**").hasAnyRole("ADMIN") // 관리자 권한만 허용
.requestMatchers("/","/kakaoLogout").permitAll() // login page
.requestMatchers(PathRequest.toH2Console()).permitAll()
.anyRequest().authenticated()
);
http.oauth2Login(config -> config
.successHandler(customOAuth2SuccessHandler)
.failureHandler(customAuthExceptionHandler)
.userInfoEndpoint(endpointConfig -> endpointConfig
.userService(customOAuth2UserService)));
return http.build();
}
}
6️⃣ 사용자 정보 처리
✅ CustomOAuth2UserService.java
- 사용자 정보 처리 관련 자바 설정
- 제일 중요
import com.silencelog.silencelog.member.Member;
import com.silencelog.silencelog.member.MemberRepository;
import com.silencelog.silencelog.member.MemberStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@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("oAuth2User: {}", oauth2User);
final Map<String, Object> attributes = oauth2User.getAttributes();
final String oauthId = String.valueOf(attributes.get("id"));
final String oauthType = userRequest.getClientRegistration().getRegistrationId();
// 회원 조회 또는 생성
Member member = memberRepository.findByOAuthIdAndOAuthType(oauthId, oauthType)
.orElseGet(() -> createNewMember(attributes, oauthId, oauthType));
// 권한 설정
List<SimpleGrantedAuthority> authorities = getAuthorities(member);
return new DefaultOAuth2User(authorities, oauth2User.getAttributes(), "id");
}
private Member createNewMember(Map<String, Object> attributes, String oauthId, String oauthType) {
log.info("신규 회원 생성: oauthId={}, oauthType={}", oauthId, oauthType);
// OAuth 제공자별 정보 추출
String email = extractEmail(attributes, oauthType);
String name = extractName(attributes, oauthType);
String profileImage = extractProfileImage(attributes, oauthType);
Member newMember = Member.builder()
.OAuthId(oauthId)
.OAuthType(oauthType)
.memberStatus(MemberStatus.USER)
.createdAt(LocalDateTime.now())
.build();
return memberRepository.save(newMember);
}
private String extractEmail(Map<String, Object> attributes, String oauthType) {
switch (oauthType) {
case "kakao":
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
return kakaoAccount != null ? (String) kakaoAccount.get("email") : null;
case "google":
return (String) attributes.get("email");
case "naver":
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return response != null ? (String) response.get("email") : null;
default:
return null;
}
}
private String extractName(Map<String, Object> attributes, String oauthType) {
switch (oauthType) {
case "kakao":
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return properties != null ? (String) properties.get("nickname") : null;
case "google":
return (String) attributes.get("name");
case "naver":
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return response != null ? (String) response.get("name") : null;
default:
return null;
}
}
private String extractProfileImage(Map<String, Object> attributes, String oauthType) {
switch (oauthType) {
case "kakao":
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
return properties != null ? (String) properties.get("profile_image") : null;
case "google":
return (String) attributes.get("picture");
case "naver":
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return response != null ? (String) response.get("profile_image") : null;
default:
return null;
}
}
private List<SimpleGrantedAuthority> getAuthorities(Member member) {
switch (member.getMemberStatus()) {
case ADMIN:
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
case USER:
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
default:
return List.of(new SimpleGrantedAuthority("ROLE_GUEST"));
}
}
}
✅ CustomOAuth2SuccessHandler.java
- 성공 시 로직
- 뭐 커스텀으로 추가하고 싶으면 넣어도 된다. 여기서는 기본 리다이렉트만 시켜준다.
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@RequiredArgsConstructor
@Component
@Slf4j
public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/");
}
}
✅CustomAuthExceptionHandler.java
- 로그인 하다가 뭔가 에러가 난 경우
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class CustomAuthExceptionHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// 여기에 로그인 실패 후 처리할 내용을 작성하기!
log.info("무언가 에러가 발생했어요");
response.sendRedirect("/login-failure");
}
}
맴버 관련은 2편에서,,
'Dev > SpringBoot' 카테고리의 다른 글
| Spring Boot 3 kakao oauth2 Login 구현 - 2 (0) | 2025.07.15 |
|---|---|
| [Spring Boot] Controller에서 매개변수 넘겨받기 (0) | 2022.07.18 |
| JPA 사용해서 무한 계층 댓글 구현해보기 - 03 (0) | 2022.01.20 |
| JPA 사용해서 무한 계층 댓글 구현해보기 - 02 (0) | 2022.01.20 |
| JPA 사용해서 무한 계층 댓글 구현해보기 - 01 (0) | 2022.01.20 |