# Spring Boot + OAuth2 #2

홍준식·2024년 2월 4일

OAuth 플로우는 일반적으로 아래와 같다.

출처: https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

여기서 Service Client는 프런트엔드, Service Server는 백엔드이다.
이때 6번 302 리다이렉트 URL을 프런트엔드로 전달해주는 방법과 서버로 전달하는 방법이 존재한다.

프런트엔드로 리다이렉트하는 경우 발급받은 토큰을 통해 access_token을 발급 받고, 백엔드 서버는 access_token이 유효한지를 확인하는 방식이다.

백엔드로의 콜백 방식은 최종적으로 프런트로 리다이렉팅할 주소를 알아야한다는 단점이 존재하지만 모든 것을 백엔드에서 처리하기에 방식이 간단해진다는 장점이 있고 Spring Security에서 제공하는 플로우도 콜백 URL을 백엔드로 받기에 백엔드에서 처리하도록 한다.

최종적으로 리다이렉팅할 프런트엔드의 URL 아는 방식으로 백엔드 서버에서 프런트 서버 URL 정보를 저장하는 방식과 위의 이미지의 1번에서 백엔드에 URL 요청을 하는 과정에 쿠키에 리다이렉트 URL을 저장해두는 방식 그리고 referer로 접근하는 방식이 존재한다.
프런트에서 유동적으로 URL을 변경하고 이것을 백엔드와 완전 독립적으로 동작할 수 있도록 하기 위해 쿠키에 저장해두는 방식을 선택하였다.

Spring OAuth

Security Config 설정

@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
public class OAuth2LoginSecurityConfig {  
    private final AuthenticationSuccessHandler authenticationSuccessHandler;  
    private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;  
  
    @Bean  
    public SecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception {  
        http  
                .oauth2Login(oauth2 -> oauth2  
                        .authorizationEndpoint(  
                                authorization -> authorization  
                                        .authorizationRequestRepository(authorizationRequestRepository)  
                        )  
                        .successHandler(authenticationSuccessHandler)  
                );  
        return http.build();  
    }  
}

oAuth2Login 결과에 필터를 설정하는 것으로 승인 후 User 생성(authorizationRequestRepository) 및 최종 리다이렉팅 URL(authenticationSuccessHandler)을 설정하도록 하겠다.

AuthorizationRequestRepository

@Component  
public class OAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {  
  
    public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";  
    private static final int COOKIE_EXPIRE_SECONDS = 180;  
  
    private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME = HttpSessionOAuth2AuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST";  
    private final String sessionAttributeName;  
  
    public OAuth2AuthorizationRequestRepository() {  
        this.sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;  
    }  
  
    @Override  
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {  
        Assert.notNull(request, "request cannot be null");  
        String stateParameter = this.getStateParameter(request);  
        if (stateParameter == null) {  
            return null;  
        } else {  
            OAuth2AuthorizationRequest authorizationRequest = this.getAuthorizationRequest(request);  
            return authorizationRequest != null && stateParameter.equals(authorizationRequest.getState()) ? authorizationRequest : null;  
        }  
    }  
  
    @Override  
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {  
        Assert.notNull(request, "request cannot be null");  
        Assert.notNull(response, "response cannot be null");  
        if (authorizationRequest == null) {  
            this.removeAuthorizationRequest(request, response);  
        } else {  
            String state = authorizationRequest.getState();  
            Assert.hasText(state, "authorizationRequest.state cannot be empty");  
            request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest);  
        }  
        saveRedirectUriInCookie(request, response);  
    }  
  
    @Override  
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {  
        Assert.notNull(response, "response cannot be null");  
        OAuth2AuthorizationRequest authorizationRequest = this.loadAuthorizationRequest(request);  
        if (authorizationRequest != null) {  
            request.getSession().removeAttribute(this.sessionAttributeName);  
        }  
  
        return authorizationRequest;  
    }  
  
    private String getStateParameter(HttpServletRequest request) {  
        return request.getParameter("state");  
    }  
  
    private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) {  
        HttpSession session = request.getSession(false);  
        return session != null ? (OAuth2AuthorizationRequest)session.getAttribute(this.sessionAttributeName) : null;  
    }  
  
    public void saveRedirectUriInCookie(HttpServletRequest request, HttpServletResponse response) {  
        String redirectUri = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);  
        if (redirectUri != null && !redirectUri.isBlank()) {  
            Cookie cookie = new Cookie(REDIRECT_URI_PARAM_COOKIE_NAME, redirectUri);  
            cookie.setPath("/");  
            cookie.setHttpOnly(true);  
            cookie.setMaxAge(COOKIE_EXPIRE_SECONDS);  
            response.addCookie(cookie);  
        }  
    }  
}

Default 레포지토리인 HttpSessionOAuth2AuthorizationRequestRepository를 참조하여 http://localhost:8080/oauth2/authorization/github?redirect_uri=http://localhost:3000/login 와 같이 redirect_uri라는 쿼리 파라미터를 통해 이동할 최종 콜백 uri를 쿠키에 저장해두고 Session을 통해 유저를 구분하도록 한다.

OAuth2AuthenticationSuccessHandler

