This commit is contained in:
mindol1004
2024-09-25 17:04:19 +09:00
parent 4f3d7e659b
commit ec4f4f74c0
15 changed files with 99 additions and 66 deletions

View File

@@ -30,7 +30,9 @@ public class PostCreateBatch extends AbstractBatchTask {
@Autowired
@Override
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
public void setTransactionManager(
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager
) {
super.setTransactionManager(transactionManager);
}

View File

@@ -42,10 +42,13 @@ import lombok.extern.slf4j.Slf4j;
public class PostCreateBatchChunk {
private final JobRepository jobRepository;
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER)
private final PlatformTransactionManager transactionManager;
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
private final EntityManagerFactory entityManagerFactory;
private final PostRepository postRepository;
private final PostBackUpRepository postBackUpRepository;

View File

@@ -6,7 +6,6 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.StringUtils;
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
@@ -48,7 +47,7 @@ public class EntityScanner {
if (!reader.getAnnotationMetadata().hasAnnotation(Entity.class.getName())) {
return false;
}
if (StringUtils.hasText(dbName)) {
if (dbName != null) {
var attributes = reader.getAnnotationMetadata().getAnnotationAttributes(DatabaseSelector.class.getName());
return attributes != null && dbName.equals(attributes.get("value"));
}

View File

@@ -51,7 +51,6 @@ public class SecurityConfig {
"/",
"/h2-console/**",
"/favicon.ico",
"/sign-up",
"/api/user/sign-up"
};
@@ -95,7 +94,8 @@ public class SecurityConfig {
.addFilterAfter(
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
AuthenticationProcessingFilter.class
).addFilterAfter(
)
.addFilterAfter(
new RedirectIfAuthenticatedFilter(),
JwtAuthenticationFilter.class
)

View File

@@ -16,6 +16,7 @@ public enum SecurityExceptionRule implements ErrorRule {
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰 정보가 없습니다."),
SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
MALFORMED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."),
EXPIRED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다. 다시 로그인해주세요.");

View File

@@ -55,37 +55,33 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
) throws ServletException, IOException {
String requestURI = request.getRequestURI();
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI))) {
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI)) && !"/".equals(requestURI)) {
filterChain.doFilter(request, response);
return;
}
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
if (jwtTokenService.validateAccessToken(accessToken)) {
setAuthenticationToContext(accessToken);
filterChain.doFilter(request, response);
return;
}
try {
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
if (jwtTokenService.validateAccessToken(accessToken)) {
setAuthenticationToContext(accessToken);
return;
}
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
if (StringUtils.hasText(refreshToken)) {
if (validateToken(refreshToken, request)) {
try {
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
if (StringUtils.hasText(refreshToken)) {
if (validateToken(refreshToken, request)) {
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
jwtTokenService.generateRefreshToken(response, authentication);
setAuthenticationToContext(reissuedAccessToken);
} catch (Exception e) {
jwtTokenService.deleteCookie(response);
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
}
filterChain.doFilter(request, response);
return;
} else {
jwtTokenService.deleteCookie(response);
}
} catch (Exception e) {
jwtTokenService.deleteCookie(response);
filterChain.doFilter(request, response);
} else {
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
} finally {
filterChain.doFilter(request, response);
}

View File

@@ -23,7 +23,7 @@ public class RedirectIfAuthenticatedFilter extends OncePerRequestFilter {
String requestURI = request.getRequestURI();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && "/".equals(requestURI)) {
response.sendRedirect("/main");
response.sendRedirect("/dashboard");
return;
}
filterChain.doFilter(request, response);

View File

@@ -7,6 +7,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@@ -35,12 +37,23 @@ public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoin
AuthenticationException authException) throws IOException, ServletException {
if (!endpointChecker.isEndpointExist(request)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (isApiRequest(request)) {
handleApiRequest(request, response, authException);
} else {
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
} else {
resolver.resolveException(request, response, null, authException);
}
response.sendRedirect("/");
}
}
private boolean isApiRequest(HttpServletRequest request) {
String accept = request.getHeader(HttpHeaders.ACCEPT);
return accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE);
}
private void handleApiRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
} else {
resolver.resolveException(request, response, null, authException);
}
}

View File

