单点登录中间件

本章将要和豪门大快朵颐的是3个单点登录中间件,中间件听起来高深其实那里只是吧单点登录要用到的逻辑和处理流程封装成了几个点子而已,默许帮忙使用redis服务保存session的主意,也得以行使参数Func<>主意来做自定义session存款和储蓄操作的不二诀要,就不用笔者暗许提供的redis存储的法子了;要说本章内容的来源于,其实是自笔者在原先的ShenNiu.MVC管制种类中进入了如今做的检察问卷模块,那么些问卷考察和ShenNiu.MVC不是三个站点,可是作者的问卷调查系统可定在珍贵问卷或题材的时候要求登录人的音讯,小编又不想再单独弄壹套账号方面包车型地铁程序了,所以就利用那种单点登录形式,以此来提供调查探讨问卷的所供给的用户音讯,以及为了尽早的前天祥和写的某部模块也急需管住用户新闻的话,就能省略掉用户模块了,不得不说单点登录在此时宣布的功力之大;本章内容希望大家能够喜欢,也可望各位多多”扫码协理”和”推荐”多谢!如若你想要和大家交换越来越多mvc相关音讯方可来Ninesky框架作者:洞庭夕照
内定的法定群 4283十5六叁调换;

 

» 单点登录验证手画示例图

» ShenNiuApi.SDK封装中间件代码

» 侦察问卷系统使用中间件示例

» 推广调查问卷系统

 

下边一步一个脚印的来享受:

» 单点登录验证手画示例图

先是,咋们要做一个简短的单点登录作用,供给精通其执行的流程和平运动行的规律,那里将有板有眼重点建议本人觉得根本的地点,先上1幅手工业图:

bifa365必发 1

看起来图画的不是很为难,不过本身想表明的意味感觉依然表明清楚了;作为2个单点登录验证模块,最要害的流水生产线有:

一.
未登录时:提供联合登录入口=》去数据库验证账号正确性=》存款和储蓄会话session(那里运用redis存款和储蓄token和用户登陆消息,利用其数额过期策略充当session会电话机制)=》重定向到redirectUrl内定的地点

二.
已登录时:获取站点的cookie存款和储蓄的sessionId(token)=》调用验证token有效接口=》那里有二种境况(a,b)

    a)
有效token=》获取登录用户的session存款和储蓄的音信(redis存款和储蓄的value音信)

    b) 无效token=》重回无效新闻,构造登录入口地址

通过上边分析,大概的流水生产线应该很显眼了上面大家就来看包装的代码;

 

» ShenNiuApi.SDK封装中间件代码

此地要看的是中间件的一个办法:SsoMiddleWareServer(登录入口操作),SsoMiddleWareClient(Token验证及获得登录新闻),SsoMiddleWareLoginOut(注销操作);那里自个儿已经把艺术打包放到了nuget上: Install-Package
ShenNiuApi.SDK ,只须要下载最新的sdk,就能自在帮你达成二个单点登录架构,上边来看具体的代码;

SsoMiddleWareServer(登录入口操作):

 1         /// <summary>
 2         /// 单点登录操作 SSOMiddleWare服务端(方法功能:
 3         /// 1.生成sessionId 
 4         /// 2.存储session到redis(60分钟失效)或者自定义sessionStoreFunc方法中 
 5         /// 3.构造带有token的重定向地址)
 6         /// 注:默认采用redis保存session,因此需要在conf中配置ReadAndWritePorts和OnlyReadPorts两个appSettings节点:
 7         /// ReadAndWritePorts在conf中配置格式如:pwd@ip:port,多个使用‘|’隔开       实例:shenniubuxing3@127.0.0.1:6377
 8         /// OnlyReadPorts在conf中配置格式如:pwd@ip:port,多个使用‘|’隔开              实例:shenniubuxing3@127.0.0.1:6377
 9         /// </summary>
