본문 바로가기
스프링/SpringSecurity

JWT + SpringSecurity

by 공부 안하고 싶은 사람 2021. 3. 20.
반응형

JWT 인증

JWT (JSON Web Token)
장점

  • Stateless : 토큰을 사용하면 세션을 통한 방식과 달리 서버나 메모리 부하를 낮출 수 있다.
  • Scalability : 분산/클라우드 기반 인프라스트럭처에 더 잘 대응할 수 있다.
  • 보안성 : 쿠키로 세션을 관리하지 않기 때문에 쿠키로인한 취약점은 없다.

단점

  • 저장할 필드 수가 커질 수록 토큰이 커지며, 보안에 취약해진다.
  • 토큰이 모든 요청에 포함되므로, 데이터 트래픽에 영향을 줄 수 있다.
  • 한 번 발급되면 유효기간이 완료될 때 까지는 계속 사용하므로 탈취 가능성 존재
    따라서, Access Token유효기간을 짧게하고, Refresh Token을 새로 발급 받도록 변경해야함

Header : 헤더는 토큰 타입, 해싱 알고리즘을 지정. 토큰 타입은 JWT이고, 해싱 알고리즘은 SHA256 혹은 RSA가 사용

Payload : 토큰에 담을 정보(클레임)가 들어있습니다.
종류 : 등록된 클레임, 공개된 클레임, 비공개 클레임

Signature : 헤더의 인코딩 값과 정보의 인코딩 값을 합친 후 비밀키로 해쉬를 하여 생성

주의

  • 중요 정보는 payload에 넣지 말아야함(암호화X)
    Refresh token은 탈취걱정해야함

적용(간략하게 요약만)

  1. UsernamePasswordAuthenticationFilter 이후에 추가 인증과정을 추가하기 위해,
    GenericFilterBean을 상속받은 필터가 return SecurityContextHolder.getContext().setAuthentication(authentication)할 수 있도록 설정해준다.
  2. authentication은 쿠키의 jwt의 payload를 읽어 새로운 token을 AuthenticationManager에게 전달해줄 수 있도록한다.
  3. AuthenticationManager가 새로운 AuthenticationProvider를 통해 인증할 수 있도록 설정에추가.
    AuthenticationProvider을 상속받아 jwt토큰을 생성할 수 있도록 구현
  4. 세션생성 전략 변경, csrt설정 끄기
  5. refreshtoken 생성을 위한 url 설정과 controller구현
http
        .httpBasic().disable() // rest api 이므로 기본설정 사용안함. 기본설정은 비인증시 로그인폼 화면으로 리다이렉트 된다.
        .csrf().disable() // rest api이므로 csrf 보안이 필요없으므로 disable처리.
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증할것이므로 세션필요없음

  1. 로그인 호출
  2. 인증
  3. jwt토큰 생성/반환
  4. 브라우저는 토큰을 SessionStorage에 저장
  5. api 호출시 jwt토큰전달
  6. JwtInterceptor는 jwt토큰 유효성 확인
  7. api 결과 반환

jjwt라이브러리 추가, JDK9 이상이면 jaxb-api 필요

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>
<dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.3.1</version>
</dependency>

application.yml 설정

tas.security.jwt:
  tokenExpirationTime: 1440 # Number of minutes
  refreshTokenExpTime: 1440 # Minutes
  tokenIssuer: https://tas.humuson.com
  tokenSigningKey: dkaghghkehlszl
  salt: MYSALT
bezkoder.app:
  jwtSecret: bezKoderSecretKey
  jwtExpirationMs: 86400000  
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
    return new AuthTokenFilter();
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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

public class AuthTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(jwt);

                UserDetails userDetails = userService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }

        filterChain.doFilter(request, response);
    }

    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");

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

        return null;
    }

}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.security.SignatureException;
import java.util.Date;
import io.jsonwebtoken.*;

@Component
public class JwtUtils {
    @Value("${bezkoder.app.jwtSecret}")
    private String jwtSecret;

    @Value("${bezkoder.app.jwtExpirationMs}")
    private int jwtExpirationMs;

    public String generateJwtToken(Authentication authentication) {

        UserAdminAdpater userPrincipal = (UserAdminAdpater) authentication.getPrincipal();

        return Jwts.builder()
                .setSubject((userPrincipal.getUsername()))
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public String getUserNameFromJwtToken(String token) {
        return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (MalformedJwtException e) {
            //logger.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            //logger.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            //logger.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            //logger.error("JWT claims string is empty: {}", e.getMessage());
        }

        return false;
    }
}

 

첫 로그인 시 ID/PW 확인후 

성공 필터에서 token 2개 생성 

 

이후 token을 header 혹은 cookie에 담아

request마다 customfilter에서 검사

 

refresh token은 레디스에 저장하여 꺼내쓰도록 한다. 

 

로그아웃시 refresh 토큰은 비활성 처리하고

header 혹은 cookie의 정보를 없앤다.

 

refresh/access 각각의 expried time과 payload 정보를 어떻게 설정할지 고민해보자.

로그인 로그인  
토큰 2개 발급 레디스에서 토큰확인하여 (로그아웃확인) 2개 발급  
클라이언트가 내부에 저장 -> 쿠키?웹스토리지? -> http only cookie 사용 쿠키 + 안전한 장소에 저장 access는 쿠키 refresh는 DB
access token으로 요청 유효기간을 확인하여 refresh할지 결정 1개씩 확인하고, DB확인하여 refresh
만료됏으면 만료를 응답하고 refresh토큰 요청    
refresh토큰 확인 후 토큰 2개 새로 발급 -> refresh토큰을 db에 넣어 유효성 체크 레디스에서 refresh토큰 확인 refresh도 새로발급?
     

참고 : https://velog.io/@tlatldms/%EC%84%9C%EB%B2%84%EA%B0%9C%EB%B0%9C%EC%BA%A0%ED%94%84-Refresh-JWT-%EA%B5%AC%ED%98%84

728x90
반응형

'스프링 > SpringSecurity' 카테고리의 다른 글

SPRING SECURITY  (0) 2021.02.09

댓글