JWT를 구현하느라 여기저기 돌아다니며 힘들게 구현했고, 미천하지만 같은 내용을 찾는 분들에게 조금이나마 도움이 되고자 글을 작성하고자 한다.
이전에 JWT 도입 과정 글을 작성했지만, 내용이 모호하고 로직이 쓸데없이 복잡한 것 같아서 조금 간소화해보고자 한다. 마침 캡스톤 프로젝트 때 JWT를 적용해보지 못했기에 리팩터링할 겸 시간을 내게 됐다.

JwtAuthenticationFilter, JwtAuthorizationFilter 를 추가해줬다.

Proxy Filter Chain의 순서로, JwtAuthorizationFilter -> JwtAuthenticationFilter 로 이루어져 있는 것을 알 수 있다.
Header에 Token 값이 없는 경우, JwtAuthorizationFilter를 통과하고 JwtAuthenticationFilter 를 거쳐 로그인을 진행하도록 하고
Header에 Token 값이 있다면, JwtAuthorizationFilter에서 JWT를 인증하는 과정을 거쳐서 SecurityContextHolder에 Authentication 객체를 주입함으로써 인가를 완료할 예정이다.
JwtAuthenticationFilter는 1차적으로 로그인을 처리하는 Filter이다.
JwtAuthenticationFilter에서는 로그인 값들로 들어온 [username/password]를 가지고 인증 과정을 거친 후 Response Header에 JWT를 담아서 반환해주는 역할을 한다.
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
private final UserService userService;
private final long VALID_TIME = 1000L * 60 * 60; // 1시간
@Value("${jwt.secret}")
private String SECRET_KEY;
public JwtAuthenticationFilter(UserService userService) {
this.userService = userService;
setFilterProcessesUrl("/api/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("--JWT AUTHENTICATION FILTER--");
try {
UserLoginVO creds = new ObjectMapper().readValue(request.getInputStream(), UserLoginVO.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
null
));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String email = ((PrincipalDetails)authResult.getPrincipal()).getUsername();
UserResponse userResponse = userService.findByEmail(email);
String jwtToken = Jwts.builder()
.setSubject(userResponse.getEmail())
.setExpiration(new Date(System.currentTimeMillis() + VALID_TIME))
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
response.addHeader("token", jwtToken);
response.addHeader("username", userResponse.getUsername());
}
private Key getSecretKey() {
byte[] KeyBytes = SECRET_KEY.getBytes();
return Keys.hmacShaKeyFor(KeyBytes);
}
}
JwtAuthenticationFilter는 UsernamePasswordAuthenticationFilter를 상속받아서 구현했다. 가장 주의해야 할 점은, UsernamePasswordAuthenticationFilter의 default processing url이 /login으로 설정되어 있기에 url을 변경하고자 한다면 생성자에서 setFilterProcessingUrl메서드를 통해서 수정을 해주어야 한다.JWT의 subject 값으로는 email을 넣어주고, 유효기간은 1시간으로 설정했다.JwtAuthorizationFilter는 JWT의 인가를 처리하는 역할을 한다. JwtAuthenticationFilter에 앞서서 처리해야 하는 이유는 SecurityContextHolder에 Authentication을 주입해줌으로써 인가 처리를 해주기 위함이다.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter{
@Value("${jwt.secret}")
private String SECRET_KEY;
private final UserService userService;
private final PrincipalDetailsService principalDetailsService;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager
, UserService userService
, PrincipalDetailsService principalDetailsService) {
super(authenticationManager);
this.userService = userService;
this.principalDetailsService = principalDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
String tokenHeader = request.getHeader("Authorization");
String jwtToken = null;
if(StringUtils.hasText(tokenHeader) && tokenHeader.startsWith("Bearer")) {
jwtToken = tokenHeader.replace("Bearer ", "");
}
if(jwtToken != null && isValid(jwtToken)) {
SecurityContextHolder.getContext().setAuthentication(getAuth(jwtToken));
}
}catch (Exception e) {
throw new RuntimeException(e);
}
chain.doFilter(request, response);
}
private Authentication getAuth(String jwtToken) {
PrincipalDetails user = (PrincipalDetails)principalDetailsService.loadUserByUsername(getEmail(jwtToken));
return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
}
private String getEmail(String jwtToken) {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(jwtToken).getBody()
.getSubject();
}
private boolean isValid(String jwtToken) {
boolean ret = true;
Jws<Claims> jws = null;
try {
jws = Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(jwtToken);
if( jws == null ||
jws.getBody().getSubject() == null ||
jws.getBody().getExpiration().before(new Date())) {
ret = false;
}
}catch (Exception e) {
ret = false;
}
return ret;
}
private Key getSecretKey() {
byte[] keyBytes = SECRET_KEY.getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService{
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
log.info("email :: {}",email);
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("회원 없음"));
log.info("LOAD USER BY USERNAME = USER : {}, {}",user.getEmail(), user.getPassword());
return new PrincipalDetails(user);
}
}
AuthenticationProvider가 호출할 loadUserByUsername 메서드다. DB에 존재하는 데이터를 가져와서 JwtAuthenticationFilter에서 만들어서 return 했었던 UsernamePasswordAuthenticationToken와 비교하는 과정을 거치게 된다.@Slf4j
public class PrincipalDetails implements UserDetails{
private User user;
public PrincipalDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(user.getRole().name()));
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails의 구현체다.
와 진짜 감사합니다 필터이용해서 구현중에 안 풀리는 문제가 있었는데 덕분에 해결했습니다