admin: RT 를 통한 AT 재발급 프로세스 추가

This commit is contained in:
geonhos 2024-05-22 14:21:23 +09:00
parent bbd2bc4ded
commit 62952885ff
14 changed files with 267 additions and 171 deletions

View File

@ -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<String> getValueFromCookieWithName(String name) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();
Cookie[] cookies = request.getCookies();
return getValue(cookies, name);
}
public static Optional<String> getValueFromCookieWithName(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
return getValue(cookies, name);
}
private static Optional<String> 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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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<AdminToken, Long> {
}

View File

@ -1,13 +1,12 @@
package com.bpgroup.poc.admin.filter; package com.bpgroup.poc.admin.filter;
import com.bpgroup.poc.admin.app.admin.AdminActionLogAppService; 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.JwtTokenConstants;
import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -18,19 +17,23 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
@Slf4j @Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE) @Order(Ordered.LOWEST_PRECEDENCE)
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class LoggingFilter extends OncePerRequestFilter { public class LoggingFilter extends OncePerRequestFilter {
private final AdminActionLogAppService adminActionLogAppService; 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 @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 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) { private void loggingRequest(ContentCachingRequestWrapper request, String sessionId) {
String requestValue = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8); String requestValue = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
Optional<String> 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 { try {
Claims claims = JwtHelper.getClaims(JwtTokenConstants.KEY, jwtToken.get()); Claims claims = jwtTokenProvider.getClaims(accessToken);
String username = claims.get("username", String.class); String username = claims.get("username", String.class);
adminActionLogAppService.create( adminActionLogAppService.create(
@ -66,10 +70,8 @@ public class LoggingFilter extends OncePerRequestFilter {
username username
) )
); );
} catch (ExpiredJwtException e) {
log.info("SESSION ID: {} Request - JWT 토큰 만료", sessionId);
} catch (Exception e) { } 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 추가 * 로깅을 제외할 content type 추가
*/ */
private boolean isLoggingContentType(String contentType) { private boolean isLoggingContentType(String contentType) {
return Stream.of( return Stream.of(EXCLUDED_CONTENT_TYPES).noneMatch(contentType::contains);
"text/html"
).noneMatch(contentType::contains);
} }
/** /**
@ -111,16 +111,6 @@ public class LoggingFilter extends OncePerRequestFilter {
*/ */
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) { protected boolean shouldNotFilter(HttpServletRequest request) {
return Stream.of( return Stream.of(EXCLUDED_URL_PATTERNS).anyMatch(s -> request.getRequestURI().contains(s));
"/login",
"/logout",
"/error",
"/css",
"/js",
"/images",
"/favicon.ico",
"/common/modal",
"/font"
).anyMatch(s -> request.getRequestURI().contains(s));
} }
} }

View File

