前言
之前在項目中一直都是手動測試接口, 流程一般是手動獲取token之后添加到header里(或者是使用工具的環(huán)境變量等等), 然后測試; 使用junit直接測試方法好用, 但也有它的局限性, 像一些參數(shù)的校驗(如果你的參數(shù)校驗是使用javax的validation)和spring security用戶的獲取就無法測試; 因為我在v2ex上的一篇帖子, 我開始琢磨一些單測的東西, 一開始就發(fā)現(xiàn)有一個阻礙, 每次測試接口都需要自己手動獲取token的話就沒辦法將測試自動化; 在網(wǎng)上找了一下相關(guān)的資料發(fā)現(xiàn)對于spring security oauth2的測試資料不太多, 但是還是找到了Stack Overflow上的一個答案, 基于這位前輩在15年的分享, 有了這篇文章;
其實這里大家也可能會說, 可以使用腳本或者工具自動的使用賬號獲取token, 然后自動化測試啊; 但是這個方式在我的項目中行不通, 因為目前我的這個系統(tǒng)中有一種用戶是微信小程序用戶, 登錄方式為小程序用戶使用code到后端換取token, 換取token的過程中我會使用用戶的code從微信那里獲取openid; 所以這部分用戶我沒辦法使用腳本來獲取token;
思路
首先我最先想到的是有沒有一個測試框架對spring security oauth2做了適配, 然后我就可以直接使用, 那就美滋滋了; 但是我找了一天, 未果, 如果大家知道的話請不吝賜教;
那既然沒有現(xiàn)成的測試框架可以使用的話我們就得自己搞點事情了; 首先我們上面說到問題的根源其實就是在獲取token的過程需要我們手動來獲取, 那我們就這個問題往下探討; 如果我們可以自動獲取token就問題就解決了, 可我上面說過, 這個token我沒辦法通過腳本來獲取, 怎么辦呢? 那就直接從spring security入手, 我們在調(diào)用接口之前先往spring security的context里面直接放入用戶(沒有經(jīng)過系統(tǒng)里面的用戶校驗邏輯哦); 然后用該用戶的token調(diào)用接口就可以了嘛; 話不多說, 動手;
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);
// 關(guān)鍵是這里, 將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
是需要我們自己實現(xiàn)的, 其他的流程是spring security oauth2獲取token的方式; 接著來貼一下AuthenticationFactory
的實現(xiàn)方式;
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("用戶不存在");
}
}
可以看到, 我在構(gòu)造方法里面實例化了我系統(tǒng)里面的兩種用戶, getAuthentication
這個方法根據(jù)傳入的用戶名返回相應(yīng)的Authentication
; component單例注入;
寫到這里其實我們的token就準(zhǔn)備好了, 來嘗試一下咯
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) // 設(shè)置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測接口了;
總結(jié)
這篇文章主要是講了一種思路, 具體代碼里面的實現(xiàn)可以有其他不同的方式, 這段時間在學(xué)習(xí)使用mockmvc我就用了這種方式; 一起折騰吧;