diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77d85e1..e08469a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: CI / CD on: push: - branches: [main] + branches: [feat/#85-logout] jobs: CI: diff --git a/src/main/java/com/opendata/docs/LogoutControllerDocs.java b/src/main/java/com/opendata/docs/LogoutControllerDocs.java new file mode 100644 index 0000000..0213610 --- /dev/null +++ b/src/main/java/com/opendata/docs/LogoutControllerDocs.java @@ -0,0 +1,20 @@ +package com.opendata.docs; + + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "로그아웃 API") +public interface LogoutControllerDocs { + @Operation(summary = "로그아웃") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃 성공"), + @ApiResponse(responseCode = "500", description = "서버 에러") + }) + ResponseEntity doLogout(); +} + diff --git a/src/main/java/com/opendata/domain/user/controller/LogoutController.java b/src/main/java/com/opendata/domain/user/controller/LogoutController.java new file mode 100644 index 0000000..0be0dba --- /dev/null +++ b/src/main/java/com/opendata/domain/user/controller/LogoutController.java @@ -0,0 +1,19 @@ +package com.opendata.domain.user.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.opendata.docs.LogoutControllerDocs; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class LogoutController implements LogoutControllerDocs { + + @PostMapping("/logout") + public ResponseEntity doLogout(){ + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/opendata/global/config/SecurityConfig.java b/src/main/java/com/opendata/global/config/SecurityConfig.java index e7f9401..f534a0f 100644 --- a/src/main/java/com/opendata/global/config/SecurityConfig.java +++ b/src/main/java/com/opendata/global/config/SecurityConfig.java @@ -7,6 +7,7 @@ import com.opendata.domain.oauth2.service.CustomOAuth2UserService; import com.opendata.domain.user.repository.UserRepository; import com.opendata.global.jwt.CookieUtil; +import com.opendata.global.jwt.CustomLogoutFilter; import com.opendata.global.jwt.JwtFilter; import com.opendata.global.jwt.JwtUtil; import com.opendata.global.jwt.LoginFilter; @@ -29,6 +30,7 @@ import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -111,8 +113,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .successHandler(customSuccessHandler) ); - http - .addFilterBefore(new JwtFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new JwtFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class); + + http.addFilterBefore(new CustomLogoutFilter(jwtUtil), LogoutFilter.class); // .addFilterBefore(oAuth2RedirectUriCookieFilter, OAuth2AuthorizationRequestRedirectFilter.class); // .addFilterAfter(new JwtFilter(jwtUtil,userDetailsService), OAuth2LoginAuthenticationFilter.class); // .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, cookieUtil, userRepository), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/opendata/global/jwt/CustomLogoutFilter.java b/src/main/java/com/opendata/global/jwt/CustomLogoutFilter.java new file mode 100644 index 0000000..637aa3b --- /dev/null +++ b/src/main/java/com/opendata/global/jwt/CustomLogoutFilter.java @@ -0,0 +1,70 @@ +package com.opendata.global.jwt; + +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +@RequiredArgsConstructor +public class CustomLogoutFilter extends GenericFilterBean { + + private final JwtUtil jwtUtil; + + @Override + public void doFilter( + jakarta.servlet.ServletRequest servletRequest, + jakarta.servlet.ServletResponse servletResponse, + FilterChain filterChain + ) throws IOException, ServletException { + doFilter((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, filterChain); + } + + private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + String requestUri = request.getRequestURI(); + if (!requestUri.equals("/logout") && !requestUri.equals("/api/logout")) { + chain.doFilter(request, response); + return; + } + + String requestMethod = request.getMethod(); + if (!requestMethod.equals("POST")) { + chain.doFilter(request, response); + return; + } + + String refresh = CookieUtil.getRefreshTokenFromRequest(request).orElse(null); + + if (refresh == null) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + try { + jwtUtil.isExpired(refresh); + } catch (ExpiredJwtException e) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + String category = jwtUtil.getCategory(refresh); + if (!"refresh".equals(category)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + String domain = ".yourse-seoul.com"; + response.addHeader("Set-Cookie", + "refresh=; Max-Age=0; Path=/; Domain=" + domain + "; HttpOnly; Secure; SameSite=Strict"); + response.addHeader("Set-Cookie", + "access=; Max-Age=0; Path=/; Domain=" + domain + "; HttpOnly; Secure; SameSite=Strict"); + + response.setStatus(HttpServletResponse.SC_OK); + } +} diff --git a/src/main/java/com/opendata/global/jwt/JwtUtil.java b/src/main/java/com/opendata/global/jwt/JwtUtil.java index aa6144e..d552073 100644 --- a/src/main/java/com/opendata/global/jwt/JwtUtil.java +++ b/src/main/java/com/opendata/global/jwt/JwtUtil.java @@ -54,6 +54,10 @@ public String getRole(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload() .get("role", String.class); } + public String getCategory(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload() + .get("category", String.class); + }