@ -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.security.authorization.CustomAuthorizationManager;
import com.bpgroup.poc.admin.app.authorization.AuthorizationService; import com.bpgroup.poc.admin.app.authorization.AuthorizationService;
import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants; 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 com.bpgroup.poc.admin.security.jwt.JwtTokenValidateFilter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -35,24 +36,36 @@ public class SecurityConfig {
private static final String ERROR_PATH = "/error"; private static final String ERROR_PATH = "/error";
private final AuthenticationService authenticationService; private final AuthenticationService authenticationService;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final JwtTokenProvider jwtTokenProvider;
@Bean @Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { 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)); http.sessionManagement(t -> t.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}
// 보안 기본 설정 private void configureHeaders(HttpSecurity http) throws Exception {
http.headers(c -> c http.headers(c -> c
.frameOptions(fo -> fo.sameOrigin()) // X-Frame-Options: Same Origin .frameOptions(fo -> fo.sameOrigin())
.xssProtection(xp -> xp.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) // X-XSS-Protection: 1; mode=block .xssProtection(xp -> xp.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff .contentTypeOptions(Customizer.withDefaults())
.cacheControl(cache -> cache.disable()) //ERR_CACHE_MISS .cacheControl(cache -> cache.disable())
); );
}
// 인가 설정 private void configureAuthorization(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(c -> c http.authorizeHttpRequests(c -> c
.requestMatchers("/css/**", "/images/**", "/js/**", "/font/**", "/favicon.ico").permitAll() .requestMatchers("/css/**", "/images/**", "/js/**", "/font/**", "/favicon.ico").permitAll()
.requestMatchers("/common/modal/**").permitAll() .requestMatchers("/common/modal/**").permitAll()
@ -60,29 +73,28 @@ public class SecurityConfig {
.anyRequest() .anyRequest()
.access(new CustomAuthorizationManager(authorizationService)) .access(new CustomAuthorizationManager(authorizationService))
); );
}
http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리 private void configureFormLogin(HttpSecurity http) throws Exception {
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정 http.formLogin(AbstractHttpConfigurer::disable);
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(new JwtTokenValidateFilter(), BasicAuthenticationFilter.class); http.addFilterAfter(new JwtTokenValidateFilter(jwtTokenProvider), BasicAuthenticationFilter.class);
}
private void configureLogout(HttpSecurity http) throws Exception {
http.logout(c -> c http.logout(c -> c
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET")) .logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
.logoutSuccessUrl(LOGIN_PATH) .logoutSuccessUrl(LOGIN_PATH)
.deleteCookies(JwtTokenConstants.ACCESS_TOKEN_NAME)
.deleteCookies(JwtTokenConstants.REFRESH_TOKEN_NAME)
); );
}
private void configureExceptionHandling(HttpSecurity http) throws Exception {
http.exceptionHandling(c -> { http.exceptionHandling(c -> {
c.authenticationEntryPoint(new CustomAuthenticationEntryPoint()); // Authentication 실패 처리 c.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
c.accessDeniedHandler(new CustomAccessDeniedHandler()); // Authorization 실패 처리 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 @Bean
@ -92,18 +104,21 @@ public class SecurityConfig {
@Bean @Bean
public AuthenticationManager authenticationManager() { public AuthenticationManager authenticationManager() {
return new ProviderManager(getAuthenticationProviders());
}
private List<AuthenticationProvider> getAuthenticationProviders() {
List<AuthenticationProvider> providers = new ArrayList<>(); List<AuthenticationProvider> providers = new ArrayList<>();
providers.add(customAuthenticationProvider()); providers.add(customAuthenticationProvider());
return new ProviderManager(providers); return providers;
} }
@Bean @Bean
public CustomAuthenticationFilter authenticationGenerateFilter() { public CustomAuthenticationFilter authenticationGenerateFilter() {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager()); filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler()); filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler(jwtTokenProvider));
filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler()); filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
return filter; return filter;
} }
} }

View File

@ -1,29 +1,28 @@
package com.bpgroup.poc.admin.security.authentication; package com.bpgroup.poc.admin.security.authentication;
import com.bpgroup.poc.admin.common.CookieHelper;
import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants; import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.util.WebUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional;
@Slf4j @Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override @Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Optional<String> jwtToken = CookieHelper.getValueFromCookieWithName(JwtTokenConstants.ACCESS_TOKEN); Cookie cookie = WebUtils.getCookie(request, JwtTokenConstants.ACCESS_TOKEN_NAME);
if (jwtToken.isPresent()) { if (cookie != null) {
response.sendRedirect("/login?error=" + URLEncoder.encode("로그인 세션이 만료되었습니다. 다시 로그인 해주세요.", StandardCharsets.UTF_8)); response.sendRedirect("/login?error=" + URLEncoder.encode("로그인 세션이 만료되었습니다. 다시 로그인 해주세요.", StandardCharsets.UTF_8));
} else { } else {
response.sendRedirect("/login?error=" + URLEncoder.encode("로그인이 필요합니다.", StandardCharsets.UTF_8)); response.sendRedirect("/login?error=" + URLEncoder.encode("로그인이 필요합니다.", StandardCharsets.UTF_8));
} }
} }
} }

