Java

spring security oauth2如何进行接口单元测试

2019-12-05  本文已影响0人  蔺荆门

前言

之前在项目中一直都是手动测试接口, 流程一般是手动获取token之后添加到header里(或者是使用工具的环境变量等等), 然后测试; 使用junit直接测试方法好用, 但也有它的局限性, 像一些参数的校验(如果你的参数校验是使用javax的validation)和spring security用户的获取就无法测试; 因为我在v2ex上的一篇帖子, 我开始琢磨一些单测的东西, 一开始就发现有一个阻碍, 每次测试接口都需要自己手动获取token的话就没办法将测试自动化; 在网上找了一下相关的资料发现对于spring security oauth2的测试资料不太多, 但是还是找到了Stack Overflow上的一个答案, 基于这位前辈在15年的分享, 有了这篇文章;

其实这里大家也可能会说, 可以使用脚本或者工具自动的使用账号获取token, 然后自动化测试啊; 但是这个方式在我的项目中行不通, 因为目前我的这个系统中有一种用户是微信小程序用户, 登录方式为小程序用户使用code到后端换取token, 换取token的过程中我会使用用户的code从微信那里获取openid; 所以这部分用户我没办法使用脚本来获取token;

思路

首先我最先想到的是有没有一个测试框架对spring security oauth2做了适配, 然后我就可以直接使用, 那就美滋滋了; 但是我找了一天, 未果, 如果大家知道的话请不吝赐教;

那既然没有现成的测试框架可以使用的话我们就得自己搞点事情了; 首先我们上面说到问题的根源其实就是在获取token的过程需要我们手动来获取, 那我们就这个问题往下探讨; 如果我们可以自动获取token就问题就解决了, 可我上面说过, 这个token我没办法通过脚本来获取, 怎么办呢? 那就直接从spring security入手, 我们在调用接口之前先往spring security的context里面直接放入用户(没有经过系统里面的用户校验逻辑哦); 然后用该用户的token调用接口就可以了嘛; 话不多说, 动手;

show me some code

我们使用mockmvc测试接口(但其实思路有了之后用什么client都可以), 先写一下如何往spring security的context里面放入用户的代码;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.request.RequestPostProcessor;

import java.util.Collections;

/**
 * @author sunhao
 * @date create in 2019-12-05 14:08:31
 */
@Component
public class TokenFactory {

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthenticationFactory authenticationFactory;

    @Qualifier("defaultAuthorizationServerTokenServices")
    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    public RequestPostProcessor token(String username) {

        return request -> {

            ClientDetails clientDetails = getClientDetails();

            OAuth2AccessToken oAuth2AccessToken = getAccessToken(authenticationFactory.getAuthentication(username), clientDetails);

            // 关键是这里, 将token放入header
            request.addHeader("Authorization", String.format("bearer %s", oAuth2AccessToken.getValue()));
            return request;
        };
    }

    private ClientDetails getClientDetails() {

        return clientDetailsService.loadClientByClientId("atom");
    }

    private OAuth2AccessToken getAccessToken(Authentication authentication, ClientDetails clientDetails) {

        TokenRequest tokenRequest = new TokenRequest(Collections.emptyMap(), clientDetails.getClientId(), clientDetails.getScope(), "diy");
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
        return authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
    }

}

上面的代码中, AuthenticationFactory是需要我们自己实现的, 其他的流程是spring security oauth2获取token的方式; 接着来贴一下AuthenticationFactory的实现方式;

import com.atom.projects.mall.entity.Admin;
import com.atom.projects.mall.entity.Employee;
import com.atom.projects.mall.entity.MiniProCustomer;
import com.atom.projects.mall.enums.CustomerGender;
import com.atom.projects.mall.security.AtomAuthenticationToken;
import org.springframework.stereotype.Component;

import static org.springframework.security.core.authority.AuthorityUtils.commaSeparatedStringToAuthorityList;

/**
 * @author sunhao
 * @date create in 2019-12-05 14:15:08
 */
@Component
public class AuthenticationFactory {

    private final AtomAuthenticationToken admin;
    private final AtomAuthenticationToken employeeAdmin;
    private final AtomAuthenticationToken employeeCommon;
    private final AtomAuthenticationToken customer;

    public AuthenticationFactory() {

        Admin admin = new Admin();
        admin.setId(1L);
        admin.setUsername("admin");
        admin.setPassword("password");
        this.admin = new AtomAuthenticationToken(admin, null, commaSeparatedStringToAuthorityList("admin"));

        MiniProCustomer customer = new MiniProCustomer();
        customer.setId(2L);
        customer.setMerchantId(1L);
        customer.setOpenId("openId");
        customer.setAvatarUrl("merchantId");
        customer.setNickname("nickname");
        customer.setPhoneNumber("13888888888");
        customer.setCountry("china");
        customer.setProvince("yunnan");
        customer.setCity("kunming");
        customer.setGender(CustomerGender.MALE);
        this.customer = new AtomAuthenticationToken(customer, null, commaSeparatedStringToAuthorityList("customer"));
    }

    public AtomAuthenticationToken getAuthentication(String username) {

        if ("admin".equals(username)) {
            return this.admin;
        }

        if ("customer".equals(username)) {
            return this.customer;
        }

        throw new RuntimeException("用户不存在");
    }
}

可以看到, 我在构造方法里面实例化了我系统里面的两种用户, getAuthentication这个方法根据传入的用户名返回相应的Authentication; component单例注入;

写到这里其实我们的token就准备好了, 来尝试一下咯

public class AuthControllerTest extends MallApplicationTests {

    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private TokenFactory tokenFactory;

    @Test
    public void testEmployeeAuth() throws Exception {

        //获取token
        RequestPostProcessor token = tokenFactory.token("admin");
        
        MvcResult mvcResult = mockMvc.perform(
            MockMvcRequestBuilders.get("/auth/employee").with(token) // 设置token
        ).andReturn();
        
        int status = mvcResult.getResponse().getStatus();
        System.out.println("status = " + status);
        String contentAsString = mvcResult.getResponse().getContentAsString();
        System.out.println("contentAsString = " + contentAsString);
    }
    
}

有一些注解写在了MallApplicationTests里面, 这样所有的测试类只要继承就可以使用了, 不用每次都写;

@RunWith(SpringRunner.class)
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@AutoConfigureMockMvc
public class MallApplicationTests {

    @Test
    public void contextLoads() {

    }

}

OK了, 这样就可以不用手动获取token测接口了;

总结

这篇文章主要是讲了一种思路, 具体代码里面的实现可以有其他不同的方式, 这段时间在学习使用mockmvc我就用了这种方式; 一起折腾吧;

上一篇下一篇

猜你喜欢

热点阅读