[스프링 시큐리티 JWT] JWT을 발급하고 검증하자.

코린이서현이·2024년 6월 19일
0
post-thumbnail

들어가면서

아이리X 버티컬 마우스 쓰지 마세요^^
휠 고장나서 짜증나서 미쳐버리겠으니까~^^

JWT 발급(로그인 시), 검증(요청에 대해서)을 구현해보자~!

JWT 발급과 검증

사용자(클라이언트)가 JWT를 받는 타이밍

  • 사용자는 로그인 시 클라이언트에게 JWT를 발급받는다.
    👉 UsernamePasswordAuthenticationFilter 클래스의 successfulAuthentication메서드

서버가 JWT를 검증하는 타이밍

  • 서버는 JWT를 받은 사용자가 재 요청을 하면서 받은 JWT를 검증한다.
    👉 (이후에 내가 추가할)JwtFilter 클래스의 doFilterInternal메서드
//SecurityConfig에서 필터 등록하는 코드
http
	.addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class);
```ㅌ
>### `JwtFilter` 클래스의 `doFilterInternal`메서드는??
- `JwtFilter`는 `OncePerRequestFilter`의 하위 클래스
```java
public class JwtFilter extends OncePerRequestFilter {

OncePerRequestFilter 클래스의 doFilterInternal메서드는?

  • 각 요청에 대해 필터링 작업을 수행하는 메서드이다.
    특정 역할이 정해진 것은 아니고, 요청에 대한 작업을 하도록 하는 메서드이다.
    • 요청을 필터링하고, 필요한 경우 요청을 수정하기도 하고, 필터 체인의 다음 필터로 요청을 넘긴다.
	filterChain.doFilter(request, response);  

JWT에 대해서

Header.Payload.Signature

  • JWT임을 명시
  • 사용된 암호화 알고리즘

Payload.

  • 정보(실제 정보: body 데이터)
  • 다른 사람들도 쉽게 볼 수 있다.

Signature

  • 암호화알고리즘((BASE64(Header))+(BASE64(Payload)) + 암호화키)
  • 헤더와 페이로드를 서버가 가진 시크릿키를 활용해서 암호화를 진행한다.
  • 내부 정보를 단순 BASE64 방식으로 인코딩하기 때문에 외부에서 쉽게 디코딩
  • 외부에서 열람해도 되는 정보를 담아야한다.

JWT을 사용하는 이유

  • 발급처에 대한 보장을 하기 위해서!
  • 아! 이거 내가 발급한거야!!@ 라고 생각할 수 있도록..!!

JWT 발급키 종류

양방향

  • 대칭키 : ex_ HS256
  • 비대칭키 : 공개키, 비밀키

단방향

구현하기

서버의 암호키 저장

  • 서버의 암호키는 노출되면 안되기 때문에, 자바 코드 내부가 아니라, 변수 설정 부분에서 설정해준다.
spring : 
   jwt:
	secret : vmfhaltmskdlstkfkdgodyroqkfwkdbalroqkfwkdbalaaaaaaaaaaaaaaaabbbbb

JWT를 발급하는 클래스

package com.jwt.jwtstudy_youtube.jwt;

import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JwtUtil {

    private SecretKey secretKey;

    //생성자
    public JwtUtil(@Value("${spring.jwt.secret}") String secret) {
        this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    //토튼 만들기
    public String createJwt(String username, String role, Long expiredMs) {

        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis())) // 토큰 발생시간
                .setExpiration(new Date(System.currentTimeMillis() + expiredMs))// 소멸시간 셋팅
                .signWith(secretKey) // 시그니처~!
                .compact();
    }
}

로그인 시 JWT 발급

  • 저번 글에서 로그인을 담당하는, UsernamePasswordAuthenticationFilter의 하위 클래스 LoginFilter에 성공시 호출 메서드가 기억나는가? 바로 successfulAuthentication!!
  • successfulAuthentication메서드에 jwt를 발급하는 로직을 추가하자.
  • 어떤걸 활용해서?? JwtUtilcreateJwt메서드를 가지고!!
package com.jwt.jwtstudy_youtube.jwt;


