171 lines
6.8 KiB
Java
171 lines
6.8 KiB
Java
package com.kamco.cd.kamcoback.members;
|
|
|
|
import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
|
|
import com.kamco.cd.kamcoback.auth.RefreshTokenService;
|
|
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
|
import com.kamco.cd.kamcoback.members.dto.SignInRequest;
|
|
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;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
import java.nio.file.AccessDeniedException;
|
|
import java.time.Duration;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.http.HttpHeaders;
|
|
import org.springframework.http.ResponseCookie;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.security.authentication.AuthenticationManager;
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.web.ErrorResponse;
|
|
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;
|
|
|
|
@Tag(name = "인증(Auth)", description = "로그인, 토큰 재발급, 로그아웃 API")
|
|
@RestController
|
|
@RequestMapping("/api/auth")
|
|
@RequiredArgsConstructor
|
|
public class AuthController {
|
|
|
|
private final AuthenticationManager authenticationManager;
|
|
private final JwtTokenProvider jwtTokenProvider;
|
|
private final RefreshTokenService refreshTokenService;
|
|
|
|
@Value("${token.refresh-cookie-name}")
|
|
private String refreshCookieName;
|
|
|
|
@Value("${token.refresh-cookie-secure:true}")
|
|
private boolean refreshCookieSecure;
|
|
|
|
@PostMapping("/signin")
|
|
@Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.")
|
|
@ApiResponses({
|
|
@ApiResponse(
|
|
responseCode = "200",
|
|
description = "로그인 성공",
|
|
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
|
|
@ApiResponse(
|
|
responseCode = "401",
|
|
description = "ID 또는 비밀번호 불일치",
|
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
|
})
|
|
public ApiResponseDto<TokenResponse> signin(
|
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
description = "로그인 요청 정보",
|
|
required = true)
|
|
@RequestBody
|
|
SignInRequest request,
|
|
HttpServletResponse response) {
|
|
Authentication authentication =
|
|
authenticationManager.authenticate(
|
|
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
|
|
|
|
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
|
|
|
|
String accessToken = jwtTokenProvider.createAccessToken(username);
|
|
String refreshToken = jwtTokenProvider.createRefreshToken(username);
|
|
|
|
// Redis에 RefreshToken 저장 (TTL = 7일)
|
|
refreshTokenService.save(
|
|
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
|
|
|
|
// HttpOnly + Secure 쿠키에 RefreshToken 저장
|
|
ResponseCookie cookie =
|
|
ResponseCookie.from(refreshCookieName, refreshToken)
|
|
.httpOnly(true)
|
|
.secure(refreshCookieSecure)
|
|
.path("/")
|
|
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
|
|
.sameSite("Strict")
|
|
.build();
|
|
|
|
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
|
return ApiResponseDto.createOK(new TokenResponse(accessToken));
|
|
}
|
|
|
|
@PostMapping("/refresh")
|
|
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
|
|
@ApiResponses({
|
|
@ApiResponse(
|
|
responseCode = "200",
|
|
description = "재발급 성공",
|
|
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
|
|
@ApiResponse(
|
|
responseCode = "401",
|
|
description = "만료되었거나 유효하지 않은 리프레시 토큰",
|
|
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
|
})
|
|
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) throws AccessDeniedException {
|
|
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
|
|
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
|
|
}
|
|
|
|
String username = jwtTokenProvider.getSubject(refreshToken);
|
|
|
|
// Redis에 저장된 RefreshToken과 일치하는지 확인
|
|
if (!refreshTokenService.validate(username, refreshToken)) {
|
|
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
|
|
}
|
|
|
|
// 새 토큰 발급
|
|
String newAccessToken = jwtTokenProvider.createAccessToken(username);
|
|
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
|
|
|
|
// Redis 갱신
|
|
refreshTokenService.save(
|
|
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
|
|
|
|
// 쿠키 갱신
|
|
ResponseCookie cookie =
|
|
ResponseCookie.from(refreshCookieName, newRefreshToken)
|
|
.httpOnly(true)
|
|
.secure(refreshCookieSecure)
|
|
.path("/")
|
|
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
|
|
.sameSite("Strict")
|
|
.build();
|
|
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
|
|
|
return ResponseEntity.ok(new TokenResponse(newAccessToken));
|
|
}
|
|
|
|
@PostMapping("/logout")
|
|
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
|
|
@ApiResponses({
|
|
@ApiResponse(
|
|
responseCode = "200",
|
|
description = "로그아웃 성공",
|
|
content = @Content(schema = @Schema(implementation = Void.class)))
|
|
})
|
|
public ApiResponseDto<ResponseEntity<Object>> logout(Authentication authentication, HttpServletResponse response) {
|
|
if (authentication != null) {
|
|
String username = authentication.getName();
|
|
// Redis에서 RefreshToken 삭제
|
|
refreshTokenService.delete(username);
|
|
}
|
|
|
|
// 쿠키 삭제 (Max-Age=0)
|
|
ResponseCookie cookie =
|
|
ResponseCookie.from(refreshCookieName, "")
|
|
.httpOnly(true)
|
|
.secure(refreshCookieSecure)
|
|
.path("/")
|
|
.maxAge(0)
|
|
.sameSite("Strict")
|
|
.build();
|
|
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
|
|
|
return ApiResponseDto.createOK(ResponseEntity.noContent().build());
|
|
}
|
|
|
|
public record TokenResponse(String accessToken) {
|
|
|
|
}
|
|
}
|