Spring JWT에 Authentication 설정을 하도록 한다.
package com.palindrome.studit.global.config.security.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Builder
@AllArgsConstructor
public class JwtUserDetails implements UserDetails {
private Long userId;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return userId.toString();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Spring Authentication의 UserDetails를 상속받은 Custom User Details인 JwtUserDetails를 추가한다.
package com.palindrome.studit.global.config.security.config;
import com.palindrome.studit.domain.user.exception.InvalidTokenException;
import com.palindrome.studit.global.config.security.application.TokenService;
import com.palindrome.studit.global.config.security.dto.JwtUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final TokenService tokenService;
@Value("${jwt.type}")
private String tokenType;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String token = extractToken(servletRequest);
if (token != null && tokenService.validateToken(token)) {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String extractToken(ServletRequest request) {
String token = ((HttpServletRequest) request).getHeader("Authorization");
if (token != null && !token.isBlank() && token.startsWith(tokenType)) {
return token.substring(tokenType.length() + 1);
}
return null;
}
public Authentication getAuthentication(String token) {
try {
Long subject = Long.parseLong(tokenService.parseSubject(token));
JwtUserDetails user = JwtUserDetails.builder().userId(subject).build();
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (InvalidTokenException e) {
throw new AccessDeniedException("잘못된 엑세스 토큰입니다.");
}
}
}
JwtAuthenticationFilter 에서는 doFilter 함수에서 헤더의 Authorization과 Bearer를 검증하고 내부 엑세스 토큰을 검증한다.
여기서 주의해야할 것이 doFilter에서 토큰이 잘못되었다고 해서 에러를 던지지 않고 정상적으로 다음 필터가 실행되도록 하는 것이다.
만약 토큰이 잘못되었다고 에러를 발생시키면 헤더가 필요한 요청 뿐만 아니라 엑세스 토큰이 필요없는 모든 요청에 대해서 ExceptionHandler가 동작하고 설정한 AuthenticationEntryPoint가 동작하게 된다.
이후 SecurityFilter에
package com.palindrome.studit.global.config.security.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((request) -> request
.requestMatchers("/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/studies").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
return http.build();
}
}
위와 같이 설정하면 된다.
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) 를 통해 UsernamePasswordAuthenticationFilter 필터 이전에 설정한 jwt filter를 거치도록 하고, 인증이 필요없는 부분에는 .requestMatchers(HttpMethod.GET, "/studies").permitAll()와 같이 인증을 허가해주도록 한다.
package com.palindrome.studit.domain.study.api;
import com.palindrome.studit.domain.study.application.StudyService;
import com.palindrome.studit.domain.study.dto.CreateStudyDTO;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
@RequestMapping("/studies")
public class StudyController {
private final StudyService studyService;
@PostMapping
public ResponseEntity<Object> createStudy(Authentication authentication, @Valid @RequestBody CreateStudyDTO createStudyDTO) {
studyService.createStudy(Long.parseLong(authentication.getName()), createStudyDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
그러면 위와 같이 Authentication authentication 구문을 통해 Authentication 객체를 가져올 수 있고 저장한 accessToken의 sub 부분을 getName을 통해 읽어들일 수 있다.