장학금
55
2022-02-08 00:01:49
2
372

spring security oauth2 client 테스트 질문하겠습니다.


안녕하세요. 직전 질문이 너무 성의가 없어서 질문을 다시 드리려합니다.


우선, spring-security-oauth2-client 로 소셜 로그인(카카오) 기능을 구현했습니다. 구현하는데는 지장이 없었는데요.


문제는 테스트입니다.


제가 소셜 로그인을 구현한 방법은

spring:
security:
oauth2:
client:
registration:
kakao:
clientId: ENC(6F/Ea4fNRic5dWQ6Bhbyc8w0rHvIh6LTpbao8Mq1zS/b2vaaxYT6NNHr6pYntYIc)
clientSecret: ENC(2AA+DBLwJBQPGuG4feRKGJWDyKCOGUnJUg5b48xSu26/wPo3xD1SCXXD/i8RnGhW)
clientAuthenticationMethod: POST
authorizationGrantType: authorization_code
redirectUri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope:
- profile_nickname
- profile_image
- account_email
clientName: Kakao
provider:
kakao:
authorizationUri: https://kauth.kakao.com/oauth/authorize
tokenUri: https://kauth.kakao.com/oauth/token
userInfoUri: https://kapi.kakao.com/v2/user/me
userNameAttribute: id

이렇게 application.yml을 설정해주었고,

@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

private final ObjectMapper objectMapper;
private final LoginFailureHandler loginFailureHandler;
private final LoginSuccessHandler loginSuccessHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final RestAccessDeniedHandler restAccessDeniedHandler;
private final CustomOAuth2Service customOAuth2Service;
private final TokenAuthenticationFilter tokenAuthenticationFilter;
private final TokenAuthenticationErrorFilter tokenAuthenticationErrorFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers().frameOptions().sameOrigin()

.and()
.authorizeRequests()
.anyRequest().permitAll()

.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()
.formLogin().disable()
.httpBasic().disable()
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restAccessDeniedHandler)

.and()
.oauth2Login()
.successHandler(loginSuccessHandler)
.userInfoEndpoint()
.userService(customOAuth2Service)

.and()
.authorizationEndpoint()
.baseUri("/api/oauth2/authorization");

http.addFilterBefore(tokenAuthenticationErrorFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter(objectMapper);
loginFilter.setFilterProcessesUrl("/api/login");
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
return loginFilter;
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

SecurityConfig.class도 위와 같이 작성해주었습니다.


@Service
@RequiredArgsConstructor
@Transactional
public class CustomOAuth2Service extends DefaultOAuth2UserService {

private final MemberRepository memberRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
Member member = saveOrUpdate(attributes);
return MemberPrincipal.of(member, attributes.getAttributes());
} }


마지막으로 위와 같이 CustomOAuth2Service를 정의해두었으며, 제가 이해한 플로우는 브라우저에서 카카오 아이디, 비밀번호를 입력하면 카카오를 갔다 와서 회원을 저장 후 LoginSuccessHandler에서 JWT 발급해주고 마무리 되는 플로우로 이해했습니다.


본격적인 질문인 몇일동안 골머리 앓고있는 단위 테스트입니다.


@WebMvcTest
@ExtendWith(RestDocumentationExtension.class)
@Import({RestDocsConfig.class, TestConfig.class})
class AuthControllerTest {

@Autowired MockMvc mockMvc;
@Autowired ObjectMapper objectMapper;
@Autowired WebApplicationContext ctx;
@Autowired RestDocumentationResultHandler restDocs;

@BeforeEach
public void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.ctx)
.apply(documentationConfiguration(provider))
.apply(springSecurity())
.alwaysDo(print())
.alwaysDo(restDocs)
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.build();
}

@DisplayName("oauth2 로그인 / 리다이렉션 성공")
@Test
void login_oauth2_kakao_success() throws Exception {

// when, then, docs
mockMvc.perform(get("/api/oauth2/authorization/{provider}", "kakao"))
.andExpect(status().is3xxRedirection())
.andDo(print())
.andDo(restDocs.document(
pathParameters(
parameterWithName("provider").description("OAuth2 소셜 로그인 제공자")
)));
}

