Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation("com.google.code.gson:gson:2.10.1")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.ecommercewebservice.domain.admin.controller;

import com.example.ecommercewebservice.domain.user.dto.UserRoleUpdateRequest;
import com.example.ecommercewebservice.domain.user.service.UserService;
import com.example.ecommercewebservice.global.security.annotation.RoleRequired;
import com.example.ecommercewebservice.domain.user.entity.UserRole;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
@RoleRequired(UserRole.ADMIN)
public class AdminController {

private final UserService userService;

/**
* 사용자 역할 변경 API
* 관리자만 접근 가능
*
* @param userId 변경할 사용자 ID
* @param request 역할 변경 요청
* @return 성공/실패 응답
*/
@PutMapping("/users/{userId}/role")
public ResponseEntity<?> updateUserRole(
@PathVariable Long userId,
@RequestBody UserRoleUpdateRequest request) {
userService.updateUserRole(userId, request.getRole());
return ResponseEntity.ok().build();
}

/**
* 관리자 대시보드 API
* 관리자만 접근 가능
*
* @return 대시보드 데이터
*/
@GetMapping("/dashboard")
public ResponseEntity<?> getDashboard() {
// TODO: 대시보드 데이터 조회 로직 구현
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.ecommercewebservice.domain.user.dto;

import com.example.ecommercewebservice.domain.user.entity.UserRole;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class UserRoleUpdateRequest {
private UserRole role;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.example.ecommercewebservice.domain.user.dto.LoginRequest;
import com.example.ecommercewebservice.domain.user.dto.LoginResponse;
import com.example.ecommercewebservice.domain.user.dto.SignupRequest;
import com.example.ecommercewebservice.domain.user.dto.UserRoleUpdateRequest;
import com.example.ecommercewebservice.domain.user.entity.User;
import com.example.ecommercewebservice.domain.user.entity.UserRole;

public interface UserService {
/**
Expand All @@ -29,4 +31,12 @@ public interface UserService {
*/
void logout(String token);

/**
* 사용자 역할 변경
* 관리자만 호출 가능
*
* @param userId 변경할 사용자 ID
* @param role 새로운 역할
*/
void updateUserRole(Long userId, UserRole role);
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,14 @@ public void logout(String token) {
throw new BusinessException(ErrorCode.INVALID_TOKEN);
}
}

@Override
@Transactional
public void updateUserRole(Long userId, UserRole role) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found with id: " + userId));

user.getRoles().clear();
user.getRoles().add(role.getRole());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisC

return redisTemplate;
}
}
}//
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.ecommercewebservice.global.security.annotation;

import com.example.ecommercewebservice.domain.user.entity.UserRole;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleRequired {
UserRole[] value() default {UserRole.USER};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.example.ecommercewebservice.global.security.aspect;

import com.example.ecommercewebservice.domain.user.entity.UserRole;
import com.example.ecommercewebservice.global.security.annotation.RoleRequired;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
* RoleRequired 어노테이션을 처리하는 AOP 구현체
* 어노테이션이 적용된 메서드가 호출될 때 권한 검사를 수행
*/
@Slf4j
@Aspect
@Component
public class RoleRequiredAspect {

/**
* RoleRequired 어노테이션이 적용된 메서드 호출을 가로채서 권한 검사
*
* @param joinPoint 가로챈 메서드 실행 지점
* @param roleRequired 메서드에 적용된 RoleRequired 어노테이션
* @return 원본 메서드의 실행 결과
* @throws Throwable 메서드 실행 중 발생한 예외 또는 권한 부족 시 AccessDeniedException
*/
@Around("@annotation(roleRequired) || @within(roleRequired)")
public Object checkRole(ProceedingJoinPoint joinPoint, RoleRequired roleRequired) throws Throwable {
// 메서드 레벨 어노테이션이 없으면 클래스 레벨 어노테이션 확인
if (roleRequired == null) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();

// 메서드에 어노테이션이 있는지 확인
roleRequired = method.getAnnotation(RoleRequired.class);

// 메서드에 없으면 클래스에서 확인
if (roleRequired == null) {
roleRequired = method.getDeclaringClass().getAnnotation(RoleRequired.class);
}
}

// 어노테이션이 없으면 그냥 진행
if (roleRequired == null) {
return joinPoint.proceed();
}

// 현재 인증 정보 가져오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || !authentication.isAuthenticated()) {
log.warn("인증되지 않은 사용자의 보호된 메서드 접근 시도: {}", joinPoint.getSignature());
throw new AccessDeniedException("인증되지 않은 사용자입니다.");
}

UserRole[] requiredRoles = roleRequired.value();
log.debug("권한 검사 시작: 필요한 권한={}, 메서드={}",
Arrays.toString(requiredRoles), joinPoint.getSignature());

// 필요한 권한 중 하나라도 있는지 확인
boolean hasRequiredRole = Arrays.stream(requiredRoles)
.anyMatch(role -> authentication.getAuthorities().contains(
new SimpleGrantedAuthority(role.getRole())));

if (!hasRequiredRole) {
log.warn("권한 부족: 사용자={}, 필요한 권한={}, 메서드={}",
authentication.getName(), Arrays.toString(requiredRoles), joinPoint.getSignature());
throw new AccessDeniedException("접근 권한이 없습니다. 필요한 권한: " + Arrays.toString(requiredRoles));
}

log.debug("권한 검사 통과: 사용자={}, 메서드={}",
authentication.getName(), joinPoint.getSignature());

// 권한 검사 통과 시 원래 메서드 실행
return joinPoint.proceed();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletRequest;

/**
* JWT 토큰의 생성, 검증, 파싱 등을 담당하는 유틸리티 클래스
Expand All @@ -34,6 +36,8 @@ public class JwtTokenProvider {
private long tokenValidityInMilliseconds;
private String issuer;
private TokenRepository tokenRepository;
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";

/**
* 기본 생성자
Expand Down Expand Up @@ -170,4 +174,35 @@ public boolean validateToken(String token) {
public void invalidateToken(String token) {
tokenRepository.invalidateToken(token);
}

/**
* HTTP 요청에서 JWT 토큰을 추출
*
* @param request HTTP 요청
* @return 추출된 JWT 토큰 또는 null
*/
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (bearerToken != null && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}

/**
* JWT 토큰에서 사용자 역할을 추출
*
* @param token JWT 토큰
* @return 사용자 역할 목록
*/
public List<String> getRoles(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();

String authorities = claims.get("auth", String.class);
return Arrays.asList(authorities.split(","));
}
}
Loading