spring security oauth2如何進(jìn)行接口單元測(cè)試

前言

之前在項(xiàng)目中一直都是手動(dòng)測(cè)試接口, 流程一般是手動(dòng)獲取token之后添加到header里(或者是使用工具的環(huán)境變量等等), 然后測(cè)試; 使用junit直接測(cè)試方法好用, 但也有它的局限性, 像一些參數(shù)的校驗(yàn)(如果你的參數(shù)校驗(yàn)是使用javax的validation)和spring security用戶的獲取就無(wú)法測(cè)試; 因?yàn)槲以趘2ex上的一篇帖子, 我開(kāi)始琢磨一些單測(cè)的東西, 一開(kāi)始就發(fā)現(xiàn)有一個(gè)阻礙, 每次測(cè)試接口都需要自己手動(dòng)獲取token的話就沒(méi)辦法將測(cè)試自動(dòng)化; 在網(wǎng)上找了一下相關(guān)的資料發(fā)現(xiàn)對(duì)于spring security oauth2的測(cè)試資料不太多, 但是還是找到了Stack Overflow上的一個(gè)答案, 基于這位前輩在15年的分享, 有了這篇文章;

其實(shí)這里大家也可能會(huì)說(shuō), 可以使用腳本或者工具自動(dòng)的使用賬號(hào)獲取token, 然后自動(dòng)化測(cè)試啊; 但是這個(gè)方式在我的項(xiàng)目中行不通, 因?yàn)槟壳拔业倪@個(gè)系統(tǒng)中有一種用戶是微信小程序用戶, 登錄方式為小程序用戶使用code到后端換取token, 換取token的過(guò)程中我會(huì)使用用戶的code從微信那里獲取openid; 所以這部分用戶我沒(méi)辦法使用腳本來(lái)獲取token;

思路

首先我最先想到的是有沒(méi)有一個(gè)測(cè)試框架對(duì)spring security oauth2做了適配, 然后我就可以直接使用, 那就美滋滋了; 但是我找了一天, 未果, 如果大家知道的話請(qǐng)不吝賜教;

那既然沒(méi)有現(xiàn)成的測(cè)試框架可以使用的話我們就得自己搞點(diǎn)事情了; 首先我們上面說(shuō)到問(wèn)題的根源其實(shí)就是在獲取token的過(guò)程需要我們手動(dòng)來(lái)獲取, 那我們就這個(gè)問(wèn)題往下探討; 如果我們可以自動(dòng)獲取token就問(wèn)題就解決了, 可我上面說(shuō)過(guò), 這個(gè)token我沒(méi)辦法通過(guò)腳本來(lái)獲取, 怎么辦呢? 那就直接從spring security入手, 我們?cè)谡{(diào)用接口之前先往spring security的context里面直接放入用戶(沒(méi)有經(jīng)過(guò)系統(tǒng)里面的用戶校驗(yàn)邏輯哦); 然后用該用戶的token調(diào)用接口就可以了嘛; 話不多說(shuō), 動(dòng)手;

show me some code

我們使用mockmvc測(cè)試接口(但其實(shí)思路有了之后用什么client都可以), 先寫(xiě)一下如何往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是需要我們自己實(shí)現(xiàn)的, 其他的流程是spring security oauth2獲取token的方式; 接著來(lái)貼一下AuthenticationFactory的實(shí)現(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)造方法里面實(shí)例化了我系統(tǒng)里面的兩種用戶, getAuthentication這個(gè)方法根據(jù)傳入的用戶名返回相應(yīng)的Authentication; component單例注入;

寫(xiě)到這里其實(shí)我們的token就準(zhǔn)備好了, 來(lái)嘗試一下咯

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);
    }
    
}

有一些注解寫(xiě)在了MallApplicationTests里面, 這樣所有的測(cè)試類(lèi)只要繼承就可以使用了, 不用每次都寫(xiě);

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

    @Test
    public void contextLoads() {

    }

}

OK了, 這樣就可以不用手動(dòng)獲取token測(cè)接口了;

總結(jié)

這篇文章主要是講了一種思路, 具體代碼里面的實(shí)現(xiàn)可以有其他不同的方式, 這段時(shí)間在學(xué)習(xí)使用mockmvc我就用了這種方式; 一起折騰吧;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容