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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ CEOS 22기 백엔드 스터디 - CGV 클론 코딩 프로젝트
- wk3
- [✨ wk3: 인증 방법 정리](https://github.com/yooniicode/spring-cgv-22nd/wiki/wk3:-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC)
- [✨ wk3: 로그인 구현하기](https://github.com/yooniicode/spring-cgv-22nd/wiki/wk3;-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0)

- wk4
- [✨ wk4: 동시성 문제 해결(락)](https://github.com/yooniicode/spring-cgv-22nd/wiki/wk4-:-%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95)
1 change: 0 additions & 1 deletion cgvclone/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ dependencies {
testAnnotationProcessor 'org.projectlombok:lombok'
// runtimeOnly 'com.h2database:h2'

// 버전 명시 안하면 안되는 이유는 멀까.. .. ㅜㅜ
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.ceos22.spring_boot.common.auth.controller;

import com.ceos22.spring_boot.common.auth.dto.LoginRequestDto;
import com.ceos22.spring_boot.common.auth.dto.LoginResponseDto;
import com.ceos22.spring_boot.common.auth.security.jwt.JwtTokenProvider;
import com.ceos22.spring_boot.common.response.ApiResponse;
import com.ceos22.spring_boot.common.response.status.SuccessStatus;
import com.ceos22.spring_boot.domain.user.User;
import com.ceos22.spring_boot.domain.user.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
Expand All @@ -16,58 +18,36 @@
@Tag(name = "Auth API", description = "인증 관련 API")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthenticationManager am;
private final JwtTokenProvider tokenProvider;
private final UserService userService;

public AuthController(AuthenticationManager am, JwtTokenProvider tokenProvider, UserService userService) {
this.am = am;
this.tokenProvider = tokenProvider;
this.userService = userService;
}

@Operation(
summary = "로그인",
description = "사용자 이름과 비밀번호로 로그인 후 JWT 액세스 토큰을 발급받습니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "로그인 성공, 토큰 발급",
content = @Content(
schema = @Schema(implementation = TokenResponse.class)
)
),
@ApiResponse(responseCode = "401", description = "인증 실패")
}
description = "사용자 이름과 비밀번호로 로그인 후 JWT 액세스 토큰을 발급받습니다."
)
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest req) {
public ResponseEntity<ApiResponse<LoginResponseDto>> login(@RequestBody LoginRequestDto req) {
Authentication auth = am.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password())
);

String token = tokenProvider.issueToken(auth);
return ResponseEntity.ok(TokenResponse.bearer(token));
}

public record LoginRequest(String username, String password) {}
public record TokenResponse(String accessToken, String tokenType) {
public static TokenResponse bearer(String t) { return new TokenResponse(t, "Bearer"); }
User user = userService.findByUsername(req.username());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: fetching user again by username after authentication succeeds is redundant - the authenticated principal already contains user details. Can you extract user details from the Authentication object instead of querying the database again?

Prompt To Fix With AI
This is a comment left during a code review.
Path: cgvclone/src/main/java/com/ceos22/spring_boot/common/auth/controller/AuthController.java
Line: 40:40

Comment:
**style:** fetching user again by username after authentication succeeds is redundant - the authenticated principal already contains user details. Can you extract user details from the Authentication object instead of querying the database again?

How can I resolve this? If you propose a fix, please make it concise.

long expiresIn = tokenProvider.getExpiration(token);
LoginResponseDto response = LoginResponseDto.of(token, user, expiresIn);
return ApiResponse.onSuccess(SuccessStatus.LOGIN_SUCCESS, response);
}



@Operation(
summary = "회원가입",
description = "새로운 사용자를 등록합니다. 비밀번호는 솔트와 함께 해시되어 저장됩니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "회원가입 성공",
content = @Content(schema = @Schema(implementation = AuthController.RegisterResponse.class))
),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "409", description = "중복된 사용자")
}
description = "새로운 사용자를 등록합니다. 비밀번호는 솔트와 함께 해시되어 저장됩니다."
)
@PostMapping("/register")
public ResponseEntity<AuthController.RegisterResponse> register(@RequestBody AuthController.RegisterRequest req) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ceos22.spring_boot.common.auth.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

