JWT - 인증이 필요한 페이지 요청시 토큰으로 권한 인증

최고요·2023년 5월 3일
2

JWT V1 

목록 보기
15/15
post-thumbnail

인증된 사용자에게 토큰을 담은 응답하기

package com.choigoyo.config.JWT;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.choigoyo.config.auth.PrincipalDetails;
import com.choigoyo.entity.UserEntityJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    /**
     * Spring Security 의 UsernamePasswordAuthenticationFilter 를 확장하여 사용하게되면
     * http://localhost:8081/login 주소로 userName,password 를 post 형식으로 전송하면
     * UsernamePasswordAuthenticationFilter 가 동작함 SecurityConFig 클래스에 form 로그인을 비활성화 시켜서 동작하지 않는 상태이나,
     * JwtAuthenticationFilter 를 다시 SecurityConFig 클래스에 등록해주면 동작하게됨
     */

    private final AuthenticationManager authenticationManager;


    // http://localhost:8081/login 요청이 들어오면 authenticationManager 통해서 로그인시도
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("JwtAuthenticationFilter : 로그인 시도중...");

        // 1 사용자의 userName, password 를 받는다
        // 2 로그인이 가능한지 확인
        // 3 PrincipalDetails 를 세션에 담는다 (권한 관리를 위해)
        // 4 JWT 토큰을 만들어서 응답해준다

        try {
            // request.getInputStream() 에 담겨있는 값 확인하기
//            BufferedReader reader = request.getReader();
//            String input = null;
//            while ((input = reader.readLine())!= null) {
//                System.out.println(input); // 보내온 값을 한줄씩 출력


            // json 데이터를 파싱해줌
            ObjectMapper objectMapper = new ObjectMapper();
            UserEntityJWT userEntityJWT = objectMapper.readValue(request.getInputStream(), UserEntityJWT.class);
            // 유저에 정보가 잘 담겼는지 print 해보기
            System.out.println(userEntityJWT);
            // 토큰 생성(userName, password 를 담은)
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(userEntityJWT.getUserName(),userEntityJWT.getPassword());
            // authenticationManager.authenticate() 인증 수행하기
            // 그리고 PrincipalDetailsService 의 loadByUsername() 함수가 실행됨
            Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
            System.out.println(request.getInputStream()); // request.getInputStream() 은 HttpServletRequest 객체로부터 입력 스트림을 가져오는 데 사용
            // 클라이언트 또는 다른 웹브라우저에서 서버로 전송된 데이터를 읽는데 사용할 수 있는 InputStream 객체
            // authenticate 객체가 session 영역에 저장됨
            PrincipalDetails principalDetails = (PrincipalDetails)authenticate.getPrincipal();
            System.out.println("==================로그인 완료=================="); // 구분선
            System.out.println("principalDetails userName :"+principalDetails.getUser().getUserName());
            System.out.println("==============================================="); // 구분선
            return authenticate;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    // 순서 ->  attemptAuthentication 에서 인증이 정상적으로 실행되고 successfulAuthentication 메서드 실행
    // JWT 토큰을 만들어서 request 요청한 사용자에게 토큰을 응답과함께 전달
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        System.out.println("사용자 인증이 완료되어 successfulAuthentication 메서드가 실행됩니다.");

        PrincipalDetails principalDetails = (PrincipalDetails)authResult.getPrincipal();

        // 빌드패턴으로 토큰 생성하기
        String jwtToken = JWT.create()
                .withSubject("JWT-TOKEN")
                .withExpiresAt(new Date(System.currentTimeMillis() + (60000 * 10))) // 토큰의 유효시간을 10분으로 지정
                .withClaim("id", principalDetails.getUser().getId())
                .withClaim("username", principalDetails.getUser().getUserName())
                .sign(Algorithm.HMAC512("server-secret")); // server만 알고있는 secret값 으로 서명

        response.addHeader("Authentication","Bearer "+jwtToken);  // 사용자에게 응답

        System.out.println("==================토큰 생성==================");
        System.out.println("name : Authentication");
        System.out.println("value: Bearer "+ jwtToken);

    }
}

클라이언트가 로그인 요청을하고 인증단계에 들어갑니다.
인증이 완료가되면 successfulAuthentication 메서드가 실행되며 토큰을 만들어 클라이언트에게 응답합니다.
(응답 Header의 Authentication 객체 안에는 토큰이 들어있습니다.)

