diff --git a/src/main/java/com/beat/domain/booking/api/BookingApi.java b/src/main/java/com/beat/domain/booking/api/BookingApi.java index be93a150..ac1e2553 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingApi.java +++ b/src/main/java/com/beat/domain/booking/api/BookingApi.java @@ -19,6 +19,7 @@ import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -92,6 +93,7 @@ ResponseEntity>> getMemberBo @CurrentMember Long memberId ); + @DisableSwaggerSecurity @Operation(summary = "비회원 예매 API", description = "비회원이 예매를 요청하는 POST API입니다.") @ApiResponses( value = { @@ -125,6 +127,7 @@ ResponseEntity> createGuestBookings( @RequestBody GuestBookingRequest guestBookingRequest ); + @DisableSwaggerSecurity @Operation(summary = "비회원 예매 조회 API", description = "비회원이 예매를 조회하는 POST API입니다.") @ApiResponses( value = { @@ -143,6 +146,7 @@ ResponseEntity>> getGuestBook @RequestBody GuestBookingRetrieveRequest guestBookingRetrieveRequest ); + @DisableSwaggerSecurity @Operation(summary = "유료공연 예매 환불 요청 API", description = "유료공연 예매자가 환불 요청하는 PATCH API입니다.") @ApiResponses( value = { @@ -161,6 +165,7 @@ ResponseEntity> refundBookings( @RequestBody BookingRefundRequest bookingRefundRequest ); + @DisableSwaggerSecurity @Operation(summary = "무료공연/미입금 예매 취소 요청 API", description = "무료공연/미입금 예매자가 취소 요청하는 PATCH API입니다.") @ApiResponses( value = { diff --git a/src/main/java/com/beat/domain/member/api/MemberApi.java b/src/main/java/com/beat/domain/member/api/MemberApi.java index 89e052b0..e6309ca1 100644 --- a/src/main/java/com/beat/domain/member/api/MemberApi.java +++ b/src/main/java/com/beat/domain/member/api/MemberApi.java @@ -1,14 +1,17 @@ package com.beat.domain.member.api; -import java.security.Principal; - import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import com.beat.domain.member.dto.AccessTokenGetSuccess; import com.beat.domain.member.dto.LoginSuccessResponse; +import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.auth.client.dto.MemberLoginRequest; import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -21,6 +24,7 @@ @Tag(name = "Member", description = "회원 관련 API") public interface MemberApi { + @DisableSwaggerSecurity @Operation(summary = "로그인/회원가입 API", description = "로그인/회원가입하는 POST API입니다.") @ApiResponses( value = { @@ -41,8 +45,8 @@ public interface MemberApi { } ) ResponseEntity> signUp( - String authorizationCode, - MemberLoginRequest loginRequest, + @RequestParam final String authorizationCode, + @RequestBody final MemberLoginRequest loginRequest, HttpServletResponse response ); @@ -60,8 +64,8 @@ ResponseEntity> signUp( ) } ) - ResponseEntity> refreshToken( - String refreshToken + ResponseEntity> issueAccessTokenUsingRefreshToken( + @RequestHeader("Authorization_Refresh") final String refreshToken ); @Operation(summary = "로그아웃 API", description = "로그아웃하는 POST API입니다.") @@ -78,6 +82,8 @@ ResponseEntity> refreshToken( ) } ) - ResponseEntity> signOut(Principal principal); + ResponseEntity> signOut( + @CurrentMember final Long memberId + ); } diff --git a/src/main/java/com/beat/domain/member/api/MemberController.java b/src/main/java/com/beat/domain/member/api/MemberController.java index 63a01720..227f6034 100644 --- a/src/main/java/com/beat/domain/member/api/MemberController.java +++ b/src/main/java/com/beat/domain/member/api/MemberController.java @@ -1,13 +1,11 @@ package com.beat.domain.member.api; -import java.security.Principal; - -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -17,6 +15,7 @@ import com.beat.domain.member.dto.AccessTokenGetSuccess; import com.beat.domain.member.dto.LoginSuccessResponse; import com.beat.domain.member.exception.MemberSuccessCode; +import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.auth.client.dto.MemberLoginRequest; import com.beat.global.auth.jwt.application.TokenService; import com.beat.global.common.dto.SuccessResponse; @@ -52,7 +51,7 @@ public ResponseEntity> signUp( .httpOnly(true) .build(); response.setHeader("Set-Cookie", cookie.toString()); - return ResponseEntity.status(HttpStatus.OK) + return ResponseEntity.ok() .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, LoginSuccessResponse.of(loginSuccessResponse.accessToken(), null, loginSuccessResponse.nickname(), loginSuccessResponse.role()))); @@ -60,22 +59,21 @@ public ResponseEntity> signUp( @Override @GetMapping("/refresh-token") - public ResponseEntity> refreshToken( - @RequestParam final String refreshToken + public ResponseEntity> issueAccessTokenUsingRefreshToken( + @RequestHeader("Authorization_Refresh") final String refreshToken ) { - AccessTokenGetSuccess accessTokenGetSuccess = authenticationService.generateAccessTokenFromRefreshToken( - refreshToken); - return ResponseEntity.status(HttpStatus.OK) - .body(SuccessResponse.of(MemberSuccessCode.ISSUE_REFRESH_TOKEN_SUCCESS, accessTokenGetSuccess)); + AccessTokenGetSuccess accessTokenGetSuccess = authenticationService.generateAccessTokenFromRefreshToken(refreshToken); + return ResponseEntity.ok() + .body(SuccessResponse.of(MemberSuccessCode.ISSUE_ACCESS_TOKEN_USING_REFRESH_TOKEN, accessTokenGetSuccess)); } @Override @PostMapping("/sign-out") public ResponseEntity> signOut( - final Principal principal + @CurrentMember final Long memberId ) { - tokenService.deleteRefreshToken(Long.valueOf(principal.getName())); - return ResponseEntity.status(HttpStatus.OK) + tokenService.deleteRefreshToken(memberId); + return ResponseEntity.ok() .body(SuccessResponse.from(MemberSuccessCode.SIGN_OUT_SUCCESS)); } -} \ No newline at end of file +} diff --git a/src/main/java/com/beat/domain/member/application/AuthenticationService.java b/src/main/java/com/beat/domain/member/application/AuthenticationService.java index c164f5b8..b712574c 100644 --- a/src/main/java/com/beat/domain/member/application/AuthenticationService.java +++ b/src/main/java/com/beat/domain/member/application/AuthenticationService.java @@ -30,7 +30,7 @@ @Service @RequiredArgsConstructor public class AuthenticationService { - + private static final String BEARER_PREFIX = "Bearer "; private final JwtTokenProvider jwtTokenProvider; private final TokenService tokenService; @@ -69,11 +69,16 @@ public LoginSuccessResponse generateLoginSuccessResponse(final Long memberId, fi * Refresh Token에서 사용자 ID와 Role 정보를 추출한 후, * Role에 따라 Admin 또는 Member 권한으로 새로운 Access Token을 발급합니다. * - * @param refreshToken 사용자의 Refresh Token + * @param refreshTokenWithBearer "Bearer + 사용자의 Refresh Token" * @return 새로운 Access Token 정보가 포함된 AccessTokenGetSuccess 객체 */ @Transactional - public AccessTokenGetSuccess generateAccessTokenFromRefreshToken(final String refreshToken) { + public AccessTokenGetSuccess generateAccessTokenFromRefreshToken(final String refreshTokenWithBearer) { + String refreshToken = refreshTokenWithBearer; + if (refreshToken.startsWith(BEARER_PREFIX)) { + refreshToken = refreshToken.substring(BEARER_PREFIX.length()); + } + log.info("Validation result for refresh token: {}", jwtTokenProvider.validateToken(refreshToken)); JwtValidationType validationType = jwtTokenProvider.validateToken(refreshToken); @@ -139,4 +144,4 @@ private UsernamePasswordAuthenticationToken createAuthenticationToken(Long membe return new MemberAuthentication(memberId, null, authorities); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java b/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java index 30a8d0ad..6963ed17 100644 --- a/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java +++ b/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java @@ -13,7 +13,7 @@ public enum MemberSuccessCode implements BaseSuccessCode { */ SIGN_UP_SUCCESS(200, "로그인 성공"), ISSUE_ACCESS_TOKEN_SUCCESS(200, "엑세스토큰 발급 성공"), - ISSUE_REFRESH_TOKEN_SUCCESS(200, "리프레쉬토큰 발급 성공"), + ISSUE_ACCESS_TOKEN_USING_REFRESH_TOKEN(200, "리프레쉬 토큰으로 액세스 토큰 재발급 성공"), SIGN_OUT_SUCCESS(200, "로그아웃 성공"), USER_DELETE_SUCCESS(200, "회원 탈퇴 성공"); diff --git a/src/main/java/com/beat/domain/performance/api/HomeApi.java b/src/main/java/com/beat/domain/performance/api/HomeApi.java index cb550a8c..7f48e695 100644 --- a/src/main/java/com/beat/domain/performance/api/HomeApi.java +++ b/src/main/java/com/beat/domain/performance/api/HomeApi.java @@ -5,12 +5,10 @@ import com.beat.domain.performance.application.dto.home.HomeFindAllResponse; import com.beat.domain.performance.domain.Genre; -import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; 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.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,6 +16,7 @@ @Tag(name = "Home", description = "홈 화면에서 공연 및 홍보목록 조회 API") public interface HomeApi { + @DisableSwaggerSecurity @Operation(summary = "전체 공연 및 홍보 목록 조회", description = "홈 화면에서 전체 공연 목록 및 홍보 목록을 조회하는 GET API") @ApiResponses( value = { diff --git a/src/main/java/com/beat/domain/performance/api/PerformanceApi.java b/src/main/java/com/beat/domain/performance/api/PerformanceApi.java index f9d4b52b..87298ab3 100644 --- a/src/main/java/com/beat/domain/performance/api/PerformanceApi.java +++ b/src/main/java/com/beat/domain/performance/api/PerformanceApi.java @@ -15,6 +15,7 @@ import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -108,6 +109,7 @@ ResponseEntity> getPerformanceF @PathVariable Long performanceId ); + @DisableSwaggerSecurity @Operation(summary = "공연 상세정보 조회 API", description = "공연 상세페이지의 공연 상세정보를 조회하는 GET API입니다.") @ApiResponses( value = { @@ -126,6 +128,7 @@ ResponseEntity> getPerformanceDetail( @PathVariable Long performanceId ); + @DisableSwaggerSecurity @Operation(summary = "예매하기 관련 공연 정보 조회 API", description = "예매하기 페이지에서 필요한 예매 관련 공연 정보를 조회하는 GET API입니다.") @ApiResponses( value = { diff --git a/src/main/java/com/beat/domain/schedule/api/ScheduleApi.java b/src/main/java/com/beat/domain/schedule/api/ScheduleApi.java index 0dce018e..cfcdaea2 100644 --- a/src/main/java/com/beat/domain/schedule/api/ScheduleApi.java +++ b/src/main/java/com/beat/domain/schedule/api/ScheduleApi.java @@ -7,6 +7,7 @@ import com.beat.domain.schedule.application.dto.TicketAvailabilityResponse; import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -18,6 +19,7 @@ @Tag(name = "Schedule", description = "스케줄 관련 API") public interface ScheduleApi { + @DisableSwaggerSecurity @Operation(summary = "티켓 구매 가능 여부 조회 API", description = "티켓 구매 가능 여부를 확인하는 GET API입니다.") @ApiResponses( value = { diff --git a/src/main/java/com/beat/domain/user/api/HealthCheckApi.java b/src/main/java/com/beat/domain/user/api/HealthCheckApi.java index 97f7ab9c..d855f033 100644 --- a/src/main/java/com/beat/domain/user/api/HealthCheckApi.java +++ b/src/main/java/com/beat/domain/user/api/HealthCheckApi.java @@ -2,6 +2,8 @@ import org.springframework.web.bind.annotation.GetMapping; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -10,15 +12,12 @@ @Tag(name = "Health-Check", description = "헬스 체크 API") public interface HealthCheckApi { - @Operation( - summary = "헬스 체크 조회 API", - description = "서버 상태를 확인하기 위한 헬스 체크 API로, 정상적으로 동작할 경우 'OK' 문자열을 반환합니다." - ) + @DisableSwaggerSecurity + @Operation(summary = "헬스 체크 조회 API", description = "서버 상태를 확인하기 위한 헬스 체크 API로, 정상적으로 동작할 경우 'OK' 문자열을 반환합니다.") @ApiResponses( value = { @ApiResponse(responseCode = "200", description = "서버가 정상적으로 동작 중입니다.") } ) - @GetMapping String healthcheck(); } diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java index 1ac5d6ea..3feaa269 100644 --- a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java @@ -54,38 +54,6 @@ public String issueRefreshToken(final Authentication authentication) { return issueToken(authentication, refreshTokenExpireTime); } - private String issueToken(final Authentication authentication, final long expiredTime) { - final Date now = new Date(); - - final Claims claims = Jwts.claims().setIssuedAt(now).setExpiration(new Date(now.getTime() + expiredTime)); - - claims.put(MEMBER_ID, authentication.getPrincipal()); - log.info("Added member ID to claims: {}", authentication.getPrincipal()); - log.info("Authorities before token generation: {}", authentication.getAuthorities()); - - String role = authentication.getAuthorities() - .stream() - .map(GrantedAuthority::getAuthority) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No authorities found for user")); - - log.info("Selected role for token: {}", role); - - claims.put(ROLE_KEY, role); - log.info("Added role to claims: {}", role); - - return Jwts.builder() - .setHeaderParam(Header.TYPE, Header.JWT_TYPE) - .setClaims(claims) - .signWith(getSigningKey()) - .compact(); - } - - private SecretKey getSigningKey() { - String encodedKey = Base64.getEncoder().encodeToString(jwtSecret.getBytes()); - return Keys.hmacShaKeyFor(encodedKey.getBytes()); - } - public JwtValidationType validateToken(String token) { try { Claims claims = getBody(token); @@ -108,10 +76,6 @@ public JwtValidationType validateToken(String token) { } } - private Claims getBody(final String token) { - return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); - } - public Long getMemberIdFromJwt(String token) { Claims claims = getBody(token); Long memberId = Long.valueOf(claims.get(MEMBER_ID).toString()); @@ -134,4 +98,40 @@ public Role getRoleFromJwt(String token) { return Role.valueOf(enumValue.toUpperCase()); } -} \ No newline at end of file + + private String issueToken(final Authentication authentication, final long expiredTime) { + final Date now = new Date(); + + final Claims claims = Jwts.claims().setIssuedAt(now).setExpiration(new Date(now.getTime() + expiredTime)); + + claims.put(MEMBER_ID, authentication.getPrincipal()); + log.info("Added member ID to claims: {}", authentication.getPrincipal()); + log.info("Authorities before token generation: {}", authentication.getAuthorities()); + + String role = authentication.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No authorities found for user")); + + log.info("Selected role for token: {}", role); + + claims.put(ROLE_KEY, role); + log.info("Added role to claims: {}", role); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(jwtSecret.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } +} diff --git a/src/main/java/com/beat/global/swagger/annotation/DisableSwaggerSecurity.java b/src/main/java/com/beat/global/swagger/annotation/DisableSwaggerSecurity.java new file mode 100644 index 00000000..d49c8fd6 --- /dev/null +++ b/src/main/java/com/beat/global/swagger/annotation/DisableSwaggerSecurity.java @@ -0,0 +1,10 @@ +package com.beat.global.swagger.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DisableSwaggerSecurity {} diff --git a/src/main/java/com/beat/global/swagger/SwaggerConfig.java b/src/main/java/com/beat/global/swagger/config/SwaggerConfig.java similarity index 61% rename from src/main/java/com/beat/global/swagger/SwaggerConfig.java rename to src/main/java/com/beat/global/swagger/config/SwaggerConfig.java index d4017e5a..0a307427 100644 --- a/src/main/java/com/beat/global/swagger/SwaggerConfig.java +++ b/src/main/java/com/beat/global/swagger/config/SwaggerConfig.java @@ -1,4 +1,6 @@ -package com.beat.global.swagger; +package com.beat.global.swagger.config; + +import java.util.Collections; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -7,10 +9,13 @@ import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; +import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.beat.global.swagger.annotation.DisableSwaggerSecurity; + @Configuration public class SwaggerConfig { @@ -21,23 +26,36 @@ public class SwaggerConfig { public OpenAPI openAPI() { String jwt = "JWT"; SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); + Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() .name(jwt) .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT") ); - return new OpenAPI().addServersItem(new Server().url(serverUrl)) - .components(new Components()) + + return new OpenAPI() + .addServersItem(new Server().url(serverUrl)) + .components(components) .info(apiInfo()) - .addSecurityItem(securityRequirement) - .components(components); + .addSecurityItem(securityRequirement); + } + + @Bean + public OperationCustomizer customize() { + return (operation, handlerMethod) -> { + DisableSwaggerSecurity methodAnnotation = handlerMethod.getMethodAnnotation(DisableSwaggerSecurity.class); + if (methodAnnotation != null) { + operation.setSecurity(Collections.emptyList()); + } + return operation; + }; } private Info apiInfo() { return new Info() .title("BEAT Project API") .description("간편하게 소규모 공연을 등록하고 관리할 수 있는 티켓 예매 플랫폼") - .version("1.1.0"); + .version("1.2.0"); } -} \ No newline at end of file +}