10         /// <typeparam name="TUserBaseInfo">存储登录信息的对象</typeparam>
11         /// <param name="userBaseInfo">登录信息</param>
12         /// <param name="redirectUrl">重定向地址(注:格式应为http://或者https://;并经过UrlEncode转码后的地址;如果是同站点下面的话无需http://标记)</param>
13         /// <param name="token">执行方法无误后ref返回唯一的token(注:token生成规则是唯一的tokenKey+guid+时间戳)</param>
14         /// <param name="tokenKey">生成token的Key(默认:666666)</param>
15         /// <param name="sessionStoreFun">自定义session存储方法(提供自定义操作保存session的方法,覆盖默认的reids存储方式)</param>
16         /// <param name="timeOut">60(分钟)</param>
17         /// <returns>追加有token的重定向地址</returns>
18         public string SsoMiddleWareServer<TUserBaseInfo>(TUserBaseInfo userBaseInfo, string redirectUrl, ref string token, string tokenKey = "666666", Func<TUserBaseInfo, bool> sessionStoreFun = null, int timeOut = 60)
19             where TUserBaseInfo : class,new()
20         {
21             var returnUrl = string.Empty;
22             try
23             {
24                 //非空验证  
25                 if (string.IsNullOrWhiteSpace(redirectUrl) || userBaseInfo == null) { return returnUrl; }
26 
27                 //生成Token
28                 token = Md5Extend.GetSidMd5Hash(tokenKey);
29 
30                 // ShenNiuApi默认的Redis存储session
31                 if (sessionStoreFun == null && userBaseInfo != null)
32                 {
33                     if (!CacheRepository.Current(CacheType.RedisCache).SetCache<TUserBaseInfo>(token, userBaseInfo, timeOut, true)) { return returnUrl; }
34                 }
35                 else { if (!sessionStoreFun(userBaseInfo)) { return returnUrl; } }
36 
37                 //通域名站内系统登录
38                 if (!Uri.IsWellFormedUriString(redirectUrl, UriKind.Absolute))
39                 {
40                     returnUrl = redirectUrl;
41                     return returnUrl;
42                 }
43 
44                 #region 解析并构造跳转链接
45                 redirectUrl = HttpUtility.UrlDecode(redirectUrl);
46                 redirectUrl = redirectUrl.TrimEnd('&');
47                 redirectUrl = Regex.Replace(redirectUrl, "(&)?token=[^&]+(&)?", "");
48                 Uri uri = new Uri(redirectUrl);
49                 var queryStr = uri.Query;
50                 redirectUrl += queryStr.Contains('?') ? "" : "?";
51                 redirectUrl += string.IsNullOrWhiteSpace(queryStr.TrimStart('?')) ? "" : "&";
52                 returnUrl = string.Format("{0}token={1}", redirectUrl, token);
53                 #endregion
54             }
55             catch (Exception ex)
56             {
57                 throw new Exception(ex.Message);
58             }
59             finally
60             {
61                 if (string.IsNullOrWhiteSpace(returnUrl)) { token = string.Empty; }
62             }
63             return returnUrl;
64         }

SsoMiddleWareClient(Token验证及取得登录新闻):

 1   /// <summary>
 2         /// 单点登录操作 SSOMiddleWare客户端(方法功能:
 3         /// 1.验证客户端是否有sid或者url地址中带有最新的token 
 4         /// 2.获取服务端session的基本信息(注:默认直接读取服务端的redis库,同server方法一样需要配置对应的账号节点ReadAndWritePorts和OnlyReadPorts)
 5         /// 3.重新设置客户端cookie有效期和服务端存储session的有效期)
 6         /// </summary>
 7         /// <typeparam name="TUserBaseInfo">登陆用户信息对象</typeparam>
 8         /// <param name="httpContext">上下文HttpContext</param>
 9         /// <param name="ssoLoginUrl">sso统一登陆入口地址</param>