@Component  
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {  
    @Override  
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {  
        Cookie cookie = Arrays.stream(request.getCookies()).filter(s -> s.getName().equals(REDIRECT_URI_PARAM_COOKIE_NAME)).findAny().orElse(null);  
  
        if (cookie != null) {  
            String targetUri = UriComponentsBuilder.fromUriString(cookie.getValue())  
                .build().toUriString();  
            RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();  
            redirectStrategy.sendRedirect(request, response, targetUri);  
        }  
    }  
}

OAuth 승인 시 처리하는 OAuth2AuthenticationSuccessHandler 를 통해 쿠키에 저장된 값을 최종적으로 이동할 URL을 설장한다.

이러면 http://localhost:8080/oauth2/authorization/github?redirect_uri=http://localhost:3000/login로 접근하고 깃허브 로그인을 완료하면 http://localhost:3000/login로 리다이렉팅 되는 것을 확인할 수 있다.

JWT 설정

이번에는 로그인된 사용자의 정보를 저장하고, access token을 발급하도록 한다.
JWT 설정을 위해 build.grade의 dependencies에

// https://mvnrepository.com/artifact/com.auth0/java-jwt  
implementation group: 'com.auth0', name: 'java-jwt', version: '4.4.0'

위와 같이 추가하도록 한다.

토큰 생성을 위한 유틸 클래스를 추가한다.

public class TokenService {  
  
    private static final String SECRET = "temporarilySecret";  
    private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";  
    private static final String EMAIL_CLAIM = "email";  
    private static final int ACCESS_TOKEN_EXPIRE_PERIOD = 15 * 60 * 1000;  // 15분  
  
    public static String createAccessToken(String email) {  
        Date now = new Date();  
        Algorithm algorithm = Algorithm.HMAC512(SECRET);  
        return JWT.create()  
                .withSubject(ACCESS_TOKEN_SUBJECT)  
                .withExpiresAt(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_PERIOD))  
                .withClaim(EMAIL_CLAIM, email)  
                .sign(algorithm);  
    }  
  
    public static boolean validateToken(String token) {  
        try {  
            return JWT.require(Algorithm.HMAC512(SECRET)).build().verify(token) != null;  
        } catch (JWTVerificationException e) {  
            return false;  
        }  
    }  
}

위는 이메일을 입력받고 토큰을 생성하는 클래스이다.

요청이 들어오면 User를 생성하고 토큰을 발급하여 응답하도록 한다.

@Getter  
@Setter  
@NoArgsConstructor  
public class User {  
    private String nickName;  
}
@Component  
public class InMemoryUserRepository {  
    private final HashMap<String, User> userMap = new HashMap<>();  
  
    public void saveUser(User user) {  
        userMap.put(user.getNickName(), user);  
    }  
}

간단한 확인을 위해 메모리에 HashMap을 통해 유저를 관리하도록 한다.

@RequiredArgsConstructor  
public class CustomOAuth2User implements OAuth2User {  
    private final User user;  
    private final Map<String, Object> attributes;  
  
    @Override  
    public Map<String, Object> getAttributes() {  
        return this.attributes;  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        return null;  
    }  
  
    @Override  
    public String getName() {  
        return this.user.getNickName();  
    }  
}

위와 같이 User를 추가하고 User를 통해 CustomOAuth2User를 생성한다.

@Service  
@RequiredArgsConstructor  
public class CustomOAuth2UserService extends DefaultOAuth2UserService {  
    private final InMemoryUserRepository inMemoryUserRepository;  
  
    @Override  
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {  
        OAuth2User oAuth2User = super.loadUser(userRequest);  
        User user = createUser(userRequest.getClientRegistration());  
        return new CustomOAuth2User(user, oAuth2User.getAttributes());  
    }  
  
    private User createUser(ClientRegistration clientRegistration) {  
        User user = new User();  
        user.setNickName(clientRegistration.getClientId());  
        inMemoryUserRepository.saveUser(user);  
        return user;  
    }  
}

CustomOAuth2UserService 를 생성하여 OAuth2 로그인이 되면 user를 자동으로 생성하도록 설정한다.

@Component  
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {  
    @Override  
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {  
        OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();  
        String accessToken = TokenService.createAccessToken(oAuth2User.getName());  
  
        Cookie cookie = Arrays.stream(request.getCookies()).filter(s -> s.getName().equals(REDIRECT_URI_PARAM_COOKIE_NAME)).findAny().orElse(null);  
  
        if (cookie != null) {  
            String targetUri = UriComponentsBuilder.fromUriString(cookie.getValue())  
                .queryParam("accessToken", accessToken)  
                .build().toUriString();  
            RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();  
            redirectStrategy.sendRedirect(request, response, targetUri);  
        }  
    }  
}

이후 OAuth2AuthenticationSuccessHandler를 위와 같이 수정하는 것으로 accessToken을 발급받고 응답을 처리할 수 있다.

결과적으로 http://localhost:8080/oauth2/authorization/github?redirect_uri=http://localhost:3000/login로 요청하면 http://localhost:3000/login?accessToken={access_token}로 응답이 오는 것을 확인할 수 있다.

0개의 댓글