본문 바로가기
오답노트

기술글 아닙니다.뻘글 주의 JWT사용해보기 반성과 고찰

by titlejjk 2023. 8. 25.

주의!!!!

이 글은 JWT기술글이 아닙니다.

이번 협업 프로젝트를 진행하면서 JWT라는 것을 사용해 보고 다시한번 어떤식으로 코드를 작성했는지 되돌아 보는 글입니다. 많이 여쭤봐주시고 많이 비판해주셨으면 감사하겠습니다!

 

https://titlejjk.tistory.com/313

글에서 JWT글을 작성해보긴했는데 글이 올라간 10일부터 지금까지

JWT를 무조건 써봐야해가 아닌 천천히 시간을 두고 하나씩 이해해 보려고 했다.

 

JWT를 왜 쓰고 이해를 해야지 그냥 좋은 기술이다라는 생각으로 접했던 내가 부끄러워 위에 글을 링크에 걸어두었다...

 

프로젝트전 무조건 신기술이 최고야 하는 나를 반성하면서 내가 써본 JWT를 써내려가보려고 한다.🫣

 

JSON Web Token 쉽게 말해 JSON으로 이루어진 Web의 Token이며, 개체 사이의 정보를 안전하게 전송하는 방법을 제공하는 것이다.

 

이번 프로젝트에서는 헤더에 토큰을 담아보고 그 토큰안에 정보도 담아보고 토큰을 해석해보기도 했다.(문자열해석 🙅)

 

이번 공부로 페이로드가 뭔지 헤더에 무엇이 담기는지 토큰을 만들기 위해 무엇을 했는지 되돌아 보려고한다.

 

package com.project.project.security;


@Service
public class TokenProvider {

    //JWT 서명 및 검증에 사용되는 키

    private final static String SECURITY_KEY = "jwtseckey!@";


    //사용자 이메일을 받아 JWT를 생성하는 메서드
    public String create(UserDto userDto){
        //토큰에 담을 클레임 정의
        Claims claims = Jwts.claims().setSubject(userDto.getUserEmail());
        claims.put("userNum", userDto.getUserNum());
        claims.put("userEmail", userDto.getUserEmail());
        claims.put("userNickname", userDto.getUserNickname());
        claims.put("petTypes", userDto.getPetTypes());
        claims.put("petTypeIds", userDto.getPetTypeIds());
        //토큰 만료시간 = 현재시간 + 1시간
        Date exprTime = Date.from(Instant.now().plus(1, ChronoUnit.HOURS));
        //JWT생성
        return Jwts.builder()
                //암호화 알고리즘, 키
                .signWith(SignatureAlgorithm.HS512, SECURITY_KEY)
                //토큰에 claims담기
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(exprTime)
                //JWT문자열 반환
                .compact();
    }

    //매개변수로 전달된 토큰을 검증하고 , 복호화하여 페이로드에서 제목을 추출하는 역할
    public  String validate(String token){
        //토큰을 검증하기 위해 키를 설정
        //토큰을 복호화하고, 페이로드의 내용을 추출
        Claims claims = Jwts.parser().setSigningKey(SECURITY_KEY).parseClaimsJws(token).getBody();
        //추출한 페이로드에서 제목을 가져와 변환
        //제목은 토큰에 설정된 사용자 이메일
        return claims.getSubject();
    }
}

먼저 내가 만들어본 JWT를 생성하는 Class이다. 어떤 값을 토큰에 담을지, 토큰을 만들 때 어떠한 알고리즘으로 토큰의 유효시간은 얼마나 할지 만들어본 코드이다.

 

 

다음은 JWT인증을 수행하는 Spring Security 필터 클래스이다. 사실 지금 거의 프로젝트가 2주남은 상황에 아직 Security를 공부중이라 완벽하게 쓰이지는 못하는 클래스라 생각된다..

Spring Security는 내게 너무 어려운 스킬이라 천천히 공부해보려고한다..

(지금은 프로젝트에는 모든 경로에 접근할 수 있도록 열어두었다.)

package com.project.project.filter;


@Component
@RequiredArgsConstructor
public class JwtAuthencationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;

    // 로거 인스턴스
    private static final Logger logger = LoggerFactory.getLogger(JwtAuthencationFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            logger.debug("Processing authentication for '{}'", request.getRequestURL()); // 로깅 추가

            String token = parseBearerToken(request);

            if (token != null && !token.equalsIgnoreCase("null")) {
                String userEmail = tokenProvider.validate(token);
                AbstractAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userEmail, null, AuthorityUtils.NO_AUTHORITIES);
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                securityContext.setAuthentication(authentication);
                SecurityContextHolder.setContext(securityContext);

                logger.debug("Authenticated user '{}'", userEmail); // 로깅 추가
                logger.debug("SecurityContext created with authentication: '{}'", securityContext.getAuthentication());
            }
        } catch (Exception e) {
            logger.error("Failed to process authentication request", e); // 로깅 추가
        }

        filterChain.doFilter(request, response);
    }

    private String parseBearerToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.isEmpty(bearerToken) || !bearerToken.startsWith("Bearer ")) {
            return null;
        }
        return bearerToken.substring(7);
    }
}