@DisplayName("oauth2 로그인 / 성공")
@Test
void login_oauth2_success() throws Exception {

// given
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", "test_code");
params.add("state", "test_state");

// when, then
mockMvc.perform(get("/login/oauth2/code/kakao").params(params))
.andExpect(status().isOk());
} }


위 코드에서 직접 로그인 후 발급 받는 AccessToken, RefreshToken을 직접 받는 단위테스트를 진행해보고 싶은데,

어떤 방법으로 mocking하고 단위테스트를 진행해야되는지 도무지 감이 안잡혀서 선배님들께 질문드리고 싶습니다..!


위 경우는 어떻게 단위테스트를 진행해야하나요??





1
  • 답변 2

  • TorenA
    1k
    2022-02-08 08:54:59

    AccessToken 과 RefreshToken 을 발급받는 로직은 어디에 있나요?




  • 장학금
    55
    2022-02-08 12:48:15

    TorenA

    안녕하세요!

    @Component
    @RequiredArgsConstructor
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    private final TokenProvider tokenProvider;
    private final ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    response.setStatus(HttpStatus.OK.value());
    response.setContentType(APPLICATION_JSON_VALUE);
    response.setCharacterEncoding("UTF-8");

    String refreshToken = tokenProvider.createRefreshToken(authentication);
    String accessToken = tokenProvider.createAccessToken(authentication);
    AuthResponse authResponse = AuthResponse.of(accessToken,refreshToken);

    objectMapper.writeValue(response.getWriter(), authResponse);
    }
    }

    위 LoginSuccessHandler에서 실질적으로 이루어지게 구현했습니다.

    @Slf4j
    @Component
    public class TokenProvider {

    private static final String AUTH_PROVIDER = "authProvider";

    private final String secretKey;
    private final long accessTokenExpirationTimeInMilliSeconds;
    private final long refreshTokenExpirationTimeInMilliSeconds;

    public TokenProvider(
    @Value("${jwt.secret-key}") String secretKey,
    @Value("${jwt.access-expiration-time}") long accessTokenExpirationTimeInMilliSeconds,
    @Value("${jwt.refresh-expiration-time}") long refreshTokenExpirationTimeInMilliSeconds) {
    this.secretKey = secretKey;
    this.accessTokenExpirationTimeInMilliSeconds = accessTokenExpirationTimeInMilliSeconds;
    this.refreshTokenExpirationTimeInMilliSeconds = refreshTokenExpirationTimeInMilliSeconds;
    }

    public String createAccessToken(Authentication authentication) {
    return createToken(authentication, accessTokenExpirationTimeInMilliSeconds);
    }

    public String createRefreshToken(Authentication authentication){
    return createToken(authentication, refreshTokenExpirationTimeInMilliSeconds);
    }

    private String createToken(Authentication authentication, long expirationTimeMilliSeconds) {
    MemberPrincipal principal = (MemberPrincipal) authentication.getPrincipal();
    Date now = new Date();
    Date expiryDate = new Date(now.getTime() + expirationTimeMilliSeconds);
    return Jwts.builder()
    .setSubject(principal.getUsername())
    .claim(AUTH_PROVIDER, principal.getMember().getAuthProvider())
    .setIssuedAt(now)
    .setExpiration(expiryDate)
    .signWith(SignatureAlgorithm.HS512, secretKey)
    .compact();
    }

    public String getUserEmail(String token) {
    Claims claims = getClaims(token);
    return claims.getSubject();
    }

    public AuthProvider getAuthProvider(String token) {
    Claims claims = getClaims(token);
    String authProviderType = claims.get(AUTH_PROVIDER, String.class);
    return AuthProvider.valueOf(authProviderType);
    }

    public long getRemainingMilliSecondsFromToken(String token){
    Date expiration = getClaims(token).getExpiration();
    return expiration.getTime() - (new Date()).getTime();
    }

    private Claims getClaims(String token) {
    return Jwts.parser()
    .setSigningKey(secretKey)
    .parseClaimsJws(token)
    .getBody();
    }

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

    위 코드는 TokenProvider로 실제 JWT를 발급시켜주는 클래스입니다.



  • 로그인을 하시면 답변을 등록할 수 있습니다.