FunTester 单点登录性能测试方案

FunTester · 2020年03月31日 · 1210 次阅读

项目登录系统升级,改为单点登录:英文全称 Single Sign On。SSO 是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
之前有的统一登录方式被废弃,由于单点登录比较之前的登录系统复杂很多。之前的方案请求一个接口即可获得用户校验令牌。
先分享一下单点登录的技术方案的时序图:

单点登录时序图

然后发一下我梳理的前端调用接口的时序图:

单点登录时序图

性能测试分成了两个场景:
性能压测场景分析:
跳过不必要的 302 响应状态请求,只测试业务逻辑相关接口,不处理页面相关接口(资源文件等),登录完成请求额外接口完成登录验证。

  • 场景一:单个用户登录单个系统。
    第一步:请求 cas 服务 login 页面,解析页面获取秘钥串(lt/execution)
    第二步:请求 cas 服务登录接口,获取 TGC 令牌和 ST 令牌
    第三步:请求 svr 服务校验 ST 令牌,获取 admin_jsessionid 信息
    第四步:请求额外接口完成登录状态验证

  • 场景二:单个用户登录两个系统
    第一步:请求 cas 服务 login 页面,解析页面获取秘钥串(lt/execution)
    第二步:请求 cas 服务登录接口,获取 TGC 令牌和 ST1 令牌
    第三步:请求 svr1 服务校验 ST1 令牌,获取 admin_jsessionid 信息
    第四步:请求额外接口完成登 svr1 录状态验证
    第五步:请求 cas 服务登录接口(携带 TGC 令牌),获取 svr2 对应的 ST2 令牌
    第六步:请求 svr2 服务校验校验 ST2 令牌,获取 admin_jsessionid 信息
    第七步:请求额外接口完成 svr2 登录状态校验

针对这两个场景,测试脚本如下:

import com.fun.base.constaint.ThreadBase
import com.fun.config.SqlConstant
import com.fun.frame.excute.Concurrent
import com.fun.utils.Time
import com.okayqa.teacherweb.base.OkayBase
import org.slf4j.Logger
import org.slf4j.LoggerFactory

class Tss extends OkayBase {
    private static Logger logger = LoggerFactory.getLogger(Tss.class)


    public static void main(String[] args) {
        def threadNum = changeStringToInt(args[0])
        def times = changeStringToInt(args[1])
        SqlConstant.flag = false


//        def threadNum = 3
//        def times = 2
        def arrayList = new ArrayList<ThreadBase>()
        for (int i = 0; i < threadNum; i++) {
            def thread = new ThreadBase<Integer>(new Integer(i)) {
                @Override
                protected void before() {

                }

                @Override
                protected void doing() throws Exception {
                    def mark = Time.getTimeStamp()
                    def base = getBase(changeStringToInt(getT()))
//                    def cookies = base.getCookies()
//                    def base1 = new com.okayqa.publicweb.base.OkayBase() //创建public-web项目的用户对象
//                    base1.init(cookies)//初始化用户对象
//                    def common = new SchoolCommon(base1)//创建学校公共接口请求对象
//                    def years = common.getYears()//请求学校学年接口
                    def mark0 = Time.getTimeStamp()
                    def i1 = mark0 - mark
                    logger.error("----------------" + i1 + EMPTY)
                }

                @Override
                protected void after() {

                }
            }

            thread.setTimes(times)
            arrayList << thread
        }
        new Concurrent(arrayList).start()
//
        allOver()

    }
}

首先各个项目用户对象代码如下:

package com.okayqa.teacherweb.base;

import com.fun.base.bean.BeanUtil;
import com.fun.base.bean.RequestInfo;
import com.fun.base.interfaces.IBase;
import com.fun.config.HttpClientConstant;
import com.fun.config.SqlConstant;
import com.fun.config.SysInit;
import com.fun.frame.SourceCode;
import com.fun.frame.httpclient.FanLibrary;
import com.okayqa.common.CasCredential;
import com.okayqa.common.Common;
import com.okayqa.common.Users;
import com.okayqa.teacherweb.bean.UserInfoBean;
import com.okayqa.teacherweb.function.UserCenter;
import com.okayqa.teacherweb.profile.Profile;
import com.okayqa.teacherweb.profile.UserApi;
import net.sf.json.JSONObject;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;

/**
 * 教师空间项目
 * qa项目base
 */
public class OkayBase extends SourceCode implements IBase {

    private static Logger logger = LoggerFactory.getLogger(OkayBase.class);

    private static OkayBase base;

    static {
        SqlConstant.REQUEST_TABLE = Common.SQL_REQUEST;
        SqlConstant.flag = Common.SQL_KEY;
        SqlConstant.PERFORMANCE_TABLE = Common.SQL_PERFORMANCE;
        if (FanLibrary.getiBase() == null) FanLibrary.setiBase(new OkayBase());
    }

