SPRING-SECURITY

JWT + OAuth 구현 - (4)

cocodingding 2023. 12. 14. 00:25
package com.example.demo.auth.filter;

import com.example.demo.auth.jwt.JwtService;
import com.example.demo.auth.util.PasswordUtil;
import com.example.demo.member.entity.Member;
import com.example.demo.member.repository.MemberRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {

    private static final String NO_CHECK_URL = "/login"; // "/login"으로 들어오는 요청은 Filter 작동 X

    private final JwtService jwtService;
    private final MemberRepository memberRepository;

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getRequestURI().equals(NO_CHECK_URL)) {
            filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출
            return; // return으로 이후 현재 필터 진행 막기 (안해주면 아래로 내려가서 계속 필터 진행시킴)
        }

        String refreshToken = jwtService.extractRefreshToken(request)
                .filter(jwtService::isTokenValid)
                .orElse(null);

        if (refreshToken != null) {
            checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
            return; // RefreshToken을 보낸 경우에는 AccessToken을 재발급 하고 인증 처리는 하지 않게 하기위해 바로 return으로 필터 진행 막기
        }

        if (refreshToken == null) {
            checkAccessTokenAndAuthentication(request, response, filterChain);
        }
    }

    public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
        memberRepository.findByRefreshToken(refreshToken)
                .ifPresent(member -> {
                    String reIssuedRefreshToken = reIssueRefreshToken(member);
                    jwtService.sendAccessAndRefreshToken(response, jwtService.createAccessToken(member.getEmail()),
                            reIssuedRefreshToken);
                });
    }

    private String reIssueRefreshToken(Member member) {
        String reIssuedRefreshToken = jwtService.createRefreshToken();
        member.updateRefreshToken(reIssuedRefreshToken);
        memberRepository.saveAndFlush(member);
        return reIssuedRefreshToken;
    }

    public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
                                                  FilterChain filterChain) throws ServletException, IOException {
        log.info("checkAccessTokenAndAuthentication() 호출");
        jwtService.extractAccessToken(request)
                .filter(jwtService::isTokenValid)
                .ifPresent(accessToken -> jwtService.extractEmail(accessToken)
                        .ifPresent(email -> memberRepository.findByEmail(email)
                                .ifPresent(this::saveAuthentication)));

        filterChain.doFilter(request, response);
    }

   
    public void saveAuthentication(Member myMember) {
        String password = myMember.getPassword();
        if (password == null) { // 소셜 로그인 유저의 비밀번호 임의로 설정 하여 소셜 로그인 유저도 인증 되도록 설정
            password = PasswordUtil.generateRandomPassword();
        }

        UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
                .username(myMember.getEmail())
                .password(password)
                .roles(myMember.getRole().name())
                .build();

        Authentication authentication =
                new UsernamePasswordAuthenticationToken(userDetailsUser, null,
                        authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

 

doFilterInternal

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    if (request.getRequestURI().equals(NO_CHECK_URL)) {
        filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출
        return; // return으로 이후 현재 필터 진행 막기 (안해주면 아래로 내려가서 계속 필터 진행시킴)
    }

    String refreshToken = jwtService.extractRefreshToken(request)
            .filter(jwtService::isTokenValid)
            .orElse(null);
    if (refreshToken != null) {
        checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
        return; // RefreshToken을 보낸 경우에는 AccessToken을 재발급 하고 인증 처리는 하지 않게 하기위해 바로 return으로 필터 진행 막기
    }
    if (refreshToken == null) {
        checkAccessTokenAndAuthentication(request, response, filterChain);
    }
}

인증 처리/ 인증 실패/토큰 재발급을 처리한다.

"/login" 요청이 들어오면 filterChain.doFilter()를 호출해 현재 필터를 통과하고

바로 return을 해줘서 밑으로 코드진행을 하지 않고 다음 필터를 호출해서 넘긴다

String refreshToken = jwtService.extractRefreshToken(request)
        .filter(jwtService::isTokenValid)
        .orElse(null);

JwtService에서 만들었던 jwtService.extractRefreshToken()을 통해

요청 헤더에서 refreshToken을 추출하고,

filter()를 통해 유효한 RefreshToken을 반환한다.

RefreshToken이 유효하지 않거나 존재하지 않는다면 null을 반환 

클라이언트의 요청 헤더에 RefreshToken이 있는 경우는,

AccessToken이 만료되어 클라이언트가 RefreshToken을 요청에 담아 보낸 경우밖에 없다.

따라서, RefreshToken이 있는 경우는 RefreshToken 비교 후 AccessToken을 재발급하면 된다.

if (refreshToken != null) {
    checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
    return; // RefreshToken을 보낸 경우에는 AccessToken을 재발급 하고 인증 처리는 하지 않게 하기위해 바로 return으로 필터 진행 막기
}

 RefreshToken이 존재하고, 유효할 때 처리하는 로직

RefreshToken이 요청 헤더에 존재했다면, 클라이언트가 AccessToken이 만료되어서 요청한 것이므로

해당 RefreshToken을 통해 DB에서 유저를 찾고, AccessToken/RefreshToken을 재발급 해주는 메소드인

checkRefreshTokenAndReIssueAccessToken()를 호출

if (refreshToken == null) {
    checkAccessTokenAndAuthentication(request, response, filterChain);
}

RefreshToken이 존재하지 않거나 유효하지 않을 때 처리하는 로직

checkAccessTokenAndAuthentication()을 호출하여,

AccessToken의 유효성을 검증하고 인증 성공, 실패 처리

 

checkRefreshTokenAndReIssueAccessToken

public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
    memberRepository.findByRefreshToken(refreshToken)
            .ifPresent(member -> {
                String reIssuedRefreshToken = reIssueRefreshToken(member);
                jwtService.sendAccessAndRefreshToken(response, jwtService.createAccessToken(member.getEmail()),
                        reIssuedRefreshToken);
            });
}

