Skip to content

Commit

Permalink
엑세스 토큰 만료 시 리프레시 토큰으로 액세스 토큰 재발급 하는 기능 추가 (#124)
Browse files Browse the repository at this point in the history
* feat: 유효한 자격증명을 제공하지 않을 때 401 에러 리턴하는 기능 구현

* feat: 유효한 권한이 존재하지 않는 경우 403 에러 리턴하는 기능 구현

* feat: 401, 403에러 exception handling

* refactor: 토큰 필드를 TokenDto를 생성하여 따로 분리

* feat: accessToken 만료시 refreshToken 재발급 하는 기능 구현

* feat: 에러 코드 추가

* refactor: yml 파일 config로 이동

* refactor: JwtTokenProvider 에서 AuthenticationManager 삭제하여 순환 참조 에러 해결
  • Loading branch information
jschoi-96 authored Feb 28, 2024
1 parent ab9e096 commit 9702c4b
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 24 deletions.
8 changes: 8 additions & 0 deletions src/main/java/balancetalk/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package balancetalk.global.config;

import balancetalk.global.jwt.JwtAccessDeniedHandler;
import balancetalk.global.jwt.JwtAuthenticationEntryPoint;
import balancetalk.global.jwt.JwtAuthenticationFilter;
import balancetalk.global.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
Expand All @@ -20,6 +22,8 @@
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

@Bean
public BCryptPasswordEncoder passwordEncoder() {
Expand All @@ -35,6 +39,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.disable())
.exceptionHandling(exception -> {
exception.authenticationEntryPoint(jwtAuthenticationEntryPoint);
exception.accessDeniedHandler(jwtAccessDeniedHandler);
})
// h2 콘솔 사용
.headers(header -> header.frameOptions(frameOption -> frameOption.disable()).disable())
// 세션 사용 X (jwt 사용)
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/balancetalk/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ public enum ErrorCode {
MIME_TYPE_NULL(BAD_REQUEST, "MIME 타입이 null입니다."),
FILE_UPLOAD_FAILED(BAD_REQUEST, "파일 업로드에 실패했습니다."),
FILE_SIZE_EXCEEDED(BAD_REQUEST, "파일 크기가 초과되었습니다."),

EXPIRED_JWT_TOKEN(BAD_REQUEST, "만료된 토큰 입니다."),
INVALID_JWT_TOKEN(BAD_REQUEST, "유효하지 않은 토큰입니다."),
INVALID_REFRESH_TOKEN(BAD_REQUEST, "RefreshToken이 존재하지 않습니다."),

// 401
MISMATCHED_EMAIL_OR_PASSWORD(UNAUTHORIZED, "이메일 또는 비밀번호가 잘못되었습니다."),
AUTHENTICATION_ERROR(UNAUTHORIZED, "인증 오류가 발생했습니다."),
BAD_CREDENTIAL_ERROR(UNAUTHORIZED, "로그인에 실패했습니다."),

// 403
FORBIDDEN_COMMENT_MODIFY(FORBIDDEN, "댓글 수정 권한이 없습니다."), // TODO : Spring Security 적용 후 적용 필요
Expand All @@ -45,6 +48,10 @@ public enum ErrorCode {
ALREADY_LIKE_COMMENT(CONFLICT, "이미 추천을 누른 댓글입니다."),
ALREADY_LIKE_POST(CONFLICT, "이미 추천을 누른 게시글입니다."),

// 500
REDIS_CONNECTION_FAIL(INTERNAL_SERVER_ERROR, "Redis 연결에 실패했습니다.");


// 500
DUPLICATE_EMAIL(INTERNAL_SERVER_ERROR, "이미 존재하는 이메일 입니다. 다른 이메일을 입력해주세요"),
AUTHORIZATION_CODE_MISMATCH(INTERNAL_SERVER_ERROR, "인증 번호가 일치하지 않습니다.");
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/balancetalk/global/jwt/JwtAccessDeniedHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package balancetalk.global.jwt;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package balancetalk.global.jwt;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package balancetalk.global.jwt;

import balancetalk.global.exception.BalanceTalkException;
import balancetalk.global.exception.ErrorCode;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

Expand All @@ -29,9 +32,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
} catch (RedisConnectionFailureException e) {
SecurityContextHolder.clearContext();
throw new IllegalAccessError("Redis 연결 실패");
} catch (Exception e) {
throw new IllegalArgumentException("JWT가 유효하지 않음.");
throw new BalanceTalkException(ErrorCode.REDIS_CONNECTION_FAIL);
} catch (ExpiredJwtException e) {
log.error(e.getMessage());
throw new BalanceTalkException(ErrorCode.EXPIRED_JWT_TOKEN);
}
chain.doFilter(request, response);
}
Expand Down
30 changes: 21 additions & 9 deletions src/main/java/balancetalk/global/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package balancetalk.global.jwt;

import balancetalk.global.exception.BalanceTalkException;
import balancetalk.global.exception.ErrorCode;
import balancetalk.global.redis.application.RedisService;
import balancetalk.module.member.domain.Role;
import balancetalk.module.member.dto.TokenDto;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.time.Duration;
import java.util.Date;

Expand Down Expand Up @@ -76,9 +75,7 @@ public String createRefreshToken(Authentication authentication) {
public Authentication getAuthentication(String token) {
String userPrincipal = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(userPrincipal);

return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());

}

// http 헤더로부터 bearer 토큰 가져옴
Expand All @@ -97,10 +94,25 @@ public boolean validateToken(String token) {
return true;
} catch (ExpiredJwtException e) {
log.error(e.getMessage());
throw new IllegalArgumentException("토큰 만료");
throw new BalanceTalkException(ErrorCode.EXPIRED_JWT_TOKEN);
} catch (JwtException e) {
log.error(e.getMessage());
throw new IllegalArgumentException("유효하지 않은 JWT");
throw new BalanceTalkException(ErrorCode.INVALID_JWT_TOKEN);
}
}
public TokenDto reissueToken(String refreshToken) {
validateToken(refreshToken);
Authentication authentication = getAuthentication(refreshToken);

// redis에 저장된 RefreshToken 값을 가져옴
String redisRefreshToken = redisService.getValues(authentication.getName());
if (!redisRefreshToken.equals(refreshToken)) {
throw new BalanceTalkException(ErrorCode.INVALID_REFRESH_TOKEN);
}
TokenDto tokenDto = new TokenDto(
createAccessToken(authentication),
createRefreshToken(authentication)
);
return tokenDto;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import balancetalk.global.exception.BalanceTalkException;
import balancetalk.global.exception.ErrorCode;
import balancetalk.global.jwt.JwtTokenProvider;
import balancetalk.global.redis.application.RedisService;
import balancetalk.module.member.domain.Member;
import balancetalk.module.member.domain.MemberRepository;
import balancetalk.module.member.dto.*;
Expand Down Expand Up @@ -48,16 +47,15 @@ public LoginSuccessDto login(final LoginDto loginDto) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword())
);

