Filter 란 Web Application 에서 관리되는 영역으로써 Spring Boot Framework 에서 클라이언트로부터 오는 요청(req) / 응답(res) 에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 요청(req) / 응답(res) 의 정보를 변경하거나, Spring 에 의해서 데이터가 변환되기 전의 순수한 클라이언트의 요청(req) / 응답(res) 값을 확인할 수 있다.
유일하게, ServletRequest, ServletResponse 의 객체를 변환할 수 있다.
주로 Spring Framework 에서는 request / response 의 Logging 용도로 활용하거나, 인증과 관련된 Logic 들을 해당 Filter 에서 처리한다.
이를 선/후 처리 함으로써, Service business logic 과 분리 시킨다.
Interceptor
Interceptor 란 Filter 와 매우 유사한 형태로 존재하지만, 차이점은 Spring Context 에 등록된다. AOP 와 유사한 기능을 제공할 수 있으며, 주로 인증 단계를 처리하거나 Logging 을 하는데 사용한다.
Filter 와 비슷하게 선 / 후 처리 함으로써, Service business logic 과 분리 시킨다.
JWT 토큰을 발급하고 인증에 대한 처리를 할 때 Filter 를 사용할 수 있다.
실제 구성은 위와 같으며, 실제 사용되는 부분은 utils 패키지 내부 클래스와 config 패키지의 SecurityConfig 에서 사용했다.
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
// JWT를 생성하고 검증하는 컴포넌트
private String secretKey = "thisissecretkey";
// 토큰 유효시간 30분
private long tokenValidTime = 30 * 60 * 1000L;
private final UserDetailsService userDetailsService;
// 객체 초기화, secretKey를 Base64로 인코딩한다.
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
// JWT 토큰 생성
public String createToken(String userPk) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
claims.put("userPk", userPk); // 정보는 key / value 쌍으로 저장된다.
Date now = new Date();
return Jwts.builder()
.setHeaderParam("typ","JWT")
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
return request.getHeader("Authorization");
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
JwtTokenProvider 는 JWT 를 생성하고 검증하는 객체이다.
토큰 유효시간을 설정할 수 있고, 내가 임의로 만든 문자열을 Base64 를 사용하여 인코딩한 후 JWT 를 생성할 때 암호화 알고리즘의 값으로 넣어줄 수 있다. 토큰을 사용한 모든 유틸들이 이 객체에 있다고 생각하면 된다.
JWT 에 대한 자세한 정보는 구글링을 하면 나오므로 여기서는 생략을 한다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
// JwtTokenProvider 컴포넌트를 완성했지만 실제 이 컴포넌트를 이용하는 것은 인증작업을 진행하는 filter
// 이 필터는 검증이 끝난 jwt 유저로부터 유저정보를 받아와서 UserNamePasswordAuthenticationFilterfh 전달해야함
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 헤더에서 JWT 를 받아옵니다.
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 유효한 토큰인지 확인합니다.
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext 에 Authentication 객체를 저장합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
먼저 JwtAuthenticationFilter 는 GenericFilterBean 을 상속받아 doFilter 를 오버라이드해 로직을 작성해준다.
@RequiredArgsConstructor 를 사용해서 JwtTokenProvider 를 DI 받는다.
Filter 는 요청(req) / 응답(res) 를 맨 처음으로 처리할 수 있는 부분이다. 즉, 요청이 오면 제일 처음 필터가 응답을 받는 것이라고 생각하면 된다.
JwtTokenProvider 의 resolveToken 메소드를 사용하여 Header 에서 JWT 토큰을 가져온다.
if 문에서 토큰이 존재하는지 확인하고, JwtTokenProvider 의 validateToken 메소드를 사용하여 토큰이 유효한지 검증을 한다. 둘 중 하나라도 참이 아닐 경우 인증을 하지 않는다.
토큰이 유효할 경우 토큰으로부터 유저 정보를 받아오고 SecurityContext 에 유저 정보를 갖고 있는 Authentication 객체를 저장한다.
SecurityContext 에 Authentication 객체가 저장되어 있으므로 어느 곳에서든 SecurityContext 에 접근하여 유저 정보를 얻을 수 있다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 필터 등록하고 필요한 부분 채워 넣기
private final JwtTokenProvider jwtTokenProvider;
// 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// authenticationManager를 Bean 등록합니다.
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
.csrf().disable() // csrf 보안 토큰 disable처리.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
.and()
.authorizeRequests() // 요청에 대한 사용권한 체크
.antMatchers("/api/**").authenticated() // /api 가 접두사로 오는 모든 요청이 인증되어야 실행할 수 있다.
.antMatchers("/login/**","/join/**").permitAll() // 그외 나머지 요청은 누구나 접근 가능
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);
// JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**"); //.addResourceLocations( "classpath:static/assets/" );
web.ignoring().antMatchers("/css/**"); //.addResourceLocations( "classpath:static/css/" );
web.ignoring().antMatchers("/images/**"); //.addResourceLocations( "classpath:static/images/" );
web.ignoring().antMatchers("/js/**"); //.addResourceLocations( "classpath:static/js/" );
// For Swagger-ui
web.ignoring().antMatchers("/swagger**");
web.ignoring().antMatchers("/v2/api-docs/**");
}
}
어플리케이션의 보안을 담당하는 SecurityConfig 파일에서 JwtTokenProvider 를 DI 하여 사용할 수 있도록 세팅한다.
핵심은 configure 메소드 부분이다.
.authorizeRequests() 를 사용하여 요청에 대한 사용권한 체크를 해준다. 그 후 .addFilterBefore() 를 사용하여 request, 요청이 올 때 Filter 에서 먼저 처리를 하도록 JwtAuthenticationFilter 와 JwtTokenProvider 를 등록해준다.
그러면 위에서 설정한 JWT 등록, 유효성 검증에 대한 부분들을 request 가 오면 맨 처음에 처리를 할 수 있게 된다.