[Spring Security] JWT로그인 구현 - 2. JWT 필터, TokenProvider
JWT 로그인을 구현하면서 이해한 내용을 정리해보았다.
인프런에서 제공하는 아래 무료 강의를 보고 작성한 코드이다.
[무료] 스프링부트 시큐리티 & JWT 강의 - 인프런 | 강의
스프링부트 시큐리티에 대한 개념이 잡힙니다., 스프링부트 시큐리티,제가 공부했던 걸 쉽게 설명드릴게요 🙂 스프링부트 시큐리티 github https://github.com/codingspecialist/-Springboot-Security-OAuth2.0-V3 htt
www.inflearn.com
나는 프론트엔드 부분을 추가했기 때문에 JWT 필터 부분은 강의와 같지만 컨트롤러가 좀 다르다.
그리고 회원 엔티티도 강의와 다르게 만들었기 때문에 UserDetailsService 쪽도 차이가 있을 것이다.
이 글은 그냥 JWT 구현 방식의 구조에 대해 이해하기 위해서 참고만 하면 좋을 것 같다. 그래도 아무것도 모르고 코드를 따라만 치던 내가 이걸 이해한 끝에 작성한 글이니 읽어보면 도움은 될 것이라고 믿는다.
1편
https://bbazackcoding.tistory.com/10
[Spring Security] JWT로그인 구현 - 1. 필터 구조, CustomUserDetailsService
JWT 로그인을 구현하면서 이해한 내용을 정리해보았다. 인프런에서 제공하는 아래 무료 강의를 보고 작성한 코드이다. https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB
bbazackcoding.tistory.com
코드 설명 전 전체적인 구조를 파악하고 가는 것이 좋을 것 같아 정리해보았다.
1. 필터 구조
가장 핵심이 되는 시큐리티 필터 구조이다.
Spring Security 필터 중 Authentication과 관련된 필터는 일부이고, 그 중 가장 먼저 등장하는 것이 UsernamePasswordAuthentication filter이다. 그 앞에 JWT 필터를 끼워넣었다.
Security filterchain 구조. 이 중 건드린 것은 JWT filter와 UsernamePasswordAuthentication filter 뿐이기에 두 개만 크게 표시해두었다.
2. JWT 필터
이 경우는 1편과 달리 구현할 것이 많다. Filter는 물론, AuthenticationProvider와 AccessDeniedHandler, Entrypoint까지 직접 구현해야 한다.
1편에서는 AuthenticationProvider을 직접 생성하지 않았는데 왜 JWT 필터에서는 직접 구현해야 할까?
그 이유는 1편에서 사용한 UsernamePassword~~ 필터, 매니저, 프로바이더는 모두 스프링에서 제공되는 Authentication 관련 요소들이기 때문이다. 그래서 필요한 부분만 오버라이드 해서 고쳐주면 됐다.
하지만 JWT 필터는 제공되는 필터가 아니기에 filter와 provider을 직접 생성해야 한다.
2. 구현 : JWT 필터 만들기
아래는 JWT 필터 코드 전문이다.
HttpRequest의 헤더에 실린 jwt 토큰이 곧 Authentication 객체이다.
AuthenticationManager와 AuthenticationProvider는 없지만 대신 TokenProvider가 인증 처리를 해준다.
TokenProvider에서 true를 리턴하면, 즉 인증을 해준다면 JWT 필터에서 이제서야 Authentication 객체를 만들고 이를 SecurityContextHolder에 저장한다.
public class JwtFilter extends GenericFilterBean {
private static final Logger logger= LoggerFactory.getLogger(JwtFilter.class);
public static final String AUTHORIZATION_HEADER="Authorization";
private TokenProvider tokenProvider;
public JwtFilter(TokenProvider tokenProvider){
this.tokenProvider=tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest httpServletRequest=(HttpServletRequest) servletRequest;
String jwt=ResolveToken(httpServletRequest);
String requestURI=httpServletRequest.getRequestURI();
logger.debug("JwtFilter-doFilter");
System.out.println("JwtFilter-doFilter");
//유효한 토큰이면 권한을 줌
if(StringUtils.hasText(jwt)&& tokenProvider.validateToken(jwt)){
//jwt 토큰에 기록되어 있던 사용자의 권한 정보를 가져옴
Authentication authentication= tokenProvider.getAuthentication(jwt);
//security context에 이 사용자의 정보를 저장함
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.debug("JwtFilter-doFilter : SecurityContext에 '{}' 인증 정보를 저장했음. url: {}",authentication.getName(),requestURI);
System.out.println("JwtFilter-doFilter : SecurityContext에 인증 정보를 저장했음. url:"+authentication.getName()+requestURI);
System.out.println("이 사용자의 정보:"+authentication.getName()+authentication.getAuthorities());
}
else{
logger.debug("JwtFilter-doFilter : 유효한 Jwt 토큰이 없음. url: {}",requestURI);
System.out.println("JwtFilter-doFilter : no JWT token. url: {}"+requestURI);
}
filterChain.doFilter(servletRequest,servletResponse);
}
private String ResolveToken(HttpServletRequest httpServletRequest) {
logger.debug("JwtFilter-ResolveToken");
System.out.println("JWTFilter-resolvetoken");
String bearerToken= httpServletRequest.getHeader(AUTHORIZATION_HEADER);
System.out.println(bearerToken);
//jwt 토큰은 bearer로 시작함
if(StringUtils.hasText(bearerToken)&&bearerToken.startsWith("Bearer ")){
logger.debug("JwtFilter-ResolveToken:succeed");
System.out.println("JWTFilter-ResolveToken:succeed");
//bearer 뒤부터 유의미한 정보이기 때문에
return bearerToken.substring(7);
}
System.out.println("JWTFilter-Resolvetoken:fail");
return null;
}
}
3. 구현 : TokenProvider 만들기
@Component
public class TokenProvider implements InitializingBean {
//로그를 찍을 수 있도록 해줌.
private final Logger logger= LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY="auth";
private final String secret;
private final long tokenValidityInMilliseconds;
private Key key;
public TokenProvider(@Value("${jwt.secret}")String secret, @Value("${jwt.token-validity-in-seconds}")long tokenValidityInMilliseconds) {
this.secret = secret;
this.tokenValidityInMilliseconds = tokenValidityInMilliseconds;
}
//다른 속성들이 다 설정되고 객체가 생성된 후 실행됨
@Override
public void afterPropertiesSet(){
logger.debug("TokenProvider-afterPropertiesSet");
System.out.println("TokenProvider-afterPropertiesSet");
byte[] KeyBytes= Base64.getDecoder().decode(secret);
this.key=Keys.hmacShaKeyFor(KeyBytes);
}
//
public String createToken(Authentication authentication){
logger.debug("TokenProvider-createToken");
System.out.println("TokenProvider-createToken");
String authorities=authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)//GrantedAuthority 클래스 내의 getAuthority를 호출하여 이를 스트링 타입으로 변환
.collect(Collectors.joining(",")); //얻은 스트링들을 ,로 연결한 후 반환
long now=(new Date()).getTime();
Date validity=new Date(now+this.tokenValidityInMilliseconds);
//jwt의 페이로드에 담기는 내용
//claim: 사용자 권한 정보와 데이터를 일컫는 말
return Jwts.builder()
.setSubject(authentication.getName()) //토큰 제목
.claim(AUTHORITIES_KEY,authorities) //토큰에 담길 내용
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}
//유효한 jwt 토큰을 가지고 있는 자에게 권한을 줌.
public Authentication getAuthentication(String token){
logger.debug("TokenProvider-getAuthentication");
System.out.println("TokenProvider-getAuthentication");
Claims claims=Jwts
.parserBuilder() //받은 token을 파싱할 수 있는 객체(JwtParserBuilder)를 리턴
.setSigningKey(key) //ParserBuilder의 key 설정
.build() //ParserBuilder을 통해 Parser 리턴.
.parseClaimsJws(token) //토큰을 파싱하여
.getBody(); //body를 리턴함
Collection<? extends GrantedAuthority> authorities= Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
Member principal=new Member(claims.getSubject(),"", authorities);
System.out.println(claims.getSubject());
System.out.println("auth : "+authorities);
//일반적인 세션 기반의 id/pw 인증방식이었다면 token에 pw가 담겼을 것...?
//jwt 로그인에서는 발급받은 jwt 토큰인 token을 기반으로 인증하기 때문에 credential에 token이 담김
//얘는 인증받은 토큰을 리턴함.
return new UsernamePasswordAuthenticationToken(principal,token,authorities);
}
public boolean validateToken(String token){
logger.debug("TokenProvider-validateToken");
System.out.println("TokenProvider-validateToken");
try{
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch(io.jsonwebtoken.security.SecurityException|MalformedJwtException e){
logger.info("잘못된 JWT 서명입니다.");
System.out.println("1 JWT");
} catch(ExpiredJwtException e){
logger.info("2 JWT");
System.out.println("만료된 JWT 토큰입니다");
}catch(UnsupportedJwtException e){
logger.info("지원되지 않는 JWT 토큰입니다");
System.out.println("3 JWT");
}catch(IllegalArgumentException e){
logger.info("JWT 토큰이 잘못되었습니다");
System.out.println("4 JWT");
}
return false;
}
}
4. 구현 : SecurityFilterChain에 필터 추가하기 - JWTSecurityConfig
이렇게 JWTSecurityConfig를 작성해주고
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenProvider tokenProvider;
public JwtSecurityConfig(TokenProvider tokenProvider){
this.tokenProvider=tokenProvider;
}
@Override
public void configure(HttpSecurity httpSecurity){
JwtFilter customFilter=new JwtFilter(tokenProvider);
httpSecurity.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
이런 방식으로 전체 SecurityConfig에서 JwtSecurityConfig를 적용해주면 된다.