first commit

This commit is contained in:
2026-03-06 14:20:30 +09:00
commit 4eefcfc0a8
21 changed files with 1261 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
package com.paynuri.common.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* 로깅 인터셉터 예제
* 필요시 WebConfig에서 등록하여 사용
*/
@Slf4j
@Component
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("[preHandle][{}] {}", request.getMethod(), request.getRequestURI());
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.debug("[postHandle][{}] {}", request.getMethod(), request.getRequestURI());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
long executeTime = endTime - startTime;
log.debug("[afterCompletion][{}] {} - {}ms", request.getMethod(), request.getRequestURI(), executeTime);
if (ex != null) {
log.error("[afterCompletion] Exception: ", ex);
}
}
}

View File

@@ -0,0 +1,69 @@
package com.paynuri.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Redis Session Configuration
*/
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
/**
* Development Profile Session Configuration
*/
@Configuration
@Profile("dev")
@EnableRedisHttpSession(redisNamespace = "spring:session:dev")
static class DevRedisSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("DEV_SESSION");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
}
/**
* Test Profile Session Configuration
*/
@Configuration
@Profile("test")
@EnableRedisHttpSession(redisNamespace = "spring:session:test")
static class TestRedisSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("TEST_SESSION");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
}
/**
* Production Profile Session Configuration
*/
@Configuration
@Profile("real")
@EnableRedisHttpSession(redisNamespace = "spring:session:real")
static class RealRedisSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSION");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setUseHttpOnlyCookie(true);
serializer.setUseSecureCookie(true);
return serializer;
}
}
}

View File

@@ -0,0 +1,40 @@
package com.paynuri.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 설정
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// 요청 인증 설정
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
.requestMatchers("/", "/home", "/public/**").permitAll()
.requestMatchers("/session/**", "/session").permitAll()
.anyRequest().authenticated()
)
// 로그인 설정
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
// 로그아웃 설정
.logout(logout -> logout
.permitAll()
);
return http.build();
}
}

View File