postman으로 클라이언트의 아이디와 비밀번호 정보를 json으로 입력해주고
post 타입이르 로그인 요청을 보내면, JwtAuthenticationFilter 클래스의 attemptAuthentication 메서드가 실행되면 데이터를 파싱하고 유저정보를 가지고 인증을 수행합니다.

인증이 완료되고나서 successfulAuthentication 메서드에서 토큰을 생성해 응답 Header의Authentication 객체에 토큰을 담아 응답합니다.



권한이 필요한 페이지를 요청했을 때

package com.choigoyo.config.JWT;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.choigoyo.config.auth.PrincipalDetails;
import com.choigoyo.entity.UserEntityJWT;
import com.choigoyo.repository.UserRepository;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
    // BasicAuthenticationFilter Spring Security에서 제공하는 필터 중 하나로, HTTP 요청이 들어올 때 기본 인증(Basic Authentication)을 처리하는 역할
    // 인증이나 권한이 필요한 주소요청이 있을 때 해당 필터를 거치게됨
    private UserRepository userRepository;

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
        super(authenticationManager);
        this.userRepository = userRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("인증이나 권한이 필요한 주소가 요청되었습니다.");
        String jwtHeader = request.getHeader("Authorization"); // header 값을 확인해서 출력해보기
        System.out.println("jwtHeader : "+jwtHeader);
        // JWT 토큰을 검증해서 정상적인 사용자인지 확인
        if (jwtHeader == null || !jwtHeader.startsWith("Bearer ")) { // header에 토큰이 존재하지 않거나 토큰의 시작이 Bearer이 아니라면
            chain.doFilter(request,response); // 다시 필터를 거치게
            return;
        }

        // Authorization 에서 토큰값만 저장하기위해  "Bearer "을 빈 문자열로 변경하여 저장 (토큰값만 뽑아내는 작업)
        String jwtToken = request.getHeader("Authorization").replace("Bearer ", "");
        // 서명하여 user의 이름을 가져온다.
        String userName = JWT.require(Algorithm.HMAC512("server-secret")).build().verify(jwtToken)
                .getClaim("username").asString();
        System.out.println("================ username ==================");
        System.out.println("userName : "+userName);
        // 서명이 정상적으로 되면 if문 실행
        if (userName != null) {
            UserEntityJWT userEntityJWT = userRepository.findByUserName(userName);
            System.out.println("=================== 토큰으로 사용자 정보 찾기 ===================");
            System.out.println("userEntityJWT : "+userEntityJWT);
            PrincipalDetails principalDetails = new PrincipalDetails(userEntityJWT);

            Authentication authentication =
                    new UsernamePasswordAuthenticationToken(
                            principalDetails, //나중에 컨트롤러에서 DI해서 쓸 때 사용하기 편함.
                            null, // 패스워드는 모르니까 null 처리
                            principalDetails.getAuthorities());

            // 강제로 세션공간에 접근하여 Authentication 객체를 세션 영역에 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request,response); // 다시 필터를 거치게
        }

    }
}


JwtAuthorizationFilter 클래스는 권한이 필요한 페이지에 대한 요청이 발생할 때 호출되며, 사용자가 제공한 JWT 토큰을 검증합니다. 검증이 완료되면 토큰에서 사용자 이름을 추출하고, 이를 사용하여 데이터베이스에서 사용자 정보를 가져옵니다.

PrincipalDetails 객체를 생성한 후, UsernamePasswordAuthenticationToken 객체를 생성하여 사용자 정보와 권한을 설정합니다. 이후, SecurityContextHolder를 사용하여 인증 객체를 세션에 저장하고, 필터 체인을 계속 실행하여 요청을 처리합니다.

요약하면, 이 클래스는 다음과 같은 작업을 수행합니다:

  1. 요청 헤더에서 JWT 토큰을 검색합니다.
  2. 토큰이 올바른지 검증하고, 사용자 이름을 추출합니다.
  3. 사용자 이름을 기반으로 데이터베이스에서 사용자 정보를 가져옵니다.
  4. 사용자 정보와 권한을 포함하는 UsernamePasswordAuthenticationToken 객체를 생성합니다.
  5. SecurityContextHolder를 사용하여 인증 객체를 세션에 저장합니다.
  6. 필터 체인을 계속 실행하여 요청을 처리합니다.

이 프로세스는 권한이 필요한 페이지에 대한 요청을 처리하는 데 사용되며, 올바른 사용자 인증을 보장합니다.

profile
i'm best

0개의 댓글