From a153536521a873866788d9d109c939a3d2fb55db Mon Sep 17 00:00:00 2001
From: geonhos
Date: Mon, 20 May 2024 12:03:14 +0900
Subject: [PATCH] =?UTF-8?q?admin:=20=EC=9D=B8=EC=A6=9D=20=EC=98=A4?=
=?UTF-8?q?=EB=A5=98=20=EC=B2=98=EB=A6=AC=20=20custom=20AuthenticationEntr?=
=?UTF-8?q?yPoint=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../poc/admin/common/CookieHelper.java | 27 +++++++
.../poc/admin/fitler/LoggingFilter.java | 6 +-
.../poc/admin/security/SecurityConfig.java | 18 ++++-
.../CustomAuthenticationEntryPoint.java | 29 ++++++++
.../CustomAuthenticationSuccessHandler.java | 27 ++-----
.../poc/admin/security/jwt/JwtConstants.java | 10 ---
.../admin/security/jwt/JwtTokenConstants.java | 12 ++++
.../admin/security/jwt/JwtTokenGenerator.java | 30 ++++++++
.../security/jwt/JwtTokenValidateFilter.java | 57 +++++++++++++++
.../security/jwt/JwtTokenValidatorFilter.java | 71 -------------------
.../poc/admin/web/login/LoginController.java | 8 ++-
.../main/resources/templates/login/login.html | 6 +-
12 files changed, 188 insertions(+), 113 deletions(-)
create mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/common/CookieHelper.java
create mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationEntryPoint.java
delete mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtConstants.java
create mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenConstants.java
create mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenGenerator.java
create mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java
delete mode 100644 poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidatorFilter.java
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
new file mode 100644
index 0000000..6c8d815
--- /dev/null
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/common/CookieHelper.java
@@ -0,0 +1,27 @@
+package com.bpgroup.poc.admin.common;
+
+import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants;
+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 Optional getValueFromCookieWithName(String name) {
+ ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
+ HttpServletRequest request = attr.getRequest();
+
+ Cookie[] cookies = request.getCookies();
+ if (null != cookies) {
+ for (Cookie cookie : cookies) {
+ if (JwtTokenConstants.NAME.equals(cookie.getName())) {
+ return Optional.of(cookie.getValue());
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
+}
diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/fitler/LoggingFilter.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/fitler/LoggingFilter.java
index 5bf0224..1e7ff15 100644
--- a/poc/admin/src/main/java/com/bpgroup/poc/admin/fitler/LoggingFilter.java
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/fitler/LoggingFilter.java
@@ -43,9 +43,11 @@ public class LoggingFilter extends OncePerRequestFilter {
}
private void loggingResponse(ContentCachingResponseWrapper response, String sessionId) {
- log.info("Session ID: {} Response - status: {} Content-Type: {}", sessionId, response.getStatus(), response.getContentType());
+ String contentType = response.getContentType();
- if (isLoggingContentType(response.getContentType())) {
+ log.info("Session ID: {} Response - status: {} Content-Type: {}", sessionId, response.getStatus(), contentType);
+
+ if (contentType != null && isLoggingContentType(contentType)) {
log.info("Session ID: {} Response - body: {}", sessionId, new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
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 223e24a..378e53d 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
@@ -1,9 +1,11 @@
package com.bpgroup.poc.admin.security;
+import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationEntryPoint;
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationFilter;
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationProvider;
import com.bpgroup.poc.admin.security.authentication.service.LoginService;
-import com.bpgroup.poc.admin.security.jwt.JwtTokenValidatorFilter;
+import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants;
+import com.bpgroup.poc.admin.security.jwt.JwtTokenValidateFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -48,7 +50,7 @@ public class SecurityConfig {
// 인증 설정
http.authorizeHttpRequests(c -> c
- .requestMatchers("/css/**", "/images/**", "/js/**", "/font/**").permitAll()
+ .requestMatchers("/css/**", "/images/**", "/js/**", "/font/**", "/favicon.ico").permitAll()
.requestMatchers("/common/modal/**").permitAll()
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
.anyRequest().authenticated());
@@ -56,13 +58,23 @@ public class SecurityConfig {
http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정
- http.addFilterAfter(new JwtTokenValidatorFilter(), BasicAuthenticationFilter.class);
+ http.addFilterAfter(new JwtTokenValidateFilter(), BasicAuthenticationFilter.class);
http.logout(c -> c
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
.logoutSuccessUrl(LOGIN_PATH)
);
+ http.exceptionHandling(c -> {
+ c.authenticationEntryPoint(new CustomAuthenticationEntryPoint()); // Authentication 실패 처리
+ });
+
+ http.logout(c -> c
+ .logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
+ .logoutSuccessUrl(LOGIN_PATH)
+ .deleteCookies(JwtTokenConstants.NAME)
+ );
+
return http.build();
}
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
new file mode 100644
index 0000000..0b7fd59
--- /dev/null
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationEntryPoint.java
@@ -0,0 +1,29 @@
+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.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+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.NAME);
+ if (jwtToken.isPresent()) {
+ 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 e52bac0..8ddccc2 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,10 +1,8 @@
package com.bpgroup.poc.admin.security.authentication;
import com.bpgroup.poc.admin.security.authentication.service.LoginResponse;
-import com.bpgroup.poc.admin.security.jwt.JwtConstants;
+import com.bpgroup.poc.admin.security.jwt.JwtTokenGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -12,7 +10,6 @@ import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
-import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -23,11 +20,8 @@ public class CustomAuthenticationSuccessHandler implements AuthenticationSuccess
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
- // JWT 토큰을 쿠키에 추가
- String jwtToken = createJwtToken(authentication);
- Cookie jwtCookie = new Cookie(JwtConstants.JWT_TOKEN_NAME, jwtToken);
- jwtCookie.setHttpOnly(true);
- jwtCookie.setPath("/");
+ String jwtToken = JwtTokenGenerator.generate(authentication.getName());
+ Cookie jwtCookie = JwtTokenGenerator.createJwtCookie(jwtToken);
response.addCookie(jwtCookie);
String jsonResponse = new ObjectMapper().writeValueAsString(
@@ -36,17 +30,4 @@ public class CustomAuthenticationSuccessHandler implements AuthenticationSuccess
response.getWriter().write(jsonResponse);
}
-
- private static String createJwtToken(Authentication authentication) {
- SecretKey key = Keys.hmacShaKeyFor(JwtConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
- return Jwts.builder()
- .issuer("BP Admin")
- .subject("Jwt Token")
- .claim("username", authentication.getName())
- .claim("details", authentication.getDetails())
- .issuedAt(new java.util.Date())
- .expiration(new java.util.Date(System.currentTimeMillis() + JwtConstants.EXPIRATION_TIME))
- .signWith(key)
- .compact();
- }
-}
+}
\ No newline at end of file
diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtConstants.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtConstants.java
deleted file mode 100644
index 0da505a..0000000
--- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtConstants.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.bpgroup.poc.admin.security.jwt;
-
-/**
- * TODO: 사용 시 별도 property 파일로 관리 필요
- */
-public class JwtConstants {
- public static final String JWT_KEY = "8530b13adb4e420d9694b27570635b47";
- public static final String JWT_TOKEN_NAME = "JWT-TOKEN";
- public static final long EXPIRATION_TIME = 60 * 10 * 1000;
-}
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
new file mode 100644
index 0000000..b69578c
--- /dev/null
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenConstants.java
@@ -0,0 +1,12 @@
+package com.bpgroup.poc.admin.security.jwt;
+
+/**
+ * TODO: 사용 시 별도 property 파일로 관리 필요
+ */
+public class JwtTokenConstants {
+ public static final String ISSUER = "BP";
+ public static final String SUBJECT = "Jwt Token";
+ public static final String KEY = "8530b13adb4e420d9694b27570635b47";
+ public static final String NAME = "JWT-TOKEN";
+ public static final long EXPIRATION_TIME = 30000;
+}
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
new file mode 100644
index 0000000..03ede9c
--- /dev/null
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenGenerator.java
@@ -0,0 +1,30 @@
+package com.bpgroup.poc.admin.security.jwt;
+
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import jakarta.servlet.http.Cookie;
+
+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();
+ }
+
+ public static Cookie createJwtCookie(String jwtToken) {
+ Cookie jwtCookie = new Cookie(JwtTokenConstants.NAME, jwtToken);
+ jwtCookie.setHttpOnly(true);
+ jwtCookie.setPath("/");
+ return jwtCookie;
+ }
+}
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
new file mode 100644
index 0000000..d62d4d4
--- /dev/null
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java
@@ -0,0 +1,57 @@
+package com.bpgroup.poc.admin.security.jwt;
+
+import com.bpgroup.poc.admin.common.CookieHelper;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+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 javax.crypto.SecretKey;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+@Slf4j
+public class JwtTokenValidateFilter extends OncePerRequestFilter {
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ try {
+ try {
+ Optional jwtToken = CookieHelper.getValueFromCookieWithName(JwtTokenConstants.NAME);
+ if (jwtToken.isPresent()) {
+ SecretKey key = Keys.hmacShaKeyFor(JwtTokenConstants.KEY.getBytes(StandardCharsets.UTF_8));
+
+ Claims claims = Jwts.parser()
+ .verifyWith(key)
+ .build()
+ .parseSignedClaims(jwtToken.get())
+ .getPayload();
+
+ String username = claims.get("username", String.class);
+ UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null);
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ }
+ } catch (Exception e) {
+ log.error("JWT validation failed: {}", e.getMessage());
+ }
+ } finally {
+ filterChain.doFilter(request, response);
+ }
+ }
+
+ @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));
+ }
+
+}
diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidatorFilter.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidatorFilter.java
deleted file mode 100644
index 3a8bcc6..0000000
--- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidatorFilter.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.bpgroup.poc.admin.security.jwt;
-
-import com.bpgroup.poc.admin.security.SecurityFilterConstants;
-import com.bpgroup.poc.admin.security.authentication.AuthenticationDetail;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.security.Keys;
-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.extern.slf4j.Slf4j;
-import org.springframework.security.authentication.BadCredentialsException;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import javax.crypto.SecretKey;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-@Slf4j
-public class JwtTokenValidatorFilter extends OncePerRequestFilter {
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- String jwt = getJwtFromCookie(request);
- if (null != jwt) {
- try {
- SecretKey key = Keys.hmacShaKeyFor(
- JwtConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
-
- Claims claims = Jwts.parser()
- .verifyWith(key)
- .build()
- .parseSignedClaims(jwt)
- .getPayload();
- String username = claims.get("username", String.class);
- String details = claims.get("details", String.class);
- UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null);
- auth.setDetails(AuthenticationDetail.fromJsonString(details));
- SecurityContextHolder.getContext().setAuthentication(auth);
- } catch (Exception e) {
- throw new BadCredentialsException("Invalid Jwt Token");
- }
- }
-
- filterChain.doFilter(request, response);
- }
-
- private static String getJwtFromCookie(HttpServletRequest request) {
- Cookie[] cookies = request.getCookies();
- if (null != cookies) {
- for (Cookie cookie : cookies) {
- if (JwtConstants.JWT_TOKEN_NAME.equals(cookie.getName())) {
- return cookie.getValue();
- }
- }
- }
-
- return null;
- }
-
- @Override
- protected boolean shouldNotFilter(HttpServletRequest request) {
- List paths = List.of(SecurityFilterConstants.EXCLUDE_FILTER_STARTS_WITH_URI);
- return paths.stream().anyMatch(request.getRequestURI()::startsWith);
- }
-
-}
diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/web/login/LoginController.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/web/login/LoginController.java
index 1a7ca47..1333b54 100644
--- a/poc/admin/src/main/java/com/bpgroup/poc/admin/web/login/LoginController.java
+++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/web/login/LoginController.java
@@ -1,15 +1,21 @@
package com.bpgroup.poc.admin.web.login;
import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/login")
public class LoginController {
@GetMapping
- public String loginPage() {
+ public String loginPage(@RequestParam(required = false) String error, Model model) {
+ if (error != null) {
+ model.addAttribute("error", error);
+ }
+
return "login/login";
}
diff --git a/poc/admin/src/main/resources/templates/login/login.html b/poc/admin/src/main/resources/templates/login/login.html
index adb85ad..9ffb11f 100644
--- a/poc/admin/src/main/resources/templates/login/login.html
+++ b/poc/admin/src/main/resources/templates/login/login.html
@@ -90,10 +90,10 @@