본문 바로가기

Spring

[Spring Boot JWT Tutorial - 인프런] 스프링 JWT 적용하기 part2. JWT 관련 설정하기

 

인프런의 Spring Boot JWT Tutorial의 강의를 보고 정리해 보았습니다.

 

[무료] Spring Boot JWT Tutorial - 인프런 | 강의

Spring Boot, Spring Security, JWT를 이용한 튜토리얼을 통해 인증과 인가에 대한 기초 지식을 쉽고 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

이전 파트에서 스프링 시큐리티 적용을 위한 초기 세팅 방법에 대해 기록하였다.

[Spring Boot JWT Tutorial - 인프런] 스프링 JWT 적용하기 part1. 초기 세팅


1. TokenProvider

  • 토큰 생성
  • 토큰 유효성 검증
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class TokenProvider implements InitializingBean {

    private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
    private static final String AUTHORITIES_KEY = "auth";
    private final String secret;
    private final long tokenValidityInMilliseconds;

    private Key key;

    public TokenProvider( @Value("${jwt.secret}") String secret,
                          @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 빈이 생성이 되고 주입을 받은 후에 secret 값을 Base64 Decode해서 key변수에 할당
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    // Authentication 객체의 권한 정보를 이용해서 토큰을 생성한다.
    public String createToken(Authentication authentication) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        //application.yml에서 설정했던 만료시간을 설정
        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenValidityInMilliseconds);

        //JWT 토큰 생성 후 리턴
        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();
    }

    // 토큰을 파라미터로 받아서 Token에 담겨있는 정보를 이용해 Authentication 객체를 리턴하는 메소드
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        // claims의 권한정보를 통해 유저 객체 생성
        User principal = new User(claims.getSubject(), "", authorities);
        //                                             유저객체,  토큰,   권한정보
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    // 토큰의 유효성 검증을 수행
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;  //문제가 없으면 true 반환
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            logger.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            logger.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            logger.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            logger.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;  //문제가 있으면 false 반환
    }
}


2. JwtFilter

  • JWT 커스텀 필터
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class JwtFilter  extends GenericFilterBean {

    private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
    public static final String AUTHORIZATION_HEADER = "Authorization";
    private TokenProvider tokenProvider;
    public JwtFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    //GenericFilterBean의 doFilter 오버라이드하여 해당 메서드에 필터링 로직을 작성한다
    //JWT Token의 인증정보를 SecurityContext에 저장하는 역할 수행
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String jwt = resolveToken(httpServletRequest);  //아래의 메서드를 통해 토큰 정보를 가져옴
        String requestURI = httpServletRequest.getRequestURI();

        // jwt에 텍스트가 있고 TokenProvider의 유효성 검증을 확인
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);  //SecurityContext에 저장
            logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

    //Request의 Header에서 토큰 정보를 꺼내오기 위한 메서드
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }

        return null;
    }
}


3. JwtSecurityConfig

  • TokenProvider, JwtFilter를 SecurityConfig에 적용하기 위해 사용하는 클래스
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private TokenProvider tokenProvider;
    public JwtSecurityConfig(TokenProvider tokenProvider) { // tokenprovider 주입
        this.tokenProvider = tokenProvider;
    }


    @Override
    public void configure(HttpSecurity http) {
        http.addFilterBefore(
                new JwtFilter(tokenProvider),  //JwtFilter를 통해 Security 로직에 필터를 등록
                UsernamePasswordAuthenticationFilter.class
        );
    }
}


4. JwtAuthenticationEntryPoint

  • 유효한 자격증명을 제공하지 않고 접근하려 할 때 에러를 리턴하기 위한 클래스
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 Unauthorized 에러를 리턴
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}


5. JwtAccessDeniedHandler

  • 필요한 권한이 존재하지 않는 경우
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //필요한 권한을 가지고 있지 않고 접근하려 할때 403 Forbidden에러 리턴
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}


6. SecurityConfig에 토큰 관련 설정 적용

  • JWT 관련 설정 5가지 적용하기
package com.example.tutorial.config;

import com.example.tutorial.jwt.JwtAccessDeniedHandler;
import com.example.tutorial.jwt.JwtAuthenticationEntryPoint;
import com.example.tutorial.jwt.JwtSecurityConfig;
import com.example.tutorial.jwt.TokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  // -> @PreAuthorize 어노테이션을 메소드 단위로 추가하기 위해 적용 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;  // 유효하지 않은 자격증명의 경우 
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;  //필요한 권한이 존재하지 않는 경우

    public SecurityConfig(
            TokenProvider tokenProvider,
            JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
            JwtAccessDeniedHandler jwtAccessDeniedHandler) {

        this.tokenProvider = tokenProvider;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .antMatchers(
                        "/h2-console/**"
                        ,"/favicon.ico"
                );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()// 토큰을 사용하기 때문에 csrf설정은 disable

                .exceptionHandling()  //exception 핸들링할 때 만들어둔 클래스들 추가
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                .and()  //h2-console을 위한 설정을 추가
                .headers()
                .frameOptions()
                .sameOrigin()

                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // session을 사용하지 않기 때문에 session설정을 STATELESS

                .and()
                .authorizeRequests()  // HttpServletRequest를 사용하는 요청들에 대한 접근 제한을 설정하겠다는 의미
                .antMatchers("/api/hello").permitAll()  //인증없이 접근 허용
                .antMatchers("/api/authenticate").permitAll()// 로그인은 토큰이 없는 상태에서 요청이 들어오므로 permit all
                .antMatchers("/api/signup").permitAll()// 회원가입은 토큰이 없는 상태에서 요청이 들어오므로 permit all
                .anyRequest().authenticated()

                .and()
                .apply(new JwtSecurityConfig(tokenProvider));  //addFilterBefore 메서드를 가지고 있는 JwtSecurityConfig 클래스 적용
    }
}

 

다음 포스팅

[Backend/Spring] - [Spring Boot JWT Tutorial - 인프런] 스프링 JWT 적용하기 part3. Repository, 로그인 API 구현

 


 

GitHub - SilverNine/spring-boot-jwt-tutorial

Contribute to SilverNine/spring-boot-jwt-tutorial development by creating an account on GitHub.

github.com