이전까지는 API로 프로젝트를 해봤다면 JSP나 Thymeleaf를 통한 템플릿 엔진으로 서버사이드 렌더링을 활용해보기로 했다.
그 과정에서 당연히 서비스의 기본이 되는 로그인 기능부터 구현해보기로 하였다.
당연히 인증 방식에 대해선 세션을 통한 인증, 토큰을 통한 인증 둘 중 하나를 고려해야한다.
토큰 기반 인증 방식과 세션 기반 인증 방식은 각각 장단점을 가지고 있다. ChatGPT 및 여러 블로그를 참조한 자료이다!
토큰 중 하나인 JWT는 사용자 인증 정보와 토큰의 발급시각, 만료시각, 토큰의 ID등 담겨있는 정보가 세션 ID에 비해 비대하므로 세션 방식보다 훨씬 더 많은 네트워크 트래픽을 사용한다.
일반적으로 웹 어플리케이션의 서버 확장 방식은 수평 확장을 사용한다. 수평적 vs 수직적
즉, 한대가 아닌 여러대의 서버가 요청을 처리하게 된다. 이때 별도의 작업을 해주지 않는다면, 세션 기반 인증 방식은 세션 불일치 문제를 겪게 된다.
만약 세션 정보를 공유하지 않으면 사용자가 한 서버에서 로그인한 후에 다른 서버로 이동했을 때 로그인 상태가 유지되지 않게된다.
다중 서버 환경에서 세션 기반 인증을 사용할 때는 세션 정보를 공유하기 위해서 세션 공유방법이 필요하다!
모든 요청이 들어올 때 Filter에서 인증 유무를 판단하여 이후 로직을 실행할 것이다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
또한 Spring Security에서는 인증과 접근 제어 기능이 Filter로 구현되어진다.
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenProvider tokenProvider;
// h2 database 테스트가 원활하도록 관련 API 들은 전부 무시
// local 관련 파일 접속들 허용
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.antMatchers("/h2-console/**", "/favicon.ico", "/js/**", "/image/**", "/css/**", "/scss/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** security filter 설정 변경
* https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
* */
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
JwtFilter customFilter = new JwtFilter(tokenProvider);
// CSRF 설정 Disable, JWT는 CSRF 공격 걱정 ㄴㄴ
http.csrf().disable()
// h2-console 을 위한 설정을 추가
.headers()
.frameOptions()
.sameOrigin()
// 세션 관련 설정 : 사용하지 않기때문에 stateless
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 모든 url 인증
.and()
.authorizeRequests()
// 허용 url 설정
.antMatchers("/member/auth/**").permitAll() // 로그인, 회원가입 관련
.antMatchers("/").permitAll() // 홈화면
.anyRequest().authenticated()
.and() // 지정된 필터 앞에 커스텀 필터를 추가 (UsernamePasswordAuthenticationFilter 보다 먼저 실행된다)
.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
SecurityConfig에서 인증 설정을 구성하고 허용할 범위를 설정한다.
나는 여기서 마지막 부분의 addFilterBefore를 통해 Customfilter인 jwtfilter를 적용할 것이다.
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER = "Authorization";
private final TokenProvider tokenProvider;
public JwtFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
private String resolveToken(HttpServletRequest request){ // 토큰정보 획득, 쿠키값 이슈로 'Bearer ' 변경
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer_")){
return bearerToken.substring(7);
}
return null;
}
private String resolveCookie(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
log.info("쿠키 값 찾기 ={}",cookie.getValue().substring(7));
return cookie.getValue().substring(7);
}
}
}
log.info("쿠키 못꺼내옴");
return null;
}
@Override
protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
// Request Header 에서 토큰을 꺼냄
// String jwt = resolveToken(servletRequest);
// 1. 쿠키에서 값 꺼내야 됨
String jwt = resolveCookie(servletRequest);
log.info("jwt 값={}",jwt);
String requestURI = servletRequest.getRequestURI();
// 2. validateToken 으로 토큰 유효성 검사
// 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장
if(StringUtils.hasText(jwt)&& tokenProvider.validateToken(jwt)){
Authentication authentication = tokenProvider.getAuthentication(jwt);
// 유저 저장.
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Security Context, Member_ID ='{}' 인증정보 저장 및 조회 , uri= {} ",authentication.getName(),requestURI);
}else {
log.debug("통과 url : {} ",requestURI);
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
OncePerRequestFilter는 그 이름에서도 알 수 있듯이 모든 서블릿에 일관된 요청을 처리하기 위해 만들어진 필터이다.
이 추상 클래스를 구현한 필터는 사용자의 한번에 요청 당 딱 한번만 실행되는 필터를 만들 수 있다.
이 필터가 하는일은 JWT 토큰값에 대한 검증을 할 것이다. 인증이 필요한 URL 접속시 쿠키값에 들어있는 토큰값을 꺼내고 TokenProvider에게 전달한다. 그리고 SecurityContextHolder 저장하는데 우선 유저 정보를 저장한다고 생각하면 된다.
이 이후에는 어떻게 JWT 토큰을 복호화 하고 인증을 처리하는지 알아보자.