개발자의 서재

Chaptor.03 - JWT코드, Security 설정 추가 본문

SpringBootProject/SringBoot_JWT_RestApi

Chaptor.03 - JWT코드, Security 설정 추가

ironmask431 2022. 4. 3. 16:58

<진행할 것>

- JWT 설정 추가

- JWT 관련 코드 추가 

- Security 설정 추가

 

1. application.yaml 에 jwt관련 설정 추가

여기서는 HS512 알고리즘을 사용해 암호화를 할 것이기 때문에 Secret Key는 64 byte 이상이어야 한다.

Secret Key는 특정 문자열을 Base64 로 인코딩한 값으로 설정한다.

 

jwt:
  header: Authorization
  #secret : 별도 문자열을 base64로 암호화한값
  secret: c3ByaW5nYm9vdC1qd3QtdHV0b3JpYWwtc3ByaW5nYm9vdC1qd3QtdHV0b3JpYWwtc3ByaW5nYm9vdC1qd3QtdHV0b3JpYWwK
  #토큰만료시간(ms)
  token-validity-in-seconds: 3600

 

base64 인코딩은 리눅스에서 아래와 같이 입력하면 확인 가능

echo '문자열'|base64 

 

secret key가 너무 짧으면 구동시 아래와 같은 에러가 발생함. 

 Invocation of init method failed; nested exception is io.jsonwebtoken.security.WeakKeyException: 
 The specified key byte array is 192 bits which is not secure enough for any JWT HMAC-SHA algorithm.  
 The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms 
 MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).  
 Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) 
 method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.

 

2. jwt 패키지 생성 / 토큰의 생성, 유효성 검증을 담당할 Token Provider 생성

 

TokenProvider.java  생성

package com.leesh.springbootjwttutorial.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@Component
public class TokenProvider implements InitializingBean {

    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 tokenValidityInMilliseconds
    ){
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInMilliseconds * 1000; //3600 * 1000 ms = 1시간
    }

    @Override
    public void afterPropertiesSet(){
        //TokenProvider bean 생성 후 생성자로 주입받은 secret 값을 이용해 암호화 키 생성
        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(","));

        //토큰 유효시간 설정
        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();
    }
    //토큰을 받아서 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());
        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;
        }catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e){
            log.info("잘못된 JWT 서명입니다.");
        }catch (ExpiredJwtException e){
            log.info("만료된 JWT 서명입니다.");
        }catch (UnsupportedJwtException e){
            log.info("지원되지 않는 JWT 서명입니다.");
        }catch (IllegalArgumentException e){
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}

 

JwtFilter.java 생성

 

package com.leesh.springbootjwttutorial.jwt;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
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;

@Slf4j
@Component
public class JwtFilter extends GenericFilterBean {

    //토큰 헤더에 입력시 설정한 key값
    public static final String AUTHORIZATION_HEADER = "Authorization";

    private TokenProvider tokenProvider;

    public JwtFilter(TokenProvider tokenProvider){
        this.tokenProvider = tokenProvider;
    }

    //토큰의 인증정보를 securityContext에 저장하는 역할 수행
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException{
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        //resolveToken을 통해 request에서 jwt 토큰을 획득
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        //validateToken 을 통해 토큰의 유효성 검사, 정상이면 토큰을 Security Context에 저장
        if(StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)){
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.debug("Security Context에 '{}' 인증정보를 저장했습니다. uri:{}",authentication.getName(), requestURI);
        }else{
            //log.debug("유효한 JWT 토큰이 없습니다.",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;
    }

}

 

 JwtSecurityConfig.java 생성

 

package com.leesh.springbootjwttutorial.jwt;

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;
import org.springframework.stereotype.Component;

@Component
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private TokenProvider tokenProvider;

    public JwtSecurityConfig(TokenProvider tokenProvider){
        this.tokenProvider = tokenProvider;
    }

    //security로직에 JwtFilter를 등록함
    @Override
    public void configure(HttpSecurity http){
        JwtFilter customerFilter = new JwtFilter(tokenProvider);
        http.addFilterBefore(customerFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

JwtAuthenticationEntryPoint.java 생성

 

package com.leesh.springbootjwttutorial.jwt;

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 {

    //유효한 토큰 없이 접근시 401 UNAUTHORIZED 에러 리턴.
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                  AuthenticationException authException)
            throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }

}

 

JwtAccessDeniedHandler.java 생성 

 

package com.leesh.springbootjwttutorial.jwt;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    //권한 불충분시 403 FORBIDDEN 접근이 거부되었습니다. 리턴
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException{
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

 

3. 위 5개의 클래스들을 SecurityConfig 에 추가하기

 

SecurityConfig.java 코드추가

 

package com.leesh.springbootjwttutorial.config;

import com.leesh.springbootjwttutorial.jwt.JwtAccessDeniedHandler;
import com.leesh.springbootjwttutorial.jwt.JwtAuthenticationEntryPoint;
import com.leesh.springbootjwttutorial.jwt.JwtSecurityConfig;
import com.leesh.springbootjwttutorial.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
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;

@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

	//패스워드 암호화시 사용
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //h2-console, favicon, swagger-ui  시큐리티에 필터에 걸리지않도록 설정
    @Override
    public void configure(WebSecurity web) throws Exception {
       web.ignoring()
               .antMatchers(
                       "/h2-console/**", //h2 console
                       "/favicon.ico",
               );
    }

    //http 요청에대한 권한 제한 설정
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                .and()
                .headers()
                .frameOptions()
                .sameOrigin()

                .and()
                .sessionManagement()
                //세션 사용하지 않으므로 STATELESS 로 지정
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                .antMatchers("/api/hello/**").permitAll() //hello 테스트  > 전체허용
                .antMatchers("/api/auth/**").permitAll() //로그인  > 전체허용
                .antMatchers("/api/join/**").permitAll() //회원가입  > 전체허용
                .anyRequest().authenticated()

                .and()
                .apply(new JwtSecurityConfig(tokenProvider));
    }
}

 

4. 서버실행 > 정상 구동 확인

Comments