Skip to content

Commit

Permalink
feat: JWT Filter를 추가한다. (#14)
Browse files Browse the repository at this point in the history
* feat: JWT Filter와 Error Handler를 추가한다.

* feat: TokenService에 AccessToken을 validate하는 메소드를 추가한다.

* feat: 인증 객체를 담는 dto(JwtAuthentication)를 추가한다.

* feat: Jwt Filter를 Security FilterChain에 등록한다.
  • Loading branch information
rlarltj authored Aug 2, 2024
1 parent 936a6fe commit 1d0890e
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.dnd.accompany.domain.auth.dto.jwt;

import java.util.Objects;

import org.apache.commons.lang3.StringUtils;

import com.dnd.accompany.domain.auth.exception.InvalidTokenException;
import com.dnd.accompany.global.common.response.ErrorCode;

import lombok.Getter;

@Getter
public class JwtAuthentication {

private Long id;
private String accessToken;

public JwtAuthentication(Long id, String accessToken) {
this.id = validateId(id);
this.accessToken = validateAccessToken(accessToken);
}

private Long validateId(Long id) {
if (Objects.isNull(id) || id <= 0L) {
throw new InvalidTokenException(ErrorCode.INVALID_TOKEN);
}

return id;
}

private String validateAccessToken(String accessToken) {
if (StringUtils.isEmpty(accessToken)) {
throw new InvalidTokenException(ErrorCode.INVALID_TOKEN);
}

return accessToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.dnd.accompany.domain.auth.dto.jwt;

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

private final Object principal;
private String credentials;

public JwtAuthenticationToken(String principal, String credentials) {
super(null);
super.setAuthenticated(false);

this.principal = principal;
this.credentials = credentials;
}

/**
* 인증이 완료될 경우 사용하는 생성자입니다.
*/
public JwtAuthenticationToken(Object principal, String credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
super.setAuthenticated(true);

this.principal = principal;
this.credentials = credentials;
}

@Override
public Object getPrincipal() {
return principal;
}

@Override
public String getCredentials() {
return credentials;
}

@Override
public void setAuthenticated(boolean isAuthenticated) {
if (isAuthenticated) {
throw new IllegalArgumentException("유효하지 않은 접근입니다.");
}
super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package com.dnd.accompany.domain.auth.service;

import java.util.List;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import com.dnd.accompany.domain.auth.dto.AuthUserInfo;
import com.dnd.accompany.domain.auth.dto.Tokens;
import com.dnd.accompany.domain.auth.dto.jwt.JwtAuthentication;
import com.dnd.accompany.domain.auth.dto.jwt.JwtAuthenticationToken;
import com.dnd.accompany.domain.auth.entity.RefreshToken;
import com.dnd.accompany.domain.auth.exception.ExpiredTokenException;
import com.dnd.accompany.domain.auth.exception.RefreshTokenNotFoundException;
import com.dnd.accompany.domain.auth.infrastructure.RefreshTokenRepository;
import com.dnd.accompany.global.common.response.ErrorCode;

import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;

@Service
Expand Down Expand Up @@ -85,4 +90,23 @@ public void deleteRefreshToken(String refreshToken) {
.ifPresent(refreshTokenRepository::delete);
}

/**
* AccessToken에서 유효한 Authentication(인증 주체) 객체를 추출합니다.
*/
public JwtAuthenticationToken getAuthenticationByAccessToken(String accessToken) {
jwtTokenProvider.validateToken(accessToken);

Claims claims = jwtTokenProvider.getClaims(accessToken);

Long id = claims.get("userId", Long.class);

JwtAuthentication principal = new JwtAuthentication(id, accessToken);

/**
* 권한 분리가 필요할 경우 수정합니다.
*/
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("user"));

return new JwtAuthenticationToken(principal, null, authorities);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.dnd.accompany.global.filter.ExceptionHandlerFilter;
import com.dnd.accompany.global.filter.JwtAuthenticationEntryPoint;
import com.dnd.accompany.global.filter.JwtAuthenticationFilter;

import lombok.RequiredArgsConstructor;

Expand All @@ -15,6 +20,10 @@
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final ExceptionHandlerFilter exceptionHandlerFilter;

@Bean
public SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception {
return http
Expand All @@ -25,6 +34,9 @@ public SecurityFilterChain httpSecurity(HttpSecurity http) throws Exception {
.formLogin(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.exceptionHandling(handler -> handler.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(exceptionHandlerFilter, JwtAuthenticationFilter.class)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.dnd.accompany.global.filter;

import java.io.IOException;

import org.apache.commons.lang3.CharEncoding;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.dnd.accompany.domain.auth.exception.TokenException;
import com.dnd.accompany.global.common.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* TokenException이 발생할 경우 Handling합니다.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ExceptionHandlerFilter extends OncePerRequestFilter {

private final ObjectMapper objectMapper;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (TokenException e) {
log.debug("[ExceptionHandler] token error message = {}", e.getMessage());
generateErrorResponse(response, e);
}
}

private void generateErrorResponse(HttpServletResponse response, TokenException e) throws IOException {
response.setStatus(e.getErrorCode().getStatus().intValue());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(CharEncoding.UTF_8);
response.getWriter()
.write(objectMapper.writeValueAsString(
ErrorResponse.from(e.getErrorCode())
));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.dnd.accompany.global.filter;

import static org.springframework.http.HttpStatus.UNAUTHORIZED;

import java.io.IOException;

import org.apache.commons.lang3.CharEncoding;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.dnd.accompany.global.common.response.ErrorCode;
import com.dnd.accompany.global.common.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* JwtAuthenticationFilter에서 사용자 인증에 실패한 경우 발생하는 401 예외를 핸들링합니다.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private static final String ERROR_LOG_MESSAGE = "[ERROR] {} : {}";

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.info(ERROR_LOG_MESSAGE, authException.getClass().getSimpleName(), authException.getMessage());
response.setStatus(UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(CharEncoding.UTF_8);
response.getWriter()
.write(objectMapper.writeValueAsString(
ErrorResponse.from(ErrorCode.ACCESS_DENIED)
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.dnd.accompany.global.filter;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;

import java.io.IOException;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.dnd.accompany.domain.auth.dto.jwt.JwtAuthenticationToken;
import com.dnd.accompany.domain.auth.service.TokenService;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* JWT 인증 필터로, accessToken을 검증합니다.
* 토큰이 유효할 경우 유효한 인증 객체를 SecurityContextHolder에 넣어줍니다.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_TYPE = "Bearer";
private final TokenService tokenService;

@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String accessToken = getAccessToken(request);

if (accessToken != null) {
JwtAuthenticationToken authentication = tokenService.getAuthenticationByAccessToken(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().endsWith("tokens");
}

private String getAccessToken(HttpServletRequest request) {
String authHeaderValue = request.getHeader(AUTHORIZATION);
if (authHeaderValue != null) {
return extractToken(authHeaderValue);
}

return null;
}

private String extractToken(String authHeaderValue) {
if (authHeaderValue.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) {
return authHeaderValue.substring(BEARER_TYPE.length()).trim();
}

return null;
}
}

0 comments on commit 1d0890e

Please sign in to comment.