JWT란? JWT와 시큐리티를 이용한 로그인

yookyungmin·2023년 7월 7일
0

JWT 강의를 들으며 실습을 하던 도중 간단하게 정리를 해보고자 한다.

JSON WEB TOKEN이란?

  • JWT는 당사자간에 정보를 JSON 객체로 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준입니다
  • JWT를 암호화 하여 당사자간에 비밀을 제공할 수도 있지만 서명 된 토큰에 중점을 둔다

JWT 의 구조

  • HEADER - PAYLOAD - SIGNATURE
  • XXXXXX - YYYYYYY - ZZZZZZZZZ
  • 헤더
    {
    "alg": "HS256",
    "typ": "JWT"
    }
    alg : 서명 암호화 알고리즘(ex: HMAC SHA256, RSA)
    typ : 토큰 유형
  • 페이로드
    {
    "sub": "1234567890",
    "lat": 1516239022
    }
    iss(issuer; 발행자),
    exp(expireation time; 만료 시간),
    sub(subject; 제목),
    iat(issued At; 발행 시간),
    jti(JWI ID)
  • 시그니처
    HEADER + PAYLOAD + SECRET

JWT 인증

  @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
            log.debug("디버그: attemptAuthentication 호출됨");
        try {
            ObjectMapper om = new ObjectMapper(); //로그인하면 request안에 json이있어서 om이 필요
            LoginReqDto loginReqDto = om.readValue(request.getInputStream(), LoginReqDto.class); //LoginReqDto.class타입

            //강제 로그인
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    loginReqDto.getUsername(), loginReqDto.getPassword());  //토큰 생성

            //UserDetailService 의 LoadUserByUsername을 호출
            //JWT를 쓴다 하더라도 컨트롤러 진입을 하면 시큐리티의 권한체크 인증체크의 도움을 받을 수 있게 세션을 만든다.//인증이 되고 세션은 만들어지자마자
            // CustomReponseUtil.success(response, loginRespDto);에 의해 리턴되며 컨트롤러까지 안가고 없어진다 //
            // 이세션의 유효기간은 request하고 response하면 끝
            Authentication authentication = authenticationManager.authenticate(authenticationToken);  //강제 로그인
            //Authentication은 authenticationManager 매니저가 필요하다
            //강제 로그인
            //authentication.getPrincipal() < - LoginUser 객체가 담긴다

            return authentication; //successfulAuthentication() 호출

        }catch (Exception e){
            //unsuccessfulAuthentication 호출함
            throw new InternalAuthenticationServiceException(e.getMessage());
        }
    }
  	// 로그인 실패
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        CustomReponseUtil.fail(response, "로그인 실패", HttpStatus.UNAUTHORIZED);
    }
    //return authentication이 잘 작동하면 successfulAuthentication 호출
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, 	FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.debug("디버그: successfulAuthentication 호출됨");
        //로그인이 됐다, 세션이 만들어짐

        LoginUser loginUser = (LoginUser) authResult.getPrincipal(); //로그인유저
        String jwtToken = JwtProcess.create(loginUser); //jwtToken 토큰 생성

        response.addHeader(jwtVo.Header, jwtToken); //토큰을 헤더에 담는다

        LoginRespDto loginRespDto = new LoginRespDto(loginUser.getUser());  //유저 정보를 넣어주면된다

        CustomReponseUtil.success(response, loginRespDto);  //jwt 토큰 필터 구현완료
    }

JWT 인증 테스트 코드