위에서 추출한 RefreshToken을 통해 userRepository.findByRefreshToken()으로 유저를 찾고

유저가 존재한다면, 리프레시 토큰을 재발급하는 reIssuedRefreshToken()을 호출하여 리프레시 토큰을 재발급하고,

jwtService.createAccessToken()으로 액세스 토큰을 재발급한다.

그 후 jwtService.sendAccessAndRefreshToken()으로

재발급한 액세스 토큰, 리프레시 토큰을 Response에 보낸다.

reIssueRefreshToken

private String reIssueRefreshToken(Member member) {
    String reIssuedRefreshToken = jwtService.createRefreshToken();
    member.updateRefreshToken(reIssuedRefreshToken);
    memberRepository.saveAndFlush(member);
    return reIssuedRefreshToken;
}

jwtService.createRefreshToken()으로 RefreshToken을 생성하여,

member.updateRefreshToken()으로 DB의 RefreshToken을 업데이트 시킨 후,

재발급한 RefreshToken을 반환

 

AccessToken의 유효성을 검증 및 인증 처리

public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              FilterChain filterChain) throws ServletException, IOException {
    log.info("checkAccessTokenAndAuthentication() 호출");
    jwtService.extractAccessToken(request)
            .filter(jwtService::isTokenValid)
            .ifPresent(accessToken -> jwtService.extractEmail(accessToken)
                    .ifPresent(email -> memberRepository.findByEmail(email)
                            .ifPresent(this::saveAuthentication)));

    filterChain.doFilter(request, response);
}

JwtService.extractAccessToken()으로 액세스 토큰을 추출하여

유효성을 검증 후, jwtService.extractEmail()으로 이메일을 추출 후 

해당 이메일로 유저를 찾아 saveAuthentication()의 파라미터로 유저를 넘겨서

해당 유저를 인증 처리

인증 처리

public void saveAuthentication(Member myMember) {
    String password = myMember.getPassword();
    if (password == null) { // 소셜 로그인 유저의 비밀번호 임의로 설정 하여 소셜 로그인 유저도 인증 되도록 설정
        password = PasswordUtil.generateRandomPassword();
    }

    UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
            .username(myMember.getEmail())
            .password(password)
            .roles(myMember.getRole().name())
            .build();

    Authentication authentication =
            new UsernamePasswordAuthenticationToken(userDetailsUser, null,
                    authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities()));

    SecurityContextHolder.getContext().setAuthentication(authentication);
}

UserDetails의 User를 Builder로 생성 후 해당 객체를 인증 처리하여 

해당 유저 객체를 SecurityContextHolder에 담아 인증 처리를 진행

(소셜 로그인의 경우 password가 null인데, 인증 처리 시 password가 null이면 안 되므로, 랜덤 패스워드를 임의로 부여)