单点登录中间件

本章将要和名门饥寒交迫的是一个单点登陆中间件,中间件听起来高深其实这里只是吧单点登陆要用到的逻辑和拍卖流程封装成了多少个点子而已,暗中同意协助接收redis服务保存session的方法,也得以运用参数Func<>办法来做自定义session存款和储蓄操作的主意,就无须本身暗许提供的redis存储的措施了;要说本章内容的来自,其实是本人在原先的ShenNiu.MVC拘留类别中参与了目前做的调查问卷模块,那几个问卷考查和ShenNiu.MVC不是多少个站点,可是小编的问卷考察系统可定在保养问卷或主题材料的时候须求登陆位的新闻,作者又不想再单独弄生机勃勃套账号方面包车型客车程序了,所以就利用这种单点登入方式,以此来提供应用钻走访卷的所急需的顾客音讯,以致为了尽早的以往和谐写的某部模块也急需管住客户音讯的话,就可以省略掉客户模块了,一定要说单点登陆在这里儿发布的机能之大;本章内容希望我们能够赏识,也可望各位多多"扫码帮忙"和"推荐"多谢!如果你想要和大家交换越多mvc相关音讯方可来Ninesky框架作者:洞庭夕照 钦定的官方群 428310563沟通;

 

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

» ShenNiuApi.SDK封装中间件代码

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

» 推广考察问卷系统

 

上边实事求是的来享受:

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

率先,咋们要做贰个总结的单点登陆功效,要求明白其实施的流程和周转的规律,这里将绘影绘声入眼提议自身认为主要的地点,先上风姿浪漫幅手工业图:

银河网址 1

看起来图画的不是很窘迫,可是自身想表明的情致认为照旧表明清楚了;作为二个单点登入验证模块,最入眼的流水生产线有:

1. 未登陆时:提供联合登入入口=》去数据库验证账号正确性=》存款和储蓄会话session(这里运用redis存款和储蓄token和客商登入音讯,利用其数额过期攻略充作session会话机制)=》重定向到redirectUrl钦命的地点

2. 已登入时:获取站点的cookie存款和储蓄的sessionId(token卡塔尔(英语:State of Qatar)=》调用验证token有效接口=》这里有三种景况(a,b卡塔尔

    a卡塔尔国有效token=》获取登陆顾客的session存款和储蓄的消息(redis存款和储蓄的value消息)

    b卡塔尔国 无效token=》重回无效音信,布局登入入口地址

透过上面分析,大致的流程应该很扎眼了上边大家就来看包装的代码;

 

» ShenNiuApi.SDK封装中间件代码

那边要看的是中间件的3个章程: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;

有人会问怎会如此做,您单点登陆难道最尾部用的不是接口来操作登陆或证实的呢?这里酌量有与此相类似叁个实用项景,作为一位中型Mini型集团的职工来讲,接触到服务器常常安顿了全体集团的站点举个例子:站点1,站点2...固然域名差异等只是都在长期以来台服务器上,再试想下假设用redis来积累session会话,此刻是或不是就能够感觉自己那台服务器就颇负直接待上访谈redis的读写权限(当然假若redis服务也在此台服务器上的话就更毫不说了),那自个儿间接在中间件中贮存公共操作redis获取session,存款和储蓄session等操作是或不是都没难题,如此那般那大家还索要单独弄一个session(token)验证的api么,没供给的事情(对于单点登入站点和重定向站点来说没供给),由此作者就这么干了,嵌入三个暗许的redis操作哈哈(不性格很顽强在艰难困苦或巨大压力面前不屈能够来辨);尽管如此不能不考虑更加多的作业场景,万风姿洒脱登入账单和别的站点不在三个服务器(或许说不能直接待上访谈redis呢),这里在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卡塔尔(英语:State of Qatar); 就可以完成问卷系统单点登入的表达和获取登入客户的音信,各样深入剖析和设置sid的cookie音信都早就在中间件方法中成就了,是或不是天翻地覆减少了您的编码量;为了相比下边小编直接贴出未有应用SsoMiddleWareClient方法时候的代码量:

银河网址 2银河网址 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父类后,咋们还须要有一个登入的地点,这里自身新创制的花色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并表明是或不是早就登入过,假如登入过了直接通过 return new RedirectResult(returnUrl卡塔尔(英语:State of Qatar); 重定向到returnUrl的地址中去;若无那么步向登入分界面,录入账号新闻后:

银河网址 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:ConfShenNiuApi.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,那么只须求信守上边步骤就会急速的搭建叁个单点登大陆布局;这里自个儿提供下侦查问卷使用单点登陆测量试验的地点:www.lovexins.com:1001/Subject 测量检验账号:shenniu003 密码:123123,注意登陆分界面包车型地铁域名和问卷考查的域名同样,只是端口不相像而已,假诺你要看效果能够在浏览器F12,然后如图操作:

银河网址 5

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

 

» 推广考察问卷系统

侦察问卷笔者想大多小卖部都会用到,大家平常都会自身做风度翩翩套,小编这里要为我们推荐的是神牛问卷,具体怎么试用呢,能够登入地址 账号:shenniiu003 密码:123123,步向系统后平昔点击“问卷处理”=>"考查问卷",在此您就足以增添你想考察的问卷新闻和筛选:

银河网址 6

要是您增多成就问卷音信后,能够直接点击“阅览”查看您的问卷呈现内容和方式(扶植活动手提式有线电话机浏览访谈),那也是填写考查问卷的人见状的分界面,近些日子支撑的标题类型有(单选,多选,文本输入),测试地方:,地址中的shenniu003是基于账号来展示的,假若您是有些集团的hr只怕CEO这里地址栏能够一贯登记成您企业的拼音名称或然汉字(是或不是感到仍然为能够吧):

银河网址 7

关键点来了,有了填写的客商咋们供给解析并做总结,此时只必要你点击问卷列表中的"统计",就能够来看如下名指标图纸:

银河网址 8

您能够点击某一个主题素材接纳对应的“红色”条,间接进去客商接纳的剖析报表:

银河网址 9

看起来效果依旧相比科学的呢,关键有多少总结给老总娘依然其余朋友看的时候,令人以为“高大上”,那是采纳样式的总计图,那么只倘使客户填写类的总计吗,是之类那样的列表:

银河网址 10

特点:

  1. 包蕴单选,多选,顾客填写类的难题类型

  2. 单点登入结构,能飞速嵌入到别的系统中

  3. 银河网址,手提式有线电话机web也能访谈考查问问卷,问答难点

  4. 详尽的表格计算

  5. 标准的保险人士哈哈

注明:最终要说的是此考查问卷系统是为着便利要求用到此效率的爱人和供销合作社,假设您以为仍然为能够想爆发机勃勃三个问卷考察内容,能够调换自个儿并让笔者给您单独分配一个首长账号,当然就算你是有个别集团带头人也想悠久使用此考查系统能够联系邮箱:841202396@qq.com,随意您发多少问卷只要顺应法定剧情;

 

补充:

2017.03.06

应七个博友的须要,这里发送交核实察问卷源码:ShenNiu.Question(问卷考查-源码包卡塔尔(英语:State of Qatar)

本文由银河网址发布于银河网址,转载请注明出处:单点登录中间件

您可能还会对下面的文章感兴趣: