Jwt

bird.j·2021년 4월 12일
0

SpringBoot

목록 보기
12/22
post-custom-banner

Spring security + jwt 사용을 위하여 구성하여야할 부분을 크게 나누어보면,

  1. UserDetailsService(및 UserDetails)
  2. Authentication(jwt)
  3. Filter(인증을 위한 사용자 구현 필터)

위의 실제 사용자 구현부는 WebSecurityConfigurerAdapter 구현체에서 사용된다.

💡 jwt란?


json 객체를 사용해서 토큰 자체에 정보들을 저장하고 있는 web token.



💡 jwt 구성


header, payload, signature로 구성되어있다.

  • header : signature를 해싱하기 위한 알고리즘 정보들이 담겨있음

  • payload : 서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보에 대한 내용들이 담겨있음

  • signature : 토큰의 유효성 검증을 위한 문자열. 이 문자열을 통해 서버에서는 이 토큰이 유효한 토큰인지를 검증할 수 있다.



💡 jwt 장점


  • 중앙의 인증서버, 데이터 스토어에 대한 의존성 없음, 시스템 수평 확장 유리
    • 인증에 필요한 정보를 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요 없어 서버 측 부하를 줄일 수 있다.
    • 세션의 경우 서버에 저장이 되기 때문에 서버 측 부하가 증가하게 된다.
  • Base64 URL Safe Encoding을 이용하기 때문에 URL, cookie, header모두 사용 가능하다.



💡 jwt 단점


  • payload에 저장하는 정보가 많아지면 네트워크 사용량이 증가, 데이터 설계 고려 필요
  • 토큰이 클라이언트에 저장, 서버에서 클라이언트의 토큰을 조작할 수 없음



구현해보기


🎈 WebSecurityConfig

@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    // 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다.
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
                .csrf().disable() // csrf 보안 토큰 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
                .and()
                .authorizeRequests() // 요청에 대한 사용권한 체크
                .antMatchers("/**").permitAll()
                .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class);
        // JwtAuthenticationFilterUsernamePasswordAuthenticationFilter 전에 넣는다
    }
}

@EnableWebSecurity : 기본적인 web보안을 활성화하겠다.
@Bean : spring IoC 컨테이너에 Bean을 등록. 개발자가 직접 제어가 불가능한 외부 라이브러리 등을 Bean으로 등록하려할 때. 개발자가 작성한 메서드를 통해 반환되는 객체를 Bean으로 등록하려할 때


추가적인 설정을 위해서 WebSecurityConfigurerimplements하거나 WebSecurityConfigurerAdapterextends하는 방법이 있다.

  • 위 코드에서 WebSecurityConfigurerAdapterextends 방식 사용

    • WebSecurityConfigurerAdapter의 configure메소드를 오버라이드

      • .authorizeRequests() : HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다.
      • .andMatchers(path).permitAll() : path에 대한 요청은 인증없이 접근을 허용하겠다.
      • .anyRequest().authenticated() : 나머지 요청들은 모두 인증되어야한다.

🎈 JwtTokenProvider

  • 토큰의 생성, 토큰의 유효성 검증 담당
@RequiredArgsConstructor
@Component
public class JwtTokenProvider { // 토큰 생성, 검증

    private String secretKey = "clonekurlyclonekurlyclonekurlyclonekurlyclonekurlyclonekurlyclonekurly";

    private long tokenValidTime = 30*60*1000L;
    private final UserDetailsService userDetailsService;

    protected void init(){
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public String createToken(String userPk){
        Claims claims = Jwts.claims().setSubject(userPk);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

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


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

    // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("X-AUTH-TOKEN");
    }

    // 토큰의 유효성 + 만료일자 확인
    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;
        }
    }
}

@Component : spring IoC 컨테이너에 Bean을 등록. 개발자가 직접 작성한 class를 Bean으로 등록하려할 때
claim : JWT의 payload에 담기는 정보의 한 조각



🎈 JwtAuthenticationFilter

  • Jwt를 위한 커스텀 필터
  • GenericFilterBeanextends해서 doFilter override, 실제 필터링 로직은 doFilter내부에 작성
  • doFilter : 토큰의 인증정보를 SecurityContext에 저장하는 역할수행

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean { //토큰확인, 유저정보 받아서 전달

    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);
    }
}

https://m.blog.naver.com/mds_datasecurity/221971440548
https://imbf.github.io/spring/2020/06/29/Spring-Security-with-JWT.html

post-custom-banner

0개의 댓글