View File

@ -1,38 +1,43 @@
package com.bpgroup.poc.admin.security.authentication; package com.bpgroup.poc.admin.security.authentication;
import com.bpgroup.poc.admin.app.authentication.AuthenticationResponse; import com.bpgroup.poc.admin.app.authentication.AuthenticationResponse;
import com.bpgroup.poc.admin.common.CookieHelper; import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider;
import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants;
import com.bpgroup.poc.admin.security.jwt.JwtTokenGenerator;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.UUID;
@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { 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.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); 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( String jsonResponse = new ObjectMapper().writeValueAsString(
AuthenticationResponse.success() AuthenticationResponse.success()
); );
response.getWriter().write(jsonResponse); response.getWriter().write(jsonResponse);
} }
} }

View File

@ -5,9 +5,12 @@ package com.bpgroup.poc.admin.security.jwt;
*/ */
public class JwtTokenConstants { public class JwtTokenConstants {
public static final String ISSUER = "BP"; 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 KEY = "8530b13adb4e420d9694b27570635b47";
public static final String ACCESS_TOKEN = "AT"; public static final String ACCESS_TOKEN_NAME = "AT";
public static final String REFRESH_TOKEN = "RT"; public static final String REFRESH_TOKEN_NAME = "RT";
public static final long EXPIRATION_TIME = 30000; public static final long AT_EXPIRATION_TIME = 30 * 1000;
public static final long RT_EXPIRATION_TIME = 60 * 1000;
} }

View File

@ -0,0 +1,7 @@
package com.bpgroup.poc.admin.security.jwt;
public class JwtTokenExpiredException extends Exception {
public JwtTokenExpiredException() {
super();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
package com.bpgroup.poc.admin.security.jwt;
public class JwtTokenInvalidException extends Exception {
public JwtTokenInvalidException() {
super();
}
}

View File

@ -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();
}
}
}

View File

@ -1,52 +1,84 @@
package com.bpgroup.poc.admin.security.jwt; 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.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
@Slf4j @Slf4j
@RequiredArgsConstructor
public class JwtTokenValidateFilter extends OncePerRequestFilter { 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 @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String sessionId = request.getSession().getId(); String sessionId = request.getSession().getId();
try { try {
try { Cookie cookie = WebUtils.getCookie(request, JwtTokenConstants.ACCESS_TOKEN_NAME);
Optional<String> jwtToken = CookieHelper.getValueFromCookieWithName(JwtTokenConstants.ACCESS_TOKEN); if (cookie != null) {
if (jwtToken.isPresent()) { String accessToken = cookie.getValue();
Claims claims = JwtHelper.getClaims(JwtTokenConstants.KEY, jwtToken.get()); Claims claims = jwtTokenProvider.getClaims(accessToken);
String username = claims.get("username", String.class); String username = claims.get("username", String.class);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null); setSecurityContext(username);
SecurityContextHolder.getContext().setAuthentication(auth); } else {
} log.error("SESSION ID: {} Request - JWT AT 토큰 없음", sessionId);
} catch (ExpiredJwtException e) {
log.info("SESSION ID: {} Request - JWT 토큰 만료", sessionId);
} catch (Exception e) {
log.error("SESSION ID: {} Request - JWT 토큰 검증 실패", sessionId);
} }
} catch (JwtTokenExpiredException e) {
regenerateToken(request, response, sessionId);
} catch (JwtTokenInvalidException e) {
log.error("SESSION ID: {} Request - JWT AT 토큰 검증 실패", sessionId);
} finally { } finally {
filterChain.doFilter(request, response); 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 @Override
protected boolean shouldNotFilter(HttpServletRequest request) { protected boolean shouldNotFilter(HttpServletRequest request) {
return Stream.of( return Stream.of(EXCLUDED_URL_PATTERNS).anyMatch(c -> request.getRequestURI().contains(c));
"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font"
).anyMatch(c -> request.getRequestURI().contains(c));
} }
} }