정말 많은 우여곡절을 겪으며 만든 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를 구현한게 아니라면 필터의 순서를 정해줘야 하기 때문에 위와 같은 에러가 발생한 것이였다.
해당 필터는 UsernamePasswordAuthenticationFilter를 상속받기 때문에 해당 필터의 특징을 생각해야 한다. 내가 Jwt 필터를 구현하면서 제일 고생한 부분이 이 부분을 생각하지 못했기 때문이다. UsernamePasswordAuthenticationFilter 의 특징은 다음과 같다.
successfulAuthentication/unSuccessfulAuthentication를 구현해야 하는 이유다/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();
}
}
DaoAuthenticationProvider에서 UserDetails 객체의 정보와 비교해 인증을 진행하게 된다.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();
}
}
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는 토큰 인증을 담당한다.SecurityContextHolder에 인증된Authentication 객체를 집어 넣음으로써 인가한다.
Securityconfig에 addfiltter하셨을때 JwtAuthenticationFilter필터를 어느 부분에 추가하셨나요??