    public final static String HOST = Profile.HOST;


    /**
     * 登录响应
     */
    JSONObject loginResponse;

    private UserInfoBean userInfoBean = new UserInfoBean();

    /**
     * 获取对象方法
     * <p>
     * 暂未进行用户管理,同意使用单例
     * </p>
     *
     * @return
     */
    public static OkayBase getBase() {
        if (base == null) base = new OkayBase(0);
        return base;
    }

    public static OkayBase getBase(int i) {
        return new OkayBase(i);
    }

    public static OkayBase getBase(String name) {
        return new OkayBase(name);
    }

    long uid;

    String token;

    String username;

    public JSONObject getCookies() {
        return cookies;
    }

    public void setCookies(JSONObject cookies) {
        this.cookies = cookies;
    }

    public void addCookie(JSONObject cookies) {
        this.cookies.putAll(cookies);
    }

    JSONObject cookies = new JSONObject();


    @Override
    public void login() {
//        /**
//         * 单点登录方式
        String url = UserApi.LOGIN;
        JSONObject params = new JSONObject();
        params.put("loginType", "1");
        params.put("platformType", "teacher");
        params.put("username", username);
        params.put("password", getPassword());
        params.put("pictureVerifyCode", "");
        params.put("phone", "");
        params.put("traceno", "");
        params.put("phoneVerifyCode", "");
        JSONObject tgc = CasCredential.getTGC(HOST, params);
        this.cookies = tgc.getJSONObject("cookie");
        String location = tgc.containsKey("location") ? tgc.getString("location") : EMPTY;
        if (!location.contains("ticket=ST-")) logger.error("登录失败!");
        JSONObject getResponse = this.getGetResponse(location.replace(HOST, EMPTY));
        UserCenter userCenter = new UserCenter(this.cookies);
        userInfoBean = userCenter.getUserinfo();
        logger.info("账号:{},昵称:{},学科名称:{},登录成功!", username,userInfoBean.getName(),userInfoBean.getSubjectName());
    }

    /**
     * 获取到明文的默认密码
     *
     * @return
     */
    public String getPassword() {
        return Profile.PWD;
    }

    public OkayBase(String username) {
        this.username = username;
        login();
    }

    public OkayBase(int i) {
        this.username = Users.getTeaUser(i);
        login();
    }

    protected OkayBase() {
    }

    public OkayBase(OkayBase okayBase) {
        this.uid = okayBase.uid;
        this.username = okayBase.username;
        this.token = okayBase.token;
        this.userInfoBean = okayBase.userInfoBean;
        this.cookies = okayBase.cookies;
    }

    public JSONObject getParams() {
        return getJson("_=" + getMark());
    }


    @Override
    public void init(JSONObject jsonObject) {
        addCookie(jsonObject);
        HttpGet get = FanLibrary.getHttpGet(Profile.LOGIN_REDIRECT);
        get.addHeader(FanLibrary.getCookies(jsonObject));
        JSONObject response = FanLibrary.getHttpResponse(get);
        JSONObject credential = CasCredential.verifyST(response.getString("location"));
        addCookie(credential);
    }

    public JSONObject getLoginResponse() {
        return loginResponse;
    }

    public long getUid() {
        return uid;
    }

    public String getToken() {
        return token;
    }

    public String getUname() {
        return username;
    }

    public UserInfoBean getUserInfoBean() {
        return userInfoBean;
    }

    @Override
    public HttpGet getGet(String s) {
        return FanLibrary.getHttpGet(HOST + s);
    }

    @Override
    public HttpGet getGet(String s, JSONObject jsonObject) {
        return FanLibrary.getHttpGet(HOST + s, jsonObject);
    }

    @Override
    public HttpPost getPost(String s) {
        return FanLibrary.getHttpPost(HOST + s);
    }

    @Override
    public HttpPost getPost(String s, JSONObject jsonObject) {
        return FanLibrary.getHttpPost(HOST + s, jsonObject);
    }

    @Override
    public HttpPost getPost(String s, JSONObject jsonObject, File file) {
        return FanLibrary.getHttpPost(HOST + s, jsonObject, file);
    }

    @Override
    public JSONObject getResponse(HttpRequestBase httpRequestBase) {
        setHeaders(httpRequestBase);
        JSONObject response = FanLibrary.getHttpResponse(httpRequestBase);
        handleResponseHeader(response);
        return response;
    }

    @Override
    public void setHeaders(HttpRequestBase httpRequestBase) {
        httpRequestBase.addHeader(Common.REQUEST_ID);
        this.addCookie(getJson("user_phone_check_" + this.username + "=true"));
        if (!cookies.isEmpty()) httpRequestBase.addHeader(FanLibrary.getCookies(cookies));
    }