@Transactional //전체 테스트시에 롤백으로 독립적인 테스트를 위해
@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class JwtAuthenticationFilterTest extends DummyObject {

    @Autowired
    private ObjectMapper om; // JSON으로 변경하기 위해 필요

    @Autowired
    private MockMvc mvc;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    public void setUp() throws Exception{
        User user = userRepository.save(newUser("saar", "쌀"));
    }

    @Test //로그인 성공
    public void successfulAuthentication_test() throws Exception{
        //given
        LoginReqDto loginReqDto = new LoginReqDto();
        loginReqDto.setUsername("saar");
        loginReqDto.setPassword("1234");

        String requestBody = om.writeValueAsString(loginReqDto);
        System.out.println("테스트 = " + requestBody);


        //when
        ResultActions resultActions = mvc.perform(post("/api/login").content(requestBody).contentType(MediaType.APPLICATION_JSON));
        String responseBody = resultActions.andReturn().getResponse().getContentAsString();
        String jwtToken = resultActions.andReturn().getResponse().getHeader(jwtVo.Header);
        System.out.println("테스트 = " + responseBody);
        System.out.println("테스트 = " + jwtToken);

        //then
        resultActions.andExpect(status().isOk());
        assertNotNull(jwtToken); //jwt 토큰이 null 이 아니길 기대
        assertTrue(jwtToken.startsWith(jwtVo.TOKEN_PREFIX)); //접두사 확인
        resultActions.andExpect(jsonPath("$.data.username").value("saar"));//usernam이 saar인지 검증
    }


    @Test  //로그인 실패
    public void unsuccessfulAuthentication_test() throws Exception{

        //given
        LoginReqDto loginReqDto = new LoginReqDto();
        loginReqDto.setUsername("saar");
        loginReqDto.setPassword("12345");

        String requestBody = om.writeValueAsString(loginReqDto);
        System.out.println("테스트 = " + requestBody);


        //when
        ResultActions resultActions = mvc.perform(post("/api/login").content(requestBody).contentType(MediaType.APPLICATION_JSON));
        String responseBody = resultActions.andReturn().getResponse().getContentAsString();
        String jwtToken = resultActions.andReturn().getResponse().getHeader(jwtVo.Header);
        System.out.println("테스트 = " + responseBody);
        System.out.println("테스트 = " + jwtToken);

        //then
        resultActions.andExpect(status().isUnauthorized());
    }
}

JWT 검증

//모든 주소에서 동작함(토큰검증) 인가 필터
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

 public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
     super(authenticationManager);
 }
 
 //JWT 토큰 헤더를 추가하지 않아도 해당 필터는 통과 할수는 있지만, 결국 시큐리티 단에서 세션값 검증에 실패
 @Override
 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

     if(isHeaderVerify(request, response)){//토큰이 존재한다면
         //토큰 접두사 제거
         String token = request.getHeader(jwtVo.Header).replace(jwtVo.TOKEN_PREFIX,"");
         LoginUser loginUser = JwtProcess.verify(token);  //토큰검증


         //UserDetails 타입 or username) 을 넣을수 있는데 NULL이기떄문에  loginUser = userdetails 통쨰로 넣음
         //임시 세션 만들기기//토큰도 있고 검증도 되었으니 인증된 유저로 보면 된다 // 강제로 토큰에 세션을 만든다
         Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
         //강제로 authentication 객체를 생성

         SecurityContextHolder.getContext().setAuthentication(authentication); //강제 로그인
         //authentication 객체를 SecurityContextHolder 담는다.
         //stateless 정책때문에 SecurityContextHolder 에 로그인하면서 loginUser가 저장되더있던게 사라져 있기떄문에 다시 생성하는 과정
         //세션에 인증과 권한 체크용으로만 저장을 하고 응답을 하면 사라진다.
         // /api/s/hello 면 인증인지 확인만 하면 되고 /api/admin/hello는 권한체크까지 해야하기 때문에

         //세션은 브라우저를 안끄거나 로그아웃을 안하면 원래 유지가 됨
         
         //필터를 다 타면 Dispatcher Servlet을 갔다가 컨트롤러로 간다

     } //SecurityConfig 의 jwt 필터 등록 필요

     chain.doFilter(request, response); // 필터가없으면 다음 필터로 간다
      //else로 짰으면 토큰없이는 테스트를 못한다.
     // 토큰이 있으면 세션이 만들어지고, 토큰이 없으면 세션이 안만들어지고 컨트롤러로 들어가서 시큐리티가 제어하게끔 했다.

 }
 //헤더 검증하는 메서드
 private boolean isHeaderVerify(HttpServletRequest request, HttpServletResponse response){
     String header = request.getHeader(jwtVo.Header);

     if (header == null || !header.startsWith(jwtVo.TOKEN_PREFIX)) { //헤더가 널이거나 시작값이 Bearer 이 아니면
         return false;
     }else{
         return true;
     }
 }

