[보안 설정 개선] JwtTokenIssuanceFilter 및 JwtTokenValidationFilter 추가로 JWT 인증 로직을 강화하고, SecurityPathConfig를 통해 허용 경로 설정을 외부화하여 보안 유연성 향상. JwtAccessDeniedHandler 및 JwtAuthenticationEntryPoint 삭제로 코드 정리.
This commit is contained in:
@@ -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) {
|
||||
|
@@ -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<String> permitAllPaths;
|
||||
|
||||
public List<String> getPermitAllPaths() {
|
||||
return permitAllPaths;
|
||||
}
|
||||
|
||||
public void setPermitAllPaths(List<String> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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();
|
@@ -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
|
@@ -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;
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
springdoc.default-consumes-media-type=application/json
|
||||
|
||||
# ========================================
|
||||
# 보안 설정 - 허용할 경로
|
||||
# ========================================
|
||||
security.permit-all-paths=/login,/members/register,/swagger-ui/**,/api-docs/**,/ws/**
|
Reference in New Issue
Block a user