mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-12 19:49:10 +00:00
kos-mock 상품변경 실제 DB 업데이트 기능 추가
- MockDataService에 updateCustomerProduct 메서드 추가 - KosMockService에 실제 고객 데이터 업데이트 로직 추가 - 상품변경 시 고객의 current_product_code를 실제로 업데이트하도록 수정 - 트랜잭션 처리로 데이터 일관성 보장 - product-service Hibernate dialect 설정 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="BILL_INQUIRY_URL" value="http://localhost:8082" />
|
||||
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:3000" />
|
||||
<entry key="DB_HOST" value="20.249.70.6" />
|
||||
<entry key="DB_KIND" value="postgresql" />
|
||||
<entry key="DB_NAME" value="phonebill_auth" />
|
||||
@@ -11,11 +12,9 @@
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_USERNAME" value="auth_user" />
|
||||
<entry key="DDL_AUTO" value="update" />
|
||||
<entry key="JWT_SECRET" value="phonebill-jwt-secret-key-2025-dev" />
|
||||
<entry key="LOG_FILE" value="logs/user-service.log" />
|
||||
<entry key="LOG_LEVEL_APP" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_ROOT" value="INFO" />
|
||||
<entry key="PRODUCT_CHANGE_URL" value="http://localhost:8083" />
|
||||
<entry key="JWT_ACCESS_TOKEN_VALIDITY" value="18000000" />
|
||||
<entry key="JWT_REFRESH_TOKEN_VALIDITY" value="86400000" />
|
||||
<entry key="JWT_SECRET" value="nwe5Yo9qaJ6FBD/Thl2/j6/SFAfNwUorAY1ZcWO2KI7uA4bmVLOCPxE9hYuUpRCOkgV2UF2DdHXtqHi3+BU/ecbz2zpHyf/720h48UbA3XOMYOX1sdM+dQ==" />
|
||||
<entry key="REDIS_DATABASE" value="0" />
|
||||
<entry key="REDIS_HOST" value="20.249.193.103" />
|
||||
<entry key="REDIS_PASSWORD" value="Redis2025Dev!" />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.phonebill.user.config;
|
||||
|
||||
import com.phonebill.user.entity.AuthPermissionEntity;
|
||||
import com.phonebill.user.enums.PermissionCode;
|
||||
import com.phonebill.user.repository.AuthPermissionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 애플리케이션 시작 시 기본 데이터 초기화
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DataInitializer implements CommandLineRunner {
|
||||
|
||||
private final AuthPermissionRepository authPermissionRepository;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) throws Exception {
|
||||
initializePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 권한 데이터 초기화
|
||||
*/
|
||||
private void initializePermissions() {
|
||||
log.info("권한 데이터 초기화 시작");
|
||||
|
||||
for (PermissionCode permissionCode : PermissionCode.values()) {
|
||||
// 이미 존재하는 권한인지 확인
|
||||
if (!authPermissionRepository.existsByPermissionCodeAndIsActiveTrue(permissionCode.getCode())) {
|
||||
AuthPermissionEntity permission = AuthPermissionEntity.builder()
|
||||
.serviceCode("USER_SERVICE")
|
||||
.permissionCode(permissionCode.getCode())
|
||||
.permissionName(permissionCode.getCode())
|
||||
.permissionDescription(permissionCode.getDescription())
|
||||
.isActive(true)
|
||||
.build();
|
||||
|
||||
authPermissionRepository.save(permission);
|
||||
log.info("권한 생성: {}", permissionCode.getCode());
|
||||
} else {
|
||||
log.debug("권한 이미 존재: {}", permissionCode.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("권한 데이터 초기화 완료");
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,9 @@ public class JwtConfig {
|
||||
*/
|
||||
@Bean
|
||||
public JwtTokenProvider jwtTokenProvider(
|
||||
@Value("${security.jwt.secret:phonebill-jwt-secret-key-2025-dev}") String secret,
|
||||
@Value("${security.jwt.access-token-expiration:3600}") long tokenValidityInSeconds) {
|
||||
@Value("${jwt.secret}") String secret,
|
||||
@Value("${jwt.access-token-validity}") long tokenValidityInMilliseconds) {
|
||||
long tokenValidityInSeconds = tokenValidityInMilliseconds / 1000;
|
||||
return new JwtTokenProvider(secret, tokenValidityInSeconds);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.phonebill.user.config;
|
||||
import com.phonebill.common.security.JwtAuthenticationFilter;
|
||||
import com.phonebill.common.security.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -28,7 +29,9 @@ import java.util.List;
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${cors.allowed-origins")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
@@ -46,8 +49,10 @@ public class SecurityConfig {
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// Public endpoints (인증 불필요)
|
||||
.requestMatchers(
|
||||
"/auth/login",
|
||||
"/auth/refresh",
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/register",
|
||||
"/api/v1/auth/refresh",
|
||||
"/api/v1/users",
|
||||
"/actuator/health",
|
||||
"/actuator/info",
|
||||
"/actuator/prometheus",
|
||||
@@ -102,10 +107,11 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 개발환경에서는 모든 Origin 허용, 운영환경에서는 특정 도메인만 허용
|
||||
configuration.setAllowedOriginPatterns(List.of("*"));
|
||||
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
@@ -2,11 +2,14 @@ package com.phonebill.user.controller;
|
||||
|
||||
import com.phonebill.user.dto.*;
|
||||
import com.phonebill.user.service.AuthService;
|
||||
import com.phonebill.user.service.JwtService;
|
||||
import com.phonebill.user.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
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.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -19,12 +22,14 @@ import org.springframework.web.bind.annotation.*;
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Authentication", description = "인증 관련 API")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final JwtService jwtService;
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 사용자 로그인
|
||||
@@ -47,7 +52,10 @@ public class AuthController {
|
||||
@Parameter(description = "로그인 요청 정보", required = true)
|
||||
@Valid @RequestBody LoginRequest loginRequest
|
||||
) {
|
||||
log.info("로그인 요청: userId={}", loginRequest.getUserId());
|
||||
log.info("로그인 요청 받음: userId={}, password존재={}, autoLogin={}",
|
||||
loginRequest.getUserId(),
|
||||
loginRequest.getPassword() != null,
|
||||
loginRequest.getAutoLogin());
|
||||
|
||||
LoginResponse response = authService.login(loginRequest);
|
||||
|
||||
@@ -85,29 +93,43 @@ public class AuthController {
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
* @param userId 사용자 ID
|
||||
* @param refreshToken Refresh Token
|
||||
* @param request HTTP 요청 (Authorization Header에서 JWT 토큰 추출)
|
||||
* @return 로그아웃 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "사용자 로그아웃",
|
||||
description = "현재 세션을 종료하고 Refresh Token을 무효화합니다."
|
||||
description = "Authorization Header의 JWT 토큰에서 사용자 정보를 추출하여 현재 세션을 종료하고 Refresh Token을 무효화합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "로그아웃 성공"),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
|
||||
@ApiResponse(responseCode = "401", description = "유효하지 않은 토큰"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<String> logout(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@RequestParam String userId,
|
||||
@Parameter(description = "Refresh Token", required = true)
|
||||
@RequestParam String refreshToken
|
||||
) {
|
||||
public ResponseEntity<String> logout(HttpServletRequest request) {
|
||||
// Authorization Header에서 JWT 토큰 추출
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
return ResponseEntity.badRequest().body("Authorization Header가 필요합니다.");
|
||||
}
|
||||
|
||||
String accessToken = authHeader.substring(7); // "Bearer " 제거
|
||||
|
||||
// JWT 유효성 확인 (AuthService에서 블랙리스트도 확인함)
|
||||
if (!jwtService.validateToken(accessToken)) {
|
||||
return ResponseEntity.badRequest().body("유효하지 않은 토큰입니다.");
|
||||
}
|
||||
|
||||
// JWT에서 사용자 ID 추출
|
||||
String userId = jwtService.getUserIdFromToken(accessToken);
|
||||
if (userId == null) {
|
||||
return ResponseEntity.badRequest().body("유효하지 않은 토큰입니다.");
|
||||
}
|
||||
|
||||
log.info("로그아웃 요청: userId={}", userId);
|
||||
|
||||
authService.logout(userId, refreshToken);
|
||||
// 해당 사용자의 모든 활성 세션 무효화 (Access Token 기반)
|
||||
authService.logoutWithAccessToken(userId, accessToken);
|
||||
|
||||
log.info("로그아웃 성공: userId={}", userId);
|
||||
return ResponseEntity.ok("로그아웃이 완료되었습니다.");
|
||||
@@ -175,6 +197,62 @@ public class AuthController {
|
||||
return ResponseEntity.ok("비밀번호가 성공적으로 변경되었습니다. 다시 로그인해 주세요.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정 잠금 해제 (관리자용)
|
||||
* @param userId 사용자 ID
|
||||
* @return 처리 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "계정 잠금 해제",
|
||||
description = "잠겨있는 사용자 계정의 잠금을 해제합니다. (관리자 권한 필요)"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "잠금 해제 성공"),
|
||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@PostMapping("/unlock/{userId}")
|
||||
public ResponseEntity<String> unlockAccount(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId
|
||||
) {
|
||||
log.info("계정 잠금 해제 요청: userId={}", userId);
|
||||
|
||||
userService.unlockAccount(userId);
|
||||
|
||||
log.info("계정 잠금 해제 성공: userId={}", userId);
|
||||
return ResponseEntity.ok("계정 잠금이 성공적으로 해제되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 등록
|
||||
* @param request 사용자 등록 요청
|
||||
* @return 등록 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "사용자 등록",
|
||||
description = "새로운 사용자를 등록하고 지정된 권한을 부여합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "사용자 등록 성공"),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청 (입력값 검증 실패 또는 중복 데이터)"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<UserRegistrationResponse> registerUser(
|
||||
@Parameter(description = "사용자 등록 요청 정보", required = true)
|
||||
@Valid @RequestBody UserRegistrationRequest request
|
||||
) {
|
||||
log.info("사용자 등록 요청 받음: request={}", request);
|
||||
log.info("상세 정보 - userId={}, customerId={}, lineNumber={}, userName={}",
|
||||
request.getUserId(), request.getCustomerId(), request.getLineNumber(), request.getUserName());
|
||||
|
||||
UserRegistrationResponse response = userService.registerUser(request);
|
||||
|
||||
log.info("사용자 등록 API 처리 완료: userId={}", request.getUserId());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 헬스 체크
|
||||
* @return 서비스 상태
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.phonebill.user.controller;
|
||||
|
||||
import com.phonebill.user.dto.*;
|
||||
import com.phonebill.user.entity.AuthUserEntity;
|
||||
import com.phonebill.user.dto.UserInfoResponse;
|
||||
import com.phonebill.user.dto.PermissionsResponse;
|
||||
import com.phonebill.user.enums.PermissionCode;
|
||||
import com.phonebill.user.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -22,13 +23,35 @@ import java.util.List;
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequestMapping("/api/v1/users")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "User Management", description = "사용자 관리 API")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 모든 사용자 정보 조회
|
||||
* @return 사용자 정보 목록
|
||||
*/
|
||||
@Operation(
|
||||
summary = "모든 사용자 정보 조회",
|
||||
description = "등록된 모든 사용자의 정보를 조회합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "조회 성공"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@GetMapping
|
||||
public ResponseEntity<List<UserInfoResponse>> getAllUsers() {
|
||||
log.info("모든 사용자 정보 조회 요청");
|
||||
|
||||
List<UserInfoResponse> response = userService.getAllUsers();
|
||||
|
||||
log.info("모든 사용자 정보 조회 성공: 사용자 수={}", response.size());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정보 조회
|
||||
* @param userId 사용자 ID
|
||||
@@ -110,93 +133,6 @@ public class UserController {
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 권한 목록 조회
|
||||
* @param userId 사용자 ID
|
||||
* @return 권한 목록
|
||||
*/
|
||||
@Operation(
|
||||
summary = "사용자 권한 목록 조회",
|
||||
description = "사용자가 보유한 모든 권한 목록을 조회합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "조회 성공"),
|
||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@GetMapping("/{userId}/permissions")
|
||||
public ResponseEntity<PermissionsResponse> getUserPermissions(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId
|
||||
) {
|
||||
log.info("사용자 권한 목록 조회 요청: userId={}", userId);
|
||||
|
||||
PermissionsResponse response = userService.getUserPermissions(userId);
|
||||
|
||||
log.info("사용자 권한 목록 조회 성공: userId={}, 권한 수={}", userId, response.getPermissions().size());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 권한 보유 여부 확인
|
||||
* @param request 권한 확인 요청
|
||||
* @return 권한 확인 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "특정 권한 보유 여부 확인",
|
||||
description = "사용자가 특정 권한을 보유하고 있는지 확인합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "확인 완료"),
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@PostMapping("/check-permission")
|
||||
public ResponseEntity<PermissionCheckResponse> checkPermission(
|
||||
@Parameter(description = "권한 확인 요청", required = true)
|
||||
@Valid @RequestBody PermissionCheckRequest request
|
||||
) {
|
||||
log.info("권한 확인 요청: userId={}, permissionCode={}",
|
||||
request.getUserId(), request.getPermissionCode());
|
||||
|
||||
PermissionCheckResponse response = userService.checkPermission(request);
|
||||
|
||||
log.info("권한 확인 완료: userId={}, permissionCode={}, hasPermission={}",
|
||||
request.getUserId(), request.getPermissionCode(), response.getHasPermission());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스별 사용자 권한 조회
|
||||
* @param userId 사용자 ID
|
||||
* @param serviceCode 서비스 코드
|
||||
* @return 서비스별 권한 목록
|
||||
*/
|
||||
@Operation(
|
||||
summary = "서비스별 사용자 권한 조회",
|
||||
description = "특정 서비스에 대한 사용자 권한 목록을 조회합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "조회 성공"),
|
||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@GetMapping("/{userId}/permissions/{serviceCode}")
|
||||
public ResponseEntity<List<String>> getUserPermissionsByService(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId,
|
||||
@Parameter(description = "서비스 코드", required = true)
|
||||
@PathVariable String serviceCode
|
||||
) {
|
||||
log.info("서비스별 사용자 권한 조회 요청: userId={}, serviceCode={}", userId, serviceCode);
|
||||
|
||||
List<String> permissions = userService.getUserPermissionsByService(userId, serviceCode);
|
||||
|
||||
log.info("서비스별 사용자 권한 조회 성공: userId={}, serviceCode={}, 권한 수={}",
|
||||
userId, serviceCode, permissions.size());
|
||||
return ResponseEntity.ok(permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 부여
|
||||
* @param userId 사용자 ID
|
||||
@@ -206,7 +142,12 @@ public class UserController {
|
||||
*/
|
||||
@Operation(
|
||||
summary = "권한 부여",
|
||||
description = "사용자에게 특정 권한을 부여합니다."
|
||||
description = "사용자에게 특정 권한을 부여합니다.\n\n" +
|
||||
"**사용 가능한 권한 코드:**\n" +
|
||||
"- `BILL_INQUIRY`: 요금 조회 서비스 권한\n" +
|
||||
"- `PRODUCT_CHANGE`: 상품 변경 서비스 권한\n" +
|
||||
"- `ADMIN`: 관리자 권한\n" +
|
||||
"- `USER_MANAGEMENT`: 사용자 관리 권한"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "권한 부여 성공"),
|
||||
@@ -218,7 +159,11 @@ public class UserController {
|
||||
public ResponseEntity<String> grantPermission(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId,
|
||||
@Parameter(description = "권한 코드", required = true)
|
||||
@Parameter(
|
||||
description = "권한 코드",
|
||||
required = true,
|
||||
example = "BILL_INQUIRY"
|
||||
)
|
||||
@PathVariable String permissionCode,
|
||||
@Parameter(description = "권한 부여자", required = true)
|
||||
@RequestParam String grantedBy
|
||||
@@ -226,6 +171,9 @@ public class UserController {
|
||||
log.info("권한 부여 요청: userId={}, permissionCode={}, grantedBy={}",
|
||||
userId, permissionCode, grantedBy);
|
||||
|
||||
// 권한 코드 유효성 검증
|
||||
PermissionCode.fromCode(permissionCode);
|
||||
|
||||
userService.grantPermission(userId, permissionCode, grantedBy);
|
||||
|
||||
log.info("권한 부여 성공: userId={}, permissionCode={}", userId, permissionCode);
|
||||
@@ -240,7 +188,12 @@ public class UserController {
|
||||
*/
|
||||
@Operation(
|
||||
summary = "권한 철회",
|
||||
description = "사용자의 특정 권한을 철회합니다."
|
||||
description = "사용자의 특정 권한을 철회합니다.\n\n" +
|
||||
"**사용 가능한 권한 코드:**\n" +
|
||||
"- `BILL_INQUIRY`: 요금 조회 서비스 권한\n" +
|
||||
"- `PRODUCT_CHANGE`: 상품 변경 서비스 권한\n" +
|
||||
"- `ADMIN`: 관리자 권한\n" +
|
||||
"- `USER_MANAGEMENT`: 사용자 관리 권한"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "권한 철회 성공"),
|
||||
@@ -252,128 +205,26 @@ public class UserController {
|
||||
public ResponseEntity<String> revokePermission(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId,
|
||||
@Parameter(description = "권한 코드", required = true)
|
||||
@Parameter(
|
||||
description = "권한 코드",
|
||||
required = true,
|
||||
example = "BILL_INQUIRY"
|
||||
)
|
||||
@PathVariable String permissionCode
|
||||
) {
|
||||
log.info("권한 철회 요청: userId={}, permissionCode={}", userId, permissionCode);
|
||||
|
||||
// 권한 코드 유효성 검증
|
||||
PermissionCode.fromCode(permissionCode);
|
||||
|
||||
userService.revokePermission(userId, permissionCode);
|
||||
|
||||
log.info("권한 철회 성공: userId={}, permissionCode={}", userId, permissionCode);
|
||||
return ResponseEntity.ok("권한이 성공적으로 철회되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정 상태 조회
|
||||
* @param userId 사용자 ID
|
||||
* @return 계정 상태
|
||||
*/
|
||||
@Operation(
|
||||
summary = "계정 상태 조회",
|
||||
description = "사용자 계정의 현재 상태를 조회합니다."
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "조회 성공"),
|
||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@GetMapping("/{userId}/status")
|
||||
public ResponseEntity<AuthUserEntity.AccountStatus> getAccountStatus(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId
|
||||
) {
|
||||
log.info("계정 상태 조회 요청: userId={}", userId);
|
||||
|
||||
AuthUserEntity.AccountStatus status = userService.getAccountStatus(userId);
|
||||
|
||||
log.info("계정 상태 조회 성공: userId={}, status={}", userId, status);
|
||||
return ResponseEntity.ok(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정 잠금 해제 (관리자용)
|
||||
* @param userId 사용자 ID
|
||||
* @return 처리 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "계정 잠금 해제",
|
||||
description = "잠겨있는 사용자 계정의 잠금을 해제합니다. (관리자 권한 필요)"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "잠금 해제 성공"),
|
||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
|
||||
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
|
||||
})
|
||||
@PostMapping("/{userId}/unlock")
|
||||
public ResponseEntity<String> unlockAccount(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId
|
||||
) {
|
||||
log.info("계정 잠금 해제 요청: userId={}", userId);
|
||||
|
||||
userService.unlockAccount(userId);
|
||||
|
||||
log.info("계정 잠금 해제 성공: userId={}", userId);
|
||||
return ResponseEntity.ok("계정 잠금이 성공적으로 해제되었습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 ID 존재 여부 확인
|
||||
* @param userId 사용자 ID
|
||||
* @return 존재 여부
|
||||
*/
|
||||
@Operation(
|
||||
summary = "사용자 ID 존재 여부 확인",
|
||||
description = "해당 사용자 ID가 시스템에 존재하는지 확인합니다."
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "확인 완료")
|
||||
@GetMapping("/{userId}/exists")
|
||||
public ResponseEntity<Boolean> existsUserId(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@PathVariable String userId
|
||||
) {
|
||||
log.info("사용자 ID 존재 여부 확인 요청: userId={}", userId);
|
||||
|
||||
boolean exists = userService.existsUserId(userId);
|
||||
|
||||
log.info("사용자 ID 존재 여부 확인 완료: userId={}, exists={}", userId, exists);
|
||||
return ResponseEntity.ok(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객 ID 존재 여부 확인
|
||||
* @param customerId 고객 ID
|
||||
* @return 존재 여부
|
||||
*/
|
||||
@Operation(
|
||||
summary = "고객 ID 존재 여부 확인",
|
||||
description = "해당 고객 ID가 시스템에 존재하는지 확인합니다."
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "확인 완료")
|
||||
@GetMapping("/customer/{customerId}/exists")
|
||||
public ResponseEntity<Boolean> existsCustomerId(
|
||||
@Parameter(description = "고객 ID", required = true)
|
||||
@PathVariable String customerId
|
||||
) {
|
||||
log.info("고객 ID 존재 여부 확인 요청: customerId={}", customerId);
|
||||
|
||||
boolean exists = userService.existsCustomerId(customerId);
|
||||
|
||||
log.info("고객 ID 존재 여부 확인 완료: customerId={}, exists={}", customerId, exists);
|
||||
return ResponseEntity.ok(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 헬스 체크
|
||||
* @return 서비스 상태
|
||||
*/
|
||||
@Operation(
|
||||
summary = "사용자 서비스 헬스 체크",
|
||||
description = "사용자 서비스의 상태를 확인합니다."
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "서비스 정상")
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<String> healthCheck() {
|
||||
return ResponseEntity.ok("User Service is running");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
@@ -7,21 +8,25 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 로그인 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginRequest {
|
||||
|
||||
@JsonProperty("userId")
|
||||
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||
@Size(min = 3, max = 20, message = "사용자 ID는 3-20자 사이여야 합니다")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "사용자 ID는 영문, 숫자, '_', '-'만 사용 가능합니다")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("password")
|
||||
@NotBlank(message = "비밀번호는 필수입니다")
|
||||
@Size(min = 8, max = 50, message = "비밀번호는 8-50자 사이여야 합니다")
|
||||
private String password;
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 로그아웃 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LogoutRequest {
|
||||
|
||||
@JsonProperty("userId")
|
||||
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("refreshToken")
|
||||
@NotBlank(message = "리프레시 토큰은 필수입니다")
|
||||
private String refreshToken;
|
||||
|
||||
// 보안을 위해 toString에서 토큰 일부만 표시
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LogoutRequest{" +
|
||||
"userId='" + userId + '\'' +
|
||||
", refreshToken='" + (refreshToken != null ? refreshToken.substring(0, Math.min(refreshToken.length(), 10)) + "..." : "null") +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 권한 확인 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PermissionCheckRequest {
|
||||
|
||||
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||
private String userId;
|
||||
|
||||
@NotBlank(message = "권한 코드는 필수입니다")
|
||||
private String permissionCode;
|
||||
|
||||
@NotBlank(message = "서비스 타입은 필수입니다")
|
||||
@Pattern(regexp = "^(BILL_INQUIRY|PRODUCT_CHANGE)$",
|
||||
message = "서비스 타입은 BILL_INQUIRY 또는 PRODUCT_CHANGE만 허용됩니다")
|
||||
private String serviceType;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 권한 확인 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PermissionCheckResponse {
|
||||
|
||||
private String userId;
|
||||
private String permissionCode;
|
||||
private String serviceType;
|
||||
private Boolean hasPermission;
|
||||
private String message;
|
||||
private PermissionDetails permissionDetails;
|
||||
|
||||
/**
|
||||
* 권한 상세 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PermissionDetails {
|
||||
private String permission;
|
||||
private String description;
|
||||
private Boolean granted;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 토큰 갱신 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RefreshTokenRequest {
|
||||
|
||||
@JsonProperty("refreshToken")
|
||||
@NotBlank(message = "리프레시 토큰은 필수입니다")
|
||||
private String refreshToken;
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ public class UserInfoResponse {
|
||||
private String customerId;
|
||||
private String lineNumber;
|
||||
private String userName;
|
||||
private String phoneNumber;
|
||||
private String email;
|
||||
private String status;
|
||||
private String accountStatus;
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 사용자 등록 요청 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "사용자 등록 요청")
|
||||
public class UserRegistrationRequest {
|
||||
|
||||
@JsonProperty("userId")
|
||||
@Schema(description = "사용자 ID", example = "mvno001")
|
||||
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||
@Size(min = 3, max = 20, message = "사용자 ID는 3자 이상 20자 이하여야 합니다")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "사용자 ID는 영문, 숫자, _, - 만 사용 가능합니다")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("customerId")
|
||||
@Schema(description = "고객 ID", example = "CU202401001")
|
||||
@NotBlank(message = "고객 ID는 필수입니다")
|
||||
@Size(max = 20, message = "고객 ID는 20자 이하여야 합니다")
|
||||
private String customerId;
|
||||
|
||||
@JsonProperty("lineNumber")
|
||||
@Schema(description = "회선번호", example = "010-1234-5678")
|
||||
@NotBlank(message = "회선번호는 필수입니다")
|
||||
@Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "회선번호는 010-XXXX-XXXX 형식이어야 합니다")
|
||||
private String lineNumber;
|
||||
|
||||
@JsonProperty("userName")
|
||||
@Schema(description = "사용자 이름", example = "홍길동")
|
||||
@NotBlank(message = "사용자 이름은 필수입니다")
|
||||
@Size(max = 50, message = "사용자 이름은 50자 이하여야 합니다")
|
||||
private String userName;
|
||||
|
||||
@JsonProperty("password")
|
||||
@Schema(description = "비밀번호", example = "securePassword123!")
|
||||
@NotBlank(message = "비밀번호는 필수입니다")
|
||||
@Size(min = 8, max = 50, message = "비밀번호는 8자 이상 50자 이하여야 합니다")
|
||||
@Pattern(
|
||||
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
|
||||
message = "비밀번호는 대문자, 소문자, 숫자, 특수문자를 각각 최소 1개씩 포함해야 합니다"
|
||||
)
|
||||
private String password;
|
||||
|
||||
@JsonProperty("permissions")
|
||||
@Schema(description = "권한 목록", example = "[\"BILL_INQUIRY\", \"PRODUCT_CHANGE\"]")
|
||||
@NotEmpty(message = "권한은 최소 1개 이상 필요합니다")
|
||||
private List<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.phonebill.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 사용자 등록 응답 DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "사용자 등록 응답")
|
||||
public class UserRegistrationResponse {
|
||||
|
||||
@Schema(description = "응답 성공 여부", example = "true")
|
||||
private boolean success;
|
||||
|
||||
@Schema(description = "응답 메시지", example = "사용자가 성공적으로 등록되었습니다")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "등록된 사용자 정보")
|
||||
private UserData data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "등록된 사용자 데이터")
|
||||
public static class UserData {
|
||||
|
||||
@Schema(description = "사용자 ID", example = "mvno001")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "고객 ID", example = "CU202401001")
|
||||
private String customerId;
|
||||
|
||||
@Schema(description = "회선번호", example = "010-1234-5678")
|
||||
private String lineNumber;
|
||||
|
||||
@Schema(description = "사용자 이름", example = "홍길동")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "계정 상태", example = "ACTIVE")
|
||||
private String accountStatus;
|
||||
|
||||
@Schema(description = "등록 시간", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "부여된 권한 목록", example = "[\"BILL_INQUIRY\", \"PRODUCT_CHANGE\"]")
|
||||
private List<String> permissions;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ public class AuthUserEntity extends BaseTimeEntity {
|
||||
@Column(name = "line_number", length = 20)
|
||||
private String lineNumber;
|
||||
|
||||
@Column(name = "user_name", length = 100)
|
||||
private String userName;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "account_status", length = 20)
|
||||
@Builder.Default
|
||||
|
||||
@@ -19,7 +19,8 @@ public class AuthUserSessionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "session_id", length = 100)
|
||||
private String sessionId;
|
||||
@Builder.Default
|
||||
private String sessionId = java.util.UUID.randomUUID().toString();
|
||||
|
||||
@Column(name = "user_id", nullable = false, length = 50)
|
||||
private String userId;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.phonebill.user.enums;
|
||||
|
||||
/**
|
||||
* 권한 코드 열거형
|
||||
* 시스템에서 사용 가능한 모든 권한 코드를 정의
|
||||
*/
|
||||
public enum PermissionCode {
|
||||
|
||||
/**
|
||||
* 요금 조회 서비스 권한
|
||||
*/
|
||||
BILL_INQUIRY("요금 조회 서비스 권한"),
|
||||
|
||||
/**
|
||||
* 상품 변경 서비스 권한
|
||||
*/
|
||||
PRODUCT_CHANGE("상품 변경 서비스 권한"),
|
||||
|
||||
/**
|
||||
* 관리자 권한
|
||||
*/
|
||||
ADMIN("관리자 권한"),
|
||||
|
||||
/**
|
||||
* 사용자 관리 권한
|
||||
*/
|
||||
USER_MANAGEMENT("사용자 관리 권한");
|
||||
|
||||
private final String description;
|
||||
|
||||
PermissionCode(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 코드 문자열로부터 enum 객체를 찾는다
|
||||
* @param code 권한 코드 문자열
|
||||
* @return PermissionCode enum 객체
|
||||
* @throws IllegalArgumentException 유효하지 않은 권한 코드인 경우
|
||||
*/
|
||||
public static PermissionCode fromCode(String code) {
|
||||
try {
|
||||
return PermissionCode.valueOf(code.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("유효하지 않은 권한 코드입니다: " + code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 권한 코드 문자열을 배열로 반환
|
||||
* @return 권한 코드 문자열 배열
|
||||
*/
|
||||
public static String[] getAllCodes() {
|
||||
PermissionCode[] values = PermissionCode.values();
|
||||
String[] codes = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
codes[i] = values[i].name();
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -157,6 +157,6 @@ public class UserServiceExceptionHandler {
|
||||
*/
|
||||
private String getRequestPath() {
|
||||
// 실제 구현에서는 HttpServletRequest를 주입받아 사용
|
||||
return "/api/auth";
|
||||
return "/auth";
|
||||
}
|
||||
}
|
||||
+5
@@ -111,6 +111,11 @@ public interface AuthUserSessionRepository extends JpaRepository<AuthUserSession
|
||||
*/
|
||||
Optional<AuthUserSessionEntity> findByUserIdAndRefreshTokenAndIsActiveTrue(String userId, String refreshToken);
|
||||
|
||||
/**
|
||||
* 사용자 ID와 세션 토큰으로 활성 세션 조회
|
||||
*/
|
||||
Optional<AuthUserSessionEntity> findByUserIdAndSessionTokenAndIsActiveTrue(String userId, String sessionToken);
|
||||
|
||||
/**
|
||||
* 사용자의 모든 세션 비활성화
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,7 @@ public class AuthService {
|
||||
private final JwtService jwtService;
|
||||
private final JwtConfig jwtConfig;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final TokenBlacklistService tokenBlacklistService;
|
||||
|
||||
private static final int MAX_LOGIN_ATTEMPTS = 5;
|
||||
private static final long LOCKOUT_DURATION = 30 * 60 * 1000L; // 30분
|
||||
@@ -65,7 +67,7 @@ public class AuthService {
|
||||
String refreshToken = jwtService.generateRefreshToken(user);
|
||||
|
||||
// 세션 저장
|
||||
saveUserSession(user, refreshToken);
|
||||
saveUserSession(user, accessToken, refreshToken);
|
||||
|
||||
log.info("사용자 로그인 성공: userId={}", user.getUserId());
|
||||
|
||||
@@ -89,7 +91,12 @@ public class AuthService {
|
||||
public RefreshTokenResponse refreshToken(RefreshTokenRequest request) {
|
||||
String refreshToken = request.getRefreshToken();
|
||||
|
||||
// Refresh Token 유효성 검증
|
||||
// 1. 블랙리스트 확인
|
||||
if (tokenBlacklistService.isBlacklisted(refreshToken)) {
|
||||
throw InvalidTokenException.invalid();
|
||||
}
|
||||
|
||||
// 2. JWT 토큰 유효성 검증
|
||||
if (!jwtService.validateToken(refreshToken) || !jwtService.isRefreshToken(refreshToken)) {
|
||||
throw InvalidTokenException.invalid();
|
||||
}
|
||||
@@ -119,7 +126,7 @@ public class AuthService {
|
||||
|
||||
// 기존 세션 비활성화 및 새 세션 생성
|
||||
session.deactivate();
|
||||
saveUserSession(user, newRefreshToken);
|
||||
saveUserSession(user, newAccessToken, newRefreshToken);
|
||||
|
||||
log.info("토큰 갱신 성공: userId={}", userId);
|
||||
|
||||
@@ -132,7 +139,7 @@ public class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
* 로그아웃 (Refresh Token 기반)
|
||||
* @param userId 사용자 ID
|
||||
* @param refreshToken Refresh Token
|
||||
*/
|
||||
@@ -148,12 +155,65 @@ public class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 검증
|
||||
* 로그아웃 (Access Token 기반)
|
||||
* @param userId 사용자 ID
|
||||
* @param accessToken Access Token
|
||||
*/
|
||||
@Transactional
|
||||
public void logoutWithAccessToken(String userId, String accessToken) {
|
||||
// 1. Access Token을 블랙리스트에 추가 (즉시 무효화)
|
||||
tokenBlacklistService.addToBlacklist(accessToken, "LOGOUT");
|
||||
|
||||
// 2. Access Token과 일치하는 활성 세션 찾아서 비활성화
|
||||
Optional<AuthUserSessionEntity> sessionOpt = authUserSessionRepository
|
||||
.findByUserIdAndSessionTokenAndIsActiveTrue(userId, accessToken);
|
||||
|
||||
if (sessionOpt.isPresent()) {
|
||||
AuthUserSessionEntity session = sessionOpt.get();
|
||||
session.deactivate();
|
||||
|
||||
// 3. 해당 세션의 Refresh Token도 블랙리스트에 추가
|
||||
if (session.getRefreshToken() != null) {
|
||||
tokenBlacklistService.addToBlacklist(session.getRefreshToken(), "LOGOUT");
|
||||
}
|
||||
|
||||
log.info("Access Token 기반 로그아웃 완료: userId={}", userId);
|
||||
} else {
|
||||
// 세션이 없는 경우, 해당 사용자의 모든 활성 세션 무효화
|
||||
List<AuthUserSessionEntity> activeSessions = authUserSessionRepository
|
||||
.findByUserId(userId)
|
||||
.stream()
|
||||
.filter(AuthUserSessionEntity::isActive)
|
||||
.toList();
|
||||
|
||||
// 모든 활성 세션의 토큰들을 블랙리스트에 추가
|
||||
for (AuthUserSessionEntity session : activeSessions) {
|
||||
if (session.getSessionToken() != null) {
|
||||
tokenBlacklistService.addToBlacklist(session.getSessionToken(), "LOGOUT_ALL");
|
||||
}
|
||||
if (session.getRefreshToken() != null) {
|
||||
tokenBlacklistService.addToBlacklist(session.getRefreshToken(), "LOGOUT_ALL");
|
||||
}
|
||||
}
|
||||
|
||||
authUserSessionRepository.deactivateAllUserSessions(userId);
|
||||
log.info("모든 세션 무효화 로그아웃 완료: userId={}, 무효화된 세션 수={}", userId, activeSessions.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 검증 (블랙리스트 포함)
|
||||
* @param token 검증할 토큰
|
||||
* @return 토큰 검증 결과
|
||||
*/
|
||||
public TokenVerifyResponse verifyToken(String token) {
|
||||
try {
|
||||
// 1. 블랙리스트 확인
|
||||
if (tokenBlacklistService.isBlacklisted(token)) {
|
||||
return TokenVerifyResponse.invalid();
|
||||
}
|
||||
|
||||
// 2. JWT 유효성 확인
|
||||
if (!jwtService.validateToken(token)) {
|
||||
return TokenVerifyResponse.invalid();
|
||||
}
|
||||
@@ -225,11 +285,13 @@ public class AuthService {
|
||||
/**
|
||||
* 사용자 세션 저장
|
||||
* @param user 사용자 정보
|
||||
* @param sessionToken Session Token (Access Token)
|
||||
* @param refreshToken Refresh Token
|
||||
*/
|
||||
private void saveUserSession(AuthUserEntity user, String refreshToken) {
|
||||
private void saveUserSession(AuthUserEntity user, String sessionToken, String refreshToken) {
|
||||
AuthUserSessionEntity session = AuthUserSessionEntity.builder()
|
||||
.userId(user.getUserId())
|
||||
.sessionToken(sessionToken)
|
||||
.refreshToken(refreshToken)
|
||||
.expiresAt(jwtService.getExpirationDateFromToken(refreshToken))
|
||||
.isActive(true)
|
||||
|
||||
@@ -123,7 +123,7 @@ public class JwtService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 유효성 검증
|
||||
* 토큰 유효성 검증 (JWT 자체만 검증, 블랙리스트는 AuthService에서 확인)
|
||||
* @param token JWT 토큰
|
||||
* @return 유효성 여부
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.phonebill.user.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JWT 토큰 블랙리스트 서비스
|
||||
* Redis를 사용해서 무효화된 토큰을 관리
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TokenBlacklistService {
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
private final JwtService jwtService;
|
||||
|
||||
private static final String BLACKLIST_PREFIX = "blacklist:";
|
||||
|
||||
/**
|
||||
* 토큰을 블랙리스트에 추가
|
||||
* @param token JWT 토큰
|
||||
* @param reason 무효화 사유
|
||||
*/
|
||||
public void addToBlacklist(String token, String reason) {
|
||||
try {
|
||||
// JWT에서 만료시간 추출
|
||||
LocalDateTime expiresAt = jwtService.getExpirationDateFromToken(token);
|
||||
if (expiresAt == null) {
|
||||
log.warn("토큰에서 만료시간을 추출할 수 없음: {}", token.substring(0, Math.min(token.length(), 20)));
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재시간부터 토큰 만료시간까지의 TTL 계산
|
||||
long ttlSeconds = expiresAt.atZone(ZoneId.systemDefault()).toEpochSecond() -
|
||||
LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond();
|
||||
|
||||
// TTL이 양수인 경우만 블랙리스트에 추가 (이미 만료된 토큰은 추가하지 않음)
|
||||
if (ttlSeconds > 0) {
|
||||
String key = BLACKLIST_PREFIX + token;
|
||||
redisTemplate.opsForValue().set(key, reason, ttlSeconds, TimeUnit.SECONDS);
|
||||
log.info("토큰이 블랙리스트에 추가됨: reason={}, ttl={}초", reason, ttlSeconds);
|
||||
} else {
|
||||
log.info("이미 만료된 토큰이므로 블랙리스트에 추가하지 않음");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("블랙리스트 추가 실패: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰이 블랙리스트에 있는지 확인
|
||||
* @param token JWT 토큰
|
||||
* @return 블랙리스트 여부
|
||||
*/
|
||||
public boolean isBlacklisted(String token) {
|
||||
try {
|
||||
String key = BLACKLIST_PREFIX + token;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
} catch (Exception e) {
|
||||
log.error("블랙리스트 확인 실패: {}", e.getMessage(), e);
|
||||
// Redis 오류 시 안전하게 false 반환 (서비스 중단 방지)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 모든 토큰을 블랙리스트에 추가
|
||||
* @param userId 사용자 ID
|
||||
* @param reason 무효화 사유
|
||||
*/
|
||||
public void addUserTokensToBlacklist(String userId, String reason) {
|
||||
try {
|
||||
// 패턴으로 해당 사용자의 모든 토큰을 찾을 수는 없으므로,
|
||||
// 실제로는 세션 테이블에서 활성 토큰을 조회해서 블랙리스트에 추가해야 함
|
||||
log.info("사용자 {} 의 모든 토큰 무효화: {}", userId, reason);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("사용자 토큰 블랙리스트 추가 실패: userId={}, error={}", userId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 블랙리스트에서 토큰 제거 (관리용)
|
||||
* @param token JWT 토큰
|
||||
*/
|
||||
public void removeFromBlacklist(String token) {
|
||||
try {
|
||||
String key = BLACKLIST_PREFIX + token;
|
||||
redisTemplate.delete(key);
|
||||
log.info("토큰이 블랙리스트에서 제거됨");
|
||||
} catch (Exception e) {
|
||||
log.error("블랙리스트 제거 실패: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 블랙리스트 통계 조회
|
||||
* @return 블랙리스트 토큰 수
|
||||
*/
|
||||
public long getBlacklistCount() {
|
||||
try {
|
||||
return redisTemplate.keys(BLACKLIST_PREFIX + "*").size();
|
||||
} catch (Exception e) {
|
||||
log.error("블랙리스트 통계 조회 실패: {}", e.getMessage(), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,25 @@
|
||||
package com.phonebill.user.service;
|
||||
|
||||
import com.phonebill.user.dto.*;
|
||||
import com.phonebill.user.dto.UserInfoResponse;
|
||||
import com.phonebill.user.dto.UserRegistrationRequest;
|
||||
import com.phonebill.user.dto.UserRegistrationResponse;
|
||||
import com.phonebill.user.entity.AuthUserEntity;
|
||||
import com.phonebill.user.entity.AuthPermissionEntity;
|
||||
import com.phonebill.user.entity.AuthUserPermissionEntity;
|
||||
import com.phonebill.user.exception.UserNotFoundException;
|
||||
import com.phonebill.user.enums.PermissionCode;
|
||||
import com.phonebill.user.repository.AuthUserRepository;
|
||||
import com.phonebill.user.repository.AuthPermissionRepository;
|
||||
import com.phonebill.user.repository.AuthUserPermissionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -30,6 +36,33 @@ public class UserService {
|
||||
private final AuthUserRepository authUserRepository;
|
||||
private final AuthPermissionRepository authPermissionRepository;
|
||||
private final AuthUserPermissionRepository authUserPermissionRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* 모든 사용자 정보 조회
|
||||
* @return 사용자 정보 목록
|
||||
*/
|
||||
public List<UserInfoResponse> getAllUsers() {
|
||||
List<AuthUserEntity> users = authUserRepository.findAll();
|
||||
|
||||
return users.stream()
|
||||
.map(user -> {
|
||||
// 사용자 권한 목록 조회
|
||||
List<String> permissions = authUserPermissionRepository.findPermissionCodesByUserId(user.getUserId());
|
||||
|
||||
return UserInfoResponse.builder()
|
||||
.userId(user.getUserId())
|
||||
.customerId(user.getCustomerId())
|
||||
.lineNumber(user.getLineNumber())
|
||||
.userName(user.getUserName())
|
||||
.accountStatus(user.getAccountStatus().name())
|
||||
.lastLoginAt(user.getLastLoginAt())
|
||||
.lastPasswordChangedAt(user.getLastPasswordChangedAt())
|
||||
.permissions(permissions)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정보 조회
|
||||
@@ -40,13 +73,18 @@ public class UserService {
|
||||
AuthUserEntity user = authUserRepository.findById(userId)
|
||||
.orElseThrow(() -> UserNotFoundException.byUserId(userId));
|
||||
|
||||
// 사용자 권한 목록 조회
|
||||
List<String> permissions = authUserPermissionRepository.findPermissionCodesByUserId(userId);
|
||||
|
||||
return UserInfoResponse.builder()
|
||||
.userId(user.getUserId())
|
||||
.customerId(user.getCustomerId())
|
||||
.lineNumber(user.getLineNumber())
|
||||
.userName(user.getUserName())
|
||||
.accountStatus(user.getAccountStatus().name())
|
||||
.lastLoginAt(user.getLastLoginAt())
|
||||
.lastPasswordChangedAt(user.getLastPasswordChangedAt())
|
||||
.permissions(permissions)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -59,13 +97,18 @@ public class UserService {
|
||||
AuthUserEntity user = authUserRepository.findByCustomerId(customerId)
|
||||
.orElseThrow(() -> UserNotFoundException.byCustomerId(customerId));
|
||||
|
||||
// 사용자 권한 목록 조회
|
||||
List<String> permissions = authUserPermissionRepository.findPermissionCodesByUserId(user.getUserId());
|
||||
|
||||
return UserInfoResponse.builder()
|
||||
.userId(user.getUserId())
|
||||
.customerId(user.getCustomerId())
|
||||
.lineNumber(user.getLineNumber())
|
||||
.userName(user.getUserName())
|
||||
.accountStatus(user.getAccountStatus().name())
|
||||
.lastLoginAt(user.getLastLoginAt())
|
||||
.lastPasswordChangedAt(user.getLastPasswordChangedAt())
|
||||
.permissions(permissions)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -78,116 +121,21 @@ public class UserService {
|
||||
AuthUserEntity user = authUserRepository.findByLineNumber(lineNumber)
|
||||
.orElseThrow(() -> UserNotFoundException.byLineNumber(lineNumber));
|
||||
|
||||
// 사용자 권한 목록 조회
|
||||
List<String> permissions = authUserPermissionRepository.findPermissionCodesByUserId(user.getUserId());
|
||||
|
||||
return UserInfoResponse.builder()
|
||||
.userId(user.getUserId())
|
||||
.customerId(user.getCustomerId())
|
||||
.lineNumber(user.getLineNumber())
|
||||
.userName(user.getUserName())
|
||||
.accountStatus(user.getAccountStatus().name())
|
||||
.lastLoginAt(user.getLastLoginAt())
|
||||
.lastPasswordChangedAt(user.getLastPasswordChangedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 권한 목록 조회
|
||||
* @param userId 사용자 ID
|
||||
* @return 권한 목록
|
||||
*/
|
||||
public PermissionsResponse getUserPermissions(String userId) {
|
||||
// 사용자 존재 확인
|
||||
if (!authUserRepository.existsByUserId(userId)) {
|
||||
throw UserNotFoundException.byUserId(userId);
|
||||
}
|
||||
|
||||
// 사용자가 보유한 권한 코드 목록 조회
|
||||
List<String> permissionCodes = authUserPermissionRepository.findPermissionCodesByUserId(userId);
|
||||
|
||||
// 권한 코드를 Permission 객체로 변환
|
||||
List<PermissionsResponse.Permission> permissions = permissionCodes.stream()
|
||||
.map(code -> PermissionsResponse.Permission.builder()
|
||||
.permission(code)
|
||||
.description(getPermissionDescription(code))
|
||||
.granted(true)
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PermissionsResponse.builder()
|
||||
.userId(userId)
|
||||
.permissions(permissions)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 권한 보유 여부 확인
|
||||
* @param request 권한 확인 요청
|
||||
* @return 권한 확인 결과
|
||||
*/
|
||||
public PermissionCheckResponse checkPermission(PermissionCheckRequest request) {
|
||||
String userId = request.getUserId();
|
||||
String permissionCode = request.getPermissionCode();
|
||||
|
||||
// 사용자 존재 확인
|
||||
if (!authUserRepository.existsByUserId(userId)) {
|
||||
return PermissionCheckResponse.builder()
|
||||
.userId(userId)
|
||||
.permissionCode(permissionCode)
|
||||
.hasPermission(false)
|
||||
.message("사용자를 찾을 수 없습니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 권한 존재 확인
|
||||
Optional<AuthPermissionEntity> permissionOpt =
|
||||
authPermissionRepository.findByPermissionCodeAndIsActiveTrue(permissionCode);
|
||||
|
||||
if (permissionOpt.isEmpty()) {
|
||||
return PermissionCheckResponse.builder()
|
||||
.userId(userId)
|
||||
.permissionCode(permissionCode)
|
||||
.hasPermission(false)
|
||||
.message("존재하지 않는 권한입니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
AuthPermissionEntity permission = permissionOpt.get();
|
||||
boolean hasPermission = authUserPermissionRepository.hasPermission(userId, permission.getPermissionId());
|
||||
|
||||
return PermissionCheckResponse.builder()
|
||||
.userId(userId)
|
||||
.permissionCode(permissionCode)
|
||||
.hasPermission(hasPermission)
|
||||
.message(hasPermission ? "권한이 있습니다." : "권한이 없습니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스별 사용자 권한 조회
|
||||
* @param userId 사용자 ID
|
||||
* @param serviceCode 서비스 코드
|
||||
* @return 서비스별 권한 목록
|
||||
*/
|
||||
public List<String> getUserPermissionsByService(String userId, String serviceCode) {
|
||||
// 사용자 존재 확인
|
||||
if (!authUserRepository.existsByUserId(userId)) {
|
||||
throw UserNotFoundException.byUserId(userId);
|
||||
}
|
||||
|
||||
// 서비스별 사용자 권한 조회
|
||||
List<AuthUserPermissionEntity> userPermissions =
|
||||
authUserPermissionRepository.findUserPermissionsByService(userId, serviceCode);
|
||||
|
||||
// 권한 코드 목록으로 변환
|
||||
return userPermissions.stream()
|
||||
.map(up -> {
|
||||
// 권한 정보 조회
|
||||
Optional<AuthPermissionEntity> permissionOpt =
|
||||
authPermissionRepository.findById(up.getPermissionId());
|
||||
return permissionOpt.map(AuthPermissionEntity::getPermissionCode).orElse(null);
|
||||
})
|
||||
.filter(permissionCode -> permissionCode != null)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 부여
|
||||
* @param userId 사용자 ID
|
||||
@@ -252,35 +200,8 @@ public class UserService {
|
||||
log.info("권한 철회 완료: userId={}, permissionCode={}", userId, permissionCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 ID 존재 여부 확인
|
||||
* @param userId 사용자 ID
|
||||
* @return 존재 여부
|
||||
*/
|
||||
public boolean existsUserId(String userId) {
|
||||
return authUserRepository.existsByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객 ID 존재 여부 확인
|
||||
* @param customerId 고객 ID
|
||||
* @return 존재 여부
|
||||
*/
|
||||
public boolean existsCustomerId(String customerId) {
|
||||
return authUserRepository.existsByCustomerId(customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정 상태 확인
|
||||
* @param userId 사용자 ID
|
||||
* @return 계정 상태 정보
|
||||
*/
|
||||
public AuthUserEntity.AccountStatus getAccountStatus(String userId) {
|
||||
AuthUserEntity user = authUserRepository.findById(userId)
|
||||
.orElseThrow(() -> UserNotFoundException.byUserId(userId));
|
||||
|
||||
return user.getAccountStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정 활성 상태 확인
|
||||
@@ -309,6 +230,128 @@ public class UserService {
|
||||
log.info("계정 잠금 해제: userId={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 등록
|
||||
* @param request 사용자 등록 요청
|
||||
* @return 등록된 사용자 정보
|
||||
*/
|
||||
@Transactional
|
||||
public UserRegistrationResponse registerUser(UserRegistrationRequest request) {
|
||||
log.info("사용자 등록 요청: userId={}, customerId={}", request.getUserId(), request.getCustomerId());
|
||||
|
||||
// 중복 검사
|
||||
validateUserUniqueness(request);
|
||||
|
||||
// 권한 코드 유효성 검증
|
||||
validatePermissionCodes(request.getPermissions());
|
||||
|
||||
// 사용자 엔티티 생성
|
||||
AuthUserEntity user = createUserEntity(request);
|
||||
|
||||
// 사용자 저장
|
||||
AuthUserEntity savedUser = authUserRepository.save(user);
|
||||
|
||||
// 권한 부여
|
||||
grantUserPermissions(savedUser.getUserId(), request.getPermissions());
|
||||
|
||||
// 응답 생성
|
||||
UserRegistrationResponse response = buildRegistrationResponse(savedUser, request.getPermissions(), request.getUserName());
|
||||
|
||||
log.info("사용자 등록 완료: userId={}", savedUser.getUserId());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 유니크 필드 중복 검사
|
||||
*/
|
||||
private void validateUserUniqueness(UserRegistrationRequest request) {
|
||||
// 사용자 ID 중복 확인
|
||||
if (authUserRepository.existsByUserId(request.getUserId())) {
|
||||
throw new RuntimeException("이미 존재하는 사용자 ID입니다: " + request.getUserId());
|
||||
}
|
||||
|
||||
// 고객 ID 중복 확인
|
||||
if (authUserRepository.existsByCustomerId(request.getCustomerId())) {
|
||||
throw new RuntimeException("이미 존재하는 고객 ID입니다: " + request.getCustomerId());
|
||||
}
|
||||
|
||||
// 회선번호 중복 확인 - lineNumber로 사용자 검색해서 존재 여부 확인
|
||||
if (authUserRepository.findByLineNumber(request.getLineNumber()).isPresent()) {
|
||||
throw new RuntimeException("이미 존재하는 회선번호입니다: " + request.getLineNumber());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 코드 유효성 검증
|
||||
*/
|
||||
private void validatePermissionCodes(List<String> permissionCodes) {
|
||||
for (String code : permissionCodes) {
|
||||
try {
|
||||
PermissionCode.fromCode(code);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("유효하지 않은 권한 코드입니다: " + code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 엔티티 생성
|
||||
*/
|
||||
private AuthUserEntity createUserEntity(UserRegistrationRequest request) {
|
||||
// Salt 생성 (UUID 기반)
|
||||
String salt = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
||||
|
||||
// password + salt 결합 후 해시
|
||||
String saltedPassword = request.getPassword() + salt;
|
||||
String hashedPassword = passwordEncoder.encode(saltedPassword);
|
||||
|
||||
return AuthUserEntity.builder()
|
||||
.userId(request.getUserId())
|
||||
.customerId(request.getCustomerId())
|
||||
.lineNumber(request.getLineNumber())
|
||||
.userName(request.getUserName())
|
||||
.passwordHash(hashedPassword)
|
||||
.passwordSalt(salt)
|
||||
.accountStatus(AuthUserEntity.AccountStatus.ACTIVE)
|
||||
.failedLoginCount(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자에게 권한 부여
|
||||
*/
|
||||
private void grantUserPermissions(String userId, List<String> permissionCodes) {
|
||||
for (String permissionCode : permissionCodes) {
|
||||
try {
|
||||
grantPermission(userId, permissionCode, "SYSTEM");
|
||||
} catch (Exception e) {
|
||||
log.error("권한 부여 실패: userId={}, permissionCode={}", userId, permissionCode, e);
|
||||
throw new RuntimeException("권한 부여 중 오류가 발생했습니다: " + permissionCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 등록 응답 생성
|
||||
*/
|
||||
private UserRegistrationResponse buildRegistrationResponse(AuthUserEntity user, List<String> permissions, String userName) {
|
||||
UserRegistrationResponse.UserData userData = UserRegistrationResponse.UserData.builder()
|
||||
.userId(user.getUserId())
|
||||
.customerId(user.getCustomerId())
|
||||
.lineNumber(user.getLineNumber())
|
||||
.userName(userName)
|
||||
.accountStatus(user.getAccountStatus().name())
|
||||
.createdAt(user.getCreatedAt())
|
||||
.permissions(permissions)
|
||||
.build();
|
||||
|
||||
return UserRegistrationResponse.builder()
|
||||
.success(true)
|
||||
.message("사용자가 성공적으로 등록되었습니다.")
|
||||
.data(userData)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 코드에 대한 설명 조회
|
||||
* @param permissionCode 권한 코드
|
||||
|
||||
@@ -1,83 +1,4 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:phonebill_auth}
|
||||
username: ${DB_USERNAME:phonebill_user}
|
||||
password: ${DB_PASSWORD:phonebill_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
# JPA 설정
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis 설정
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
# 개발환경 로깅 설정 (더 상세한 로그)
|
||||
logging:
|
||||
level:
|
||||
com.phonebill: DEBUG
|
||||
org.springframework.security: DEBUG
|
||||
org.springframework.data.redis: DEBUG
|
||||
org.springframework.cache: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql: TRACE
|
||||
org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG
|
||||
org.springframework.transaction: DEBUG
|
||||
pattern:
|
||||
console: "%d{HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] - %msg%n"
|
||||
|
||||
# 개발환경 액추에이터 설정 (모든 엔드포인트 노출)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
|
||||
# 개발용 CORS 설정
|
||||
cors:
|
||||
allowed-origins:
|
||||
- "http://localhost:3000"
|
||||
- "http://localhost:3001"
|
||||
- "http://127.0.0.1:3000"
|
||||
allowed-methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
max-age: 3600
|
||||
|
||||
# 개발환경 보안 설정 (덜 엄격)
|
||||
auth:
|
||||
password:
|
||||
bcrypt-strength: 10 # 개발환경에서는 빠른 처리를 위해 낮춤
|
||||
@@ -1,120 +1,7 @@
|
||||
spring:
|
||||
# 운영환경 데이터소스 설정
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:phonebill_auth}
|
||||
username: ${DB_USERNAME}
|
||||
password: ${DB_PASSWORD}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 50
|
||||
minimum-idle: 10
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 0 # 운영환경에서는 비활성화
|
||||
|
||||
# 운영환경 JPA 설정
|
||||
jpa:
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: false
|
||||
use_sql_comments: false
|
||||
generate_statistics: false
|
||||
jdbc:
|
||||
batch_size: 50
|
||||
order_inserts: true
|
||||
order_updates: true
|
||||
hibernate:
|
||||
ddl-auto: validate # 운영환경에서는 스키마 변경 금지
|
||||
|
||||
# 운영환경 Redis 설정 (클러스터 구성)
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD}
|
||||
ssl: true # 운영환경에서는 SSL 활성화
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: -1ms
|
||||
cluster:
|
||||
refresh:
|
||||
adaptive: true
|
||||
period: 30s
|
||||
database: 0
|
||||
|
||||
# 운영환경 로깅 설정 (최소 로그)
|
||||
logging:
|
||||
level:
|
||||
com.phonebill: INFO
|
||||
org.springframework.security: WARN
|
||||
org.hibernate.SQL: WARN
|
||||
org.springframework.web: WARN
|
||||
org.springframework.data: WARN
|
||||
org.springframework.cache: WARN
|
||||
org.springframework.transaction: WARN
|
||||
org.springframework.boot.actuate: WARN
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] [%X{traceId:-},%X{spanId:-}] - %msg%n"
|
||||
file:
|
||||
name: /var/log/phonebill/user-service.log
|
||||
max-size: 100MB
|
||||
max-history: 30
|
||||
total-size-cap: 3GB
|
||||
|
||||
# 운영환경 액추에이터 설정 (보안 강화)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: never
|
||||
show-components: never
|
||||
security:
|
||||
enabled: true
|
||||
|
||||
# 운영환경 JWT 토큰 설정
|
||||
jwt:
|
||||
secret: ${JWT_SECRET} # 환경변수에서 필수로 받아옴
|
||||
access-token-validity: 1800000 # 30분
|
||||
refresh-token-validity: 86400000 # 24시간
|
||||
|
||||
# 운영환경 보안 설정 (엄격)
|
||||
auth:
|
||||
login:
|
||||
max-failed-attempts: 3 # 운영환경에서는 더 엄격
|
||||
lockout-duration: 3600000 # 1시간
|
||||
password:
|
||||
bcrypt-strength: 12
|
||||
|
||||
# 운영환경 성능 튜닝 설정
|
||||
server:
|
||||
tomcat:
|
||||
threads:
|
||||
max: 200
|
||||
min-spare: 10
|
||||
max-connections: 8192
|
||||
accept-count: 100
|
||||
connection-timeout: 30000
|
||||
max-http-post-size: 2MB
|
||||
|
||||
# 운영환경 외부 서비스 연동 설정
|
||||
external:
|
||||
services:
|
||||
bill-inquiry:
|
||||
base-url: ${BILL_INQUIRY_URL}
|
||||
timeout: 3000 # 운영환경에서는 더 짧은 타임아웃
|
||||
product-change:
|
||||
base-url: ${PRODUCT_CHANGE_URL}
|
||||
timeout: 3000
|
||||
|
||||
|
||||
# 운영환경 모니터링 설정
|
||||
metrics:
|
||||
export:
|
||||
|
||||
@@ -4,18 +4,44 @@ spring:
|
||||
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
|
||||
# JPA 공통 설정
|
||||
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:phonebill_auth}
|
||||
username: ${DB_USERNAME:phonebill_user}
|
||||
password: ${DB_PASSWORD:phonebill_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
# JPA 설정
|
||||
jpa:
|
||||
open-in-view: false
|
||||
show-sql: false
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
default_batch_fetch_size: 100
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:validate}
|
||||
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis 설정
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
# Jackson 설정
|
||||
jackson:
|
||||
property-naming-strategy: SNAKE_CASE
|
||||
@@ -32,23 +58,19 @@ spring:
|
||||
# 서버 설정
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
|
||||
|
||||
# CORS
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000}
|
||||
|
||||
# JWT 토큰 설정
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:Y2xhdWRlLWNvZGUtcGhvbmViaWxsLXNlY3JldC1rZXktZm9yLWF1dGgtc2VydmljZQ==}
|
||||
access-token-validity: 1800000 # 30분 (milliseconds)
|
||||
refresh-token-validity: 86400000 # 24시간 (milliseconds)
|
||||
issuer: phonebill-auth-service
|
||||
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800000}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400000}
|
||||
|
||||
# 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
root: ${LOG_LEVEL_ROOT:INFO}
|
||||
com.phonebill: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.security: DEBUG
|
||||
org.springframework.web: INFO
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql: TRACE
|
||||
file:
|
||||
name: logs/user-service.log
|
||||
|
||||
@@ -94,13 +116,3 @@ auth:
|
||||
auto-login-timeout: 86400000 # 24시간 (milliseconds)
|
||||
password:
|
||||
bcrypt-strength: 12
|
||||
|
||||
# 외부 서비스 연동 설정 (향후 확장용)
|
||||
external:
|
||||
services:
|
||||
bill-inquiry:
|
||||
base-url: ${BILL_INQUIRY_URL:http://localhost:8082}
|
||||
timeout: 5000
|
||||
product-change:
|
||||
base-url: ${PRODUCT_CHANGE_URL:http://localhost:8083}
|
||||
timeout: 5000
|
||||
Reference in New Issue
Block a user