-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat : Swagger JWT 활성화 Swagger에서 JWT token 로그인이 가능하도록 활성화한다. * chore : jwt 관련 환경 설정 application.yml에 jwt 관련 변수 추가와 build.gradle에 Security 관련 dependency 추가 * chore : SecurityConfig 추가 Spring Security 관련 설정을 할 Security Config class를 추가한다. * feat : Entity 관련 class 생성 User domain 관련 Entity 추가 User, BaseEntity, UserRepository 생성 * feat : JWT 토큰 관련 로직 작성 JwtRequestFilter, JwtService, JwtToken 생성 * feat : Controller 작성 회원가입, 로그인, 로그아웃, refresh token 재생성을 할 Presentation Layer 함수 작성 * feat : dto 작성 이후 사용할지는 모르겠으나 일단 필요해보이는 dto 모두 작성 * feat : service 로직 작성 비즈니스 메인 로직을 할 Service 로직 작성, 추가적으로 converter와 ErrorStatus 추가 * refactor : package 분류 class package 분류 * fix : import 문제 해결 git merge를 하며 잘 못 import 된 CommonResponse import 문제 해결 * fix : SQL 예약어 충돌 문제 해결 user와 group은 sql 예약어여서 해당 문제 해결 * fix : swagger 및 security 해결 SwaggerConfig, SecurityConfig 수정을 통해 Security에서 회원가입, 로그인을 제외한 나머지 경로로는 토큰 인증을 하도록 하고 swagger에서 걸리도록 * fix : 부분적인 로직 수정 부분적으로 잘못되거나 수정이 필요한 로직 수정 * refactor : 안쓰는 애들 해결 안쓰는 코드, import 등 지저분한 코드 해결 * refactor : SwaggerConfig 주석 삭제 안쓰는 주석 삭제 * refactor : refresh 로직 수정 refresh 실행 시 JwtRequestFilter의 reIssueRefreshToken 사용하도록 * fix : error code numbering ErrorStatus의 ErrorCode를 넘버링하여 Id처럼 활용할 수 있게 함 * fix(userdetailService) : Array -> List Collection을 활용하기 위해 Array에서 List로 변경 * fix : CommonResponse 변경 CommonResponse 반환하도록 Controller 반환 타입 변경 * fix : reIssueRefreshToken 위치 변경 refresh 토큰 재발급 함수의 위치를 RequestFilter에서 Service로 변경 * fix : 안쓰는 Dto 삭제 * fix(service) : service 로직 수정 login 시 accessToken과 refreshToken 두 개다 반환하도록 변경, Transactional 적용 만약을 위해 refreshAllToken 함수 작성해놓음 * fix(security) : 허용 엔드포인트 수정 이전 변경사항에 알맞게 security 허용 엔드포인트 수정 * fix(environment) : 환경 변수 처리 환경 변수를 .env file로 처리하여 이에 맞게 수정 * fix : 불필요한 주석 제거 * fix : user, sender 연관관계 설정 * fix : 요청 사항 적용 * fix(test) : test yml에 env config 추가 딱히 쓸모는 없지만 우선 추가해놓음
- Loading branch information
Showing
20 changed files
with
742 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
HELP.md | ||
.env | ||
.gradle | ||
build/ | ||
!gradle/wrapper/gradle-wrapper.jar | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.pictalk.global.common; | ||
|
||
import com.fasterxml.jackson.annotation.JsonFormat; | ||
import jakarta.persistence.Column; | ||
import jakarta.persistence.EntityListeners; | ||
import jakarta.persistence.MappedSuperclass; | ||
import java.time.LocalDateTime; | ||
import java.time.temporal.ChronoUnit; | ||
import lombok.Getter; | ||
import org.springframework.data.annotation.CreatedDate; | ||
import org.springframework.data.annotation.LastModifiedDate; | ||
import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
||
@Getter | ||
@MappedSuperclass | ||
@EntityListeners(AuditingEntityListener.class) | ||
public class BaseEntity { | ||
|
||
@CreatedDate | ||
@JsonFormat(timezone = "Asia/Seoul") | ||
private LocalDateTime createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES); | ||
|
||
@LastModifiedDate | ||
@JsonFormat(timezone = "Asia/Seoul") | ||
private LocalDateTime updatedAt = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES); | ||
|
||
@Column(name = "deleted_at") | ||
@JsonFormat(timezone = "Asia/Seoul") | ||
private LocalDateTime deletedAt; | ||
|
||
public void softDelete() { | ||
this.deletedAt = LocalDateTime.now(); | ||
} | ||
} |
97 changes: 97 additions & 0 deletions
97
src/main/java/com/pictalk/global/config/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package com.pictalk.global.config; | ||
|
||
|
||
import com.pictalk.global.jwt.JwtRequestFilter; | ||
import com.pictalk.global.jwt.JwtService; | ||
import com.pictalk.user.repository.UserRepository; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
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.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.security.web.DefaultRedirectStrategy; | ||
import org.springframework.security.web.RedirectStrategy; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; | ||
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; | ||
|
||
import org.springframework.web.cors.CorsConfiguration; | ||
import org.springframework.web.cors.CorsConfigurationSource; | ||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
@RequiredArgsConstructor | ||
public class SecurityConfig { | ||
|
||
private final JwtRequestFilter jwtFilter; | ||
|
||
@Bean | ||
public PasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
|
||
@Bean | ||
public CorsConfigurationSource corsConfigurationSource() { | ||
CorsConfiguration config = new CorsConfiguration(); | ||
|
||
config.setAllowCredentials(true); | ||
config.setAllowedOrigins(List.of("http://localhost:8080", "http://localhost:5173")); | ||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); | ||
config.setAllowedHeaders(List.of("*")); | ||
config.setExposedHeaders(List.of("*")); | ||
|
||
|
||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | ||
source.registerCorsConfiguration("/**", config); | ||
return source; | ||
} | ||
|
||
@Bean | ||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
http.csrf(AbstractHttpConfigurer::disable) | ||
.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) | ||
.httpBasic(HttpBasicConfigurer::disable) | ||
.formLogin(AbstractHttpConfigurer::disable) | ||
.logout(AbstractHttpConfigurer::disable) | ||
.authorizeHttpRequests(authorize -> authorize | ||
.requestMatchers( | ||
"/users/signup", | ||
"/users/signin", | ||
"/users/refresh", | ||
"/users/logout", | ||
"/swagger-ui/**", | ||
"/v3/api-docs/**", | ||
"/swagger-ui.html", | ||
"/swagger/**" | ||
).permitAll() // /auth/** 엔드포인트는 인증 없이 접근 가능 | ||
.anyRequest().authenticated() // 그 외 모든 요청은 인증 필요 | ||
) | ||
// .authorizeHttpRequests(requests -> | ||
// requests.anyRequest().permitAll() // 모든 요청을 모든 사용자에게 허용 | ||
// ) | ||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // JWT를 사용하므로 세션 사용 안함 | ||
|
||
// JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가 | ||
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); | ||
|
||
return http.build(); | ||
} | ||
|
||
@Bean | ||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { | ||
return authenticationConfiguration.getAuthenticationManager(); | ||
} | ||
|
||
@Bean | ||
public RedirectStrategy redirectStrategy() { | ||
return new DefaultRedirectStrategy(); // 기본 리다이렉트 전략 사용 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
src/main/java/com/pictalk/global/jwt/JwtRequestFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package com.pictalk.global.jwt; | ||
|
||
import com.pictalk.global.exception.GeneralException; | ||
import com.pictalk.global.payload.status.ErrorStatus; | ||
import com.pictalk.user.domain.User; | ||
import com.pictalk.user.repository.UserRepository; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import java.io.IOException; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; | ||
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
@RequiredArgsConstructor | ||
@Slf4j | ||
@Component | ||
public class JwtRequestFilter extends OncePerRequestFilter { | ||
|
||
private final JwtService jwtService; | ||
private final UserRepository userRepository; | ||
|
||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
checkAccessTokenAndAuthentication(request, response, filterChain); | ||
} | ||
|
||
public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, | ||
FilterChain filterChain) throws ServletException, IOException { | ||
log.info("checkAccessTokenAndAuthentication() 호출"); | ||
|
||
// Access Token 추출 및 유효성 검사 | ||
jwtService.extractAccessToken(request) | ||
.filter(jwtService::isTokenValid) | ||
.flatMap(jwtService::extractEmail) // 이메일 추출 | ||
.flatMap(userRepository::findByEmail) // 사용자 정보 조회 | ||
.ifPresent(user -> { | ||
log.info("사용자 정보가 발견되었습니다: {}", user); | ||
saveAuthentication((User) user); | ||
log.info("사용자 인증 정보가 저장되었습니다: {}", user); | ||
}); | ||
|
||
// 다음 필터 체인 실행 | ||
filterChain.doFilter(request, response); | ||
} | ||
|
||
public void saveAuthentication(User user) { | ||
UserDetails userDetails = org.springframework.security.core.userdetails.User.builder() | ||
.username(user.getEmail()) | ||
.password(user.getPassword()) | ||
.build(); | ||
|
||
Authentication authentication = new UsernamePasswordAuthenticationToken( | ||
userDetails, null, authoritiesMapper.mapAuthorities(userDetails.getAuthorities())); | ||
|
||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} | ||
|
||
public void validatePassword(String password) { | ||
if (password == null || password.length() < 8) { | ||
throw new GeneralException(ErrorStatus.USER_PASSWORD_NOT_VALID); | ||
} | ||
|
||
if (!password.matches(".*[a-z].*")) { | ||
throw new GeneralException(ErrorStatus.USER_PASSWORD_NOT_VALID); | ||
} | ||
|
||
if (!password.matches(".*\\d.*")) { | ||
throw new GeneralException(ErrorStatus.USER_PASSWORD_NOT_VALID); | ||
} | ||
|
||
if (!password.matches(".*[!@#$%^&*()].*")) { | ||
throw new GeneralException(ErrorStatus.USER_PASSWORD_NOT_VALID); | ||
} | ||
} | ||
} |
Oops, something went wrong.