前言
之前在项目中一直都是手动测试接口, 流程一般是手动获取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我就用了这种方式; 一起折腾吧;
网友评论