diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index 5bd6adb4..156cba96 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -1,8 +1,6 @@ package poomasi.domain.auth.config; -import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,22 +13,23 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +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.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import poomasi.domain.auth.security.filter.JwtLogoutFilter; import poomasi.domain.auth.security.filter.CustomUsernamePasswordAuthenticationFilter; import poomasi.domain.auth.security.filter.JwtAuthenticationFilter; -import poomasi.domain.auth.security.handler.CustomSuccessHandler; +import poomasi.domain.auth.security.handler.OAuth2FailureHandler; +import poomasi.domain.auth.security.handler.OAuth2SuccessHandler; import poomasi.domain.auth.security.userdetail.OAuth2UserDetailServiceImpl; import poomasi.domain.auth.security.userdetail.UserDetailsServiceImpl; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; +import poomasi.domain.auth.token.refreshtoken.service.RefreshTokenService; import poomasi.domain.auth.token.util.JwtUtil; -import java.util.Arrays; -import java.util.Collections; - @AllArgsConstructor @Configuration @@ -41,26 +40,37 @@ public class SecurityConfig { private final AuthenticationConfiguration authenticationConfiguration; private final JwtUtil jwtUtil; private final MvcRequestMatcher.Builder mvc; - private final CustomSuccessHandler customSuccessHandler; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; private final UserDetailsServiceImpl userDetailsService; private final CorsConfigurationSource corsConfigurationSource; + private final BlacklistJpaService blacklistService; + private final RefreshTokenService refreshTokenService; @Autowired private OAuth2UserDetailServiceImpl oAuth2UserDetailServiceImpl; + + + /*@Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring() + .requestMatchers("/error"); + } +*/ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } - @Description("순서 : Oauth2 -> jwt -> login -> logout") + @Description("순서 : logout -> Oauth2 -> jwt -> login ") @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - //form login disable + //기본 폼로그인 비활성화 http.formLogin(AbstractHttpConfigurer::disable); - //basic login disable + //http basic 비활성화 http.httpBasic(AbstractHttpConfigurer::disable); //csrf 해제 @@ -74,58 +84,65 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - //기본 로그아웃 해제 + //기본 로그아웃 비활성화 http.logout(AbstractHttpConfigurer::disable); - // 기본 경로 및 테스트 경로 http.authorizeHttpRequests((authorize) -> authorize + + + // 기본 경로 및 테스트 경로 + // 인증 및 인가가 필요한 부분을 "authenticated"로 표시해주세요 + + //건호 api + .requestMatchers("/oauth2/authentication/kakao").authenticated() + .requestMatchers("api/order/**").authenticated() + .requestMatchers(HttpMethod.POST, "api/logout").authenticated() + + //진택 api + .requestMatchers(HttpMethod.POST, "/api/member/update/**").authenticated() + .requestMatchers(HttpMethod.GET, "/api/member/**").authenticated() + .requestMatchers(HttpMethod.POST, "/api/reiusse").authenticated() + + //지민 api + .requestMatchers(HttpMethod.GET, "/api/image/**").permitAll() + + //풍헌 api + + /*.requestMatchers(HttpMethod.POST, "/api/farm/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/review/**").permitAll() .requestMatchers(HttpMethod.GET, "/health").permitAll() .requestMatchers(HttpMethod.GET, "/api/image/**").permitAll() - .requestMatchers("/api/farm/**", "/api/member/sign-up", "/api/login", "api/reissue", "api/payment/**", "api/order/**", "api/reservation/**", "/api/v1/farmer/reservations", "/api/v1/biz/farmer/**").permitAll() + .requestMatchers("/api/member/sign-up", "/api/login", "/api/reissue", "/api/payment/**", "/api/order/**", "api/reservation/**", "/api/v1/farmer/reservations").permitAll() .requestMatchers("/api/need-auth/**").authenticated() - .anyRequest(). - authenticated() + .requestMatchers("/api/logout").permitAll()*/ + .anyRequest().permitAll() ); - - //endpoint : {domain}/oauth2/authentication/kakao -// http -// .oauth2Login((oauth2) -> oauth2 -// .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig -// .userService(oAuth2UserDetailServiceImpl)) -// .successHandler(customSuccessHandler) -// ); - + //oauth2 filter + http + .oauth2Login((oauth2) -> oauth2 + .userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig + .userService(oAuth2UserDetailServiceImpl) + ) + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler) + ); + + //username password filter CustomUsernamePasswordAuthenticationFilter customUsernameFilter = - new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil); + new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenService); customUsernameFilter.setFilterProcessesUrl("/api/login"); - http.addFilterAt(customUsernameFilter, UsernamePasswordAuthenticationFilter.class); - http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class); - - /* - 로그아웃 필터 등록하기 - LogoutHandler[] handlers = { - new CookieClearingLogoutHandler(), - new ClearAuthenticationHandler() - }; - CustomLogoutFilter customLogoutFilter = new CustomLogoutFilter(jwtUtil, new CustomLogoutSuccessHandler(), handlers); - customLogoutFilter.setFilterProcessesUrl("/api/logout"); - customLogoutFilter. - http.addFilterAt(customLogoutFilter, LogoutFilter.class); - - http.logout( (logout) -> - logout. - logoutSuccessHandler(new CustomLogoutSuccessHandler()) - .addLogoutHandler(new CookieClearingLogoutHandler()) - .addLogoutHandler(new ClearAuthenticationHandler()) - ); - */ - //http.addFilterAfter(customLogoutFilter, JwtAuthenticationFilter.class); + //jwt filter + http.addFilterAfter(new JwtAuthenticationFilter(jwtUtil, userDetailsService, blacklistService), + OAuth2LoginAuthenticationFilter.class); + + //logout filter + JwtLogoutFilter customLogoutFilter = new JwtLogoutFilter(jwtUtil, blacklistService, refreshTokenService); + http.addFilterAfter(customLogoutFilter, JwtAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/poomasi/domain/auth/security/AuthTestController.java b/src/main/java/poomasi/domain/auth/security/AuthTestController.java index 3694523e..8b9a5516 100644 --- a/src/main/java/poomasi/domain/auth/security/AuthTestController.java +++ b/src/main/java/poomasi/domain/auth/security/AuthTestController.java @@ -50,4 +50,4 @@ public String Test(){ return "Success"; } -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/AuthTestService.java b/src/main/java/poomasi/domain/auth/security/AuthTestService.java index 7a237b38..32d108ef 100644 --- a/src/main/java/poomasi/domain/auth/security/AuthTestService.java +++ b/src/main/java/poomasi/domain/auth/security/AuthTestService.java @@ -24,4 +24,4 @@ public String Test(){ return "SUCCESS"; } -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java deleted file mode 100644 index 5843d8f6..00000000 --- a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java +++ /dev/null @@ -1,70 +0,0 @@ - - -package poomasi.domain.auth.security.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.web.filter.OncePerRequestFilter; -import poomasi.domain.auth.token.util.JwtUtil; - -import java.io.IOException; -import java.io.PrintWriter; - -@Slf4j -public class CustomLogoutFilter extends LogoutFilter { - - private JwtUtil jwtUtil; - - public CustomLogoutFilter(JwtUtil jwtUtil, LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) { - super(logoutSuccessHandler, handlers); - this.jwtUtil=jwtUtil; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); - } - - public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { - - log.info("[logout filter] - 로그아웃 진행합니다."); - - // POST : /api/logout 아니라면 넘기기 - String requestURI = request.getRequestURI(); - String requestMethod = request.getMethod(); - if (!"/api/logout".equals(requestURI) || !requestMethod.equals("POST")) { - log.info("[logout url not matching] "); - filterChain.doFilter(request, response); - return; - } - - - boolean isLogoutSuccess = true; - - if(isLogoutSuccess){ - PrintWriter out = response.getWriter(); - out.println("logout success~. "); - return; - } - - /* - * 로그아웃 로직 - * access token , refresh token 관리하기 - * */ - PrintWriter out = response.getWriter(); - out.println("logout success~. "); - //return; - //filterChain.doFilter(request, response); - } -} - diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java index 5860e373..882154bf 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java @@ -16,6 +16,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.refreshtoken.service.RefreshTokenService; import poomasi.domain.auth.token.util.JwtUtil; @@ -30,15 +31,16 @@ public class CustomUsernamePasswordAuthenticationFilter extends UsernamePassword private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; + private final RefreshTokenService refreshTokenService; @Description("인증 시도 메서드") @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{ log.info("email - password 기반으로 인증을 시도 합니다 : CustomUsernamePasswordAuthenticationFilter"); ObjectMapper loginRequestMapper = new ObjectMapper(); - String email; - String password; + String email = null; + String password = null; try { BufferedReader reader = request.getReader(); @@ -59,20 +61,21 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ @Description("로그인 성공 시, accessToken과 refreshToken 발급") protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { UserDetailsImpl customUserDetails = (UserDetailsImpl) authentication.getPrincipal(); + String username = customUserDetails.getUsername(); + String role = customUserDetails.getAuthority(); Long memberId = customUserDetails.getMember().getId(); String accessToken = jwtUtil.generateAccessTokenById(memberId); String refreshToken = jwtUtil.generateRefreshTokenById(memberId); - log.info("usename password 기반 로그인 성공 . cookie에 토큰을 넣어 발급합니다."); - response.setHeader("access", accessToken); + log.info("username password 기반 로그인 성공 . cookie에 토큰을 넣어 발급합니다."); response.addCookie(createCookie("refresh", refreshToken)); response.setStatus(HttpStatus.OK.value()); - // 나중에 주석 해야 함 - PrintWriter out = response.getWriter(); - out.println("access : " + accessToken + ", refresh : " + refreshToken); - out.close(); + refreshTokenService.putRefreshToken(refreshToken, memberId); + + response.setContentType("application/json"); // Content-Type 설정 + response.getWriter().write("{\"access\": \"" + accessToken + "\", \"refresh\": \"" + refreshToken + "\"}"); } @Override diff --git a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java index eeb3a93a..4ca9cb79 100644 --- a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java @@ -16,6 +16,9 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.filter.OncePerRequestFilter; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; +import poomasi.domain.auth.token.blacklist.service.BlacklistRedisService; +import poomasi.domain.auth.token.reissue.service.ReissueTokenService; import poomasi.domain.auth.token.util.JwtUtil; import poomasi.domain.member.entity.Member; import poomasi.domain.member.entity.Role; @@ -31,37 +34,52 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; + private final BlacklistJpaService blacklistRedisService; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - - log.info("jwt 인증 필터입니다"); + log.info("[JwtAuthenticationFilter] - jwt 인증 필터입니다"); String requestHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String accessToken = null; + String requestUri = request.getRequestURI(); + if ("/oauth2/authentication/kakao".equals(requestUri)) { + log.info("[JwtAuthenticationFilter] - 카카오 인증 요청이므로 필터를 통과합니다."); + // 요청을 그대로 다음 필터로 넘김 + filterChain.doFilter(request, response); + return; + } + + if("/api/login".equals(requestUri)) { + log.info("[JwtAuthenticationFilter] - 로그인 요청이므로 필터를 통과합니다."); + // 요청을 그대로 다음 필터로 넘김 + filterChain.doFilter(request, response); + return; + } + + if (requestHeader == null || !requestHeader.startsWith("Bearer ")) { - log.info("access token을 header로 갖지 않았으므로 다음 usernamepassword 필터로 이동합니다"); + log.info("[JwtAuthenticationFilter] : access token을 header로 갖지 않았으므로 다음 필터로 이동합니다"); filterChain.doFilter(request, response); - }else{ - //access 추출하기 - log.info("access token 추출하기"); + return; + }else{ //존재하고 Bearer로 존재한다면 + log.info("[JwtAuthenticationFilter] : access token 추출하기"); accessToken = requestHeader.substring(7); } - log.info("access token 추출 완료: " + accessToken); + log.info("[JwtAuthenticationFilter] : access token 추출 완료: " + accessToken); - if (accessToken == null) { + if (accessToken == null) { log.info("access token이 존재하지 않아서 다음 filter로 넘어갑니다."); filterChain.doFilter(request, response); return; } - // 만료 검사 - if(jwtUtil.isTokenExpired(accessToken)){ - log.warn("[인증 실패] - 토큰이 만료되었습니다."); - PrintWriter writer = response.getWriter(); - writer.print("만료된 토큰입니다."); + if(blacklistRedisService.hasKeyBlackList(accessToken)){ + log.info("블랙리스트에 있는 토큰입니다."); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("{\"message\": \"" + "token is in Blacklist"); return; } @@ -74,6 +92,14 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } + // 만료 검사 + if(jwtUtil.isTokenExpired(accessToken)){ + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write("{\"message\": \"" + "token is expired"); + filterChain.doFilter(request, response); + return; + } + log.info("토큰 검증 완료"); String username = jwtUtil.getEmailFromToken(accessToken); UserDetailsImpl userDetailsImpl = (UserDetailsImpl) userDetailsService.loadUserByUsername(username); @@ -83,9 +109,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.getContext().setAuthentication(authToken); filterChain.doFilter(request, response); - + return; } + } diff --git a/src/main/java/poomasi/domain/auth/security/filter/JwtLogoutFilter.java b/src/main/java/poomasi/domain/auth/security/filter/JwtLogoutFilter.java new file mode 100644 index 00000000..91783413 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/filter/JwtLogoutFilter.java @@ -0,0 +1,127 @@ + + +package poomasi.domain.auth.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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.filter.OncePerRequestFilter; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.blacklist.service.BlacklistJpaService; +import poomasi.domain.auth.token.refreshtoken.service.RefreshTokenService; +import poomasi.domain.auth.token.util.JwtUtil; +import poomasi.domain.member.entity.Member; + +import java.io.IOException; +import java.time.Duration; + +@RequiredArgsConstructor +@Slf4j +public class JwtLogoutFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final BlacklistJpaService blacklistServices; + private final RefreshTokenService refreshTokenService; + + + @Value("${jwt.access-token-expiration-time}") + private long ACCESS_TOKEN_EXPIRE_TIME; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 로그아웃 URL이 아니면 필터 실행하지 않음 + if (!"/api/logout".equals(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + // 토큰 추출 + String accessToken = extractJwtFromRequest(request); + if (accessToken == null || !jwtUtil.validateToken(accessToken)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("{\"error\": \"Invalid token\"}"); + return; + } + + // if(만료되었으면) -> response 만료됨 + if(jwtUtil.isTokenExpired(accessToken)){ + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("{\"message\": \"logout completes\"}"); + return; + } + + // 블랙리스팅 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object impl = authentication.getPrincipal(); + Member member = ((UserDetailsImpl) impl).getMember(); + Long memberId = member.getId(); + + if(blacklistServices.hasKeyBlackList(accessToken)){ + log.info("jwt logout filter - access token이 블랙리스트에 있어요."); + }else{ + log.info("jwt logout filter - access token이 블랙리스트에 없어요. 저장할게요"); + blacklistServices.setBlackList(accessToken, memberId.toString(), Duration.ofSeconds(ACCESS_TOKEN_EXPIRE_TIME)); + } + + //db refresh token 제거 + Cookie[] cookies = request.getCookies(); + String refreshToken = getRefreshToken(cookies); + + if(refreshToken!=null) { + log.info("jwt logout filter - refresh token을 지웁니다"); + refreshTokenService.removeMemberRefreshToken(memberId); + } + + // 쿠키 삭제 + clearAuthCookie(response); + // 세션 무효화 + request.getSession().invalidate(); + // 4. SecurityContext를 비워 인증 정보 해제 + SecurityContextHolder.clearContext(); + + // 로그아웃 성공 응답 설정 + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("{\"message\": \"Logout successful\"}"); + return; + } + + private String extractJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + private void clearAuthCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("refresh", null); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + + private String getRefreshToken(Cookie[] cookies) { + + if (cookies != null) { + // 쿠키 목록을 순회하면서 "refresh" 쿠키를 찾습니다. + for (Cookie cookie : cookies) { + if ("refresh".equals(cookie.getName())) { + return cookie.getValue(); // 쿠키의 값 반환 + } + } + } + return null; // "refresh" 쿠키가 없다면 null 반환 + } + +} diff --git a/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java b/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java deleted file mode 100644 index f4ceee19..00000000 --- a/src/main/java/poomasi/domain/auth/security/handler/ClearAuthenticationHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package poomasi.domain.auth.security.handler; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.logout.LogoutHandler; - -@Slf4j -public class ClearAuthenticationHandler implements LogoutHandler { - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - log.info("[logout handler] - security context 제거"); - SecurityContextHolder.clearContext(); - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java deleted file mode 100644 index 4a9dd8a9..00000000 --- a/src/main/java/poomasi/domain/auth/security/handler/CookieClearingLogoutHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package poomasi.domain.auth.security.handler; - -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.Authentication; -import org.springframework.security.web.authentication.logout.LogoutHandler; - -@Slf4j -public class CookieClearingLogoutHandler implements LogoutHandler { - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - cookie.setValue(null); - cookie.setMaxAge(0); // 쿠키 제거 - cookie.setPath("/"); // 적용할 경로 설정 - response.addCookie(cookie); - } - log.info("Cookies cleared"); - } - log.info("[logout handler] - cookie 제거"); - } -} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java b/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java deleted file mode 100644 index 351bd24a..00000000 --- a/src/main/java/poomasi/domain/auth/security/handler/CustomLogoutSuccessHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package poomasi.domain.auth.security.handler; - -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.Authentication; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; - -import java.io.IOException; - -@Slf4j -public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { - - @Override - public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - - log.info("[logout success handler] - cookie 제거"); - expireCookie(response, "access"); - expireCookie(response, "refresh"); - } - - private void expireCookie(HttpServletResponse response, String key) { - Cookie cookie = new Cookie(key, null); // 쿠키를 null로 설정 - cookie.setMaxAge(0); // 쿠키의 최대 생명 주기를 0으로 설정 - cookie.setPath("/"); // 쿠키의 경로를 설정 (원래 설정한 경로와 동일하게) - response.addCookie(cookie); // 응답에 쿠키 추가 - } -} diff --git a/src/main/java/poomasi/domain/auth/security/handler/OAuth2FailureHandler.java b/src/main/java/poomasi/domain/auth/security/handler/OAuth2FailureHandler.java new file mode 100644 index 00000000..0ac64ed4 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/handler/OAuth2FailureHandler.java @@ -0,0 +1,24 @@ +package poomasi.domain.auth.security.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class OAuth2FailureHandler implements AuthenticationFailureHandler { + + @Value("{http://localhost:3000}") + private String baseUrl; + + @Override + public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception) throws IOException, ServletException { + String redirectUrl = baseUrl + "oauth2-fauiler"; + response.sendRedirect(redirectUrl); + } +} diff --git a/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java b/src/main/java/poomasi/domain/auth/security/handler/OAuth2SuccessHandler.java similarity index 75% rename from src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java rename to src/main/java/poomasi/domain/auth/security/handler/OAuth2SuccessHandler.java index c4f0c483..f7c37344 100644 --- a/src/main/java/poomasi/domain/auth/security/handler/CustomSuccessHandler.java +++ b/src/main/java/poomasi/domain/auth/security/handler/OAuth2SuccessHandler.java @@ -1,40 +1,36 @@ package poomasi.domain.auth.security.handler; -/* - * TODO : Oauth2.0 로그인이 성공하면 access, refresh를 발급해야 함. - * - * */ import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.token.refreshtoken.service.RefreshTokenService; import poomasi.domain.auth.token.util.JwtUtil; import poomasi.domain.member.entity.Member; import java.io.IOException; -import java.util.Collection; -import java.util.Iterator; @Slf4j @Component -public class CustomSuccessHandler implements AuthenticationSuccessHandler { +@RequiredArgsConstructor +public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final JwtUtil jwtUtil; + private final RefreshTokenService refreshTokenService; - public CustomSuccessHandler(JwtUtil jwtUtil) { - this.jwtUtil = jwtUtil; - } + @Value("${spring.security.redirect_url}") + private String redirectUrl; @Description("TODO : Oauth2.0 로그인이 성공하면 server access, refresh token을 발급하는 메서드") @Override @@ -51,11 +47,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String refreshToken = jwtUtil.generateRefreshTokenById(memberId); response.addCookie(createCookie("refresh", refreshToken)); - response.addCookie(createCookie("access", accessToken)); - response.setStatus(HttpStatus.OK.value()); - response.getWriter(); + //refresh token db에 저장 + refreshTokenService.putRefreshToken(refreshToken, memberId); + response.sendRedirect(redirectUrl+"/access=" + accessToken); } private Cookie createCookie(String key, String value) { @@ -69,3 +65,5 @@ private Cookie createCookie(String key, String value) { return cookie; } } + + diff --git a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java index 12739679..410efab3 100644 --- a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java +++ b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2KakaoResponse.java @@ -33,4 +33,10 @@ public LoginType getLoginType(){ return LoginType.KAKAO; } + @Override + public String getNickname(){ + String nickname = String.valueOf(((Map) attribute.get("profile")).get("nickname")); + return nickname; + } + } diff --git a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java index 56497ac7..87331040 100644 --- a/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java +++ b/src/main/java/poomasi/domain/auth/security/oauth2/dto/response/OAuth2Response.java @@ -7,4 +7,5 @@ public interface OAuth2Response { String getProviderId(); String getEmail(); String getName(); + String getNickname(); } diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java index e3923435..7447142b 100644 --- a/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java +++ b/src/main/java/poomasi/domain/auth/security/userdetail/OAuth2UserDetailServiceImpl.java @@ -46,7 +46,9 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic // 정보 추출 String providerId = oAuth2UserInfo.getProviderId(); + //String nickName = oAuth2UserInfo.getNickname(); String email = oAuth2UserInfo.getEmail(); + Role role = Role.ROLE_CUSTOMER; LoginType loginType = oAuth2UserInfo.getLoginType(); @@ -59,6 +61,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic .loginType(loginType) .provideId(providerId) .memberProfile(new MemberProfile()) + //.name(nickName) .build(); memberRepository.save(member); diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java b/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java index 2d3684f8..0afddefe 100644 --- a/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java +++ b/src/main/java/poomasi/domain/auth/token/blacklist/repository/BlacklistRepository.java @@ -11,7 +11,7 @@ public interface BlacklistRepository extends JpaRepository { void deleteByTokenKey(String key); Optional findByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); - boolean existsByTokenKeyAndExpireAtAfter(String key, LocalDateTime now); + boolean existsByTokenKeyAndExpireAtBefore(String key, LocalDateTime now); void deleteAllByExpireAtBefore(LocalDateTime now); } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java index 6b7bd092..e105e4ee 100644 --- a/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java +++ b/src/main/java/poomasi/domain/auth/token/blacklist/service/BlacklistJpaService.java @@ -43,7 +43,7 @@ public void deleteBlackList(String key) { @Override public boolean hasKeyBlackList(String key) { - return blacklistRepository.existsByTokenKeyAndExpireAtAfter(key, LocalDateTime.now()); + return blacklistRepository.existsByTokenKeyAndExpireAtBefore(key, LocalDateTime.now()); } @Transactional diff --git a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java index 898cdeb7..87907352 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/controller/ReissueTokenController.java @@ -1,6 +1,5 @@ package poomasi.domain.auth.token.reissue.controller; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -19,12 +18,8 @@ public class ReissueTokenController { private final ReissueTokenService reissueTokenService; @PostMapping("/api/reissue") - public ResponseEntity reissue(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader, - @Valid @RequestBody ReissueRequest reissueRequest){ - - String accessToken = authorizationHeader.replace("Bearer ", ""); - - return ResponseEntity.ok(reissueTokenService.reissueToken(accessToken, reissueRequest)); + public ResponseEntity reissue(@RequestBody ReissueRequest reissueRequest){ + return ResponseEntity.ok(reissueTokenService.reissueToken(reissueRequest)); } } diff --git a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java index 3d638e8b..66d8944a 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueRequest.java @@ -1,8 +1,4 @@ package poomasi.domain.auth.token.reissue.dto; -import jakarta.validation.constraints.NotBlank; - -public record ReissueRequest( - @NotBlank(message = "리프레시 토큰을 입력해야 합니다.") - String refreshToken) { -} +public record ReissueRequest(String refreshToken) { +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java index 258ce50d..ebb7caaa 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/dto/ReissueResponse.java @@ -1,4 +1,4 @@ package poomasi.domain.auth.token.reissue.dto; public record ReissueResponse(String accessToken, String refreshToken) { -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java index b0b21182..79c90fbd 100644 --- a/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java +++ b/src/main/java/poomasi/domain/auth/token/reissue/service/ReissueTokenService.java @@ -18,19 +18,14 @@ public class ReissueTokenService { private final RefreshTokenService refreshTokenService; // 토큰 재발급 - public ReissueResponse reissueToken(String accessToken, ReissueRequest reissueRequest) { - Long memberId = jwtUtil.getIdFromToken(accessToken); + public ReissueResponse reissueToken(ReissueRequest reissueRequest) { String refreshToken = reissueRequest.refreshToken(); Long requestMemberId = jwtUtil.getIdFromToken(refreshToken); - if (!requestMemberId.equals(memberId)) { - throw new BusinessException(REFRESH_TOKEN_NOT_VALID); - } - - checkRefreshToken(refreshToken, memberId); + checkRefreshToken(refreshToken, requestMemberId); - return getTokenResponse(memberId); + return getTokenResponse(requestMemberId); } public ReissueResponse getTokenResponse(Long memberId) { @@ -47,4 +42,4 @@ private void checkRefreshToken(final String refreshToken, Long memberId) { if(!jwtUtil.validateRefreshToken(refreshToken, memberId)) throw new BusinessException(REFRESH_TOKEN_NOT_VALID); } -} +} \ No newline at end of file diff --git a/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java b/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java index bd6ab3df..68eedf36 100644 --- a/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java +++ b/src/main/java/poomasi/domain/auth/token/util/JwtUtil.java @@ -47,7 +47,7 @@ public void init() { } public String generateAccessTokenById(final Long memberId) { - Map claims = createClaims(memberId); + Map claims = createClaims(memberId); //id, email, role claims.put("type", ACCESS); return Jwts.builder() .setClaims(claims) @@ -86,8 +86,9 @@ public Long getIdFromToken(final String token) { return getClaimFromToken(token, "id", Long.class); } - public String getEmailFromToken(final String token) { + public String getEmailFromToken(final String token){ return getClaimFromToken(token, "email", String.class); + } public Date getExpirationDateFromToken(final String token) { @@ -134,7 +135,7 @@ public Boolean validateAccessToken(final String accessToken){ return true; } - private Boolean validateToken(final String token) { + public Boolean validateToken(final String token) { try { Jwts.parserBuilder() .setSigningKey(secretKey) @@ -165,4 +166,5 @@ public boolean isTokenExpired(String token) { return true; } } + } \ No newline at end of file diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index 811c8b04..45752b64 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -82,7 +82,7 @@ public Member(String name, String email, String password, LoginType loginType, R } @Builder - public Member(Long id, String email, String password, Role role, LoginType loginType, String provideId, MemberProfile memberProfile) { + public Member(Long id, String email, String password, Role role, LoginType loginType, String provideId, MemberProfile memberProfile, String name) { this.id = id; this.password = password; this.email = email; @@ -90,6 +90,7 @@ public Member(Long id, String email, String password, Role role, LoginType login this.loginType = loginType; this.provideId = provideId; this.memberProfile = memberProfile; + this.name = name; } public boolean isCustomer() {