Backend
home

2025-6-26 (목)

생성일
2025/06/25 23:26
태그
JPA
Spring Security
소셜 로그인
Prometheus
Grafana

1. 테스트

JwtToken 테스트

•
Postman을 통해 JWT 토큰을 활용한 게시글 등록 테스트 진행
◦
먼저 로그인을 통해 JWT 토큰을 발급 받는다.
◦
발급받은 JWT 토큰을 활용하여 게시글 등록을 한다.
•
게시글 수정
•
게시글 상세 조회
•
게시글 삭제

2. 소셜 로그인 (Google, Kakao)

Config 수정

•
SecurityConfig
package org.example.backendproject.security.config; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.example.backendproject.oauth2.OAuth2LoginSuccessHandler; import org.example.backendproject.oauth2.OAuth2LogoutSuccessHandler; import org.example.backendproject.oauth2.OAuth2UserService; import org.example.backendproject.oauth2.RedisOAuth2AuthorizationRequestRepository; import org.example.backendproject.security.jwt.JwtTokenFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; 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.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration // 설정 클래스 등록 @EnableWebSecurity // 스프링 시큐리티 활성화 @RequiredArgsConstructor // 생성자 자동 생성 public class SecurityConfig { private final JwtTokenFilter jwtTokenFilter; private final OAuth2LogoutSuccessHandler oAuth2LogoutSuccessHandler; private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; private final OAuth2UserService oAuth2UserService; private final RedisTemplate<String, Object> redisTemplate; @Bean public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() { return new RedisOAuth2AuthorizationRequestRepository(redisTemplate); } // 스프링 시큐리티에서 어떤 순서로 어떤 보안 규칙의 필터를 거칠지를 정의하는 클래스 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // CSRF 보호 기능 비활성화 .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests((auth) -> auth .requestMatchers("/", "/index.html", "/*.html", "/favicon.ico", "/css/**", "/fetchWithAuth.js", "/js/**", "/images/**", "/.well-known/**" ).permitAll() // 정적 리소스 누구나 접근 .requestMatchers("/boards/**", "/api/comments/**").authenticated() //인증필요 .requestMatchers( "/api/auth/**", // 로그인/회원가입/로그아웃 등 인증 없이 사용 // "/api/comments/**", // 댓글 읽기 등 인증 없이 사용 "/oauth2/**", // 소셜 로그인 엔드포인트는 누구나 접근 "/login/**", // 스프링 시큐리티 내부 로그인 관련 엔드포인트 "/ws-gpt", "/ws-chat", // 웹소켓 핸드셰이크 "/actuator/prometheus" //프로메테우스 ).permitAll() // 웹소켓 핸드셰이크는 모두 허용! .requestMatchers( "/api/user/**", "/api/rooms/**" ).authenticated() //인증이 필요한 경로 ) // 인증 실패시 예외 처리 .exceptionHandling(e -> e // 인증 안 된 사용자가 접근하려고 할 때 .authenticationEntryPoint((request, response, authException) -> { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); }) // 인증은 되었지만 권한이 없을 때 .authenticationEntryPoint((request, response, authException) -> { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"); }) ) // 스프링 시큐리티에서 세션관리정책을 설정하는 부분 // 기본적으로 스프링 시큐리티는 세션을 생성함 // 하지만 JWT 기반 인증은 세션상태를 저장하지 않는 무상태(stateless) 방식 // 인증 정보를 세션에 저장하지 않고, 매 요청마다 토큰으로 인증 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) .oauth2Login(oauth2->oauth2 .loginPage("/index.html") .userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService)) .successHandler(oAuth2LoginSuccessHandler) .authorizationEndpoint(authorization -> authorization .authorizationRequestRepository(authorizationRequestRepository())) ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessHandler(oAuth2LogoutSuccessHandler) .permitAll() ) .build(); // 위 명시한 설정들을 적용 } // 회원가입시 비밀번호를 암호화해주는 메서드 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
Java
복사
•
RedisConfig
package org.example.backendproject.stompwebsocket.redis; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; //@Profile("!test") @RequiredArgsConstructor @Configuration public class RedisConfig { @Value("${spring.data.redis.host}") private String host; @Value("${spring.data.redis.port}") private int port; private final RedisSubscriber redisSubscriber; // .... 중략 ... // 추가 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); return template; } // ... 중략 ... }
Java
복사

소셜 로그인 코드 작성

