admin: RT 를 통한 AT 재발급 프로세스 추가
This commit is contained in:
parent
bbd2bc4ded
commit
62952885ff
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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<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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthenticationProvider> getAuthenticationProviders() {
|
||||
List<AuthenticationProvider> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.bpgroup.poc.admin.security.jwt;
|
||||
|
||||
public class JwtTokenExpiredException extends Exception {
|
||||
public JwtTokenExpiredException() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.bpgroup.poc.admin.security.jwt;
|
||||
|
||||
public class JwtTokenInvalidException extends Exception {
|
||||
public JwtTokenInvalidException() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> jwtToken = CookieHelper.getValueFromCookieWithName(JwtTokenConstants.ACCESS_TOKEN);
|
||||
if (jwtToken.isPresent()) {
|
||||
Claims claims = JwtHelper.getClaims(JwtTokenConstants.KEY, jwtToken.get());
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue