MVC - 單點登陸中間件

本章將要和你們分享的是一個單點登陸中間件,中間件聽起來高深其實這裏只是吧單點登陸要用到的邏輯和處理流程封裝成了幾個方法而已,默認支持採用redis服務保存session的方式,也可使用參數Func<>方法來作自定義session存儲操做的方式,就不用我默認提供的redis存儲的方法了;要說本章內容的來源,實際上是我在之前的ShenNiu.MVC管理系統中加入了最近作的調查問卷模塊,這個問卷調查和ShenNiu.MVC不是一個站點,可是個人問卷調查系統可定在維護問卷或題目的時候須要登陸人的信息,我又不想再單獨弄一套帳號方面的程序了,因此就採用這種單點登陸模式,以此來提供調查問卷的所須要的用戶信息,以及爲了避免久的未來本身寫的某個模塊也須要管理用戶信息的話,就能省略掉用戶模塊了,不得不說單點登陸在此刻發揮的做用之大;本章內容但願你們可以喜歡,也但願各位多多"掃碼支持"和"推薦"謝謝!若是您想要和咱們交流更多mvc相關信息能夠來Ninesky框架做者:洞庭夕照 指定的官方羣 428310563交流;html

 

» 單點登陸驗證手畫示例圖web

» ShenNiuApi.SDK封裝中間件代碼redis

» 調查問卷系統使用中間件示例數據庫

» 推廣調查問卷系統api

 

下面一步一個腳印的來分享:瀏覽器

» 單點登陸驗證手畫示例圖服務器

首先,咋們要作一個簡易的單點登陸功能,須要明白其執行的流程和運做的原理,這裏將圖文並茂重點提出我認爲關鍵的地方,先上一幅手工圖:cookie

看起來圖畫的不是很好看,不過我想表達的意思感受仍是表達清楚了;做爲一個單點登陸驗證模塊,最主要的流程有:session

1. 未登陸時:提供統一登陸入口=》去數據庫驗證帳號正確性=》存儲會話session(這裏採用redis存儲token和用戶登錄信息,利用其數據過時策略充當session會話機制)=》重定向到redirectUrl指定的地址架構

2. 已登陸時:獲取站點的cookie存儲的sessionId(token)=》調用驗證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;

有人會問爲何會這樣作,您單點登陸難道最底層用的不是接口來操做登陸或驗證的嗎?這裏考慮有這樣一個實用場景,做爲一位中小型公司的員工來講,接觸到服務器一般部署了整個公司的站點好比:站點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); 便可完成問卷系統單點登陸的驗證和獲取登陸用戶的信息,各類解析和設置sid的cookie信息都已經在中間件方法中完成了,是否是極大減小了您的編碼量;爲了對比下面我直接貼出沒有使用SsoMiddleWareClient方法時候的代碼量:

 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); 重定向到returnUrl的地址中去;若是沒有那麼進入登陸界面,錄入帳號信息後:

提交登陸,進入咋們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,那麼只須要按照上面步驟就能快速的搭建一個單點登陸架構;這裏我提供下調查問卷使用單點登陸測試的地址:www.lovexins.com:1001/Subject 測試帳號:shenniu003 密碼:123123,注意登陸界面的域名和問卷調查的域名同樣,只是端口不同而已,若是您要看效果能夠在瀏覽器F12,而後如圖操做:

可以看到這個sid就是地址欄中的token值,這就是咋們定義的sessionId拉,您不想試試嗎。

 

» 推廣調查問卷系統

調查問卷我想不少公司都會用到,你們通常都會本身作一套,我這裏要爲你們推薦的是神牛問卷,具體怎麼試用呢,能夠登陸地址http://www.lovexins.com:8081/User/Login 帳號:shenniiu003 密碼:123123,進入系統後直接點擊「問卷管理」=>"調查問卷",在這裏您就能夠添加您想調查的問卷信息和選項:

若是您添加完成問卷信息後,能夠直接點擊「閱覽」查看您的問卷展現內容和方式(支持移動手機瀏覽訪問),這也是填寫調查問卷的人看到的界面,目前支持的題目類型有(單選,多選,文本輸入),測試地址:http://www.lovexins.com:1001/shenniu003/wenjuan7,地址中的shenniu003是根據帳號來顯示的,若是您是某個企業的hr或者老闆這裏地址欄能夠直接註冊成您公司的拼音名稱或者漢字(是否是感受還能夠呢):

關鍵點來了,有了填寫的用戶咋們須要分析並作統計,這個時候只須要您點擊問卷列表中的"統計",就能看到以下名目的圖表:

您能夠點擊某一個問題選項對應的「紅色」條,直接進入用戶選項的分析報表:

看起來效果仍是比較不錯的吧,關鍵有數據統計給老闆或者其餘朋友看的時候,讓人感受「高大上」,這是選項樣式的統計圖,那麼若是是用戶填寫類的統計呢,是以下這樣的列表:

特色:

1. 富含單選,多選,用戶填寫類的題目類型

2. 單點登陸架構,能快速嵌入到其餘系統中

3. 手機web也能訪問調查問問卷,問答問題

4. 詳細的報表統計

5. 專業的維護人員哈哈

說明:最後要說的是此調查問卷系統是爲了方便須要用到此功能的朋友和企業,若是您以爲還能夠想發一兩個問卷調查內容,能夠聯繫我並讓我給您單獨分配一個管理者帳號,固然若是您是某個企業帶頭人也想長久使用此調查系統能夠聯繫郵箱:841202396@qq.com,隨便您發多少問卷只要符合法定內容;

 

補充:

2017.03.06

應多個博友的需求,這裏發送調查問卷源碼:ShenNiu.Question(問卷調查-源碼包)

相關文章
相關標籤/搜索