diff --git a/README.md b/README.md index 619cae5..91c5996 100644 --- a/README.md +++ b/README.md @@ -94,20 +94,9 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요 -

-# 4. API 명세 -image -image -image -image -image -image -image -

+# 4. 기술 스택 (Tech Stack) -# 5. 기술 스택 (Tech Stack) - -### 5.1 백엔드 +### 4.1 백엔드 | 구분 | 기술 | |------|------| @@ -119,14 +108,14 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요 | 테스트 | ![JUnit5](https://img.shields.io/badge/JUnit%205-25A162?style=flat&logo=jest&logoColor=white)
![Mockito](https://img.shields.io/badge/Mockito-5A6268?style=flat&logo=mockito&logoColor=white) | | DB 마이그레이션 | ![Liquibase](https://img.shields.io/badge/Liquibase-2962FF?style=flat&logo=liquibase&logoColor=white) | -### 5.2 프론트엔드 +### 4.2 프론트엔드 | 구분 | 기술 | |------|------| | 라이브러리 | ![React](https://img.shields.io/badge/React-61DAFB?style=flat&logo=react&logoColor=black) | | HTTP 클라이언트 | ![Axios](https://img.shields.io/badge/Axios-5A29E4?style=flat&logo=axios&logoColor=white) | -### 5.3 AI +### 4.3 AI | 구분 | 기술 | |------|------| @@ -134,14 +123,14 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요 | AI 플랫폼 | ![OpenAI](https://img.shields.io/badge/OpenAI-412991?style=flat&logo=openai&logoColor=white)
![Hugging Face](https://img.shields.io/badge/Hugging%20Face-FFD21F?style=flat&logo=huggingface&logoColor=black) | | LLM 프레임워크 | ![LangChain](https://img.shields.io/badge/LangChain-000000?style=flat&logo=python&logoColor=white) | -### 5.4 데이터베이스 +### 4.4 데이터베이스 | 구분 | 기술 | |------|------| | RDBMS | ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?style=flat&logo=postgresql&logoColor=white) | | 벡터DB 확장 | ![pgvector](https://img.shields.io/badge/pgvector-000000?style=flat&logo=postgresql&logoColor=white) | -### 5.5 협업 & 형상 관리 +### 4.5 협업 & 형상 관리 | 구분 | 기술 | |------|------| @@ -152,7 +141,7 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요 -## 5.6 프로젝트 구조 +## 4.6 프로젝트 구조 - 시스템 아키텍처
![Image](https://github.com/user-attachments/assets/dfb7d2f7-f93f-46d5-a135-7e75038697d5) @@ -160,6 +149,18 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요
![Image](https://github.com/user-attachments/assets/271fbd65-50ba-4528-a96c-3ae0b16a7d34)

+

+ +# 5. API 명세 +image +image +image +image +image +image +image +

+ # 6. 기대 효과 ### 사용자 측면 diff --git a/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java b/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java index e5ea123..7a5549f 100644 --- a/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java +++ b/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java @@ -45,7 +45,10 @@ public User handleKakaoLogin(String code, HttpServletResponse response){ tokenRepository.findByUser(user) .ifPresentOrElse( - token -> token.updateRefreshToken(refreshToken, expiryTime), + token -> { + token.updateRefreshToken(refreshToken, expiryTime); + tokenRepository.save(token); + }, () -> tokenRepository.save(Token.builder() .user(user) .refreshToken(refreshToken) diff --git a/src/main/java/com/ureca/uplait/domain/review/service/ReviewService.java b/src/main/java/com/ureca/uplait/domain/review/service/ReviewService.java index e6eb11d..55feaee 100644 --- a/src/main/java/com/ureca/uplait/domain/review/service/ReviewService.java +++ b/src/main/java/com/ureca/uplait/domain/review/service/ReviewService.java @@ -66,6 +66,9 @@ public ReviewCreateResponse createReview(User user, ReviewCreateRequest request) @Transactional public ReviewUpdateResponse updateReview(User user, ReviewUpdateRequest request) { + validateBanWords(request.getTitle()); + validateBanWords(request.getContent()); + Review review = reviewRepository.findById(request.getReviewId()).get(); review.updateReview( diff --git a/src/main/java/com/ureca/uplait/global/config/SecurityConfig.java b/src/main/java/com/ureca/uplait/global/config/SecurityConfig.java index 8743855..1057c2c 100644 --- a/src/main/java/com/ureca/uplait/global/config/SecurityConfig.java +++ b/src/main/java/com/ureca/uplait/global/config/SecurityConfig.java @@ -2,6 +2,7 @@ import com.ureca.uplait.domain.user.repository.UserRepository; import com.ureca.uplait.global.security.jwt.JwtValidator; +import com.ureca.uplait.global.security.jwt.entrypoint.JwtAuthenticationEntryPoint; import com.ureca.uplait.global.security.jwt.filter.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -40,12 +41,17 @@ public SecurityFilterChain filterChain(HttpSecurity http, UserRepository userRep .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .formLogin(form -> form.disable()) + .exceptionHandling(e -> e + .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) + ) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/login", "/auth/reissue", "/auth/logout").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll() .requestMatchers("/health").permitAll() + .requestMatchers("/plan/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(HttpMethod.POST, "/user/extra-info").hasRole("TMP_USER") + .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(jwtValidator, userRepository), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/ureca/uplait/global/security/jwt/JwtProvider.java b/src/main/java/com/ureca/uplait/global/security/jwt/JwtProvider.java index 070d7d1..f3c0b28 100644 --- a/src/main/java/com/ureca/uplait/global/security/jwt/JwtProvider.java +++ b/src/main/java/com/ureca/uplait/global/security/jwt/JwtProvider.java @@ -5,6 +5,7 @@ import java.util.Date; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; import com.ureca.uplait.domain.user.entity.User; @@ -57,43 +58,56 @@ public String createRefreshToken(User user) { } public void addAccessTokenCookie(HttpServletResponse response, String token){ - String cookieString = String.format( - "accessToken=%s; Path=/; Max-Age=%d; HttpOnly; Secure=%s; SameSite=%s; Domain=%s", - token, - ACCESS_TOKEN_VALIDITY_SECONDS / 1000, - isSecure ? "Secure" : "", - sameSite, - cookieDomain - ); - response.addHeader("Set-Cookie", cookieString); + ResponseCookie cookie = ResponseCookie.from("accessToken", token) + .path("/") + .httpOnly(true) + .secure(isSecure) + .maxAge(ACCESS_TOKEN_VALIDITY_SECONDS / 1000) + .sameSite(sameSite) + .domain(cookieDomain.isBlank() ? null : cookieDomain) + .build(); + + response.addHeader("Set-Cookie", cookie.toString()); } public void addRefreshTokenCookie(HttpServletResponse response, String token){ - Cookie cookie = new Cookie("refreshToken", token); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setPath("/"); - cookie.setMaxAge((int) (REFRESH_TOKEN_VALIDITY_SECONDS / 1000)); - response.addCookie(cookie); + ResponseCookie cookie = ResponseCookie.from("refreshToken", token) + .path("/") + .httpOnly(true) + .secure(isSecure) + .maxAge(REFRESH_TOKEN_VALIDITY_SECONDS / 1000) + .sameSite(sameSite) + .domain(cookieDomain.isBlank() ? null : cookieDomain) + .build(); + + response.addHeader("Set-Cookie", cookie.toString()); } public void deleteAccessTokenCookie(HttpServletResponse response){ - String cookieString = String.format( - "accessToken=; Path=/; Max-Age=0; HttpOnly; Secure=%s; SameSite=%s; Domain=%s", - isSecure ? "Secure" : "", - sameSite, - cookieDomain - ); - response.addHeader("Set-Cookie", cookieString); + ResponseCookie cookie = ResponseCookie.from("accessToken", "") + .path("/") + .httpOnly(true) + .secure(isSecure) + .maxAge(0) + .sameSite(sameSite) + .domain(cookieDomain.isBlank() ? null : cookieDomain) + .build(); + + response.addHeader("Set-Cookie", cookie.toString()); } public void deleteRefreshTokenCookie(HttpServletResponse response){ - Cookie cookie = new Cookie("refreshToken", null); - cookie.setMaxAge(0); - cookie.setPath("/"); - cookie.setHttpOnly(true); - response.addCookie(cookie); + ResponseCookie cookie = ResponseCookie.from("refreshToken", "") + .path("/") + .httpOnly(true) + .secure(isSecure) + .maxAge(0) + .sameSite(sameSite) + .domain(cookieDomain.isBlank() ? null : cookieDomain) + .build(); + + response.addHeader("Set-Cookie", cookie.toString()); } public LocalDateTime getRefreshTokenExpiry(String token){ diff --git a/src/main/java/com/ureca/uplait/global/security/jwt/entrypoint/JwtAuthenticationEntryPoint.java b/src/main/java/com/ureca/uplait/global/security/jwt/entrypoint/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..8743de8 --- /dev/null +++ b/src/main/java/com/ureca/uplait/global/security/jwt/entrypoint/JwtAuthenticationEntryPoint.java @@ -0,0 +1,23 @@ +package com.ureca.uplait.global.security.jwt.entrypoint; + +import java.io.IOException; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import com.ureca.uplait.global.security.jwt.JwtProvider; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/src/main/java/com/ureca/uplait/global/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/ureca/uplait/global/security/jwt/filter/JwtAuthenticationFilter.java index 0fcf34d..8203b66 100644 --- a/src/main/java/com/ureca/uplait/global/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/ureca/uplait/global/security/jwt/filter/JwtAuthenticationFilter.java @@ -28,6 +28,11 @@ public JwtAuthenticationFilter(JwtValidator jwtValidator, UserRepository userRep this.userRepository = userRepository; } + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return request.getRequestURI().equals("/auth/reissue"); + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3a10a53..52f4d1d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -45,7 +45,7 @@ kakao: client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URI} jwt: - secret-key: ${JWT_SECRET} + secret: ${JWT_SECRET} access-token-validity: 1800000 refresh-token-validity: 604800000 vector: