Skip to content

Commit

Permalink
Merge pull request #27 from CSID-DGU/backend/feature/jwt
Browse files Browse the repository at this point in the history
[feature] jwt 적용 #1, #6, #25
  • Loading branch information
HOJEONGKIMM authored Nov 10, 2024
2 parents 53d65d4 + 13513f3 commit 0810321
Show file tree
Hide file tree
Showing 16 changed files with 381 additions and 68 deletions.
6 changes: 6 additions & 0 deletions src/backend/Eyesee/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-security'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' // JSON 파싱을 위해 jackson을 사용

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public enum BaseResponseCode {
INVALID_EXAM_STATUS("E0004", HttpStatus.BAD_REQUEST, "유효하지 않은 시험 상태입니다."),
EXAM_ACCESS_DENIED("E0005", HttpStatus.FORBIDDEN, "시험에 접근할 권한이 없습니다."),

// Session Errors
NOT_FOUND_SESSION("E0006", HttpStatus.NOT_FOUND, "세션을 찾을 수 없습니다."),

// 기타 추가 오류 코드 ...

;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.fortune.eyesee.config;

import com.fortune.eyesee.security.JwtAuthenticationFilter;
import com.fortune.eyesee.utils.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
Expand All @@ -13,29 +20,42 @@
import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final JwtUtil jwtUtil;

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

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정 추가
.csrf(csrf -> csrf.disable()) // CSRF 비활성화 (필요시 활성화 가능)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/signup", "/api/admin/login").permitAll() // 회원가입, 로그인은 인증 필요 없음
.anyRequest().permitAll() // 나머지 요청도 인증 필요 없음
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정 추가
.authorizeRequests(auth -> auth
.requestMatchers(
"/api/admins/signup",
"/api/admins/login",
"/api/sessions/join",
"/api/sessions/student"
).permitAll() // 인증 불필요 경로
.anyRequest().authenticated() // 나머지 요청은 인증 필요
)
.formLogin(form -> form.disable()); // 기본 로그인 폼 비활성화
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안 함 (JWT 사용)
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); // JWT 인증 필터 추가

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://example.com", "http://localhost:3000")); // 허용할 도메인 설정
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 허용할 HTTP 메서드
configuration.setAllowedHeaders(List.of("*")); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보 포함 여부
configuration.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 도메인 설정
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 허용할 HTTP 메서드
configuration.setAllowedHeaders(List.of("*")); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보 포함 여부

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
Expand All @@ -46,4 +66,4 @@ public CorsConfigurationSource corsConfigurationSource() {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,44 @@

import com.fortune.eyesee.common.response.BaseResponse;
import com.fortune.eyesee.dto.AdminLoginRequestDTO;
import com.fortune.eyesee.dto.AdminLoginResponseDTO;
import com.fortune.eyesee.dto.AdminSignupRequestDTO;
import com.fortune.eyesee.dto.TokenResponseDTO;
import com.fortune.eyesee.service.AdminService;
import com.fortune.eyesee.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.servlet.http.HttpSession;

@Slf4j
@RestController
@RequestMapping("/api/admins")
public class AdminController {
@Autowired
private AdminService adminService;
@Autowired
private JwtUtil jwtUtil;

// 회원가입 API
@PostMapping("/signup")
public ResponseEntity<BaseResponse<String>> registerAdmin(@RequestBody AdminSignupRequestDTO adminSignupRequestDTO) {
adminService.registerAdmin(adminSignupRequestDTO);
return ResponseEntity.ok(BaseResponse.success("회원가입 성공"));
public ResponseEntity<BaseResponse<TokenResponseDTO>> registerAndLogin(@RequestBody AdminSignupRequestDTO adminSignupRequestDTO) {
TokenResponseDTO tokens = adminService.registerAndLogin(adminSignupRequestDTO);
return ResponseEntity.ok(new BaseResponse<>(tokens, "회원가입 및 로그인 성공"));
}

// 로그인 성공 시, AdminResponseDTO 정보와 함께 메시지를 포함하여 응답
// 로그인 API
@PostMapping("/login")
public ResponseEntity<BaseResponse<AdminLoginResponseDTO>> loginAdmin(@RequestBody AdminLoginRequestDTO adminLoginRequestDTO, HttpSession session) {
AdminLoginResponseDTO adminResponse = adminService.loginAdmin(adminLoginRequestDTO);
session.setAttribute("adminId", adminResponse.getAdminId()); // 관리자 ID만 저장

// AdminResponseDTO와 "로그인 성공" 메시지를 포함하여 응답
return ResponseEntity.ok(new BaseResponse<>(adminResponse, "로그인 성공"));
public ResponseEntity<BaseResponse<TokenResponseDTO>> loginAdmin(@RequestBody AdminLoginRequestDTO adminLoginRequestDTO) {
TokenResponseDTO tokens = adminService.loginAdmin(adminLoginRequestDTO);
return ResponseEntity.ok(new BaseResponse<>(tokens, "로그인 성공"));
}

// 로그아웃 API
@PostMapping("/logout")
public ResponseEntity<BaseResponse<String>> logoutAdmin(HttpSession session) {
session.invalidate(); // 세션 무효화
return ResponseEntity.ok(BaseResponse.success("로그아웃 성공"));
}
// // 로그아웃 API
// @PostMapping("/logout")
// public ResponseEntity<BaseResponse<String>> logoutAdmin(HttpSession session) {
// session.invalidate(); // 세션 무효화
// return ResponseEntity.ok(BaseResponse.success("로그아웃 성공"));
// }

}
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,6 @@ private ResponseEntity<BaseResponse<List<ExamResponseDTO>>> getExamsByStatus(Str
return ResponseEntity.ok(new BaseResponse<>(examList));
}



// ExamCode로 특정 Exam 조회 (POST 요청)
@PostMapping("/{examId}/code")
public ResponseEntity<BaseResponse<ExamResponseDTO>> getExamByCode(@PathVariable Integer examId, @RequestBody ExamCodeRequestDTO examCodeRequestDTO, HttpSession session) {
// examId가 존재하는지 확인
if (!examService.existsById(examId)) {
throw new BaseException(BaseResponseCode.NOT_FOUND_EXAM);
}

// examCode로 시험 정보 조회
ExamResponseDTO examResponseDTO = examService.getExamByCode(examCodeRequestDTO.getExamCode());

// examId 비교
if (!examResponseDTO.getExamId().equals(examId)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new BaseResponse<>(HttpStatus.NOT_FOUND.value(),
BaseResponseCode.NOT_FOUND_EXAM.getCode(),
"제공된 examId와 조회된 시험의 examId가 일치하지 않습니다."));
}

return ResponseEntity.ok(new BaseResponse<>(examResponseDTO));
}

// 특정 시험 ID에 해당하는 세션 내 모든 학생들의 리스트를 조회
@GetMapping("/{examId}/sessions")
public ResponseEntity<BaseResponse<UserListResponseDTO>> getUserListByExamId(@PathVariable Integer examId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.fortune.eyesee.controller;

import com.fortune.eyesee.common.response.BaseResponse;
import com.fortune.eyesee.dto.ExamCodeRequestDTO;
import com.fortune.eyesee.dto.ExamResponseDTO;
import com.fortune.eyesee.dto.TokenResponseDTO;
import com.fortune.eyesee.dto.UserInfoRequestDTO;
import com.fortune.eyesee.service.ExamService;
import com.fortune.eyesee.service.SessionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/sessions")
public class SessionController {

private final SessionService sessionService;

@Autowired
public SessionController(SessionService sessionService) {
this.sessionService = sessionService;
}

@Autowired
private ExamService examService;

// 시험 세션 입장
@PostMapping("/join")
public ResponseEntity<BaseResponse<ExamResponseDTO>> joinExam(@RequestBody ExamCodeRequestDTO examCodeRequestDTO) {
// examCode로 시험 정보 조회
ExamResponseDTO examResponseDTO = examService.getExamByCode(examCodeRequestDTO.getExamCode());
return ResponseEntity.ok(new BaseResponse<>(examResponseDTO, "시험 세션 입장 성공"));

}

// 학생 정보 입력
@PostMapping("/student")
public ResponseEntity<BaseResponse<TokenResponseDTO>> addUserInfo(@RequestBody UserInfoRequestDTO userInfoRequestDTO) {
TokenResponseDTO tokenResponseDTO = sessionService.addUserInfo(userInfoRequestDTO);
return ResponseEntity.ok(new BaseResponse<>(tokenResponseDTO, "사용자 정보 입력 성공"));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.fortune.eyesee.dto;

public class TokenResponseDTO {
private String access_token;
private String refresh_token;

public TokenResponseDTO(String accessToken, String refreshToken) {
this.access_token = accessToken;
this.refresh_token = refreshToken;
}

public String getAccess_token() {
return access_token;
}

public String getRefresh_token() {
return refresh_token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

package com.fortune.eyesee.dto;

import lombok.Data;

@Data
public class UserInfoRequestDTO {
private String examCode; // 시험 코드
private String name; // 사용자 이름
private String department; // 학과
private Integer userNum; // 학번
private Integer seatNum; // 좌석 번호
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
import lombok.Data;

@Entity
@Table(name = "User")
@Table(name = "User", uniqueConstraints = {@UniqueConstraint(columnNames = {"sessionId", "userNum"})})
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer userId;


// @Column(name = "sessionId", nullable = false)
// private int sessionId; // 세션 ID (int 타입)
//
Expand All @@ -23,7 +22,9 @@ public class User {
@JoinColumn(name = "sessionId", referencedColumnName = "sessionId")
private Session session;

private Integer userNum;
@Column(nullable = false)
private Integer userNum; // 학번, 복합 키의 일부

private String department;
private String userName;
private Integer seatNum;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.fortune.eyesee.entity;

import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class UserId implements Serializable {
private Integer sessionId; // 세션 ID
private Integer userNum; // 학번
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface SessionRepository extends JpaRepository<Session, Integer> {
List<Session> findByExam_ExamId(Integer examId);

Optional<Session> findByExamExamRandomCode(String examCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public interface UserRepository extends JpaRepository<User, Integer> {

// Session과 UserId로 특정 사용자 검색하는 메소드
Optional<User> findBySessionAndUserId(Session session, Integer userId);

Optional<User> findBySessionAndUserNum(Session session, Integer userNum);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.fortune.eyesee.security;

import com.fortune.eyesee.utils.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;

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

@Override
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);
}
chain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Loading

0 comments on commit 0810321

Please sign in to comment.