public record LoginRequestDto(
@NotBlank
@Schema(description = "사용자 이름", example = "evan523")
String username,

@NotBlank
@Schema(description = "비밀번호", example = "password123")
String password
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ceos22.spring_boot.common.auth.dto;

import com.ceos22.spring_boot.domain.user.User;

import java.util.UUID;

public record LoginResponseDto(
String accessToken,
String tokenType,
UUID publicId,
String username,
String name,
String email,
long expiresIn
) {
public static LoginResponseDto of(String token, User user, long expiresIn) {
return new LoginResponseDto(
token,
"Bearer",
user.getPublicId(),
user.getUsername(),
user.getName(),
user.getEmail(),
expiresIn
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;

@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(String secret, long accessTokenValidityInSeconds) {}
public record JwtProperties(String secret, Duration accessTokenValidity) {}
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ public Authentication authenticate(Authentication authentication) throws Authent
String username = String.valueOf(authentication.getPrincipal());
String rawPassword = String.valueOf(authentication.getCredentials());

User u = users.findByUsername(username)
User user = users.findByUsername(username)
.orElseThrow(() ->
new AuthFailureException(ErrorStatus.USER_NOT_FOUND, "존재하지 않는 사용자입니다.")
);

if (!saltService.matches(rawPassword, u.getSalt(), u.getPasswordHash())) {
if (!saltService.matches(rawPassword, user.getSalt(), user.getPasswordHash())) {
throw new AuthFailureException(ErrorStatus.INVALID_CREDENTIALS, "아이디 또는 비밀번호가 올바르지 않습니다.");
}

var authorities = List.<GrantedAuthority>of(() -> "ROLE_USER");
var principal = new CustomUserPrincipal(
u.getUserId(), u.getPublicId(), u.getUsername(),
u.getPasswordHash(), u.getName(), u.getEmail(), authorities
user.getUserId(), user.getPublicId(), user.getUsername(),
user.getPasswordHash(), user.getName(), user.getEmail(), authorities
);
return new UsernamePasswordAuthenticationToken(principal, null, authorities);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,37 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;
import java.io.IOException;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper om = new ObjectMapper();
private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest req,
HttpServletResponse res,
AuthenticationException ex) throws IOException {
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
ErrorStatus status;
String message = ex.getMessage();

if (ex instanceof AuthFailureException afe) {
log.warn("Unauthorized access: {}", exception.getMessage());

if (exception instanceof AuthFailureException afe) {
status = afe.getErrorStatus();
} else {
status = ErrorStatus._UNAUTHORIZED;
}

var responseEntity = ApiResponse.onFailure(status);

res.setStatus(status.getHttpStatus().value());
res.setContentType("application/json;charset=UTF-8");
om.writeValue(res.getWriter(), responseEntity.getBody());
response.setStatus(status.getHttpStatus().value());
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), responseEntity.getBody());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ protected void doFilterInternal(HttpServletRequest request,

String token = resolveToken(request);

log.info("Authorization Header = {}", request.getHeader("Authorization"));
log.info("Parsed token = {}", token);
if (log.isDebugEnabled()) {
log.debug("Authorization Header = {}", request.getHeader("Authorization"));
log.debug("Parsed token = {}", token);
}

if (token != null && tokenProvider.validate(token)) {
var authentication = tokenProvider.getAuthentication(token);
Expand All @@ -46,7 +48,10 @@ protected void doFilterInternal(HttpServletRequest request,
private String resolveToken(HttpServletRequest req) {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
String token = header.substring(7);
if (!token.isBlank()) {
return token;
}
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
public class JwtTokenProvider {

private static final String AUTH_CLAIM = "auth";
Expand All @@ -26,7 +28,7 @@ public class JwtTokenProvider {

public JwtTokenProvider(JwtProperties props, UserRepository users) {
this.key = Keys.hmacShaKeyFor(props.secret().getBytes(StandardCharsets.UTF_8));
this.validityMillis = props.accessTokenValidityInSeconds() * 1000L;
this.validityMillis = props.accessTokenValidity().toMillis();
this.users = users;
}

Expand All @@ -37,8 +39,17 @@ public String issueToken(Authentication authentication) {
.map(a -> a.getAuthority())
.collect(Collectors.joining(","));

Object principal = authentication.getPrincipal();

String subject;
if (principal instanceof CustomUserPrincipal customPrincipal) {
subject = customPrincipal.getPublicId().toString();
} else {
subject = authentication.getName();
}

return Jwts.builder()
.setSubject(authentication.getName())
.setSubject(subject)
.claim(AUTH_CLAIM, authorities)
.setIssuedAt(now)
.setExpiration(expiry)
Expand All @@ -48,16 +59,25 @@ public String issueToken(Authentication authentication) {

public Authentication getAuthentication(String token) {
Claims claims = parseClaims(token);
String username = claims.getSubject();

String subject = claims.getSubject();
UUID publicId;
try {
publicId = UUID.fromString(subject);
} catch (IllegalArgumentException e) {
throw new GeneralException(ErrorStatus.INVALID_TOKEN, "잘못된 토큰 형식입니다: " + subject);
}

String authStr = claims.get(AUTH_CLAIM, String.class);
var authorities = Optional.ofNullable(authStr)
.stream()
.flatMap(s -> Arrays.stream(s.split(",")))
.filter(s -> !s.isBlank())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User u = users.findByUsername(username)
.orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST, "사용자를 찾을 수 없습니다: " + username));
User u = users.findByPublicId(publicId)
.orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST, "사용자를 찾을 수 없습니다: " + publicId));
Comment on lines +78 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Database lookup on every request for token validation can impact performance. Consider caching user data or only fetching when necessary.

Prompt To Fix With AI
This is a comment left during a code review.
Path: cgvclone/src/main/java/com/ceos22/spring_boot/common/auth/security/jwt/JwtTokenProvider.java
Line: 70:71

Comment:
**style:** Database lookup on every request for token validation can impact performance. Consider caching user data or only fetching when necessary.

How can I resolve this? If you propose a fix, please make it concise.



var principal = new CustomUserPrincipal(
u.getUserId(),
Expand All @@ -77,14 +97,23 @@ public boolean validate(String token) {
parseClaims(token);
return true;
} catch (ExpiredJwtException e) {
return false;
log.info("JWT expired at {}", e.getClaims().getExpiration());
throw new GeneralException(ErrorStatus.TOKEN_EXPIRED, "토큰이 만료되었습니다.");
} catch (JwtException | IllegalArgumentException e) {
return false;
throw new GeneralException(ErrorStatus.INVALID_TOKEN, "유효하지 않은 토큰입니다.");
}
}


private Claims parseClaims(String token) {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody();
}

public long getExpiration(String token) {
Claims claims = parseClaims(token);
Date expiration = claims.getExpiration();
return (expiration.getTime() - System.currentTimeMillis()) / 1000L;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.ceos22.spring_boot.common.enums;

public enum PaymentMethod {
카드, 현금, 네이버페이, 카카오페이
CARD, CASH, NAVER, KAKAO
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ public ResponseEntity<ApiResponse<Void>> handleNoResourceFoundException(NoResour
return ApiResponse.onFailure(ErrorStatus._NOT_FOUND);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(GeneralException.class)
public ResponseEntity<ApiResponse<Void>> handleGeneralException(GeneralException e) {
return ApiResponse.onFailure(e.getErrorStatus(), e.getMessage());
Expand All @@ -58,5 +53,9 @@ public ResponseEntity<ApiResponse<Void>> handleAuthFailure(AuthFailureException
return ApiResponse.onFailure(e.getErrorStatus(), e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
}
Comment on lines +56 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Log the exception details before returning generic error response to aid debugging of unexpected errors

Suggested change
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
log.error("Unhandled exception: ", e);
return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: cgvclone/src/main/java/com/ceos22/spring_boot/common/exception/GlobalExceptionHandler.java
Line: 56:59

Comment:
**style:** Log the exception details before returning generic error response to aid debugging of unexpected errors

```suggestion
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
        log.error("Unhandled exception: ", e);
        return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
    }
```

How can I resolve this? If you propose a fix, please make it concise.


}
Loading