[API] SSO 다중 도메인 로그인 작업 커밋

This commit is contained in:
2026-03-31 17:46:33 +09:00
parent 07683e843d
commit 9da2585f29
50 changed files with 466 additions and 722 deletions
+3 -4
View File
@@ -26,7 +26,6 @@
- `src/main/java/com/alist/api`: 애플리케이션 시작점과 업무 코드를 둔다. - `src/main/java/com/alist/api`: 애플리케이션 시작점과 업무 코드를 둔다.
- `src/main/java/com/alist/api/modules`: 기능별 모듈 패키지를 둔다. - `src/main/java/com/alist/api/modules`: 기능별 모듈 패키지를 둔다.
- `src/main/resources`: 설정 파일과 로깅 설정을 관리한다. - `src/main/resources`: 설정 파일과 로깅 설정을 관리한다.
- `src/test/java`: 테스트 코드를 둔다.
- `deploy`: 배포 관련 리소스가 있으면 이 경로를 우선 확인한다. - `deploy`: 배포 관련 리소스가 있으면 이 경로를 우선 확인한다.
## 현재 확인된 구조 ## 현재 확인된 구조
@@ -35,7 +34,7 @@
- MyBatis는 인터페이스와 XML을 함께 사용한다. - MyBatis는 인터페이스와 XML을 함께 사용한다.
- Mapper 인터페이스는 `src/main/java/.../mapper`, SQL XML은 `src/main/resources/mapper/...` 경로를 짝으로 맞춘다. - Mapper 인터페이스는 `src/main/java/.../mapper`, SQL XML은 `src/main/resources/mapper/...` 경로를 짝으로 맞춘다.
- 공통 응답은 `common/response`, 보안은 `config/security, jwt`, 전역 예외 처리는 `config/exception` 아래에 둔다. - 공통 응답은 `common/response`, 보안은 `config/security, jwt`, 전역 예외 처리는 `config/exception` 아래에 둔다.
- 모듈 패키지는 현재 `auth`, `file`, `main`, `testUser` 형태로 구성되어 있고, 필요한 모듈만 `dto`, `form`, `mapper`, `service`, `vo`를 둔다. - 모듈 패키지는 현재 `auth`, `file`, `main`, `user` 형태로 구성되어 있고, 필요한 모듈만 `dto`, `form`, `mapper`, `service`, `vo`를 둔다.
## 파일 생성 규칙 ## 파일 생성 규칙
- 새 기능은 가능하면 `modules/{도메인명}Controller` 단위로 패키지를 만들고 그 아래에 `service`, `mapper`, `dto`, `vo`를 필요한 만큼만 추가한다. - 새 기능은 가능하면 `modules/{도메인명}Controller` 단위로 패키지를 만들고 그 아래에 `service`, `mapper`, `dto`, `vo`를 필요한 만큼만 추가한다.
@@ -50,7 +49,7 @@
- `Vo`는 조회 결과나 스케줄 실행 판단에 필요한 읽기 전용 성격의 값으로 본다. - `Vo`는 조회 결과나 스케줄 실행 판단에 필요한 읽기 전용 성격의 값으로 본다.
- `Form`은 Controller 입력 검증과 요청 바인딩 전용으로 두고, `@Valid` 와 Jakarta Validation 어노테이션을 우선 사용한다. - `Form`은 Controller 입력 검증과 요청 바인딩 전용으로 두고, `@Valid` 와 Jakarta Validation 어노테이션을 우선 사용한다.
- 현재 코드처럼 DTO/VO는 Lombok `@Getter`, 필요한 경우에만 `@Setter`를 사용한다. - 현재 코드처럼 DTO/VO는 Lombok `@Getter`, 필요한 경우에만 `@Setter`를 사용한다.
- Form 안에는 DTO 변환 메서드를 둘 수 있다. 예: `testUserDto()`, `fileUploadDto()` - Form 안에는 DTO 변환 메서드를 둘 수 있다. 예: `userDto()`, `fileUploadDto()`
- DTO 안에 연관된 다른 DTO 변환이 꼭 필요할 때만 최소한의 보조 메서드를 둔다. - DTO 안에 연관된 다른 DTO 변환이 꼭 필요할 때만 최소한의 보조 메서드를 둔다.
- 외부 응답에 노출되면 안 되는 내부 필드는 DTO 에 `@JsonIgnore`로 숨긴다. - 외부 응답에 노출되면 안 되는 내부 필드는 DTO 에 `@JsonIgnore`로 숨긴다.
- Mapper 메서드명은 SQL 동작이 드러나도록 `select`, `insert`, `update` 접두어를 사용한다. - Mapper 메서드명은 SQL 동작이 드러나도록 `select`, `insert`, `update` 접두어를 사용한다.
@@ -144,7 +143,7 @@
- Swagger: `/v3/api-docs/**`, `/swagger-ui/**` → HTTP Basic 인증 (InMemory) - Swagger: `/v3/api-docs/**`, `/swagger-ui/**` → HTTP Basic 인증 (InMemory)
- API: JWT Bearer 토큰 인증 (Stateless) - API: JWT Bearer 토큰 인증 (Stateless)
- 세션/쿠키: Redis Session 저장소 사용, 쿠키 속성은 프로파일별 `cookie.*` 설정으로 제어 - 세션/쿠키: Redis Session 저장소 사용, 쿠키 속성은 프로파일별 `cookie.*` 설정으로 제어
- 공개 경로: `/`, `/actuator/health`, `/auth/**`, `/test/**`, `/files/tusHook` - 공개 경로: `/`, `/actuator/health`, `/sso/**`, `/auth/**`, `/user/signup`, `/files/tusHook`
- Swagger 인증과 API 인증은 `SecurityFilterChain` 을 분리해서 관리한다. - Swagger 인증과 API 인증은 `SecurityFilterChain` 을 분리해서 관리한다.
## 응답 코드 규칙 ## 응답 코드 규칙
+1 -3
View File
@@ -51,7 +51,6 @@ src/main/java/com/alist/api
└── modules └── modules
├── auth # 인증/세션 관련 API ├── auth # 인증/세션 관련 API
├── main # 루트 리다이렉트 ├── main # 루트 리다이렉트
└── testUser # 테스트 사용자 API
``` ```
리소스 파일은 아래 위치를 사용합니다. 리소스 파일은 아래 위치를 사용합니다.
@@ -141,7 +140,6 @@ java -jar build/libs/api.jar --spring.profiles.active=local
- `/` - `/`
- `/actuator/health` - `/actuator/health`
- `/auth/**` - `/auth/**`
- `/test/**`
- `/files/tusHook` - `/files/tusHook`
루트 `/` 요청은 `/swagger-ui/index.html`로 리다이렉트됩니다. 루트 `/` 요청은 `/swagger-ui/index.html`로 리다이렉트됩니다.
@@ -186,7 +184,7 @@ java -jar build/libs/api.jar --spring.profiles.active=local
- `POST /auth/refresh` - `POST /auth/refresh`
- `GET /auth/loginChecked` - `GET /auth/loginChecked`
- `POST /auth/logout` - `POST /auth/logout`
- `POST /test/testSignup` - `POST /user/signup`
## API 예시 요청/응답 ## API 예시 요청/응답
-1
View File
@@ -59,7 +59,6 @@ dependencies {
// Redis (SSO 세션 공유) // Redis (SSO 세션 공유)
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'
} }
tasks.named('test') { tasks.named('test') {
@@ -1,44 +0,0 @@
package com.alist.api.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 7200) // 2시간
public class RedisSessionConfig {
@Value("${cookie.secure}")
private boolean cookieSecure;
@Value("${cookie.domain:}")
private String cookieDomain;
@Value("${cookie.name}")
private String cookieName;
@Value("${cookie.same-site:Lax}")
private String cookieSameSite;
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
// SSO를 위한 쿠키 설정
serializer.setCookieName(cookieName);
serializer.setDomainName(cookieDomain); // 모든 서브도메인에서 공유
serializer.setCookiePath("/");
serializer.setUseSecureCookie(cookieSecure); // 환경별 설정 (local: false, pjt: true)
serializer.setUseHttpOnlyCookie(true); // XSS 방지
serializer.setSameSite(cookieSameSite); // CSRF 방지
if (cookieDomain != null && !cookieDomain.isBlank()) {
serializer.setDomainName(cookieDomain.trim());
}
return serializer;
}
}
@@ -59,7 +59,7 @@ public class SecurityConfig {
.accessDeniedHandler(new JwtAccessDeniedHandler()) .accessDeniedHandler(new JwtAccessDeniedHandler())
) )
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/actuator/health", "/sso/**", "/auth/**", "/test/**", "/files/tusHook").permitAll() .requestMatchers("/", "/actuator/health", "/sso/**", "/auth/**", "/user/signup", "/files/tusHook").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@@ -1,7 +1,7 @@
package com.alist.api.config.cache; package com.alist.api.config.cache;
import com.alist.api.modules.auth.vo.TestCorsOriginVo; import com.alist.api.modules.auth.vo.CorsOriginVo;
import com.alist.api.modules.auth.mapper.TestCorsMapper; import com.alist.api.modules.auth.mapper.CorsMapper;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CorsAllowedOriginsCache { public class CorsAllowedOriginsCache {
private final TestCorsMapper testCorsMapper; private final CorsMapper corsMapper;
// Thread-safe List (volatile로 가시성 보장) // Thread-safe List (volatile로 가시성 보장)
private volatile List<String> allowedOrigins = new ArrayList<>(); private volatile List<String> allowedOrigins = new ArrayList<>();
@@ -35,9 +35,9 @@ public class CorsAllowedOriginsCache {
*/ */
public synchronized void refresh() { public synchronized void refresh() {
try { try {
List<String> origins = testCorsMapper.selectTestCorsAllowedList() List<String> origins = corsMapper.selectCorsAllowedList()
.stream() .stream()
.map(TestCorsOriginVo::getAllowedOrigin) .map(CorsOriginVo::getAllowedOrigin)
.distinct() .distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -1,31 +1,29 @@
package com.alist.api.modules.auth; package com.alist.api.modules.auth;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.common.utils.SessionUtil; import com.alist.api.common.utils.SessionUtil;
import com.alist.api.config.jwt.JwtTokenProvider;
import com.alist.api.modules.auth.dto.SsoExchangeDto; import com.alist.api.modules.auth.dto.SsoExchangeDto;
import com.alist.api.modules.auth.dto.TestLoginDto; import com.alist.api.modules.auth.dto.LoginDto;
import com.alist.api.modules.auth.dto.TokenDto; import com.alist.api.modules.auth.dto.TokenDto;
import com.alist.api.modules.auth.form.ApikeyLoginForm;
import com.alist.api.modules.auth.form.TokenForm;
import com.alist.api.modules.auth.service.SsoService; import com.alist.api.modules.auth.service.SsoService;
import com.alist.api.modules.auth.service.TestAuthService; import com.alist.api.modules.auth.service.AuthService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.config.jwt.JwtTokenProvider;
import com.alist.api.modules.auth.form.TokenForm;
import jakarta.validation.Valid;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Tag(name = "00. 엑세스 토큰 발급", description = "강제 엑세스 토큰 발급 (JSON 전달, 테스트용)") @Tag(name = "02. 엑세스 토큰 발급", description = "엑세스 토큰 발급")
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
public class AuthController { public class AuthController {
@@ -36,9 +34,6 @@ public class AuthController {
@Value("${cookie.domain}") @Value("${cookie.domain}")
private String cookieDomain; private String cookieDomain;
@Value("${cookie.name}")
private String cookieName;
@Value("${cookie.same-site:Lax}") @Value("${cookie.same-site:Lax}")
private String cookieSameSite; private String cookieSameSite;
@@ -54,22 +49,22 @@ public class AuthController {
private final SsoService ssoService; private final SsoService ssoService;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final TestAuthService testAuthService; private final AuthService authService;
public AuthController(SsoService ssoService, JwtTokenProvider jwtTokenProvider, TestAuthService testAuthService) { public AuthController(SsoService ssoService, JwtTokenProvider jwtTokenProvider, AuthService authService) {
this.ssoService = ssoService; this.ssoService = ssoService;
this.jwtTokenProvider = jwtTokenProvider; this.jwtTokenProvider = jwtTokenProvider;
this.testAuthService = testAuthService; this.authService = authService;
} }
@Operation( @Operation(
summary = "엑세스 토큰 발급", summary = "엑세스 토큰 발급 (테스트용 실사용X)",
description = "엑세스 토큰이 발급됩니다. (테스트용 실사용X)" description = "엑세스 토큰이 발급됩니다. (테스트용 실사용X)"
) )
@PostMapping("/token") @PostMapping("/token")
public ResponseEntity<ApiResponse<TokenDto>> token(@Valid @RequestBody TokenForm tokenForm) { public ResponseEntity<ApiResponse<TokenDto>> token(@Valid @RequestBody TokenForm tokenForm) {
TokenDto tokenDto = new TokenDto(); TokenDto tokenDto = new TokenDto();
tokenDto.setAccessToken(jwtTokenProvider.createToken(tokenForm.tokenDto().getId())); tokenDto.setAccessToken(jwtTokenProvider.createToken(tokenForm.toTokenDto().getId()));
return ApiResponse.entity(tokenDto, ApiResponseCode.CODE_2001, "엑세스 토큰"); return ApiResponse.entity(tokenDto, ApiResponseCode.CODE_2001, "엑세스 토큰");
} }
@@ -93,11 +88,11 @@ public class AuthController {
String accessToken = jwtTokenProvider.createAccessToken(ssoSession.getUserTokenIdx(), ssoSession.getUserRole()); String accessToken = jwtTokenProvider.createAccessToken(ssoSession.getUserTokenIdx(), ssoSession.getUserRole());
String refreshToken = jwtTokenProvider.createRefreshToken(ssoSession.getUserTokenIdx()); String refreshToken = jwtTokenProvider.createRefreshToken(ssoSession.getUserTokenIdx());
TestLoginDto testLoginDto = new TestLoginDto(); LoginDto loginDto = new LoginDto();
testLoginDto.setUserTokenIdx(ssoSession.getUserTokenIdx()); loginDto.setUserTokenIdx(ssoSession.getUserTokenIdx());
testLoginDto.setRefreshToken(refreshToken); loginDto.setRefreshToken(refreshToken);
testAuthService.updateRefreshToken(testLoginDto); authService.updateRefreshToken(loginDto);
SessionUtil.addTokenCookie(response, "accessToken", accessToken, cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds); SessionUtil.addTokenCookie(response, "accessToken", accessToken, cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
SessionUtil.addTokenCookie(response,"refreshToken", refreshToken, cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds); SessionUtil.addTokenCookie(response,"refreshToken", refreshToken, cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
@@ -121,73 +116,30 @@ public class AuthController {
, HttpServletResponse response , HttpServletResponse response
) { ) {
TestLoginDto testLogin = testAuthService.refreshByToken(refreshToken); LoginDto result = authService.refreshByToken(refreshToken);
if (testLogin == null) { if (result == null) {
return ApiResponse.entity(Map.of("refreshed", false), ApiResponseCode.CODE_401); return ApiResponse.entity(Map.of("refreshed", false), ApiResponseCode.CODE_401);
} }
SessionUtil.addTokenCookie(response, "accessToken", testLogin.getAccessToken(), cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds); SessionUtil.addTokenCookie(response, "accessToken", result.getAccessToken(), cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
SessionUtil.addTokenCookie(response, "refreshToken", testLogin.getRefreshToken(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds); SessionUtil.addTokenCookie(response, "refreshToken", result.getRefreshToken(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
return ApiResponse.entity(Map.of("refreshed", true), ApiResponseCode.CODE_200); return ApiResponse.entity(Map.of("refreshed", true), ApiResponseCode.CODE_200);
} }
@Operation( @Operation(
summary = "SSO 로그인 상태 확", summary = "API Key 로그",
description = "SSO 인증 받은 후 현재 세션이 있는지 확인" description = "API Key로 인증하고 JSON으로 토큰을 반환합니다. (OpenAPI/외부 서비스용)"
) )
@GetMapping("/loginChecked") @PostMapping("/apiKeyLogin")
public ResponseEntity<ApiResponse<Map<String, Object>>> loginChecked(HttpServletRequest request) { public ResponseEntity<ApiResponse<LoginDto>> apiKeyLogin(@Valid @RequestBody ApikeyLoginForm apiKeyLoginForm) {
Map<String, Object> sessionInfo = new HashMap<>(); LoginDto result = authService.selectUserApiKeyLogin(apiKeyLoginForm.toLoginDto());
HttpSession session = request.getSession(false); // 새 세션 생성 금지
if (session == null) { if (result.getResultCode() == 2003) {
sessionInfo.put("loggedIn", false); return ApiResponse.entity(result, ApiResponseCode.CODE_2003);
return ApiResponse.entity(sessionInfo, ApiResponseCode.CODE_200);
} }
Integer userIdx = (Integer) session.getAttribute("userIdx"); return ApiResponse.entity(result, ApiResponseCode.CODE_2001, "엑세스 토큰");
if (userIdx == null) {
sessionInfo.put("loggedIn", false);
return ApiResponse.entity(sessionInfo, ApiResponseCode.CODE_200);
}
sessionInfo.put("loggedIn", true);
sessionInfo.put("userIdx", userIdx);
sessionInfo.put("userTokenIdx", session.getAttribute("userTokenIdx"));
sessionInfo.put("userRole", session.getAttribute("userRole"));
sessionInfo.put("userId", session.getAttribute("userId"));
sessionInfo.put("sessionId", session.getId());
return ApiResponse.entity(sessionInfo, ApiResponseCode.CODE_200);
}
@Operation(
summary = "로그아웃",
description = "로그아웃시 redis 에 세션 삭제 합니다."
)
@PostMapping("/logout")
public ResponseEntity<ApiResponse<Map<String, Object>>> logout(
HttpServletRequest request
, HttpServletResponse response
, @CookieValue(name = "refreshToken", required = false) String refreshToken
) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
testAuthService.clearRefreshToken(refreshToken);
SessionUtil.expireCookie(response, cookieName, cookieDomain, cookieSecure, cookieSameSite);
SessionUtil.expireCookie(response, "accessToken", cookieDomain, cookieSecure, cookieSameSite);
SessionUtil.expireCookie(response, "refreshToken", cookieDomain, cookieSecure, cookieSameSite);
Map<String, Object> result = new HashMap<>();
result.put("message", "로그아웃되었습니다.");
return ApiResponse.entity(result, ApiResponseCode.CODE_200);
} }
} }
@@ -13,11 +13,11 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "03. 테스트 CRS 갱신", description = "테스트 CORS 설정 관리 API") @Tag(name = "04. CRS 갱신", description = "테스트 CORS 설정 관리 API")
@RestController @RestController
@RequestMapping("/test") @RequestMapping("/cors")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TestCorsAllowController { public class CorsAllowController {
private final CorsAllowedOriginsCache corsCache; private final CorsAllowedOriginsCache corsCache;
@@ -25,7 +25,7 @@ public class TestCorsAllowController {
summary = "CORS 화이트리스트 갱신", summary = "CORS 화이트리스트 갱신",
description = "DB에서 CORS 허용 도메인 목록을 다시 로딩합니다. DB에 도메인 추가 후 호출하세요." description = "DB에서 CORS 허용 도메인 목록을 다시 로딩합니다. DB에 도메인 추가 후 호출하세요."
) )
@PostMapping("/reloadCors") @PostMapping("/reload")
public ResponseEntity<ApiResponse<List<String>>> reloadCors() { public ResponseEntity<ApiResponse<List<String>>> reloadCors() {
corsCache.refresh(); corsCache.refresh();
List<String> cacheStatus = corsCache.getCacheStatus(); List<String> cacheStatus = corsCache.getCacheStatus();
@@ -9,7 +9,7 @@ import com.alist.api.modules.auth.dto.SsoLoginDto;
import com.alist.api.modules.auth.form.SsoExchangeForm; import com.alist.api.modules.auth.form.SsoExchangeForm;
import com.alist.api.modules.auth.form.SsoLoginForm; import com.alist.api.modules.auth.form.SsoLoginForm;
import com.alist.api.modules.auth.service.SsoService; import com.alist.api.modules.auth.service.SsoService;
import com.alist.api.modules.auth.service.TestAuthService; import com.alist.api.modules.auth.service.AuthService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -23,7 +23,7 @@ import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Tag(name = "02. SSO 인증", description = "공통 로그인, 인가 코드 발급, 코드 교환, 로그아웃 관련 API") @Tag(name = "01. SSO 인증", description = "공통 로그인, 인가 코드 발급, 코드 교환, 로그아웃 관련 API")
@RestController @RestController
@RequestMapping("/sso") @RequestMapping("/sso")
public class SsoController { public class SsoController {
@@ -42,11 +42,11 @@ public class SsoController {
@Value("${sso.cookie.name}") @Value("${sso.cookie.name}")
private String ssoCookieName; private String ssoCookieName;
private final SsoService ssoService; private final SsoService ssoService;
private final TestAuthService testAuthService; private final AuthService authService;
public SsoController(SsoService ssoService, TestAuthService testAuthService) { public SsoController(SsoService ssoService, AuthService authService) {
this.ssoService = ssoService; this.ssoService = ssoService;
this.testAuthService = testAuthService; this.authService = authService;
} }
@Operation( @Operation(
@@ -59,7 +59,7 @@ public class SsoController {
, HttpServletRequest request , HttpServletRequest request
, HttpServletResponse response , HttpServletResponse response
) { ) {
SsoLoginDto ssoLoginDto = ssoService.login(ssoLoginForm.ssoLoginDto(), request); SsoLoginDto ssoLoginDto = ssoService.login(ssoLoginForm.toSsoLoginDto(), request);
SsoLoginCheckDto result = new SsoLoginCheckDto(); SsoLoginCheckDto result = new SsoLoginCheckDto();
@@ -136,7 +136,7 @@ public class SsoController {
@Valid @RequestBody SsoExchangeForm ssoExchangeForm @Valid @RequestBody SsoExchangeForm ssoExchangeForm
, HttpServletResponse response , HttpServletResponse response
) { ) {
SsoExchangeDto result = ssoService.exchange(ssoExchangeForm.ssoExchangeDto()); SsoExchangeDto result = ssoService.exchange(ssoExchangeForm.toSsoExchangeDto());
SessionUtil.addTokenCookie(response, ssoCookieName, result.getSsoSessionId(), cookieDomain, cookieSecure, cookieSameSite, ssoSessionTtlSeconds); SessionUtil.addTokenCookie(response, ssoCookieName, result.getSsoSessionId(), cookieDomain, cookieSecure, cookieSameSite, ssoSessionTtlSeconds);
@@ -156,7 +156,7 @@ public class SsoController {
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName); String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
ssoService.logout(ssoSessionId); ssoService.logout(ssoSessionId);
testAuthService.clearRefreshToken(refreshToken); authService.clearRefreshToken(refreshToken);
SessionUtil.expireCookie(response, ssoCookieName, cookieDomain, cookieSecure, cookieSameSite); SessionUtil.expireCookie(response, ssoCookieName, cookieDomain, cookieSecure, cookieSameSite);
SessionUtil.expireCookie(response, "accessToken", cookieDomain, cookieSecure, cookieSameSite); SessionUtil.expireCookie(response, "accessToken", cookieDomain, cookieSecure, cookieSameSite);
@@ -1,92 +0,0 @@
package com.alist.api.modules.auth;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.common.utils.SessionUtil;
import com.alist.api.modules.auth.dto.TestLoginDto;
import com.alist.api.modules.auth.form.TestApikeyLoginForm;
import com.alist.api.modules.auth.form.TestLoginForm;
import com.alist.api.modules.auth.service.TestAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Tag(name = "01. 테스트 인증", description = "테스트 사용자 로그인 및 API Key 인증 관련 API")
@RestController
@RequestMapping("/test")
public class TestAuthController {
private final TestAuthService testAuthService;
@Value("${cookie.secure}")
private boolean cookieSecure;
@Value("${cookie.domain}")
private String cookieDomain;
@Value("${cookie.same-site:Lax}")
private String cookieSameSite;
@Value("${jwt.access-token-validity-seconds}")
private long accessTokenValiditySeconds;
@Value("${jwt.refresh-token-validity-seconds}")
private long refreshTokenValiditySeconds;
public TestAuthController(TestAuthService testAuthService) {
this.testAuthService = testAuthService;
}
@Operation(
summary = "테스트 사용자 로그인",
description = "아이디/비밀번호로 로그인하고 HttpOnly 쿠키로 토큰을 전달합니다. (웹 브라우저용)"
)
@PostMapping("/testLogin")
public ResponseEntity<ApiResponse<TestLoginDto>> testUserLogin(
@Valid @RequestBody TestLoginForm testLoginForm
, HttpServletResponse response
, HttpSession session
) {
TestLoginDto testLoginDto = testAuthService.selectTestLogin(testLoginForm.testLoginDto());
if (testLoginDto.getResultCode() == 2003) {
return ApiResponse.entity(testLoginDto, ApiResponseCode.CODE_2003);
}
// SSO를 위한 세션에 사용자 정보 저장
session.setAttribute("userIdx", testLoginDto.getUserIdx());
session.setAttribute("userTokenIdx", testLoginDto.getUserTokenIdx());
session.setAttribute("userId", testLoginDto.getId());
session.setAttribute("userRole", "USER");
// accessToken 쿠키 설정
SessionUtil.addTokenCookie(response, "accessToken", testLoginDto.getAccessToken(), cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
// refreshToken 쿠키 설정
SessionUtil.addTokenCookie(response, "refreshToken", testLoginDto.getRefreshToken(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
// JSON 응답에서는 토큰 제거 (쿠키로만 전달)
testLoginDto.setAccessToken(null);
testLoginDto.setRefreshToken(null);
return ApiResponse.entity(testLoginDto, ApiResponseCode.CODE_2001, "로그인");
}
@Operation(
summary = "테스트 API Key 로그인",
description = "API Key로 인증하고 JSON으로 토큰을 반환합니다. (OpenAPI/외부 서비스용)"
)
@PostMapping("/testApiKeyLogin")
public ResponseEntity<ApiResponse<TestLoginDto>> testUserApkKeyLogin(@Valid @RequestBody TestApikeyLoginForm testApiKeyLoginForm) {
TestLoginDto testLoginDto = testAuthService.selectTestUserApiKeyLogin(testApiKeyLoginForm.testLoginDto());
if (testLoginDto.getResultCode() == 2003) {
return ApiResponse.entity(testLoginDto, ApiResponseCode.CODE_2003);
}
return ApiResponse.entity(testLoginDto, ApiResponseCode.CODE_2001, "엑세스 토큰");
}
}
@@ -9,7 +9,7 @@ import java.time.Instant;
@Getter @Getter
@Setter @Setter
public class TestLoginDto { public class LoginDto {
@Schema(description = "아이디") @Schema(description = "아이디")
private String id; private String id;
@Schema(description = "엑세스 토큰") @Schema(description = "엑세스 토큰")
@@ -1,6 +1,5 @@
package com.alist.api.modules.auth.dto; package com.alist.api.modules.auth.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -17,10 +16,10 @@ public class SsoLoginDto {
private String userId; private String userId;
private String userRole; private String userRole;
public TestLoginDto testLoginDto() { public LoginDto toLoginDto() {
TestLoginDto testLoginDto = new TestLoginDto(); LoginDto loginDto = new LoginDto();
testLoginDto.setId(this.id); loginDto.setId(this.id);
testLoginDto.setPassword(this.password); loginDto.setPassword(this.password);
return testLoginDto; return loginDto;
} }
} }
@@ -1,16 +1,14 @@
package com.alist.api.modules.auth.form; package com.alist.api.modules.auth.form;
import com.alist.api.modules.auth.dto.TestLoginDto; import com.alist.api.modules.auth.dto.LoginDto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class TestApikeyLoginForm { public class ApikeyLoginForm {
@Schema( @Schema(
description = "api key", description = "api key",
example = "aaaa111dsddd" example = "aaaa111dsddd"
@@ -18,10 +16,10 @@ public class TestApikeyLoginForm {
@NotBlank(message = "apiKey를 입력해주세요.") @NotBlank(message = "apiKey를 입력해주세요.")
private String userApiKey; private String userApiKey;
public TestLoginDto testLoginDto() { public LoginDto toLoginDto() {
TestLoginDto testLoginDto = new TestLoginDto(); LoginDto loginDto = new LoginDto();
testLoginDto.setUserApiKey(this.userApiKey.trim()); loginDto.setUserApiKey(this.userApiKey.trim());
return testLoginDto; return loginDto;
} }
} }
@@ -12,7 +12,7 @@ public class SsoExchangeForm {
@NotBlank @NotBlank
private String clientId; private String clientId;
public SsoExchangeDto ssoExchangeDto() { public SsoExchangeDto toSsoExchangeDto() {
SsoExchangeDto ssoExchangeDto = new SsoExchangeDto(); SsoExchangeDto ssoExchangeDto = new SsoExchangeDto();
ssoExchangeDto.setCode(code == null ? null : code.trim()); ssoExchangeDto.setCode(code == null ? null : code.trim());
ssoExchangeDto.setClientId(clientId == null ? null : clientId.trim()); ssoExchangeDto.setClientId(clientId == null ? null : clientId.trim());
@@ -12,7 +12,7 @@ public class SsoLoginForm {
@NotBlank @NotBlank
private String password; private String password;
public SsoLoginDto ssoLoginDto() { public SsoLoginDto toSsoLoginDto() {
SsoLoginDto ssoLoginDto = new SsoLoginDto(); SsoLoginDto ssoLoginDto = new SsoLoginDto();
ssoLoginDto.setId(id == null ? null : id.trim()); ssoLoginDto.setId(id == null ? null : id.trim());
ssoLoginDto.setPassword(password == null ? null : password.trim()); ssoLoginDto.setPassword(password == null ? null : password.trim());
@@ -1,40 +0,0 @@
package com.alist.api.modules.auth.form;
import com.alist.api.modules.auth.dto.TestLoginDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TestLoginForm {
@Schema(
description = "사용자 아이디 (공백 불가)",
example = "test"
)
@NotBlank(message = "아이디를 입력해주세요.")
private String id;
@Schema(
description = "비밀번호 (8~64자, 영문 + 숫자 조합, 공백 불가)",
example = "pass1234"
)
@NotBlank(message = "비밀번호를 입력해주세요.")
@Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.")
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d)\\S+$",
message = "비밀번호는 영문과 숫자를 포함하고 공백이 없어야 합니다."
)
private String password;
public TestLoginDto testLoginDto() {
TestLoginDto testLoginDto = new TestLoginDto();
testLoginDto.setId(this.id.trim());
testLoginDto.setPassword(this.password);
return testLoginDto;
}
}
@@ -15,7 +15,7 @@ public class TokenForm {
@Schema(description = "사용자명", example = "test") @Schema(description = "사용자명", example = "test")
private String id; private String id;
public TokenDto tokenDto() { public TokenDto toTokenDto() {
TokenDto tokenDto = new TokenDto(); TokenDto tokenDto = new TokenDto();
tokenDto.setId(this.id.trim()); tokenDto.setId(this.id.trim());
@@ -0,0 +1,24 @@
package com.alist.api.modules.auth.mapper;
import com.alist.api.modules.auth.dto.LoginDto;
import com.alist.api.modules.auth.vo.LoginTokenVo;
import com.alist.api.modules.auth.vo.LoginVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AuthMapper {
LoginVo selectLogin(LoginDto loginDto);
LoginTokenVo selectUserTokenByUserId(LoginDto loginDto);
LoginTokenVo selectUserTokenByUserApiKey(LoginDto loginDto);
void updateRefreshToken(LoginDto loginDto);
LoginTokenVo selectUserTokenByUserTokenIdx(int userTokenIdx);
void clearRefreshTokenByUserTokenIdx(int userTokenIdx);
void updateLoginByLastAt(LoginDto loginDto);
}
@@ -1,15 +1,15 @@
package com.alist.api.modules.auth.mapper; package com.alist.api.modules.auth.mapper;
import com.alist.api.modules.auth.vo.TestCorsOriginVo; import com.alist.api.modules.auth.vo.CorsOriginVo;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@Mapper @Mapper
public interface TestCorsMapper { public interface CorsMapper {
/** /**
* 전체 CORS 허용 도메인 목록 조회 (user_token_idx별로) * 전체 CORS 허용 도메인 목록 조회 (user_token_idx별로)
* @return CorsOriginDto 리스트 * @return CorsOriginDto 리스트
*/ */
List<TestCorsOriginVo> selectTestCorsAllowedList(); List<CorsOriginVo> selectCorsAllowedList();
} }
@@ -5,5 +5,5 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface SsoMapper { public interface SsoMapper {
SsoClientVo selectSsoClient(String clientId, String redirectUri); SsoClientVo selectSsoClient(String clientId);
} }
@@ -1,24 +0,0 @@
package com.alist.api.modules.auth.mapper;
import com.alist.api.modules.auth.dto.TestLoginDto;
import com.alist.api.modules.auth.vo.TestLoginTokenVo;
import com.alist.api.modules.auth.vo.TestLoginVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface TestAuthMapper {
TestLoginVo selectTestLogin(TestLoginDto testLoginDto);
TestLoginTokenVo selectTestUserTokenByUserId(TestLoginDto testLoginDto);
TestLoginTokenVo selectTestUserTokenByUserApiKey(TestLoginDto testLoginDto);
void updateRefreshToken(TestLoginDto testLoginDto);
TestLoginTokenVo selectTestUserTokenByUserTokenIdx(@Param("userTokenIdx") int userTokenIdx);
void clearRefreshTokenByUserTokenIdx(@Param("userTokenIdx") int userTokenIdx);
void updateTestLoginByLastAt(TestLoginDto testLoginDto);
}
@@ -0,0 +1,144 @@
package com.alist.api.modules.auth.service;
import com.alist.api.config.jwt.JwtTokenProvider;
import com.alist.api.config.properties.JwtProperties;
import com.alist.api.modules.auth.dto.LoginDto;
import com.alist.api.modules.auth.mapper.AuthMapper;
import com.alist.api.modules.auth.vo.LoginTokenVo;
import com.alist.api.modules.auth.vo.LoginVo;
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.Instant;
@Slf4j
@Service
public class AuthService {
private final AuthMapper authMapper;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final JwtProperties jwtProperties;
public AuthService(AuthMapper authMapper, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider, JwtProperties jwtProperties) {
this.authMapper = authMapper;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.jwtProperties = jwtProperties;
}
// id login
public LoginDto selectLogin(LoginDto loginDto) {
LoginVo loginVo = authMapper.selectLogin(loginDto);
if (loginVo != null) {
if (passwordEncoder.matches(loginDto.getPassword(), loginVo.getPassword())) {
loginDto.setUserIdx(loginVo.getUserIdx());
LoginTokenVo loginTokenVo = authMapper.selectUserTokenByUserId(loginDto);
if (loginTokenVo != null) {
loginDto.setAccessToken(jwtTokenProvider.createAccessToken(loginTokenVo.getUserTokenIdx(), loginTokenVo.getUserRole()));
loginDto.setRefreshToken(jwtTokenProvider.createRefreshToken(loginTokenVo.getUserTokenIdx()));
loginDto.setUserTokenIdx(loginTokenVo.getUserTokenIdx());
Instant now = Instant.now();
Instant expiresAt = now.plusSeconds(jwtProperties.getRefreshTokenValiditySeconds());
loginDto.setExpiresAt(expiresAt);
authMapper.updateRefreshToken(loginDto);
// 마지막 로그인 시간 추가
authMapper.updateLoginByLastAt(loginDto);
loginDto.setResultCode(2001);
} else {
loginDto.setResultCode(2003);
}
} else {
loginDto.setResultCode(2003);
}
} else {
loginDto.setResultCode(2003);
}
return loginDto;
}
// api key login
public LoginDto selectUserApiKeyLogin(LoginDto loginDto) {
if (loginDto.getUserApiKey() != null) {
LoginTokenVo loginTokenVo = authMapper.selectUserTokenByUserApiKey(loginDto);
if (loginTokenVo != null) {
loginDto.setAccessToken(jwtTokenProvider.createAccessToken(loginTokenVo.getUserTokenIdx(), loginTokenVo.getUserRole()));
loginDto.setRefreshToken(jwtTokenProvider.createRefreshToken(loginTokenVo.getUserTokenIdx()));
loginDto.setResultCode(2001);
} else {
loginDto.setResultCode(2003);
}
} else {
loginDto.setResultCode(2003);
}
return loginDto;
}
public LoginDto refreshByToken(String refreshToken) {
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
return null;
}
int userTokenIdx;
try {
userTokenIdx = Integer.parseInt(jwtTokenProvider.getUserTokenIdx(refreshToken));
} catch (NumberFormatException e) {
return null;
}
LoginTokenVo tokenVo = authMapper.selectUserTokenByUserTokenIdx(userTokenIdx);
if (tokenVo == null || tokenVo.getRefreshToken() == null) {
return null;
}
if (!refreshToken.equals(tokenVo.getRefreshToken())) {
return null;
}
LoginDto result = new LoginDto();
result.setUserTokenIdx(userTokenIdx);
result.setAccessToken(jwtTokenProvider.createAccessToken(tokenVo.getUserTokenIdx(), tokenVo.getUserRole()));
result.setRefreshToken(jwtTokenProvider.createRefreshToken(tokenVo.getUserTokenIdx()));
result.setExpiresAt(Instant.now().plusSeconds(jwtProperties.getRefreshTokenValiditySeconds()));
authMapper.updateRefreshToken(result);
return result;
}
public void clearRefreshToken(String refreshToken) {
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
return;
}
try {
int userTokenIdx = Integer.parseInt(jwtTokenProvider.getUserTokenIdx(refreshToken));
authMapper.clearRefreshTokenByUserTokenIdx(userTokenIdx);
} catch (NumberFormatException ignored) {
}
}
@Transactional
public void updateRefreshToken(LoginDto loginDto) {
Instant now = Instant.now();
Instant expiresAt = now.plusSeconds(jwtProperties.getRefreshTokenValiditySeconds());
loginDto.setExpiresAt(expiresAt);
authMapper.updateRefreshToken(loginDto);
}
}
@@ -2,7 +2,7 @@ package com.alist.api.modules.auth.service;
import com.alist.api.modules.auth.dto.SsoExchangeDto; import com.alist.api.modules.auth.dto.SsoExchangeDto;
import com.alist.api.modules.auth.dto.SsoLoginDto; import com.alist.api.modules.auth.dto.SsoLoginDto;
import com.alist.api.modules.auth.dto.TestLoginDto; import com.alist.api.modules.auth.dto.LoginDto;
import com.alist.api.modules.auth.mapper.SsoMapper; import com.alist.api.modules.auth.mapper.SsoMapper;
import com.alist.api.modules.auth.vo.SsoClientVo; import com.alist.api.modules.auth.vo.SsoClientVo;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -18,34 +18,32 @@ import java.util.UUID;
@Service @Service
public class SsoService { public class SsoService {
private final TestAuthService testAuthService; private final AuthService authService;
private final SsoMapper ssoMapper; private final SsoMapper ssoMapper;
private final StringRedisTemplate redisTemplate; private final StringRedisTemplate redisTemplate;
private final Duration ssoSessionTtl; private final Duration ssoSessionTtl;
private final Duration ssoCodeTtl; private final Duration ssoCodeTtl;
public SsoService(TestAuthService testAuthService, SsoMapper ssoMapper, StringRedisTemplate redisTemplate, @Value("${sso.session.ttl-seconds}") long ssoSessionTtlSeconds, @Value("${sso.code.ttl-seconds}") long ssoCodeTtlSeconds) { public SsoService(AuthService authService, SsoMapper ssoMapper, StringRedisTemplate redisTemplate, @Value("${sso.session.ttl-seconds}") long ssoSessionTtlSeconds, @Value("${sso.code.ttl-seconds}") long ssoCodeTtlSeconds) {
this.testAuthService = testAuthService; this.authService = authService;
this.ssoMapper = ssoMapper; this.ssoMapper = ssoMapper;
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
this.ssoSessionTtl = Duration.ofSeconds(ssoSessionTtlSeconds); this.ssoSessionTtl = Duration.ofSeconds(ssoSessionTtlSeconds);
this.ssoCodeTtl = Duration.ofSeconds(ssoCodeTtlSeconds); this.ssoCodeTtl = Duration.ofSeconds(ssoCodeTtlSeconds);
} }
public SsoLoginDto login(SsoLoginDto loginDto, HttpServletRequest request) { public SsoLoginDto login(SsoLoginDto ssoLoginDto, HttpServletRequest request) {
TestLoginDto testLoginDto = testAuthService.selectTestLogin(loginDto.testLoginDto()); LoginDto loginDto = authService.selectLogin(ssoLoginDto.toLoginDto());
SsoLoginDto result = new SsoLoginDto(); SsoLoginDto result = new SsoLoginDto();
result.setResultCode(testLoginDto.getResultCode()); result.setResultCode(loginDto.getResultCode());
if (testLoginDto.getResultCode() == 2003) { if (loginDto.getResultCode() == 2003) {
return result; return result;
} }
String ssoSessionId = "SSO_" + UUID.randomUUID(); String ssoSessionId = "SSO_" + UUID.randomUUID();
String latestSessionKey = "alist:sso:userIdx:" + testLoginDto.getUserIdx(); String latestSessionKey = "alist:sso:userIdx:" + loginDto.getUserIdx();
String sessionKey = "alist:sso:session:" + ssoSessionId; String sessionKey = "alist:sso:session:" + ssoSessionId;
String oldSsoSessionId = redisTemplate.opsForValue().get(latestSessionKey); String oldSsoSessionId = redisTemplate.opsForValue().get(latestSessionKey);
@@ -53,17 +51,17 @@ public class SsoService {
redisTemplate.delete("alist:sso:session:" + oldSsoSessionId); redisTemplate.delete("alist:sso:session:" + oldSsoSessionId);
} }
redisTemplate.opsForHash().put(sessionKey, "userIdx", String.valueOf(testLoginDto.getUserIdx())); redisTemplate.opsForHash().put(sessionKey, "userIdx", String.valueOf(loginDto.getUserIdx()));
redisTemplate.opsForHash().put(sessionKey, "userTokenIdx", String.valueOf(testLoginDto.getUserTokenIdx())); redisTemplate.opsForHash().put(sessionKey, "userTokenIdx", String.valueOf(loginDto.getUserTokenIdx()));
redisTemplate.opsForHash().put(sessionKey, "userId", testLoginDto.getId()); redisTemplate.opsForHash().put(sessionKey, "userId", loginDto.getId());
redisTemplate.opsForHash().put(sessionKey, "userRole", "USER"); redisTemplate.opsForHash().put(sessionKey, "userRole", "USER");
redisTemplate.expire(sessionKey, ssoSessionTtl); redisTemplate.expire(sessionKey, ssoSessionTtl);
redisTemplate.opsForValue().set(latestSessionKey, ssoSessionId, ssoSessionTtl); redisTemplate.opsForValue().set(latestSessionKey, ssoSessionId, ssoSessionTtl);
result.setSsoSessionId(ssoSessionId); result.setSsoSessionId(ssoSessionId);
result.setUserId(testLoginDto.getId()); result.setUserId(loginDto.getId());
result.setUserIdx(testLoginDto.getUserIdx()); result.setUserIdx(loginDto.getUserIdx());
result.setUserRole("USER"); result.setUserRole("USER");
result.setResultCode(200); result.setResultCode(200);
@@ -119,7 +117,7 @@ public class SsoService {
return "/login"; return "/login";
} }
SsoClientVo ssoClientVo = ssoMapper.selectSsoClient(clientId, redirectUri); SsoClientVo ssoClientVo = ssoMapper.selectSsoClient(clientId);
if (ssoClientVo == null) { if (ssoClientVo == null) {
throw new IllegalArgumentException("허용되지 않은 clientId 또는 redirectUri 입니다."); throw new IllegalArgumentException("허용되지 않은 clientId 또는 redirectUri 입니다.");
} }
@@ -1,144 +0,0 @@
package com.alist.api.modules.auth.service;
import com.alist.api.config.jwt.JwtTokenProvider;
import com.alist.api.config.properties.JwtProperties;
import com.alist.api.modules.auth.dto.TestLoginDto;
import com.alist.api.modules.auth.mapper.TestAuthMapper;
import com.alist.api.modules.auth.vo.TestLoginTokenVo;
import com.alist.api.modules.auth.vo.TestLoginVo;
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.Instant;
@Slf4j
@Service
public class TestAuthService {
private final TestAuthMapper testAuthMapper;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final JwtProperties jwtProperties;
public TestAuthService(TestAuthMapper testAuthMapper, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider, JwtProperties jwtProperties) {
this.testAuthMapper = testAuthMapper;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.jwtProperties = jwtProperties;
}
// id login
public TestLoginDto selectTestLogin(TestLoginDto testLoginDto) {
TestLoginVo testLoginVo = testAuthMapper.selectTestLogin(testLoginDto);
if (testLoginVo != null) {
if (passwordEncoder.matches(testLoginDto.getPassword(), testLoginVo.getPassword())) {
testLoginDto.setUserIdx(testLoginVo.getUserIdx());
TestLoginTokenVo testLoginTokenVo = testAuthMapper.selectTestUserTokenByUserId(testLoginDto);
if (testLoginTokenVo != null) {
testLoginDto.setAccessToken(jwtTokenProvider.createAccessToken(testLoginTokenVo.getUserTokenIdx(), testLoginTokenVo.getUserRole()));
testLoginDto.setRefreshToken(jwtTokenProvider.createRefreshToken(testLoginTokenVo.getUserTokenIdx()));
testLoginDto.setUserTokenIdx(testLoginTokenVo.getUserTokenIdx());
Instant now = Instant.now();
Instant expiresAt = now.plusSeconds(jwtProperties.getRefreshTokenValiditySeconds());
testLoginDto.setExpiresAt(expiresAt);
testAuthMapper.updateRefreshToken(testLoginDto);
// 마지막 로그인 시간 추가
testAuthMapper.updateTestLoginByLastAt(testLoginDto);
testLoginDto.setResultCode(2001);
} else {
testLoginDto.setResultCode(2003);
}
} else {
testLoginDto.setResultCode(2003);
}
} else {
testLoginDto.setResultCode(2003);
}
return testLoginDto;
}
// api key login
public TestLoginDto selectTestUserApiKeyLogin(TestLoginDto testLoginDto) {
if (testLoginDto.getUserApiKey() != null) {
TestLoginTokenVo testLoginTokenVo = testAuthMapper.selectTestUserTokenByUserApiKey(testLoginDto);
if (testLoginTokenVo != null) {
testLoginDto.setAccessToken(jwtTokenProvider.createAccessToken(testLoginTokenVo.getUserTokenIdx(), testLoginTokenVo.getUserRole()));
testLoginDto.setRefreshToken(jwtTokenProvider.createRefreshToken(testLoginTokenVo.getUserTokenIdx()));
testLoginDto.setResultCode(2001);
} else {
testLoginDto.setResultCode(2003);
}
} else {
testLoginDto.setResultCode(2003);
}
return testLoginDto;
}
public TestLoginDto refreshByToken(String refreshToken) {
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
return null;
}
int userTokenIdx;
try {
userTokenIdx = Integer.parseInt(jwtTokenProvider.getUserTokenIdx(refreshToken));
} catch (NumberFormatException e) {
return null;
}
TestLoginTokenVo tokenVo = testAuthMapper.selectTestUserTokenByUserTokenIdx(userTokenIdx);
if (tokenVo == null || tokenVo.getRefreshToken() == null) {
return null;
}
if (!refreshToken.equals(tokenVo.getRefreshToken())) {
return null;
}
TestLoginDto testLogin = new TestLoginDto();
testLogin.setUserTokenIdx(userTokenIdx);
testLogin.setAccessToken(jwtTokenProvider.createAccessToken(tokenVo.getUserTokenIdx(), tokenVo.getUserRole()));
testLogin.setRefreshToken(jwtTokenProvider.createRefreshToken(tokenVo.getUserTokenIdx()));
testLogin.setExpiresAt(Instant.now().plusSeconds(jwtProperties.getRefreshTokenValiditySeconds()));
testAuthMapper.updateRefreshToken(testLogin);
return testLogin;
}
public void clearRefreshToken(String refreshToken) {
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
return;
}
try {
int userTokenIdx = Integer.parseInt(jwtTokenProvider.getUserTokenIdx(refreshToken));
testAuthMapper.clearRefreshTokenByUserTokenIdx(userTokenIdx);
} catch (NumberFormatException ignored) {
}
}
@Transactional
public void updateRefreshToken(TestLoginDto testLoginDto) {
Instant now = Instant.now();
Instant expiresAt = now.plusSeconds(jwtProperties.getRefreshTokenValiditySeconds());
testLoginDto.setExpiresAt(expiresAt);
testAuthMapper.updateRefreshToken(testLoginDto);
}
}
@@ -5,6 +5,6 @@ import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class TestCorsOriginVo { public class CorsOriginVo {
private String allowedOrigin; private String allowedOrigin;
} }
@@ -3,7 +3,7 @@ package com.alist.api.modules.auth.vo;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public class TestLoginTokenVo { public class LoginTokenVo {
private Integer userTokenIdx; private Integer userTokenIdx;
private String userApiKey; private String userApiKey;
private String userRole; private String userRole;
@@ -3,7 +3,7 @@ package com.alist.api.modules.auth.vo;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public class TestLoginVo { public class LoginVo {
private Integer userIdx; private Integer userIdx;
private String id; private String id;
private String password; private String password;
@@ -1,20 +1,22 @@
package com.alist.api.modules.file; package com.alist.api.modules.file;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.common.utils.SecurityUtil; import com.alist.api.common.utils.SecurityUtil;
import com.alist.api.modules.file.dto.*; import com.alist.api.modules.file.dto.*;
import com.alist.api.modules.file.form.*; import com.alist.api.modules.file.form.*;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.modules.file.service.FileService; import com.alist.api.modules.file.service.FileService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.*; import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException; import java.io.IOException;
@@ -44,7 +46,7 @@ public class FileController {
public ResponseEntity<ApiResponse<FileUploadDto>> uploadInit( public ResponseEntity<ApiResponse<FileUploadDto>> uploadInit(
@Valid @RequestBody FileUploadForm fileUploadForm @Valid @RequestBody FileUploadForm fileUploadForm
) { ) {
FileUploadDto fileUploadDto = fileUploadForm.fileUploadDto(); FileUploadDto fileUploadDto = fileUploadForm.toFileUploadDto();
Integer userTokenIdx = SecurityUtil.getLoginUserTokenIdx(); Integer userTokenIdx = SecurityUtil.getLoginUserTokenIdx();
@@ -109,7 +111,7 @@ public class FileController {
public ResponseEntity<ApiResponse<UploadStatusDto>> uploadStatus( public ResponseEntity<ApiResponse<UploadStatusDto>> uploadStatus(
@Valid @RequestBody UploadStatusForm uploadStatusForm @Valid @RequestBody UploadStatusForm uploadStatusForm
) { ) {
UploadStatusDto status = fileService.getUploadStatus(uploadStatusForm.uploadStatusDto()); UploadStatusDto status = fileService.getUploadStatus(uploadStatusForm.toUploadStatusDto());
if (status == null) { if (status == null) {
return ApiResponse.entity(new UploadStatusDto(), ApiResponseCode.CODE_2003); return ApiResponse.entity(new UploadStatusDto(), ApiResponseCode.CODE_2003);
} }
@@ -122,10 +124,10 @@ public class FileController {
) )
@PostMapping("/tusHook") @PostMapping("/tusHook")
public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) { public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) {
boolean accepted = fileService.updateFileUploadStatus(tusHookForm.tusHookDto()); boolean accepted = fileService.updateFileUploadStatus(tusHookForm.toTusHookDto());
if (accepted) { if (accepted) {
fileService.saveUploadStatusRedisTusHook(tusHookForm.tusHookDto()); fileService.saveUploadStatusRedisTusHook(tusHookForm.toTusHookDto());
} }
return ApiResponse.entity("", ApiResponseCode.CODE_200); return ApiResponse.entity("", ApiResponseCode.CODE_200);
@@ -273,7 +275,7 @@ public class FileController {
public ResponseEntity<ApiResponse<String>> fileDelete( public ResponseEntity<ApiResponse<String>> fileDelete(
@Valid @RequestBody FileDeleteForm fileDeleteForm @Valid @RequestBody FileDeleteForm fileDeleteForm
) { ) {
boolean deleted = fileService.updateFileDelete(fileDeleteForm.fileDeleteDto()); boolean deleted = fileService.updateFileDelete(fileDeleteForm.toFileDeleteDto());
if (!deleted) { if (!deleted) {
return ApiResponse.entity("", ApiResponseCode.CODE_2005, "파일 삭제"); return ApiResponse.entity("", ApiResponseCode.CODE_2005, "파일 삭제");
@@ -11,7 +11,7 @@ public class FileDeleteForm {
@NotBlank @NotBlank
private String fileUuid; private String fileUuid;
public FileDeleteDto fileDeleteDto() { public FileDeleteDto toFileDeleteDto() {
FileDeleteDto fileDeleteDto = new FileDeleteDto(); FileDeleteDto fileDeleteDto = new FileDeleteDto();
fileDeleteDto.setFileUuid(fileUuid == null ? null : fileUuid.trim()); fileDeleteDto.setFileUuid(fileUuid == null ? null : fileUuid.trim());
return fileDeleteDto; return fileDeleteDto;
@@ -23,7 +23,7 @@ public class FileUploadForm {
@Valid @Valid
private List<FileUploadItemForm> itemList; private List<FileUploadItemForm> itemList;
public FileUploadDto fileUploadDto() { public FileUploadDto toFileUploadDto() {
FileUploadDto fileUploadDto = new FileUploadDto(); FileUploadDto fileUploadDto = new FileUploadDto();
fileUploadDto.setFileCategory(this.fileCategory.trim()); fileUploadDto.setFileCategory(this.fileCategory.trim());
@@ -16,7 +16,7 @@ public class TusHookForm {
@JsonProperty("Event") @JsonProperty("Event")
private TusHookEventForm tusHookEventForm; private TusHookEventForm tusHookEventForm;
public TusHookDto tusHookDto() { public TusHookDto toTusHookDto() {
TusHookDto tusHookDto = new TusHookDto(); TusHookDto tusHookDto = new TusHookDto();
tusHookDto.setType(this.type); tusHookDto.setType(this.type);
@@ -11,7 +11,7 @@ public class UploadStatusForm {
@NotBlank @NotBlank
private String fileUuid; private String fileUuid;
public UploadStatusDto uploadStatusDto() { public UploadStatusDto toUploadStatusDto() {
UploadStatusDto uploadStatusDto = new UploadStatusDto(); UploadStatusDto uploadStatusDto = new UploadStatusDto();
uploadStatusDto.setFileUuid(this.fileUuid == null ? null : this.fileUuid.trim()); uploadStatusDto.setFileUuid(this.fileUuid == null ? null : this.fileUuid.trim());
return uploadStatusDto; return uploadStatusDto;
@@ -1,46 +0,0 @@
package com.alist.api.modules.testUser;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.modules.testUser.dto.TestUserDto;
import com.alist.api.modules.testUser.form.TestUserSignupForm;
import com.alist.api.modules.testUser.service.TestUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "02. 테스트 사용자 관리", description = "테스트 사용자 회원가입 및 관리 API")
@Slf4j
@RestController
@RequestMapping("/test")
public class TestUserController {
public final TestUserService testUserService;
public TestUserController(TestUserService testUserService) {
this.testUserService = testUserService;
}
@Operation(
summary = "테스트 사용자 회원가입",
description = "새로운 테스트 사용자를 등록합니다. 아이디 중복 시 오류를 반환합니다."
)
@PostMapping("/testSignup")
public ResponseEntity<ApiResponse<TestUserDto>> testSignup(@Valid @RequestBody TestUserSignupForm testUserSignupForm) {
TestUserDto testUserDto = testUserService.insertTestUserProc(testUserSignupForm.testUserDto());
if (testUserDto.getResultCode() == 2004) {
return ApiResponse.entity(testUserDto, ApiResponseCode.CODE_2004, "아이디");
}
return ApiResponse.entity(testUserDto, ApiResponseCode.CODE_2002, "아이디");
}
}
@@ -1,14 +0,0 @@
package com.alist.api.modules.testUser.mapper;
import com.alist.api.modules.testUser.dto.TestUserDto;
import com.alist.api.modules.testUser.dto.TestUserTokenDto;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TestUserMapper {
int selectDuplicateTestUserCount(TestUserDto testUserDto);
int insertTestUserSignup(TestUserDto testUserDto);
int insertTestUserTokenSignup(TestUserTokenDto testUserTokenDto);
}
@@ -1,65 +0,0 @@
package com.alist.api.modules.testUser.service;
import com.alist.api.common.utils.ApiKeyGenerator;
import com.alist.api.modules.testUser.dto.TestUserDto;
import com.alist.api.modules.testUser.dto.TestUserTokenDto;
import com.alist.api.modules.testUser.mapper.TestUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class TestUserService {
private final TestUserMapper testUserMapper;
private final PasswordEncoder passwordEncoder;
public TestUserService(TestUserMapper testUserMapper, PasswordEncoder passwordEncoder) {
this.testUserMapper = testUserMapper;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public TestUserDto insertTestUserProc(TestUserDto testUserDto) {
// 중복체크
if (testUserMapper.selectDuplicateTestUserCount(testUserDto) > 0) {
testUserDto.setResultCode(2004);
return testUserDto;
}
// 비밀번호 생성
testUserDto.setNewPassword(passwordEncoder.encode(testUserDto.getPassword()));
// 회원정보 입력
int result = testUserMapper.insertTestUserSignup(testUserDto);
if (result > 0) {
// user_api_key 생성
TestUserTokenDto testUserTokenDto = new TestUserTokenDto();
testUserTokenDto.setUserIdx(testUserDto.getUserIdx());
apiKeyWithRetry(testUserTokenDto);
}
log.info("cnt : " + testUserDto.getUserIdx());
log.info("userIdx : " + testUserDto.getUserIdx());
return testUserDto;
}
@Transactional
public int apiKeyWithRetry(TestUserTokenDto testUserTokenDto) {
for (int i = 0; i < 5; i++) {
testUserTokenDto.setUserApiKey(ApiKeyGenerator.userApiKeyProc());
try {
return testUserMapper.insertTestUserTokenSignup(testUserTokenDto);
} catch (DuplicateKeyException e) {
// 충돌이면 다시 생성
log.warn("Duplicate API key. retrying... userId={}", testUserTokenDto.getUserIdx());
}
}
throw new IllegalStateException("API key generation failed after retries.");
}
}
@@ -1,8 +0,0 @@
package com.alist.api.modules.testUser.vo;
import lombok.Getter;
@Getter
public class TestUserVo {
private String id;
}
@@ -0,0 +1,46 @@
package com.alist.api.modules.user;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.modules.user.dto.UserDto;
import com.alist.api.modules.user.form.UserSignupForm;
import com.alist.api.modules.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "05. 사용자 관리", description = "사용자 회원가입 및 관리 API")
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
public final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@Operation(
summary = "사용자 회원가입",
description = "새로운 사용자를 등록합니다. 아이디 중복 시 오류를 반환합니다."
)
@PostMapping("/signup")
public ResponseEntity<ApiResponse<UserDto>> signup(@Valid @RequestBody UserSignupForm userSignupForm) {
UserDto userDto = userService.insertUser(userSignupForm.toUserDto());
if (userDto.getResultCode() == 2004) {
return ApiResponse.entity(userDto, ApiResponseCode.CODE_2004, "아이디");
}
return ApiResponse.entity(userDto, ApiResponseCode.CODE_2002, "아이디");
}
}
@@ -1,4 +1,4 @@
package com.alist.api.modules.testUser.dto; package com.alist.api.modules.user.dto;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter; import lombok.Getter;
@@ -6,7 +6,7 @@ import lombok.Setter;
@Setter @Setter
@Getter @Getter
public class TestUserDto { public class UserDto {
private Integer userIdx; private Integer userIdx;
private String id; private String id;
@@ -1,11 +1,11 @@
package com.alist.api.modules.testUser.dto; package com.alist.api.modules.user.dto;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Setter @Setter
@Getter @Getter
public class TestUserTokenDto { public class UserTokenDto {
private Integer userTokenIdx; private Integer userTokenIdx;
private Integer userIdx; private Integer userIdx;
private String userApiKey; private String userApiKey;
@@ -1,6 +1,6 @@
package com.alist.api.modules.testUser.form; package com.alist.api.modules.user.form;
import com.alist.api.modules.testUser.dto.TestUserDto; import com.alist.api.modules.user.dto.UserDto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
@@ -11,7 +11,7 @@ import lombok.Setter;
@Setter @Setter
@Getter @Getter
@Schema(description = "회원 가입 요청 폼") @Schema(description = "회원 가입 요청 폼")
public class TestUserSignupForm { public class UserSignupForm {
@Schema( @Schema(
description = "사용자 아이디 (공백 불가)", description = "사용자 아이디 (공백 불가)",
example = "test" example = "test"
@@ -31,10 +31,10 @@ public class TestUserSignupForm {
) )
private String password; private String password;
public TestUserDto testUserDto() { public UserDto toUserDto() {
TestUserDto testUserDto = new TestUserDto(); UserDto userDto = new UserDto();
testUserDto.setId(id.trim()); userDto.setId(id.trim());
testUserDto.setPassword(password); userDto.setPassword(password);
return testUserDto; return userDto;
} }
} }
@@ -0,0 +1,14 @@
package com.alist.api.modules.user.mapper;
import com.alist.api.modules.user.dto.UserDto;
import com.alist.api.modules.user.dto.UserTokenDto;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
int selectDuplicateUserCount(UserDto userDto);
int insertUserSignup(UserDto userDto);
int insertUserTokenSignup(UserTokenDto userTokenDto);
}
@@ -0,0 +1,62 @@
package com.alist.api.modules.user.service;
import com.alist.api.common.utils.ApiKeyGenerator;
import com.alist.api.modules.user.dto.UserDto;
import com.alist.api.modules.user.dto.UserTokenDto;
import com.alist.api.modules.user.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
public UserService(UserMapper userMapper, PasswordEncoder passwordEncoder) {
this.userMapper = userMapper;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public UserDto insertUser(UserDto userDto) {
// 중복체크
if (userMapper.selectDuplicateUserCount(userDto) > 0) {
userDto.setResultCode(2004);
return userDto;
}
// 비밀번호 생성
userDto.setNewPassword(passwordEncoder.encode(userDto.getPassword()));
// 회원정보 입력
int result = userMapper.insertUserSignup(userDto);
if (result > 0) {
// user_api_key 생성
UserTokenDto userTokenDto = new UserTokenDto();
userTokenDto.setUserIdx(userDto.getUserIdx());
apiKeyWithRetry(userTokenDto);
}
return userDto;
}
@Transactional
public int apiKeyWithRetry(UserTokenDto userTokenDto) {
for (int i = 0; i < 5; i++) {
userTokenDto.setUserApiKey(ApiKeyGenerator.userApiKeyProc());
try {
return userMapper.insertUserTokenSignup(userTokenDto);
} catch (DuplicateKeyException e) {
// 충돌이면 다시 생성
log.warn("Duplicate API key. retrying... userId={}", userTokenDto.getUserIdx());
}
}
throw new IllegalStateException("API key generation failed after retries.");
}
}
+3 -10
View File
@@ -11,19 +11,12 @@ spring:
minimum-idle: 1 # 최소로 minimum-idle: 1 # 최소로
connection-timeout: 10000 connection-timeout: 10000
idle-timeout: 30000 idle-timeout: 30000
data: data:
redis: redis:
host: 121.160.234.222 host: 121.160.234.222
port: 3001 port: 3001
password: 1qaz2wsx!@ password: 1qaz2wsx!@
session:
store-type: redis
timeout: 7200 # 2시간 (초 단위)
redis:
namespace: spring:session:alist
logging: logging:
config: classpath:logback-local.xml config: classpath:logback-local.xml
@@ -33,10 +26,10 @@ jwt:
refresh-token-validity-seconds: 2592000 refresh-token-validity-seconds: 2592000
cookie: cookie:
secure: true # 로컬 개발 환경 (HTTP) secure: true # (HTTP) -> false
domain: api-alist.pjt.kr domain: api-alist.pjt.kr # (HTTP) -> 비워두세요
name: ALIST_SSO name: ALIST_SSO
same-site: None same-site: None # (HTTP) -> Lax
swagger: swagger:
login: login:
+1 -8
View File
@@ -13,19 +13,12 @@ spring:
minimum-idle: 1 # 최소로 minimum-idle: 1 # 최소로
connection-timeout: 10000 connection-timeout: 10000
idle-timeout: 30000 idle-timeout: 30000
data: data:
redis: redis:
host: ${REDIS_HOST} host: ${REDIS_HOST}
port: ${REDIS_PORT} port: ${REDIS_PORT}
password: ${REDIS_PASSWORD} password: ${REDIS_PASSWORD}
session:
store-type: redis
timeout: 7200 # 2시간 (초 단위)
redis:
namespace: spring:session:alist
logging: logging:
config: classpath:logback-pjt.xml config: classpath:logback-pjt.xml
@@ -36,7 +29,7 @@ jwt:
cookie: cookie:
secure: true # 프로젝트 환경 (HTTPS) secure: true # 프로젝트 환경 (HTTPS)
domain: api-alist.pjt.kr # sso 인증시 주석해제 domain: api-alist.pjt.kr
name: ALIST_SSO name: ALIST_SSO
same-site: None same-site: None
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alist.api.modules.user.mapper.UserMapper">
<insert id="insertUserSignup" useGeneratedKeys="true" keyProperty="userIdx">
/*UserMapper.insertUserSignup*/
insert into ALISTLMS.test_user (id, password, del_yn, create_at, update_at)
value (#{id}, #{newPassword}, 1, now(), now())
</insert>
<insert id="insertUserTokenSignup" useGeneratedKeys="true" keyProperty="userTokenIdx">
/*UserMapper.insertUserTokenSignup*/
insert into ALISTLMS.test_user_token (user_idx, user_api_key, user_role, created_at, updated_at)
value (#{userIdx}, #{userApiKey}, 'user', now(), now())
</insert>
<select id="selectDuplicateUserCount" resultType="int">
/*UserMapper.selectDuplicateUserCount*/
select count(*)
from ALISTLMS.test_user
where id = #{id}
</select>
</mapper>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alist.api.modules.auth.mapper.CorsMapper">
<select id="selectCorsAllowedList" resultType="com.alist.api.modules.auth.vo.CorsOriginVo">
/* CorsMapper.selectCorsAllowedList */
SELECT allowed_origin
FROM ALISTLMS.test_cors_allowed_list
WHERE del_yn = 1
ORDER BY cors_idx
</select>
</mapper>
@@ -1,65 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alist.api.modules.auth.mapper.TestAuthMapper"> <mapper namespace="com.alist.api.modules.auth.mapper.AuthMapper">
<update id="updateRefreshToken"> <update id="updateRefreshToken">
/*TestLoginMapper.selectTestLogin*/ /*LoginMapper.updateRefreshToken*/
update test_user_token update ALISTLMS.test_user_token
set refresh_token = #{refreshToken} set refresh_token = #{refreshToken}
, expires_at = #{expiresAt} , expires_at = #{expiresAt}
, updated_at = now() , updated_at = now()
where user_token_idx = #{userTokenIdx} where user_token_idx = #{userTokenIdx}
</update> </update>
<select id="selectTestLogin" resultType="com.alist.api.modules.auth.vo.TestLoginVo"> <select id="selectLogin" resultType="com.alist.api.modules.auth.vo.LoginVo">
/*TestLoginMapper.selectTestLogin*/ /*LoginMapper.selectLogin*/
select user_idx select user_idx
, id , id
, password , password
from test_user from ALISTLMS.test_user
where id = #{id} where id = #{id}
and del_yn = 1 and del_yn = 1
</select> </select>
<select id="selectTestUserTokenByUserId" resultType="com.alist.api.modules.auth.vo.TestLoginTokenVo"> <select id="selectUserTokenByUserId" resultType="com.alist.api.modules.auth.vo.LoginTokenVo">
/*TestLoginMapper.selectUserTokenByUserId*/ /*LoginMapper.selectUserTokenByUserId*/
select user_token_idx select user_token_idx
, user_role , user_role
, refresh_token , refresh_token
from test_user_token from ALISTLMS.test_user_token
where user_idx = #{userIdx} where user_idx = #{userIdx}
</select> </select>
<select id="selectTestUserTokenByUserApiKey" resultType="com.alist.api.modules.auth.vo.TestLoginTokenVo"> <select id="selectUserTokenByUserApiKey" resultType="com.alist.api.modules.auth.vo.LoginTokenVo">
/*TestLoginMapper.selectTestUserTokenByUserApiKey*/ /*LoginMapper.selectUserTokenByUserApiKey*/
select user_token_idx select user_token_idx
, user_role , user_role
, refresh_token , refresh_token
from test_user_token from ALISTLMS.test_user_token
where user_api_key = #{userApiKey} where user_api_key = #{userApiKey}
</select> </select>
<select id="selectTestUserTokenByUserTokenIdx" resultType="com.alist.api.modules.auth.vo.TestLoginTokenVo"> <select id="selectUserTokenByUserTokenIdx" resultType="com.alist.api.modules.auth.vo.LoginTokenVo">
/*TestLoginMapper.selectTestUserTokenByUserTokenIdx*/ /*LoginMapper.selectUserTokenByUserTokenIdx*/
select user_token_idx select user_token_idx
, user_role , user_role
, refresh_token , refresh_token
from test_user_token from ALISTLMS.test_user_token
where user_token_idx = #{userTokenIdx} where user_token_idx = #{userTokenIdx}
</select> </select>
<update id="clearRefreshTokenByUserTokenIdx"> <update id="clearRefreshTokenByUserTokenIdx">
/*TestLoginMapper.clearRefreshTokenByUserTokenIdx*/ /*LoginMapper.clearRefreshTokenByUserTokenIdx*/
update test_user_token update ALISTLMS.test_user_token
set refresh_token = null set refresh_token = null
, expires_at = null , expires_at = null
, updated_at = now() , updated_at = now()
where user_token_idx = #{userTokenIdx} where user_token_idx = #{userTokenIdx}
</update> </update>
<update id="updateTestLoginByLastAt"> <update id="updateLoginByLastAt">
/*TestLoginMapper.updateTestLoginByLastAt*/ /*LoginMapper.updateLoginByLastAt*/
update test_user update ALISTLMS.test_user
set last_login_at = now() set last_login_at = now()
, update_at = now() , update_at = now()
where user_idx = #{userIdx} where user_idx = #{userIdx}
+1 -1
View File
@@ -10,7 +10,7 @@
, REDIRECT_URI , REDIRECT_URI
, MEMO , MEMO
, USE_YN , USE_YN
FROM SSO_CLIENT FROM ALISTLMS.SSO_CLIENT
WHERE CLIENT_ID = #{clientId} WHERE CLIENT_ID = #{clientId}
AND USE_YN = 'Y' AND USE_YN = 'Y'
LIMIT 1 LIMIT 1
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alist.api.modules.auth.mapper.TestCorsMapper">
<select id="selectTestCorsAllowedList" resultType="com.alist.api.modules.auth.vo.TestCorsOriginVo">
/* TestCorsMapper.selectTestCorsAllowedList */
SELECT allowed_origin
FROM test_cors_allowed_list
WHERE del_yn = 1
ORDER BY cors_idx
</select>
</mapper>
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alist.api.modules.testUser.mapper.TestUserMapper">
<insert id="insertTestUserSignup" useGeneratedKeys="true" keyProperty="userIdx">
/*UserSignupMapper.insertTestUserSignup*/
insert into test_user (id, password, del_yn, create_at, update_at)
value (#{id}, #{newPassword}, 1, now(), now())
</insert>
<insert id="insertTestUserTokenSignup" useGeneratedKeys="true" keyProperty="userTokenIdx">
/*UserSignupMapper.insertTestUserTokenSignup*/
insert into test_user_token (user_idx, user_api_key, user_role, created_at, updated_at)
value (#{userIdx}, #{userApiKey}, 'user', now(), now())
</insert>
<select id="selectDuplicateTestUserCount" resultType="int">
/*UserSignupMapper.selectDuplicateTestUserCount*/
select count(*)
from test_user
where id = #{id}
</select>
</mapper>