TokenDto tokenDto = new TokenDto(jwtTokenProvider.createAccessToken(authentication), jwtTokenProvider.createRefreshToken(authentication));
return LoginSuccessDto.builder()
.email(member.getEmail())
.password(member.getPassword())
.role(member.getRole())
.accessToken(jwtTokenProvider.createAccessToken(authentication))
.refreshToken(jwtTokenProvider.createRefreshToken(authentication))
.tokenDto(tokenDto)
.build();
} catch (BadCredentialsException e) {
throw new BadCredentialsException("credential 오류!!");
throw new BalanceTalkException(ErrorCode.BAD_CREDENTIAL_ERROR);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@ public class LoginSuccessDto {
@Schema(description = "회원 역할", example = "USER")
private Role role;

private String accessToken;

private String refreshToken;
private TokenDto tokenDto;
}
14 changes: 14 additions & 0 deletions src/main/java/balancetalk/module/member/dto/TokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package balancetalk.module.member.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TokenDto {
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ void LoginMember_Success() {

Member member = joinDto.toEntity();
when(memberRepository.findByEmail(any())).thenReturn(Optional.of(member));
when(jwtTokenProvider.createToken(member.getEmail(), member.getRole())).thenReturn("token");
//when(jwtTokenProvider.createToken(member.getEmail(), member.getRole())).thenReturn("token");
// TODO: jwt 토큰 null -> 인증 오류

// when
Expand Down

0 comments on commit 9702c4b

Please sign in to comment.