로그인 기능 추가

This commit is contained in:
2025-12-04 16:51:24 +09:00
parent a7d03a0086
commit f41e82e3ca
10 changed files with 335 additions and 307 deletions

View File

@@ -26,39 +26,41 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화
// .sessionManagement(
// sm ->
// sm.sessionCreationPolicy(
// SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음 요청은 JWT 인증
// .formLogin(form -> form.disable()) // react에서 로그인 요청 관리
// .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용
// .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음
// .authenticationProvider(
// customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용
// .authorizeHttpRequests(
// auth ->
// auth.requestMatchers(
// "/api/auth/signin",
// "/api/auth/refresh",
// "/swagger-ui/**",
// "/v3/api-docs/**")
// .permitAll()
// .anyRequest()
// .authenticated())
// .addFilterBefore(
// jwtAuthenticationFilter,
// UsernamePasswordAuthenticationFilter
// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장.
/*
http.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화
.sessionManagement(
sm ->
sm.sessionCreationPolicy(
SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증
.formLogin(form -> form.disable()) // react에서 로그인 요청 관리
.httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용
.logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음
.authenticationProvider(
customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용
.authorizeHttpRequests(
auth ->
auth.requestMatchers(
"/api/auth/signin",
"/api/auth/refresh",
"/swagger-ui/**",
"/v3/api-docs/**")
.permitAll()
.anyRequest()
.authenticated())
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter
.class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장.
*/
http.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth -> auth.anyRequest().permitAll() // 🔥 인증 필요 없음
);
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth -> auth.anyRequest().permitAll() // 🔥 인증 필요 없음
);
;
return http.build();
@@ -66,10 +68,15 @@ public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
throws Exception {
return configuration.getAuthenticationManager();
}
/**
* CORS 설정
*
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성

View File

@@ -1,10 +1,12 @@
package com.kamco.cd.kamcoback.config;
// @Configuration
// @SecurityScheme(
//@Configuration
//@SecurityScheme(
// name = "BearerAuth",
// type = SecuritySchemeType.HTTP,
// scheme = "bearer",
// bearerFormat = "JWT"
// )
public class SwaggerConfig {}
//)
public class SwaggerConfig {
}

View File

@@ -35,7 +35,6 @@ public class ApiLogFunction {
// 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) {
try {
Object userId = request.getUserPrincipal();
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) {
return null;
@@ -65,22 +64,22 @@ public class ApiLogFunction {
}
public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams =
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
try {
// json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper));
@@ -92,8 +91,8 @@ public class ApiLogFunction {
// Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request");
}

View File

@@ -1,15 +1,16 @@
package com.kamco.cd.kamcoback.config.api;
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity;
import com.kamco.cd.kamcoback.postgres.repository.log.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
@@ -30,19 +31,19 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용
return returnType.getParameterType().equals(ApiResponseDto.class);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
ContentCachingRequestWrapper contentWrapper = (ContentCachingRequestWrapper) servletRequest;
@@ -52,21 +53,34 @@ public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
response.setStatusCode(apiResponse.getHttpStatus());
String ip = ApiLogFunction.getClientIp(servletRequest);
// TODO : userid 가 계정명인지, uid 인지 확인 후 로직 수정 필요함
Long userid =
Long.valueOf(Optional.ofNullable(ApiLogFunction.getUserId(servletRequest)).orElse("1"));
Long userid = null;
/**
* servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
* 이 요청이 JWT 인증을 통과한 요청인가? 그리고 Spring Security Authentication 객체가 UsernamePasswordAuthenticationToken 타입인가? 체크
*/
/**
* auth.getPrincipal() instanceof CustomUserDetails customUserDetails
* principal 안에 들어있는 객체가 내가 만든 CustomUserDetails 타입인가? 체크
*/
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
// audit 에는 long 타입 user_id가 들어가지만 토큰 sub은 uuid여서 user_id 가져오기
userid = customUserDetails.getMember().getId();
}
// TODO: menuUid 를 동적으로 가져오게끔 해야함
AuditLogEntity log =
new AuditLogEntity(
userid,
ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse),
"MU_01_01",
ip,
servletRequest.getRequestURI(),
ApiLogFunction.getRequestBody(servletRequest, contentWrapper),
apiResponse.getErrorLogUid());
new AuditLogEntity(
userid,
ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse),
"MU_01_01",
ip,
servletRequest.getRequestURI(),
ApiLogFunction.getRequestBody(servletRequest, contentWrapper),
apiResponse.getErrorLogUid());
// tb_audit_log 테이블 저장
auditLogRepository.save(log);

View File

