Conversation
📝 WalkthroughWalkthroughIntroduces Spring Security + JWT auth, user registration/login/profile endpoints, JWT filter and providers, cookie-based refresh token handling, password validation, consent Term entity, BaseEntity package move, and several user/domain enums, DTOs, services, repositories, and error/status additions. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Controller as UserController
participant Service as UserServiceImpl
participant Repo as UserRepository
participant Encoder as PasswordEncoder
participant Jwt as JwtTokenProvider
participant Cookie as AuthCookieProvider
User->>Controller: POST /api/auth/signup (JoinDto)
Controller->>Service: signup(joinDto)
Service->>Repo: findByEmail(email)
Repo-->>Service: Optional.empty / present
alt email exists
Service-->>Controller: throw UserException
Controller-->>User: 409 CONFLICT
else create user
Service->>Encoder: encode(password)
Encoder-->>Service: encodedPassword
Service->>Repo: save(User)
Repo-->>Service: saved User
Service-->>Controller: JoinResultDto
Controller-->>User: 200 OK
end
sequenceDiagram
actor User
participant Controller as UserController
participant Service as UserServiceImpl
participant Repo as UserRepository
participant Encoder as PasswordEncoder
participant Jwt as JwtTokenProvider
participant Cookie as AuthCookieProvider
User->>Controller: POST /api/auth/login (LoginDto)
Controller->>Service: login(loginDto)
Service->>Repo: findByEmail(email)
Repo-->>Service: Optional<User>
Service->>Encoder: matches(password, user.password)
alt invalid
Service-->>Controller: throw UserException
Controller-->>User: 400 BAD_REQUEST
else valid
Service->>Jwt: createAccessToken(email)
Jwt-->>Service: accessToken
Service->>Jwt: createRefreshToken(email)
Jwt-->>Service: refreshToken
Service->>Repo: save(user with refreshToken)
Service->>Cookie: refreshTokenCookie(refreshToken)
Cookie-->>Controller: Set-Cookie
Controller-->>User: 200 OK + cookie
end
sequenceDiagram
actor Client
participant Filter as JwtAuthenticationFilter
participant Jwt as JwtTokenProvider
participant UserDetails as UserDetailsServiceImpl
participant Repo as UserRepository
participant Security as SecurityContextHolder
Client->>Filter: Request with Authorization header
Filter->>Jwt: resolveToken(request)
Jwt-->>Filter: token
alt token present and valid
Filter->>Jwt: validateToken(token)
Jwt-->>Filter: true
Filter->>Jwt: extractAuthentication(request)
Jwt->>UserDetails: loadUserByUsername(email)
UserDetails->>Repo: findByEmail(email)
Repo-->>UserDetails: User
Filter->>Security: set Authentication
Filter-->>Client: proceed (authenticated)
else invalid or absent
Filter-->>Client: proceed (unauthenticated)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
🤖 Fix all issues with AI agents
In `@build.gradle`:
- Around line 35-44: Replace the hardcoded jjwt module versions in build.gradle
with the official BOM: add a platform import for io.jsonwebtoken:jjwt-bom:0.13.0
and remove versions from the jjwt dependencies (jjwt-api, jjwt-impl,
jjwt-jackson) so they are resolved by the BOM; keep jjwt-api as implementation
and jjwt-impl/jjwt-jackson as runtimeOnly but do not specify versions, and
ensure the BOM line appears before those dependency declarations.
In `@src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java`:
- Around line 22-24: The Term.user OneToOne mapping should cascade deletes so
removing a User also deletes its Term; update the Term entity's `@OneToOne`
annotation on the user field to include cascade = CascadeType.REMOVE (or
CascadeType.ALL) and/or orphanRemoval = true so the related Term is deleted when
the User is deleted; ensure the mapping stays consistent with any inverse
mapping on User (mappedBy) if present and run tests for the user withdraw/delete
flow to verify no FK/orphan issues.
- Around line 26-33: In class Term, remove the `@Builder.Default` annotation and
the "= true" default assignment for the tosConsent and privacyConsent fields so
the entity does not silently default to consent; keep the `@Column`(name =
"tos_consent", nullable = false) and `@Column`(name = "privacy_consent", nullable
= false) annotations so persistence still enforces non-null, and rely on
callers/DTOs to provide explicit values (ensuring Term.builder() requires
explicit values rather than inheriting true).
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java`:
- Around line 119-128: The logout endpoint (UserController.logout) currently
calls userService.logout(request) but does not clear the client's HTTP-only
refresh token cookie; update the controller to include the cookie-clearing
response header by adding the cookie from a new
AuthCookieProvider.clearRefreshTokenCookie() method to the ResponseEntity (so
the controller returns ResponseEntity.ok().header(...set-cookie...).body(...)).
Add AuthCookieProvider.clearRefreshTokenCookie() that creates the same cookie
name/path/domain/HttpOnly/secure settings as your existing refresh cookie but
with maxAge(0) (or equivalent) so the browser removes it when the logout
response is received.
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java`:
- Around line 65-73: The signup flow currently converts JoinDto to a User via
UserConverter.toUser and saves only the User, so the consent fields
(JoinDto.getTosConsent, getPrivacyConsent, getMarketingConsent) never persist;
after UserServiceImpl.signup() creates and saves the User, construct a Term
entity (using the Term class or Term.builder) populated with the three consent
booleans and associate it to the saved User (set the one-to-one link), then
persist it via the TermRepository (e.g., termRepository.save(term)); ensure the
Term is created using the saved User reference and that any transactional
boundaries in UserServiceImpl encompass both saves so the user and term are
stored atomically.
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java`:
- Around line 71-90: The PasswordMatch validator currently expects methods
getPassword()/getPasswordConfirm() but ChangePasswordDto uses
newPassword/newPasswordConfirm, causing runtime reflection failures; fix by
making the PasswordMatch annotation/validator generic (add annotation attributes
like firstField/secondField or fieldNames) and update its validator to read
those attribute names and compare the corresponding getters, or alternatively
create a new annotation/validator (e.g., NewPasswordMatch) specifically for
ChangePasswordDto and apply it to ChangePasswordDto; ensure the validator
references the unique symbols PasswordMatch (or the new annotation), its
validator class, and ChangePasswordDto/newPassword/newPasswordConfirm so the
correct fields are compared.
- Around line 39-45: In UserRequestDto, replace the `@NotNull` on tosConsent and
privacyConsent with `@AssertTrue` so these fields are validated to be true
(mandatory consent) rather than merely non-null; keep the existing `@Schema`
descriptions/examples, update the validation message strings if desired, and add
the javax.validation.constraints.AssertTrue import; leave marketingConsent
validation as-is since it is optional.
In `@src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java`:
- Around line 46-47: The User entity currently persists plaintext refresh tokens
in the refreshToken field (and similarly at the other occurrence), which is
unsafe; change the entity to store a hashed value or an opaque token identifier
(rename field to refreshTokenHash or refreshTokenId) and update setters/getters
and any code paths that call User.setRefreshToken/getRefreshToken to instead
hash the raw token (using a secure hash like HMAC-SHA256 with app secret or
salted SHA-256) before persisting and to verify by hashing the presented token
and performing a constant-time compare; also update token issuance/rotation
logic to persist the hash/ID and adjust revocation/expiry checks accordingly.
In `@src/main/java/com/eatsfine/eatsfine/domain/user/enums/Grade.java`:
- Around line 1-4: The Grade enum is empty and should not remain as a
placeholder; either populate the enum Grade with the intended constants (e.g.,
add the project's membership/role levels like BASIC, SILVER, GOLD or the actual
names used elsewhere) and any necessary fields/methods, or delete the Grade enum
class and remove/update any references to it; if this file is intentionally
scaffolded for future work, add a clear TODO javadoc on the Grade type
indicating it is intentionally empty and when it will be implemented. Locate the
class by the symbol Grade in the package com.eatsfine.eatsfine.domain.user.enums
to apply the fix.
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java`:
- Around line 170-175: The logout method calls user.updateRefreshToken(null) but
the change is never flushed because the entity is detached; annotate the
logout(HttpServletRequest) method in UserServiceImpl with `@Transactional` (or
mark the class) so the persistence context will track and flush the update, and
ensure the proper import of
org.springframework.transaction.annotation.Transactional; alternatively, if you
prefer not to use transactions here, call the repository save method after
getCurrentUser(request) (e.g., userRepository.save(user)) to persist the null
refresh token—refer to logout(HttpServletRequest), getCurrentUser(...), and
User.updateRefreshToken(...) to locate the code.
- Around line 34-48: The signup method in UserServiceImpl performs a write but
lacks `@Transactional`; add the transactional annotation to the signup method (or
class) to ensure proper transaction boundaries for UserServiceImpl.signup, and
ensure the save is executed within that transaction. Also inspect JoinDto
consent fields (tosConsent, privacyConsent, marketingConsent) and either map and
persist them (e.g., add fields to User or create a Term/Consent entity and
persist alongside User within the same transaction in UserConverter.toUser or
immediately after save) or remove/adjust the DTO validation if they are not
intended to be stored.
- Around line 104-127: The code uploads the new profile image via
s3Service.upload before the DB transaction commits, risking orphaned objects if
the transaction rolls back; change the flow so that after obtaining newKey from
s3Service.upload you register a TransactionSynchronization (via
TransactionSynchronizationManager.registerSynchronization) that deletes newKey
in afterCompletion when the transaction status is not COMMITTED (or
alternatively perform the actual upload inside afterCommit if MultipartFile is
still available), and keep the existing afterCommit deletion of oldKey; update
calls referencing validateProfileImage, s3Service.upload,
user.updateProfileImage,
TransactionSynchronizationManager.registerSynchronization,
afterCommit/afterCompletion and s3Service.deleteByKey accordingly so new uploads
are cleaned up on rollback.
- Around line 161-168: Add a transactional boundary and ensure S3 cleanup before
deleting the User: annotate withdraw with `@Transactional` (or run within a
transactional context), retrieve the current User via getCurrentUser(request),
call the service that removes the user's S3 profile image (e.g.,
s3Service.deleteProfileImageIfExists(user) or removeProfileImage(user)) while
the entity is still managed, then clear the refresh token
(user.updateRefreshToken(null)) and delete via userRepository.delete(user); this
ensures the delete runs in a transaction and the S3 object is removed prior to
entity deletion.
In `@src/main/java/com/eatsfine/eatsfine/global/auth/AuthCookieProvider.java`:
- Around line 10-17: The refreshTokenCookie method in AuthCookieProvider can
emit a cookie with a null/blank value; add defensive validation at the start of
refreshTokenCookie(String refreshToken) to check
StringUtils.hasText(refreshToken) (or refreshToken == null ||
refreshToken.trim().isEmpty()) and if invalid either throw an
IllegalArgumentException (or a custom RuntimeException) with a clear message
like "refreshToken must not be null or blank", so the method returns/aborts
before calling ResponseCookie.from("refreshToken", refreshToken). Ensure the
validation is applied before any call to ResponseCookie.from.
In `@src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java`:
- Around line 26-29: In loadUserByUsername (UserDetailsServiceImpl) don't
default a null password to ""; instead detect when user.getPassword() == null
and throw an authentication exception (e.g., UsernameNotFoundException or
BadCredentialsException) with a clear message indicating the account uses social
login, so password-based authentication is rejected for that user; update any
downstream code expecting a non-null password accordingly.
In
`@src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java`:
- Around line 8-13: Add fail-fast validation to JwtProperties by marking the
class with validation support and the secret field as required: annotate the
JwtProperties class with the validation trigger (e.g., `@Validated`) and annotate
the secret field with a non-blank constraint (e.g., `@NotBlank`) so Spring fails
on startup when jwt.secret is missing or empty; if validation dependencies are
missing, ensure javax/jakarta validation (spring-boot-starter-validation) is on
the classpath. Use the class name JwtProperties and the field secret to locate
where to add these annotations.
In `@src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java`:
- Line 74: The CORS config currently calls
config.setAllowedOriginPatterns(List.of("*")) while also calling
config.setAllowCredentials(true), which permits credentials from any origin; in
SecurityConfig you should restrict allowed origins to your production frontend
domains (replace the wildcard with an explicit list of origins) or remove
allowCredentials(true) if credentials are not needed; update the code paths that
call config.setAllowedOriginPatterns and config.setAllowCredentials in
SecurityConfig (and the similar occurrence around line 79) so only trusted
origins are listed (e.g., a configurable list via application properties) and
ensure allowCredentials remains true only when using explicit origins.
- Around line 77-78: The second call to config.setExposedHeaders in
SecurityConfig overwrites the first and drops "Set-Cookie"; change the code so
the exposed headers include both values in one call (i.e., ensure
config.setExposedHeaders is invoked with both "Authorization" and "Set-Cookie"
together) or merge the lists before setting so the HTTP-only refreshToken cookie
is not lost.
- Line 60: Remove the redundant/incorrect permit rule in SecurityConfig that
references requestMatchers("/auth/**", "/login", "/signup") — locate the
HttpSecurity configuration method in the SecurityConfig class (the block that
builds authorizeHttpRequests()/requestMatchers()) and delete that
requestMatchers(...) call so only the correct permitted pattern "/api/auth/**"
remains; this eliminates the dead/unused path patterns and keeps authorization
rules consistent with the controller endpoints.
In
`@src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java`:
- Around line 8-28: PasswordMatchValidator is currently typed to
UserRequestDto.JoinDto so it never runs for ChangePasswordDto and also uses
JoinDto-specific getters (getPassword/getPasswordConfirm) that ChangePasswordDto
doesn't have; change the validator to accept Object (class
PasswordMatchValidator implements ConstraintValidator<PasswordMatch, Object>)
and update isValid(Object dto, ConstraintValidatorContext) to detect the DTO
type (instanceof UserRequestDto.JoinDto or instanceof
UserRequestDto.ChangePasswordDto), read the appropriate fields (join:
getPassword/getPasswordConfirm, change: getNewPassword/getNewPasswordConfirm or
their getters), perform null/mismatch checks, and build the constraint violation
on the correct property node ("passwordConfirm" or "newPasswordConfirm") via
context.disableDefaultConstraintViolation()/buildConstraintViolationWithTemplate(...).addPropertyNode(...).addConstraintViolation();
ensure the method returns true only when matching for the detected DTO type and
false otherwise.
🧹 Nitpick comments (18)
src/main/java/com/eatsfine/eatsfine/domain/image/status/ImageErrorStatus.java (1)
18-18: Consider using HTTP 413 for oversized uploads.Line 18 currently uses
BAD_REQUEST, butPAYLOAD_TOO_LARGE(413) better reflects an exceeded upload size and improves client-side handling.💡 Suggested change
- FILE_TOO_LARGE(HttpStatus.BAD_REQUEST, "IMAGE4005", "첨부하는 이미지의 크기가 너무 큽니다.") + FILE_TOO_LARGE(HttpStatus.PAYLOAD_TOO_LARGE, "IMAGE4005", "첨부하는 이미지의 크기가 너무 큽니다.")src/main/java/com/eatsfine/eatsfine/global/auth/CustomAuthenticationEntryPoint.java (1)
13-16: Align 401 responses with JSON error format.Consider returning JSON like the access-denied handler to keep clients consistent.
♻️ Suggested fix (JSON response)
`@Override` public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "인증이 필요합니다."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"message\": \"인증이 필요합니다.\"}"); }src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java (1)
31-31: Populate authorities from the user role.
Returning an empty authority list will break role-based authorization rules.✅ Suggested adjustment
+import org.springframework.security.core.authority.SimpleGrantedAuthority; ... - return new User(user.getEmail(), password, List.of()); + return new User( + user.getEmail(), + password, + List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) + );src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java (2)
18-19: Align nickname length validation with DB constraints.
User.nickNameis limited to 20 chars; validating here prevents persistence errors.✅ Suggested validation
+import jakarta.validation.constraints.Size; ... `@NotBlank`(message = "이름은 필수입니다.") + `@Size`(max = 20, message = "이름은 20자 이내여야 합니다.") private String nickName; // 이름
63-68: Add validation to UpdateDto to prevent invalid updates.
Bean Validation ignores nulls, so these annotations won’t block partial updates.✅ Suggested validation
`@Getter` `@Setter` public static class UpdateDto { + `@Email`(message = "유효한 이메일 형식이어야 합니다.") private String email; + `@Size`(max = 20, message = "이름은 20자 이내여야 합니다.") private String nickName; + `@Pattern`(regexp = "^010\\d{8}$", message = "휴대전화 번호는 010으로 시작하는 11자리 숫자여야 합니다.") private String phoneNumber; }src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java (3)
32-32: ReplaceSystem.out.printlnwith proper logging.Production code should use SLF4J logger instead of
System.out.println. Add@Slf4jannotation and uselog.debug()orlog.error()accordingly.♻️ Proposed fix
+import lombok.extern.slf4j.Slf4j; + `@Component` +@Slf4j `@RequiredArgsConstructor` public class JwtAuthenticationFilter extends OncePerRequestFilter {- System.out.println("요청 URI: " + uri); + log.debug("요청 URI: {}", uri);} catch (Exception e) { - // 예외 로그 출력 - System.out.println("JWT 인증 오류: " + e.getMessage()); + log.error("JWT 인증 오류: {}", e.getMessage()); }Also applies to: 58-58
35-41: Path bypass logic duplicatesSecurityConfig.permitAll()rules.The hardcoded paths here (
/api/auth/login,/api/auth/signup,/oauth2,/login) duplicate the configuration inSecurityConfig. This creates maintenance risk when one is updated but not the other. Consider using a shared constant or relying solely on Spring Security'sshouldNotFilter()override withrequestMatchers.
56-59: Silently catching all exceptions may hide important authentication failures.The broad
catch (Exception e)swallows all errors and continues the filter chain without authentication. While this allows the request to proceed to the 401 handler, consider logging atwarnorerrorlevel with the full stack trace for debugging purposes in non-production environments.src/main/java/com/eatsfine/eatsfine/domain/user/service/UserService.java (1)
14-22: Consider extracting user identity before passing to service layer.Passing
HttpServletRequestto service methods couples the service layer to HTTP transport. A cleaner approach is to extract the authenticated user's email/ID in the controller (or via@AuthenticationPrincipal) and pass it to the service. This improves testability and separation of concerns.♻️ Example signature change
- UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request); + UserResponseDto.UserInfoDto getMemberInfo(String email); - String updateMemberInfo(UserRequestDto.UpdateDto updateDto, MultipartFile profileImage, HttpServletRequest request); + String updateMemberInfo(UserRequestDto.UpdateDto updateDto, MultipartFile profileImage, String email); - void withdraw(HttpServletRequest request); + void withdraw(String email); - void logout(HttpServletRequest request); + void logout(String email);src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtTokenProvider.java (3)
36-37: Token validity durations should be externalized to configuration.Hardcoding
accessTokenValidityandrefreshTokenValiditymakes it difficult to adjust token lifetimes across environments without code changes. Consider moving these toJwtPropertiesorapplication.yml.
7-7: Unused importErrorStatus.The import
com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatusis not used in this file.♻️ Proposed fix
-import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus;
73-84:getAuthenticationcreates a principal with empty authorities.This method creates a
UserwithCollections.emptyList()for authorities. WhileJwtAuthenticationFilterloads actualUserDetailsviauserDetailsService.loadUserByUsername(), thisgetAuthenticationmethod is still public and could be called directly, returning an auth token with no roles. Consider adding a note or deprecating if not intended for direct use.src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java (3)
36-36: Unused dependencyjwtTokenProvider.
JwtTokenProvideris injected but never used in this controller. Consider removing it to keep dependencies clean.♻️ Proposed fix
public class UserController { private final UserService userService; - private final JwtTokenProvider jwtTokenProvider; private final AuthCookieProvider authCookieProvider;
41-43: Inconsistent response wrapper:signupreturns raw DTO while others useApiResponse.The
signupendpoint returnsResponseEntity<JoinResultDto>directly, whereaslogin,getMyInfo, and other endpoints wrap responses inApiResponse. Consider wrapping for consistency.♻️ Proposed fix
- public ResponseEntity<UserResponseDto.JoinResultDto> signup(`@RequestBody` `@Valid` UserRequestDto.JoinDto joinDto) { + public ResponseEntity<ApiResponse<UserResponseDto.JoinResultDto>> signup(`@RequestBody` `@Valid` UserRequestDto.JoinDto joinDto) { UserResponseDto.JoinResultDto result = userService.signup(joinDto); - return ResponseEntity.ok(result); + return ResponseEntity.ok(ApiResponse.onSuccess(result)); }
114-114: Avoid wildcard return typeResponseEntity<?>.Using
ResponseEntity<?>loses type information. Use a concrete type likeResponseEntity<ApiResponse<String>>for consistency with other endpoints.♻️ Proposed fix
- public ResponseEntity<?> withdraw(HttpServletRequest request) { + public ResponseEntity<ApiResponse<String>> withdraw(HttpServletRequest request) {src/main/java/com/eatsfine/eatsfine/domain/user/dto/response/UserResponseDto.java (1)
41-49:@Setteron response DTO is unusual.Response DTOs are typically immutable.
UpdateResponseDtohas@Setterwhich could allow unintended mutation. If setters aren't needed, consider removing@Setterand relying solely on the builder pattern.♻️ Proposed fix
`@Getter` - `@Setter` `@Builder` + `@AllArgsConstructor` public static class UpdateResponseDto{src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java (2)
76-81: Consider@Transactional(readOnly = true)for read-only operation.This method only reads data. Using
readOnly = trueenables Hibernate optimizations (skips dirty checking) and allows routing to read replicas if configured.Proposed fix
`@Override` -@Transactional +@Transactional(readOnly = true) public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) {
134-136: Redundantsave()andflush()calls.Within a
@Transactionalmethod, the entity is already managed. JPA dirty checking will automatically persist changes on commit. These explicit calls are unnecessary.Proposed fix
- userRepository.save(user); - userRepository.flush(); - log.info("[Service] Updated userId={}, nickname={}, phone={}, profileKey={}",
| //security | ||
| //implementation 'org.springframework.boot:spring-boot-starter-security' | ||
| implementation 'org.springframework.boot:spring-boot-starter-security' | ||
|
|
||
| // JWT | ||
| implementation 'io.jsonwebtoken:jjwt-api:0.11.5' | ||
| runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' | ||
| runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' | ||
|
|
||
| // oauth | ||
| implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
io.jsonwebtoken jjwt latest version 2025 2026
💡 Result:
As of February 3, 2026, the latest JJWT (io.jsonwebtoken) release is 0.13.0. [1][2]
- Latest artifacts (all at
0.13.0):jjwt-api,jjwt-impl,jjwt-jackson,jjwt-gson, etc. [2] - Latest BOM:
io.jsonwebtoken:jjwt-bom:0.13.0(Aug 20, 2025). [3]
Gradle (recommended via BOM):
dependencies {
implementation platform("io.jsonwebtoken:jjwt-bom:0.13.0")
implementation "io.jsonwebtoken:jjwt-api"
runtimeOnly "io.jsonwebtoken:jjwt-impl"
runtimeOnly "io.jsonwebtoken:jjwt-jackson" // or jjwt-gson
}Maven:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-bom</artifactId>
<version>0.13.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Sources
[1] GitHub Releases – jwtk/jjwt (shows latest release 0.13.0)
[2] javadoc.io – io.jsonwebtoken artifacts (shows latest version 0.13.0)
[3] mvnrepository – io.jsonwebtoken:jjwt-bom (shows latest 0.13.0, dated Aug 20, 2025)
🌐 Web query:
CVE io.jsonwebtoken jjwt 0.11.5 security vulnerabilities
💡 Result:
For io.jsonwebtoken (jjwt) 0.11.5, the main publicly tracked issue you’ll see in scanners is:
- CVE-2024-31033 (aka GHSA-r65j-6h5f-4f92) – “ignored characters” in key handling may cause users to overestimate signing-key strength (reported as affecting JJWT “through 0.12.5,” i.e., it includes 0.11.5). Status: disputed by the JJWT vendor in the NVD record. [1]
- GitHub’s advisory entry for this was withdrawn as disputed, but OSV still lists 0.11.5 among affected versions in the withdrawn GHSA record, which is why many tools still flag it. [2]
- The JJWT project discusses this CVE report (and dispute context) in issue
#930. [3] - Some advisory databases mark this as a false positive for
io.jsonwebtoken:jjwt-impl. [4]
Are there other CVEs for jjwt 0.11.5?
From the sources above, CVE-2024-31033 is the one commonly associated with jjwt 0.11.5; other “JWT CVEs” you might encounter are typically for different libraries/products with similar names.
Practical guidance
- If you’re being blocked purely by policy/scanner findings: document that CVE-2024-31033 is disputed/withdrawn and reference the vendor discussion. [1][2][3]
- If you still want to reduce risk/noise: consider upgrading—Snyk currently reports
jjwt-impllatest as 0.13.0 and “latest non vulnerable version” as 0.13.0. [5]
Sources:
[1] NVD – CVE-2024-31033 https://nvd.nist.gov/vuln/detail/CVE-2024-31033
[2] OSV – GHSA-r65j-6h5f-4f92 (withdrawn) https://osv.dev/vulnerability/GHSA-r65j-6h5f-4f92
[3] jwtk/jjwt Issue #930 jwtk/jjwt#930
[4] GitLab Advisory DB (marked false positive) https://advisories.gitlab.com/pkg/maven/io.jsonwebtoken/jjwt-impl/CVE-2024-31033/
[5] Snyk package page for jjwt-impl@0.11.5 https://security.snyk.io/package/maven/io.jsonwebtoken%3Ajjwt-impl/0.11.5
🌐 Web query:
jjwt library maintenance status current version
💡 Result:
- Current version (latest release): 0.13.0, released Aug 20, 2025. [1][2][3]
- Maintenance status: The project is not archived and is described by maintainers as “supported and maintained by a community of contributors.” [4] The latest release notes also indicate ongoing development is intended (future work planned for 0.14.0, with 0.13.x reserved for emergency fixes). [1]
Sources: [1] GitHub releases, [2] Javadoc.io latest versions, [3] MVNRepository release info, [4] GitHub README.
Upgrade JJWT to 0.13.0 and use the official Bill of Materials approach.
Version 0.11.5 is outdated (0.13.0 released Aug 20, 2025) and has a CVE entry (CVE-2024-31033, though disputed by the vendor). Use the official BOM pattern for proper version alignment across JJWT modules:
Recommended: use jjwt-bom for version management
implementation 'org.springframework.boot:spring-boot-starter-security'
+ // JJWT (via BOM)
+ implementation platform('io.jsonwebtoken:jjwt-bom:0.13.0')
implementation 'io.jsonwebtoken:jjwt-api'
runtimeOnly 'io.jsonwebtoken:jjwt-impl'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson'
-
- // oauth🤖 Prompt for AI Agents
In `@build.gradle` around lines 35 - 44, Replace the hardcoded jjwt module
versions in build.gradle with the official BOM: add a platform import for
io.jsonwebtoken:jjwt-bom:0.13.0 and remove versions from the jjwt dependencies
(jjwt-api, jjwt-impl, jjwt-jackson) so they are resolved by the BOM; keep
jjwt-api as implementation and jjwt-impl/jjwt-jackson as runtimeOnly but do not
specify versions, and ensure the BOM line appears before those dependency
declarations.
| @OneToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false, unique = true) | ||
| private User user; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, locate the Term entity and examine its current configuration
find src -name "Term.java" -type f | head -5Repository: Eatsfine/BE
Length of output: 119
🏁 Script executed:
#!/bin/bash
# Search for User deletion logic (회원탈퇴)
rg -n "delete|withdraw|탈퇴" src/main/java --type java -B 2 -A 5 | head -100Repository: Eatsfine/BE
Length of output: 10362
🏁 Script executed:
#!/bin/bash
# Look for repository interfaces and their delete methods
rg -n "TermRepository|UserRepository" src/main/java --type javaRepository: Eatsfine/BE
Length of output: 1207
🏁 Script executed:
#!/bin/bash
# Search for cascade and orphanRemoval configurations in Term entity and related entities
rg -n "cascade|orphanRemoval|CascadeType" src/main/java --type javaRepository: Eatsfine/BE
Length of output: 1192
🏁 Script executed:
#!/bin/bash
# Get the Term entity definition
cat -n src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.javaRepository: Eatsfine/BE
Length of output: 1353
🏁 Script executed:
#!/bin/bash
# Get the User entity definition to check if it has a relationship with Term
cat -n src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.javaRepository: Eatsfine/BE
Length of output: 2173
🏁 Script executed:
#!/bin/bash
# Verify if there are any other places where Term deletion is handled
rg -n "termRepository\.delete|Term.*delete" src/main/java --type javaRepository: Eatsfine/BE
Length of output: 37
Add cascade deletion to Term.user relationship.
When a user withdraws (deletes account), the related Term record is not deleted, causing FK constraint violations or orphaning data. The withdraw flow directly deletes the User without handling the Term record.
Recommended fix
- `@OneToOne`(fetch = FetchType.LAZY)
+ `@OneToOne`(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
`@JoinColumn`(name = "user_id", nullable = false, unique = true)
private User user;🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java` around
lines 22 - 24, The Term.user OneToOne mapping should cascade deletes so removing
a User also deletes its Term; update the Term entity's `@OneToOne` annotation on
the user field to include cascade = CascadeType.REMOVE (or CascadeType.ALL)
and/or orphanRemoval = true so the related Term is deleted when the User is
deleted; ensure the mapping stays consistent with any inverse mapping on User
(mappedBy) if present and run tests for the user withdraw/delete flow to verify
no FK/orphan issues.
src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java
Outdated
Show resolved
Hide resolved
src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java
Show resolved
Hide resolved
src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java
Show resolved
Hide resolved
src/main/java/com/eatsfine/eatsfine/global/config/properties/JwtProperties.java
Show resolved
Hide resolved
| "/swagger-resources/**" | ||
| ).permitAll() | ||
|
|
||
| .requestMatchers("/auth/**", "/login", "/signup").permitAll() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and read the SecurityConfig.java file to understand the context around line 60
find . -name "SecurityConfig.java" -type f | head -5Repository: Eatsfine/BE
Length of output: 126
🏁 Script executed:
# Search for controller endpoints and their path mappings
rg -n '@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)' --type=java -A 2 | grep -B 2 -E '(auth|login|signup)' | head -50Repository: Eatsfine/BE
Length of output: 1498
🏁 Script executed:
# Also search for any controller class definitions to understand the base paths
rg -n '@RestController|@Controller' --type=java -A 3 | grep -E '(RequestMapping|auth|login|signup)' | head -50Repository: Eatsfine/BE
Length of output: 1000
🏁 Script executed:
# Read the SecurityConfig.java file to see the context around line 60
cat -n ./src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java | sed -n '50,75p'Repository: Eatsfine/BE
Length of output: 1336
🏁 Script executed:
# Get the full content of SecurityConfig to understand the entire config
wc -l ./src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.javaRepository: Eatsfine/BE
Length of output: 129
Remove unused path patterns that don't match actual controller endpoints.
Line 60 permits /auth/**, /login, and /signup, but all actual authentication controller endpoints use the /api/auth/** prefix (e.g., /api/auth/signup, /api/auth/login). The /api/auth/** pattern is already permitted on line 52, making line 60 redundant and dead code. Remove it to reduce confusion.
🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java` at
line 60, Remove the redundant/incorrect permit rule in SecurityConfig that
references requestMatchers("/auth/**", "/login", "/signup") — locate the
HttpSecurity configuration method in the SecurityConfig class (the block that
builds authorizeHttpRequests()/requestMatchers()) and delete that
requestMatchers(...) call so only the correct permitted pattern "/api/auth/**"
remains; this eliminates the dead/unused path patterns and keeps authorization
rules consistent with the controller endpoints.
| @Bean | ||
| public CorsConfigurationSource corsConfigurationSource() { // cors 설정 | ||
| CorsConfiguration config = new CorsConfiguration(); | ||
| config.setAllowedOriginPatterns(List.of("*")); // 운영 환경에서는 정확한 도메인만 명시 |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Security: Wildcard origin pattern with allowCredentials(true) is risky for production.
setAllowedOriginPatterns(List.of("*")) combined with setAllowCredentials(true) allows any origin to send credentials. Before deploying to production, restrict origins to your actual frontend domains to prevent CSRF-like attacks.
Also applies to: 79-79
🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java` at
line 74, The CORS config currently calls
config.setAllowedOriginPatterns(List.of("*")) while also calling
config.setAllowCredentials(true), which permits credentials from any origin; in
SecurityConfig you should restrict allowed origins to your production frontend
domains (replace the wildcard with an explicit list of origins) or remove
allowCredentials(true) if credentials are not needed; update the code paths that
call config.setAllowedOriginPatterns and config.setAllowCredentials in
SecurityConfig (and the similar occurrence around line 79) so only trusted
origins are listed (e.g., a configurable list via application properties) and
ensure allowCredentials remains true only when using explicit origins.
src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java
Outdated
Show resolved
Hide resolved
src/main/java/com/eatsfine/eatsfine/global/validator/valid/PasswordMatchValidator.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java`:
- Around line 45-65: The login controller is not triggering bean validation
because the `@Valid` annotation is missing on the request body parameter; update
the login(...) method signature in UserController to annotate the
UserRequestDto.LoginDto parameter with `@Valid` (i.e., change the parameter from
`@RequestBody` UserRequestDto.LoginDto loginDto to `@RequestBody` `@Valid`
UserRequestDto.LoginDto loginDto) and ensure the proper
javax/jakarta.validation.Valid import is added so the `@NotBlank/`@Email
constraints on LoginDto are enforced.
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java`:
- Around line 89-107: updateMemberInfo currently assumes updateDto is non-null
and calls updateDto.getNickName()/getPhoneNumber(), causing an NPE when
updateDto is null (e.g., called from updateProfileImage). Fix by null-checking
updateDto at the start of updateMemberInfo (or treat it as optional): if
updateDto == null skip nickname/phone update logic (or create a safe local
reference), then proceed with profileImage handling and transaction logic;
update references to UserRequestDto.UpdateDto and calls like
updateDto.getNickName() and updateDto.getPhoneNumber() in the method to be
guarded by this null check so no dereference occurs when updateDto is null.
In `@src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java`:
- Around line 23-24: Replace the detailed exception message that includes the
email in UserDetailsServiceImpl where userRepository.findByEmail(email) throws
UsernameNotFoundException; throw a generic message (e.g., "User not found" or
"Invalid credentials") instead of including the email, and if you need the email
for debugging record it via the class logger (e.g., logger.debug or logger.info)
rather than exposing it in the exception message.
🧹 Nitpick comments (3)
src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java (1)
34-36: Unused dependency:jwtTokenProvider.
JwtTokenProvideris injected but never used in this controller. Token validation is handled withinUserServiceImpl.getCurrentUser(). Consider removing this unused field.♻️ Proposed fix
public class UserController { private final UserService userService; - private final JwtTokenProvider jwtTokenProvider; private final AuthCookieProvider authCookieProvider;src/main/java/com/eatsfine/eatsfine/domain/user/dto/request/UserRequestDto.java (1)
60-66: Consider adding validation toUpdateDto.email.The
UpdateDtohas no validation constraints. If a user attempts to update their email to an invalid format, it will pass through without validation. If email updates are supported, consider adding♻️ Proposed fix (if email updates are allowed)
`@Getter` `@Setter` public static class UpdateDto { + `@Email`(message = "유효한 이메일 형식이어야 합니다.") private String email; private String nickName; private String phoneNumber; }src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java (1)
82-87: ConsiderreadOnly = truefor read-only transaction.
getMemberInfoonly reads data. Using@Transactional(readOnly = true)enables potential optimizations (e.g., no dirty checking, replica routing).♻️ Proposed fix
`@Override` -@Transactional +@Transactional(readOnly = true) public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) {
| @PostMapping("/api/auth/login") | ||
| @Operation(summary = "로그인 API", description = "사용자 로그인을 처리하는 API입니다.") | ||
| public ResponseEntity<ApiResponse<UserResponseDto.LoginResponseDto>> login(@RequestBody UserRequestDto.LoginDto loginDto) { | ||
| UserResponseDto.LoginResponseDto loginResult = userService.login(loginDto); | ||
|
|
||
| if (loginResult.getRefreshToken() == null || loginResult.getRefreshToken().isBlank()) { | ||
| throw new UserException(UserErrorStatus.REFRESH_TOKEN_NOT_ISSUED); | ||
| } | ||
|
|
||
| ResponseCookie refreshCookie = authCookieProvider.refreshTokenCookie(loginResult.getRefreshToken()); | ||
|
|
||
| UserResponseDto.LoginResponseDto body = UserResponseDto.LoginResponseDto.builder() | ||
| .id(loginResult.getId()) | ||
| .accessToken(loginResult.getAccessToken()) | ||
| .refreshToken(null) | ||
| .build(); | ||
|
|
||
| return ResponseEntity.ok() | ||
| .header(HttpHeaders.SET_COOKIE, refreshCookie.toString()) | ||
| .body(ApiResponse.onSuccess(body)); | ||
| } |
There was a problem hiding this comment.
Missing @Valid annotation on login request body.
The LoginDto has @NotBlank and @Email validation constraints, but @Valid is not applied to the parameter. Validation will not be triggered.
🛡️ Proposed fix
- public ResponseEntity<ApiResponse<UserResponseDto.LoginResponseDto>> login(`@RequestBody` UserRequestDto.LoginDto loginDto) {
+ public ResponseEntity<ApiResponse<UserResponseDto.LoginResponseDto>> login(`@RequestBody` `@Valid` UserRequestDto.LoginDto loginDto) {🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/controller/UserController.java`
around lines 45 - 65, The login controller is not triggering bean validation
because the `@Valid` annotation is missing on the request body parameter; update
the login(...) method signature in UserController to annotate the
UserRequestDto.LoginDto parameter with `@Valid` (i.e., change the parameter from
`@RequestBody` UserRequestDto.LoginDto loginDto to `@RequestBody` `@Valid`
UserRequestDto.LoginDto loginDto) and ensure the proper
javax/jakarta.validation.Valid import is added so the `@NotBlank/`@Email
constraints on LoginDto are enforced.
| @Override | ||
| @Transactional | ||
| public String updateMemberInfo(UserRequestDto.UpdateDto updateDto, | ||
| MultipartFile profileImage, | ||
| HttpServletRequest request) { | ||
|
|
||
| User user = getCurrentUser(request); | ||
|
|
||
| boolean changed = false; | ||
|
|
||
| //닉네임/전화번호 부분 수정 | ||
| if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) { | ||
| user.updateNickname(updateDto.getNickName()); | ||
| changed = true; | ||
| } | ||
| if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) { | ||
| user.updatePhoneNumber(updateDto.getPhoneNumber()); | ||
| changed = true; | ||
| } |
There was a problem hiding this comment.
Null updateDto will cause NullPointerException.
When called from updateProfileImage() controller endpoint, updateDto is null. Accessing updateDto.getNickName() at line 100 will throw NPE.
🐛 Proposed fix
public String updateMemberInfo(UserRequestDto.UpdateDto updateDto,
MultipartFile profileImage,
HttpServletRequest request) {
User user = getCurrentUser(request);
boolean changed = false;
//닉네임/전화번호 부분 수정
- if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) {
- user.updateNickname(updateDto.getNickName());
- changed = true;
- }
- if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) {
- user.updatePhoneNumber(updateDto.getPhoneNumber());
- changed = true;
+ if (updateDto != null) {
+ if (updateDto.getNickName() != null && !updateDto.getNickName().isBlank()) {
+ user.updateNickname(updateDto.getNickName());
+ changed = true;
+ }
+ if (updateDto.getPhoneNumber() != null && !updateDto.getPhoneNumber().isBlank()) {
+ user.updatePhoneNumber(updateDto.getPhoneNumber());
+ changed = true;
+ }
}🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/domain/user/service/UserServiceImpl.java`
around lines 89 - 107, updateMemberInfo currently assumes updateDto is non-null
and calls updateDto.getNickName()/getPhoneNumber(), causing an NPE when
updateDto is null (e.g., called from updateProfileImage). Fix by null-checking
updateDto at the start of updateMemberInfo (or treat it as optional): if
updateDto == null skip nickname/phone update logic (or create a safe local
reference), then proceed with profileImage handling and transaction logic;
update references to UserRequestDto.UpdateDto and calls like
updateDto.getNickName() and updateDto.getPhoneNumber() in the method to be
guarded by this null check so no dereference occurs when updateDto is null.
| com.eatsfine.eatsfine.domain.user.entity.User user = userRepository.findByEmail(email) | ||
| .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다: " + email)); |
There was a problem hiding this comment.
Avoid user enumeration via detailed not-found messages.
Including the email in the exception message can leak account existence if the message is exposed. Prefer a generic message and log the email internally if needed.
✅ Suggested change
- com.eatsfine.eatsfine.domain.user.entity.User user = userRepository.findByEmail(email)
- .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다: " + email));
+ com.eatsfine.eatsfine.domain.user.entity.User user = userRepository.findByEmail(email)
+ .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다."));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| com.eatsfine.eatsfine.domain.user.entity.User user = userRepository.findByEmail(email) | |
| .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다: " + email)); | |
| com.eatsfine.eatsfine.domain.user.entity.User user = userRepository.findByEmail(email) | |
| .orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다.")); |
🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/global/auth/UserDetailsServiceImpl.java`
around lines 23 - 24, Replace the detailed exception message that includes the
email in UserDetailsServiceImpl where userRepository.findByEmail(email) throws
UsernameNotFoundException; throw a generic message (e.g., "User not found" or
"Invalid credentials") instead of including the email, and if you need the email
for debugging record it via the class logger (e.g., logger.debug or logger.info)
rather than exposing it in the exception message.
💡 작업 개요
✅ 작업 내용
🧪 테스트 내용
📝 기타 참고 사항
[FEAT]: 회원가입/로그인 API 구현 #64
Summary by CodeRabbit
New Features
Security
Validation & Errors