[Spring Security] JWT 필터 적용 과정

신명철·2022년 3월 2일
12

Spring Security

목록 보기
4/6

정말 많은 우여곡절을 겪으며 만든 JWT 인증 과정을 기록하기 위해서 포스팅을 한다....

에러

the filter class community.locals.config.jwt.jwtauthenticationfilter does not have
a registered order and cannot be added without a specified order. 
consider using addfilterbefore or addfilterafter instead.

addFilter 메서드로 OncePerRequest 필터를 상속한 JwtAuthenticationFilter를 등록하려고 했는데 위와 같은 에러메세지가 나왔다. 해당 에러는 내가 Security Filter Chain 에 무지했기 때문이다.

Spring Security 는 기본적으로 순서가 있는 Security Filter 들을 제공하고, Spring Security가 제공하는 Filter를 구현한게 아니라면 필터의 순서를 정해줘야 하기 때문에 위와 같은 에러가 발생한 것이였다.

각 필터의 역할 참고 자료

AuthroizationFilter

해당 필터는 UsernamePasswordAuthenticationFilter를 상속받기 때문에 해당 필터의 특징을 생각해야 한다. 내가 Jwt 필터를 구현하면서 제일 고생한 부분이 이 부분을 생각하지 못했기 때문이다. UsernamePasswordAuthenticationFilter 의 특징은 다음과 같다.

  1. 필터를 거치고 다음 필터로 동작을 넘기지 않는다. 인증 성공 여부에 따른 메서드successfulAuthentication/unSuccessfulAuthentication를 구현해야 하는 이유다
  2. 해당 필터는 /login에 접근할 때만 동작한다. 그렇기 때문에 내가 원하는 Url에서 필터가 동작하길 원한다면 setFilterProcessesUrl()로 Url를 설정해줘야 작동한다.

위와 같은 특징 때문에 Jwt 필터를 따로 만들어줘야 한다.

@Slf4j
public class JwtAuthorizationFilter extends UsernamePasswordAuthenticationFilter{
	
	private final AuthenticationManager authenticationManager;
	private final JwtTokenProvider jwtTokenProvider;
	
	public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
		this.authenticationManager = authenticationManager;
		this.jwtTokenProvider = jwtTokenProvider;
		
		setFilterProcessesUrl("/api/member/login");
	}
	
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		log.info("USERNAMEPASSWORD_FILTER");
		ObjectMapper om = new ObjectMapper();
		MemberLogin memberLogin = null;
		
		try {
			memberLogin = om.readValue(request.getInputStream(), MemberLogin.class);
		}catch (Exception e) {
			e.printStackTrace();
		}
		log.info("member : {}", memberLogin);
		UsernamePasswordAuthenticationToken authenticationToken = 
				new UsernamePasswordAuthenticationToken(memberLogin.getUsername(), memberLogin.getPassword());
		
		Authentication authentication = authenticationManager.authenticate(authenticationToken);
		
		return authentication;
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		
		PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
		
		String jwtToken = jwtTokenProvider.generateToken(principalDetails.getUsername());
		
		response.getWriter().write("Bearer " + jwtToken);
		response.getWriter().flush();
	}
	
}
  • 해당 필터는 Request Body 에 들어온 username, password(credential)를 받아서 DaoAuthenticationProvider에서 UserDetails 객체의 정보와 비교해 인증을 진행하게 된다.
  • 로그인에 성공하면 jwtToken을 생성해서 response 에 담아서 클라이언트에게 보낸다.

JwtTokenProvider

public class JwtTokenProvider {
	
	private final long VALID_MILISECOND = 1000L * 60 * 60; // 1 시간
	private final PrincipalDetailsService principalDetailsService;
	
	@Value("${jwt.secret}")
	private String secretKey;
	
	private Key getSecretKey(String secretKey) {
		byte[] KeyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
		return Keys.hmacShaKeyFor(KeyBytes);
	}

	private String getUsername(String jwtToken) {
		return Jwts.parserBuilder()
			.setSigningKey(getSecretKey(secretKey))
			.build()
			.parseClaimsJws(jwtToken)
			.getBody()
			.getSubject();
	}
	
	public boolean validateToken(String jwtToken) {
		try {
			log.info("validate..");
			Jws<Claims>  claims = Jwts.parserBuilder()
										.setSigningKey(getSecretKey(secretKey))
										.build()
										.parseClaimsJws(jwtToken);
			log.info("{}",claims.getBody().getExpiration());
			return !claims.getBody().getExpiration().before(new Date());
		}catch(Exception e) {
			return false;
		}
	}
	
	public Authentication getAuthentication(String jwtToken) {
		UserDetails userDetails = principalDetailsService.loadUserByUsername(getUsername(jwtToken));
		log.info("PASSWORD : {}",userDetails.getPassword());
		return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
	}
	
	
	public String generateToken(String username) {
		return Jwts.builder()
					.setSubject(username)
					.setIssuedAt(new Date())
					.setExpiration(new Date(new Date().getTime() + VALID_MILISECOND))
					.signWith(getSecretKey(secretKey), SignatureAlgorithm.HS256)
					.compact();
	}
}
  • 공식 문서에 parser 가 depreceated 됐고 대신 parserBuilder를 이용하라고 해서 parserBuilder 를 사용했다. 많이 사용하던 메서드가 depreceated 됐기 때문에 이 부분에서도 꽤 고생했다...
  • jwtToken 생성, 검증 역할을 한다.

JwtAuthenticationFilter

package community.locals.config.jwt;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.fasterxml.jackson.databind.ObjectMapper;

import community.locals.dto.MemberRegister;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	private final JwtTokenProvider jwtTokenProvider;
	

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		log.info("jwt Filter...");
		String jwtToken = parseJwt(request);
		log.info("jwtToken = {}",jwtToken);
		if (jwtToken != null && jwtTokenProvider.validateToken(jwtToken)) {
			Authentication auth = jwtTokenProvider.getAuthentication(jwtToken);
			SecurityContextHolder.getContext().setAuthentication(auth);
		}
		log.info("next Filter");
		filterChain.doFilter(request, response);
	}

	private String parseJwt(HttpServletRequest request) {
		String headerAuth = request.getHeader("Authorization");
		if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
			return headerAuth.substring(7, headerAuth.length());
		}
		return null;
	}
	
}
  • JwtAuthorizationFilter가 로그인을 담당/ 토큰 발급을 진행했고 JwtAuthenticationFilter는 토큰 인증을 담당한다.
  • Request Header에 담겨있는 jwtToken을 파싱해서 인증을 진행한다. 인증에 성공하면 SecurityContextHolder에 인증된Authentication 객체를 집어 넣음으로써 인가한다.
profile
내 머릿속 지우개

5개의 댓글

comment-user-thumbnail
2022년 6월 28일

Securityconfig에 addfiltter하셨을때 JwtAuthenticationFilter필터를 어느 부분에 추가하셨나요??

2개의 답글
comment-user-thumbnail
2022년 7월 15일

현재 JwtAuthenticationFilter에서 토큰검사하고, usernamePassword를 상속한 JwtAuthorizationFilter 가 실행되어서 인증처리하고싶은데, JwtAuthorizationFilter가 실행되지 않네요ㅠㅠ..
securityConfig파일을 잠깐 엿볼수있을까요??

1개의 답글