Jwt_Security (2) - JwtProvider,JwtAuthenticationFilter

Yu Seong Kim·2024년 1월 4일
0

SpringBoot

목록 보기
7/29
post-thumbnail

이번 포스트는 JwtProvider와 JwtAuthenticationFilter에 중점적으로 내용을 전달할 것입니다!
User, UserDetailService, UserRepository는 다른 포스트에서 소개 하였기 때문에 코드만 작성하겠습니다.

개발환경

  • JDK 11
  • MAVEN
  • IntelliJ
  • MariaDB
  • Apache_Tomcat

User 엔티티 생성(UserDetails구현)

package com.springboot.jwt_securityprac.entity;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
@Table
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false,unique = true)
    private String uid;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

    @ElementCollection(fetch = FetchType.EAGER)
    @Builder.Default
    private List<String> roles = new ArrayList<>();
    //계정이 가지고 있는 권한 목록 리턴
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities(){
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
    //계정의 비밀번호 리턴
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public String getPassword(){
        return this.getPassword();
    }
    //계정의 이름 리턴
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public String getUsername() {
        return this.getUid();
    }

    //계정이 만료됐는지 리턴 - true면 만료 X
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //계정이 잠겨 있는지 리턴 -> true면 안잠김.
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //계정의 비밀번호가 만료됐는지 리턴 -> ture면 만료 X
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //계정이 활성화돼 있는지 리턴 -> true면 활성화
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserRepository구현

현재 ID 값은 인덱스 값이기 때문에 ID 값을 토큰 생성 정보로 사용하기 위해 getByUid()메서드 생성.

package com.springboot.jwt_securityprac.repository;

import com.springboot.jwt_securityprac.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Long> {
    User getByUid(String uid);
}

UserDetailsService구현

  • UserDetailsService을 통해 입력된 로그인 정보를 가지고 데이터베이스에서 사용자 정보를 가져오는 역할을 수행.
  • UserDetailsService 인터페이스 loadUserByUsername()를 구현하도록 설정돼 있습니다.
package com.springboot.jwt_securityprac.service;

import com.springboot.jwt_securityprac.repository.UserRepository;


import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service

public class UserDetailServiceImpl implements UserDetailsService {
    private final UserRepository userRepository;


    private final Logger logger = LoggerFactory.getLogger(UserDetailServiceImpl.class);
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException{
        logger.info("[loadUserByUsername] loadUserByUsername 수횅 . username: {}",username);
        return userRepository.getByUid(username);
    }

}

JwtProvider 란?

Jwt을 생성하고 파싱하는 클래스입니다.

JwtProvider에서 토큰을 생성하고, 토큰으로 인증 정보 조회, 회원 구별 정보 추출 등
여러 메서드를 작성할 수 있습니다.

JwtProvider 구현

application.properties 시크릿 키 저장.

springboot.jwt.secret = @awdfsijfncmasopidh213215rw!^
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;

    private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

    @Value("${springboot.jwt.secret}")
    private String secretKey = "secretKey";

    private final long tokenValidMillisecond = 1000L*60*60;    //1시간 토큰 유효

.....
}
  • JwtTokenProvider클래스에는 @Component 어노테이션이 지정돼 있어 어플리케이션이 가동되면 빈으로 자동 주입됩니다.
  • @Value("${springboot.jwt.secret}") -> application.properties에 저장한 시크릿키 가져오기
  • tokenValidMillisecond -> 토큰 유효 시간(1시간)

