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