10         /// <param name="redirectUrl">待重定向的地址</param>
11         /// <param name="userBaseInfo">获取的登陆用户信息</param>
12         /// <param name="token">唯一token(即:sid)</param>
13         /// <param name="getOrsetSessionFun">自定义获取服务端用户信息方法并且同时要满足重新设置新的session有效时间</param>
14         /// <param name="sidName">cookie保存的sid名称</param>
15         /// <param name="timeOut">过期时间</param>
16         /// <returns></returns>
17         public string SsoMiddleWareClient<TUserBaseInfo>(HttpContext httpContext, string ssoLoginUrl, string redirectUrl, ref TUserBaseInfo userBaseInfo, ref string token, Func<string, int, TUserBaseInfo> getAndsetSessionFun = null, string sidName = "sid", int timeOut = 60)
18                where TUserBaseInfo : class,new()
19         {
20             var returnUrl = string.Empty;
21             try
22             {
23                 userBaseInfo = default(TUserBaseInfo);
24                 token = string.Empty;
25                 if (string.IsNullOrWhiteSpace(ssoLoginUrl) || string.IsNullOrWhiteSpace(redirectUrl) || string.IsNullOrWhiteSpace(sidName)) { return returnUrl; }
26 
27                 //设置过期后验证url串 
28                 returnUrl = string.Format("{0}?returnUrl={1}", ssoLoginUrl, HttpUtility.UrlEncode(redirectUrl));
29 
30                 //获取token
31                 var cookie = httpContext.Request.Cookies.Get(sidName);
32                 token = httpContext.Request.Params["token"];
33                 token = string.IsNullOrWhiteSpace(token) ? (cookie == null ? "" : cookie.Value) : token;
34                 if (string.IsNullOrWhiteSpace(token)) { return returnUrl; }
35 
36                 //获取用户基本信息
37                 if (getAndsetSessionFun != null)
38                 {
39                     userBaseInfo = getAndsetSessionFun(token, timeOut);
40                 }
41                 else
42                 {
43                     userBaseInfo = CacheRepository.Current(CacheType.RedisCache).GetCache<TUserBaseInfo>(token, true);
44                 }
45                 if (userBaseInfo == null)
46                 {
47                     //过期cookie,清空
48                     if (cookie != null)
49                     {
50                         cookie.Expires = DateTime.Now.AddDays(-1);
51                         httpContext.Response.SetCookie(cookie);
52                     }
53                     return returnUrl;
54                 }
55 
56                 //cookie被清除,需要重新设置
57                 if (cookie == null)
58                 {
59                     cookie = new HttpCookie(sidName, token);
60                     cookie.Expires = DateTime.Now.AddMinutes(timeOut);
61                     httpContext.Response.AppendCookie(cookie);
62                 }
63                 else
64                 {
65                     //登陆验证都成功后,需要重新设置cookie中的toke失效时间
66                     cookie.Value = token;
67                     cookie.Expires = DateTime.Now.AddMinutes(timeOut);
68                     httpContext.Response.SetCookie(cookie);
69                 }
70 
71                 //设置服务端session的失效时间
72                 if (getAndsetSessionFun == null)
73                 {
74                     CacheRepository.Current(CacheType.RedisCache).AddExpire(token, timeOut);
75                 }
76                 returnUrl = string.Empty;
77             }
78             catch (Exception ex)
79             {
80                 throw new Exception(ex.Message);
81             }
82             finally { if (!string.IsNullOrWhiteSpace(returnUrl)) { token = string.Empty; } }
83             return returnUrl;
84         }

SsoMiddleWareLoginOut(注销操作):

 1  /// <summary>
 2         /// 单点登录操作 SSOMiddleWare 退出登陆
 3         /// </summary>
 4         /// <param name="httpContext">Http向下文</param>
 5         /// <param name="removeSession">自定义移除方法</param>
 6         /// <param name="sidName">cookie保存的sid名称</param>
 7         /// <returns>true或false</returns>
 8         public bool SsoMiddleWareLoginOut(HttpContext httpContext, Func<string, bool> removeSession = null, string sidName = "sid")
 9         {
10             var isfalse = true;
11             try
12             {
13                 if (string.IsNullOrWhiteSpace(sidName)) { sidName = "sid"; }
14 
15                 //获取cookie中的token
16                 var cookie = httpContext.Request.Cookies.Get(sidName);
17                 if (cookie == null) { return isfalse; }
18 
19                 //设置过期cookie(先过期cookie)
20                 var key = cookie.Value;
21                 cookie.Expires = DateTime.Now.AddDays(-1);
22                 httpContext.Response.SetCookie(cookie);
23 
24                 //移除session
25                 if (removeSession != null)
26                 {
27                     isfalse = removeSession(key);
28                 }
29                 else
30                 {
31                     isfalse = CacheRepository.Current(CacheType.RedisCache).Remove(key);
32                 }
33             }
34             catch (Exception ex)
35             {
36 
37                 throw new Exception(ex.Message);
38             }
39             return isfalse;
40         }

各种方法的参数及成效,每行逻辑代码的都有注释,各位不要紧研读下;这里要说的是种种方法都暗中认可有操作redis存款和储蓄session的步子,因而能够见到在那之中间件暗中认可使用的是redis服务存款和储蓄session;

