[백업] 발급받은 JWT로 요청하기

박솔찬·2022년 6월 12일
0

+ 이 글을 작성하던 당시(2021년 8월 31일) 비교적 적은 스프링부트에 대한 지식을 바탕으로 작성된 게시글입니다.

전체적인 발급받은 JWT로 요청하는 메인 로직만 확인 후, 코드를 클린하게 수정하여 작성하시길 바랍니다.

로그인과 JWT토큰 발급
이전 포스트에서 Spring Security를 통해 로그인을 진행하고, 토큰을 발급하는 과정을 진행하였다.
이번에는 발급받은 JWT를 통해 권한이 필요한 URI에 요청을 해보자.

Spring Security는 여러가지의 필터를 순차적으로 돌며 해당되는 필터를 실행한다.
그리고 인증에 관련된 책임은 AuthenticationManager에 의해 수행된다.

Spring Security의 인증 흐름은 다음 포스트를 참고하면 도움이 될 것이다.
스프링 시큐리티 기본 개념과 인증 흐름

기본적으로 Filter로 수행되는 것은 Form기반의 아이디와 비밀번호로 진행되는 UsernamePasswordAuthenticationFilter가 수행된다.

하지만 JWT 인증을 위해서는 새로운 필터를 만들어 UsernamePasswordAuthenticationFilter보다 먼저 실행되게 설정해야 한다.

JWT 검증 설정

JwtAuthenticationFilter 생성

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 헤더에서 JWT 를 받아옵니다.
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
        // 유효한 토큰인지 확인합니다.
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext 에 Authentication 객체를 저장합니다.
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}

SecurityConfig Filter 설정

이제 JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter보다 앞으로 설정하면 된다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    // AuthenticationManager를 Bean으로 등록합니다.
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 X
                .and()
                .authorizeRequests()
                .antMatchers("/join", "/login").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                UsernamePasswordAuthenticationFilter.class);
        // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
    }
}

addFilterBefore을 통해 설정한다.

CustomUserDetailService 생성

JwtTokenProvider에서 토큰의 payload에서 가져온 email 정보를 통해 Repository에서 유저 정보를 가져와야한다.

그러기 위해서 UserDetailSerivce를 구현하는 클래스를 생성하여 loadUserByUsername 오버라이드 해서 이를통해 가져오면 된다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("회원이 존재하지 않습니다."));
    }
}

JwtTokenProvider 수정

이전 포스트에서 작성했던 JwtTokenProvider에 Token을 통해 사용자 정보를 조회할 수 있는 메서드를 작성한다.

ToDo라는 메세지 주석으로 남겨놓았던 부분을 작성한다.

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {

    // 키
    private String secretKey = "lalala";

    // 토큰 유효시간 | 30min
    private long tokenValidTime = 30 * 60 * 1000L;

    private final CustomUserDetailService customUserDetailService;

    // 의존성 주입 후, 초기화를 수행
    // 객체 초기화, secretKey를 Base64로 인코딩한다.
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT Token 생성.
    public String createToken(String user, List<String> roles){
        Claims claims = Jwts.claims().setSubject(user); // claims 생성 및 payload 설정
        claims.put("roles", roles); // 권한 설정, key/ value 쌍으로 저장

        Date date = new Date();
        return Jwts.builder()
                .setClaims(claims) // 발행 유저 정보 저장
                .setIssuedAt(date) // 발행 시간 저장
                .setExpiration(new Date(date.getTime() + tokenValidTime)) // 토큰 유효 시간 저장
                .signWith(SignatureAlgorithm.HS256, secretKey) // 해싱 알고리즘 및 키 설정
                .compact(); // 생성
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = customUserDetailService.loadUserByUsername(this.getUserEmail(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 회원 정보 추출
    public String getUserEmail(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    // Request의 Header에서 token 값을 가져옵니다. "authorization" : "token'
    public String resolveToken(HttpServletRequest request) {
        if(request.getHeader("authorization") != null )
            return request.getHeader("authorization").substring(7);
        return null;
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

그리고 CustomUserDetailService 등록하여 loadUserByUsername메서드를 사용한다.

권한이 필요한 URI 요청하기

이제 로그인을 진행하고 토큰을 발급받아 권한이 필요한 URI에 요청을 보내어 응답을 받는 테스트를 진행해보자.

GET /test 생성

헤더에 토큰을 설정하고,
GET /test 요청을 하면,
Hello, User?를 응답하는 Controller를 만들자

Controller에 다음을 추가하자.

 // JWT 인증 요청 테스트
    @GetMapping("/test")
    public String test() {
        return "Hello, User?";
    }

요청하기

이제 최종적으로 테스트를 진행해보자

header = {"authorization: bearer token...}
GET /test

[##Image|kage@JsCYK/btrs2nAruX6/pAEvWtP3e4K6OhnwBuwwU0/img.png|CDM|1.3|{"originWidth":1280,"originHeight":881,"style":"alignCenter"}##]

권한이 필요한 URI에 성공적으로 요청을 하였고, 정상적인 응답을 확인할 수 있다.


알게된 부분

쿠키/세션(JSESSION) 방식과 JWT 로그인 방식을 공부하면서 두 방식의 차이점을 알게되었다.

loadUserByUsername에 대해서 쿠키/세션과 JWT의 차이를 정리해보면, 쿠키/세션은 첫 로그인 요청시 호출하여 Context에 저장하고
JWT는 토큰을 통해 권한이 필요한 URI에 요청할 때 마다 호출한다.

이 부분은 Spring Security의 인증 흐름을 공부하면 이유를 알 수 있다.


혹시나 잘못된 정보일 경우 언제든 피드백 부탁드립니다!.

profile
Why? How? What?

0개의 댓글