
무효화할 RefreshToken을 저장할 테이블이다.
@Override
public Optional<RefreshToken> findValidRefToken(Long memberId) {
String jpql = "select rf from RefreshToken rf left join RefreshTokenBlackList rtb on rf = rtb.refreshToken where rf.member.id = :memberId and rtb.id is null";
return entityManager.createQuery(jpql, RefreshToken.class)
.setParameter("memberId", memberId)
.getResultStream()
.findFirst();
}
memberId로 리프레시 토큰 조회 -> RefreshTokenBlackList에 있나?rf (리프레시 토큰 엔티티 클래스) = rtb.refreshToken (블랙리스트 엔티티에서 참조하고 있는 필드)member_id 가 아니라 객체 중심으로 생각하여 member.id, rtb.id와 같이 객체와 필드를 이용해서 값을 나타낸다.findFirst()로 Optional<> 타입을 반환받는다.💡 Reminder
OAuth2SuccessHandler는 로그인이 성공한 후, 즉, 인증이 성공한 후 호출된다 !
Authentication객체가 존재한 이후에 호출되며, OAuth2 로그인 성공 이후의 처리를 담당하는 핸들러이다.
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Value("${custom.jwt.redirection.base}")
private String baseUrl;
private final MemberService memberService;
private final JwtTokenProvider jwtTokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
MemberDetails principal = (MemberDetails) authentication.getPrincipal();
Member findMember = memberService.getById(principal.getId());
HashMap<String, String> params = new HashMap<>();
// Optional 타입의 유효한 Refresh Token
Optional<RefreshToken> refreshTokenOptional = jwtTokenProvider.findRefreshToken(principal.getId());
// 유효하지 않다면 재발급 ! (혹은 처음 발급 받는 것)
if( refreshTokenOptional.isEmpty() ) {
TokenPair tokenPair = jwtTokenProvider.generateTokenPair(findMember); // access랑 refresh 발급받기
params.put("access", tokenPair.getAccessToken());
params.put("refresh", tokenPair.getRefreshToken());
} else { // 유효하다면 access token만 재발급 !
String accessToken = jwtTokenProvider.issueAccessToken(principal.getId(), principal.getRole());
params.put("access", accessToken);
params.put("refresh", refreshTokenOptional.get().getRefreshToken());
}
String urlStr = genUrlStr(params);
getRedirectStrategy().sendRedirect(request, response, urlStr);
}
@Value("${custom.jwt.redirection.base}") : application.xml 파일에 환경변수처럼 baseUrl을 설정해두었다. -> http://localhost:3000/authauthentication.getPrincipal() : 로그인한 유저의 principal을 꺼낸다. memberService.getById() : 토큰 발급 시 DB 정보를 활용할 수 있도록 Member 엔티티를 조회한다.HashMap<String, String> params : access token과 refresh token을 담아줄 자료구조이다. jwtTokenProvider.findRefreshToken() : DB에 유효한 RefreshToken이 있는지 찾아본다.params에 담아준다.genUrlStr() : 리다이렉트할 URL을 문자열로 만들어주는 메서드이다.getRedirectStrategy().sendRedirect(request, response, urlStr) : 리디렉션 전략을 반환하는 메서드이다. 사용자 혹은 클라이언트를 urlStr로 리디렉션하며, request, response 객체를 넘겨준다.Q1-1. 리다이렉트는 Request, Response 객체를 재활용하지 않는 것으로 알고 있는데, 왜 리다이렉트할 때 함께 넘겨주는 걸까?
A1-1. 핵심은 response인데, response 객체를 통해서 클라이언트에게 302 응답 (리다이렉션을 의미하는 Status code) + Location 헤더를 보내준다. 아래와 같이 서버가 응답을 보낼 수 있으려면, 응답 객체를 사용해야 하기 때문에 Response 객체를 함께 넘겨주는 것이다.
response.sendRedirect("http://localhost:3000/auth?access=abc&refresh=xyz");
Location 헤더 ❓
HTTP 응답에서 사용되는 HTTP 헤더로서, 리디렉션할 대상 URL을 명시해준다. 아래와 같다.
HTTP/1.1 302 Found Location: https://your-frontend.com/auth?access=abc&refresh=xyz
그렇게 되면 클라이언트(브라우저)가 Location 헤더를 따라 새로 요청한다. 그리고 이 요청을 React 같은 프론트엔드 앱이 처리해서 로그인 완료 화면을 띄우거나, 토큰을 저장하기도 한다.
GET /auth?access=eyJ...&refresh=eyJ... HTTP/1.1
Host: localhost:3000
Q1-2. 그렇다면 request는 왜 넘길까?
A1-2. 보통 request는 리디렉션할 URL을 만들 때 요청 정보를 참고하기 위해 넘긴다. 예를 들어, 현재 요청이 어떤 경로로 들어왔는지, 세션 정보가 있는지 등의 정보를 담고 있다.
private String genUrlStr(HashMap<String, String> params) {
return UriComponentsBuilder.fromUriString(baseUrl)
.queryParam("access", params.get("access"))
.queryParam("refresh", params.get("refresh"))
.build()
.toUri()
.toString();
}
http://localhost:3000/auth?access=abc123&refresh=xyz456 이러한 URL을 만들게 되는 것이다.최초 로그인을 할 때는 OAuth2SuccessHandler가 실행되고, 이 핸들러에서 Access Token과 Refresh Token을 발급해준다. 그러고, 클라이언트에게 리디렉션을 하며 토큰을 전달하게 된다.
사용자가 토큰을 들고 API에 접근할 때마다, 사용자가 가지고 있는 토큰을 확인해야 한다. JWT 토큰을 검증하고, 인증 정보를 SecurityContext에 설정하는 역할을 하는 메서드를 구현해야 한다.
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
UsernamePasswordAuthenticationFilter 보다 jwtAuthenticationFilter을 먼저 실행하라고 설정한 것이다. 하지만, 우리는 JWT를 사용하기 때문에, 토큰을 헤더에 넣어서 요청이 들어오고, 이 때 커스텀으로 작성해준 jwtAuthenticationFilter이 실행되어야 하는 것이다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final MemberService memberService;
OncePerRequestFilter : 요청마다 한 번만 실행되는 필터이다. @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.validate(token)) {
TokenBody tokenBody = jwtTokenProvider.parseJwt(token);
MemberDetails memberDetails = memberService.getMemberDetailsById(tokenBody.getMemberId());
Authentication authentication = new UsernamePasswordAuthenticationToken(
memberDetails, token, memberDetails.getAuthorities());
getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
resolveToken(request) : 요청 헤더에서 JWT토큰을 추출하는 메서드이다. Authorization 헤더에서 Bearer 로 시작하는 값을 추출한다.jwtTokenProvider.validate() : 해당 토큰이 문제가 없는지, 서명이 맞는지, 아직 유효기간이 끝나지 않았는지를 검사한다.parseJwt(token) : JWT 토큰을 파싱하여 Payload에 있는 Claim을 가져온다. -> 여기서는 id와 role을 가져와 DTO인 tokenBody에 넣어준다. id를 추출하고, 그 id에 맞는 회원 정보를 데이터베이스에서 조회한다. -> 사용자 인증 정보를 제공하는 객체인 MemberDetails로 반환한다.UsernamePasswordAuthenticationToken : Spring Security에서 인증을 나타내는 객체로서, principal, credentials, grantedAuthority로 구성된다.SecurityContextHolder에서 현재의 SecurityContext를 가져온다. setAuthentication(authentication) : Security Context에 인증 정보를 설정하여, 이후의 Spring Security 필터나 인증 과정에서 현재 사용자의 인증 정보를 사용하도록 한다.filterChain.doFilter() : 다음 필터로 요청을 전달하는 메서드로, 이 메서드를 호출하지 않으면 요청이 중단된다. private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if( bearerToken != null && bearerToken.startsWith("Bearer ") ) {
return bearerToken.substring(7);
}
return null;
}
Bearer 로 시작하는 부분을 잘라내고 그 뒤의 토큰을 반환한다.오늘 아침에 꽃가루 알러지가 너무 심해져서 잠에서 일찍 깼다. 아침 일찍부터 알러지 약도 먹고, 머리도 너무 아파서 타이레놀도 먹었다 ㅎ.. 아니 컨디션 안 좋은 채로 수업 듣는 게 너무... 말도 안돼서 약 싫어, 병원 싫어 인간도 스스로 약 먹고 병원 가도록 한다 허허허.. 다행히 한 1교시 듣고나서부터 많이 괜찮아졌었다 ! 아프지 망고 !
오우.. 오늘은 오후 수업 때 살짝 또 이해가 안됐다. 지금은 또,, 이해가 돼서 다행이다 휴.. 내일부터 프로젝튼데.. 아주 두근두근 걱정걱정이다. 한 1주일동안 파이팅해서 열심히 해봐야지 !! 파이팅..!