•
AuthController - 일반 로그인 로직 수정 + 로그아웃 기능 추가
package org.example.backendproject.Auth.controller; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.backendproject.Auth.dto.LoginRequestDTO; import org.example.backendproject.Auth.dto.LoginResponseDTO; import org.example.backendproject.Auth.dto.SignUpRequestDTO; import org.example.backendproject.Auth.service.AuthService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController @RequestMapping("/api/auth") @Slf4j public class AuthController { private final AuthService authService; .... 중략 .... /** 로그인 **/ // @PostMapping("/loginSecurity") // public ResponseEntity<LoginResponseDTO> login(@RequestBody LoginRequestDTO loginRequestDTO) { // LoginResponseDTO loginResponseDTO = authService.login(loginRequestDTO); // return ResponseEntity.ok(loginResponseDTO); // } @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequestDTO loginRequestDTO) { LoginResponseDTO loginResponseDTO = authService.login(loginRequestDTO); return ResponseEntity.ok(loginResponseDTO); } .... 중략 .... @PostMapping("/logout") public ResponseEntity<?> logout(HttpServletResponse response) { // accessToken 쿠키 삭제 Cookie accessTokenCookie = new Cookie("accessToken", null); accessTokenCookie.setHttpOnly(true); accessTokenCookie.setPath("/"); accessTokenCookie.setMaxAge(0); // 즉시 만료! // refreshToken 쿠키 삭제 Cookie refreshTokenCookie = new Cookie("refreshToken", null); refreshTokenCookie.setHttpOnly(true); refreshTokenCookie.setPath("/"); refreshTokenCookie.setMaxAge(0); // 응답에 쿠키 삭제 포함 response.addCookie(accessTokenCookie); response.addCookie(refreshTokenCookie); // (추가) 서버 세션도 있다면 만료 // request.getSession().invalidate(); return ResponseEntity.ok().body("로그아웃 완료 (쿠키 삭제됨)"); } }
Java
복사
•
AuthRepository - Auth 엔티티에 토큰 저장
package org.example.backendproject.Auth.repository; import java.util.Optional; import org.example.backendproject.Auth.entity.Auth; import org.example.backendproject.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface AuthRepository extends JpaRepository<Auth, Long> { Optional<Auth> findByRefreshToken(String refreshToken); boolean existsByUser(User user); // Auth 엔티티에 토큰 저장 (User와 1:1 매핑) Optional<Auth> findByUser(User user); }
Java
복사
•
OAuth2LoginSuccessHandler
package org.example.backendproject.oauth2; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; 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.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; @Slf4j @Component @RequiredArgsConstructor public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { // 로그인 동작을 커스텀으로 구현하고 싶을 때 사용하는 인터페이스 // OAuth2 로그인 성공시 호출되는 메서드 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal(); Map<String, Object> attributes = oAuth2User.getAttributes(); String accessToken = (String) attributes.get("accessToken"); String refreshToken = (String) attributes.get("refreshToken"); String name = (String) attributes.get("name"); System.out.println("[OAuth2_LOG]" + "소셯 로그인 시도한 이름 = "+name); // 사용자 ID를 안전하게 꺼내기 (null 체크 및 타입 캐스팅) Long id = null; Object idObj = attributes.get("id"); if (idObj != null) { // Long 타입이 아닐 수도 있으니 안전하게 변환 id = Long.valueOf(idObj.toString()); } //토큰 전달방식 // 또는, 보안을 강화하려면 아래처럼 HttpOnly 쿠키로 전달해도 됨 Cookie accessTokenCookie = new Cookie("accessToken", accessToken); accessTokenCookie.setHttpOnly(true); accessTokenCookie.setPath("/"); // accessTokenCookie.setMaxAge(60 * 3); // 3분짜리 임시쿠키 response.addCookie(accessTokenCookie); Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken); refreshTokenCookie.setHttpOnly(true); refreshTokenCookie.setPath("/"); // refreshTokenCookie.setMaxAge(60 * 60 * 24); // 1일짜리 response.addCookie(refreshTokenCookie); response.sendRedirect("/main.html?" + "&id=" + id); // response.sendRedirect("http://localhost:3000/main" + (id != null ? "?id=" + id : ""));//리액트 } }
Java
복사
•
OAuth2LogoutSuccessHandler
package org.example.backendproject.oauth2; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; @Component public class OAuth2LogoutSuccessHandler implements LogoutSuccessHandler { // 카카오 REST API 키 (환경변수나 properties에서 가져오세요) private final String kakaoClientId = "kakaoClientId"; // 카카오 개발자 사이트에 있는 ClientID 추가 private final String kakaoLogoutRedirectUri = "http://localhost:8080/login.html"; // 앱 환경에 맞게 변경 @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 기본 리디렉션 URL → 일반 로그아웃 시 index.html로 이동 String redirectUrl = "/index.html"; if (authentication != null && authentication.getPrincipal() instanceof DefaultOAuth2User auth2User){ Map<String,Object> attributes = auth2User.getAttributes(); Object email = attributes.get("email"); if (email != null && email.toString().endsWith("@gmail.com")){ System.out.println("구글 로그아웃입니다."); redirectUrl = "https://accounts.google.com/Logout"; } // 카카오 로그인 사용자인 경우 (attributes에 'id' 키가 있음) else if (attributes.containsKey("id")){ System.out.println("카카오 로그아웃입니다."); redirectUrl = "https://kauth.kakao.com/oauth/logout?client_id=" + kakaoClientId + "&logout_redirect_uri=" + kakaoLogoutRedirectUri; } } // 최종적으로 redirectUrl로 리디렉트 response.sendRedirect(redirectUrl); } }
Java
복사
•
OAuth2UserService
package org.example.backendproject.oauth2; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.backendproject.Auth.entity.Auth; import org.example.backendproject.Auth.repository.AuthRepository; import org.example.backendproject.security.core.CustomUserDetails; import org.example.backendproject.security.core.Role; import org.example.backendproject.security.jwt.JwtTokenProvider; import org.example.backendproject.user.entity.User; import org.example.backendproject.user.entity.UserProfile; import org.example.backendproject.user.repository.UserRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; 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.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @Service @Slf4j @RequiredArgsConstructor public class OAuth2UserService extends DefaultOAuth2UserService { // DefaultOAuth2UserService - spring security에서 기본적으로 제공하는 OAuth2 인증 정보를 처리하는 클래스 private final AuthRepository authRepository; // JWT 저장용 (access, refresh) private final UserRepository userRepository; // 회원 정보 DB 접근 private final JwtTokenProvider jwtTokenProvider; // JWT 발급기 @Value("${jwt.accessTokenExpirationTime}") private Long jwtAccessTokenExpirationTime; @Value("${jwt.refreshTokenExpirationTime}") private Long jwtRefreshTokenExpirationTime; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { /** 소셜 로그인 성공 시 사용자 정보를 구글이나 카카오에서 받아서 처리하는 메소드 **/ // 기본 OAuth2User 정보 가져오기 (email, name 등) OAuth2User oAuth2User = super.loadUser(userRequest); // 어떤 소셜 로그인 제공자인지 (google, kakao 등) String provider = userRequest.getClientRegistration().getRegistrationId(); // 소셜 사용자 정보 추출 Map<String, Object> attributes = oAuth2User.getAttributes(); String userid, username, email; // provider 별로 파싱 방식이 다름 if ("google".equals(provider)) { email = (String) attributes.get("email"); // 구글의 고유 키 userid = email; // 사용자 아이디로 email 사용 username = (String) attributes.get("name"); } else if ("kakao".equals(provider)) { userid = attributes.get("id").toString()+"@kakao"; // 고유 id + 구분자 Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account"); email = (String) kakaoAccount.get("email"); Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile"); username = (String) profile.get("nickname"); } else { // 기타 provider 처리 안함 email = null; username = null; userid = null; } System.out.println(provider + " 로그인 확인 userid = " + userid); System.out.println(provider + " 로그인 확인 email = " + email); System.out.println(provider + " 로그인 확인 username = " + username); // 회원 정보가 DB에 존재하는지 확인 User user = userRepository.findByUserid(userid) .orElseGet(() -> { // 회원이 없다면 자동 회원가입 처리 User newUser = new User(); newUser.setUserid(userid); newUser.setPassword(""); // 소셜 로그인은 비밀번호 없음 newUser.setRole(Role.ROLE_USER); // 기본 권한 설정 // 프로필 엔티티 생성 및 양방향 관계 설정 UserProfile profile = new UserProfile(); profile.setUsername(username != null ? username : "소셜유저"); profile.setEmail(email != null ? email : ""); profile.setPhone(""); profile.setAddress(""); profile.setUser(newUser); // 주인 쪽 설정 newUser.setUserProfile(profile); // 양방향 설정 return userRepository.save(newUser); // 회원가입 저장 }); // 시큐리티에서 사용할 인증 객체 생성 CustomUserDetails customUserDetails = new CustomUserDetails(user); Authentication authentication = new UsernamePasswordAuthenticationToken( customUserDetails, user.getPassword(), Collections.singleton(new SimpleGrantedAuthority(user.getRole().name()))); // JWT 액세스 & 리프레시 토큰 발급 String accessToken = jwtTokenProvider.generateToken(authentication,jwtAccessTokenExpirationTime); String refreshToken = jwtTokenProvider.generateToken(authentication,jwtRefreshTokenExpirationTime); log.info("accessToken = " + accessToken); log.info("refreshToken = " + refreshToken); // JWT는 SuccessHandler에서 쿠키/쿼리로 전달 → 여기선 속성에만 담아 둠 Map<String, Object> customAttributes = new HashMap<>(attributes); customAttributes.put("accessToken", accessToken); customAttributes.put("refreshToken", refreshToken); customAttributes.put("id", user.getId()); // ← PK(id) 추가 // Auth 엔티티에 토큰 저장 (User와 1:1 매핑) Optional<Auth> optionalAuth = authRepository.findByUser(user); if (optionalAuth.isPresent()) { Auth auth = optionalAuth.get(); auth.updateAccessToken(accessToken); auth.updateRefreshToken(refreshToken); authRepository.save(auth); // 반드시 저장! user.setAuth(auth); // (option) 연관관계 유지 } else { // Auth auth = new Auth(user, refreshToken, accessToken, "Bearer"); Auth auth = new Auth(user, refreshToken, accessToken, "Bearer"); authRepository.save(auth); user.setAuth(auth); } // 최종적으로 Spring Security에 전달할 OAuth2User 반환 return new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority(user.getRole().name())), // 권한 customAttributes, // 속성 정보 (JWT 포함) "id" // PK로 사용할 식별자 (프론트에서도 사용할 수 있음) ); } }
Java
복사
•
RedisOAuth2AuthorizationRequestRepository
package org.example.backendproject.oauth2; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import java.time.Duration; @RequiredArgsConstructor public class RedisOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> { // Redis에 저장할 때 사용할 key prefix (고유 식별자 역할) private static final String PREFIX = "oauth2_auth_request:"; // RedisTemplate은 Spring에서 제공하는 Redis 클라이언트 private final RedisTemplate<String, Object> redisTemplate; // public RedisOAuth2AuthorizationRequestRepository(RedisTemplate<String, Object> redisTemplate) { // this.redisTemplate = redisTemplate; // } @Override public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { // 요청 파라미터에서 state 값을 가져옴 (OAuth2의 CSRF 방지 토큰 역할) String state = request.getParameter("state"); if (state == null) return null; // Redis에서 해당 state 값을 가진 AuthorizationRequest 조회 return (OAuth2AuthorizationRequest) redisTemplate.opsForValue().get(PREFIX + state); } @Override public void saveAuthorizationRequest( OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { // null일 경우 저장하지 않음 if (authorizationRequest == null) return; // 고유 식별자인 state 값을 키로 사용 String state = authorizationRequest.getState(); // Redis에 10분동안 저장(10분 뒤 삭제됨) redisTemplate.opsForValue().set( PREFIX + state, authorizationRequest, Duration.ofMinutes(10)); } /** * [3] 인가 요청 삭제하기 * 인증 과정이 끝나면 Redis에서 해당 요청 정보를 제거함 */ @Override public OAuth2AuthorizationRequest removeAuthorizationRequest( HttpServletRequest request, HttpServletResponse response) { String state = request.getParameter("state"); if (state == null) return null; String key = PREFIX + state; // Redis에서 해당 요청을 가져오고 OAuth2AuthorizationRequest authRequest = (OAuth2AuthorizationRequest) redisTemplate.opsForValue().get(key); // 가져온 후 삭제 (한 번 사용 후 만료되므로) redisTemplate.delete(key); return authRequest; } }
Java
복사
== OAuth2 소셜 로그인에서의 Redis 역할 ==
소셜 로그인을하면 세션이 발생하는데 세션은 하나의 서버에서만 유효한거라 서버 분산 환경에서는 세션이 공유가 되지 않아서 로그인 유지가 안 됨. 그러므로 레디스에 정보를 저장해서 서버들이랑 공유함.
•
JWtTokenFilter
package org.example.backendproject.security.jwt; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.backendproject.security.core.CustomUserDetailsService; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @Component @RequiredArgsConstructor @Slf4j public class JwtTokenFilter extends OncePerRequestFilter { // JwtTokenFilter 모든 HTTP 요청을 가로채서 JWT 토큰을 검사하는 필터 역할 // OncePerRequestFilter는 한 요청당 딱 한 번만 실행되는 필터 역할 private final JwtTokenProvider jwtTokenProvider; private final CustomUserDetailsService customUserDetailsService; // HTTP 매 요청마다 호출 @Override protected void doFilterInternal(HttpServletRequest request, // http 요청 HttpServletResponse response, // http 응답 FilterChain filterChain ) throws ServletException, IOException { String accessToken = extractTokenFromRequest(request); // 이 부분 수정 if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) { UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(accessToken); // 토큰에서 사용자를 꺼내서 담은 사용자 인증 객체 authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // http 요청으로부터 부가 정보(ip, 세션 등)를 추출해서 사용자 인증 객체에 넣어줌 SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 토큰에서 사용자 인증정보를 조회해서 인증정보를 현재 스레드에 인증된 사용자로 등록 String url = request.getRequestURI().toString(); log.info("현재 들어온 HTTP 요청 = " + url); String method = request.getMethod(); // GET, POST, PUT ... log.info("HTTP 메소드 + method = " + method); } /** * CharacterEncodingFilter: 문자 인코딩 처리 * CorsFilter: CORS 정책 처리 * CsrfFilter: CSRF 보안 처리 * JWTTokenFilter: JWT 토큰 처리(핵심) * SecurityContextFilter: 인증/인가 정보 저장 * ExceptionFilter: 예외 처리 */ filterChain.doFilter(request, response); // JwtTokenFilter를 거치고 다음 필터로 넘어감 (이동...이동...이동) } // HTTP 요청 헤더에서 토큰을 추출하는 메서드 // public String getTokenFromRequest(HttpServletRequest request) { // // String token = null; // // String bearerToken = request.getHeader("Authorization"); // if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { // token = bearerToken.substring(7); // } // // return token; // } ... 중략 ... // 쿠키에서 accessToken을 추출하는 메서드 (추가!) private String extractTokenFromCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals("accessToken")) { return cookie.getValue(); } } } return null; } }
Java
복사

application.properties 에 google, kakao 관련 정보 추가

#google spring.security.oauth2.client.registration.google.client-id= spring.security.oauth2.client.registration.google.client-secret= #spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} spring.security.oauth2.client.registration.google.scope=profile, email spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code #kakao spring.security.oauth2.client.registration.kakao.client-id= spring.security.oauth2.client.registration.kakao.client-secret= spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post #spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} spring.security.oauth2.client.registration.kakao.scope=profile_nickname, profile_image spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize 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.provider.kakao.user-name-attribute=id
Java
복사

구글 로그인 테스트

•
구글 로그인 클릭
•
로그인 확인
•
로그아웃 후 메인페이지 복귀

카카오톡 로그인 테스트

•
로그인 하기
•
전체 동의하기 클릭 후 “동의하고 계속하기” 클릭
•
정상 동작 확인

3. 토큰을 제대로 불러오지 못하는 에러

JwtTokenFilter 쪽에 Cookie 쪽 토큰을 바라보는 메서드 추가 후 doFilterInternal 메서드에 적용

private String extractTokenFromRequest(HttpServletRequest request) { // 1. 쿠키에서 accessToken 확인 String cookieToken = extractTokenFromCookie(request); if (StringUtils.hasText(cookieToken)) { return cookieToken; } // 2. Authorization 헤더에서 토큰 확인 String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); // "Bearer " 제거 } return null; } // HTTP 매 요청마다 호출 @Override protected void doFilterInternal(HttpServletRequest request, // http 요청 HttpServletResponse response, // http 응답 FilterChain filterChain ) throws ServletException, IOException { String accessToken = extractTokenFromRequest(request); // 요청 헤더에서 토큰 추출 if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) { UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(accessToken); // 토큰에서 사용자를 꺼내서 담은 사용자 인증 객체 authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // http 요청으로부터 부가 정보(ip, 세션 등)를 추출해서 사용자 인증 객체에 넣어줌 SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 토큰에서 사용자 인증정보를 조회해서 인증정보를 현재 스레드에 인증된 사용자로 등록 String url = request.getRequestURI().toString(); log.info("현재 들어온 HTTP 요청 = " + url); String method = request.getMethod(); // GET, POST, PUT ... log.info("HTTP 메소드 + method = " + method); } /** * CharacterEncodingFilter: 문자 인코딩 처리 * CorsFilter: CORS 정책 처리 * CsrfFilter: CSRF 보안 처리 * JWTTokenFilter: JWT 토큰 처리(핵심) * SecurityContextFilter: 인증/인가 정보 저장 * ExceptionFilter: 예외 처리 */ filterChain.doFilter(request, response); // JwtTokenFilter를 거치고 다음 필터로 넘어감 (이동...이동...이동) }
Java
복사

4. 모니터링 실행

build.gradle 설정

// Spring Boot Actuator 매트릭/모니터링 implementation 'org.springframework.boot:spring-boot-starter-actuator' // 프로메테우스 implementation 'io.micrometer:micrometer-registry-prometheus'
Shell
복사

application-properties 설정

# actuator and metric and prometheus # prometheus 전용 앤드포인트 생성 management.prometheus.metrics.export.enabled=true management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always # never -> 절대 상세 정보 제공 안함 (항상 status만 응답) # when-authorized -> 인증된 사용자/로컬 요청에만 상세 정보 제공 (기본값, 권장) # always -> 항상 상세 정보 포함 (외부/내부 관계없이 상세 정보 노출, 개발·테스트용)
Shell
복사

Docker-compose yml 파일로 Grafana, Prometheus 세팅

services: prometheus: image: prom/prometheus:latest container_name: prometheus restart: unless-stopped ports: - "9090:9090" volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml # [설정파일] - ./volumes/prometheus:/prometheus # [데이터 볼륨] # depends_on: # - backend1 # - backend2 # - backend3 networks: - prod_server grafana: image: grafana/grafana:latest container_name: grafana restart: unless-stopped ports: - "3000:3000" depends_on: - prometheus volumes: - ./volumes/grafana:/var/lib/grafana # [데이터 볼륨] networks: - prod_server # # elasticsearch: # image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0 # container_name: elasticsearch # environment: # - node.name=es01 # - discovery.type=single-node # - xpack.security.enabled=false # ulimits: # memlock: # soft: -1 # hard: -1 # ports: # - "9200:9200" # - "9300:9300" # volumes: # - ./volumes/esdata:/usr/share/elasticsearch/data # networks: # - prod_server ## ## ## kibana: ## image: docker.elastic.co/kibana/kibana:8.12.0 ## container_name: kibana ## environment: ## - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ## - SERVER_SSL_ENABLED=false ## ports: ## - "5601:5601" ## volumes: ## - ./volumes/kibana-data:/usr/share/kibana/data ## depends_on: ## - elasticsearch ## networks: ## - prod_server ## ## logstash: ## image: docker.elastic.co/logstash/logstash:8.12.0 ## container_name: logstash ## ports: ## - "5044:5044" # For beats (optional) ## - "5000:5000" # TCP input ## volumes: ## - ./logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf #logstash 설정파일 ## - ./logstash/logstash.yml:/usr/share/logstash/config/logstash.yml:ro # <-- 이 줄 추가! ## - ../../logs:/logs #로그 볼륨 ## depends_on: ## - elasticsearch ## networks: ## - prod_server networks: prod_server: external: true #도커 자체 볼륨을 사용할떄 선언해야 함 #volumes: # volumes:
Shell
복사
•
docker-compose 실행

Docker Container 확인

•
•
admin/admin으로 로그인 후 비밀번호 설정 → 1234
•
localhost:9090 - 프로메테우스 접속
•
SpringBoot 서버 실행한 이후에 Status > Target Health 메뉴에서 다음 화면 확인
◦
SpringBoot 켜져있지 않으면 모두 Down 처리 되어있음
•
Data sources에서 다음과 같이 설정
•
대시보드 만들기 > Import a dashboard
•
만들어진 Dashboard 확인
•
Import dashboard에서 grafana.com/dashboards 클릭
•
spring 입력 > JVM 클릭 > Download JSON
•
다운받은 json 파일을 Import 하기
•
Import 후 다음과 같이 설정
•
대시보드 내용 확인

5. 오늘 푸시한 커밋리스트

날짜
커밋 메시지
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26
2025-06-26