이전글에서 구현했던 OAuth2UserService # loadUser() 가 실행이 완료되면
세션 방식 로그인에서는 시큐리티 세션(의 Authentication 객체)에 사용자 정보가 들어가게 됩니다.
(JWT 방식에서는... 별 일 없는 것 같습니다.)

여기까지 수행에 성공하면
AuthenticationSuccessHandler 타입 객체의 onAuthenticationSuccess 가 실행됩니다.

(이렇게 실행되게 하기 위해서
AuthenticationSuccessHandler 타입 객체를 만들고
oauth2 로그인 방식의 successHandler로 등록해주면 됩니다. 이것은 나중에 하도록 하겠습니다.)

인증 과정이 여기까지 진행되었다면, 이제 우리 서버는 토큰을 만들어서 브라우저에게 내려주는 일만 남았습니다.
이 작업을 AuthenticationSuccessHandler # onAuthenticationSuccess() 메서드에서 수행하도록 구현하겠습니다.

1. SimpleUrlAuthenticationSuccessHandler 상속

package com.yelim.security.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {

    }
}

 

2. TokenProvider 구현

JWT 토큰과 TokenProvider에 대해서는 다른 글에서 자세히 다루도록 하겠습니다.

package com.yelim.security;

import com.yelim.security.config.AppProperties;
import com.yelim.security.domain.UserPrincipal;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import java.security.Key;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

@Component
public class TokenProvider {

    private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

    private Long tokenExpirationMsec;
    private Key tokenSecretKey;

    public TokenProvider(AppProperties appProperties) {
        this.tokenSecretKey = encodeTokenSecret(appProperties.getAuth().getTokenSecret());
        this.tokenExpirationMsec = appProperties.getAuth().getTokenExpirationMsec();
    }

    public String createToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + tokenExpirationMsec);

        return Jwts.builder()
            .setSubject(Long.toString(userPrincipal.getId()))
            .setIssuedAt(new Date())
            .setExpiration(expiryDate)
            .signWith(tokenSecretKey, SignatureAlgorithm.HS512)
            .compact();
    }

    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(tokenSecretKey)
            .parseClaimsJws(token)
            .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser()
                .setSigningKey(tokenSecretKey)
                .parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false;
    }

    private Key encodeTokenSecret(String tokenSecret) {
        return Keys.hmacShaKeyFor(tokenSecret.getBytes());
    }
}

 

3. 구현

백엔드가 토큰을 내려줄 때 부터

토큰이 브라우저에 저장되기까지의

flow를 그림으로 그려보았습니다.

이 그림처럼 동작하게 만들어보겠습니다.

 

브라우저가 http://localhost:3000/oauth2/redirect?token=토큰내용 으로 redirect 하도록

302 응답을 내려줍니다.

 

package com.yelim.security.config;

import com.yelim.security.TokenProvider;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final TokenProvider tokenProvider;
    private final AppProperties appProperties;

    public OAuth2AuthenticationSuccessHandler(TokenProvider tokenProvider, AppProperties appProperties) {
        this.tokenProvider = tokenProvider;
        this.appProperties = appProperties;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException {
        String url = makeRedirectUrl(tokenProvider.createToken(authentication));

        if (response.isCommitted()) {
            logger.debug("응답이 이미 커밋된 상태입니다. " + url + "로 리다이렉트하도록 바꿀 수 없습니다.");
            return;
        }
        getRedirectStrategy().sendRedirect(request, response, url);
    }

    private String makeRedirectUrl(String token) {
        return UriComponentsBuilder.fromUriString("http://localhost:3000/oauth2/redirect")
            .queryParam("token", token)
            .build().toUriString();
    }
}

 

+ Recent posts