diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/common/CookieHelper.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/common/CookieHelper.java deleted file mode 100644 index a2b6ae2..0000000 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/common/CookieHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.bpgroup.poc.admin.common; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import java.util.Optional; - -public class CookieHelper { - - public static Cookie createCookie(String cookieName, String cookieValue) { - Cookie jwtCookie = new Cookie(cookieName, cookieValue); - jwtCookie.setHttpOnly(true); - jwtCookie.setPath("/"); - return jwtCookie; - } - - public static Optional getValueFromCookieWithName(String name) { - ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); - HttpServletRequest request = attr.getRequest(); - - Cookie[] cookies = request.getCookies(); - return getValue(cookies, name); - } - - public static Optional getValueFromCookieWithName(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - return getValue(cookies, name); - } - - private static Optional getValue(Cookie[] cookies, String name) { - if (null != cookies) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals(name)) { - return Optional.of(cookie.getValue()); - } - } - } - - return Optional.empty(); - } -} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/common/JwtHelper.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/common/JwtHelper.java deleted file mode 100644 index b81af3f..0000000 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/common/JwtHelper.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.bpgroup.poc.admin.common; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; - -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; - -public class JwtHelper { - public static Claims getClaims(String jwtKey, String token) { - SecretKey key = Keys.hmacShaKeyFor(jwtKey.getBytes(StandardCharsets.UTF_8)); - return Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java new file mode 100644 index 0000000..69a328d --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java @@ -0,0 +1,31 @@ +package com.bpgroup.poc.admin.domain.base.admin.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "admin_token") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AdminToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "access_token", nullable = false) + private String accessToken; + + @Column(name = "refresh_token", length = 32, nullable = false) + private String refreshToken; + + @Column(name = "expired_refresh_token", nullable = false) + private LocalDateTime expiredRefreshToken; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "admin_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), nullable = false) + private Admin admin; + +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java new file mode 100644 index 0000000..51314d8 --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java @@ -0,0 +1,6 @@ +package com.bpgroup.poc.admin.domain.base.admin.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminTokenRepository extends JpaRepository { +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/filter/LoggingFilter.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/filter/LoggingFilter.java index fdacfa7..c537438 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/filter/LoggingFilter.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/filter/LoggingFilter.java @@ -1,13 +1,12 @@ package com.bpgroup.poc.admin.filter; import com.bpgroup.poc.admin.app.admin.AdminActionLogAppService; -import com.bpgroup.poc.admin.common.CookieHelper; -import com.bpgroup.poc.admin.common.JwtHelper; import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants; +import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -18,19 +17,23 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Optional; import java.util.stream.Stream; @Slf4j -@Order(Ordered.HIGHEST_PRECEDENCE) +@Order(Ordered.LOWEST_PRECEDENCE) @RequiredArgsConstructor @Component public class LoggingFilter extends OncePerRequestFilter { private final AdminActionLogAppService adminActionLogAppService; + private final JwtTokenProvider jwtTokenProvider; + + private static final String[] EXCLUDED_URL_PATTERNS = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font"}; + private static final String[] EXCLUDED_CONTENT_TYPES = {"text/html"}; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -51,11 +54,12 @@ public class LoggingFilter extends OncePerRequestFilter { private void loggingRequest(ContentCachingRequestWrapper request, String sessionId) { String requestValue = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8); - Optional jwtToken = CookieHelper.getValueFromCookieWithName(request, JwtTokenConstants.ACCESS_TOKEN); + Cookie cookie = WebUtils.getCookie(request, JwtTokenConstants.ACCESS_TOKEN_NAME); - if (jwtToken.isPresent()) { + if (cookie != null) { + String accessToken = cookie.getValue(); try { - Claims claims = JwtHelper.getClaims(JwtTokenConstants.KEY, jwtToken.get()); + Claims claims = jwtTokenProvider.getClaims(accessToken); String username = claims.get("username", String.class); adminActionLogAppService.create( @@ -66,10 +70,8 @@ public class LoggingFilter extends OncePerRequestFilter { username ) ); - } catch (ExpiredJwtException e) { - log.info("SESSION ID: {} Request - JWT 토큰 만료", sessionId); } catch (Exception e) { - log.error("SESSION ID: {} Request - JWT 토큰 검증 실패", sessionId); + // Do nothing (no-op) } } @@ -101,9 +103,7 @@ public class LoggingFilter extends OncePerRequestFilter { * 로깅을 제외할 content type 추가 */ private boolean isLoggingContentType(String contentType) { - return Stream.of( - "text/html" - ).noneMatch(contentType::contains); + return Stream.of(EXCLUDED_CONTENT_TYPES).noneMatch(contentType::contains); } /** @@ -111,16 +111,6 @@ public class LoggingFilter extends OncePerRequestFilter { */ @Override protected boolean shouldNotFilter(HttpServletRequest request) { - return Stream.of( - "/login", - "/logout", - "/error", - "/css", - "/js", - "/images", - "/favicon.ico", - "/common/modal", - "/font" - ).anyMatch(s -> request.getRequestURI().contains(s)); + return Stream.of(EXCLUDED_URL_PATTERNS).anyMatch(s -> request.getRequestURI().contains(s)); } } diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java index 6964a74..de6ab08 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java @@ -6,6 +6,7 @@ import com.bpgroup.poc.admin.security.authorization.CustomAccessDeniedHandler; import com.bpgroup.poc.admin.security.authorization.CustomAuthorizationManager; import com.bpgroup.poc.admin.app.authorization.AuthorizationService; import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants; +import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider; import com.bpgroup.poc.admin.security.jwt.JwtTokenValidateFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -35,24 +36,36 @@ public class SecurityConfig { private static final String ERROR_PATH = "/error"; private final AuthenticationService authenticationService; - private final AuthorizationService authorizationService; + private final JwtTokenProvider jwtTokenProvider; @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + configureSessionManagement(http); + configureHeaders(http); + configureAuthorization(http); + configureFormLogin(http); + configureLogout(http); + configureExceptionHandling(http); + return http.build(); + } + + private void configureSessionManagement(HttpSecurity http) throws Exception { http.sessionManagement(t -> t.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + } - // 보안 기본 설정 + private void configureHeaders(HttpSecurity http) throws Exception { http.headers(c -> c - .frameOptions(fo -> fo.sameOrigin()) // X-Frame-Options: Same Origin - .xssProtection(xp -> xp.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) // X-XSS-Protection: 1; mode=block - .contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff - .cacheControl(cache -> cache.disable()) //ERR_CACHE_MISS + .frameOptions(fo -> fo.sameOrigin()) + .xssProtection(xp -> xp.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) + .contentTypeOptions(Customizer.withDefaults()) + .cacheControl(cache -> cache.disable()) ); + } - // 인가 설정 + private void configureAuthorization(HttpSecurity http) throws Exception { http.authorizeHttpRequests(c -> c .requestMatchers("/css/**", "/images/**", "/js/**", "/font/**", "/favicon.ico").permitAll() .requestMatchers("/common/modal/**").permitAll() @@ -60,29 +73,28 @@ public class SecurityConfig { .anyRequest() .access(new CustomAuthorizationManager(authorizationService)) ); + } - http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리 - http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정 - - http.addFilterAfter(new JwtTokenValidateFilter(), BasicAuthenticationFilter.class); + private void configureFormLogin(HttpSecurity http) throws Exception { + http.formLogin(AbstractHttpConfigurer::disable); + http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); + http.addFilterAfter(new JwtTokenValidateFilter(jwtTokenProvider), BasicAuthenticationFilter.class); + } + private void configureLogout(HttpSecurity http) throws Exception { http.logout(c -> c .logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET")) .logoutSuccessUrl(LOGIN_PATH) + .deleteCookies(JwtTokenConstants.ACCESS_TOKEN_NAME) + .deleteCookies(JwtTokenConstants.REFRESH_TOKEN_NAME) ); + } + private void configureExceptionHandling(HttpSecurity http) throws Exception { http.exceptionHandling(c -> { - c.authenticationEntryPoint(new CustomAuthenticationEntryPoint()); // Authentication 실패 처리 - c.accessDeniedHandler(new CustomAccessDeniedHandler()); // Authorization 실패 처리 + c.authenticationEntryPoint(new CustomAuthenticationEntryPoint()); + c.accessDeniedHandler(new CustomAccessDeniedHandler()); }); - - http.logout(c -> c - .logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET")) - .logoutSuccessUrl(LOGIN_PATH) - .deleteCookies(JwtTokenConstants.ACCESS_TOKEN) - ); - - return http.build(); } @Bean @@ -92,18 +104,21 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager() { + return new ProviderManager(getAuthenticationProviders()); + } + + private List getAuthenticationProviders() { List providers = new ArrayList<>(); providers.add(customAuthenticationProvider()); - return new ProviderManager(providers); + return providers; } @Bean public CustomAuthenticationFilter authenticationGenerateFilter() { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); - filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler()); + filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler(jwtTokenProvider)); filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler()); return filter; } - } diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationEntryPoint.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationEntryPoint.java index bba9b95..77fcc64 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationEntryPoint.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationEntryPoint.java @@ -1,29 +1,28 @@ package com.bpgroup.poc.admin.security.authentication; -import com.bpgroup.poc.admin.common.CookieHelper; import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.web.util.WebUtils; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.Optional; @Slf4j public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - Optional jwtToken = CookieHelper.getValueFromCookieWithName(JwtTokenConstants.ACCESS_TOKEN); - if (jwtToken.isPresent()) { + Cookie cookie = WebUtils.getCookie(request, JwtTokenConstants.ACCESS_TOKEN_NAME); + if (cookie != null) { response.sendRedirect("/login?error=" + URLEncoder.encode("로그인 세션이 만료되었습니다. 다시 로그인 해주세요.", StandardCharsets.UTF_8)); } else { response.sendRedirect("/login?error=" + URLEncoder.encode("로그인이 필요합니다.", StandardCharsets.UTF_8)); } - } } \ No newline at end of file diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java index be97525..5db7640 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java @@ -1,38 +1,43 @@ package com.bpgroup.poc.admin.security.authentication; import com.bpgroup.poc.admin.app.authentication.AuthenticationResponse; -import com.bpgroup.poc.admin.common.CookieHelper; -import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants; -import com.bpgroup.poc.admin.security.jwt.JwtTokenGenerator; +import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.UUID; +@RequiredArgsConstructor public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + String username = (String) authentication.getPrincipal(); + + Cookie accessTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS); + response.addCookie(accessTokenCookie); + + Cookie refreshTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH); + response.addCookie(refreshTokenCookie); + response.setStatus(HttpServletResponse.SC_OK); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - String jwtToken = JwtTokenGenerator.generate(authentication.getName()); - Cookie at = CookieHelper.createCookie(JwtTokenConstants.ACCESS_TOKEN, jwtToken); - Cookie rt = CookieHelper.createCookie(JwtTokenConstants.REFRESH_TOKEN, UUID.randomUUID().toString()); - response.addCookie(at); - response.addCookie(rt); - String jsonResponse = new ObjectMapper().writeValueAsString( AuthenticationResponse.success() ); response.getWriter().write(jsonResponse); } + } \ No newline at end of file diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenConstants.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenConstants.java index 1cdf05e..199ec4c 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenConstants.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenConstants.java @@ -5,9 +5,12 @@ package com.bpgroup.poc.admin.security.jwt; */ public class JwtTokenConstants { public static final String ISSUER = "BP"; - public static final String SUBJECT = "TOKEN"; + public static final String AT_SUBJECT = "AT"; + public static final String RT_SUBJECT = "RT"; + public static final String KEY = "8530b13adb4e420d9694b27570635b47"; - public static final String ACCESS_TOKEN = "AT"; - public static final String REFRESH_TOKEN = "RT"; - public static final long EXPIRATION_TIME = 30000; + public static final String ACCESS_TOKEN_NAME = "AT"; + public static final String REFRESH_TOKEN_NAME = "RT"; + public static final long AT_EXPIRATION_TIME = 30 * 1000; + public static final long RT_EXPIRATION_TIME = 60 * 1000; } diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenExpiredException.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenExpiredException.java new file mode 100644 index 0000000..3bbdc57 --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenExpiredException.java @@ -0,0 +1,7 @@ +package com.bpgroup.poc.admin.security.jwt; + +public class JwtTokenExpiredException extends Exception { + public JwtTokenExpiredException() { + super(); + } +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenGenerator.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenGenerator.java deleted file mode 100644 index 1d13bea..0000000 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenGenerator.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.bpgroup.poc.admin.security.jwt; - -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; - -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; - -public class JwtTokenGenerator { - - public static String generate(String username) { - SecretKey key = Keys.hmacShaKeyFor(JwtTokenConstants.KEY.getBytes(StandardCharsets.UTF_8)); - return Jwts.builder() - .issuer(JwtTokenConstants.ISSUER) - .subject(JwtTokenConstants.SUBJECT) - .claim("username", username) - .issuedAt(new java.util.Date()) - .expiration(new java.util.Date(System.currentTimeMillis() + JwtTokenConstants.EXPIRATION_TIME)) - .signWith(key) - .compact(); - } -} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenInvalidException.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenInvalidException.java new file mode 100644 index 0000000..0f92e2f --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenInvalidException.java @@ -0,0 +1,7 @@ +package com.bpgroup.poc.admin.security.jwt; + +public class JwtTokenInvalidException extends Exception { + public JwtTokenInvalidException() { + super(); + } +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..dec1dc3 --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java @@ -0,0 +1,85 @@ +package com.bpgroup.poc.admin.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + // TODO: 추후 Key 별도 관리 필요 + private static final SecretKey JWT_KEY = Keys.hmacShaKeyFor(JwtTokenConstants.KEY.getBytes(StandardCharsets.UTF_8)); + + public enum JwtTokenType { + ACCESS, REFRESH + } + + public Cookie createCookieWithToken(String username, JwtTokenType type) { + String tokenName; + String subject; + long expirationTime; + + if (type == JwtTokenType.ACCESS) { + tokenName = JwtTokenConstants.ACCESS_TOKEN_NAME; + subject = JwtTokenConstants.AT_SUBJECT; + expirationTime = JwtTokenConstants.AT_EXPIRATION_TIME; + } else { + tokenName = JwtTokenConstants.REFRESH_TOKEN_NAME; + subject = JwtTokenConstants.RT_SUBJECT; + expirationTime = JwtTokenConstants.RT_EXPIRATION_TIME; + } + + String token = generateToken(JwtTokenConstants.ISSUER, subject, username, expirationTime); + + Cookie tokenCookie = new Cookie(tokenName, token); + tokenCookie.setHttpOnly(true); + tokenCookie.setPath("/"); + + return tokenCookie; + } + + private Cookie createCookieWithToken(String tokenName, String subject, String username, long expirationTime) { + String token = generateToken(JwtTokenConstants.ISSUER, subject, username, expirationTime); + + Cookie tokenCookie = new Cookie(tokenName, token); + tokenCookie.setHttpOnly(true); + tokenCookie.setPath("/"); + + return tokenCookie; + } + + private String generateToken(String issuer, String subject, String username, long expirationTime) { + return Jwts.builder() + .issuer(issuer) + .subject(subject) + .claim("username", username) .issuedAt(new Date()) + .expiration(getExpirationDate(expirationTime)) + .signWith(JWT_KEY) + .compact(); + } + + private Date getExpirationDate(long expirationTime) { + return new Date(System.currentTimeMillis() + expirationTime); + } + + public Claims getClaims(String token) throws JwtTokenExpiredException, JwtTokenInvalidException { + try { + return Jwts.parser() + .verifyWith(JWT_KEY) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { + throw new JwtTokenExpiredException(); + } catch (Exception e) { + throw new JwtTokenInvalidException(); + } + } +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java index 55b5e8e..0102217 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java @@ -1,52 +1,84 @@ package com.bpgroup.poc.admin.security.jwt; -import com.bpgroup.poc.admin.common.CookieHelper; -import com.bpgroup.poc.admin.common.JwtHelper; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; import java.io.IOException; -import java.util.Optional; import java.util.stream.Stream; @Slf4j +@RequiredArgsConstructor public class JwtTokenValidateFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private static final String[] EXCLUDED_URL_PATTERNS = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font"}; + + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String sessionId = request.getSession().getId(); try { - try { - Optional jwtToken = CookieHelper.getValueFromCookieWithName(JwtTokenConstants.ACCESS_TOKEN); - if (jwtToken.isPresent()) { - Claims claims = JwtHelper.getClaims(JwtTokenConstants.KEY, jwtToken.get()); - String username = claims.get("username", String.class); + Cookie cookie = WebUtils.getCookie(request, JwtTokenConstants.ACCESS_TOKEN_NAME); + if (cookie != null) { + String accessToken = cookie.getValue(); + Claims claims = jwtTokenProvider.getClaims(accessToken); + String username = claims.get("username", String.class); - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null); - SecurityContextHolder.getContext().setAuthentication(auth); - } - } catch (ExpiredJwtException e) { - log.info("SESSION ID: {} Request - JWT 토큰 만료", sessionId); - } catch (Exception e) { - log.error("SESSION ID: {} Request - JWT 토큰 검증 실패", sessionId); + setSecurityContext(username); + } else { + log.error("SESSION ID: {} Request - JWT AT 토큰 없음", sessionId); } + } catch (JwtTokenExpiredException e) { + regenerateToken(request, response, sessionId); + } catch (JwtTokenInvalidException e) { + log.error("SESSION ID: {} Request - JWT AT 토큰 검증 실패", sessionId); } finally { filterChain.doFilter(request, response); } } + private void regenerateToken(HttpServletRequest request, HttpServletResponse response, String sessionId) { + log.info("SESSION ID: {} Request - JWT AT 토큰 만료로 인한 RT 재발급 프로세스 시작", sessionId); + Cookie cookie = WebUtils.getCookie(request, JwtTokenConstants.REFRESH_TOKEN_NAME); + if (cookie != null) { + String refreshToken = cookie.getValue(); + try { + Claims claims = jwtTokenProvider.getClaims(refreshToken); + String username = claims.get("username", String.class); + + Cookie accessTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS); + response.addCookie(accessTokenCookie); + + Cookie refreshTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH); + response.addCookie(refreshTokenCookie); + + setSecurityContext(username); + } catch (JwtTokenExpiredException e) { + log.error("SESSION ID: {} JWT RT 토큰 만료", sessionId); + } catch (JwtTokenInvalidException e) { + log.error("SESSION ID: {} JWT RT 토큰 재발급 실패", sessionId); + } + } + } + + private static void setSecurityContext(String username) { + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null); + SecurityContextHolder.getContext().setAuthentication(auth); + } + @Override protected boolean shouldNotFilter(HttpServletRequest request) { - return Stream.of( - "/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font" - ).anyMatch(c -> request.getRequestURI().contains(c)); + return Stream.of(EXCLUDED_URL_PATTERNS).anyMatch(c -> request.getRequestURI().contains(c)); } }