From 19f094b92f9b3c54c581827312560db869980565 Mon Sep 17 00:00:00 2001 From: suhyeon7497 Date: Sun, 29 Sep 2024 19:56:44 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[feat]=20cookie=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cookie에 있는 token을 검증하는 로직 관련 이슈: #2 --- .../security/config/SecurityConfig.java | 18 +++++- .../security/config/SecurityFilterConfig.java | 15 +++++ .../inplace/security/filter/JwtFilter.java | 58 +++++++++++++++++++ .../handler/CustomSuccessHandler.java | 2 +- .../team7/inplace/security/util/JwtUtil.java | 11 +++- .../video/presentation/VideoController.java | 10 ++-- 6 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 src/main/java/team7/inplace/security/config/SecurityFilterConfig.java create mode 100644 src/main/java/team7/inplace/security/filter/JwtFilter.java diff --git a/src/main/java/team7/inplace/security/config/SecurityConfig.java b/src/main/java/team7/inplace/security/config/SecurityConfig.java index c6b98853..99458616 100644 --- a/src/main/java/team7/inplace/security/config/SecurityConfig.java +++ b/src/main/java/team7/inplace/security/config/SecurityConfig.java @@ -8,7 +8,9 @@ import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import team7.inplace.security.application.CustomOAuth2UserService; +import team7.inplace.security.filter.JwtFilter; import team7.inplace.security.handler.CustomSuccessHandler; @Configuration @@ -17,15 +19,18 @@ public class SecurityConfig { private final CustomOAuth2UserService customOauth2UserService; private final CustomSuccessHandler customSuccessHandler; + private final JwtFilter jwtFilter; public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, - CustomSuccessHandler customSuccessHandler) { + CustomSuccessHandler customSuccessHandler, JwtFilter jwtFilter) { this.customOauth2UserService = customOAuth2UserService; this.customSuccessHandler = customSuccessHandler; + this.jwtFilter = jwtFilter; } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http) + throws Exception { //h2-console 접속 가능 http.headers((headers) -> headers.frameOptions(FrameOptionsConfig::sameOrigin)) @@ -36,13 +41,22 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) + + //authentication Service, Handler 설정 .oauth2Login((oauth2) -> oauth2 .userInfoEndpoint((userInfoEndPointConfig) -> userInfoEndPointConfig .userService(customOauth2UserService)).successHandler(customSuccessHandler)) + + //authentication Filter 설정 + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + + //authentication 경로 설정 .authorizeHttpRequests((auth) -> auth .requestMatchers("/login").permitAll() .requestMatchers("/hello").authenticated() .anyRequest().permitAll()) + + //session 설정 .sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); diff --git a/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java b/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java new file mode 100644 index 00000000..2dafce7f --- /dev/null +++ b/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java @@ -0,0 +1,15 @@ +package team7.inplace.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import team7.inplace.security.filter.JwtFilter; +import team7.inplace.security.util.JwtUtil; + +@Configuration +public class SecurityFilterConfig { + + @Bean + public JwtFilter jwtFilter(JwtUtil jwtUtil) { + return new JwtFilter(jwtUtil); + } +} diff --git a/src/main/java/team7/inplace/security/filter/JwtFilter.java b/src/main/java/team7/inplace/security/filter/JwtFilter.java new file mode 100644 index 00000000..bae59984 --- /dev/null +++ b/src/main/java/team7/inplace/security/filter/JwtFilter.java @@ -0,0 +1,58 @@ +package team7.inplace.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; +import team7.inplace.security.application.dto.CustomOAuth2User; +import team7.inplace.security.util.JwtUtil; + +public class JwtFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + public JwtFilter(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + Cookie authorizationCookie = getAuthorizationCookie(request); + + if (isCookieValidated(authorizationCookie)) { + filterChain.doFilter(request, response); + return; + } + + String token = authorizationCookie.getValue(); + String username = jwtUtil.getUsername(token); + CustomOAuth2User customOAuth2User = new CustomOAuth2User(username, null, null); + Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null); + SecurityContextHolder.getContext().setAuthentication(authToken); + filterChain.doFilter(request, response); + } + + private Cookie getAuthorizationCookie(HttpServletRequest request) { + return Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("Authorization")) + .findFirst().orElse(null); + } + + private boolean isCookieValidated(Cookie authorizationCookie) { + return isCookieEmpty(authorizationCookie) || jwtUtil.isExpired( + authorizationCookie.getValue()); + } + + private boolean isCookieEmpty(Cookie authorizationCookie) { + return authorizationCookie == null || authorizationCookie.getValue() == null; + } + +} diff --git a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java index 1e668599..40645836 100644 --- a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java +++ b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java @@ -32,7 +32,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo response.addCookie(accessTokenCookie); response.addCookie(refreshTokenCookie); - response.sendRedirect("http://localhost:8080/successCookie"); + response.sendRedirect("http://localhost:8080/auth"); } private Cookie createCookie(String key, String value) { diff --git a/src/main/java/team7/inplace/security/util/JwtUtil.java b/src/main/java/team7/inplace/security/util/JwtUtil.java index d5ac3ba8..77f562ab 100644 --- a/src/main/java/team7/inplace/security/util/JwtUtil.java +++ b/src/main/java/team7/inplace/security/util/JwtUtil.java @@ -25,16 +25,17 @@ public JwtUtil(JwtProperties jwtProperties) { } public String createAccessToken(String username) { - return createToken(username, accessTokenExpiredTime); + return createToken(username, accessTokenExpiredTime, "accessToken"); } public String createRefreshToken(String username) { - return createToken(username, refreshTokenExpiredTime); + return createToken(username, refreshTokenExpiredTime, "refreshToken"); } - private String createToken(String username, Long expiredTime) { + private String createToken(String username, Long expiredTime, String tokenType) { return Jwts.builder() .claim("username", username) + .claim("tokenType", tokenType) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expiredTime)) .signWith(secretKey) @@ -45,6 +46,10 @@ public String getUsername(String token) { return jwtParser.parseSignedClaims(token).getPayload().get("username", String.class); } + public String getTokenType(String token) { + return jwtParser.parseSignedClaims(token).getPayload().get("tokenType", String.class); + } + public Boolean isExpired(String token) { return jwtParser.parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } diff --git a/src/main/java/team7/inplace/video/presentation/VideoController.java b/src/main/java/team7/inplace/video/presentation/VideoController.java index 0a93add8..f19d8e8f 100644 --- a/src/main/java/team7/inplace/video/presentation/VideoController.java +++ b/src/main/java/team7/inplace/video/presentation/VideoController.java @@ -1,29 +1,29 @@ package team7.inplace.video.presentation; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import team7.inplace.video.application.VideoService; import team7.inplace.video.application.dto.VideoInfo; import team7.inplace.video.presentation.dto.VideoResponse; -import team7.inplace.video.application.VideoService; - -import java.util.List; @RestController @RequiredArgsConstructor public class VideoController { + private final VideoService videoService; // 내 인플루언서가 방문한 그 곳 ( 토큰 0 ) @GetMapping("/video") public ResponseEntity> readByInfluencer( - @RequestParam(name = "influencer", required = false) List influencers + @RequestParam(name = "influencer", required = false) List influencers ) { List videoInfos = videoService.findByInfluencer(influencers); - List videoResponses = videoInfos.stream().map(VideoResponse::new).toList(); + List videoResponses = videoInfos.stream().map(VideoResponse::of).toList(); return new ResponseEntity<>(videoResponses, HttpStatus.OK); } } From d239e4df8a4f8bf46e805b1d81e102ba6b99d42b Mon Sep 17 00:00:00 2001 From: suhyeon7497 Date: Sun, 29 Sep 2024 21:12:06 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[feat]=20=ED=98=84=EC=9E=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=ED=95=9C=20username=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재 로그인한 user의 username을 가져오는 기능 관련 이슈:#2 --- .../team7/inplace/security/filter/JwtFilter.java | 10 +++++++--- .../security/handler/CustomSuccessHandler.java | 5 +++++ .../inplace/security/util/AuthorizationUtil.java | 13 +++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/main/java/team7/inplace/security/util/AuthorizationUtil.java diff --git a/src/main/java/team7/inplace/security/filter/JwtFilter.java b/src/main/java/team7/inplace/security/filter/JwtFilter.java index bae59984..42613b67 100644 --- a/src/main/java/team7/inplace/security/filter/JwtFilter.java +++ b/src/main/java/team7/inplace/security/filter/JwtFilter.java @@ -27,7 +27,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse FilterChain filterChain) throws ServletException, IOException { Cookie authorizationCookie = getAuthorizationCookie(request); - if (isCookieValidated(authorizationCookie)) { + if (isCookieInvalid(authorizationCookie)) { filterChain.doFilter(request, response); return; } @@ -41,12 +41,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private Cookie getAuthorizationCookie(HttpServletRequest request) { - return Arrays.stream(request.getCookies()) + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + return Arrays.stream(cookies) .filter(cookie -> cookie.getName().equals("Authorization")) .findFirst().orElse(null); } - private boolean isCookieValidated(Cookie authorizationCookie) { + private boolean isCookieInvalid(Cookie authorizationCookie) { return isCookieEmpty(authorizationCookie) || jwtUtil.isExpired( authorizationCookie.getValue()); } diff --git a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java index 40645836..ff499b98 100644 --- a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java +++ b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java @@ -25,6 +25,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal(); String username = customOAuth2User.getName(); + addAccessAndRefreshTokenToResponse(response, username); + } + + private void addAccessAndRefreshTokenToResponse(HttpServletResponse response, String username) + throws IOException { Cookie accessTokenCookie = createCookie("access_token", jwtUtil.createAccessToken(username)); Cookie refreshTokenCookie = createCookie("refresh_token", diff --git a/src/main/java/team7/inplace/security/util/AuthorizationUtil.java b/src/main/java/team7/inplace/security/util/AuthorizationUtil.java new file mode 100644 index 00000000..e4579a25 --- /dev/null +++ b/src/main/java/team7/inplace/security/util/AuthorizationUtil.java @@ -0,0 +1,13 @@ +package team7.inplace.security.util; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class AuthorizationUtil { + + public static String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication.getName(); + } + +} From 7def8d672f38bed728959b8d6e211a98888a59b7 Mon Sep 17 00:00:00 2001 From: suhyeon7497 Date: Mon, 30 Sep 2024 16:24:07 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[feat]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20userid=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로그인한 유저의 userId는 Authorization.getUserId()로 가져옵니다 관련 이슈: #2 --- .../application/dto/CustomOAuth2User.java | 4 +++- .../team7/inplace/security/filter/JwtFilter.java | 11 +++++++---- .../security/handler/CustomSuccessHandler.java | 12 +++++++----- .../inplace/security/util/AuthorizationUtil.java | 12 +++++++++--- .../java/team7/inplace/security/util/JwtUtil.java | 15 ++++++++++----- 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/main/java/team7/inplace/security/application/dto/CustomOAuth2User.java b/src/main/java/team7/inplace/security/application/dto/CustomOAuth2User.java index 03892c7a..1ba01008 100644 --- a/src/main/java/team7/inplace/security/application/dto/CustomOAuth2User.java +++ b/src/main/java/team7/inplace/security/application/dto/CustomOAuth2User.java @@ -9,6 +9,7 @@ public record CustomOAuth2User( String username, + Long id, String nickname, UserType userType ) implements OAuth2User { @@ -29,6 +30,7 @@ public String getName() { } public static CustomOAuth2User of(User user) { - return new CustomOAuth2User(user.getUsername(), user.getNickname(), user.getUserType()); + return new CustomOAuth2User(user.getUsername(), user.getId(), user.getNickname(), + user.getUserType()); } } diff --git a/src/main/java/team7/inplace/security/filter/JwtFilter.java b/src/main/java/team7/inplace/security/filter/JwtFilter.java index 42613b67..d7e46890 100644 --- a/src/main/java/team7/inplace/security/filter/JwtFilter.java +++ b/src/main/java/team7/inplace/security/filter/JwtFilter.java @@ -26,18 +26,21 @@ public JwtFilter(JwtUtil jwtUtil) { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Cookie authorizationCookie = getAuthorizationCookie(request); - if (isCookieInvalid(authorizationCookie)) { filterChain.doFilter(request, response); return; } - String token = authorizationCookie.getValue(); + addUserToAuthentication(token); + filterChain.doFilter(request, response); + } + + private void addUserToAuthentication(String token) { String username = jwtUtil.getUsername(token); - CustomOAuth2User customOAuth2User = new CustomOAuth2User(username, null, null); + Long id = jwtUtil.getId(token); + CustomOAuth2User customOAuth2User = new CustomOAuth2User(username, id, null, null); Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null); SecurityContextHolder.getContext().setAuthentication(authToken); - filterChain.doFilter(request, response); } private Cookie getAuthorizationCookie(HttpServletRequest request) { diff --git a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java index ff499b98..caf1d18d 100644 --- a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java +++ b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java @@ -24,16 +24,18 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo throws IOException, ServletException { CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal(); - String username = customOAuth2User.getName(); - addAccessAndRefreshTokenToResponse(response, username); + addAccessAndRefreshTokenToResponse(response, customOAuth2User); } - private void addAccessAndRefreshTokenToResponse(HttpServletResponse response, String username) + private void addAccessAndRefreshTokenToResponse(HttpServletResponse response, + CustomOAuth2User customOAuth2User) throws IOException { + String username = customOAuth2User.getName(); + Long userId = customOAuth2User.id(); Cookie accessTokenCookie = createCookie("access_token", - jwtUtil.createAccessToken(username)); + jwtUtil.createAccessToken(username, userId)); Cookie refreshTokenCookie = createCookie("refresh_token", - jwtUtil.createRefreshToken(username)); + jwtUtil.createRefreshToken(username, userId)); response.addCookie(accessTokenCookie); response.addCookie(refreshTokenCookie); diff --git a/src/main/java/team7/inplace/security/util/AuthorizationUtil.java b/src/main/java/team7/inplace/security/util/AuthorizationUtil.java index e4579a25..0e2f441c 100644 --- a/src/main/java/team7/inplace/security/util/AuthorizationUtil.java +++ b/src/main/java/team7/inplace/security/util/AuthorizationUtil.java @@ -1,13 +1,19 @@ package team7.inplace.security.util; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import team7.inplace.security.application.dto.CustomOAuth2User; public class AuthorizationUtil { public static String getUsername() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return authentication.getName(); + CustomOAuth2User customOAuth2User = (CustomOAuth2User) SecurityContextHolder.getContext() + .getAuthentication().getPrincipal(); + return customOAuth2User.getName(); } + public static Long getUserId() { + CustomOAuth2User customOAuth2User = (CustomOAuth2User) SecurityContextHolder.getContext() + .getAuthentication().getPrincipal(); + return customOAuth2User.id(); + } } diff --git a/src/main/java/team7/inplace/security/util/JwtUtil.java b/src/main/java/team7/inplace/security/util/JwtUtil.java index 77f562ab..bf051937 100644 --- a/src/main/java/team7/inplace/security/util/JwtUtil.java +++ b/src/main/java/team7/inplace/security/util/JwtUtil.java @@ -24,17 +24,18 @@ public JwtUtil(JwtProperties jwtProperties) { this.refreshTokenExpiredTime = jwtProperties.refreshTokenExpiredTime(); } - public String createAccessToken(String username) { - return createToken(username, accessTokenExpiredTime, "accessToken"); + public String createAccessToken(String username, Long userId) { + return createToken(username, userId, "accessToken", accessTokenExpiredTime); } - public String createRefreshToken(String username) { - return createToken(username, refreshTokenExpiredTime, "refreshToken"); + public String createRefreshToken(String username, Long userId) { + return createToken(username, userId, "refreshToken", refreshTokenExpiredTime); } - private String createToken(String username, Long expiredTime, String tokenType) { + private String createToken(String username, Long userId, String tokenType, Long expiredTime) { return Jwts.builder() .claim("username", username) + .claim("id", userId) .claim("tokenType", tokenType) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expiredTime)) @@ -50,6 +51,10 @@ public String getTokenType(String token) { return jwtParser.parseSignedClaims(token).getPayload().get("tokenType", String.class); } + public Long getId(String token) { + return jwtParser.parseSignedClaims(token).getPayload().get("id", Long.class); + } + public Boolean isExpired(String token) { return jwtParser.parseSignedClaims(token).getPayload().getExpiration().before(new Date()); } From 71e6af48d6dc3d71a45448edf9a5020829fa0a6e Mon Sep 17 00:00:00 2001 From: suhyeon7497 Date: Thu, 3 Oct 2024 14:11:21 +0900 Subject: [PATCH 4/6] [feat] authorzation exception handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exception handling을 위해 작성하였습니다. 관련 이슈: #2 --- .../security/AuthorizationErrorCode.java | 16 ++++++ .../security/AuthorizationException.java | 18 +++++++ .../security/config/SecurityConfig.java | 16 +++--- .../security/config/SecurityFilterConfig.java | 12 +++-- ...wtFilter.java => AuthorizationFilter.java} | 49 +++++++++++-------- .../filter/ExceptionHandlingFilter.java | 39 +++++++++++++++ .../handler/CustomSuccessHandler.java | 8 +-- .../team7/inplace/security/util/JwtUtil.java | 27 +++++++--- 8 files changed, 146 insertions(+), 39 deletions(-) create mode 100644 src/main/java/team7/inplace/security/AuthorizationErrorCode.java create mode 100644 src/main/java/team7/inplace/security/AuthorizationException.java rename src/main/java/team7/inplace/security/filter/{JwtFilter.java => AuthorizationFilter.java} (53%) create mode 100644 src/main/java/team7/inplace/security/filter/ExceptionHandlingFilter.java diff --git a/src/main/java/team7/inplace/security/AuthorizationErrorCode.java b/src/main/java/team7/inplace/security/AuthorizationErrorCode.java new file mode 100644 index 00000000..5f4f9eb4 --- /dev/null +++ b/src/main/java/team7/inplace/security/AuthorizationErrorCode.java @@ -0,0 +1,16 @@ +package team7.inplace.security; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum AuthorizationErrorCode { + TOKEN_IS_EMPTY(HttpStatus.BAD_REQUEST, "Token is Empty"), + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "Invalid Token"), + TOKEN_IS_EXPIRED(HttpStatus.BAD_REQUEST, "Token is Expired"); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/team7/inplace/security/AuthorizationException.java b/src/main/java/team7/inplace/security/AuthorizationException.java new file mode 100644 index 00000000..73927334 --- /dev/null +++ b/src/main/java/team7/inplace/security/AuthorizationException.java @@ -0,0 +1,18 @@ +package team7.inplace.security; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +public class AuthorizationException extends RuntimeException { + + private final AuthorizationErrorCode errorCode; + + public HttpStatus getHttpStatus() { + return errorCode.getHttpStatus(); + } + + public String getMessage() { + return errorCode.getMessage(); + } +} diff --git a/src/main/java/team7/inplace/security/config/SecurityConfig.java b/src/main/java/team7/inplace/security/config/SecurityConfig.java index 99458616..53ad1a1e 100644 --- a/src/main/java/team7/inplace/security/config/SecurityConfig.java +++ b/src/main/java/team7/inplace/security/config/SecurityConfig.java @@ -10,7 +10,8 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import team7.inplace.security.application.CustomOAuth2UserService; -import team7.inplace.security.filter.JwtFilter; +import team7.inplace.security.filter.AuthorizationFilter; +import team7.inplace.security.filter.ExceptionHandlingFilter; import team7.inplace.security.handler.CustomSuccessHandler; @Configuration @@ -19,13 +20,16 @@ public class SecurityConfig { private final CustomOAuth2UserService customOauth2UserService; private final CustomSuccessHandler customSuccessHandler; - private final JwtFilter jwtFilter; + private final AuthorizationFilter authorizationFilter; + private final ExceptionHandlingFilter exceptionHandlingFilter; public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, - CustomSuccessHandler customSuccessHandler, JwtFilter jwtFilter) { + CustomSuccessHandler customSuccessHandler, AuthorizationFilter authorizationFilter, + ExceptionHandlingFilter exceptionHandlingFilter) { this.customOauth2UserService = customOAuth2UserService; this.customSuccessHandler = customSuccessHandler; - this.jwtFilter = jwtFilter; + this.authorizationFilter = authorizationFilter; + this.exceptionHandlingFilter = exceptionHandlingFilter; } @Bean @@ -48,8 +52,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) .userService(customOauth2UserService)).successHandler(customSuccessHandler)) //authentication Filter 설정 - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - + .addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(exceptionHandlingFilter, AuthorizationFilter.class) //authentication 경로 설정 .authorizeHttpRequests((auth) -> auth .requestMatchers("/login").permitAll() diff --git a/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java b/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java index 2dafce7f..9116c64c 100644 --- a/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java +++ b/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java @@ -2,14 +2,20 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import team7.inplace.security.filter.JwtFilter; +import team7.inplace.security.filter.AuthorizationFilter; +import team7.inplace.security.filter.ExceptionHandlingFilter; import team7.inplace.security.util.JwtUtil; @Configuration public class SecurityFilterConfig { @Bean - public JwtFilter jwtFilter(JwtUtil jwtUtil) { - return new JwtFilter(jwtUtil); + public AuthorizationFilter authorizationFilter(JwtUtil jwtUtil) { + return new AuthorizationFilter(jwtUtil); + } + + @Bean + public ExceptionHandlingFilter exceptionHandlingFilter() { + return new ExceptionHandlingFilter(); } } diff --git a/src/main/java/team7/inplace/security/filter/JwtFilter.java b/src/main/java/team7/inplace/security/filter/AuthorizationFilter.java similarity index 53% rename from src/main/java/team7/inplace/security/filter/JwtFilter.java rename to src/main/java/team7/inplace/security/filter/AuthorizationFilter.java index d7e46890..c05b5fd5 100644 --- a/src/main/java/team7/inplace/security/filter/JwtFilter.java +++ b/src/main/java/team7/inplace/security/filter/AuthorizationFilter.java @@ -7,35 +7,48 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; +import java.util.Objects; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; +import team7.inplace.security.AuthorizationErrorCode; +import team7.inplace.security.AuthorizationException; import team7.inplace.security.application.dto.CustomOAuth2User; import team7.inplace.security.util.JwtUtil; -public class JwtFilter extends OncePerRequestFilter { +public class AuthorizationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; - public JwtFilter(JwtUtil jwtUtil) { + public AuthorizationFilter(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - Cookie authorizationCookie = getAuthorizationCookie(request); - if (isCookieInvalid(authorizationCookie)) { + FilterChain filterChain) + throws ServletException, IOException { + Cookie[] cookies = request.getCookies(); + if (Objects.isNull(cookies)) { filterChain.doFilter(request, response); return; } - String token = authorizationCookie.getValue(); + String token = getTokenCookie(cookies).getValue(); addUserToAuthentication(token); filterChain.doFilter(request, response); } - private void addUserToAuthentication(String token) { + private Cookie getTokenCookie(Cookie[] cookies) throws AuthorizationException { + Cookie tokenCookie = Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals("Authorization")) + .findFirst() + .orElseThrow(() -> new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EMPTY)); + validateToken(tokenCookie); + return tokenCookie; + } + + private void addUserToAuthentication(String token) throws AuthorizationException { String username = jwtUtil.getUsername(token); Long id = jwtUtil.getId(token); CustomOAuth2User customOAuth2User = new CustomOAuth2User(username, id, null, null); @@ -43,23 +56,17 @@ private void addUserToAuthentication(String token) { SecurityContextHolder.getContext().setAuthentication(authToken); } - private Cookie getAuthorizationCookie(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return null; + private void validateToken(Cookie authorizationCookie) throws AuthorizationException { + if (isTokenEmpty(authorizationCookie)) { + throw new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EMPTY); + } + if (jwtUtil.isExpired(authorizationCookie.getValue())) { + throw new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EXPIRED); } - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals("Authorization")) - .findFirst().orElse(null); - } - - private boolean isCookieInvalid(Cookie authorizationCookie) { - return isCookieEmpty(authorizationCookie) || jwtUtil.isExpired( - authorizationCookie.getValue()); } - private boolean isCookieEmpty(Cookie authorizationCookie) { - return authorizationCookie == null || authorizationCookie.getValue() == null; + private boolean isTokenEmpty(Cookie authorizationCookie) { + return authorizationCookie.getValue() == null; } } diff --git a/src/main/java/team7/inplace/security/filter/ExceptionHandlingFilter.java b/src/main/java/team7/inplace/security/filter/ExceptionHandlingFilter.java new file mode 100644 index 00000000..34f08c20 --- /dev/null +++ b/src/main/java/team7/inplace/security/filter/ExceptionHandlingFilter.java @@ -0,0 +1,39 @@ +package team7.inplace.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.NoArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ProblemDetail; +import org.springframework.web.filter.OncePerRequestFilter; +import team7.inplace.security.AuthorizationException; + +@NoArgsConstructor +public class ExceptionHandlingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (AuthorizationException authorizationException) { + setErrorResponse(response, authorizationException); + } + } + + private void setErrorResponse(HttpServletResponse response, + AuthorizationException authorizationException) + throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + response.setStatus(authorizationException.getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( + authorizationException.getHttpStatus(), authorizationException.getMessage()); + response.getWriter().write(objectMapper.writeValueAsString(problemDetail)); + } +} diff --git a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java index caf1d18d..54aea242 100644 --- a/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java +++ b/src/main/java/team7/inplace/security/handler/CustomSuccessHandler.java @@ -1,6 +1,5 @@ package team7.inplace.security.handler; -import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -21,10 +20,10 @@ public CustomSuccessHandler(JwtUtil jwtUtil) { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) - throws IOException, ServletException { - + throws IOException { CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal(); addAccessAndRefreshTokenToResponse(response, customOAuth2User); + setRedirectUrlToResponse(response); } private void addAccessAndRefreshTokenToResponse(HttpServletResponse response, @@ -39,6 +38,9 @@ private void addAccessAndRefreshTokenToResponse(HttpServletResponse response, response.addCookie(accessTokenCookie); response.addCookie(refreshTokenCookie); + } + + private void setRedirectUrlToResponse(HttpServletResponse response) throws IOException { response.sendRedirect("http://localhost:8080/auth"); } diff --git a/src/main/java/team7/inplace/security/util/JwtUtil.java b/src/main/java/team7/inplace/security/util/JwtUtil.java index bf051937..ae2f3048 100644 --- a/src/main/java/team7/inplace/security/util/JwtUtil.java +++ b/src/main/java/team7/inplace/security/util/JwtUtil.java @@ -1,5 +1,6 @@ package team7.inplace.security.util; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts.SIG; @@ -7,6 +8,8 @@ import java.util.Date; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import team7.inplace.security.AuthorizationErrorCode; +import team7.inplace.security.AuthorizationException; import team7.inplace.security.config.JwtProperties; public class JwtUtil { @@ -43,16 +46,28 @@ private String createToken(String username, Long userId, String tokenType, Long .compact(); } - public String getUsername(String token) { - return jwtParser.parseSignedClaims(token).getPayload().get("username", String.class); + public String getUsername(String token) throws AuthorizationException { + try { + return jwtParser.parseSignedClaims(token).getPayload().get("username", String.class); + } catch (JwtException | IllegalArgumentException e) { + throw new AuthorizationException(AuthorizationErrorCode.INVALID_TOKEN); + } } - public String getTokenType(String token) { - return jwtParser.parseSignedClaims(token).getPayload().get("tokenType", String.class); + public String getTokenType(String token) throws AuthorizationException { + try { + return jwtParser.parseSignedClaims(token).getPayload().get("tokenType", String.class); + } catch (JwtException | IllegalArgumentException e) { + throw new AuthorizationException(AuthorizationErrorCode.INVALID_TOKEN); + } } - public Long getId(String token) { - return jwtParser.parseSignedClaims(token).getPayload().get("id", Long.class); + public Long getId(String token) throws AuthorizationException { + try { + return jwtParser.parseSignedClaims(token).getPayload().get("id", Long.class); + } catch (JwtException | IllegalArgumentException e) { + throw new AuthorizationException(AuthorizationErrorCode.INVALID_TOKEN); + } } public Boolean isExpired(String token) { From 9ad712e9a92fed8d2b0929319edd3e493286d62e Mon Sep 17 00:00:00 2001 From: suhyeon7497 Date: Thu, 3 Oct 2024 17:00:30 +0900 Subject: [PATCH 5/6] [test] jwtUtil Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jwtUtil test를 추가하였습니다. 관련 이슈 : #2 --- .../security/config/SecurityFilterConfig.java | 2 + .../security/filter/AuthorizationFilter.java | 14 ++-- .../team7/inplace/security/util/JwtUtil.java | 11 ++- .../repository/VideoRepositoryTest.java | 80 ------------------ .../VideoTest/service/VideoServiceTest.java | 81 ------------------- .../inplace/security/util/JwtUtilTest.java | 79 ++++++++++++++++++ 6 files changed, 96 insertions(+), 171 deletions(-) delete mode 100644 src/test/java/team7/inplace/VideoTest/repository/VideoRepositoryTest.java delete mode 100644 src/test/java/team7/inplace/VideoTest/service/VideoServiceTest.java create mode 100644 src/test/java/team7/inplace/security/util/JwtUtilTest.java diff --git a/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java b/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java index 9116c64c..e77a4b07 100644 --- a/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java +++ b/src/main/java/team7/inplace/security/config/SecurityFilterConfig.java @@ -2,10 +2,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; import team7.inplace.security.filter.AuthorizationFilter; import team7.inplace.security.filter.ExceptionHandlingFilter; import team7.inplace.security.util.JwtUtil; +@Component @Configuration public class SecurityFilterConfig { diff --git a/src/main/java/team7/inplace/security/filter/AuthorizationFilter.java b/src/main/java/team7/inplace/security/filter/AuthorizationFilter.java index c05b5fd5..ec92e883 100644 --- a/src/main/java/team7/inplace/security/filter/AuthorizationFilter.java +++ b/src/main/java/team7/inplace/security/filter/AuthorizationFilter.java @@ -57,16 +57,14 @@ private void addUserToAuthentication(String token) throws AuthorizationException } private void validateToken(Cookie authorizationCookie) throws AuthorizationException { - if (isTokenEmpty(authorizationCookie)) { - throw new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EMPTY); - } - if (jwtUtil.isExpired(authorizationCookie.getValue())) { - throw new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EXPIRED); - } + validateTokenEmpty(authorizationCookie); + jwtUtil.validateExpired(authorizationCookie.getValue()); } - private boolean isTokenEmpty(Cookie authorizationCookie) { - return authorizationCookie.getValue() == null; + private void validateTokenEmpty(Cookie authorizationCookie) { + if (authorizationCookie.getValue() == null) { + throw new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EMPTY); + } } } diff --git a/src/main/java/team7/inplace/security/util/JwtUtil.java b/src/main/java/team7/inplace/security/util/JwtUtil.java index ae2f3048..2c3ef7e1 100644 --- a/src/main/java/team7/inplace/security/util/JwtUtil.java +++ b/src/main/java/team7/inplace/security/util/JwtUtil.java @@ -1,5 +1,6 @@ package team7.inplace.security.util; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; @@ -70,7 +71,13 @@ public Long getId(String token) throws AuthorizationException { } } - public Boolean isExpired(String token) { - return jwtParser.parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + public void validateExpired(String token) throws ExpiredJwtException { + try { + jwtParser.parseSignedClaims(token).getPayload().getExpiration(); + } catch (ExpiredJwtException e) { + throw new AuthorizationException(AuthorizationErrorCode.TOKEN_IS_EXPIRED); + } catch (JwtException | IllegalArgumentException e) { + throw new AuthorizationException(AuthorizationErrorCode.INVALID_TOKEN); + } } } diff --git a/src/test/java/team7/inplace/VideoTest/repository/VideoRepositoryTest.java b/src/test/java/team7/inplace/VideoTest/repository/VideoRepositoryTest.java deleted file mode 100644 index 1333fa81..00000000 --- a/src/test/java/team7/inplace/VideoTest/repository/VideoRepositoryTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package team7.inplace.VideoTest.repository; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.transaction.Transactional; -import java.util.ArrayList; -import java.util.List; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.annotation.DirtiesContext; -import team7.inplace.influencer.domain.Influencer; -import team7.inplace.place.domain.Category; -import team7.inplace.place.domain.Place; -import team7.inplace.video.domain.Video; -import team7.inplace.video.persistence.VideoRepository; - -@DataJpaTest -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) // 각 메서드 실행마다 이전 결과 초기화 -public class VideoRepositoryTest { - @PersistenceContext - EntityManager entityManager; - @Autowired - private VideoRepository videoRepository; - - @BeforeEach - @Transactional - void init() { - Place place = Place.builder() - .name("Test Place") - .pet(false) - .wifi(true) - .parking(false) - .fordisabled(true) - .nursery(false) - .smokingroom(false) - .address1("Address 1") - .address2("Address 2") - .address3("Address 3") - .menuImgUrl("menu.jpg") - .category(Category.CAFE) - .longitude("127.0") - .latitude("37.0") - .build(); - entityManager.persist(place); - - Influencer influencer1 = new Influencer("name1", "job1", "imgUrl"); - Influencer influencer2 = new Influencer("name2", "job2", "imgUrl"); - entityManager.persist(influencer1); - entityManager.persist(influencer2); - - Video video1 = new Video("url1", influencer1, place); - Video video2 = new Video("url2", influencer1, place); - Video video3 = new Video("url3", influencer1, place); - Video video4 = new Video("url4", influencer2, place); - Video video5 = new Video("url5", influencer2, place); - entityManager.persist(video1); - entityManager.persist(video2); - entityManager.persist(video3); - entityManager.persist(video4); - entityManager.persist(video5); - } - - @Test - @DisplayName("findVideosByInfluencerIdIn Test") - void test1() { - // given - /* Before Each */ - // when - List influencerIds = new ArrayList<>(); - influencerIds.add(1L); - - List