From 0f6d715c4db10bcc5e19ebfd4ecdd3e5ecbed8a7 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:36:19 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EB=AC=B4=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=ED=86=A0=ED=81=B0=20=EB=A7=8C=EB=A3=8C=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Gotcha/common/util/CookieUtil.java | 13 ++++++++----- .../domain/auth/controller/AuthController.java | 12 ++++++------ src/main/java/Gotcha/domain/auth/dto/SignInReq.java | 5 ++++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/Gotcha/common/util/CookieUtil.java b/src/main/java/Gotcha/common/util/CookieUtil.java index 7c562fd9..083fb10c 100644 --- a/src/main/java/Gotcha/common/util/CookieUtil.java +++ b/src/main/java/Gotcha/common/util/CookieUtil.java @@ -11,15 +11,18 @@ public class CookieUtil { @Value("${token.refresh.in-cookie}") private long COOKIE_REFRESH_EXPIRATION; - public ResponseCookie createCookie(String key, String value) { + public ResponseCookie createCookie(String key, String value, boolean autoSignIn) { - return ResponseCookie.from(key, value) + ResponseCookie.ResponseCookieBuilder cookie = ResponseCookie.from(key, value) .path("/") .httpOnly(true) - .maxAge(COOKIE_REFRESH_EXPIRATION) .secure(true) - .sameSite("None") - .build(); + .sameSite("None"); + + if(autoSignIn) + cookie.maxAge(COOKIE_REFRESH_EXPIRATION); + + return cookie.build(); } public void deleteCookie(String cookieName, HttpServletResponse response) { diff --git a/src/main/java/Gotcha/domain/auth/controller/AuthController.java b/src/main/java/Gotcha/domain/auth/controller/AuthController.java index eebf772c..7f3b31cd 100644 --- a/src/main/java/Gotcha/domain/auth/controller/AuthController.java +++ b/src/main/java/Gotcha/domain/auth/controller/AuthController.java @@ -45,21 +45,21 @@ public class AuthController implements AuthApi { public ResponseEntity signUp(@Valid @RequestBody SignUpReq signUpReq) { TokenDto tokenDto = authService.signUp(signUpReq); - return createTokenRes(tokenDto); + return createTokenRes(tokenDto, false); } @PostMapping("/sign-in") public ResponseEntity signIn(@Valid @RequestBody SignInReq signInReq) { TokenDto tokenDto = authService.signIn(signInReq); - return createTokenRes(tokenDto); + return createTokenRes(tokenDto, signInReq.autoSignIn()); } @PostMapping("/guest/sign-in") public ResponseEntity guestSignIn(){ TokenDto tokenDto = authService.guestSignIn(); - return createTokenRes(tokenDto); + return createTokenRes(tokenDto, false); } @PostMapping("/token-reissue") @@ -69,7 +69,7 @@ public ResponseEntity reIssueToken(@CookieValue(name = REFRESH_COOKIE_VALUE, } TokenDto tokenDto = authService.reissueAccessToken(refreshToken); - return createTokenRes(tokenDto); + return createTokenRes(tokenDto, false); } @PostMapping("/email/send") @@ -97,7 +97,7 @@ public ResponseEntity signOut(@RequestHeader(value = ACCESS_HEADER_VALUE, req return ResponseEntity.ok(SuccessRes.from("로그아웃 되었습니다.")); } - private ResponseEntity createTokenRes(TokenDto tokenDto) { + private ResponseEntity createTokenRes(TokenDto tokenDto, boolean autoSignIn) { Map responseData = new HashMap<>(); responseData.put("accessToken", tokenDto.accessToken()); responseData.put("expiredAt", tokenDto.accessTokenExpiredAt()); @@ -105,7 +105,7 @@ private ResponseEntity createTokenRes(TokenDto tokenDto) { return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookieUtil.createCookie(REFRESH_COOKIE_VALUE, - tokenDto.refreshToken()).toString()) + tokenDto.refreshToken(), autoSignIn).toString()) .body(responseData); } } diff --git a/src/main/java/Gotcha/domain/auth/dto/SignInReq.java b/src/main/java/Gotcha/domain/auth/dto/SignInReq.java index c9dd98fe..c920dfef 100644 --- a/src/main/java/Gotcha/domain/auth/dto/SignInReq.java +++ b/src/main/java/Gotcha/domain/auth/dto/SignInReq.java @@ -12,6 +12,9 @@ public record SignInReq( @Schema(description = "비밀번호", example = "password123@") @NotBlank(message = "비밀번호를 입력해주세요.") - String password + String password, + + @Schema(description = "자동 로그인 유무", example = "true") + boolean autoSignIn ) { } From d4a903ab8eda3925f35f07fbdaebad7594d9c3bc Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:14:15 +0900 Subject: [PATCH 02/11] =?UTF-8?q?refact:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EC=9E=AC=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bd48b3aa..5eef3e42 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -37,14 +37,15 @@ spring: jwt: secret-key: ${JWT_KEY:gotchaSecretKey_mN8xG4zH2K9YtR7mN1BvLpZ5QwX3T6JpC2DfV5LqZ7YgR8K1N9BvM3X6T2D} - access-expiration: ${JWT_ACCESS_EXPIRATION:1800000} - refresh-expiration: ${JWT_REFRESH_EXPIRATION:86400000} + access-expiration: ${JWT_ACCESS_EXPIRATION:1800000} #30분 + refresh-expiration: ${JWT_REFRESH_EXPIRATION:86400000} #1일 + auto-login-refresh-expiration: ${JWT_AUTO_REFRESH_EXPIRATION:1209600000} #14일 issuer: ${JWT_ISSUER:gotcha!} token: refresh: - in-cookie: ${COOKIE_REFRESH_EXPIRATION:648000} - in-redis: ${REDIS_REFRESH_EXPIRATION:648000} + in-cookie: ${COOKIE_REFRESH_EXPIRATION:1209600} #14일 + in-redis: ${REDIS_REFRESH_EXPIRATION:1209600} #14일 cache: ttl: ${CACHE_TTL:60} From 9652097422f2b9379293e4d3c1ba81fae26586d4 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:14:28 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refact:=20=ED=86=A0=ED=81=B0=20dto=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9C=A0?= =?UTF-8?q?=EB=AC=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Gotcha/domain/auth/dto/TokenDto.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/Gotcha/domain/auth/dto/TokenDto.java b/src/main/java/Gotcha/domain/auth/dto/TokenDto.java index 75548eb6..e118bc73 100644 --- a/src/main/java/Gotcha/domain/auth/dto/TokenDto.java +++ b/src/main/java/Gotcha/domain/auth/dto/TokenDto.java @@ -5,9 +5,13 @@ public record TokenDto( String accessToken, String refreshToken, - LocalDateTime accessTokenExpiredAt + LocalDateTime accessTokenExpiredAt, + boolean autoSignIn ) { public static TokenDto of(String accessToken, String refreshToken, LocalDateTime accessTokenExpiredAt) { - return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt); + return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, false); + } + public static TokenDto of(String accessToken, String refreshToken, LocalDateTime accessTokenExpiredAt, boolean autoSignIn) { + return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn); } } \ No newline at end of file From 32550f683039e0f88cce6ec9d50c7b7056e3dd02 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:15:33 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refact:=20=EC=9E=90=EB=8F=99=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EB=AC=B4=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gotcha/common/jwt/token/JwtHelper.java | 18 +++++++------ .../common/jwt/token/TokenProvider.java | 25 ++++++++++++++++--- .../auth/controller/AuthController.java | 6 ++--- .../domain/auth/service/AuthService.java | 4 +-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/main/java/Gotcha/common/jwt/token/JwtHelper.java b/src/main/java/Gotcha/common/jwt/token/JwtHelper.java index 3a8052ea..bf77b5c7 100644 --- a/src/main/java/Gotcha/common/jwt/token/JwtHelper.java +++ b/src/main/java/Gotcha/common/jwt/token/JwtHelper.java @@ -23,29 +23,29 @@ public class JwtHelper { private final RefreshTokenService refreshTokenService; private final BlackListTokenService blackListTokenService; - public TokenDto createToken(User user) { + public TokenDto createToken(User user, boolean autoSignIn) { Long userId = user.getId(); String email = user.getEmail(); - return getTokenDto(user, userId, email); + return getTokenDto(user, userId, email, autoSignIn); } public TokenDto createGuestToken(User guest){ Long userId = guest.getId(); String username = guest.getNickname(); - return getTokenDto(guest, userId, username); + return getTokenDto(guest, userId, username, false); } - private TokenDto getTokenDto(User user, Long userId, String username) { + private TokenDto getTokenDto(User user, Long userId, String username, boolean autoSignIn) { String role = String.valueOf(user.getRole()); String accessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, username); - String refreshToken = tokenProvider.createRefreshToken(role, userId, username); + String refreshToken = tokenProvider.createRefreshToken(role, userId, username, autoSignIn); LocalDateTime accessTokenExpiredAt = tokenProvider.getExpiryDate( accessToken.replace(TOKEN_PREFIX, "").trim() ); refreshTokenService.saveRefreshToken(username, refreshToken); - return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt); + return new TokenDto(accessToken, refreshToken, accessTokenExpiredAt, autoSignIn); } public TokenDto reissueToken(String refreshToken) { @@ -59,8 +59,10 @@ public TokenDto reissueToken(String refreshToken) { Long userId = tokenProvider.getUserId(refreshToken); String role = tokenProvider.getRole(refreshToken); + boolean autoSignIn = tokenProvider.isAutoSignIn(refreshToken); + String newAccessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, username); - String newRefreshToken = tokenProvider.createRefreshToken(role, userId, username); + String newRefreshToken = tokenProvider.createRefreshToken(role, userId, username, autoSignIn); LocalDateTime newAccessTokenExpiredAt = tokenProvider.getExpiryDate( newAccessToken.replace(TOKEN_PREFIX, "").trim() ); @@ -68,7 +70,7 @@ public TokenDto reissueToken(String refreshToken) { refreshTokenService.deleteRefreshToken(refreshToken); refreshTokenService.saveRefreshToken(username, newRefreshToken); - return new TokenDto(newAccessToken, newRefreshToken, newAccessTokenExpiredAt); + return new TokenDto(newAccessToken, newRefreshToken, newAccessTokenExpiredAt, autoSignIn); } public void removeToken(String accessToken, String refreshToken, HttpServletResponse response) { diff --git a/src/main/java/Gotcha/common/jwt/token/TokenProvider.java b/src/main/java/Gotcha/common/jwt/token/TokenProvider.java index d7dba9db..e3336958 100644 --- a/src/main/java/Gotcha/common/jwt/token/TokenProvider.java +++ b/src/main/java/Gotcha/common/jwt/token/TokenProvider.java @@ -19,33 +19,44 @@ public class TokenProvider { private final SecretKey secretKey; private final long accessExpiration; private final long refreshExpiration; + private final long autoRefreshExpiration; private final String issuer; public TokenProvider(@Value("${jwt.secret-key}") String secret_key, @Value("${jwt.access-expiration}") long accessExpiration, @Value("${jwt.refresh-expiration}") long refreshExpiration, + @Value("${jwt.auto-login-refresh-expiration}") long autoRefreshExpiration, @Value("${jwt.issuer}") String issuer) { this.secretKey = new SecretKeySpec(secret_key.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); this.accessExpiration = accessExpiration; this.refreshExpiration = refreshExpiration; + this.autoRefreshExpiration = autoRefreshExpiration; this.issuer = issuer; } public String createAccessToken(String role, Long userId, String username) { - return createToken(makeClaims(role, userId), username, accessExpiration); + return createToken(makeAccessTokenClaims(role, userId), username, accessExpiration); } - public String createRefreshToken(String role, Long userId, String username) { - return createToken(makeClaims(role, userId), username, refreshExpiration); + public String createRefreshToken(String role, Long userId, String username, boolean autoSignIn) { + return createToken(makeRefreshTokenClaims(role, userId, autoSignIn), username, autoSignIn?autoRefreshExpiration:refreshExpiration); } - private Map makeClaims(String role, Long userId) { + private Map makeAccessTokenClaims(String role, Long userId) { Map claims = new HashMap<>(); claims.put("role", role); claims.put("userId", userId); return claims; } + private Map makeRefreshTokenClaims(String role, Long userId, boolean autoSignIn) { + Map claims = new HashMap<>(); + claims.put("role", role); + claims.put("userId", userId); + claims.put("auto", autoSignIn); + return claims; + } + private String createToken(Map claims, String subject, Long expiry) { return Jwts.builder() .header() @@ -68,6 +79,12 @@ private Claims getClaims(String token) { .getPayload(); } + public boolean isAutoSignIn(String token) { + Claims claims = getClaims(token); + Boolean auto = claims.get("auto", Boolean.class); + return Boolean.TRUE.equals(auto); + } + public String getUsername(String token) { return getClaims(token).getSubject(); } diff --git a/src/main/java/Gotcha/domain/auth/controller/AuthController.java b/src/main/java/Gotcha/domain/auth/controller/AuthController.java index 7f3b31cd..46ce2198 100644 --- a/src/main/java/Gotcha/domain/auth/controller/AuthController.java +++ b/src/main/java/Gotcha/domain/auth/controller/AuthController.java @@ -45,7 +45,7 @@ public class AuthController implements AuthApi { public ResponseEntity signUp(@Valid @RequestBody SignUpReq signUpReq) { TokenDto tokenDto = authService.signUp(signUpReq); - return createTokenRes(tokenDto, false); + return createTokenRes(tokenDto, tokenDto.autoSignIn()); } @PostMapping("/sign-in") @@ -59,7 +59,7 @@ public ResponseEntity signIn(@Valid @RequestBody SignInReq signInReq) { public ResponseEntity guestSignIn(){ TokenDto tokenDto = authService.guestSignIn(); - return createTokenRes(tokenDto, false); + return createTokenRes(tokenDto, tokenDto.autoSignIn()); } @PostMapping("/token-reissue") @@ -69,7 +69,7 @@ public ResponseEntity reIssueToken(@CookieValue(name = REFRESH_COOKIE_VALUE, } TokenDto tokenDto = authService.reissueAccessToken(refreshToken); - return createTokenRes(tokenDto, false); + return createTokenRes(tokenDto, tokenDto.autoSignIn()); } @PostMapping("/email/send") diff --git a/src/main/java/Gotcha/domain/auth/service/AuthService.java b/src/main/java/Gotcha/domain/auth/service/AuthService.java index 66daf412..d1e22855 100644 --- a/src/main/java/Gotcha/domain/auth/service/AuthService.java +++ b/src/main/java/Gotcha/domain/auth/service/AuthService.java @@ -61,7 +61,7 @@ public TokenDto signUp(SignUpReq signUpReq) { String encodePassword = passwordEncoder.encode(signUpReq.password()); User createdUser = userRepository.save(signUpReq.toEntity(encodePassword)); - return jwtHelper.createToken(createdUser); + return jwtHelper.createToken(createdUser, false); } @Transactional(readOnly = true) @@ -73,7 +73,7 @@ public TokenDto signIn(SignInReq signInReq){ throw new CustomException(AuthExceptionCode.INVALID_USERNAME_AND_PASSWORD); } - return jwtHelper.createToken(user); + return jwtHelper.createToken(user, signInReq.autoSignIn()); } public void signOut(String HeaderAccessToken, String refreshToken, HttpServletResponse response){ From 10ff13ef4ebe80ce1fe795ea468a9b0c141ef1a4 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 22:18:12 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 59 +++++++++++++++++++++++++ src/main/resources/application-prod.yml | 59 +++++++++++++++++++++++++ src/main/resources/application.yml | 56 +---------------------- 3 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-prod.yml diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 00000000..167c3628 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,59 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:gotcha} + username: ${DATABASE_USER:root} + password: ${DATABASE_PASSWORD:password} + + jpa: + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + properties: + hibernate: + default_batch_fetch_size: 1000 + dialect: org.hibernate.dialect.MySQLDialect + + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + timeout: 5000 + starttls: + enable: true + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASS:} + +jwt: + secret-key: ${JWT_KEY:gotchaSecretKey_mN8xG4zH2K9YtR7mN1BvLpZ5QwX3T6JpC2DfV5LqZ7YgR8K1N9BvM3X6T2D} + access-expiration: ${JWT_ACCESS_EXPIRATION:1800000} #30분 + refresh-expiration: ${JWT_REFRESH_EXPIRATION:86400000} #1일 + auto-login-refresh-expiration: ${JWT_AUTO_REFRESH_EXPIRATION:1209600000} #14일 + issuer: ${JWT_ISSUER:gotcha!} + +token: + refresh: + in-cookie: ${COOKIE_REFRESH_EXPIRATION:1209600} #14일 + in-redis: ${REDIS_REFRESH_EXPIRATION:1209600} #14일 + +cache: + ttl: ${CACHE_TTL:60} + +mail: + sender: Gotcha! + subject: "[Gotcha!] 이메일 인증을 완료해주세요." + +csrf: + cookie: + secure: false diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..a27e80a6 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,59 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:gotcha} + username: ${DATABASE_USER:root} + password: ${DATABASE_PASSWORD:password} + + jpa: + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + properties: + hibernate: + default_batch_fetch_size: 1000 + dialect: org.hibernate.dialect.MySQLDialect + + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + timeout: 5000 + starttls: + enable: true + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASS:} + +jwt: + secret-key: ${JWT_KEY:gotchaSecretKey_mN8xG4zH2K9YtR7mN1BvLpZ5QwX3T6JpC2DfV5LqZ7YgR8K1N9BvM3X6T2D} + access-expiration: ${JWT_ACCESS_EXPIRATION:1800000} #30분 + refresh-expiration: ${JWT_REFRESH_EXPIRATION:86400000} #1일 + auto-login-refresh-expiration: ${JWT_AUTO_REFRESH_EXPIRATION:1209600000} #14일 + issuer: ${JWT_ISSUER:gotcha!} + +token: + refresh: + in-cookie: ${COOKIE_REFRESH_EXPIRATION:1209600} #14일 + in-redis: ${REDIS_REFRESH_EXPIRATION:1209600} #14일 + +cache: + ttl: ${CACHE_TTL:60} + +mail: + sender: Gotcha! + subject: "[Gotcha!] 이메일 인증을 완료해주세요." + +csrf: + cookie: + secure: true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5eef3e42..caf4dfcd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,55 +1,3 @@ spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:gotcha} - username: ${DATABASE_USER:root} - password: ${DATABASE_PASSWORD:password} - - jpa: - hibernate: - ddl-auto: update - naming: - physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl - show-sql: true - properties: - hibernate: - default_batch_fetch_size: 1000 - dialect: org.hibernate.dialect.MySQLDialect - - mail: - host: smtp.gmail.com - port: 587 - username: ${MAIL_USERNAME} - password: ${MAIL_PASSWORD} - properties: - mail: - smtp: - auth: true - timeout: 5000 - starttls: - enable: true - - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASS:} - -jwt: - secret-key: ${JWT_KEY:gotchaSecretKey_mN8xG4zH2K9YtR7mN1BvLpZ5QwX3T6JpC2DfV5LqZ7YgR8K1N9BvM3X6T2D} - access-expiration: ${JWT_ACCESS_EXPIRATION:1800000} #30분 - refresh-expiration: ${JWT_REFRESH_EXPIRATION:86400000} #1일 - auto-login-refresh-expiration: ${JWT_AUTO_REFRESH_EXPIRATION:1209600000} #14일 - issuer: ${JWT_ISSUER:gotcha!} - -token: - refresh: - in-cookie: ${COOKIE_REFRESH_EXPIRATION:1209600} #14일 - in-redis: ${REDIS_REFRESH_EXPIRATION:1209600} #14일 - -cache: - ttl: ${CACHE_TTL:60} - -mail: - sender: Gotcha! - subject: "[Gotcha!] 이메일 인증을 완료해주세요." \ No newline at end of file + profiles: + active: dev \ No newline at end of file From 5b142cd0aea4480a28a9ce63f61174b05589c04e Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 22:18:38 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refact:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BF=A0=ED=82=A4=20secure=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Gotcha/common/util/CookieUtil.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/Gotcha/common/util/CookieUtil.java b/src/main/java/Gotcha/common/util/CookieUtil.java index 083fb10c..16588e6a 100644 --- a/src/main/java/Gotcha/common/util/CookieUtil.java +++ b/src/main/java/Gotcha/common/util/CookieUtil.java @@ -11,12 +11,15 @@ public class CookieUtil { @Value("${token.refresh.in-cookie}") private long COOKIE_REFRESH_EXPIRATION; + @Value("${csrf.cookie.secure}") + private boolean secure; + public ResponseCookie createCookie(String key, String value, boolean autoSignIn) { ResponseCookie.ResponseCookieBuilder cookie = ResponseCookie.from(key, value) .path("/") .httpOnly(true) - .secure(true) + .secure(secure) .sameSite("None"); if(autoSignIn) @@ -31,7 +34,7 @@ public void deleteCookie(String cookieName, HttpServletResponse response) { .path("/") .httpOnly(true) .maxAge(0) - .secure(true) + .secure(secure) .sameSite("None") .build(); From 945f25875e28c1e076b407cab72c4ee2a2390982 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 22:19:14 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20CsrfConfig=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/Gotcha/common/config/CsrfConfig.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/Gotcha/common/config/CsrfConfig.java diff --git a/src/main/java/Gotcha/common/config/CsrfConfig.java b/src/main/java/Gotcha/common/config/CsrfConfig.java new file mode 100644 index 00000000..3dc6ded6 --- /dev/null +++ b/src/main/java/Gotcha/common/config/CsrfConfig.java @@ -0,0 +1,33 @@ +package Gotcha.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; + +@Configuration +public class CsrfConfig { + + @Value("${csrf.cookie.secure}") + private boolean secure; + + @Bean + public CookieCsrfTokenRepository csrfTokenRepository() { + CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + + repository.setCookieName("XSRF-TOKEN"); + repository.setCookiePath("/"); + repository.setCookieHttpOnly(false); + repository.setSecure(secure); + + return repository; + } + + @Bean + public CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler() { + CsrfTokenRequestAttributeHandler handler = new CsrfTokenRequestAttributeHandler(); + handler.setCsrfRequestAttributeName(null); + return handler; + } +} \ No newline at end of file From 2589d4a80f5cd6f9b92767b021d3a307504a1575 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 22:19:36 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20csrf=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomAccessDeniedHandler.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java diff --git a/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java b/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..4e3d52bc --- /dev/null +++ b/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,33 @@ +package Gotcha.common.exception; + +import Gotcha.common.jwt.exception.JwtExceptionCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + private final ObjectMapper objectMapper; + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + + log.warn("[AccessDeniedException] {}", accessDeniedException.getMessage()); + + response.setStatus(JwtExceptionCode.ACCESS_DENIED.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + + objectMapper.writeValue(response.getWriter(), ExceptionRes.from(JwtExceptionCode.ACCESS_DENIED)); + } +} \ No newline at end of file From f9605ac38e3827c986332e9234b29e78c92b00bd Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 22:19:44 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20csrf=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gotcha/common/config/SecurityConfig.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/Gotcha/common/config/SecurityConfig.java b/src/main/java/Gotcha/common/config/SecurityConfig.java index 9a40f2dd..2a04a739 100644 --- a/src/main/java/Gotcha/common/config/SecurityConfig.java +++ b/src/main/java/Gotcha/common/config/SecurityConfig.java @@ -1,13 +1,12 @@ package Gotcha.common.config; -import Gotcha.common.jwt.exception.JwtExceptionCode; +import Gotcha.common.exception.CustomAccessDeniedHandler; import Gotcha.common.jwt.filter.JwtAuthenticationFilter; import Gotcha.common.jwt.filter.JwtExceptionFilter; import Gotcha.domain.user.entity.Role; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.AccessDeniedException; 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; @@ -16,6 +15,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.web.cors.CorsConfigurationSource; import static Gotcha.common.constants.SecurityConstants.ADMIN_ENDPOINTS; @@ -28,6 +29,9 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtExceptionFilter jwtExceptionFilter; private final CorsConfigurationSource corsConfigurationSource; + private final CookieCsrfTokenRepository csrfTokenRepository; + private final CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler; + private final CustomAccessDeniedHandler accessDeniedHandler; @Bean PasswordEncoder passwordEncoder() { @@ -38,7 +42,13 @@ PasswordEncoder passwordEncoder() { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) + .csrf(csrf -> csrf + .requireCsrfProtectionMatcher(request -> { + String path = request.getRequestURI(); + return path.equals("/api/v1/auth/token-reissue"); + }) + .csrfTokenRepository(csrfTokenRepository) + .csrfTokenRequestHandler(csrfTokenRequestAttributeHandler)) .cors((cors) -> cors.configurationSource(corsConfigurationSource)) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) @@ -49,10 +59,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(PUBLIC_ENDPOINTS).permitAll() .requestMatchers(ADMIN_ENDPOINTS).hasAnyRole(String.valueOf(Role.ADMIN)) .anyRequest().authenticated() - ).exceptionHandling(exception -> exception - .accessDeniedHandler((request, response, accessDeniedException) -> { - throw new AccessDeniedException(JwtExceptionCode.ACCESS_DENIED.getMessage()); - })) + ).exceptionHandling(exception -> + exception.accessDeniedHandler(accessDeniedHandler)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class); From f3d026fa89ffd4cb4f6f7ab4616d92a9a36d0f17 Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sat, 12 Apr 2025 22:19:56 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20csrf=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/AuthController.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/Gotcha/domain/auth/controller/AuthController.java b/src/main/java/Gotcha/domain/auth/controller/AuthController.java index 46ce2198..a66f84f2 100644 --- a/src/main/java/Gotcha/domain/auth/controller/AuthController.java +++ b/src/main/java/Gotcha/domain/auth/controller/AuthController.java @@ -13,11 +13,14 @@ import Gotcha.domain.auth.dto.TokenDto; import Gotcha.domain.auth.service.AuthService; import Gotcha.domain.user.service.UserService; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -40,6 +43,8 @@ public class AuthController implements AuthApi { private final CookieUtil cookieUtil; private final MailCodeService mailCodeService; private final UserService userService; + private final CookieCsrfTokenRepository csrfTokenRepository; + @PostMapping("/sign-up") public ResponseEntity signUp(@Valid @RequestBody SignUpReq signUpReq) { @@ -97,6 +102,13 @@ public ResponseEntity signOut(@RequestHeader(value = ACCESS_HEADER_VALUE, req return ResponseEntity.ok(SuccessRes.from("로그아웃 되었습니다.")); } + @GetMapping("/csrf-token") + public ResponseEntity getCsrfToken(HttpServletRequest request, HttpServletResponse response) { + CsrfToken token = csrfTokenRepository.generateToken(request); + csrfTokenRepository.saveToken(token, request, response); + return ResponseEntity.ok().build(); + } + private ResponseEntity createTokenRes(TokenDto tokenDto, boolean autoSignIn) { Map responseData = new HashMap<>(); responseData.put("accessToken", tokenDto.accessToken()); From dd070762ae7388486e58f551a3999e79572699be Mon Sep 17 00:00:00 2001 From: LeeSeunghyeon <50231309+Uralauah@users.noreply.github.com> Date: Sun, 13 Apr 2025 12:35:05 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refact:=20=EA=B6=8C=ED=95=9C=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=8B=A4=ED=8C=A8,=20CSRF=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=8B=A4=ED=8C=A8=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomAccessDeniedHandler.java | 19 +++++++++++++++---- .../exceptionCode/GlobalExceptionCode.java | 3 ++- .../common/jwt/filter/JwtExceptionFilter.java | 3 --- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java b/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java index 4e3d52bc..d6e75b98 100644 --- a/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java +++ b/src/main/java/Gotcha/common/exception/CustomAccessDeniedHandler.java @@ -1,5 +1,7 @@ package Gotcha.common.exception; +import Gotcha.common.exception.exceptionCode.ExceptionCode; +import Gotcha.common.exception.exceptionCode.GlobalExceptionCode; import Gotcha.common.jwt.exception.JwtExceptionCode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -8,6 +10,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.csrf.InvalidCsrfTokenException; +import org.springframework.security.web.csrf.MissingCsrfTokenException; import org.springframework.stereotype.Component; import java.io.IOException; @@ -23,11 +27,18 @@ public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { - log.warn("[AccessDeniedException] {}", accessDeniedException.getMessage()); + log.warn("[AccessDeniedException] {} occurred", accessDeniedException.getClass().getSimpleName()); - response.setStatus(JwtExceptionCode.ACCESS_DENIED.getStatus().value()); - response.setContentType("application/json;charset=UTF-8"); + ExceptionCode exceptionCode; + if (accessDeniedException instanceof InvalidCsrfTokenException || + accessDeniedException instanceof MissingCsrfTokenException) { + exceptionCode = GlobalExceptionCode.CSRF_INVALID; + } else { + exceptionCode = JwtExceptionCode.ACCESS_DENIED; + } - objectMapper.writeValue(response.getWriter(), ExceptionRes.from(JwtExceptionCode.ACCESS_DENIED)); + response.setStatus(exceptionCode.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + objectMapper.writeValue(response.getWriter(), ExceptionRes.from(exceptionCode)); } } \ No newline at end of file diff --git a/src/main/java/Gotcha/common/exception/exceptionCode/GlobalExceptionCode.java b/src/main/java/Gotcha/common/exception/exceptionCode/GlobalExceptionCode.java index 383a8acc..9bed9b45 100644 --- a/src/main/java/Gotcha/common/exception/exceptionCode/GlobalExceptionCode.java +++ b/src/main/java/Gotcha/common/exception/exceptionCode/GlobalExceptionCode.java @@ -7,7 +7,8 @@ public enum GlobalExceptionCode implements ExceptionCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다. 서버 팀에 연락주세요."), - FIELD_VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "필드 검증 오류입니다."),; + FIELD_VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "필드 검증 오류입니다."), + CSRF_INVALID(HttpStatus.FORBIDDEN, "CSRF 토큰이 올바르지 않습니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/Gotcha/common/jwt/filter/JwtExceptionFilter.java b/src/main/java/Gotcha/common/jwt/filter/JwtExceptionFilter.java index 341114f2..59b41359 100644 --- a/src/main/java/Gotcha/common/jwt/filter/JwtExceptionFilter.java +++ b/src/main/java/Gotcha/common/jwt/filter/JwtExceptionFilter.java @@ -14,7 +14,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.web.filter.OncePerRequestFilter; @@ -40,8 +39,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse handleTokenException(response, JwtExceptionCode.UNKNOWN_TOKEN_ERROR, HttpServletResponse.SC_UNAUTHORIZED); } catch (AuthenticationServiceException e){ handleTokenException(response, JwtExceptionCode.ACCESS_TOKEN_NOT_FOUND, HttpServletResponse.SC_UNAUTHORIZED); - } catch (AccessDeniedException e){ - handleTokenException(response, JwtExceptionCode.ACCESS_DENIED, HttpServletResponse.SC_FORBIDDEN); } }