[Spring] JWT 적용하기

yoon·2024년 3월 20일

spring-boot

목록 보기
21/41
post-thumbnail

✅ 토큰 기반 인증

스프링 시큐리티에서는 기본적으로 세션 기반 인증을 제공한다.
토큰 기반 인증을 토큰을 사용하는 방법으로, 서버에서 클라이언트를 구분하기 위한 유일한 값을 토큰이라고 한다.
서버가 토큰을 생성하여 클라이언트에 제공하면 클라이언트는 여러 요청을 이 토큰과 함께 신청한다.

✔ 특징

  • 무상태성(stateless) : 서버에서 사용자 인증 상태 저장X
  • 확장성 : 상태를 신경쓰지 않아도 되기 때문에 확장이 자유로움
  • 무결성 : 토큰을 발급한 이후 토큰 정보를 변경하는 행위는 불가

✅ JWT(Json Web Token)

JSON포맷을 이용한 claim 기반 웹 토큰이다.
일반적으로 쿠키 저장소에 jwt를 저장한다.

  • 장점
    로그인 정보를 서버에 저장하지 않고, 클라이언트에 암호화하여 저장 > 서버 부담 ↓

  • HTTP 헤더의 Authorization 키값에 "Bearer {토큰}" 형태로 담아서 보냄

  • 구조

    • header : 토큰타입, 해싱 알고리즘
    • payload : 토큰 관련 정보 (정보(키-값) = Claim)
    • signature : 토큰의 조작, 변경 여부 확인 역할
  • accessToken : 사용자 인증

  • refreshToken : accessToken 갱신을 위한 인증 정보

✔ 의존성 추가

compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

✔ jwt 적용하기 - access token

◾ jwt 생성시 사용할 secret key를 application.properties에 작성후 불러오기 위한 클래스 만들기

@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
    private String secretKey;
}

// application.properties 작성
//jwt.secret.key = ...

◾ 토큰 제공 클래스 작성

@Slf4j(topic = "TokenProvider")
@RequiredArgsConstructor
@Service
public class TokenProvider {

    private final JwtProperties jwtProperties;

    public String generateToken(User user, Duration expiredAt) {
        Date now = new Date();
        return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
    }

    private String makeToken(Date expiry, User user) {
        Date now = new Date();

        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .setSubject(user.getUsername())
                .claim("id", user.getId())
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }

    public boolean validToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtProperties.getSecretKey()).parseClaimsJws(token);
            
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

// 토큰기반으로 인증 정보 가져오기
    public Authentication getAuthentication(String token) {
        Claims claims = getClaims(token);
        Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));

// User는 내가 만든 entity가 아니라 시큐리티가 제공하는 User!!!!!
        return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject
                (), "", authorities), token, authorities);
    }
//토큰 기반으로 유저 ID 가져오기
    public Long getUserId(String token) {
        Claims claims = getClaims(token);
        return claims.get("id", Long.class);
    }

    private Claims getClaims(String token) {
        return Jwts.parser() //클레임 조회
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();
    }
}

✔ jwt 적용하기 - refresh token

refresh 토큰은 서버에 저장되어 유효하지 않은 access 토큰으로 요청이 왔을 때 새로운 access 토큰을 발급을 위해 사용한다.

◾ refresh token entity

@Entity
@Getter
@NoArgsConstructor
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "user_id", nullable = false, unique = true)
    private Long userId;

    @Column(name = "refresh_token", nullable = false)
    private String refreshToken;

    public RefreshToken(Long userId, String refreshToken) {
        this.userId = userId;
        this.refreshToken = refreshToken;
    }

    public RefreshToken update(String newRefreshToken) {
        this.refreshToken = newRefreshToken;
        return this;
    }
}


◾ 토큰 필터

  1. 요청이 들어오면 토큰 필터로 유효한 토큰인지 확인
  2. 유효하다면 security context holder에 인증 정보 저장
  3. 서비스 로직 실행
  • security context
    : 인증 객체가 저장되는 보관소
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
        String token = getAccessToken(authorizationHeader);
        if(tokenProvider.validToken(token)){
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String getAccessToken(String authorizationHeader){
        if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
            return authorizationHeader.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}

◾ 리프레시 토큰 서비스
리프레시 토큰 불러오기

@Service
@RequiredArgsConstructor
public class RefreshTokenService {
    private final RefreshTokenRepository refreshTokenRepository;

    public RefreshToken findByRefreshToken(String refreshToken){
        return refreshTokenRepository.findByRefreshToken(refreshToken)
                .orElseThrow(()-> new IllegalArgumentException("Unexpected token"));
    }

}

◾ 토큰 서비스
리프레시 토큰 유효성 검사 후 새로운 액세스 토큰 발급

@Service
@RequiredArgsConstructor
public class TokenService {
    private final TokenProvider tokenProvider;
    private final RefreshTokenService refreshTokenService;
    private final UserService userService;

    public String createNewAccessToken(String refreshToken){
        if(!tokenProvider.validToken(refreshToken)){
            throw new IllegalArgumentException("Unexpected token");
        }

        Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
        User user = userService.findById(userId);
        return tokenProvider.generateToken(user, Duration.ofHours(2));
    }
}

◾ 토큰 request, response dto 작성 후 컨트롤러 작성
requestDto에는 refreshToken이 들어가고, responseDto에는 accessToken이 들어간다고 생각하면 된다.
위에서 만든 서비스 로직을 이용하여 컨트롤러를 작성하면 끝

profile
하루하루 차근차근🌱

0개의 댓글