有人会问何故会这么做,您单点登录难道最底部用的不是接口来操作登录或表明的吗?那里思量有如此三个实用场景,作为一个人中型小型型公司的职工来说,接触到服务器平常计划了方方面面集团的站点比如:站点一,站点二…就算域名不同只是都在相同台服务器上,再试想下1旦用redis来囤积session会话,此刻是还是不是就能认为作者这台服务器就具备直接访问redis的读写权限(当然即便redis服务也在那台服务器上的话就更不要说了),那作者直接在中间件中放置公共操作redis获取session,存款和储蓄session等操作是还是不是都没难点,如此那般那我们还亟需独自弄二个session(token)验证的api么,没供给的事情(对于单点登录站点和重定向站点而言没要求),由此小编就像此干了,嵌入三个默许的redis操作哈哈(不服能够来辨);固然如此不得不思量越多的作业场景,万一登录账单和其余站点不在三个服务器(只怕说非常的小概直接待上访问redis呢),那里在2其中间件方法参数中提供了3个Func<>参数,每种方法的Func<>代表额意思有点距离,各位能够看投注释;有了那个自定义Func,中间件就能鉴定识别尽管客户端有传递此格局,那么以Func为主,未有就应用默许的格局操作redis,这样允许使用者自定义方法增加了使用者本人觉得调用token验证的api只怕此外合理的法子,那也准保了主意的通用性。

 

» 调查问卷系统使用中间件示例

上边小编将使用真实的实例来利用ShenNiuApi.SDK中的中间件方法,这里例子是在本人调研问卷系统中什么运用;首先通过nuget下载 Install-Package
ShenNiuApi.SDK 最新的sdk,然后供给在做登录验证的Filter中要么接续Controller的父类中(笔者那边是后世)添加如下代码:

 1 public class BaseController : Controller
 2     {
 3 
 4         protected StageModel.MoUserData _userData;
 5 
 6         protected override void OnActionExecuting(ActionExecutingContext filterContext)
 7         {
 8 
 9             #region 采用ShenNiuApiClient的SsoClient中间件
10 
11             ShenNiuApi.SDK.ShenNiuApiClient client = new ShenNiuApi.SDK.ShenNiuApiClient();
12 
13             var ssoLogin="http://www.lovexins.com:8081/User/Login";
14             var redirectUrl = filterContext.HttpContext.Request.Url.AbsoluteUri;
15             var token = string.Empty;
16             var returnUrl = client.SsoMiddleWareClient<StageModel.MoUserData>(System.Web.HttpContext.Current, ssoLogin, redirectUrl, ref this._userData, ref token);
17             if (string.IsNullOrWhiteSpace(token) )
18             {
19                 filterContext.Result = new RedirectResult(returnUrl);
20                 return;
21             }
22             #endregion
23         }
24 
25         protected void ShowMsg(string msg)
26         {
27 
28             ModelState.AddModelError(string.Empty, msg);
29         }
30     }

只需求一句 client.SsoMiddleWareClient<StageModel.MoUserData>(System.Web.HttpContext.Current,
ssoLogin, redirectUrl, ref this._userData, ref
token); 即可完毕问卷系统单点登录的表明和得到登录用户的新闻,各个解析和设置sid的cookie音信都早已在中间件方法中实现了,是否天翻地覆减弱了您的编码量;为了相比上面小编直接贴出未有使用SsoMiddleWareClient方法时候的代码量:

