본문 바로가기

Back-End/Spring-MVC

JWT 기반 인증 및 역할별 API 설계와 구현 : 관리자와 사용자 권한 처리

JWT를 사용해 초기 관리자를 설정하고 이를 통해 인증과 권한 관리를 구현하는 법. 사용자와 관리자로 역할을 나누고, 역할에 따라 접근 가능한 API를 RESTful하게 설계 한다!!


1. RESTful API 설계

1. 관리자 전용 API
   - 모든 사용자 조회
   - 특정 사용자 삭제

2. 사용자 전용 API
   - 사용자 프로필 조회
   - 일정 생성 및 삭제

3. Public API
   - 회원가입
   - 로그인

관리자 전용 API

HTTP Method URI 설명 권한
GET /api/admin/users 모든 사용자 조회 ADMIN
GET /api/admin/users/{id} 특정 사용자 조회 ADMIN
DELETE /api/admin/users/{id} 사용자 삭제 ADMIN

사용자 전용 API

HTTP Method URI 설명 권한
GET /api/user/profile 사용자 프로필 조회 USER, ADMIN
POST /api/user/schedules 새 일정 생성 USER, ADMIN
DELETE /api/user/schedules/{id} 본인이 작성한 일정 삭제 USER, ADMIN

Public 전용 API

HTTP Method URI 설명 권한
POST /api/public/register 회원가입 Public
POST /api/public/login 로그인 Public

2. 코드 구현

JwtFilter

필터는 요청의 JWT를 검증하고 역할(Role)에 따라 요청을 차단하거나 허용

@Component
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
        String authorizationHeader = httpRequest.getHeader("Authorization");

        // Public API는 필터를 통과
        if (requestURI.startsWith("/api/public")) {
            chain.doFilter(request, response);
            return;
        }

        // JWT 검증
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰이 필요합니다.");
            return;
        }

        String jwt = authorizationHeader.substring(7);
        if (!jwtUtil.validateToken(jwt)) {
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "유효하지 않은 JWT 토큰입니다.");
            return;
        }

        String role = jwtUtil.extractRoles(jwt);

        // 관리자 전용 API 검증
        if (requestURI.startsWith("/api/admin") && !"ADMIN".equals(role)) {
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "ADMIN 권한이 필요합니다.");
            return;
        }

        // 사용자 전용 API 검증
        if (requestURI.startsWith("/api/user") && !"USER".equals(role) && !"ADMIN".equals(role)) {
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "USER 권한이 필요합니다.");
            return;
        }

        chain.doFilter(request, response);
    }
}

Controller와 Service

AdminController

관리자는 사용자 데이터를 관리할 수 있는 권한이 있으며, 아래와 같은 API를 제공합니다:
1. 모든 사용자 조회
2. 특정 사용자 삭제
@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController {

    private final AdminService adminService;

    @GetMapping("/users")
    public ResponseEntity<List<UserDto>> getAllUsers() {
        return ResponseEntity.ok(adminService.getAllUsers());
    }

    @GetMapping("/users/{id}")
    public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
        return ResponseEntity.ok(adminService.getUserById(id));
    }

    @DeleteMapping("/users/{id}")
    public ResponseEntity<String> deleteUser(@PathVariable Long id) {
        adminService.deleteUser(id);
        return ResponseEntity.ok("사용자가 삭제되었습니다.");
    }
}

AdminService

@Service
@RequiredArgsConstructor
public class AdminService {

    private final UserRepository userRepository;

    public List<UserDto> getAllUsers() {
        User user = userRepository.findAll();
        return convertToDto(user);
    }

    public UserDto getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
        return convertToDto(user);
    }

    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

UserController

사용자는 본인의 정보를 조회하고 일정을 생성 및 삭제할 수 있습니다.
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/profile")
    public ResponseEntity<UserDto> getUserProfile(@RequestHeader("Authorization") String token) {
        return ResponseEntity.ok(userService.getUserProfile(token));
    }

    @PostMapping("/schedules")
    public ResponseEntity<ScheduleDto> createSchedule(@RequestBody Schedule schedule) {
        return ResponseEntity.ok(userService.createSchedule(schedule));
    }

    @DeleteMapping("/schedules/{id}")
    public ResponseEntity<String> deleteSchedule(@PathVariable Long id, @RequestHeader("Authorization") String token) {
        userService.deleteSchedule(id, token);
        return ResponseEntity.ok("일정이 삭제되었습니다.");
    }
}

UserService

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final ScheduleRepository scheduleRepository;
    private final JwtUtil jwtUtil;

    public UserDto getUserProfile(String token) {
        Long userId = jwtUtil.extractUserId(token);
        User user =userRepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
		return UserDto.convertDto(user);                
    }

    public ScheduleDto createSchedule(Schedule schedule) {
        Schedule schedule = scheduleRepository.save(schedule);
        return ScheduleDto.convertDto(schedule);
    }

    public void deleteSchedule(Long id, String token) {
        Long userId = jwtUtil.extractUserId(token);
        Schedule schedule = scheduleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("일정을 찾을 수 없습니다."));

        if (!schedule.getUser().getId().equals(userId)) {
            throw new IllegalArgumentException("본인이 작성한 일정만 삭제할 수 있습니다.");
        }

        scheduleRepository.delete(schedule);
    }
}

InItData(초기 관리자 설정)

@Component
@RequiredArgsConstructor
public class InitData {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @PostConstruct
    public void init() {
        if (userRepository.findByUsername("admin").isEmpty()) {
            User admin = new User(
                    null,
                    "박병천",
                    "admin@example.com",
                    passwordEncoder.encode("adminPassword"),
                    UserRoleEnum.ADMIN
            );
            userRepository.save(admin);
        }
    }
}

3. 순서도

JWT 인증 및 권한 검증 순서도

1. 클라이언트 요청 → JwtFilter 도착
2. JWT 토큰 유효성 검증
   - 토큰 없음: 401 Unauthorized 반환
   - 토큰 유효하지 않음: 403 Forbidden 반환
3. 역할(Role) 확인
   - ADMIN 전용 URI → ADMIN 검증
   - USER 전용 URI → USER 또는 ADMIN 검증
4. 요청 통과 또는 차단
5. 컨트롤러 실행