Skip to content

Commit

Permalink
Merge pull request #22 from ssafy-19-final-pjt/feature/#21-security-r…
Browse files Browse the repository at this point in the history
…efactor

Feature/#21 security refactor
  • Loading branch information
gurwls0122 authored May 23, 2024
2 parents 14e6eba + 9ef3fe0 commit ef06035
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public ResponseEntity<String> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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<LoginAttempt> staleAttempts = loginAttemptRepository.findAll().stream()
.filter(loginAttempt -> loginAttempt.getCount() >= 5)
.forEach(LoginAttempt::initCount);
.filter(loginAttempt -> loginAttempt.getLoginRecentAttemp().isBefore(thirtyMinutesAgo))
.toList();

staleAttempts.forEach(LoginAttempt::initCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -39,7 +39,7 @@ public LoginAttempt(Member member) {
}

public void updateCount(){
this.count++;
this.count = this.count + 1;
}

public void initCount(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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(
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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();
}
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" +
"입력하신 내용을 다시 확인해주세요."),

Expand Down

0 comments on commit ef06035

Please sign in to comment.