diff --git a/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java b/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java index fa7240a..8a52f08 100644 --- a/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java +++ b/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java @@ -3,6 +3,8 @@ import dmu.dasom.api.domain.common.exception.ErrorResponse; import dmu.dasom.api.domain.member.dto.SignupRequestDto; import dmu.dasom.api.domain.member.service.MemberService; +import dmu.dasom.api.global.auth.dto.TokenBox; +import dmu.dasom.api.global.auth.userdetails.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; @@ -11,11 +13,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") @@ -39,16 +40,32 @@ public class MemberController { ), @ExampleObject( name = "이메일 또는 비밀번호 형식 올바르지 않음", - value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }" - ) - } - ) - ) - }) + value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }")}))}) @PostMapping("/auth/signup") public ResponseEntity signUp(@Valid @RequestBody final SignupRequestDto request) { memberService.signUp(request); return ResponseEntity.ok().build(); } + @Operation(summary = "토큰 갱신") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 갱신 성공 (Header로 토큰 반환)"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "RefreshToken 만료", + value = "{ \"code\": \"C004\", \"message\": \"토큰이 만료되었습니다.\" }")}))}) + @GetMapping("/auth/rotation") + public ResponseEntity tokenRotation(@AuthenticationPrincipal final UserDetailsImpl userDetails) { + final TokenBox tokenBox = memberService.tokenRotation(userDetails); + final HttpHeaders headers = new HttpHeaders(); + headers.add("Access-Token", tokenBox.getAccessToken()); + headers.add("Refresh-Token", tokenBox.getRefreshToken()); + + return ResponseEntity.ok().headers(headers).build(); + } + } diff --git a/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java b/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java index 411d463..719816d 100644 --- a/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java +++ b/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java @@ -2,6 +2,8 @@ import dmu.dasom.api.domain.member.dto.SignupRequestDto; import dmu.dasom.api.domain.member.entity.Member; +import dmu.dasom.api.global.auth.dto.TokenBox; +import dmu.dasom.api.global.auth.userdetails.UserDetailsImpl; public interface MemberService { @@ -11,4 +13,6 @@ public interface MemberService { void signUp(final SignupRequestDto request); + TokenBox tokenRotation(final UserDetailsImpl userDetails); + } diff --git a/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java b/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java index b4e26fc..72a314f 100644 --- a/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java @@ -5,6 +5,9 @@ import dmu.dasom.api.domain.member.dto.SignupRequestDto; import dmu.dasom.api.domain.member.entity.Member; import dmu.dasom.api.domain.member.repository.MemberRepository; +import dmu.dasom.api.global.auth.dto.TokenBox; +import dmu.dasom.api.global.auth.jwt.JwtUtil; +import dmu.dasom.api.global.auth.userdetails.UserDetailsImpl; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -17,6 +20,7 @@ public class MemberServiceImpl implements MemberService { private final BCryptPasswordEncoder encoder; private final MemberRepository memberRepository; + private final JwtUtil jwtUtil; // 이메일로 사용자 조회 @Override @@ -42,4 +46,10 @@ public void signUp(final SignupRequestDto request) { memberRepository.save(request.toEntity(encoder.encode(request.getPassword()))); } + // 토큰 갱신 + @Override + public TokenBox tokenRotation(final UserDetailsImpl userDetails) { + return jwtUtil.tokenRotation(userDetails); + } + } diff --git a/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java b/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java index f18463a..1897b8a 100644 --- a/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java +++ b/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java @@ -49,7 +49,7 @@ public SecurityFilterChain filterChain(final HttpSecurity http, final Authentica .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/admin/**").hasRole(Role.ROLE_ADMIN.getName()) - .requestMatchers("/api/auth/logout").authenticated() + .requestMatchers("/api/auth/logout", "/api/auth/rotation").authenticated() .anyRequest().permitAll()) .addFilterBefore(jwtFilter, CustomAuthenticationFilter.class) .addFilterAt(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/dmu/dasom/api/global/auth/jwt/JwtUtil.java b/src/main/java/dmu/dasom/api/global/auth/jwt/JwtUtil.java index f9ab96b..ee5ec28 100644 --- a/src/main/java/dmu/dasom/api/global/auth/jwt/JwtUtil.java +++ b/src/main/java/dmu/dasom/api/global/auth/jwt/JwtUtil.java @@ -3,6 +3,7 @@ import dmu.dasom.api.domain.common.exception.CustomException; import dmu.dasom.api.domain.common.exception.ErrorCode; import dmu.dasom.api.global.auth.dto.TokenBox; +import dmu.dasom.api.global.auth.userdetails.UserDetailsImpl; import io.jsonwebtoken.*; import io.jsonwebtoken.security.SignatureException; import org.springframework.beans.factory.annotation.Value; @@ -130,4 +131,10 @@ public boolean isExpired(final String token) { } } + // Access, Refresh 토큰 갱신 + public TokenBox tokenRotation(final UserDetailsImpl userDetails) { + blacklistTokens(userDetails.getUsername()); + return generateTokenBox(userDetails.getUsername()); + } + }