diff --git a/backend/src/main/java/com/ssafy/home/domain/member/controller/MemberController.java b/backend/src/main/java/com/ssafy/home/domain/member/controller/MemberController.java index de1686a..4610e1d 100644 --- a/backend/src/main/java/com/ssafy/home/domain/member/controller/MemberController.java +++ b/backend/src/main/java/com/ssafy/home/domain/member/controller/MemberController.java @@ -50,6 +50,10 @@ public ResponseEntity login(@RequestBody LoginRequest loginRequest, Http TokenResponse tokenResponse = memberService.login(loginRequest.getEmail(), loginRequest.getPassword()); + if(tokenResponse == null){ + throw new AuthenticationException(ErrorCode.MEMBER_NOT_MATCH); + } + response.addHeader(JwtTokenProvider.AUTHORIZATION_HEADER, tokenResponse.getAccessToken()); Cookie cookie = new Cookie("refreshToken", tokenResponse.getRefreshToken()); diff --git a/backend/src/main/java/com/ssafy/home/domain/member/service/MemberService.java b/backend/src/main/java/com/ssafy/home/domain/member/service/MemberService.java index 8f6c2ef..2bb644a 100644 --- a/backend/src/main/java/com/ssafy/home/domain/member/service/MemberService.java +++ b/backend/src/main/java/com/ssafy/home/domain/member/service/MemberService.java @@ -26,6 +26,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j @@ -103,7 +106,7 @@ public TokenResponse login(String email, String password) { } catch (AuthenticationException e){ member.getLoginAttempt().updateCount(); - throw new AuthenticationException(ErrorCode.MEMBER_NOT_MATCH); + e.printStackTrace(); } catch (Exception e) { member.getLoginAttempt().updateCount(); e.printStackTrace(); @@ -122,9 +125,9 @@ public String reissue(String refreshToken) { .orElseThrow(() -> new AuthenticationException(ErrorCode.REFRESH_TOKEN_NOT_FOUND)); return jwtTokenProvider.createAccessToken(member); + } else { + throw new AuthenticationException(ErrorCode.NOT_VALID_TOKEN); } - - return null; } public Member getMemberById(Long memberId) { @@ -183,9 +186,13 @@ public void sendPassword(String email) { } @Transactional - public void initAttempt(){ - loginAttemptRepository.findAll().stream() + public void initAttempt() { + LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30); + List staleAttempts = loginAttemptRepository.findAll().stream() .filter(loginAttempt -> loginAttempt.getCount() >= 5) - .forEach(LoginAttempt::initCount); + .filter(loginAttempt -> loginAttempt.getLoginRecentAttemp().isBefore(thirtyMinutesAgo)) + .toList(); + + staleAttempts.forEach(LoginAttempt::initCount); } } diff --git a/backend/src/main/java/com/ssafy/home/entity/member/LoginAttempt.java b/backend/src/main/java/com/ssafy/home/entity/member/LoginAttempt.java index 25dd89f..10ccde1 100644 --- a/backend/src/main/java/com/ssafy/home/entity/member/LoginAttempt.java +++ b/backend/src/main/java/com/ssafy/home/entity/member/LoginAttempt.java @@ -8,7 +8,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDate; +import java.time.LocalDateTime; @Entity @NoArgsConstructor @@ -27,7 +27,7 @@ public class LoginAttempt { @LastModifiedDate @Column(name = "login_recent_attemp", columnDefinition = "datetime default CURRENT_TIMESTAMP") - private LocalDate loginRecentAttemp; + private LocalDateTime loginRecentAttemp; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") @@ -39,7 +39,7 @@ public LoginAttempt(Member member) { } public void updateCount(){ - this.count++; + this.count = this.count + 1; } public void initCount(){ diff --git a/backend/src/main/java/com/ssafy/home/entity/member/Member.java b/backend/src/main/java/com/ssafy/home/entity/member/Member.java index a6131e4..30f0889 100644 --- a/backend/src/main/java/com/ssafy/home/entity/member/Member.java +++ b/backend/src/main/java/com/ssafy/home/entity/member/Member.java @@ -10,6 +10,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; @NoArgsConstructor @@ -41,7 +42,7 @@ public class Member { @LastModifiedDate @Column(name = "modify_date", columnDefinition = "datetime default CURRENT_TIMESTAMP") - private LocalDate modifyDate; + private LocalDateTime modifyDate; @Column(name = "is_deleted") @ColumnDefault("false") diff --git a/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtAuthenticationFilter.java index bf8c445..2333065 100644 --- a/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtAuthenticationFilter.java @@ -4,6 +4,8 @@ import com.ssafy.home.entity.member.Member; import com.ssafy.home.global.auth.dto.MemberDto; import com.ssafy.home.global.auth.jwt.JwtTokenProvider; +import com.ssafy.home.global.error.ErrorCode; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; @@ -15,6 +17,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; +import com.ssafy.home.global.error.exception.AuthenticationException; import java.io.IOException; import java.util.Arrays; @@ -34,43 +37,28 @@ public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, MemberService @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); + + String token = ""; + if(authHeader == null || !authHeader.startsWith("Bearer ")) { - log.error("header에 없거나, 형식이 틀립니다. - {}", authHeader); - filterChain.doFilter(request,response); - return; + refreshTokenCheck(request, response, token); + throw new AuthenticationException(ErrorCode.NOT_EXISTS_AUTHORIZATION); } - String token; try{ token = authHeader.split(" ")[1].trim(); } catch (Exception e) { - log.error("토큰을 분리하는데 실패했습니다. - {}", authHeader); - filterChain.doFilter(request,response); - return; + refreshTokenCheck(request, response, token); + throw new AuthenticationException(ErrorCode.NOT_VALID_TOKEN); } - log.info("token : {}", token); if(!jwtTokenProvider.validateToken(token)){ - - Cookie[] cookies = request.getCookies(); - - String refreshToken = null; - - if (cookies != null) { - refreshToken = Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals("refreshToken")) - .map(Cookie::getValue) - .findFirst() - .orElse(null); - } - - token = memberService.reissue(refreshToken); - - response.addHeader(JwtTokenProvider.AUTHORIZATION_HEADER, token); + refreshTokenCheck(request, response, token); + throw new AuthenticationException(ErrorCode.TOKEN_EXPIRED); } Long userId = jwtTokenProvider.getInfoId(token); - log.info("userId : {}", userId); + Member member = memberService.getMemberById(userId); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( @@ -87,4 +75,25 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); } + private void refreshTokenCheck(HttpServletRequest request, HttpServletResponse response, String token){ + Cookie[] cookies = request.getCookies(); + + String refreshToken = null; + + if (cookies != null) { + refreshToken = Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals("refreshToken")) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + } + + try { + token = memberService.reissue(refreshToken); + } catch (Exception e){ + throw new JwtException("리프레시 토큰 검증 실패"); + } + response.addHeader(JwtTokenProvider.AUTHORIZATION_HEADER, token); + } + } diff --git a/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtExceptionFilter.java b/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtExceptionFilter.java new file mode 100644 index 0000000..073f270 --- /dev/null +++ b/backend/src/main/java/com/ssafy/home/global/auth/filter/JwtExceptionFilter.java @@ -0,0 +1,47 @@ +package com.ssafy.home.global.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ssafy.home.global.error.ErrorCode; +import com.ssafy.home.global.error.ErrorResponse; +import com.ssafy.home.global.error.exception.AuthenticationException; +import io.jsonwebtoken.JwtException; +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.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +@Slf4j +public class JwtExceptionFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (AuthenticationException e) { + setErrorResponse(response, ErrorCode.ACCESS_TOKEN_REFRESH); + } catch (JwtException | IllegalArgumentException | NullPointerException | UnsupportedEncodingException e) { + setErrorResponse(response, ErrorCode.NOT_VALID_TOKEN); + } + } + + private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) { + log.error("filter에서 에러 체크"); + + ObjectMapper objectMapper = new ObjectMapper(); + response.setStatus(errorCode.getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + try { + response.getWriter().write(objectMapper.writeValueAsString(ErrorResponse.of(errorCode.getErrorCode(),errorCode.getMessage()))); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ssafy/home/global/config/batch/ScheduledJobConfiguration.java b/backend/src/main/java/com/ssafy/home/global/config/batch/ScheduledJobConfiguration.java index 5d4627e..4003747 100644 --- a/backend/src/main/java/com/ssafy/home/global/config/batch/ScheduledJobConfiguration.java +++ b/backend/src/main/java/com/ssafy/home/global/config/batch/ScheduledJobConfiguration.java @@ -12,7 +12,7 @@ public class ScheduledJobConfiguration { private final MemberService memberService; - @Scheduled(cron ="0 30 * * * *", zone = "Asia/Seoul") + @Scheduled(cron ="0 * * * * *", zone = "Asia/Seoul") public void scheduledEndForm() { memberService.initAttempt(); } diff --git a/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityConfiguration.java b/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityConfiguration.java index 01d8140..48cfb59 100644 --- a/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityConfiguration.java +++ b/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityConfiguration.java @@ -2,6 +2,7 @@ import com.ssafy.home.domain.member.service.MemberService; import com.ssafy.home.global.auth.filter.JwtAuthenticationFilter; +import com.ssafy.home.global.auth.filter.JwtExceptionFilter; import com.ssafy.home.global.auth.interceptor.UserActivationInterceptor; import com.ssafy.home.global.auth.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; @@ -24,12 +25,12 @@ public class WebSecurityConfiguration implements WebMvcConfigurer { private final JwtTokenProvider jwtTokenProvider; private final MemberService memberService; - private final String[] REQUIRE_AUTH_PATH = {"/health/**", "member/logout", "member/info", "member/pw", "/board/**", "/comment/**"}; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .securityMatchers((auth) -> auth.requestMatchers(REQUIRE_AUTH_PATH)); + .securityMatchers((auth) -> auth.requestMatchers(WebSecurityPath.REQUIRE_AUTH_PATH.getPaths())); http .csrf(AbstractHttpConfigurer::disable) .sessionManagement((session) -> session @@ -38,7 +39,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) - .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, memberService), BasicAuthenticationFilter.class); + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, memberService), BasicAuthenticationFilter.class) + .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class); return http.build(); } @@ -58,6 +60,6 @@ public WebSecurityCustomizer webSecurityCustomizer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserActivationInterceptor(memberService)) - .addPathPatterns(REQUIRE_AUTH_PATH); + .addPathPatterns(WebSecurityPath.REQUIRE_AUTH_PATH.getPaths()); } } diff --git a/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityPath.java b/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityPath.java new file mode 100644 index 0000000..a26a2e6 --- /dev/null +++ b/backend/src/main/java/com/ssafy/home/global/config/security/WebSecurityPath.java @@ -0,0 +1,14 @@ +package com.ssafy.home.global.config.security; + +import lombok.Getter; + +@Getter +public enum WebSecurityPath { + REQUIRE_AUTH_PATH("/health/**", "member/logout", "member/info", "member/pw", "/board/**", "/comment/**"); + + private final String[] paths; + + WebSecurityPath(String... paths) { + this.paths = paths; + } +} diff --git a/backend/src/main/java/com/ssafy/home/global/entity/BaseTimeEntity.java b/backend/src/main/java/com/ssafy/home/global/entity/BaseTimeEntity.java index 37c7ac0..894a5a6 100644 --- a/backend/src/main/java/com/ssafy/home/global/entity/BaseTimeEntity.java +++ b/backend/src/main/java/com/ssafy/home/global/entity/BaseTimeEntity.java @@ -2,12 +2,13 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; -import java.time.LocalDateTime; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + @Getter @EntityListeners(AuditingEntityListener.class) @MappedSuperclass diff --git a/backend/src/main/java/com/ssafy/home/global/error/ErrorCode.java b/backend/src/main/java/com/ssafy/home/global/error/ErrorCode.java index 20a7b4d..8fcd18f 100644 --- a/backend/src/main/java/com/ssafy/home/global/error/ErrorCode.java +++ b/backend/src/main/java/com/ssafy/home/global/error/ErrorCode.java @@ -17,12 +17,13 @@ public enum ErrorCode { REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "A-006", "해당 refresh token은 만료됐습니다."), NOT_ACCESS_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "A-007", "해당 토큰은 ACCESS TOKEN이 아닙니다."), FORBIDDEN_ADMIN(HttpStatus.FORBIDDEN, "A-008", "관리자 Role이 아닙니다."), + ACCESS_TOKEN_REFRESH(HttpStatus.UNAUTHORIZED, "A-009", "액세스 토큰 재발급 하였습니다."), // 회원 INVALID_MEMBER_TYPE(HttpStatus.BAD_REQUEST, "M-001", "잘못된 회원 타입 입니다.(memberType : KAKAO)"), ALREADY_REGISTERED_MEMBER(HttpStatus.BAD_REQUEST, "M-002", "이미 가입된 회원 입니다."), MEMBER_NOT_EXISTS(HttpStatus.BAD_REQUEST, "M-003", "해당 회원은 존재하지 않습니다."), - MEMBER_COUNT_OUT(HttpStatus.BAD_REQUEST, "M-004", "해당 회원 로그인 시도 횟수가 초과되었습니다. (비밀번호 변경이 필요합니다.)"), + MEMBER_COUNT_OUT(HttpStatus.BAD_REQUEST, "M-004", "해당 회원 로그인 시도 횟수가 초과되었습니다. 30분 후 다시 시도하세요!"), MEMBER_NOT_MATCH(HttpStatus.BAD_REQUEST, "M-005", " 아이디(로그인 전용 아이디) 또는 비밀번호를 잘못 입력했습니다.\n" + "입력하신 내용을 다시 확인해주세요."),