자세한 예외 처리 로직이나 DTO는 생략했습니다.
Spring Security 기반으로 JWT토큰을 구현할 경우
기존의 Security의 세션&쿠키 방식의 통신에서 설정을 변경해줘야 한다.
- Config 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/h2-console/**")
.permitAll()
.antMatchers("/v1/**")
.permitAll()
.and()
.headers()
.frameOptions().sameOrigin() // H2 Page XFrame Error
.and()
// form 로그인 비활성화
.formLogin().permitAll()
// Session 기반 미사용
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.and()
.csrf().disable();
}
우선적으로 FormLogin을 비활성화를 설정하며, sessionManagement 설정을
변경해서 Stateless로 설정해줘야 한다.
2. JwtAuthenticationFilter
@Log4j2
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
// 생략
/*
로그인시 가장 먼저 호출된다.
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
AttemptLoginDto attemptLoginDto = new ObjectMapper().
readValue(request.getInputStream(), AttemptLoginDto.class);
return getAuthenticationManager().authenticate(attemptLoginDto.bindToAuthenticationToken());
} catch (IOException e) {
// TODO : 예외 추가
throw new RuntimeException(e);
}
}
// 인증이 성공(UserDetailsService)되면 해당 메서드 호출
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
log.info("success Login...");
ResponseLoginDto authenticationUser = (ResponseLoginDto) authResult.getPrincipal();
String jwtToken = JwtProvider.createToken(authenticationUser.getEmail());
log.info("jwtToken : {}", jwtToken);
response.setHeader("token", jwtToken);
}
}
UsernamePasswordAuthenticationFilter 를 상속받아 구현했다.
Security의 여러개의 필터중 하나로 다음과 같은 역할을 한다.
- attemptAuthentication : 최초 로그인 요청시 request 로 전달된 데이터를 사용해서 AuthenticationManager를 통해 인증을 요청합니다.
- successfulAuthentication : 내부동작 이후 인증을 마친 뒤 AuthenticationManager를 통해 인증이 완료된 토큰을 받은 뒤에 진행되는 로직을 작성합니다.
기본적으로 로그인은 웹 페이지라고 가정했을 때, <form> 태그를 이용해서 사용합니다.
그리고 이 방식은 기존에 Security가 사용하는 기본 동작 방식입니다.
API 개발을 위해 사용할 경우 form 태그를 통한 요청이 아니라 대부분 JSON 형태의 데이터를 전달하기 때문에, UsernamePasswordAuthenticationFilter 를 구현해서 사용합니다.
그리고, 인증 이후에 JWT 토큰을 발급해줘야 하기 때문에 successfulAuthentication 의 로직에서 JWT 토큰을 생성하는 메서드를 호출합니다.
3. JwtAuthenticationFilter 등록
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
직접 구현한 JwtAuthenticationFilter를 등록합니다.
4. Jwt 유효성 검사.
유효성을 검사하는 로직을 구현하는 부분은 여러가지가 있겠지만,
인터셉터를 사용해서 특정한 URL에서 동작하도록 설정합니다.
public class JwtTokenCheckInterceptor implements HandlerInterceptor {
private static final String AUTHENTICATION_HEADER_PREFIX = "Bearer ";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
log.info("preHandle....");
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("header : {}", header);
if(Objects.isNull(header) || !header.contains(AUTHENTICATION_HEADER_PREFIX) || header.isEmpty()){
// TODO : 예외발생
log.info("Objects.isNull(header) || !header.contains(AUTHENTICATION_HEADER_PERFIX) || header.isEmpty() ..");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false;
}
String token = header.replace("Bearer ", "");
log.info("token ... : {}", token);
boolean jwtValid = JwtProvider.isJwtValid(token);
log.info("jwtValid : {}", jwtValid);
if(!jwtValid) {
// TODO : 예외발생
log.info("Not Valid JWT Token");
response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
return false;
}
return true;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtTokenCheckInterceptor())
.addPathPatterns("/v1/todo/**");
}
@Bean
public JwtTokenCheckInterceptor jwtTokenCheckInterceptor() {
return new JwtTokenCheckInterceptor();
}
}
5. CORS 설정
서로 다른 위치의 서비스 끼리 호출할 때, 주로 발생하는 문제라고 합니다.
해당 설정은 서버 뿐만 아니라 클라이언트에서도 추가적인 Header를 작성해서 요청을 해야 합니다.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Key, Authorization");
if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(request, response);
}
}
}
'Back-End > Security' 카테고리의 다른 글
Spring Security 동작 및 구조에 대한 심화 학습 (0) | 2022.09.06 |
---|---|
Spring Security + JWT (1) (0) | 2022.04.15 |
Spring Security 동작 과정 (0) | 2022.04.15 |
서버와 토큰 기반 인증 시스템의 차이 (0) | 2022.04.15 |