우선 이 클래스 JwtAuthenticationFilter는 말 그대로 JWT토큰으로 인증하고 SecurityContextHoler에 추가하는 필터를 설정하는 클래스입니다.

 

저는 여기서 OncePerRequestFilter 클래스를 상속받아 사용했습니다. 클래스의 이름에서 알 수 있듯요청당 한 번만 필터가 실행되도록 보장되도록 도와줍니다.

OncePerRequestFilter는 Spring Security에서 사용하는 필터 중 하나로 HTTP요청이 들어올 때마다 Filter인터페이스를 구현하며, 한 번씩만 실행되도록 합니다.

예를 들면 Forwarding이 발생해도 Filter Chain이 다시 동작해 딱 한번만 들어와야할 로직이 여러번 들어오는 것을 방지 할 수 있습니다.

이를 통해 토큰 기반 인증을 구현할 수 있습니다.(사용자 인증 및 권한 부여, 특정 엔드포인트에 대한 접근을 제어)

 

다음으로 OncePerRequestFilter 클래스에서 정의되고, 실제 필터링 로직을 수행하는 doFilterInternal에 대해서 알아 보겠습니다.

이 메서드의 목적만 간단하게 얘기하자면 HTTP요청, 응답을 처리하고 필터 체인의 다음 단계로 전달하는 역할을 합니다.

매개 변수로는 HttpServletRequest, HttpServletResponse를 가지며 요청과 응답에 대한 정보를 가지고 있습니다.

위 코드에서는 토큰이 "null"이 아닌 유효한 값이면

"tokenProvider.validate(token)"을 호출하여 토큰을 검증하고 관련 사용자 이메일을 가져오도록 설정했습니다.

 //매개변수로 전달된 토큰을 검증하고 , 복호화하여 페이로드에서 제목을 추출하는 역할
    public  String validate(String token){
        //토큰을 검증하기 위해 키를 설정
        //토큰을 복호화하고, 페이로드의 내용을 추출
        Claims claims = Jwts.parser().setSigningKey(SECURITY_KEY).parseClaimsJws(token).getBody();
        //추출한 페이로드에서 제목을 가져와 변환
        //제목은 토큰에 설정된 사용자 이메일
        return claims.getSubject();
    }

 

먼저 토큰 분석을 하기위에 parseBearerToken이라는 메서드로 Bearer토큰을 추출해주었습니다.

 String token = parseBearerToken(request);

그럼 밑에 만들어둔 parseBearerToken에서 생성된 token의 Header에서 토큰을 추출합니다.

private String parseBearerToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.isEmpty(bearerToken) || !bearerToken.startsWith("Bearer ")) {
            return null;
        }
        return bearerToken.substring(7);
    }

return 할 때 .substring(7)은 앞에 "Bearer "라는 문자열을 제거하기위해 작성되었습니다.

이렇게 값이 넘어간 상태에서 다음 bearerToken.substring()을 통해서 문자열"Bearer "이 삭제 된상태로 위로 값이 넘어가게 됩니다.

이후 토큰검사가 이루어지는데 그 전에 먼저 token이 null이 아니고 문자열 "null"과 같지 않은 경우에만 이후 로직이 실행됩니다.

추출된 사용자 계정

 

그런 다음 의존성을 부여 받은 TokenProvider의 validate(token)메서드를 통해 토큰을 검증하고 토큰에 연결된 사용자 이메일을 가져옵니다.

 AbstractAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userEmail, null, AuthorityUtils.NO_AUTHORITIES);

"UsernamePasswordAuthenticationToken"객체를 생성해 인증에 사용될 사용자 이메일을 첫번째 매개변수로 전달하고, 권한은 "NO_AUTHORITIES"로 설정합니다.

 authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

 

그런 다음으로 UsernamePasswordAuthenticationToken객체를 생성하여 사용자를 인증하고 인증 상세설정, 새로운 Security Context를 생성한 다음에 객체를 설정합니다.

SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);

빈 "SecurityContext"객체를 생성하고, 앞서 만든 인증 객체를 설정한 뒤 "SecurityContextHoler"에 보안 컨텍스트를 설정하여, 이후의 요청 처리에서 인증된 사용자를 인식 할 수 있게 합니다.

유효한 토큰이 있다면 해당 토큰으로 사용자를 인증하고, 그 결과를 "SecurityContextHolder"에 저장하여 나중에 엑세스할 수 있게 설계를 했습니다.

설계의도는 보안 요구사항을 충족시키며, 사용자 인증 상태를 전체 애플리케이션에서 사용할 수 있게 했습니다만,

아직 Spring security 쪽이 부족하여 "권한"부여가 되진 않을 것같습니다.

 

이번 프로젝트를 하면서 Spring Security 다음으로 가장 많은 시간을 공들였던 것 같습니다 ㅠㅠ

많이 부족하지만 긴 글 읽어주셔서 감사합니다!

댓글