bifa365必发 2bifa365必发 3

 1 protected override void OnActionExecuting(ActionExecutingContext filterContext)
 2         {
 3 
 4 
 5             var returnUrl = filterContext.HttpContext.Request.Url.AbsoluteUri;
 6             returnUrl = HttpUtility.UrlEncode(returnUrl);
 7             // var result = new RedirectResult(string.Format("http://www.lovexins.com:8081/User/Login?returnUrl={0}", returnUrl));
 8             var result = new RedirectResult(string.Format("http://172.16.9.6:4040/User/Login?returnUrl={0}", returnUrl));
 9             var key = "Sid";
10             var timeOut = 30;
11             try
12             {
13                 var cookie = filterContext.HttpContext.Request.Cookies.Get(key);
14                 var token = filterContext.HttpContext.Request.Params["token"];
15                 token = string.IsNullOrWhiteSpace(token) ? (cookie == null ? "" : cookie.Value) : token;
16                 if (string.IsNullOrWhiteSpace(token))
17                 {
18                     filterContext.Result = result;
19                     return;
20                 }
21 
22                 this._userData = CacheRepository.Current(CacheType.RedisCache).GetCache<StageModel.MoUserData>(token, true);
23                 if (this._userData == null && cookie != null)
24                 {
25                     //清空cookie
26                     cookie.Expires = DateTime.Now.AddDays(-1);
27                     filterContext.HttpContext.Response.SetCookie(cookie);
28                     filterContext.Result = result;
29                     return;
30                 }
31                 else if (this._userData == null)
32                 {
33                     filterContext.Result = result;
34                     return;
35                 }
36 
37                 if (cookie == null)
38                 {
39                     cookie = new HttpCookie(key, token);
40                     cookie.Expires = DateTime.Now.AddMinutes(timeOut);
41                     filterContext.HttpContext.Response.AppendCookie(cookie);
42                 }
43                 else
44                 {
45                     cookie.Value = token;
46                     //登陆验证都成功后,需要重新设置cookie中的toke失效时间
47                     cookie.Expires = DateTime.Now.AddMinutes(timeOut);
48                     filterContext.HttpContext.Response.SetCookie(cookie);
49                 }
50 
51                 //设置session失效时间
52                 CacheRepository.Current(CacheType.RedisCache).AddExpire(token, timeOut);
53             }
54             catch (Exception ex)
55             {
56                 filterContext.Result = result;
57                 return;
58             }
59         }

View Code

从代码量看前者不难多了,有人会说了您那不正是弄了七个办法而已嘛,说怎么代码量少了哈哈;那不得不说平时咋们哎使用第二方的插件也许类库,那样庞大收缩了咋们工作量和升级换代了费用速度的补益,有了ShenNiuApi.SDK您还供给担心什么吧;但是研商之中的具体步骤,逻辑代码作者嘶吼相当的赞成的;

有了在踏勘问卷的自定义Controller父类后,咋们还亟需有2个记名的地方,这里笔者新创立的品种Stage.Web,在其登录get请求的Action中加进了如下代码:

 1    #region 采用ShenNiuApiClient的SsoClient中间件
 2 
 3             ShenNiuApi.SDK.ShenNiuApiClient client = new ShenNiuApi.SDK.ShenNiuApiClient();
 4             var ssoLogin = loginUrl;
 5             var redirectUrl = context.Request.Path;
 6 
 7             var token = string.Empty;
 8             t = default(T);
 9             var returnUrl = client.SsoMiddleWareClient<T>(System.Web.HttpContext.Current, ssoLogin, redirectUrl, ref t, ref token, sidName: UserLoginExtend.CookieName);
10             if (string.IsNullOrWhiteSpace(token))
11             {
12                 return new RedirectResult(returnUrl);
13             }
14             return null;
15             #endregion

从来通过中间件提供的 SsoMiddleWareClient 方法赢得登录的token并证实是还是不是已经登6过,假如登录过了一贯通过 return new
RedirectResult(returnUrl); 重定向到returnUrl的地点中去;假设未有那么进入登录界面,录入账号音信后:

bifa365必发 4

交给登录,进入咋们post的Action中进过数据库对账号相称成功了,然后径直调用中间件方法来囤积session并提供唯一的token值,再展开重定向跳转:

 1  #region 采用ShenNiuApiClient的SsoServer中间件
 2 
 3                     ShenNiuApi.SDK.ShenNiuApiClient client = new ShenNiuApi.SDK.ShenNiuApiClient();
 4 
 5                     var timeOut = 60; //分钟
 6                     var token = string.Empty;
 7                     var redirectUrl = client.SsoMiddleWareServer<StageModel.MoUserData>(userData, returnUrl, ref token, timeOut: timeOut);
 8                     sbLog.AppendFormat("redirectUrl:{0},token:{1},", redirectUrl, token);
 9                     if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(redirectUrl))
10                     {
11                         //登陆失败
12                         sbLog.Append("登陆失败,");
13                     }
14                     else
15                     {
16                         //写入Sso统一登陆站点的sid到cookie
17                         var cookie = new HttpCookie(UserLoginExtend.CookieName, token);
18                         cookie.Expires = DateTime.Now.AddMinutes(timeOut);
19                         cookie.Domain = Request.Url.Host;
20                         HttpContext.Response.AppendCookie(cookie);
21                     }
22                     var isAddLog = await StageClass._WrigLogAsync(sbLog.ToString());
23                     return new RedirectResult(string.Format("{0}", redirectUrl));
24                     #endregion