JWT 검증 테스트 코드

@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class JwtAuthorizationFilterTest {

    @Autowired
    private ObjectMapper om; // JSON으로 변경하기 위해 필요

    @Autowired
    private MockMvc mvc;

    @Test  //doFilterInternal 테스트
    public void authorization_success_test() throws Exception{
        //given
        User user = User.builder().id(1L).role(UserEnum.CUSTOMER).build();
        LoginUser loginUser= new LoginUser(user);
        String jwtToken = JwtProcess.create(loginUser);//클라이언트가 토큰을 들고 인가를 받기 떄문에 토큰 생성
        System.out.println("테스트jwtToken = " + jwtToken);
        //when
        ResultActions resultActions = mvc.perform(get("/api/s/hello/test").header(jwtVo.Header, jwtToken));

        //then
        resultActions.andExpect(status().isNotFound());
    }

    @Test  //doFilterInternal 테스트
    public void authorization_fail_test() throws Exception{
        //given

        //when
        ResultActions resultActions = mvc.perform(get("/api/s/hello/test"));

        //then
        resultActions.andExpect(status().isUnauthorized()); //401
    }

    @Test  //doFilterInternal 테스트
    public void authorization_admin_test() throws Exception{
        //given
        User user = User.builder().id(1L).role(UserEnum.CUSTOMER).build();
        LoginUser loginUser= new LoginUser(user);
        String jwtToken = JwtProcess.create(loginUser);//클라이언트가 토큰을 들고 인가를 받기 떄문에 토큰 생성
        System.out.println("테스트jwtToken = " + jwtToken);
        //when
        ResultActions resultActions = mvc.perform(get("/api/admin/hello/test").header(jwtVo.Header, jwtToken));

        //then
        resultActions.andExpect(status().isForbidden());
    }
}

순서

api/login 하면
1. UPAF 동작 username, password dto로 받는다 로그인 dto를 받는다
2. 파싱해서 loginReqDto object로 바꾼다.
3. 인증 토큰을 만든다// jwt 토큰 x authenticationToken
4. authenticationToken 인증 토큰으로 authenticate() 요청하면
userDetailService의 loadUserByUsername() 가 호출 -username, password DB확인
5. 없으면 unsuccessfulAuthentication() 실행 (컨트롤러까지 도달이 안된상태라),
6. 있으면 단순히 LoginUser 객체 생성하고 리턴
7. 시큐리티 전용 세션에 담김.(전체 세션의 SecurityContextHolder 부분만 Authtication에 LoginUser를 담는다)authentication.getPrincipal()
8. JWT 토큰을 생성하고 response 응답 헤더에 담는다. successfulAuthentication()
response되면 세션에 저장된건 사라진다 stateless정책을 사용해서

클라이언트가 헤더에 JWT를 갖고 있지만 SecurityContextHolder에는 LoginUser 정보가 사라진 상태

api/s/hello / /api/admin/hello
1. BAF 동작, 토큰 검증
2. 토큰이 존재하면 접두사 제거
3. stateless 정책때문에 SecurityContextHolder 에 로그인하면서 loginUser가 저장되더있던게 사라져 있기떄문에 authentication 객체 강제로 생성
4. authentication객체를 SecurityContextHolder에 담아준다
//세션에 인증과 권한 체크용으로만 저장을 하고 응답을 하면 사라진다.
// /api/s/hello 면 인증인지 확인만 하면 되고 /api/admin/hello는 권한체크까지 해야하기 때문에

0개의 댓글