Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.project.growfit.domain.User.controller;

import com.project.growfit.domain.User.dto.response.ParentLoginResponseDto;
import com.project.growfit.global.auth.cookie.CookieService;
import com.project.growfit.global.auth.jwt.JwtProvider;
import com.project.growfit.global.redis.entity.TokenRedis;
import com.project.growfit.global.redis.repository.TokenRedisRepository;
Expand All @@ -24,6 +25,7 @@ public class TestAuthController {

private final JwtProvider jwtProvider;
private final TokenRedisRepository tokenRedisRepository;
private final CookieService cookieService;

@PostMapping("/generate-token")
@Operation(summary = "테스트 계정 토큰 변환 api")
Expand All @@ -32,7 +34,7 @@ public ResultResponse<?> generateToken(@RequestBody Map<String, String> request,
String newAccessToken = jwtProvider.createAccessToken(email, "ROLE_PARENT", "SOCIAL_KAKAO");
String newRefreshToken = jwtProvider.createRefreshToken(email);
tokenRedisRepository.save(new TokenRedis(email, newAccessToken, newRefreshToken));
jwtProvider.saveAccessTokenToCookie(response, newAccessToken);
cookieService.saveAccessTokenToCookie(response, newAccessToken);

log.info("로그인 성공: email={}, accessToken 저장 완료", email);
ParentLoginResponseDto dto = new ParentLoginResponseDto(email, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.project.growfit.domain.User.entity.Child;
import com.project.growfit.domain.User.repository.ChildRepository;
import com.project.growfit.domain.User.service.AuthChildService;
import com.project.growfit.global.auth.cookie.CookieService;
import com.project.growfit.global.auth.jwt.JwtProvider;
import com.project.growfit.global.auth.service.CustomAuthenticationProvider;
import com.project.growfit.global.exception.BusinessException;
Expand All @@ -32,6 +33,7 @@
public class AuthChildServiceImpl implements AuthChildService {
private final ChildRepository childRepository;
private final PasswordEncoder passwordEncoder;
private final CookieService cookieService;
private final JwtProvider jwtProvider;
private final TokenRedisRepository tokenRedisRepository;
private final CustomAuthenticationProvider authenticationProvider;
Expand All @@ -47,7 +49,7 @@ public ResultResponse<?> findByCode(String code) {

@Override
public ResultResponse<?> registerChildCredentials(Long child_id, AuthChildRequestDto request) {
log.info("[registerChildCredentials] 아이 계정 정보 등록 요청: child_id={}, child_login_id={}", child_id, request.childId());
log.debug("[registerChildCredentials] 아이 계정 정보 등록 요청: child_id={}, child_login_id={}", child_id, request.childId());
boolean isExists = childRepository.existsByLoginIdOrPassword(request.childId(), request.childPassword());

if (isExists) {
Expand Down Expand Up @@ -88,10 +90,10 @@ public ResultResponse<?> login(AuthChildRequestDto request, HttpServletResponse
String newRefreshToken = jwtProvider.createRefreshToken(child.getLoginId());

tokenRedisRepository.save(new TokenRedis(child.getLoginId(), newAccessToken, newRefreshToken));
log.info("[login] 새 AccessToken 및 RefreshToken 저장 완료: child_login_id={}", request.childId());
log.debug("[login] 새 AccessToken 및 RefreshToken 저장 완료: child_login_id={}", request.childId());

jwtProvider.saveAccessTokenToCookie(response, newAccessToken);
log.info("[login] AccessToken을 쿠키에 저장 완료: child_login_id={}", request.childId());
cookieService.saveAccessTokenToCookie(response, newAccessToken);
log.debug("[login] AccessToken을 쿠키에 저장 완료: child_login_id={}", request.childId());

return new ResultResponse<>(ResultCode.CHILD_LOGIN_SUCCESS, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.project.growfit.domain.User.entity.Parent;
import com.project.growfit.domain.User.repository.ParentRepository;
import com.project.growfit.domain.User.service.OauthService;
import com.project.growfit.global.auth.cookie.CookieService;
import com.project.growfit.global.auth.jwt.JwtProvider;
import com.project.growfit.global.exception.BusinessException;
import com.project.growfit.global.exception.ErrorCode;
Expand Down Expand Up @@ -37,6 +38,7 @@ public class OauthServiceImpl implements OauthService {

private final JwtProvider jwtProvider;
private final ParentRepository parentRepository;
private final CookieService cookieService;
private final RestTemplate restTemplate = new RestTemplate();
private final TokenRedisRepository tokenRedisRepository;

Expand Down Expand Up @@ -125,7 +127,7 @@ public ResultResponse<?> kakaoLogin(String accessToken, HttpServletResponse resp
isNewUser = true;
signUp(requestDto);
parentResponse = findByUserKakaoIdentifier(requestDto.id());
jwtProvider.saveEmailToCookie(response, email);
cookieService.saveEmailToCookie(response, email);
if (parentResponse == null) {
log.error("[kakaoLogin] 회원가입 후 사용자 정보 조회 실패: email={}", requestDto.email());
throw new BusinessException(ErrorCode.USER_REGISTRATION_FAILED);
Expand Down Expand Up @@ -179,6 +181,6 @@ private void generateAndSaveTokens(HttpServletResponse response, ParentResponse
String accessToken = jwtProvider.createAccessToken(parentResponse.email(), parentResponse.roles(), "SOCIAL_KAKAO");
String refreshToken = jwtProvider.createRefreshToken(parentResponse.email());
tokenRedisRepository.save(new TokenRedis(parentResponse.email(), accessToken, refreshToken));
jwtProvider.saveAccessTokenToCookie(response, accessToken);
cookieService.saveAccessTokenToCookie(response, accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.project.growfit.global.auth.cookie;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "custom.cookie")
@Getter
@Setter
public class CookieProperties {
private boolean secure;
private boolean httpOnly;
private String sameSite;
private int maxAge;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.project.growfit.global.auth.cookie;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class CookieService {

private final CookieProperties cookieProperties;

public void saveAccessTokenToCookie(HttpServletResponse response, String token) {
ResponseCookie cookie = createCookie("accessToken", token);
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
log.info("[saveAccessTokenToCookie] Access Token이 쿠키에 저장되었습니다.");
}

public void saveEmailToCookie(HttpServletResponse response, String email) {
ResponseCookie cookie = createCookie("email", email);
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
log.info("[saveEmailToCookie] 이메일이 쿠키에 저장되었습니다.");
}

private ResponseCookie createCookie(String name, String value) {
return ResponseCookie.from(name, value)
.httpOnly(cookieProperties.isHttpOnly())
.secure(cookieProperties.isSecure())
.sameSite(cookieProperties.getSameSite())
.maxAge(cookieProperties.getMaxAge())
.path("/")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.project.growfit.global.auth.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.growfit.global.auth.cookie.CookieService;
import com.project.growfit.global.auth.dto.CustomUserDetails;
import com.project.growfit.global.auth.jwt.JwtProvider;
import jakarta.servlet.FilterChain;
Expand All @@ -20,11 +21,13 @@
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtProvider jwtProvider;
private final CookieService cookieService;
private final ObjectMapper objectMapper = new ObjectMapper();

public LoginFilter(AuthenticationManager authenticationManager, JwtProvider jwtProvider) {
public LoginFilter(AuthenticationManager authenticationManager, JwtProvider jwtProvider, CookieService cookieService) {
this.authenticationManager = authenticationManager;
this.jwtProvider = jwtProvider;
this.cookieService = cookieService;
}

@Override
Expand Down Expand Up @@ -68,7 +71,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR

String token = jwtProvider.createAccessToken(user_id, role, login_type);

jwtProvider.saveAccessTokenToCookie(response, token);
cookieService.saveAccessTokenToCookie(response, token);

log.info("[successfulAuthentication] 로그인 성공 - 사용자 ID: {}, 역할: {}, JWT 저장 완료", user_id, role);

Expand Down
53 changes: 11 additions & 42 deletions src/main/java/com/project/growfit/global/auth/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.project.growfit.domain.User.repository.ChildRepository;
import com.project.growfit.domain.User.repository.ParentRepository;
import com.project.growfit.global.auth.cookie.CookieService;
import com.project.growfit.global.auth.dto.CustomUserDetails;
import com.project.growfit.global.redis.entity.TokenRedis;
import com.project.growfit.global.redis.repository.TokenRedisRepository;
Expand Down Expand Up @@ -31,9 +32,6 @@
@Slf4j
@Component
public class JwtProvider {

@Value("${app.cookie.secure}")
private boolean isProdSecure;
private static final String AUTHORITIES_KEY = "role";

private SecretKey secretKey;
Expand All @@ -42,34 +40,31 @@ public class JwtProvider {
private final TokenRedisRepository tokenRedisRepository;
private final ParentRepository parentRepository;
private final ChildRepository childRepository;
private final CookieService cookieService;

public JwtProvider(@Value("${jwt.secret_key}") String key,
@Value("${jwt.access-token-validity-in-seconds}") long accessTokenValiditySeconds,
@Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValiditySeconds, TokenRedisRepository tokenRedisRepository, ParentRepository parentRepository, ChildRepository childRepository) {
@Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValiditySeconds, TokenRedisRepository tokenRedisRepository, ParentRepository parentRepository, ChildRepository childRepository, CookieService cookieService) {
this.secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
this.accessTokenValidityMilliSeconds = accessTokenValiditySeconds * 1000;
this.refreshTokenValidityMilliSeconds = refreshTokenValiditySeconds * 1000;
this.tokenRedisRepository = tokenRedisRepository;
this.parentRepository = parentRepository;
this.childRepository = childRepository;
log.info("[JwtProvider] JwtProvider 초기화 완료. AccessToken 유효시간: {}ms, RefreshToken 유효시간: {}ms",
this.cookieService = cookieService;
log.debug("[JwtProvider] JwtProvider 초기화 완료. AccessToken 유효시간: {}ms, RefreshToken 유효시간: {}ms",
accessTokenValidityMilliSeconds, refreshTokenValidityMilliSeconds);
}

@PostConstruct
public void init() {
log.info("[JwtProvider] isProdSecure: {}", isProdSecure); // ← 여기를 확인
}

public String createAccessToken(String userId, String role, String loginType) {
String token = createJwt(userId, role, loginType, accessTokenValidityMilliSeconds);
log.info("[createAccessToken] Access Token 생성 완료 for userId={} with role={}", userId, role);
log.debug("[createAccessToken] Access Token 생성 완료 for userId={} with role={}", userId, role);
return token;
}

public String createRefreshToken(String userId) {
String token = createJwt(userId, "REFRESH", "", refreshTokenValidityMilliSeconds);
log.info("[createRefreshToken] Refresh Token 생성 완료 for userId={}", userId);
log.debug("[createRefreshToken] Refresh Token 생성 완료 for userId={}", userId);
return token;
}

Expand Down Expand Up @@ -138,7 +133,7 @@ public UsernamePasswordAuthenticationToken createAuthenticationFromToken(String
.orElseThrow(() -> new UsernameNotFoundException("자식을 찾을 수 없습니다: " + userId));
}
CustomUserDetails userDetails = new CustomUserDetails(user);
log.info("[createAuthenticationFromToken] Authentication 생성: userId={}, 역할={}", userId, role);
log.debug("[createAuthenticationFromToken] Authentication 생성: userId={}, 역할={}", userId, role);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}

Expand All @@ -147,22 +142,18 @@ public UsernamePasswordAuthenticationToken replaceAccessToken(HttpServletRespons
TokenRedis tokenRedis = tokenRedisRepository.findByAccessToken(token)
.orElseThrow(() -> new RuntimeException("다시 로그인 해 주세요."));
String refreshToken = tokenRedis.getRefreshToken();

Jwts.parser().setSigningKey(secretKey).build().parseClaimsJws(refreshToken);
log.debug("[replaceAccessToken] 토큰 재발급 시작");

log.info("[replaceAccessToken] 토큰 재발급 시작");
String userId = tokenRedis.getId();

Result result = getResult(userId);


String newAccessToken = createAccessToken(userId, result.role(), result.login_type());

tokenRedis.updateAccessToken(newAccessToken);
tokenRedisRepository.save(tokenRedis);
log.info("[replaceAccessToken] 토큰 재발급 완료 - 새로운 액세스 토큰 발급됨: {}", newAccessToken);
log.debug("[replaceAccessToken] 토큰 재발급 완료 - 새로운 액세스 토큰 발급됨: {}", newAccessToken);

saveAccessTokenToCookie(response, newAccessToken);
cookieService.saveAccessTokenToCookie(response, newAccessToken);

return new UsernamePasswordAuthenticationToken(new CustomUserDetails(userId, result.role()), null,
List.of(new SimpleGrantedAuthority(result.role())));
Expand Down Expand Up @@ -194,28 +185,6 @@ private Result getResult(String userId) {
private record Result(String role, String login_type) {
}

public void saveAccessTokenToCookie(HttpServletResponse response, String token) {
Cookie cookie = new Cookie("accessToken", token);
cookie.setPath("/");
cookie.setHttpOnly(false);
cookie.setSecure(isProdSecure);
cookie.setMaxAge((int) (accessTokenValidityMilliSeconds / 1000));
response.addCookie(cookie);
log.info("[saveAccessTokenToCookie] Access Token이 쿠키에 저장되었습니다.");
}

public void saveEmailToCookie(HttpServletResponse response, String email) {
ResponseCookie cookie = ResponseCookie.from("email", email)
.httpOnly(false)
.secure(isProdSecure)
.maxAge(60)
.path("/")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
log.info("[saveEmailToCookie] 이메일이 쿠키에 저장되었습니다.");
}


public String getSubjectFromToken(String token) {
try {
Claims claims = Jwts.parser()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.project.growfit.global.config;

import com.project.growfit.global.auth.cookie.CookieService;
import com.project.growfit.global.auth.jwt.JwtProvider;
import com.project.growfit.global.auth.jwt.excpetion.CustomAccessDeniedHandler;
import com.project.growfit.global.auth.jwt.excpetion.CustomAuthenticationEntryPoint;
Expand Down Expand Up @@ -31,6 +32,7 @@ public class SecurityConfig {
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtProvider jwtUtil;
private final CookieService cookieService;

@Bean
public PasswordEncoder passwordEncoder() {
Expand Down Expand Up @@ -77,7 +79,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, cookieService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtCookieAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

return http.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.project.growfit.global.auth.cookie;


import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import static org.assertj.core.api.Assertions.assertThat;
/**
* CookieProperties 클래스의 @ConfigurationProperties 바인딩 테스트
*/
@SpringBootTest
@EnableConfigurationProperties(value = CookieProperties.class)
@TestPropertySource(properties = {
"jwt.secret_key=test_secret_key",
"custom.cookie.secure=true",
"custom.cookie.http-only=false",
"custom.cookie.same-site=None",
"custom.cookie.max-age=1800"
})
class CookiePropertiesTest {

@Autowired
private CookieProperties cookieProperties;

@Test
@DisplayName("application.yml 값이 정상적으로 바인딩되어야 한다")
void testCookiePropertiesBinding() {
assertThat(cookieProperties.isSecure()).isTrue();
assertThat(cookieProperties.isHttpOnly()).isFalse();
assertThat(cookieProperties.getSameSite()).isEqualTo("None");
assertThat(cookieProperties.getMaxAge()).isEqualTo(1800);
}
}
Loading