정말 많은 우여곡절을 겪으며 만든 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필터를 어느 부분에 추가하셨나요??