permitAllUrls;
+ private static final String EXCEPTION_ATTRIBUTE = "exception";
/**
* 요청마다 실행되는 필터 메소드입니다.
@@ -51,7 +55,7 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
) throws ServletException, IOException {
String requestURI = request.getRequestURI();
- if (permitAllUrls.stream().anyMatch(requestURI::startsWith)) {
+ if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI))) {
filterChain.doFilter(request, response);
return;
}
@@ -64,16 +68,27 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
}
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
- if (jwtTokenService.validateRefreshToken(refreshToken)) {
- Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
- String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
- jwtTokenService.generateRefreshToken(response, authentication);
- setAuthenticationToContext(reissuedAccessToken);
+ if (StringUtils.hasText(refreshToken)) {
+ if (validateToken(refreshToken, request)) {
+ try {
+ 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;
+ }
+
+ jwtTokenService.deleteCookie(response);
+ filterChain.doFilter(request, response);
+ } else {
filterChain.doFilter(request, response);
- return;
}
- jwtTokenService.deleteCookie(response);
}
/**
@@ -81,9 +96,19 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
*
* @param token 토큰
*/
- private void setAuthenticationToContext(String token) {
- Authentication authentication = jwtTokenService.getAuthentication(token);
+ private void setAuthenticationToContext(final String token) {
+ Authentication authentication = jwtTokenService.getJwtAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
+ private boolean validateToken(final String token, HttpServletRequest request) {
+ try {
+ jwtTokenService.validateToken(token);
+ return true;
+ } catch (Exception e) {
+ request.setAttribute(EXCEPTION_ATTRIBUTE, e);
+ return false;
+ }
+ }
+
}
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAccessDeniedHandler.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAccessDeniedHandler.java
index c57dae4..a4afcc3 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAccessDeniedHandler.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAccessDeniedHandler.java
@@ -6,9 +6,11 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerExceptionResolver;
/**
* JWT 인증에서 접근 거부 상황을 처리하는 핸들러 클래스입니다.
@@ -22,6 +24,12 @@ import org.springframework.stereotype.Component;
@Component
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
+ private final HandlerExceptionResolver resolver;
+
+ public SecurityAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
+ this.resolver = resolver;
+ }
+
/**
* 접근 거부 상황을 처리합니다.
*
@@ -37,7 +45,7 @@ public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
- response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ resolver.resolveException(request, response, null, accessDeniedException);
}
}
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAuthenticationEntryPoint.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAuthenticationEntryPoint.java
index 5e4185e..9d0ef34 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAuthenticationEntryPoint.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/SecurityAuthenticationEntryPoint.java
@@ -6,38 +6,36 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+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;
/**
* JWT 인증에서 인증되지 않은 접근을 처리하는 진입점 클래스입니다.
*
- * 이 클래스는 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근을 시도할 때 호출됩니다.
- * 이런 경우 SC_UNAUTHORIZED (401) 응답을 반환합니다.
- *
* @author mindol
* @version 1.0
*/
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
- /**
- * 인증되지 않은 접근을 처리합니다.
- *
- * 인증되지 않은 사용자가 보호된 리소스에 접근을 시도할 때 호출됩니다.
- * 이 메소드는 SC_UNAUTHORIZED (401) 상태 코드를 응답으로 전송합니다.
- *
- * @param request 현재 HTTP 요청
- * @param response 현재 HTTP 응답
- * @param authException 발생한 인증 예외
- * @throws IOException 입출력 예외 발생 시
- * @throws ServletException 서블릿 예외 발생 시
- */
+ private final HandlerExceptionResolver resolver;
+ private static final String EXCEPTION_ATTRIBUTE = "exception";
+
+ public SecurityAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
+ this.resolver = resolver;
+ }
+
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
+ resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
+ return;
+ }
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninFailureHandler.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninFailureHandler.java
index f724b35..740df27 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninFailureHandler.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninFailureHandler.java
@@ -6,20 +6,28 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerExceptionResolver;
@Component
public class SigninFailureHandler implements AuthenticationFailureHandler {
+ private final HandlerExceptionResolver resolver;
+
+ public SigninFailureHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
+ this.resolver = resolver;
+ }
+
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception
) throws IOException, ServletException {
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //401 인증 실패
+ resolver.resolveException(request, response, null, exception);
}
}
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java
index c88cb67..7a2cc53 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java
@@ -41,7 +41,7 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
} else { // 로그인 버튼 눌러서 로그인한 경우 기존에 있던 페이지로 리다이렉트
String prevPage = String.valueOf(request.getSession().getAttribute("prevPage"));
- redirectStrategy.sendRedirect(request, response, prevPage);
+ redirectStrategy.sendRedirect(request, response, "null".equals(prevPage) ? "/main" : prevPage);
}
}
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java
index 0cb4ae4..7b235b6 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java
@@ -1,20 +1,19 @@
package com.spring.infra.security.jwt;
+import java.sql.Timestamp;
import java.time.Instant;
+import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
-import javax.crypto.spec.SecretKeySpec;
-
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
/**
@@ -30,6 +29,7 @@ import lombok.RequiredArgsConstructor;
public class JwtTokenGenerator {
private final JwtProperties jwtProperties;
+ private final JwtTokenUtil jwtTokenUtil;
/**
* 액세스 토큰을 생성합니다.
@@ -42,8 +42,9 @@ public class JwtTokenGenerator {
.setHeader(createHeader())
.setClaims(createClaims(authentication))
.setSubject(authentication.getName())
- .setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.HOURS)))
- .signWith(new SecretKeySpec(jwtProperties.getAccessToken().getSecret().getBytes(), SignatureAlgorithm.HS512.getJcaName()))
+ .setIssuedAt(Timestamp.valueOf(LocalDateTime.now()))
+ .setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.MINUTES)))
+ .signWith(jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret()))
.compact();
}
@@ -57,8 +58,8 @@ public class JwtTokenGenerator {
return Jwts.builder()
.setHeader(createHeader())
.setSubject(authentication.getName())
- .setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.HOURS)))
- .signWith(new SecretKeySpec(jwtProperties.getRefreshToken().getSecret().getBytes(), SignatureAlgorithm.HS512.getJcaName()))
+ .setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.MINUTES)))
+ .signWith(jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret()))
.compact();
}
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java
index 33c382a..7af0f74 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java
@@ -28,12 +28,12 @@ public enum JwtTokenRule {
/**
* 액세스 토큰의 접두사입니다.
*/
- ACCESS_PREFIX("access"),
+ ACCESS_PREFIX("accessToken"),
/**
* 리프레시 토큰의 접두사입니다.
*/
- REFRESH_PREFIX("refresh"),
+ REFRESH_PREFIX("refreshToken"),
/**
* Bearer 인증 스키마의 접두사입니다.
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java
index ae43a43..d59603d 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java
@@ -1,6 +1,8 @@
package com.spring.infra.security.jwt;
import java.security.Key;
+import java.util.List;
+import java.util.stream.Collectors;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@@ -9,11 +11,14 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import com.spring.infra.security.service.UserPrincipalService;
+import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
/**
@@ -59,7 +64,7 @@ public class JwtTokenService {
*/
public String generateAccessToken(HttpServletResponse response, Authentication authentication) {
String accessToken = jwtTokenGenerator.generateAccessToken(authentication);
- ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration / 1000);
+ ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration);
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
return accessToken;
}
@@ -73,7 +78,7 @@ public class JwtTokenService {
*/
public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
- ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration / 1000);
+ ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration);
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
return refreshToken;
}
@@ -91,11 +96,20 @@ public class JwtTokenService {
.path("/")
.maxAge(maxAgeSeconds)
.httpOnly(true)
- .sameSite("None")
- .secure(true)
+ .sameSite("Lax")
+ .secure(false)
.build();
}
+ /**
+ * 액세스 토큰의 유효성을 검증합니다.
+ *
+ * @param token 검증할 토큰
+ */
+ public void validateToken(String token) {
+ jwtTokenUtil.tokenStatus(token, accessSecretKey);
+ }
+
/**
* 액세스 토큰의 유효성을 검증합니다.
*
@@ -133,13 +147,32 @@ public class JwtTokenService {
}
/**
- * 토큰으로부터 인증 정보를 생성합니다.
+ * JWT 토큰으로부터 인증 정보를 생성합니다.
*
* @param token JWT 토큰
* @return 생성된 Authentication 객체
*/
+ public Authentication getJwtAuthentication(String token) {
+ Claims claims = getUserClaims(token);
+ String sub = claims.getSubject();
+ List auths = claims.keySet().stream()
+ .filter(key -> key.equals(JwtTokenRule.AUTHORITIES_KEY.getValue()))
+ .flatMap(key -> ((List>) claims.get(key)).stream())
+ .map(String::valueOf)
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toList());
+ return new UsernamePasswordAuthenticationToken(sub, "", auths);
+ }
+
+ /**
+ * 토큰으로부터 인증 정보를 가져와서 DB정보를 조회한다.
+ *
+ * @param token JWT 토큰
+ * @return 생성된 Authentication 객체
+ */
+ @Transactional(readOnly = true)
public Authentication getAuthentication(String token) {
- UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token, accessSecretKey));
+ UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token));
return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities());
}
@@ -147,12 +180,25 @@ public class JwtTokenService {
* 토큰에서 사용자 식별자를 추출합니다.
*
* @param token JWT 토큰
- * @param secretKey 비밀 키
* @return 추출된 사용자 식별자
*/
- private String getUserPk(String token, Key secretKey) {
+ private Claims getUserClaims(String token) {
return Jwts.parserBuilder()
- .setSigningKey(secretKey)
+ .setSigningKey(accessSecretKey)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ /**
+ * 토큰에서 사용자 식별자를 추출합니다.
+ *
+ * @param token JWT 토큰
+ * @return 추출된 사용자 식별자
+ */
+ private String getUserPk(String token) {
+ return Jwts.parserBuilder()
+ .setSigningKey(accessSecretKey)
.build()
.parseClaimsJws(token)
.getBody()
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java
index 9a49a78..780566a 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenUtil.java
@@ -9,10 +9,14 @@ import javax.servlet.http.Cookie;
import org.springframework.stereotype.Component;
+import com.spring.infra.security.error.SecurityExceptionRule;
+
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
+import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
/**
@@ -42,14 +46,35 @@ public class JwtTokenUtil {
.parseClaimsJws(token);
return JwtTokenStatus.AUTHENTICATED;
} catch (ExpiredJwtException | IllegalArgumentException e) {
- log.error("만료된 JWT 토큰입니다.");
return JwtTokenStatus.EXPIRED;
} catch (JwtException e) {
- log.error("JWT 토큰이 잘못되었습니다.");
return JwtTokenStatus.INVALID;
}
}
+ /**
+ * JWT 토큰의 상태를 확인하고 예외를 던집니다.
+ *
+ * @param token 검증할 JWT 토큰
+ * @param secretKey 토큰 검증에 사용할 비밀 키
+ */
+ public void tokenStatus(String token, Key secretKey) {
+ try {
+ Jwts.parserBuilder()
+ .setSigningKey(secretKey)
+ .build()
+ .parseClaimsJws(token);
+ } catch (ExpiredJwtException | IllegalArgumentException e) {
+ throw new ExpiredJwtException(null, null, SecurityExceptionRule.EXPIRED_JWT_ERROR.getMessage());
+ } catch (SignatureException e) {
+ throw new SignatureException(SecurityExceptionRule.SIGNATURE_ERROR.getMessage());
+ } catch (MalformedJwtException e) {
+ throw new MalformedJwtException(SecurityExceptionRule.MALFORMED_JWT_ERROR.getMessage());
+ } catch (JwtException e) {
+ throw new JwtException(SecurityExceptionRule.JWT_TOKEN_ERROR.getMessage());
+ }
+ }
+
/**
* 쿠키에서 특정 접두사를 가진 토큰을 추출합니다.
*
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java b/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java
index 20e6f42..aa3c5f2 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java
@@ -8,6 +8,7 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
import com.spring.infra.security.service.UserPrincipalService;
@@ -20,6 +21,7 @@ public class UserAuthenticationProvider implements AuthenticationProvider {
private final PasswordEncoder passwordEncoder;
private final UserPrincipalService userPrincipalService;
+ @Transactional(readOnly = true)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginId = authentication.getName();
diff --git a/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java b/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java
index 22d6b93..55d4a3e 100644
--- a/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java
+++ b/batch-quartz/src/main/java/com/spring/infra/security/service/UserPrincipalService.java
@@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
import com.spring.domain.user.entity.AppUser;
import com.spring.domain.user.repository.AppUserRepository;
import com.spring.infra.security.domain.UserPrincipal;
+import com.spring.infra.security.error.SecurityExceptionRule;
import lombok.RequiredArgsConstructor;
@@ -22,7 +23,7 @@ public class UserPrincipalService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = appUserRepository.findByLoginId(username)
- .orElseThrow(() -> new UsernameNotFoundException("NOT FOUND USER"));
+ .orElseThrow(() -> new UsernameNotFoundException(SecurityExceptionRule.USER_UNAUTHORIZED.getMessage()));
return UserPrincipal.valueOf(user);
}
diff --git a/batch-quartz/src/main/java/com/spring/web/controller/MainController.java b/batch-quartz/src/main/java/com/spring/web/controller/MainController.java
new file mode 100644
index 0000000..c34d576
--- /dev/null
+++ b/batch-quartz/src/main/java/com/spring/web/controller/MainController.java
@@ -0,0 +1,18 @@
+package com.spring.web.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+@RequestMapping("/main")
+public class MainController {
+
+ @GetMapping
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ public String main() {
+ return "pages/main/main";
+ }
+
+}
diff --git a/batch-quartz/src/main/java/com/spring/web/controller/SignController.java b/batch-quartz/src/main/java/com/spring/web/controller/SignController.java
new file mode 100644
index 0000000..558e40b
--- /dev/null
+++ b/batch-quartz/src/main/java/com/spring/web/controller/SignController.java
@@ -0,0 +1,21 @@
+package com.spring.web.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+@RequestMapping("/")
+public class SignController {
+
+ @GetMapping
+ public String signIn() {
+ return "pages/sign/sign-in";
+ }
+
+ @GetMapping("/sign-up")
+ public String signUp() {
+ return "pages/sign/sign-up";
+ }
+
+}
diff --git a/batch-quartz/src/main/resources/application.yml b/batch-quartz/src/main/resources/application.yml
index 53e57ac..8057837 100644
--- a/batch-quartz/src/main/resources/application.yml
+++ b/batch-quartz/src/main/resources/application.yml
@@ -33,6 +33,7 @@ spring:
# - classpath:quartz-schema.sql
jpa:
+ open-in-view: false
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create
@@ -77,9 +78,9 @@ spring:
cache: false
check-template-location: false
enabled: true
- prefix: classpath:/templates
+ prefix: classpath:/templates/
suffix: .html
- view-names: /views/*
+ view-names: pages/*
h2:
console: # H2 DB를 웹에서 관리할 수 있는 기능
@@ -90,8 +91,8 @@ spring:
jwt:
access-token:
- secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290
- expiration: 900 # 15분
+ secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
+ expiration: 1
refresh-token:
- secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290
- expiration: 604800 # 7일
\ No newline at end of file
+ secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
+ expiration: 10080
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/static/css/style.css b/batch-quartz/src/main/resources/static/css/style.css
index 49eb822..cd6808d 100644
--- a/batch-quartz/src/main/resources/static/css/style.css
+++ b/batch-quartz/src/main/resources/static/css/style.css
@@ -1,11 +1,12 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(to right, #f0f0f5, #e9ecef);
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
margin: 0;
+ display: flex;
+ flex-direction: column; /* 세로 방향으로 정렬 */
+ justify-content: center; /* 수직 중앙 정렬 */
+ align-items: center; /* 수평 중앙 정렬 */
+ height: 100vh; /* 전체 화면 높이 사용 */
}
.container {
@@ -15,6 +16,7 @@ body {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
width: 350px;
text-align: center;
+ margin-bottom: 20px; /* footer와의 간격 조정 */
}
.icon {
@@ -59,17 +61,6 @@ input::placeholder {
color: #aaa; /* 플레이스홀더 색상 */
}
-.remember-me {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- margin-bottom: 15px;
-}
-
-.remember-me label {
- margin-left: 5px;
-}
-
button {
width: 100%;
padding: 12px;
@@ -84,4 +75,20 @@ button {
button:hover {
background-color: #005bb5;
+}
+
+.small-button {
+ padding: 5px 10px; /* 버튼 패딩 조정 */
+ font-size: 14px; /* 글자 크기 조정 */
+ background-color: #007aff; /* 배경색 */
+ color: white; /* 글자색 */
+ border: none; /* 테두리 없음 */
+ border-radius: 5px; /* 모서리 둥글게 */
+ cursor: pointer; /* 커서 포인터 */
+ transition: background-color 0.3s; /* 배경색 전환 효과 */
+ margin-top: 10px; /* 로그인 버튼과의 간격 추가 */
+}
+
+.small-button:hover {
+ background-color: #005bb5; /* 호버 시 배경색 변경 */
}
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/static/js/apis/user-api.js b/batch-quartz/src/main/resources/static/js/apis/user-api.js
new file mode 100644
index 0000000..9fe7a82
--- /dev/null
+++ b/batch-quartz/src/main/resources/static/js/apis/user-api.js
@@ -0,0 +1,13 @@
+import apiClient from '../common/axios-instance.js';
+
+export const signIn = async (username, password) => {
+ try {
+ await apiClient.post('/sign-in', {username, password});
+ } catch (error) {
+ console.error('Sign-in error:', error);
+ }
+};
+
+export const signUp = async (loginId, password, userName) => {
+ await apiClient.post('/api/user/sign-up', {loginId, password, userName});
+};
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/static/js/common/axios-instance.js b/batch-quartz/src/main/resources/static/js/common/axios-instance.js
new file mode 100644
index 0000000..c442081
--- /dev/null
+++ b/batch-quartz/src/main/resources/static/js/common/axios-instance.js
@@ -0,0 +1,87 @@
+// Axios apiClient 생성
+const apiClient = axios.create({
+ baseURL: 'http://localhost:8081', // 기본 URL 설정
+ timeout: 100000000, // 요청 타임아웃 설정 (10초)
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+});
+
+// 요청 인터셉터 추가
+apiClient.interceptors.request.use(
+ (config) => {
+ const token = getAccessToken();
+ if (token) {
+ config.headers['Authorization'] = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// 응답 인터셉터 추가
+apiClient.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ async (error) => {
+ const originalRequest = error.config;
+
+ // 401 Unauthorized 에러 처리
+ if (error.response && error.response.status === 401 && !originalRequest._retry) {
+ console.log("333333333333");
+ originalRequest._retry = true;
+
+ const refreshToken = getRefreshToken();
+ console.log("refreshToken===="+refreshToken);
+ if (refreshToken) {
+ try {
+ const response = await axios.post(baseURL+'/auth/refresh', {
+ refreshToken: refreshToken,
+ });
+
+ const { accessToken, newRefreshToken } = response.data;
+
+ // 새로 받은 토큰을 쿠키에 저장
+ setAccessToken(accessToken);
+ if (newRefreshToken) {
+ setRefreshToken(newRefreshToken);
+ }
+
+ // 갱신된 토큰으로 원래 요청 재시도
+ originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
+ return apiClient(originalRequest);
+ } catch (refreshError) {
+ // refresh token이 만료된 경우 처리 (로그아웃 등)
+ console.error('Token refresh failed:', refreshError);
+ // 로그아웃 처리 또는 로그인 페이지로 리다이렉트
+ }
+ }
+ }
+ return Promise.reject(error);
+ }
+);
+
+// 쿠키에서 accessToken 가져오기
+function getAccessToken() {
+ return Cookies.get('accessToken');
+}
+
+// 쿠키에서 refreshToken 가져오기
+function getRefreshToken() {
+ return Cookies.get('refreshToken');
+}
+
+// 쿠키에 accessToken 저장
+function setAccessToken(token) {
+ Cookies.set('accessToken', token, { secure: true, sameSite: 'strict' });
+}
+
+// 쿠키에 refreshToken 저장
+function setRefreshToken(token) {
+ Cookies.set('refreshToken', token, { secure: true, sameSite: 'strict' });
+}
+
+export default apiClient;
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/static/js/common/axiosInstance.js b/batch-quartz/src/main/resources/static/js/common/axiosInstance.js
deleted file mode 100644
index e2352b8..0000000
--- a/batch-quartz/src/main/resources/static/js/common/axiosInstance.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// Axios 인스턴스 생성
-const axiosInstance = axios.create({
- baseURL: 'http://localhost:8081', // 기본 URL 설정
- timeout: 10000, // 요청 타임아웃 설정 (10초)
- headers: {
- 'Content-Type': 'application/json',
- // 필요한 경우 추가 헤더 설정
- }
-});
-
-// 요청 인터셉터 (필요한 경우)
-axiosInstance.interceptors.request.use(
- config => {
- // 요청 전에 수행할 작업 (예: 토큰 추가)
- const token = localStorage.getItem('token'); // 예시: 로컬 스토리지에서 토큰 가져오기
- if (token) {
- config.headers['Authorization'] = `Bearer ${token}`;
- }
- return config;
- },
- error => {
- return Promise.reject(error);
- }
-);
-
-// 응답 인터셉터 (필요한 경우)
-axiosInstance.interceptors.response.use(
- response => {
- return response;
- },
- error => {
- console.log(error.response);
- // 오류 처리 (예: 401 Unauthorized 처리)
- if (error.response && error.response.status === 401) {
- // 로그아웃 처리 또는 리다이렉트
- console.log("111111111111111");
- console.log(error.response);
- }
- return Promise.reject(error);
- }
-);
-
-export default axiosInstance;
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/static/js/lib/cookie/js.cookie.min.js b/batch-quartz/src/main/resources/static/js/lib/cookie/js.cookie.min.js
new file mode 100644
index 0000000..53d35a3
--- /dev/null
+++ b/batch-quartz/src/main/resources/static/js/lib/cookie/js.cookie.min.js
@@ -0,0 +1,2 @@
+/*! js-cookie v3.0.5 | MIT */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t {
- try {
- const response = await axiosInstance.post('/sign-in', {
- username,
- password
- });
- console.log('로그인 성공:', response.data);
- } catch (error) {
- console.error('로그인 실패:', error);
- }
-};
-
-document.getElementById('signinForm').addEventListener('submit', function(event) {
- event.preventDefault(); // 기본 폼 제출 방지
- const username = document.getElementById('username').value;
- const password = document.getElementById('password').value;
- login(username, password);
-});
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/fragments/config.html b/batch-quartz/src/main/resources/templates/fragments/config.html
new file mode 100644
index 0000000..2e4bad7
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/fragments/config.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/fragments/footer.html b/batch-quartz/src/main/resources/templates/fragments/footer.html
new file mode 100644
index 0000000..0c7f257
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/fragments/footer.html
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/fragments/header.html b/batch-quartz/src/main/resources/templates/fragments/header.html
new file mode 100644
index 0000000..94a47b7
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/fragments/header.html
@@ -0,0 +1,4 @@
+
+
+ 사이트 로고
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/fragments/left.html b/batch-quartz/src/main/resources/templates/fragments/left.html
new file mode 100644
index 0000000..c193066
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/fragments/left.html
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/layouts/layout.html b/batch-quartz/src/main/resources/templates/layouts/layout.html
new file mode 100644
index 0000000..a175cfd
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/layouts/layout.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/layouts/signin-layout.html b/batch-quartz/src/main/resources/templates/layouts/signin-layout.html
new file mode 100644
index 0000000..99320b4
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/layouts/signin-layout.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/pages/error/error.html b/batch-quartz/src/main/resources/templates/pages/error/error.html
new file mode 100644
index 0000000..527f8a3
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/pages/error/error.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Error page
+
+
+
+ Error Page
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/pages/main/main.html b/batch-quartz/src/main/resources/templates/pages/main/main.html
new file mode 100644
index 0000000..343213b
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/pages/main/main.html
@@ -0,0 +1,11 @@
+
+
+
+
+ 본문 영역입니다.
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html b/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html
new file mode 100644
index 0000000..25b9d77
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html
@@ -0,0 +1,35 @@
+
+
+
+ 로그인 페이지
+
+
+
+
+
+

+
+
로그인
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/pages/sign/sign-up.html b/batch-quartz/src/main/resources/templates/pages/sign/sign-up.html
new file mode 100644
index 0000000..c3f10ab
--- /dev/null
+++ b/batch-quartz/src/main/resources/templates/pages/sign/sign-up.html
@@ -0,0 +1,28 @@
+
+
+
+
+ 회원가입 페이지
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/batch-quartz/src/main/resources/templates/views/user/signIn.html b/batch-quartz/src/main/resources/templates/views/user/signIn.html
deleted file mode 100644
index 36a4adb..0000000
--- a/batch-quartz/src/main/resources/templates/views/user/signIn.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
- 로그인 페이지
-
-
-
-
-
-
-
-

-
-
로그인
-
-
-
-
\ No newline at end of file