@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -28,159 +30,160 @@ public class AdminApiController {
@Operation(summary = "회원가입", description = "회원가입")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "회원가입 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
value = {
@ApiResponse(
responseCode = "201",
description = "회원가입 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/join")
public ApiResponseDto<Long> saveMember(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원가입",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.AddReq.class)))
@RequestBody
@Valid
MembersDto.AddReq addReq) {
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원가입",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.AddReq.class)))
@RequestBody
@Valid
MembersDto.AddReq addReq) {
return ApiResponseDto.createOK(adminService.saveMember(addReq));
}
@Operation(summary = "역할 추가", description = "uuid 기준으로 역할 추가")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "역할 추가",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
value = {
@ApiResponse(
responseCode = "201",
description = "역할 추가",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/roles/add")
public ApiResponseDto<UUID> saveRoles(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "역할 추가",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.RolesDto.class)))
@RequestBody
@Valid
MembersDto.RolesDto rolesDto) {
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "역할 추가",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.RolesDto.class)))
@RequestBody
@Valid
MembersDto.RolesDto rolesDto) {
adminService.saveRoles(rolesDto);
return ApiResponseDto.createOK(rolesDto.getUuid());
}
@Operation(summary = "역할 삭제", description = "uuid 기준으로 역할 삭제")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "역할 삭제",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/roles/rm")
value = {
@ApiResponse(
responseCode = "201",
description = "역할 삭제",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/roles/rm")
public ApiResponseDto<UUID> deleteRoles(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "역할 삭제",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.RolesDto.class)))
@RequestBody
@Valid
MembersDto.RolesDto rolesDto) {
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "역할 삭제",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.RolesDto.class)))
@RequestBody
@Valid
MembersDto.RolesDto rolesDto) {
adminService.deleteRoles(rolesDto);
return ApiResponseDto.createOK(rolesDto.getUuid());
}
@Operation(summary = "상태 수정", description = "상태 수정")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "상태 수정",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/status/update")
value = {
@ApiResponse(
responseCode = "201",
description = "상태 수정",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PatchMapping("{uuid}/status")
public ApiResponseDto<UUID> updateStatus(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "상태 수정",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.StatusDto.class)))
@RequestBody
@Valid
MembersDto.StatusDto statusDto) {
adminService.updateStatus(statusDto);
return ApiResponseDto.createOK(statusDto.getUuid());
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "상태 수정",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MembersDto.StatusDto.class)))
@PathVariable UUID uuid,
@RequestBody
@Valid
MembersDto.StatusDto statusDto) {
adminService.updateStatus(uuid, statusDto);
return ApiResponseDto.createOK(uuid);
}
@Operation(summary = "회원 탈퇴", description = "회원 탈퇴")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "회원 탈퇴",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/delete-account")
public ApiResponseDto<UUID> deleteAccount(MembersDto.StatusDto statusDto) {
adminService.deleteAccount(statusDto);
return ApiResponseDto.createOK(statusDto.getUuid());
value = {
@ApiResponse(
responseCode = "201",
description = "회원 탈퇴",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/delete/{uuid}")
public ApiResponseDto<UUID> deleteAccount(@PathVariable UUID uuid) {
adminService.deleteAccount(uuid);
return ApiResponseDto.createOK(uuid);
}
@Operation(summary = "패스워드 초기화", description = "패스워드 초기화")
@Operation(summary = "비밀번호 초기화", description = "비밀번호 초기화")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "패스워드 초기화",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/{memberId}/password")
value = {
@ApiResponse(
responseCode = "201",
description = "비밀번호 초기화",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PatchMapping("/{memberId}/password")
public ApiResponseDto<Long> resetPassword(@PathVariable Long memberId) {
adminService.resetPassword(memberId);
return ApiResponseDto.createOK(memberId);

View File

@@ -41,27 +41,27 @@ public class AuthController {
private boolean refreshCookieSecure;
@PostMapping("/signin")
@Operation(summary = "로그인", description = "사번 또는 이메일과 비밀번호로 로그인하여 액세스/리프레시 토큰을 발급합니다.")
@Operation(summary = "로그인", description = "사번로 로그인하여 액세스/리프레시 토큰을 발급.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
responseCode = "200",
description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "401",
description = "ID 또는 비밀번호 불일치",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
responseCode = "401",
description = "ID 또는 비밀번호 불일치",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password()));
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
@@ -70,17 +70,17 @@ public class AuthController {
// Redis에 RefreshToken 저장 (TTL = 7일)
refreshTokenService.save(
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// HttpOnly + Secure 쿠키에 RefreshToken 저장
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true)
.secure(refreshCookieSecure) // 로컬 개발에서 http만 쓰면 false 로 바꿔야 할 수도 있음
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
@@ -91,13 +91,13 @@ public class AuthController {
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "401",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
responseCode = "401",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
@@ -117,29 +117,29 @@ public class AuthController {
// Redis 갱신
refreshTokenService.save(
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// 쿠키 갱신
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, newRefreshToken)
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
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 = "현재 사용자의 토큰을 무효화(블랙리스트 처리 또는 리프레시 토큰 삭제)합니다.")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ResponseEntity<Void> logout(Authentication authentication, HttpServletResponse response) {
if (authentication != null) {
@@ -150,13 +150,13 @@ public class AuthController {
// 쿠키 삭제 (Max-Age=0)
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, "")
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(0)
.sameSite("Strict")
.build();
ResponseCookie.from(refreshCookieName, "")
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(0)
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.noContent().build();
@@ -164,8 +164,12 @@ public class AuthController {
@Schema(description = "로그인 요청 DTO")
public record SignInRequest(
@Schema(description = "사번", example = "11111") String username,
@Schema(description = "비밀번호", example = "kamco1234!") String password) {}
@Schema(description = "사번", example = "11111") String username,
@Schema(description = "비밀번호", example = "kamco1234!") String password) {
public record TokenResponse(String accessToken) {}
}
public record TokenResponse(String accessToken) {
}
}

View File

@@ -16,7 +16,7 @@ import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -31,40 +31,40 @@ public class MembersApiController {
@Operation(summary = "회원정보 목록", description = "회원정보 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<Page<Basic>> getMemberList(
@ParameterObject MembersDto.SearchReq searchReq) {
@ParameterObject MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
}
@Operation(summary = "회원정보 수정", description = "회원정보 수정")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "회원정보 수정",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/{uuid}")
value = {
@ApiResponse(
responseCode = "201",
description = "회원정보 수정",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = UUID.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/{uuid}")
public ApiResponseDto<UUID> updateMember(
@PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) {
@PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) {
membersService.updateMember(uuid, updateReq);
return ApiResponseDto.createOK(uuid);
}

View File

@@ -28,19 +28,21 @@ public class MembersDto {
private String email;
private String status;
private String roleName;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
@JsonFormatDttm
private ZonedDateTime createdDttm;
@JsonFormatDttm
private ZonedDateTime updatedDttm;
public Basic(
Long id,
UUID uuid,
String employeeNo,
String name,
String email,
String status,
String roleName,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm) {
Long id,
UUID uuid,
String employeeNo,
String name,
String email,
String status,
String roleName,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm) {
this.id = id;
this.uuid = uuid;
this.employeeNo = employeeNo;
@@ -166,10 +168,6 @@ public class MembersDto {
@AllArgsConstructor
public static class StatusDto {
@Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5")
@NotBlank
private UUID uuid;
@Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE")
@NotBlank
private String status;

View File

@@ -3,6 +3,7 @@ package com.kamco.cd.kamcoback.members.service;
import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Value;
@@ -60,17 +61,17 @@ public class AdminService {
*
* @param statusDto
*/
public void updateStatus(MembersDto.StatusDto statusDto) {
membersCoreService.updateStatus(statusDto);
public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) {
membersCoreService.updateStatus(uuid, statusDto);
}
/**
* 회원 탈퇴
*
* @param statusDto
* @param uuid
*/
public void deleteAccount(MembersDto.StatusDto statusDto) {
membersCoreService.deleteAccount(statusDto);
public void deleteAccount(UUID uuid) {
membersCoreService.deleteAccount(uuid);
}
/**

View File

@@ -41,12 +41,12 @@ public class MembersCoreService {
public Long saveMembers(MembersDto.AddReq addReq) {
if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) {
throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo());
MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo());
}
if (membersRepository.existsByEmail(addReq.getEmail())) {
throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail());
MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail());
}
MemberEntity memberEntity = new MemberEntity();
@@ -66,7 +66,7 @@ public class MembersCoreService {
*/
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
memberEntity.setEmployeeNo(updateReq.getEmployeeNo());
@@ -93,13 +93,13 @@ public class MembersCoreService {
public void saveRoles(MembersDto.RolesDto rolesDto) {
MemberEntity memberEntity =
membersRepository
.findByUUID(rolesDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException());
membersRepository
.findByUUID(rolesDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException());
if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) {
throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다.");
MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다.");
}
MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId();
@@ -120,9 +120,9 @@ public class MembersCoreService {
*/
public void deleteRoles(MembersDto.RolesDto rolesDto) {
MemberEntity memberEntity =
membersRepository
.findByUUID(rolesDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException());
membersRepository
.findByUUID(rolesDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException());
MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId();
memberRoleEntityId.setMemberUuid(rolesDto.getUuid());
@@ -140,11 +140,11 @@ public class MembersCoreService {
*
* @param statusDto
*/
public void updateStatus(MembersDto.StatusDto statusDto) {
public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) {
MemberEntity memberEntity =
membersRepository
.findByUUID(statusDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException());
membersRepository
.findByUUID(uuid)
.orElseThrow(() -> new MemberNotFoundException());
memberEntity.setStatus(statusDto.getStatus());
memberEntity.setUpdatedDttm(ZonedDateTime.now());
@@ -154,13 +154,13 @@ public class MembersCoreService {
/**
* 회원 탈퇴
*
* @param statusDto
* @param uuid
*/
public void deleteAccount(MembersDto.StatusDto statusDto) {
public void deleteAccount(UUID uuid) {
MemberEntity memberEntity =
membersRepository
.findByUUID(statusDto.getUuid())
.orElseThrow(() -> new MemberNotFoundException());
membersRepository
.findByUUID(uuid)
.orElseThrow(() -> new MemberNotFoundException());
MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId();
memberArchivedEntityId.setUserId(memberEntity.getId());
@@ -193,10 +193,10 @@ public class MembersCoreService {
*/
public void resetPassword(Long id) {
MemberEntity memberEntity =
membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException());
membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException());
String salt =
BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
// 패스워드 암호화, 초기 패스워드 고정
String hashedPassword = BCrypt.hashpw(password, salt);