지난 시간에 이어 오늘은 JWT 생성과, Spring security 필터 설정을 간단히 해보겠다.
일단 JWT를 알아보기전에 익혀두어야 할 개념이 있다. (아랫글 참조)
https://ssmyefrin.tistory.com/11
나는 토큰인증방식을 사용할 예정이고, 토큰 발행과 검증을 위한 클래스를 생성 후 Security 설정을 하겠다.
1. build.gradle 에 JWT를 위한 라이브러리를 추가한다.
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
2. JwtTokenProvider.class 생성
private final Key key;
public JwtTokenProvider(@Value("${jwt.secret.key}") String secretKey) {
byte[] secretByteKey = DatatypeConverter.parseBase64Binary(secretKey);
this.key = Keys.hmacShaKeyFor(secretByteKey);
}
/**
* 토큰 생성 및 발급
* @param authentication
* @return
*/
public TokenDto generateToken(Authentication authentication) {
return TokenDto.builder()
.grantType("Bearer")
.accessToken(createAccessToken(authentication))
.refreshToken(createRefreshToken())
.build();
}
/**
* AccessToken 생성
* @param authentication
* @return
*/
public String createAccessToken(Authentication authentication){
return Jwts.builder()
.setSubject(authentication.getName())
.claim("auth", getAuthority(authentication))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 15)) // 15분
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* RefreshToken 생성
* @return
*/
private String createRefreshToken(){
return Jwts.builder()
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 36)) //36시간
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
토큰 생성과 검증 클래스는 만들었고, 매 요청시마다 인증체크를 해야하므로 Security 설정이 필요하다.
사용자 로그인 시 Spring Security 에 대한 흐름은 아래와 같다.
1. 사용자의 요청 정보 전송
2. AuthenticationFilter가 요청을 받아서 UsernamePasswordAuthenticationToken(인증용 객체)을 생성한다.
3~4. AuthenticationManager가 인증을 처리할 수 있는 AuthenticationProvider 에게 전달한다.
5~6. DB에 있는 사용자 정보와 요청받은 정보를 비교 후 loadUserByUsername() 메소드 수행
7~8. 성공시 Authentication 객체리턴
9. Authentication 객체를 SecurityContextHolder에 담은 이후 각 결과별 Handler 처리
3. AuthenticationFilter 를 Custom한 JwtAuthenticationFilter를 만들자.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 토큰추출
String token = resolveToken(request);
// 토큰 유효성 검사
if (StringUtils.hasText(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
/**
* 헤더에서 토큰 추출(먼가 소스가 맘에안듬)
* @param request
* @return
*/
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(Constants.AUTH_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.length() >= 7
&& bearerToken.startsWith(Constants.AUTH_GRANT_TYPE)) {
return bearerToken.substring(7);
}
return null;
}
}
Filter 동작 흐름
필터 동작은 요청 스레드가 서블릿 컨테이너에 도착하기전에 시행되는데, 이때 사용자의 요청 정보에 대한 검증과 필요한 데이터가 있다면 추가하거나 변경할 수 있다. 응답에 대한 정보도 변경이 물론 가능한데, 보통은 전역적으로 처리해야하는 인코딩과 보안과 관련해서 수행한다.
필터동작을 잘 설명한 그림이 있어서 참조하려고 가져왔습니다.(https://emgc.tistory.com/125)
OncePerRequestFilter
한 요청에 대해 한번만 실행하는 필터이다. 포워딩이 발생하면 필터 체인이 다시 동작되는데, 인증은 여러번 처리가 불필요하기에 한번만 처리를 할 수 있도록 도와주는 역할을 한다.
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException;
doFilterInternal
OncePerRequestFilter 안에 보면 doFilterInternal() 추상 함수가 있는데, 사용자가 직접 구현해줘야한다.
doFilter의 로직이 종료되면 파라미터로 넘겼던 ApplicationFilterChain객체의 doFilter메서드를 다시 호출하여 재귀적으로 다음 필터가 수행된다.
보통 OncePerRequestFilter를 상속받은 후 doFilter 메소드를 override하여 사용한다. 최초 실행시에 ServletRequest객체에 자신의 이름과 함께 true값을 함께 세팅해두고, doFilterInternal 메소드로 토큰을 추출하고 유효성검사를 한다. 그 후에 리다이렉트로 다시 실행되면 요청 객체에 담아뒀던 이전 수행에 대한 여부를 체크한다.
4. 작성한 필터를 Security 에 추가해주자.
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) // 인증필터추가
이렇게 추가한다고 끝난게 아니다.
우린 Http 요청시 로그인여부, 사용자 권한을 체크 해야하므로 authorizeHttpRequests를 추가해준다.
.authorizeHttpRequests( // 요청에 대한 인가 규칙설정
authorize -> authorize
.anyRequest()
.authenticated()
)
그리고나서 구동하고 접속했더니 갑자기 접속이 안된다!?
모든 요청에 대해 규칙을 설정했으므로 예외 규칙을 설정해줘야한다.
.authorizeHttpRequests( // 요청에 대한 인가 규칙설정
authorize -> authorize
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest()
.authenticated()
)
swagger 관련 endpoint 는 예외로 권한 허용처리(permitAll) 하였다.
그럼 잘 뜰 것이다.
authorizeHttpRequests 관련된 내용은 아래 공식 사이트에 잘 설명되어 있다.
https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
다음시간엔 로그인처리, 권한관리를 해보겠다.
참조
'web > SpringBoot' 카테고리의 다른 글
DataJpaTest 중 삽질해결 기록.... (0) | 2023.08.31 |
---|---|
Spring Boot 3.1.x 으로 RestFul API 서버 만들기(3) (0) | 2023.08.31 |
SpringBoot 구조와 원리 (0) | 2023.08.22 |
Spring Boot 3.1.x 으로 RestFul API 서버 만들기(1) (1) | 2023.08.21 |
Spring Boot 3.1.x 환경세팅부터 Swagger 까지(2) (0) | 2023.08.17 |