到此出sso的代码基本做到了就那样简单,不过那里暗许使用的是本人嵌入的redis服务来存款和储蓄session新闻的,所以还供给配置三个redis相关账号密码等的节点,那里只需求您在 C:\Conf\ShenNiuApi.xml 磁盘上边扩大如下名称的xml文件,文件内容也大致:

 1 <ShenNiuApi>
 2     <RedisCache>
 3         <!--读写权限服务地址,多个使用'|'隔开(格式如:pwd@ip:port)-->
 4         <UserName>shenniubuxing3@111.111.111.152:1111</UserName>
 5         <!--只读权限服务地址,多个使用'|'隔开-->
 6         <UserPwd>shenniubuxing3@111.111.111.152:1111|shenniubuxing3@127.0.0.1:6377</UserPwd>
 7         <ApiUrl></ApiUrl>
 8         <ApiKey></ApiKey>
 9     </RedisCache>
10 </ShenNiuApi>

把内容之中的redis账号,密码,端口,地址改成您自身的就行了;因为是在C盘中所以你服务器的别的站点也能够访问,假若你暗中认可使用redis的措施存款和储蓄session,那么只供给遵从下边步骤就能高效的搭建2个单点登录架构;这里本身提供下考查问卷使用单点登录测试的地址:www.lovexins.com:1001/Subject
测试账号:shenniu00三密码:12312三,注意登录界面包车型客车域名和问卷考察的域名一样,只是端口不雷同而已,要是您要看效果能够在浏览器F1贰,然后如图操作:

bifa365必发 5

能够看出这么些sid正是地点栏中的token值,那正是咋们定义的sessionId拉,您不想尝试吧。

 

» 推广调查问卷系统

检察问卷作者想许多供销合作社都会用到,大家1般都会融洽做一套,笔者那里要为大家推荐的是神牛问卷,具体怎么试用呢,可以登录地址http://www.lovexins.com:8081/User/Login 账号:shenniiu00三密码:1231二3,进入系统后直接点击“问卷管理”=>”考查问卷”,在那里你就能够添加你想考察的问卷信息和甄选:

bifa365必发 6

设若你添加成就问卷新闻后,能够一贯点击“阅览”查看您的问卷呈现内容和办法(帮助活出手提式无线话机浏览访问),那也是填写侦查问卷的人看来的界面,最近支撑的题材类型有(单选,多选,文本输入),测试地方:http://www.lovexins.com:1001/shenniu003/wenjuan7,地址中的shenniu003是依照账号来呈现的,假诺您是某些公司的hr只怕首席执行官那里地址栏能够间接登记成您公司的拼音名称或然汉字(是或不是感觉还可以够吧):

bifa365必发 7

关键点来了,有了填写的用户咋们需求分析并做计算,这年只必要您点击问卷列表中的”统计“,就能看出如下名指标图片:

bifa365必发 8

你能够点击某3个题材采用对应的“红色”条,直接进去用户采用的解析报表:

bifa365必发 9

看起来效果还是相比不利的吧,关键有多少总括给业主依旧别的朋友看的时候,令人备感“高大上”,那是选项样式的总结图,那么1旦是用户填写类的总括吗,是之类这样的列表:

bifa365必发 10

特点:

  1. 涵盖单选,多选,用户填写类的标题类型

  2. 单点登录架构,能急速嵌入到任何系统中

  3. 手提式无线话机web也能访查问问卷,问答难点

  4. 详见的表格总计

  5. 正式的掩护人士哈哈

bifa365必发,注脚:最终要说的是此调查研商问卷系统是为了有利于需求用到此功效的朋友和公司,即使你认为还足以想发一四个问卷侦查内容,能够调换本身并让自己给你单独分配2个管理者账号,当然假诺您是某些公司带头人也想长久使用此考察系统能够调换邮箱:8412023玖陆@qq.com,随便您发多少问卷只要符合法定剧情;

 

补充:

2017.03.06

应四个博友的急需,那里发送交检查实验察问卷源码:ShenNiu.Question(问卷侦察-源码包)

发表评论

电子邮件地址不会被公开。 必填项已用*标注