본문 바로가기
web/SpringBoot

Spring Boot 3.1.x 으로 RestFul API 서버 만들기(2)

by 뽀리님 2023. 8. 28.

지난 시간에 이어 오늘은 JWT 생성과, Spring security 필터 설정을 간단히 해보겠다.

 

일단 JWT를 알아보기전에 익혀두어야 할 개념이 있다. (아랫글 참조)

https://ssmyefrin.tistory.com/11

 

HTTP 인증에 대한 처리 방식

우리가 보통 Web 을 개발할때, 유저가 누구인지 확인하는 로그인(Authentication) 절차는 필수이다. 이 때 보통 Http 요청 방식으로 많이 처리하게 되는데 나는 서버기반인증 과 토큰기반인증 크게 2개

ssmyefrin.tistory.com

 

나는 토큰인증방식을 사용할 예정이고,  토큰 발행과 검증을 위한 클래스를 생성 후 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 에 대한 흐름은 아래와 같다.

 

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)

 

출처 : 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()
)

 

그리고나서 구동하고 접속했더니 갑자기 접속이 안된다!?

 

403 에러가뜬다.

 

모든 요청에 대해 규칙을 설정했으므로 예외 규칙을 설정해줘야한다.

 

.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

 

Authorize HttpServletRequests :: Spring Security

While using a concrete AuthorizationManager is recommended, there are some cases where an expression is necessary, like with or with JSP Taglibs. For that reason, this section will focus on examples from those domains. Given that, let’s cover Spring Secu

docs.spring.io

 

다음시간엔 로그인처리, 권한관리를 해보겠다.

 

 

참조

https://jaehoney.tistory.com/348 

https://velog.io/@wjdgkrud/OncePerRequestFilter%EB%9E%80