public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil; //<-- jwt를 생성하는 클래스

    //생성자
    public LoginFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }


    @Override //필수
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
       ~~로그인 요청한 사용자 유효성 검사~~
       }


    //로그인 성공시 실행 메소드
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
        //(여기서 JWT를 발급하면 됨 ->  JwtUtil)
        CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
        String username  = customUserDetails.getUsername();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority auth = iterator.next();

        String role = auth.getAuthority();

        String token = jwtUtil.createJwt(username,role,1800000L); //30분으로 저장해줌

        response.addHeader("Authorization", "Bearer " + token);
    }


    //로그인 실패시 실행 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setStatus(404);
    }
}

로그인을 하면 유저는 어떤 응답을 받을까?

  • 응답 헤더에 키는 "Authorization", 값은 "Bearer "로 시작하는 JWT 토큰을 발급받는다.
  • 그리고 이후 요청을 할 때, 이 JWT토큰을 요청헤더에 같이 보낸다.

JWT를 검증하는 코드

  • 위에서 만들었던 JwtUtil에 검증 코드를 추가하자.
package com.jwt.jwtstudy_youtube.jwt;

@Component
public class JwtUtil {

    private SecretKey secretKey;

    //생성자
    public JwtUtil(@Value("${spring.jwt.secret}") String secret) {
        this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    //토튼 만들기
    public String createJwt(String username, String role, Long expiredMs) {
	~~~토큰 생성 부분~~~
	}
    
    //검증 진행
    public String getUsername(String token) {

        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .get("username", String.class);
    }

    public String getRole(String token) {

        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload().get("role", String.class);
    }

    //아직 유효한지

    public Boolean isExpired(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

}

검증에 사용되는 isExpired에 대해서 더 자세하게 살펴보자!

Jwts.parser()

이 부분은 Jwts클래스의 메서드를 활용해서 jwt토큰의 구조를 해석하고 검증할 수 있는 파서를 생성하는 부분이다.

verifyWith(secretKey).build()

이 부분은 jwt토큰을 검증할 때 사용할 비밀키를 설정하는 데, jwt 토큰을 생성할 때 사용된 키와 동일해야한다. 또한 파서의 설정을 완료하고 jwt 파서를 빌드한다.

.parseSignedClaims(token) ⭐⭐⭐⭐ 

전달받은 token의 서명이 올바른지 검사하고, 올바르다면 클레임을 반환한다.

.getPayload()

이 부분은 클레임에서 페이로드를 가져온다.

.getExpiration()

이 부분은 클레임에서 만료시간을 가져온다.

.before(new Date());

이 부분은 만료시간이 현재보다 이전인지를 확인한다.

JWT 검증하는 필터

  • JwtUtil을 사용해서 검증하는 필터를 만들자.

public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authorization= request.getHeader("Authorization");

        if (authorization == null || !authorization.startsWith("Bearer ")) {
            //없거나 접두사가 잘못된 경우
            System.out.println("token null");
            filterChain.doFilter(request, response); //필터들을  종료하고, 다음 필터로 넘긴다.

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }

        System.out.println("authorization now");
        //Bearer 부분 제거 후 순수 토큰만 획득
        String token = authorization.split(" ")[1];

        //토큰 소멸 시간 검증
        if (jwtUtil.isExpired(token)) {

            System.out.println("token expired");
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }
        //토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        //userEntity를 생성하여 값 set
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername(username);
        userEntity.setPassword("temppassword"); //비밀번호를 DB에서 가져오는게 아니라, 아무거나 ..^^
        userEntity.setRole(role);

        //UserDetails에 회원 정보 객체 담기
        CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

        //스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
        //세션에 사용자 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        System.out.println("올바른 허용");
            filterChain.doFilter(request, response);
    }

}

필터 등록

  • 검증하는 필터를 secrity를 등록해서 사용한다.
package com.jwt.jwtstudy_youtube.config;

@Configuration
@EnableWebSecurity //security를 위한 것이라고 알려주는 기능
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;
    private final JwtUtil jwtUtil;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JwtUtil jwtUtil) {
        this.authenticationConfiguration = authenticationConfiguration;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {

        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ~~이전글 참고~~
        http
                .addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class);

        return http.build();
    }
}

내가 궁금한 점

JwtUtil의 여러 로직에 토큰의 유효성을 검사하는 .parseSignedClaims(token)가 있을까?

  • 일관성과 안정성을 위해서 계속해서 검사를 하는 것 같다.
    하지만 솔직히 이 부분은 더 고민해봐야할 것 같다...!! 질문글로 넘어가자!!

마무리하면서...

처음이니까.. 헷갈릴 수 있어용~!
profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글