1. JwtUtil

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
    private final RefreshTokenRepository refreshTokenRepository;
    private final UserDetailsServiceImpl userDetailsService;
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long ACCESS_TIME = 60 * 60 * 1000L; // ACCESS_TIME 1시간
    private static final long REFRESH_TIME = 7 * 24 * 60 * 60 * 1000L;  // REFRESH_TIME 7일
    public static final String ACCESS_TOKEN = "Access_Token";
    public static final String REFRESH_TOKEN = "Refresh_Token";
    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // header 토큰을 가져오는 기능
    public String getHeaderToken(HttpServletRequest request, String type) {
        return type.equals("Access") ? request.getHeader(ACCESS_TOKEN) :request.getHeader(REFRESH_TOKEN);
    }

    // 토큰 생성
    public TokenDto createAllToken(String email) {
        return new TokenDto(createToken(email, "Access"), createToken(email, "Refresh"));
    }

    public String createToken(String email, String type) {
        //현재 시각
        Date date = new Date();
        //지속 시간
        long time = type.equals("Access") ? ACCESS_TIME : REFRESH_TIME;

        return Jwts.builder()
                .setSubject(email)
                .setExpiration(new Date(date.getTime() + time))
                .setIssuedAt(date)
                .signWith(key, signatureAlgorithm)
                .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            throw new CustomException(LOGIN_WRONG_SIGNATURE_JWT_TOKEN);
        } catch (ExpiredJwtException e) {
            throw new CustomException(LOGIN_EXPIRED_JWT_TOKEN);
        } catch (UnsupportedJwtException e) {
            throw new CustomException(LOGIN_NOT_SUPPORTED_JWT_TOKEN);
        } catch (IllegalArgumentException e) {
            throw new CustomException(LOGIN_WRONG_FORM_JWT_TOKEN);
        } catch (SignatureException e) {
            throw new CustomException(SIGNATURE_EXCEPTION);
        }
    }

    // refreshToken 토큰 검증
    public Boolean refreshTokenValidation(String token) {

        // 1차 토큰 검증
        if(!validateToken(token)) return false;

        // DB에 저장한 토큰 비교
        Optional<RefreshToken> refreshToken = Optional.ofNullable(refreshTokenRepository.findByEmail(getUserInfoFromToken(token)));

        return refreshToken.isPresent() && token.equals(refreshToken.get().getRefreshToken());
    }

    // 토큰에서 loginId 가져오는 기능
    public String getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject();
    }

    // 인증 객체 생성
    public Authentication createAuthentication(String email) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(email);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

JwtUtil = 토큰에 관련된 암호화, 복호화, 검증 로직이 이루어지는 곳

1. init()

  • @PostConstruct 어노테이션은 특정 클래스의 메소드에 붙여서 해당 클래스의 객체 내 모든 의존성(Bean) 들이 초기화 된 직후에 딱 한 번만 실행되도록 해준다. 만약 객체에 의존성이 하나도 없더라도 실행된다.
  • WAS가 뜰 때 서비스가 생성되며 초기화 메소드가 실행된다.
  • 깃헙엔는 올라가지 않는 application-local.properties 파일에에 저장되어 있는 Jwt Secret Key를 복호화(디코딩 = 암호화의 반댓말)한다.

2. getHeaderToken()

  • 헤더부분에 타입에 따라 Access Token 과 Refresh Token을 가져오는 메소드
  • JwtAuthFilter 클래스에 사용된다.

3. createToken() / createAllTolen()

  • 유저 정보(email)를 넘겨받아서 Access Token 과 Refresh Token 을 생성하는 메소드
  • 타입에따라 각각 유효 시간을 저장해주고, Token Dto에 담아준다.
  • KakaoService, MemberService 에서 로그인한 후  response Header에 JWT 토큰 추가할 때 사용된다.

4. validateToken/refreshTokenValidation

  • 토큰 정보를 검증한다.
  • JwtAuthFilter의 doFilterInternal()에서 토큰 만료 여부를 확인할 때 사용된다.

5. getUserInfoFromToken()

  • 토큰에서 유저정보(email) 가져오는 메소드
  • JwtAuthFilter의 doFilterInternal()에서 토큰이 유효한 경우 토큰정보를 setAuthentication()메소드를 이용하여 Authentication에 저장해 줄 때 사용

6. createAuthentication()

  • 인증 객체를 생성하는 메소드
  • JwtAuthFilter의 setAuthentication() 에서 객체를 Authentication에 저장할 때 사용

 

2. JwtAuthFilter