@@ -0,0 +1,36 @@
package com.paynuri.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 정적 리소스 핸들러 설정
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCachePeriod(3600);
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/")
.setCachePeriod(3600);
}
/**
* Interceptor 설정
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 필요한 경우 Interceptor 추가
// registry.addInterceptor(new LoggingInterceptor())
// .addPathPatterns("/**")
// .excludePathPatterns("/static/**", "/css/**", "/js/**", "/images/**");
}
}

View File

@@ -0,0 +1,42 @@
package com.paynuri.www;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class SessionController {
@GetMapping("/session")
public String sessionPage(HttpSession session, Model model) {
String username = (String) session.getAttribute("username");
Integer visitCount = (Integer) session.getAttribute("visitCount");
if (visitCount == null) {
visitCount = 0;
}
visitCount++;
session.setAttribute("visitCount", visitCount);
model.addAttribute("username", username);
model.addAttribute("visitCount", visitCount);
model.addAttribute("sessionId", session.getId());
return "session-example";
}
@PostMapping("/session/login")
public String login(@RequestParam String username, HttpSession session) {
session.setAttribute("username", username);
return "redirect:/session";
}
@PostMapping("/session/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/session";
}
}

View File

@@ -0,0 +1,13 @@
package com.paynuri.www;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WwwApplication {
public static void main(String[] args) {
SpringApplication.run(WwwApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
# Development Profile
server.port=8080
# Logging
logging.level.root=INFO
logging.level.com.paynuri.www=DEBUG
# Redis Session
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.session.store-type=redis
spring.session.timeout=1800s
# Database (H2 example)
# spring.datasource.url=jdbc:h2:mem:devdb
# spring.datasource.driver-class-name=org.h2.Driver
# spring.datasource.username=sa
# spring.datasource.password=
# Database with Log4jdbc (H2 example)
# spring.datasource.url=jdbc:log4jdbc:h2:mem:devdb
# spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# spring.datasource.username=sa
# spring.datasource.password=
# Database with Log4jdbc (MariaDB example)
# spring.datasource.url=jdbc:log4jdbc:mariadb://localhost:3306/devdb?useUnicode=true&characterEncoding=utf8
# spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# spring.datasource.username=devuser
# spring.datasource.password=devpass

View File

@@ -0,0 +1,23 @@
# Production Profile
server.port=80
# Logging
logging.level.root=WARN
logging.level.com.paynuri.www=INFO
# Redis Session
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.session.store-type=redis
spring.session.timeout=3600s
# Redis Connection Pool (Production)
spring.data.redis.lettuce.pool.max-active=10
spring.data.redis.lettuce.pool.max-idle=5
spring.data.redis.lettuce.pool.min-idle=2
# Database (Production DB example)
# spring.datasource.url=jdbc:mysql://localhost:3306/prod_db
# spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# spring.datasource.username=prod_user
# spring.datasource.password=prod_password

View File

@@ -0,0 +1,31 @@
# Test Profile
server.port=8081
# Logging
logging.level.root=INFO
logging.level.com.paynuri.www=DEBUG
# Redis Session
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.session.store-type=redis
spring.session.timeout=1800s
# Database (Test DB example)
# spring.datasource.url=jdbc:h2:mem:testdb
# spring.datasource.driver-class-name=org.h2.Driver
# spring.datasource.username=sa
# spring.datasource.password=
# Database with Log4jdbc (H2 example)
# spring.datasource.url=jdbc:log4jdbc:h2:mem:testdb
# spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# spring.datasource.username=sa
# spring.datasource.password=
# Database with Log4jdbc (MariaDB example)
# spring.datasource.url=jdbc:log4jdbc:mariadb://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8
# spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# spring.datasource.username=testuser
# spring.datasource.password=testpass

View File

@@ -0,0 +1,4 @@
spring.application.name=www
# Active Profile (dev, test, real)
spring.profiles.active=dev

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Property>
<Property name="LOG_DIR">logs</Property>
</Properties>
<Appenders>
<!-- Console Appender -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<!-- File Appender -->
<RollingFile name="RollingFile" fileName="${LOG_DIR}/application.log"
filePattern="${LOG_DIR}/application-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</Appenders>
<Loggers>
<!-- Spring Framework Logger -->
<Logger name="org.springframework" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<!-- Development Profile -->
<SpringProfile name="dev">
<Logger name="com.paynuri.www" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<!-- JDBC SQL Logging -->
<Logger name="jdbc.sqlonly" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="jdbc.sqltiming" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="jdbc.resultsettable" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="jdbc.audit" level="off"/>
<Logger name="jdbc.resultset" level="off"/>
<Logger name="jdbc.connection" level="off"/>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</SpringProfile>
<!-- Test Profile -->
<SpringProfile name="test">
<Logger name="com.paynuri.www" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<!-- JDBC SQL Logging -->
<Logger name="jdbc.sqlonly" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="jdbc.sqltiming" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="jdbc.resultsettable" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="jdbc.audit" level="off"/>
<Logger name="jdbc.resultset" level="off"/>
<Logger name="jdbc.connection" level="off"/>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</SpringProfile>
<!-- Production Profile -->
<SpringProfile name="real">
<Logger name="com.paynuri.www" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<Root level="warn">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</SpringProfile>
<!-- Default (no profile) -->
<SpringProfile name="!dev & !test & !real">
<Logger name="com.paynuri.www" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</SpringProfile>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,2 @@
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>CSRF Example</title>
</head>
<body>
<h1>CSRF 토큰 예제</h1>
<!-- Thymeleaf에서 자동으로 CSRF 토큰을 포함 -->
<form th:action="@{/submit}" method="post">
<input type="text" name="data" placeholder="데이터 입력">
<button type="submit">전송</button>
<!-- CSRF 토큰은 자동으로 추가됩니다 -->
</form>
<hr>
<!-- 수동으로 CSRF 토큰 추가 (필요한 경우) -->
<form action="/manual-submit" method="post">
<input type="text" name="data" placeholder="데이터 입력">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button type="submit">전송 (수동 CSRF)</button>
</form>
<hr>
<!-- JavaScript에서 CSRF 토큰 사용 -->
<div>
<button onclick="ajaxSubmit()">AJAX 전송</button>
</div>
<script th:inline="javascript">
const csrfToken = /*[[${_csrf.token}]]*/ '';
const csrfHeader = /*[[${_csrf.headerName}]]*/ '';
function ajaxSubmit() {
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[csrfHeader]: csrfToken
},
body: JSON.stringify({ data: 'example' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Redis Session Example</title>
</head>
<body>
<h1>Redis Session 예제</h1>
<div>
<p><strong>Session ID:</strong> <span th:text="${sessionId}">N/A</span></p>
<p><strong>방문 횟수:</strong> <span th:text="${visitCount}">0</span></p>
</div>
<hr>
<div th:if="${username == null}">
<h2>로그인</h2>
<form th:action="@{/session/login}" method="post">
<input type="text" name="username" placeholder="사용자명" required>
<button type="submit">로그인</button>
</form>
</div>
<div th:if="${username != null}">
<h2>환영합니다!</h2>
<p><strong>사용자:</strong> <span th:text="${username}">Guest</span></p>
<form th:action="@{/session/logout}" method="post">
<button type="submit">로그아웃</button>
</form>
</div>
<hr>
<div>
<h3>Redis Session 정보</h3>
<ul>
<li>세션은 Redis에 저장됩니다</li>
<li>애플리케이션 재시작 후에도 세션이 유지됩니다</li>
<li>여러 서버 간 세션 공유가 가능합니다</li>
<li>프로파일별로 다른 namespace를 사용합니다</li>
</ul>
</div>
</body>
</html>