    @Override
    public void handleResponseHeader(JSONObject response) {
        if (!response.containsKey(HttpClientConstant.COOKIE)) return;
        cookies.putAll(response.getJSONObject(HttpClientConstant.COOKIE));
        response.remove(HttpClientConstant.COOKIE);
    }

    @Override
    public JSONObject getGetResponse(String s) {
        return getResponse(getGet(s));
    }

    @Override
    public JSONObject getGetResponse(String s, JSONObject jsonObject) {
        return getResponse(getGet(s, jsonObject));
    }

    @Override
    public JSONObject getPostResponse(String s) {
        return getResponse(getPost(s));
    }

    @Override
    public JSONObject getPostResponse(String s, JSONObject jsonObject) {
        return getResponse(getPost(s, jsonObject));
    }

    @Override
    public JSONObject getPostResponse(String s, JSONObject jsonObject, File file) {
        return getResponse(getPost(s, jsonObject, file));
    }

    @Override
    public boolean isRight(JSONObject jsonObject) {
        if (jsonObject.containsKey("success")) return jsonObject.getBoolean("success");
        int code = checkCode(jsonObject, new RequestInfo(getGet(HOST)));
        try {
            JSONObject data = jsonObject.getJSONObject("data");
            return code == 0 && !data.isEmpty();
        } catch (Exception e) {
            output(jsonObject);
            return false;
        }

    }

    /**
     * 获取并检查code
     *
     * @param jsonObject
     * @return
     */
    public int checkCode(JSONObject jsonObject, RequestInfo requestInfo) {
        int code = TEST_ERROR_CODE;
        if (SysInit.isBlack(requestInfo.getHost())) return code;
        try {
            code = jsonObject.getInt("code");
        } catch (Exception e) {
            logger.warn("非标准响应:{}", jsonObject.toString());
        }
        return code;
    }

    /**
     * 测试结束,资源释放
     */
    public static void allOver() {
        FanLibrary.testOver();
    }

}

统一验证类的代码如下:

package com.okayqa.common

import com.fun.config.HttpClientConstant
import com.fun.frame.httpclient.FanLibrary
import com.fun.utils.Regex
import net.sf.json.JSONObject
import org.apache.http.client.methods.HttpGet
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * cas服务验证类,主要解决web端登录验证功能
 */
class CasCredential extends FanLibrary {
    static final String OR="/"
    private static Logger logger = LoggerFactory.getLogger(CasCredential.class)
    /**
     * 校验值,随机一次性,从login返回页面中获取
     */
    String lt
    /**
     * 校验值,随机一次性,从login返回页面中获取,正常值长度在4000+,低于4000请检查请求连接是否传入了回调服务的地址
     */
    String execution

/**
 * cas服务的login页面获取到令牌对,此处正则暂时可用,二期会修改表单提交
 */
    CasCredential(String host) {
        def get = getHttpGet(Common.CAS_LOGIN + (host.endsWith(OR) ? host : host + OR))
        get.addHeader(Common.REQUEST_ID)
        def response = getHttpResponse(get)
        def string = response.getString("content")
        this.lt = Regex.getRegex(string, "<input type=\"hidden\" name=\"lt\" value=\".*?\" />")
        this.execution = Regex.getRegex(string, " <input type=\"hidden\" name=\"execution\" value=\".*?\" />")
//        logger.info("cas服务登录host:{},lt:{},execution:{}", host, lt, execution)
    }

/**
 * 各个服务端参数一致,由各个服务自己把参数拼好之后传过来,之后在去cas服务拿到令牌对
 * @param host 服务的host地址,回调由各个服务自己完成,二次验证也是,此处的host不做兼容,有cascredential做处理
 * @param params 拼好的参数
 * @return
 */
    static JSONObject getTGC(String host, JSONObject params) {
        def credential = new CasCredential(host)
        params.put("lt", credential.getLt());
        params.put("execution", credential.getExecution())
        params.put("_eventId", "submit");
        def post = FanLibrary.getHttpPost(Common.CAS_LOGIN + (host.endsWith(OR) ? host : host + OR), params)
        post.addHeader(Common.REQUEST_ID);
        FanLibrary.getHttpResponse(post)
    }

/**
 * 通过用户
 * @param url
 * @return
 */
    public static JSONObject verifyST(String url) {
        HttpGet location = FanLibrary.getHttpGet(url);
        location.addHeader(Common.REQUEST_ID);
        JSONObject httpResponse = FanLibrary.getHttpResponse(location);
        httpResponse.getJSONObject(HttpClientConstant.COOKIE) as JSONObject
    }
}

然后顺利完工。因为之前性能测试方案都是使用 jmeter 作为解决方案,这次架构变更的测试用例难以实现,故才用了脚本。性能框架才用了之前发过的性能测试框架有兴趣的可以点击查看一下,语言以 Java 为主,脚本使用 Groovy 写的。

技术类文章精选

非技术文章精选

大咖风采

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册