@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
    public final JwtUtil jwtUtil;

    @Override //토큰 유효성 검사 후 Authentication 에 세팅
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String accessToken = jwtUtil.getHeaderToken(request, "Access");
        String refreshToken = jwtUtil.getHeaderToken(request, "Refresh");

        if (accessToken != null) {
            if (!jwtUtil.validateToken(accessToken)) {
                jwtExceptionHandler(response, "AccessToken Expired", HttpStatus.UNAUTHORIZED);
                return;
            }
            setAuthentication(jwtUtil.getUserInfoFromToken(accessToken));

        } else if (refreshToken != null) {
            if (!jwtUtil.refreshTokenValidation(refreshToken)) {
                jwtExceptionHandler(response, "RefreshToken Expired", HttpStatus.BAD_REQUEST);
                return;
            }
            // 3. 토큰이 유효하다면 토큰에서 정보를 가져와 Authentication 에 세팅
            setAuthentication(jwtUtil.getUserInfoFromToken(refreshToken));
        }
        // 4. 다음 필터로 넘어간다
        filterChain.doFilter(request, response);
    }

    public void setAuthentication(String email) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(email);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    public void jwtExceptionHandler(HttpServletResponse response, String message, HttpStatus statusCode) {
        response.setStatus(statusCode.value());
        response.setContentType("application/json; charset=UTF-8");
        try {
            String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto<>(StatusCode.BAD_REQUEST_TOKEN));
            response.getWriter().write(json);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

 

1. doFilterInternal()

  • 실제 필터링 로직을 수행하는 곳이다.
  • Request Header 에서 Access Token 과 Refresh Token을 꺼내온 후 토큰 유효 여부를 판단한다.
  • 토큰이 유효하면 Authentication에 넣어준다.
  • 가입/로그인/재발급을 제외한 모든 Request 요청은 이 필터를 거치기 때문에 토큰 정보가 없거나 유효하지 않으면 정상적으로 수행되지 않는다.

 

2. setAuthentication()

  • 토큰이 유효한 경우 토큰정보를 Authentication에 저장해 준다.

3. jwtExceptionHandler()

  • 토큰이 유효하지 않을 경우 핸들러를 통해 에러처리를 해준다.

 

3. Controller

@RestController
@RequiredArgsConstructor
@RequestMapping(produces = "application/json; charset=utf8")
public class MemberController {
    private final MemberService memberService;
    private final KakaoService kakaoService;

    // 로그인
    @PostMapping(value = "/auth/login")
    public ResponseEntity<?> login(@RequestBody SignupRequestDto signupRequestDto,
                                   HttpServletResponse response) {
        return ResponseUtil.response(memberService.login(signupRequestDto, response));
    }
    
    
    // 로그아웃
    @PostMapping("/auth/signout")
    public ResponseEntity<?> signout(@AuthenticationPrincipal UserDetailsImpl userDetails){
        return memberService.signout(userDetails.getMember().getEmail());
    }

    // 토큰 재발행
    @GetMapping("/auth/issue/token")
    public ResponseDto<String> issuedToken(@AuthenticationPrincipal UserDetailsImpl userDetails, HttpServletResponse response){
        return memberService.issuedToken(userDetails.getMember().getEmail(), response);
    }
}

 

4. Service

@RequiredArgsConstructor
@Service
@Slf4j
public class MemberService {
    private final PasswordEncoder passwordEncoder;
    private final RepositoryService repositoryService;
    private final JwtUtil jwtUtil;
    private final RefreshTokenRepository refreshTokenRepository;

    // 로그인
    @Transactional
    public MemberResponseDto login(SignupRequestDto signupRequestDto, HttpServletResponse response) {
        String email = signupRequestDto.getEmail();
        String password = signupRequestDto.getPassword();

        Member member = repositoryService.findMemberByEmail(email).orElseThrow(
                () -> new CustomException(StatusCode.LOGIN_MATCH_FAIL)
        );

        if (!passwordEncoder.matches(password, member.getPassword())) {
            throw new CustomException(StatusCode.BAD_PASSWORD);
        }

	// user email 값을 포함한 토큰 생성 후 tokenDto 에 저장
        TokenDto tokenDto = jwtUtil.createAllToken(signupRequestDto.getEmail());

        // user email 값에 해당하는 refreshToken 을 DB에서 가져옴
        Optional<RefreshToken> refreshToken = Optional.ofNullable(refreshTokenRepository.findByEmail(member.getEmail()));

        if (refreshToken.isPresent()) {
            refreshTokenRepository.saveRefreshToken(refreshToken.get().updateToken(tokenDto.getRefreshToken()));
        } else {
            RefreshToken newToken = new RefreshToken(tokenDto.getRefreshToken(), signupRequestDto.getEmail());
            refreshTokenRepository.saveRefreshToken(newToken);
        }

        setHeader(response, tokenDto);

        return new MemberResponseDto(member);
    }
    
    // 토큰 재발행
    public ResponseDto<String> issuedToken(String email, HttpServletResponse response){
        response.addHeader(JwtUtil.ACCESS_TOKEN, jwtUtil.createToken(email, "Access"));
        return ResponseDto.success("토큰재발행 성공");
    }
    
	// 로그인하면 Header에 토큰 담아주기
    private void setHeader(HttpServletResponse response, TokenDto tokenDto) {
        response.addHeader(JwtUtil.ACCESS_TOKEN, tokenDto.getAccessToken());
        response.addHeader(JwtUtil.REFRESH_TOKEN, tokenDto.getRefreshToken());
    }
    

    // 로그아웃
    public ResponseEntity<?> signout(String email) {
        // 해당 유저의 refreshtoken 이 없을 경우
        if(refreshTokenRepository.findByEmail(email) == null){
            throw new CustomException(StatusCode.INVALID_TOKEN);
        }
        // 자신의 refreshtoken 만 삭제 가능
        String memberIdrepo = refreshTokenRepository.findByEmail(email).getEmail();
        if(email.equals(memberIdrepo)){
            refreshTokenRepository.deleteRefreshToken(email);
            return ResponseUtil.response(StatusCode.SIGNOUT_OK);
        }else{
            return ResponseUtil.response(StatusCode.BAD_REFRESH_TOKEN);
        }
    }
}

 

 

※ 필요한 부분만 포스팅 했기 때문에 전체 코드는 아래 깃헙 링크를 참조해 주세요 ! ( 아직 수정중.... )

https://github.com/namoldak/Backend

 

GitHub - namoldak/Backend

Contribute to namoldak/Backend development by creating an account on GitHub.

github.com

 

 

+ Recent posts