JwtTokenProvider - init() 메서드

    @PostConstruct
    protected void init(){
        logger.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
        System.out.println(secretKey);

        //서명을 생성하기 위한 키
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
        System.out.println(secretKey);
        logger.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
    }
  • @PostConstruct 해당 객체가 빈 객체로 주입된 이후 수행되는 메서드를 가지킵니다.
    @Component 어노테이션이 지정돼 있어 어플리케이션이 가동되면 빈으로 자동 주입되는 데 이때 @PostConstruct가 지정돼 있는 init() 메서드가 자동으로 실행됩니다.

  • secretKey를 Base64 형식으로 인코딩
    <인코딩 전/후 텍스트>

    -인코딩 전-
    		@awdfsijfncmasopidh213215rw!^
     -인코딩 후(Base64 인코딩 결과)-
    		QGF3ZGZzaWpmbmNtYXNvcGlkaDIxMzIxNXJ3IV4=

    JwtTokenProvider - createToken() 메서드

        //jwt 토큰 생성
       public String createToken(String uid, List<String> roles){
           //uid를 이용하여 jwt 생성
           Claims claims = Jwts.claims().setSubject(uid);
           //클레임에 "roles"라는 이름으로 역할 정보(roles 매개변수)를 추가
           claims.put("roles",roles);
    
           Date now = new Date();
           String token = Jwts.builder()
                           .setClaims(claims)
                           .setIssuedAt(now)
                           .setExpiration(new Date(now.getTime()+tokenValidMillisecond))
                           .signWith(SignatureAlgorithm.HS256,secretKey)
                           .compact();
           logger.info("[createToken] 토큰 생성 완료");
           return  token;
       }
  • Claims claims = Jwts.claims().setSubject(uid);: 이 부분에서 JWT의 클레임(claim)을 설정합니다. 클레임은 토큰에 포함되는 정보를 나타냅니다. 여기서는 주제 클레임(subject claim)으로 사용자의 고유 식별자인 uid 값을 설정합니다.

  • claims.put("roles", roles);: 클레임에 "roles"라는 이름으로 역할 정보(roles 매개변수)를 추가합니다. 이것은 사용자 역할과 같은 추가 정보를 JWT에 담을 때 사용됩니다.

  • Date now = new Date();: 현재 날짜와 시간을 가져옵니다.

  • String token = Jwts.builder()...: JWT를 빌드합니다. 다음과 같은 정보를 JWT에 설정합니다.

  • 클레임(claims): 앞서 설정한 클레임 객체를 설정합니다.

  • 발행 시간(issuedAt): 현재 시간을 설정합니다.

  • 만료 시간(expiration): 토큰의 유효 기간을 설정합니다. 현재 시간에tokenValidMillisecond (밀리초)를 더한 시간까지 토큰이 유효합니다.

  • 서명(Signature): HMAC-SHA256 알고리즘을 사용하여 시크릿 키(secretKey)로 서명합니다.
    이 서명은 토큰이 변경되지 않았음을 검증하는 데 사용됩니다.

  • compact(): JWT를 문자열로 변환하여 반환합니다. 이 문자열로 토큰이 클라이언트에게 전송되어 인증 및 권한 부여 등에 사용됩니다.

    JwtTokenProvider - getAuthentication() 메서드

    이 메서드는 필터에서 인증이 성공했을 때 SecurityContextHolder에 저장할 Authentication을 생성하는 역할을 합니다.

    //jwt 토큰으로 인증 정보 조회
    public Authentication getAuthentication(String token){
        logger.info("[getAuthentication] 토큰 인증 정보 조회 시작");
        UserDetails userDetails = userDetailsService.loadUserByUsername(
                this.getUsername(token));
        logger.info("[getAuthenticaiton] 토큰 인증 정보 조회 완료, UserDetails userName",userDetails.getUsername());
        return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
    }

Authentication을 구현하는 편한 방법은 UsernamePasswordAuthenticationToken을 사용하는것이다.

JwtTokenProvider - getUsername() 메서드

JWT 토큰에서 회원 구별 정보 추출

public String getUsername(String token){
        logger.info("[getUsername] 토큰에서 회원 구별 정보 추출");
        String info = Jwts.parser()
                .setSigningKey(secretKey) // 시크릿키로 jwt 검증
                .parseClaimsJws(token) // 토큰 파싱하고 내용 추출
                .getBody() // 토큰 본문 가져오기(payload)
                //Subject -> 토큰 제목 - 토큰에서 사용자에 대한 식별값
                .getSubject(); //클레임에서 "sub" (subject) 필드를 가져와서 회원의 구별 정보를 추출
        logger.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}", info);
        return info;
    }

이때, Jws란 -> 서버에서 인증을 근거로 인증정보를 서버의 private key로 서명 한것을 토큰화 한 것입니다.

JwtTokenProvider - resolveToken() 메서드

HTTP 헤더 정보에 X-AUTH-TOKEN = JWT토큰 추가

    public String resolveToken(HttpServletRequest request){
      logger.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
      return request.getHeader("X-AUTH-TOKEN");
  }

JwtTokenProvider - validationToken() 메서드

JWT 토큰의 유효성 + 만료일 체크

public boolean validationToken(String token) {
      logger.info("[validateToken] 토큰 유효 체크 시작");
      try {
          Jws<Claims> claims = Jwts.parser()
                  .setSigningKey(secretKey)
                  .parseClaimsJws(token);
          logger.info("[validateToken] 토큰 유효 체크 완료");
          return !claims.getBody().getExpiration().before(new Date());
      } catch (Exception e) {
          logger.info("[validateToken] 토큰 유효 체크 예외 발생");
          return false;
      }

  }

}

JwtTokenProvider 전체코드

package com.springboot.jwt_securityprac.jwt;

import io.jsonwebtoken.*;
import lombok.Data;
import lombok.RequiredArgsConstructor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;


