[api] 파일업로드, TUS path통일, 권한 내용 변경

This commit is contained in:
2026-05-12 14:40:54 +09:00
parent a4a8b551b0
commit 4e6c87798f
22 changed files with 262 additions and 57 deletions
@@ -61,7 +61,7 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/admin/auth/**"
, "/admin/files/**"
, "/admin/user/signup"
).permitAll()
.anyRequest().hasRole("ADMIN")
)
@@ -28,7 +28,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
try {
String requestUri = request.getRequestURI();
boolean adminRequest = requestUri.startsWith("/admin/");
String token = resolveToken(request, adminRequest);
boolean sharedRequest = requestUri.startsWith("/tusFiles/") || requestUri.startsWith("/files/");
String token = resolveToken(request, adminRequest, sharedRequest);
if (token != null && jwtTokenProvider.validateToken(token)) {
String principal = jwtTokenProvider.getUserTokenIdx(token);
@@ -36,27 +37,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String scope = jwtTokenProvider.getScope(token);
String tokenType = jwtTokenProvider.getTokenType(token);
if (adminRequest) {
if (!"ADMIN".equals(scope) || !"ACCESS".equals(tokenType) || !"ADMIN".equals(role)) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Admin token required");
return;
}
} else {
if ("ADMIN".equals(scope) || "UPLOAD".equals(scope) || !"ACCESS".equals(tokenType)) {
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access token required");
return;
}
boolean userAccessToken = "USER".equals(scope) && role != null && "ACCESS".equals(tokenType);
boolean adminAccessToken = "ADMIN".equals(scope) && "ADMIN".equals(role) && "ACCESS".equals(tokenType);
boolean authenticatable = adminRequest ? adminAccessToken : sharedRequest ? userAccessToken || adminAccessToken : userAccessToken;
if (!authenticatable) {
filterChain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
principal,
null,
role == null
? List.of()
: List.of(new SimpleGrantedAuthority("ROLE_" + role))
List.of(new SimpleGrantedAuthority("ROLE_" + role))
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
@@ -69,8 +65,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request, boolean adminRequest) {
// 1) Authorization: Bearer xxx 우선
private String resolveToken(HttpServletRequest request, boolean adminRequest, boolean sharedRequest) {
String bearer = request.getHeader(HttpHeaders.AUTHORIZATION);
if (bearer != null && bearer.startsWith("Bearer ")) {
String token = bearer.substring(7).trim();
@@ -79,14 +74,27 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
}
// 2) 쿠키 fallback (브라우저용)
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
String cookieName = adminRequest ? "adminAccessToken" : "accessToken";
if (adminRequest) {
return resolveCookie(cookies, "adminAccessToken");
}
if (sharedRequest) {
String accessToken = resolveCookie(cookies, "accessToken");
if (accessToken != null) {
return accessToken;
}
return resolveCookie(cookies, "adminAccessToken");
}
return resolveCookie(cookies, "accessToken");
}
private String resolveCookie(Cookie[] cookies, String cookieName) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
String token = cookie.getValue();
@@ -95,15 +103,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
}
}
return null;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI();
return "/tusFiles/uploadAuth".equals(uri)
|| "/tusFiles/tusHook".equals(uri);
}
}
@@ -34,6 +34,8 @@ public class JwtTokenProvider {
return Jwts.builder()
.setSubject(userId)
.claim("role", "TEST")
.claim("scope", "USER")
.claim("tokenType", "ACCESS")
.setIssuedAt(now)
.setExpiration(expiry)
@@ -49,6 +51,7 @@ public class JwtTokenProvider {
return Jwts.builder()
.setSubject(String.valueOf(userTokenIdx))
.claim("role", role)
.claim("scope", "USER")
.claim("tokenType", "ACCESS")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiry))
@@ -63,6 +66,7 @@ public class JwtTokenProvider {
return Jwts.builder()
.setSubject(String.valueOf(userTokenIdx))
.claim("scope", "USER")
.claim("tokenType", "REFRESH")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiry))
@@ -19,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
@Tag(
name = "03. Admin 로그인"
name = "50. Admin 로그인"
, description = "ADMIN 사이트 로그인입니다. 로그인시 토큰 발급이 함께됩니다."
)
@RestController
@@ -0,0 +1,44 @@
package com.alist.api.modules.admin.user;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.modules.admin.user.form.AdminUserSignupForm;
import com.alist.api.modules.admin.user.service.AdminUserService;
import com.alist.api.modules.admin.user.vo.AdminUserVo;
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 = "51. ADMIN 사용자 관리", description = "ADMIN 사용자 회원가입 및 관리 API")
@RestController
@Slf4j
@RequestMapping("/admin/user")
public class AdminUserController {
private final AdminUserService adminUserService;
public AdminUserController(AdminUserService adminUserService) {
this.adminUserService = adminUserService;
}
@Operation(
summary = "사용자 회원가입",
description = "새로운 사용자를 등록합니다. 아이디 중복 시 오류를 반환합니다."
)
@PostMapping("/signup")
public ResponseEntity<ApiResponse<AdminUserVo>> signup(@Valid @RequestBody AdminUserSignupForm adminUserSignupForm) {
AdminUserVo result = adminUserService.adminInsertUser(adminUserSignupForm.toAdminUserDto());
if (result.getResultCode() == 2004) {
return ApiResponse.entity(result, ApiResponseCode.CODE_2004, "아이디");
}
return ApiResponse.entity(result, ApiResponseCode.CODE_2002, "아이디");
}
}
@@ -0,0 +1,20 @@
package com.alist.api.modules.admin.user.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class AdminUserDto {
private Integer userIdx;
private String id;
private String userRole;
@JsonIgnore
private String newPassword;
@JsonIgnore
private String password;
@JsonIgnore
private int resultCode;
}
@@ -0,0 +1,45 @@
package com.alist.api.modules.admin.user.form;
import com.alist.api.modules.admin.user.dto.AdminUserDto;
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;
@Setter
@Getter
@Schema(description = "회원 가입 요청 폼")
public class AdminUserSignupForm {
@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;
@NotBlank(message = "사용자 구분(A:관리자)")
@Pattern(regexp = "^A$", message = "관리자 사용자 구분은 A만 가능합니다.")
private String userRole;
public AdminUserDto toAdminUserDto() {
AdminUserDto adminUserDto = new AdminUserDto();
adminUserDto.setId(id.trim());
adminUserDto.setPassword(password);
adminUserDto.setUserRole(userRole);
return adminUserDto;
}
}
@@ -0,0 +1,41 @@
package com.alist.api.modules.admin.user.service;
import com.alist.api.modules.admin.user.dto.AdminUserDto;
import com.alist.api.modules.admin.user.vo.AdminUserVo;
import com.alist.api.modules.user.dto.UserDto;
import com.alist.api.modules.user.dto.UserTokenDto;
import com.alist.api.modules.user.service.UserService;
import com.alist.api.modules.user.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class AdminUserService {
private final UserService userService;
public AdminUserService(UserService userService) {
this.userService = userService;
}
@Transactional
public AdminUserVo adminInsertUser(AdminUserDto adminUserDto) {
UserDto userDto = new UserDto();
userDto.setId(adminUserDto.getId());
userDto.setPassword(adminUserDto.getPassword());
userDto.setUserRole(adminUserDto.getUserRole());
UserVo userVo = userService.insertUser(userDto);
AdminUserVo result = new AdminUserVo();
result.setResultCode(userVo.getResultCode());
result.setUserIdx(userVo.getUserIdx());
result.setId(userVo.getId());
return result;
}
}
@@ -0,0 +1,20 @@
package com.alist.api.modules.admin.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class AdminUserVo {
private Integer userIdx;
private String id;
private String userRole;
@JsonIgnore
private String newPassword;
@JsonIgnore
private String password;
@JsonIgnore
private int resultCode;
}
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "04. CRS 갱신", description = "테스트 CORS 설정 관리 API")
@Tag(name = "97. CRS 갱신", description = "테스트 CORS 설정 관리 API")
@RestController
@RequestMapping("/cors")
@RequiredArgsConstructor
@@ -20,6 +20,8 @@ public class LoginDto {
@JsonIgnore
private String password;
@JsonIgnore
private String userRole;
@JsonIgnore
private String userApiKey;
@JsonIgnore
private Integer userTokenIdx;
@@ -44,6 +44,7 @@ public class AuthService {
loginDto.setRefreshToken(jwtTokenProvider.createRefreshToken(loginTokenVo.getUserTokenIdx()));
loginDto.setUserTokenIdx(loginTokenVo.getUserTokenIdx());
loginDto.setUserRole(loginTokenVo.getUserRole());
Instant now = Instant.now();
Instant expiresAt = now.plusSeconds(jwtProperties.getRefreshTokenValiditySeconds());
@@ -54,7 +54,7 @@ public class SsoService {
redisTemplate.opsForHash().put(sessionKey, "userIdx", String.valueOf(loginDto.getUserIdx()));
redisTemplate.opsForHash().put(sessionKey, "userTokenIdx", String.valueOf(loginDto.getUserTokenIdx()));
redisTemplate.opsForHash().put(sessionKey, "userId", loginDto.getId());
redisTemplate.opsForHash().put(sessionKey, "userRole", "USER");
redisTemplate.opsForHash().put(sessionKey, "userRole", loginDto.getUserRole());
redisTemplate.expire(sessionKey, ssoSessionTtl);
redisTemplate.opsForValue().set(latestSessionKey, ssoSessionId, ssoSessionTtl);
@@ -62,7 +62,7 @@ public class SsoService {
result.setSsoSessionId(ssoSessionId);
result.setUserId(loginDto.getId());
result.setUserIdx(loginDto.getUserIdx());
result.setUserRole("USER");
result.setUserRole(loginDto.getUserRole());
result.setResultCode(200);
return result;
@@ -21,7 +21,7 @@ import java.util.List;
description = "DB 기록 없이 파일을 저장하고 uploadPath를 반환하는 단순 업로드 API입니다. SunEditor 이미지는 file-domain URL을 반환합니다."
)
@RestController
@RequestMapping({"/files", "/admin/files"})
@RequestMapping("/files")
public class FileController {
private final FileService fileService;
@@ -2,11 +2,11 @@ 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.MigrationUserForm;
import com.alist.api.modules.user.form.UserSignupForm;
import com.alist.api.modules.user.service.UserService;
import com.alist.api.modules.user.vo.MigrationUserVo;
import com.alist.api.modules.user.vo.UserVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -17,8 +17,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "05. 사용자 관리", description = "사용자 회원가입 및 관리 API")
@Slf4j
@RestController
@@ -36,15 +34,15 @@ public class UserController {
description = "새로운 사용자를 등록합니다. 아이디 중복 시 오류를 반환합니다."
)
@PostMapping("/signup")
public ResponseEntity<ApiResponse<UserDto>> signup(@Valid @RequestBody UserSignupForm userSignupForm) {
public ResponseEntity<ApiResponse<UserVo>> signup(@Valid @RequestBody UserSignupForm userSignupForm) {
UserDto userDto = userService.insertUser(userSignupForm.toUserDto());
UserVo userVo = userService.insertUser(userSignupForm.toUserDto());
if (userDto.getResultCode() == 2004) {
return ApiResponse.entity(userDto, ApiResponseCode.CODE_2004, "아이디");
if (userVo.getResultCode() == 2004) {
return ApiResponse.entity(userVo, ApiResponseCode.CODE_2004, "아이디");
}
return ApiResponse.entity(userDto, ApiResponseCode.CODE_2002, "아이디");
return ApiResponse.entity(userVo, ApiResponseCode.CODE_2002, "아이디");
}
@Operation(
@@ -9,6 +9,7 @@ import lombok.Setter;
public class UserDto {
private Integer userIdx;
private String id;
private String userRole;
@JsonIgnore
private String newPassword;
@@ -31,10 +31,14 @@ public class UserSignupForm {
)
private String password;
@NotBlank(message = "사용자 구분(S1: 학생, S2: 수강생, T:교사)")
private String userRole;
public UserDto toUserDto() {
UserDto userDto = new UserDto();
userDto.setId(id.trim());
userDto.setPassword(password);
userDto.setUserRole(userRole);
return userDto;
}
}
@@ -11,15 +11,14 @@ 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 com.alist.api.modules.user.vo.MigrationUserVo;
import com.alist.api.modules.user.vo.UserVo;
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;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@@ -38,28 +37,35 @@ public class UserService {
}
@Transactional
public UserDto insertUser(UserDto userDto) {
public UserVo insertUser(UserDto userDto) {
UserVo result = new UserVo();
result.setId(userDto.getId());
result.setUserRole(userDto.getUserRole());
// 중복체크
if (userMapper.selectDuplicateUserCount(userDto) > 0) {
userDto.setResultCode(2004);
return userDto;
result.setResultCode(2004);
return result;
}
// 비밀번호 생성
userDto.setNewPassword(passwordEncoder.encode(userDto.getPassword()));
// 회원정보 입력
int result = userMapper.insertUserSignup(userDto);
int resultCount = userMapper.insertUserSignup(userDto);
if (result > 0) {
if (resultCount > 0) {
// user_api_key 생성
UserTokenDto userTokenDto = new UserTokenDto();
userTokenDto.setUserIdx(userDto.getUserIdx());
userTokenDto.setUserRole(userDto.getUserRole());
result.setUserIdx(userDto.getUserIdx());
apiKeyWithRetry(userTokenDto);
}
return userDto;
return result;
}
@Transactional
@@ -0,0 +1,20 @@
package com.alist.api.modules.user.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class UserVo {
private Integer userIdx;
private String id;
private String userRole;
@JsonIgnore
private String newPassword;
@JsonIgnore
private String password;
@JsonIgnore
private int resultCode;
}
@@ -10,7 +10,7 @@
<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())
value (#{userIdx}, #{userApiKey}, #{userRole}, now(), now())
</insert>
<select id="selectDuplicateUserCount" resultType="int">
@@ -9,7 +9,7 @@
, expires_at = #{expiresAt}
, updated_at = now()
WHERE user_token_idx = #{userTokenIdx}
AND user_role = 'admin'
AND user_role IN ('A')
</update>
<update id="updateAdminLastLoginAt">
/*AdminLoginMapper.updateAdminLastLoginAt*/
@@ -25,7 +25,7 @@
, expires_at = null
, updated_at = now()
WHERE user_token_idx = #{userTokenIdx}
AND user_role = 'admin'
AND user_role IN ('A')
</update>
<select id="selectAdminLoginById" resultType="com.alist.api.modules.admin.auth.vo.AdminLoginVo">
/*AdminLoginMapper.selectAdminLoginById*/
@@ -37,7 +37,7 @@
INNER JOIN ALISTLMS.test_user_token ATUT ON ATUT.user_idx = ATU.user_idx
WHERE ATU.id = #{id}
AND ATU.del_yn = 1
AND ATUT.user_role = 'admin'
AND ATUT.user_role IN ('A')
LIMIT 1
</select>
<select id="selectAdminTokenByUserTokenIdx" resultType="com.alist.api.modules.admin.auth.vo.AdminLoginVo">
@@ -49,7 +49,7 @@
FROM ALISTLMS.test_user_token ATUT
INNER JOIN ALISTLMS.test_user ATU ON ATU.user_idx = ATUT.user_idx
WHERE ATUT.user_token_idx = #{userTokenIdx}
AND ATUT.user_role = 'admin'
AND ATUT.user_role IN ('A')
AND ATU.del_yn = 1
LIMIT 1
</select>
@@ -20,7 +20,7 @@
INNER JOIN ALISTLMS.test_user_token ATUT ON ATUT.user_idx = ATU.user_idx
WHERE ATU.id = #{id}
AND ATU.del_yn = 1
AND ATUT.user_role = 'user'
AND ATUT.user_role IN ('T','S1','S2')
LIMIT 1
</select>