diff --git a/src/main/java/com/bio/bio_backend/BioBackendApplication.java b/src/main/java/com/bio/bio_backend/BioBackendApplication.java index 58ec790..0679e19 100644 --- a/src/main/java/com/bio/bio_backend/BioBackendApplication.java +++ b/src/main/java/com/bio/bio_backend/BioBackendApplication.java @@ -2,10 +2,12 @@ package com.bio.bio_backend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing +@EnableConfigurationProperties public class BioBackendApplication { public static void main(String[] args) { diff --git a/src/main/java/com/bio/bio_backend/global/config/SecurityPathConfig.java b/src/main/java/com/bio/bio_backend/global/config/SecurityPathConfig.java new file mode 100644 index 0000000..7b4b57c --- /dev/null +++ b/src/main/java/com/bio/bio_backend/global/config/SecurityPathConfig.java @@ -0,0 +1,45 @@ +package com.bio.bio_backend.global.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "security") +public class SecurityPathConfig { + + private List permitAllPaths; + + public List getPermitAllPaths() { + return permitAllPaths; + } + + public void setPermitAllPaths(List permitAllPaths) { + this.permitAllPaths = permitAllPaths; + } + + /** + * 주어진 경로가 허용된 경로인지 확인 + * @param path 확인할 경로 + * @param contextPath 컨텍스트 경로 + * @return 허용 여부 + */ + public boolean isPermittedPath(String path, String contextPath) { + if (permitAllPaths == null) { + return false; + } + + return permitAllPaths.stream() + .anyMatch(permittedPath -> { + String fullPath = contextPath + permittedPath; + if (permittedPath.endsWith("/**")) { + // /** 패턴 처리 + String basePath = fullPath.substring(0, fullPath.length() - 2); + return path.startsWith(basePath); + } else { + return path.equals(fullPath); + } + }); + } +} diff --git a/src/main/java/com/bio/bio_backend/global/exception/JwtAccessDeniedHandler.java b/src/main/java/com/bio/bio_backend/global/exception/JwtAccessDeniedHandler.java deleted file mode 100644 index 26251e8..0000000 --- a/src/main/java/com/bio/bio_backend/global/exception/JwtAccessDeniedHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.bio.bio_backend.global.exception; - -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.MediaType; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerExceptionResolver; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import com.bio.bio_backend.global.dto.ApiResponseDto; -import com.bio.bio_backend.global.constants.ApiResponseCode; - -@Component -public class JwtAccessDeniedHandler implements AccessDeniedHandler { - private final HandlerExceptionResolver resolver; - - public JwtAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) { - this.resolver = resolver; - } - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { - - Exception exception = (Exception) request.getAttribute("exception"); - - if (exception != null) { - resolver.resolveException(request, response, null, exception); - } else { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - new ObjectMapper().writeValue(response.getWriter(), ApiResponseDto.fail(ApiResponseCode.COMMON_FORBIDDEN)); - } - } -} diff --git a/src/main/java/com/bio/bio_backend/global/exception/JwtAuthenticationEntryPoint.java b/src/main/java/com/bio/bio_backend/global/exception/JwtAuthenticationEntryPoint.java deleted file mode 100644 index c677347..0000000 --- a/src/main/java/com/bio/bio_backend/global/exception/JwtAuthenticationEntryPoint.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.bio.bio_backend.global.exception; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerExceptionResolver; - -import java.io.IOException; - -@Component -@Slf4j -public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { - private final HandlerExceptionResolver resolver; - - public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) { - this.resolver = resolver; - } - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - - Exception exception = (Exception) request.getAttribute("exception"); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - if (exception == null) { - return; - } - resolver.resolveException(request, response, null, exception); - } -} \ No newline at end of file diff --git a/src/main/java/com/bio/bio_backend/global/security/JwtTokenIssuanceFilter.java b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java similarity index 75% rename from src/main/java/com/bio/bio_backend/global/security/JwtTokenIssuanceFilter.java rename to src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java index 1ca97a4..5dd8c22 100644 --- a/src/main/java/com/bio/bio_backend/global/security/JwtTokenIssuanceFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenIssuanceFilter.java @@ -1,4 +1,4 @@ -package com.bio.bio_backend.global.security; +package com.bio.bio_backend.global.filter; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; @@ -9,13 +9,11 @@ import com.bio.bio_backend.global.dto.ApiResponseDto; import com.bio.bio_backend.domain.base.member.dto.LoginRequestDto; import com.bio.bio_backend.domain.base.member.dto.LoginResponseDto; import com.bio.bio_backend.domain.base.member.dto.MemberDto; -import com.bio.bio_backend.domain.base.member.service.MemberService; import com.bio.bio_backend.global.constants.ApiResponseCode; import com.bio.bio_backend.global.utils.JwtUtils; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; @@ -30,36 +28,22 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic import java.io.IOException; import java.time.LocalDateTime; -import java.util.Objects; @RequiredArgsConstructor @Slf4j public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; - private final MemberService memberService; private final JwtUtils jwtUtils; - private final Environment env; private final ObjectMapper objectMapper; // 사용자 login 인증 처리 @SneakyThrows @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException { - + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class); - -// UsernamePasswordAuthenticationToken authToken; UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(requestDto.getUserId(), requestDto.getPassword()); - /* - if (req.getLoginLogFlag() == 1) { - authToken = new UsernamePasswordAuthenticationToken("admin2", "test123!"); // 비밀번호는 실제 비밀번호로 설정해야 함 - } else { - throw new AuthenticationServiceException("login fail"); - } - */ return authenticationManager.authenticate(authToken); } @@ -69,23 +53,22 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter FilterChain chain, Authentication authResult) throws IOException, ServletException { UserDetails userDetails = (UserDetails) authResult.getPrincipal(); - MemberDto member = (MemberDto) userDetails; - String accessToken = jwtUtils.generateToken(member.getUserId(), member.getRole().getValue(), - Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")))); - - // 모듈화된 메서드 사용 - String refreshToken = jwtUtils.createAndSaveRefreshToken(member.getUserId(), member.getRole().getValue()); + // 토큰 생성 + String accessToken = jwtUtils.createAccessToken(member.getUserId(), member.getRole().getValue()); + String refreshToken = jwtUtils.createRefreshToken(member.getUserId(), member.getRole().getValue()); + + // Refresh 토큰을 DB에 저장 + jwtUtils.saveRefreshToken(member.getUserId(), refreshToken); - member.setRefreshToken(refreshToken); member.setLastLoginAt(LocalDateTime.now()); - // Refresh 토큰 쿠키 저장 - 모듈화된 메서드 사용 + // Refresh 토큰 쿠키 저장 jwtUtils.setRefreshTokenCookie(response, refreshToken); - // JWT 토큰 전달 + // Access 토큰 전달 response.setHeader("Authorization", "Bearer " + accessToken); SecurityContextHolderStrategy contextHolder = SecurityContextHolder.getContextHolderStrategy(); diff --git a/src/main/java/com/bio/bio_backend/global/security/JwtTokenValidationFilter.java b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java similarity index 93% rename from src/main/java/com/bio/bio_backend/global/security/JwtTokenValidationFilter.java rename to src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java index 5fecd4c..986fc18 100644 --- a/src/main/java/com/bio/bio_backend/global/security/JwtTokenValidationFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/JwtTokenValidationFilter.java @@ -1,4 +1,4 @@ -package com.bio.bio_backend.global.security; +package com.bio.bio_backend.global.filter; import java.io.IOException; import java.util.Objects; @@ -21,6 +21,7 @@ import com.bio.bio_backend.global.dto.ApiResponseDto; import com.bio.bio_backend.domain.base.member.service.MemberService; import com.bio.bio_backend.global.constants.ApiResponseCode; import com.bio.bio_backend.global.utils.JwtUtils; +import com.bio.bio_backend.global.config.SecurityPathConfig; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,16 +32,14 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; private final MemberService memberService; private final Environment env; + private final SecurityPathConfig securityPathConfig; @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getRequestURI(); - // permitAll 경로는 JWT 검증을 건너뜀 (WebSecurity.java의 설정과 동기화) - return path.equals("/login") || - path.startsWith("/members/register") || - path.startsWith("/swagger-ui/") || - path.startsWith("/api-docs/") || - path.startsWith("/ws/"); + String contextPath = env.getProperty("server.servlet.context-path", ""); + + return securityPathConfig.isPermittedPath(path, contextPath); } @Override diff --git a/src/main/java/com/bio/bio_backend/global/security/UriAllowFilter.java b/src/main/java/com/bio/bio_backend/global/filter/UriAllowFilter.java similarity index 86% rename from src/main/java/com/bio/bio_backend/global/security/UriAllowFilter.java rename to src/main/java/com/bio/bio_backend/global/filter/UriAllowFilter.java index 5132eea..ec365ba 100644 --- a/src/main/java/com/bio/bio_backend/global/security/UriAllowFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/UriAllowFilter.java @@ -1,11 +1,9 @@ -package com.bio.bio_backend.global.security; +package com.bio.bio_backend.global.filter; -import java.util.Arrays; import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java b/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java index 95362ac..dbcecc0 100644 --- a/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java +++ b/src/main/java/com/bio/bio_backend/global/security/WebSecurity.java @@ -1,5 +1,7 @@ package com.bio.bio_backend.global.security; +import com.bio.bio_backend.global.filter.JwtTokenIssuanceFilter; +import com.bio.bio_backend.global.filter.JwtTokenValidationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @@ -18,9 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.bio.bio_backend.domain.base.member.service.MemberService; import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler; -import com.bio.bio_backend.global.exception.JwtAccessDeniedHandler; -import com.bio.bio_backend.global.exception.JwtAuthenticationEntryPoint; import com.bio.bio_backend.global.utils.JwtUtils; +import com.bio.bio_backend.global.config.SecurityPathConfig; import lombok.RequiredArgsConstructor; @Configuration @@ -33,18 +34,17 @@ public class WebSecurity { private final JwtUtils jwtUtils; private final ObjectMapper objectMapper; private final Environment env; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; - private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final SecurityPathConfig securityPathConfig; private JwtTokenIssuanceFilter getJwtTokenIssuanceFilter(AuthenticationManager authenticationManager) throws Exception { - JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, memberService, jwtUtils, env, objectMapper); - filter.setFilterProcessesUrl("/login"); // 로그인 EndPoint + JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper); + filter.setFilterProcessesUrl("/login"); filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper)); return filter; } private JwtTokenValidationFilter getJwtTokenValidationFilter() { - return new JwtTokenValidationFilter(jwtUtils, memberService, env); + return new JwtTokenValidationFilter(jwtUtils, memberService, env, securityPathConfig); } @@ -59,12 +59,13 @@ public class WebSecurity { AuthenticationManager authenticationManager = authenticationManagerBuilder.build(); + // 설정 파일에서 허용할 경로 가져오기 + String[] permitAllPaths = securityPathConfig.getPermitAllPaths().toArray(new String[0]); + http.csrf(AbstractHttpConfigurer::disable) //csrf 비활성화 .authorizeHttpRequests(request -> //request 허용 설정 request - .requestMatchers("/members/register").permitAll() - .requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll() // Swagger 허용 - .requestMatchers("/ws/**").permitAll() // WebSocket 허용 + .requestMatchers(permitAllPaths).permitAll() // 설정 파일에서 허용할 경로 .anyRequest().authenticated() // 나머지 요청은 인증 필요 ) .authenticationManager(authenticationManager) @@ -72,14 +73,6 @@ public class WebSecurity { session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용 안함 ) .logout(AbstractHttpConfigurer::disable); - - // 예외 처리 핸들링 - http.exceptionHandling((exceptionConfig) -> - exceptionConfig - .authenticationEntryPoint(jwtAuthenticationEntryPoint) - .accessDeniedHandler(jwtAccessDeniedHandler) - ); - http // 1단계: JWT 토큰 발급 필터 (로그인 요청 처리 및 토큰 발급) .addFilterBefore(getJwtTokenIssuanceFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java b/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java index c8f7897..06b7de2 100644 --- a/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java +++ b/src/main/java/com/bio/bio_backend/global/utils/JwtUtils.java @@ -93,17 +93,30 @@ public class JwtUtils { return null; } - // Refresh Token 생성 및 DB 저장 - public String createAndSaveRefreshToken(String username, String role) { - String refreshToken = generateToken(username, role, + // Access Token 생성 + public String createAccessToken(String username, String role) { + return generateToken(username, role, + Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access")))); + } + + // Refresh Token 생성 + public String createRefreshToken(String username, String role) { + return generateToken(username, role, Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh")))); - - // MemberDto 객체 생성하여 updateRefreshToken 호출 + } + + // Refresh Token을 DB에 저장 + public void saveRefreshToken(String username, String refreshToken) { MemberDto member = new MemberDto(); member.setUserId(username); member.setRefreshToken(refreshToken); memberService.updateRefreshToken(member); - + } + + // Refresh Token 생성 및 DB 저장 (기존 호환성을 위한 메서드) + public String createAndSaveRefreshToken(String username, String role) { + String refreshToken = createRefreshToken(username, role); + saveRefreshToken(username, refreshToken); return refreshToken; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 87b9ad4..1183039 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -94,4 +94,9 @@ springdoc.swagger-ui.tagsSorter=alpha springdoc.swagger-ui.doc-expansion=none springdoc.swagger-ui.disable-swagger-default-url=true springdoc.default-produces-media-type=application/json -springdoc.default-consumes-media-type=application/json \ No newline at end of file +springdoc.default-consumes-media-type=application/json + +# ======================================== +# 보안 설정 - 허용할 경로 +# ======================================== +security.permit-all-paths=/login,/members/register,/swagger-ui/**,/api-docs/**,/ws/** \ No newline at end of file