@@ -16,6 +16,8 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.UserPrincipalService;
import io.jsonwebtoken.Claims;
@@ -136,12 +138,12 @@ public class JwtTokenService {
* @param request HTTP 요청 객체
* @param tokenPrefix 토큰 접두사
* @return 추출된 토큰
* @throws IllegalStateException 토큰이 없을 경우 발생
* @throws SecurityAuthException 토큰이 없을 경우 발생
*/
public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
throw new IllegalStateException("JWT_TOKEN_NOT_FOUND");
throw new SecurityAuthException(SecurityExceptionRule.JWT_TOKEN_NOT_FOUND);
}
return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix);
}

View File

@@ -1,13 +1,15 @@
const baseUrl = window.BASE_URL || '';
const timeOut = window.TIME_OUT || 5000;
// Axios apiClient 생성
const apiClient = axios.create({
baseURL: BASE_URL,
timeout: TIME_OUT,
baseURL: baseUrl,
timeout: timeOut,
headers: {
'Content-Type': 'application/json',
}
});
// 요청 인터셉터 추가
apiClient.interceptors.request.use(
(config) => {
return config;
@@ -17,7 +19,6 @@ apiClient.interceptors.request.use(
}
);
// 응답 인터셉터 추가
apiClient.interceptors.response.use(
(response) => {
return response.data;

View File

@@ -7,18 +7,6 @@ document.addEventListener('DOMContentLoaded', () => {
fetchDataAndRender();
});
const fetchDataAndRender = async () => {
const [year, month] = selectedMonth.split('-');
const batchData = await getBatchJobExecutionData(year, month);
const recentJobs = await getRecentJobs();
renderBatchExecutionTimeChart(batchData.jobAvgSummary);
renderBatchStatusChart(batchData.statusCounts);
renderHourlyJobExecutionChart(batchData.jobHourSummary);
renderDailyJobExecutionsChart(batchData.jobExecutionSummary);
renderRecentJobsTable(recentJobs);
};
const initMonthPicker = () => {
const monthPicker = document.getElementById('monthPicker');
const currentDate = dayjs();
@@ -36,6 +24,18 @@ const initMonthPicker = () => {
});
};
const fetchDataAndRender = async () => {
const [year, month] = selectedMonth.split('-');
const batchData = await getBatchJobExecutionData(year, month);
const recentJobs = await getRecentJobs();
renderBatchExecutionTimeChart(batchData.jobAvgSummary);
renderBatchStatusChart(batchData.statusCounts);
renderHourlyJobExecutionChart(batchData.jobHourSummary);
renderDailyJobExecutionsChart(batchData.jobExecutionSummary);
renderRecentJobsTable(recentJobs);
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,

View File

@@ -12,6 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
signIn(username, password).then(response => {
console.log(response);
if (response.status) {
window.location.href = response.redirectUrl;
}

View File

@@ -3,9 +3,9 @@
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head th:replace="fragments/config :: config"></head>
<body>
<div th:replace="fragments/header::header"></div>
<div th:replace="fragments/left::sidebar"></div>
<div layout:fragment="content"></div>
</body>
<body>
<div th:replace="fragments/header::header"></div>
<div th:replace="fragments/left::sidebar"></div>
<div layout:fragment="content"></div>
</body>
</html>

View File

@@ -3,7 +3,7 @@
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head th:replace="fragments/config :: config"/>
<body>
<section layout:fragment="content"></section>
</body>
<body>
<section layout:fragment="content"></section>
</body>
</html>

View File

@@ -1,12 +1,27 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/signin-layout}" lang="ko" xml:lang="ko">
<head>
<meta charset="UTF-8">
<title>Error page</title>
<link rel="stylesheet" href="/css/error.css">
<title>Error</title>
</head>
<body>
<h1>Error Page</h1>
<span th:text="${message}"></span>
</body>
<body class="bg-light">
<section layout:fragment="content">
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
<div class="card border-0 shadow-lg" style="max-width: 500px;">
<div class="card-body text-center p-5">
<div class="display-1 text-danger mb-4">
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
<h1 class="display-4 fw-bold text-danger mb-4" th:text="${#strings.substring(message, 0, 3)}">500</h1>
<h2 class="h4 text-secondary mb-4" th:text="${#strings.substring(message, 4)}">내부 서버 오류</h2>
<p class="text-muted mb-4">죄송합니다. 문제가 발생했습니다. 기술팀이 이 문제를 해결하기 위해 노력하고 있습니다.</p>
<a href="/" class="btn btn-primary btn-lg d-inline-flex align-items-center">
<i class="bi bi-house-door-fill me-2"></i>홈으로 돌아가기
</a>
</div>
</div>
</div>
</section>
</body>
</html>