最近在做的一个ASP.NET MVC的项目,要用到第三方的php系统,为了实现两个系统的互联互通。决定将两者的session打通共享。让asp.net mvc 和php 都能正常访问和修改Session内容。
在决定实现之前,先搜索了一下院子里有没有相类似的文章,对于跨语言的程序互通,有两种方案:
(1) SSO单点登录,其实就是把用户名和密码传给另一个系统在另一个系统上自动创建session 来解决。
(2) 还有就是用同一套Session,在数据库或缓存上架设session服务。
第一种方案因为两套系统的交互比较多,不仅仅是登陆和验证,所以不适合。
第二种方案网上资料不多,不过关于session 数据库持久化的内容不少。选择第二种方案。
先说一下 大概的思路:
(1) 将Asp.net 的Session 采用mysql 存储。
(2) 将PHP的session也采用同一个数据库,同一张表存储。
(3) 修改Asp.net的Session内容的序列化和反序列化方式,使之与PHP的序列号方式相同。
(4) 修改两者的 cookie 中的sessionID 名称让二者采用同一个Sessionid名称。
(5) 修改两者的cookie作用域,使两者的Cookie Domin 位于主域名如 xxx.com。
(6) 解决后续出现的问题。
* php session 序列化格式: user_id|s:1:"3";user_name|s:4:"test";email|s:12:"fdff@fdf.com";avatarSrc|s:0:"";
闲话不多说直接上代码。
--Mysql 数据表的创建代码 CREATE TABLE `sessions` ( `SessionId` VARCHAR(80) NOT NULL, `ApplicationName` VARCHAR(255) NOT NULL, `Created` DATETIME NOT NULL, `Expires` DATETIME NOT NULL, `LockDate` DATETIME NOT NULL, `LockId` INT(10) UNSIGNED NOT NULL, `Timeout` INT(10) UNSIGNED NOT NULL, `Locked` SMALLINT(1) UNSIGNED NOT NULL, `SessionItems` LONGTEXT NOT NULL, `Flags` INT(10) UNSIGNED NOT NULL, PRIMARY KEY (`SessionId`,`ApplicationName`) ) ENGINE=INNODB DEFAULT CHARSET=utf8
Asp.net SessionProvider 代码
public class SessionProvider : SessionStateStoreProviderBase { private SessionStateSection pConfig = null; private string connectionString; private ConnectionStringSettings pConnectionStringSettings; private string eventSource = "OdbcSessionStateStore"; private string eventLog = "Application"; private string exceptionMessage = "An exception occurred. Please contact your administrator."; private string pApplicationName; // // If false, exceptions are thrown to the caller. If true, // exceptions are written to the event log. // private bool pWriteExceptionsToEventLog = false; public bool WriteExceptionsToEventLog { get { return pWriteExceptionsToEventLog; } set { pWriteExceptionsToEventLog = value; } } // // The ApplicationName property is used to differentiate sessions // in the data source by application. // public string ApplicationName { get { return pApplicationName; } } public override void Initialize(string name, NameValueCollection config) { // // Initialize values from web.config. // if (config == null) throw new ArgumentNullException("config"); if (name == null || name.Length == 0) name = "OdbcSessionStateStore"; if (String.IsNullOrEmpty(config["description"])) { config.Remove("description"); config.Add("description", "Sample ODBC Session State Store provider"); } // Initialize the abstract base class. base.Initialize(name, config); // // Initialize the ApplicationName property. // pApplicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath; // // Get <sessionState> configuration element. // Configuration cfg = WebConfigurationManager.OpenWebConfiguration(ApplicationName); pConfig = (SessionStateSection)cfg.GetSection("system.web/sessionState"); // // Initialize connection string. // pConnectionStringSettings = ConfigurationManager.ConnectionStrings["MySqlSessionServices"]; //if (pConnectionStringSettings == null || // pConnectionStringSettings.ConnectionString.Trim() == "") //{ // throw new ProviderException("Connection string cannot be blank."); //} connectionString = pConnectionStringSettings.ConnectionString; // // Initialize WriteExceptionsToEventLog // pWriteExceptionsToEventLog = false; //if (config["writeExceptionsToEventLog"] != null) //{ // if (config["writeExceptionsToEventLog"].ToUpper() == "TRUE") // pWriteExceptionsToEventLog = true; //} } // // SessionStateStoreProviderBase members // public override void Dispose() { } // // SessionStateProviderBase.SetItemExpireCallback // public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) { return false; } // // SessionStateProviderBase.SetAndReleaseItemExclusive // public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem) { // Serialize the SessionStateItemCollection as a string. string sessItems = Serialize((SessionStateItemCollection)item.Items); #region odbc代码 //MySqlConnection conn = new MySqlConnection(connectionString); //MySqlCommand cmd; //MySqlCommand deleteCmd = null; #endregion MySqlConnection conn = new MySqlConnection(connectionString); MySqlCommand cmd; MySqlCommand deleteCmd = null; if (newItem) { // MySqlCommand to clear an existing expired session if it exists. deleteCmd = new MySqlCommand("DELETE FROM Sessions " + "WHERE SessionId = ? AND ApplicationName = ? AND Expires < ?", conn); deleteCmd.Parameters.Add("@SessionId",MySqlDbType.VarChar, 80).Value = id; deleteCmd.Parameters.Add ("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; deleteCmd.Parameters.Add ("@Expires", MySqlDbType.DateTime).Value = DateTime.Now; // MySqlCommand to insert the new session item. cmd = new MySqlCommand("INSERT INTO Sessions " + " (SessionId, ApplicationName, Created, Expires, " + " LockDate, LockId, Timeout, Locked, SessionItems, Flags) " + " Values(?, ?, ?, ?, ?, ? , ?, ?, ?, ?)", conn); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add ("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.Parameters.Add ("@Created", MySqlDbType.DateTime).Value = DateTime.Now; cmd.Parameters.Add ("@Expires", MySqlDbType.DateTime).Value = DateTime.Now.AddMinutes((Double)item.Timeout); cmd.Parameters.Add ("@LockDate", MySqlDbType.DateTime).Value = DateTime.Now; cmd.Parameters.AddWithValue("@LockId", MySqlDbType.Int32).Value = 0; cmd.Parameters.Add ("@Timeout", MySqlDbType.Int32).Value = item.Timeout; cmd.Parameters.AddWithValue("@Locked", MySqlDbType.Bit).Value = false; cmd.Parameters.Add ("@SessionItems", MySqlDbType.VarChar, sessItems.Length).Value = sessItems; cmd.Parameters.Add("@Flags", MySqlDbType.Int32).Value = 0; } else { // MySqlCommand to update the existing session item. cmd = new MySqlCommand( "UPDATE Sessions SET Expires = ?, SessionItems = ?, Locked = ? " + " WHERE SessionId = ? AND ApplicationName = ? AND LockId = ?", conn); cmd.Parameters.Add("@Expires", MySqlDbType.DateTime).Value =DateTime.Now.AddMinutes((Double)item.Timeout); cmd.Parameters.Add("@SessionItems", MySqlDbType.VarChar, sessItems.Length).Value = sessItems; cmd.Parameters.Add("@Locked", MySqlDbType.Bit).Value = false; cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.Parameters.Add("@LockId", MySqlDbType.Int32 ).Value = lockId; } try { conn.Open(); if (deleteCmd != null) deleteCmd.ExecuteNonQuery(); cmd.ExecuteNonQuery(); } catch (MySqlException e) { if (WriteExceptionsToEventLog) { WriteToEventLog(e, "SetAndReleaseItemExclusive"); throw new ProviderException(exceptionMessage); } else throw e; } finally { conn.Close(); } } // // SessionStateProviderBase.GetItem // public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { return GetSessionStoreItem(false, context, id, out locked, out lockAge, out lockId, out actionFlags); } // // SessionStateProviderBase.GetItemExclusive // public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { return GetSessionStoreItem(true, context, id, out locked, out lockAge, out lockId, out actionFlags); } // // GetSessionStoreItem is called by both the GetItem and // GetItemExclusive methods. GetSessionStoreItem retrieves the // session data from the data source. If the lockRecord parameter // is true (in the case of GetItemExclusive), then GetSessionStoreItem // locks the record and sets a new LockId and LockDate. // private SessionStateStoreData GetSessionStoreItem(bool lockRecord, HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { // Initial values for return value and out parameters. SessionStateStoreData item = null; lockAge = TimeSpan.Zero; lockId = null; locked = false; actionFlags = 0; // ODBC database connection. MySqlConnection conn = new MySqlConnection(connectionString); // MySqlCommand for database commands. MySqlCommand cmd = null; // DataReader to read database record. MySqlDataReader reader = null; // DateTime to check if current session item is expired. DateTime expires; // String to hold serialized SessionStateItemCollection. string serializedItems = ""; // True if a record is found in the database. bool foundRecord = false; // True if the returned session item is expired and needs to be deleted. bool deleteData = false; // Timeout value from the data store. int timeout = 0; try { conn.Open(); // lockRecord is true when called from GetItemExclusive and // false when called from GetItem. // Obtain a lock if possible. Ignore the record if it is expired. if (lockRecord) { cmd = new MySqlCommand( "UPDATE Sessions SET" + " Locked = ?, LockDate = ? " + " WHERE SessionId = ? AND ApplicationName = ? AND Locked = ? AND Expires > ?", conn); cmd.Parameters.Add("@Locked", MySqlDbType.Bit).Value = true; cmd.Parameters.Add("@LockDate", MySqlDbType.DateTime).Value= DateTime.Now; cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.Parameters.Add("@Lockid", MySqlDbType.Int32).Value = 0; cmd.Parameters.Add("@Expires", MySqlDbType.DateTime).Value = DateTime.Now; if (cmd.ExecuteNonQuery() == 0) // No record was updated because the record was locked or not found. locked = true; else // The record was updated. locked = false; } // Retrieve the current session item information. cmd = new MySqlCommand( "SELECT Expires, SessionItems, LockId, LockDate, Flags, Timeout " + " FROM Sessions " + " WHERE SessionId = ? AND ApplicationName = ?", conn); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; // Retrieve session item data from the data source. reader = cmd.ExecuteReader(CommandBehavior.SingleRow); while (reader.Read()) { expires = reader.GetDateTime(0); if (expires < DateTime.Now) { // The record was expired. Mark it as not locked. locked = false; // The session was expired. Mark the data for deletion. deleteData = true; } else foundRecord = true; serializedItems = reader.GetString(1); lockId = reader.GetInt32(2); lockAge = DateTime.Now.Subtract(reader.GetDateTime(3)); actionFlags = (SessionStateActions)reader.GetInt32(4); timeout = reader.GetInt32(5); } reader.Close(); // If the returned session item is expired, // delete the record from the data source. if (deleteData) { cmd = new MySqlCommand("DELETE FROM Sessions " + "WHERE SessionId = ? AND ApplicationName = ?", conn); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.ExecuteNonQuery(); } // The record was not found. Ensure that locked is false. if (!foundRecord) locked = false; // If the record was found and you obtained a lock, then set // the lockId, clear the actionFlags, // and create the SessionStateStoreItem to return. if (foundRecord && !locked) { lockId = (int)lockId + 1; cmd = new MySqlCommand("UPDATE Sessions SET" + " LockId = ?, Flags = 0 " + " WHERE SessionId = ? AND ApplicationName = ?", conn); cmd.Parameters.Add("@LockId", MySqlDbType.Int32).Value = lockId; cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.ExecuteNonQuery(); // If the actionFlags parameter is not InitializeItem, // deserialize the stored SessionStateItemCollection. if (actionFlags == SessionStateActions.InitializeItem) item = CreateNewStoreData(context, pConfig.Timeout.Minutes); else item = Deserialize(context, serializedItems, timeout); } } catch (MySqlException e) { if (WriteExceptionsToEventLog) { WriteToEventLog(e, "GetSessionStoreItem"); throw new ProviderException(exceptionMessage); } else throw e; } finally { if (reader != null) { reader.Close(); } conn.Close(); } return item; } // // Serialize is called by the SetAndReleaseItemExclusive method to // convert the SessionStateItemCollection into a Base64 string to // be stored in an Access Memo field. // private string Serialize(SessionStateItemCollection items) { items["LastName"] = "Wilson"; items["FirstName"] = "Dan"; string ret = ""; for (int i = 0; i < items.Count;i++ ) { string str = ""; if (items[i] != null) { str = items.Keys[i] + "|s:" + items[i].ToString().Length + ":\"" + items[i].ToString() + "\";"; } else { str = items.Keys[i] + "|N;"; } ret += str; } return ret; } #region base64 序列化session // // DeSerialize is called by the GetSessionStoreItem method to // convert the Base64 string stored in the Access Memo field to a // SessionStateItemCollection. // //private SessionStateStoreData Deserialize(HttpContext context, // string serializedItems, int timeout) //{ // MemoryStream ms = // new MemoryStream(Convert.FromBase64String(serializedItems)); // SessionStateItemCollection sessionItems = // new SessionStateItemCollection(); // if (ms.Length > 0) // { // BinaryReader reader = new BinaryReader(ms); // sessionItems = SessionStateItemCollection.Deserialize(reader); // } // return new SessionStateStoreData(sessionItems, // SessionStateUtility.GetSessionStaticObjects(context), // timeout); //} #endregion /// <summary> /// 自定义获取session项 /// </summary> /// <param name="context"></param> /// <param name="serializedItems"></param> /// <param name="timeout"></param> /// <returns></returns> private SessionStateStoreData Deserialize(HttpContext context, string serializedItems, int timeout) { SessionStateItemCollection sessionItems = new SessionStateItemCollection(); string[] arry = serializedItems.Split(‘;‘); foreach (string item in arry) { if (!string.IsNullOrEmpty(item)) { string strKey = item.Split(‘|‘)[0]; string other = item.Split(‘|‘)[1]; string strValue = ""; if (other == "N") { sessionItems[strKey]=null; } else if (other.Split(‘:‘).Count() > 2) {//s:1:"1" strValue = other.Split(‘:‘)[2]; sessionItems[strKey] = strValue.Replace("\"", ""); } else { sessionItems[strKey] = null; } } } return new SessionStateStoreData(sessionItems, SessionStateUtility.GetSessionStaticObjects(context), timeout); } public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) { MySqlConnection conn = new MySqlConnection(connectionString); MySqlCommand cmd = new MySqlCommand("UPDATE Sessions SET Locked = 0, Expires = ? " + "WHERE SessionId = ? AND ApplicationName = ? AND LockId = ?", conn); cmd.Parameters.Add("@Expires", MySqlDbType.DateTime).Value = DateTime.Now.AddMinutes(pConfig.Timeout.Minutes); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.Parameters.Add("@LockId", MySqlDbType.Int32).Value = lockId; try { conn.Open(); cmd.ExecuteNonQuery(); } catch (MySqlException e) { if (WriteExceptionsToEventLog) { WriteToEventLog(e, "ReleaseItemExclusive"); throw new ProviderException(exceptionMessage); } else throw e; } finally { conn.Close(); } } // // SessionStateProviderBase.RemoveItem // public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) { MySqlConnection conn = new MySqlConnection(connectionString); MySqlCommand cmd = new MySqlCommand("DELETE * FROM Sessions " + "WHERE SessionId = ? AND ApplicationName = ? AND LockId = ?", conn); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.Parameters.Add("@LockId", MySqlDbType.Int32).Value = lockId; try { conn.Open(); cmd.ExecuteNonQuery(); } catch (MySqlException e) { if (WriteExceptionsToEventLog) { WriteToEventLog(e, "RemoveItem"); throw new ProviderException(exceptionMessage); } else throw e; } finally { conn.Close(); } } // // SessionStateProviderBase.CreateUninitializedItem // public override void CreateUninitializedItem(HttpContext context, string id, int timeout) { //MySqlConnection conn = new MySqlConnection(connectionString); MySqlConnection conn = new MySqlConnection(connectionString); MySqlCommand cmd = new MySqlCommand("INSERT INTO Sessions " + " (SessionId, ApplicationName, Created, Expires, " + " LockDate, LockId, Timeout, Locked, SessionItems, Flags) " + " Values(?, ?, ?, ?, ?, ? , ?, ?, ?, ?)", conn); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; cmd.Parameters.Add("@Created", MySqlDbType.DateTime).Value = DateTime.Now; cmd.Parameters.Add("@Expires", MySqlDbType.DateTime).Value = DateTime.Now.AddMinutes((Double)timeout); cmd.Parameters.Add("@LockDate", MySqlDbType.DateTime).Value = DateTime.Now; cmd.Parameters.Add("@LockId", MySqlDbType.Int32).Value = 0; cmd.Parameters.Add("@Timeout", MySqlDbType.Int32).Value = timeout; cmd.Parameters.Add("@Locked", MySqlDbType.Bit).Value = false; cmd.Parameters.Add("@SessionItems", MySqlDbType.VarChar, 0).Value = ""; cmd.Parameters.Add("@Flags", MySqlDbType.Int32).Value = 1; try { conn.Open(); cmd.ExecuteNonQuery(); } catch (MySqlException e) { if (WriteExceptionsToEventLog) { WriteToEventLog(e, "CreateUninitializedItem"); throw new ProviderException(exceptionMessage); } else throw e; } finally { conn.Close(); } } // // SessionStateProviderBase.CreateNewStoreData // public override SessionStateStoreData CreateNewStoreData( HttpContext context, int timeout) { return new SessionStateStoreData(new SessionStateItemCollection(), SessionStateUtility.GetSessionStaticObjects(context), timeout); } // // SessionStateProviderBase.ResetItemTimeout // public override void ResetItemTimeout(HttpContext context, string id) { MySqlConnection conn = new MySqlConnection(connectionString); MySqlCommand cmd = new MySqlCommand("UPDATE Sessions SET Expires = ? " + "WHERE SessionId = ? AND ApplicationName = ?", conn); cmd.Parameters.Add("@Expires", MySqlDbType.DateTime).Value = DateTime.Now.AddMinutes(pConfig.Timeout.Minutes); cmd.Parameters.Add("@SessionId", MySqlDbType.VarChar, 80).Value = id; cmd.Parameters.Add("@ApplicationName", MySqlDbType.VarChar, 255).Value = ApplicationName; try { conn.Open(); cmd.ExecuteNonQuery(); } catch (MySqlException e) { if (WriteExceptionsToEventLog) { WriteToEventLog(e, "ResetItemTimeout"); throw new ProviderException(exceptionMessage); } else throw e; } finally { conn.Close(); } } public override void InitializeRequest(HttpContext context) { } public override void EndRequest(HttpContext context) { } private void WriteToEventLog(Exception e, string action) { EventLog log = new EventLog(); log.Source = eventSource; log.Log = eventLog; string message = "An exception occurred communicating with the data source.\n\n"; message += "Action: " + action + "\n\n"; message += "Exception: " + e.ToString(); log.WriteEntry(message); } }
Php Session 提供程序代码
function sess_open($save_path, $session_name) { global $SESS_DBHOST, $SESS_DBNAME, $SESS_DBUSER, $SESS_DBPASS, $SESS_DBH; if (! $SESS_DBH = mysql_pconnect($SESS_DBHOST, $SESS_DBUSER, $SESS_DBPASS)) { echo "<li>Can‘t connect to $SESS_DBHOST as $SESS_DBUSER"; echo "<li>MySQL Error: " . mysql_error(); die; } if (! mysql_select_db($SESS_DBNAME, $SESS_DBH)) { echo "<li>Unable to select database $SESS_DBNAME"; die; } return true; } function sess_close() { return true; } function sess_read($key) { global $SESS_DBH, $SESS_LIFE; $qry = "SELECT SessionItems FROM sessions WHERE SessionId = ‘$key‘ and ApplicationName=‘/‘ AND Expires > ‘" .date("Y-m-d H:i:s")."‘"; $qid = mysql_query($qry, $SESS_DBH); if (list($value) = mysql_fetch_row($qid)) { return $value; } //else //{ // sess_write($key,‘‘); // return ‘‘; //} return false; } function sess_write($key, $val) { global $SESS_DBH, $SESS_LIFE; $Tnow=date("Y-m-d H:i:s");//当前时间 $expiry =date("Y-m-d H:i:s", time() + $SESS_LIFE); //过期时间 $value = addslashes($val); $qry = "INSERT INTO sessions (SessionId, ApplicationName, Created, Expires, ". " LockDate, LockId, Timeout, Locked, SessionItems, Flags) ". " VALUES (‘$key‘,‘/‘, ‘$Tnow‘ ,‘$expiry‘,‘$Tnow‘, 0, 20,0, ‘$value‘,0 )"; $qid = mysql_query($qry, $SESS_DBH); if (! $qid) { $qry = "UPDATE sessions SET Expires = ‘$expiry‘, SessionItems = ‘$value‘ WHERE SessionId = ‘$key‘ AND Expires > ‘" . date("Y-m-d H:i:s")."‘"; $qid = mysql_query($qry, $SESS_DBH); } return $qid; } function sess_destroy($key) { global $SESS_DBH; $qry = "DELETE FROM sessions WHERE SessionId = ‘$key‘"; $qid = mysql_query($qry, $SESS_DBH); return $qid; } function sess_gc($maxlifetime) { global $SESS_DBH; $qry = "DELETE FROM sessions WHERE Expires < ‘" . date("Y-m-d H:i:s")."‘"; $qid = mysql_query($qry, $SESS_DBH); return mysql_affected_rows($SESS_DBH); } session_set_save_handler( "sess_open", "sess_close", "sess_read", "sess_write", "sess_destroy", "sess_gc"); session_start();
至于修改作用域 直接在php网站的配置里修改就行,其他的php.ini的修改网上多的是。
后续产生的问题
Asp.net 创建的session信息 ,php能读能写。Php创建的session ASP.NET 在没有创建过Session 的时候不能读取。仅当Asp.Net的程序创建过 Session后,Php 修改过的内容才会生效。
这个问题产生的原因 是ASP.Net 的Session产生机制的问题,页面请求Asp.net并不会马上创建Session和读取Cookie 的SessionID去查找Session
解决这个问题又有多种方案,而我选用的是 最简单的一种 只用 Asp.net的登陆程序做入口。
Asp.net mvc与PHP的Session共享的实现,布布扣,bubuko.com
原文地址:http://www.cnblogs.com/xiaokangufo/p/3818058.html