Skip to content

Commit

Permalink
BE: [Fix] JWT 응답 구조 수정 및 관리자 시험 조회 로직 수정 #1 #6 #25
Browse files Browse the repository at this point in the history
BE: [Fix] JWT 응답 구조 수정 및 관리자 시험 조회 로직 수정 #1 #6 #25
  • Loading branch information
JongbeomLee623 authored Dec 5, 2024
2 parents f1bdd96 + 8cd6906 commit 69d4170
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@ public enum BaseResponseCode {
CONTENT_NULL("GL004", HttpStatus.BAD_REQUEST, "내용을 입력해 주세요."),

NOT_FOUND_TOKEN("GL005", HttpStatus.NOT_FOUND, "토큰이 존재하지 않습니다"),
EXPIRED_TOKEN("GL006", HttpStatus.GATEWAY_TIMEOUT, "토큰의 기한이 지났습니다"),
WRONG_TYPE_TOKEN("GL007", HttpStatus.BAD_REQUEST, "잘못된 타입의 토큰입니다."),
UNSUPPORTED_TOKEN("GL008", HttpStatus.BAD_REQUEST, "지원하지 않는 토큰입니다."),
TOKEN_ERROR("GL009", HttpStatus.BAD_REQUEST, "토큰에 문제가 발생했습니다."),
MALFORMED_TOKEN("GL010", HttpStatus.BAD_REQUEST, "토큰의 구조가 잘못되었습니다."),
EXPIRED_TOKEN("GL006", HttpStatus.UNAUTHORIZED, "토큰의 기한이 지났습니다"),
WRONG_TYPE_TOKEN("GL007", HttpStatus.UNAUTHORIZED, "잘못된 타입의 토큰입니다."),
UNSUPPORTED_TOKEN("GL008", HttpStatus.UNAUTHORIZED, "지원하지 않는 토큰입니다."),
TOKEN_ERROR("GL009", HttpStatus.UNAUTHORIZED, "토큰에 문제가 발생했습니다."),
MALFORMED_TOKEN("GL010", HttpStatus.UNAUTHORIZED, "토큰의 구조가 잘못되었습니다."),

UNAUTHORIZED("GL011", HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."),
INVALID_STATUS("GL012", HttpStatus.BAD_REQUEST, "유효하지 않은 상태 값입니다."),
INVALID_INPUT("GL013", HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."),

NOT_FOUND_DATA("GL014", HttpStatus.NOT_FOUND, "요청한 데이터를 찾을 수 없습니다."),

EXPIRED_REFRESH_TOKEN("GL015", HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다."),
INVALID_REFRESH_TOKEN("GL016", HttpStatus.UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다."),

// User Errors
ALREADY_EXIST_USER("U0001", HttpStatus.CONFLICT, "이미 존재하는 사용자입니다"),
WRONG_PASSWORD("U0002", HttpStatus.BAD_REQUEST, "비밀번호가 틀렸습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/api/sessions/student",
"/api/cheatings",
"/api/exams/*/cheating-types",
"/api/video"
"/api/video",
"/api/auth/refresh"

).permitAll() // 인증 불필요 경로
.anyRequest().authenticated() // 나머지 요청은 인증 필요
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.fortune.eyesee.controller;

import com.fortune.eyesee.common.exception.BaseException;
import com.fortune.eyesee.common.response.BaseResponse;
import com.fortune.eyesee.common.response.BaseResponseCode;

import com.fortune.eyesee.dto.AccessTokenResponseDTO;
import com.fortune.eyesee.dto.RefreshTokenRequestDTO;
import com.fortune.eyesee.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthService authService;

@PostMapping("/refresh")
public ResponseEntity<BaseResponse<AccessTokenResponseDTO>> refreshAccessToken(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO) {
try {
AccessTokenResponseDTO accessTokenResponseDTO = authService.refreshToken(refreshTokenRequestDTO);

return ResponseEntity.ok(new BaseResponse<>(accessTokenResponseDTO, "토큰 갱신 성공"));
} catch (Exception e) {
throw new BaseException(BaseResponseCode.TOKEN_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,67 +40,33 @@ public class ExamController {
@Autowired
private SessionRepository sessionRepository;

// // "before" 상태의 Exam 리스트 조회
// @GetMapping("/before")
// public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getBeforeExams(HttpSession session) {
// return getExamsByStatus("BEFORE", session);
// }
//
// // "in-progress" 상태의 Exam 리스트 조회
// @GetMapping("/in-progress")
// public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getInProgressExams(HttpSession session) {
// return getExamsByStatus("IN_PROGRESS", session);
// }
//
// // "done" 상태의 Exam 리스트 조회
// @GetMapping("/done")
// public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getDoneExams(HttpSession session) {
// return getExamsByStatus("DONE", session);
// }

// 공통 메서드: 상태별 Exam 조회
// private ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getExamsByStatus(String status, HttpSession session) {
// Integer adminId = (Integer) session.getAttribute("adminId");
// if (adminId == null) {
// throw new BaseException(BaseResponseCode.UNAUTHORIZED);
// }
//
// ExamStatus examStatus = ExamStatus.fromString(status);
// if (examStatus == null) {
// throw new BaseException(BaseResponseCode.INVALID_STATUS);
// }
//
// List<ExamResponseDTO> examList = examService.getExamsByStatus(adminId, examStatus);
// return ResponseEntity.ok(new BaseResponse<>(examList));
// }

// "before" 상태의 Exam 리스트 조회
@GetMapping("/before")
public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getBeforeExams() {
return getExamsByStatus("BEFORE");
return fetchExamsByStatus(ExamStatus.BEFORE);
}

// "in-progress" 상태의 Exam 리스트 조회
@GetMapping("/in-progress")
public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getInProgressExams() {
return getExamsByStatus("IN_PROGRESS");
return fetchExamsByStatus(ExamStatus.IN_PROGRESS);
}

// "done" 상태의 Exam 리스트 조회
@GetMapping("/done")
public ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getDoneExams() {
return getExamsByStatus("DONE");
return fetchExamsByStatus(ExamStatus.DONE);
}

// 공통 메서드: 상태별 Exam 조회 (세션 검증 없이)
private ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getExamsByStatus(String status) {
ExamStatus examStatus = ExamStatus.fromString(status);
if (examStatus == null) {
throw new BaseException(BaseResponseCode.INVALID_STATUS);
// 공통 메서드: 상태별 Exam 조회 (세션 검증 포함)
private ResponseEntity<BaseResponse<List<ExamResponseDTO>>> fetchExamsByStatus(ExamStatus status) {
Integer adminId = (Integer) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (adminId == null) {
throw new BaseException(BaseResponseCode.UNAUTHORIZED);
}

// adminId를 사용하지 않는 조회 방식으로 수정
List<ExamResponseDTO> examList = examService.getExamsByStatus(null, examStatus);
List<ExamResponseDTO> examList = examService.getExamsByStatus(adminId, status);
return ResponseEntity.ok(new BaseResponse<>(examList));
}

Expand All @@ -122,6 +88,15 @@ public ResponseEntity<BaseResponse<Map<String, String>>> registerExam(@RequestBo
// 특정 시험 ID에 해당하는 세션 내 모든 학생들의 리스트를 조회
@GetMapping("/{examId}/users")
public ResponseEntity<BaseResponse<UserListResponseDTO>> getUserListByExamId(@PathVariable Integer examId) {

// SecurityContextHolder에서 adminId를 가져옴
Integer adminId = (Integer) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// Admin 권한 확인
if (!examService.isAdminAuthorizedForExam(adminId, examId)) {
throw new BaseException(BaseResponseCode.EXAM_ACCESS_DENIED);
}

UserListResponseDTO response = examService.getUserListByExamId(examId);
return ResponseEntity.ok(new BaseResponse<>(response, "학생 리스트 조회 성공"));
}
Expand All @@ -131,6 +106,15 @@ public ResponseEntity<BaseResponse<UserListResponseDTO>> getUserListByExamId(@Pa
public ResponseEntity<BaseResponse<UserDetailResponseDTO>> getUserDetailByExamIdAndUserId(
@PathVariable Integer examId,
@PathVariable Integer userId) {

// SecurityContextHolder에서 adminId를 가져옴
Integer adminId = (Integer) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// Admin 권한 확인
if (!examService.isAdminAuthorizedForExam(adminId, examId)) {
throw new BaseException(BaseResponseCode.EXAM_ACCESS_DENIED);
}

UserDetailResponseDTO response = examService.getUserDetailByExamIdAndUserId(examId, userId);

return ResponseEntity.ok(new BaseResponse<>(response, "학생 상세 정보 조회 성공"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.fortune.eyesee.dto;

public class AccessTokenResponseDTO {
private String access_token;

public AccessTokenResponseDTO(String accessToken) {
this.access_token = accessToken;
}

public String getAccess_token() {
return access_token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.fortune.eyesee.dto;

import lombok.Data;

@Data
public class RefreshTokenRequestDTO {
private String refresh_token;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
@Repository
public interface ExamRepository extends JpaRepository<Exam, Integer> {

// 특정 adminId에 해당하는 Exam 리스트 조회
List<Exam> findByAdmin_AdminId(Integer adminId);
// ExamId와 AdminId로 Exam 데이터 조회
boolean existsByExamIdAndAdmin_AdminId(Integer examId, Integer adminId);

// 특정 상태에 해당하는 Exam 리스트 조회
List<Exam> findByAdmin_AdminIdAndExamStatus(Integer adminId, ExamStatus examStatus);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.fortune.eyesee.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fortune.eyesee.common.exception.BaseException;
import com.fortune.eyesee.common.response.BaseResponse;
import com.fortune.eyesee.utils.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -24,15 +27,26 @@ public JwtAuthenticationFilter(JwtUtil jwtUtil) {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = getJwtFromRequest(request);

if (token != null && jwtUtil.validateToken(token)) {
Integer id = jwtUtil.getAdminIdFromToken(token);

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
id, null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
try {
if (token != null && jwtUtil.validateToken(token)) {
Integer id = jwtUtil.getAdminIdFromToken(token);

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
id, null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (BaseException e) {
// 토큰 검증 실패 시 401 응답 설정
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(
new ObjectMapper().writeValueAsString(
new BaseResponse<>(e.getBaseResponseCode()) // BaseResponse 사용
)
);
return; // 필터 체인을 멈춤
}
chain.doFilter(request, response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.fortune.eyesee.service;

import com.fortune.eyesee.common.exception.BaseException;
import com.fortune.eyesee.common.response.BaseResponseCode;
import com.fortune.eyesee.dto.AccessTokenResponseDTO;
import com.fortune.eyesee.dto.RefreshTokenRequestDTO;
import com.fortune.eyesee.utils.JwtUtil;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

private final JwtUtil jwtUtil;

public AuthService(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

public AccessTokenResponseDTO refreshToken(RefreshTokenRequestDTO request) {
// 리프레시 토큰 검증 (validateToken에서 예외 처리)
jwtUtil.validateToken(request.getRefresh_token());

// 리프레시 토큰에서 사용자 ID 추출
Integer adminId = jwtUtil.getAdminIdFromToken(request.getRefresh_token());

// 새로운 액세스 토큰 발급
String newAccessToken = jwtUtil.generateToken(adminId);

return new AccessTokenResponseDTO(newAccessToken);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,45 +139,7 @@ public boolean existsById(Integer examId) {
}

// Admin ID와 ExamStatus로 시험 목록 조회
// public List<ExamResponseDTO> getExamsByStatus(Integer adminId, ExamStatus examStatus) {
// return examRepository.findByAdmin_AdminIdAndExamStatus(adminId, examStatus).stream()
// .map(exam -> new ExamResponseDTO(
// exam.getExamId(),
// exam.getExamName(),
// exam.getExamSemester(),
// exam.getExamStudentNumber(),
// exam.getExamLocation(),
// exam.getExamDate(),
// exam.getExamStartTime(),
// exam.getExamDuration(),
// exam.getExamStatus(),
// exam.getExamNotice(),
// exam.getSession() != null ? exam.getSession().getSessionId() : null,
// exam.getExamRandomCode()
// ))
// .collect(Collectors.toList());
// }

// Admin ID 없이 ExamStatus로만 시험 목록 조회
public List<ExamResponseDTO> getExamsByStatus(Integer adminId, ExamStatus examStatus) {
if (adminId == null) {
return examRepository.findByExamStatus(examStatus).stream()
.map(exam -> new ExamResponseDTO(
exam.getExamId(),
exam.getExamName(),
exam.getExamSemester(),
exam.getExamStudentNumber(),
exam.getExamLocation(),
exam.getExamDate(),
exam.getExamStartTime(),
exam.getExamDuration(),
exam.getExamStatus(),
exam.getExamNotice(),
exam.getSession() != null ? exam.getSession().getSessionId() : null,
exam.getExamRandomCode()
))
.collect(Collectors.toList());
} else {
return examRepository.findByAdmin_AdminIdAndExamStatus(adminId, examStatus).stream()
.map(exam -> new ExamResponseDTO(
exam.getExamId(),
Expand All @@ -194,7 +156,7 @@ public List<ExamResponseDTO> getExamsByStatus(Integer adminId, ExamStatus examSt
exam.getExamRandomCode()
))
.collect(Collectors.toList());
}

}


Expand Down Expand Up @@ -331,4 +293,8 @@ public Map<String, Boolean> getCheatingTypesByExamId(Integer examId) {

return cheatingTypesMap;
}

public boolean isAdminAuthorizedForExam(Integer adminId, Integer examId) {
return examRepository.existsByExamIdAndAdmin_AdminId(examId, adminId);
}
}
Loading

0 comments on commit 69d4170

Please sign in to comment.