import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.List;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

 private final UserDetailsService userDetailsService;

 private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

 @Value("${springboot.jwt.secret}")
 private String secretKey = "secretKey";

 private final long tokenValidMillisecond = 1000L*60*60;    //1시간 토큰 유효

 //시크릿 키 초기화
 @PostConstruct
 protected void init(){
     logger.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
     System.out.println(secretKey);

     //서명을 생성하기 위한 키
     secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
     System.out.println(secretKey);
     logger.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
 }

 //jwt 토큰 생성
 public String createToken(String uid, List<String> roles){
     //uid를 이용하여 jwt 생성
     Claims claims = Jwts.claims().setSubject(uid);
     //클레임에 "roles"라는 이름으로 역할 정보(roles 매개변수)를 추가
     claims.put("roles",roles);

     Date now = new Date();
     String token = Jwts.builder()
                     .setClaims(claims)
                     .setIssuedAt(now)
                     .setExpiration(new Date(now.getTime()+tokenValidMillisecond))
                     .signWith(SignatureAlgorithm.HS256,secretKey)
                     .compact();
     logger.info("[createToken] 토큰 생성 완료");
     return  token;
 }
 //jwt 토큰으로 인증 정보 조회
 public Authentication getAuthentication(String token){
     logger.info("[getAuthentication] 토큰 인증 정보 조회 시작");
     UserDetails userDetails = userDetailsService.loadUserByUsername(
             this.getUsername(token));
     logger.info("[getAuthenticaiton] 토큰 인증 정보 조회 완료, UserDetails userName",userDetails.getUsername());
     return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
 }

 //JWT 토큰에서 회원 구별 정보 추출
 public String getUsername(String token){
     logger.info("[getUsername] 토큰에서 회원 구별 정보 추출");
     String info = Jwts.parser()
             .setSigningKey(secretKey) // 시크릿키로 jwt 검증
             .parseClaimsJws(token) // 토큰 파싱하고 내용 추출
             .getBody() // 토큰 본문 가져오기(payload)
             //Subject -> 토큰 제목 - 토큰에서 사용자에 대한 식별값
             .getSubject(); //클레임에서 "sub" (subject) 필드를 가져와서 회원의 구별 정보를 추출
     logger.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}", info);
     return info;
 }

 //HTTP 헤더 정보에 X-AUTH-TOKEN = JWT토큰 추가
 public String resolveToken(HttpServletRequest request){
     logger.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
     return request.getHeader("X-AUTH-TOKEN");
 }

 // JWT 토큰의 유효성 + 만료일 체크
 public boolean validationToken(String token) {
     logger.info("[validateToken] 토큰 유효 체크 시작");
     try {
         Jws<Claims> claims = Jwts.parser()
                 .setSigningKey(secretKey)
                 .parseClaimsJws(token);
         logger.info("[validateToken] 토큰 유효 체크 완료");
         return !claims.getBody().getExpiration().before(new Date());
     } catch (Exception e) {
         logger.info("[validateToken] 토큰 유효 체크 예외 발생");
         return false;
     }

 }

}

JwtAuthenticationFilter 구현

JwtAuthenticationFilter는 JWT 토큰으로 인증하고, SecurityContextHolder에 추가하는 필터를 설정하는 클래스 입니다.

public class JwtAuthenticationFilter extends OncePerRequestFilter {

 private Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

 private final JwtTokenProvider jwtTokenProvider;

 public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider){
     this.jwtTokenProvider = jwtTokenProvider;
 }

 @Override
 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 throws ServletException, IOException {
     String token = jwtTokenProvider.resolveToken(request);
     logger.info("[doFilterInternal] token 값 추출 완료. token : {}", token);

     logger.info("[doFilterInternal] token 값 유효성 체크 시작");
     if(token!=null&& jwtTokenProvider.validationToken(token)){
         Authentication authentication = jwtTokenProvider.getAuthentication(token);
         SecurityContextHolder.getContext().setAuthentication(authentication);
         logger.info("[doFilterInternal] token 값 유효성 체크 완료");
     }
     filterChain.doFilter(request,response);

 }

}
  • doFilterInternal 메서드: 이 메서드는 필터가 실제로 동작하는 부분입니다. 모든 HTTP 요청이 이 메서드를 통과하게 됩니다.

  • jwtTokenProvider.resolveToken(request) 호출: 이 부분에서 jwtTokenProvider 객체를 사용하여 HTTP 요청에서 JWT 토큰을 추출합니다. resolveToken 메서드는 이전에 설명한대로 HTTP 헤더에서 X-AUTH-TOKEN 헤더 값을 가져와서 JWT 토큰으로 사용합니다.

  • JWT 토큰의 유효성 검사: 추출한 JWT 토큰이 유효한지 확인하기 위해 jwtTokenProvider.validationToken(token)을 호출합니다. 이 부분은 JWT 토큰의 서명을 확인하고, 토큰의 만료 여부를 확인하는 역할을 합니다. 토큰이 유효하면 다음 단계로 이동하고, 그렇지 않으면 유효성 검사에 실패하게 됩니다.

  • 유효한 토큰일 경우 jwtTokenProvider.getAuthentication(token)을 호출하여 JWT 토큰에서 추출한 정보를 기반으로 인증(Authentication) 객체를 생성합니다. 이 인증 객체는 사용자의 인증 정보와 권한을 포함하게 됩니다.

  • SecurityContextHolder.getContext().setAuthentication(authentication)을 사용하여 Spring Security의 SecurityContext에 인증 정보를 설정합니다. 이렇게 하면 해당 요청에 대한 사용자 인증이 완료되며, 이후의 Spring Security 처리에서 이 인증 정보를 사용할 수 있습니다.

  • 마지막으로 filterChain.doFilter(request, response)를 호출하여 다음 필터로 요청을 전달합니다. 이로써 현재 필터의 처리가 완료되고 다음 단계로 넘어갑니다.

profile
Development Record Page

0개의 댓글