最新公告
  • 欢迎您光临波比源码,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入我们
  • .Net单点登录详解 (SSO)

           最近做GXP(高校平台)的项目,由于里边有好多个子系统,例如有考试系统,评教系统,基础系统,新生入学系统,权限系统,如果每一个系统都有自己的独立的登录的界面,那末就会有能访问这5个系统的人就要记住5套用户名,密码。哇,好累啊,5套!在这个背景下提出了单点登录(SSO)。

           先来讲说甚么是单点登录(SSO),单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之1。SSO的定义是在多个利用系统中,用户只需要登录1次就能够访问所有相互信任的利用系统。单点登录简单来讲,就1处登录,可以到处访问。

            SSO的解决方案很多,比如收费的有UTrust、惠普灵动等,开源的有CAS、Smart SSO等,其中利用最为广泛的是CAS。但是因本人能力有限,没有做出cas在.net下的利用。所有这篇文章来介绍1下摹拟cas情势做的1个登录,请大家多多批评指正。

             让我们来看看cas的工作原理!

                           

                

            当用户在地址栏中输入我们想访问的地址后,例如输入基础系统的url地址,基础系统的服务器会检测有无发过来票据(ST),如果没有,那末会跳转到认证中心服务器端,当发送到认证中心服务器端后,认证中心服务器端会检查有无有无发过来CooKie,如果没有,那末就会那末就会出现登录界面,让用户输入用户名,密码,然后回去数据库中进行验证,如果用户输入的正确,那末会生成1个票据(ST)和保存用户名的Cookie,票据(ST)中包括:要访问系统的URL,和进入该系统的唯1凭证。返回给阅读器,阅读器接受后,会保存Cookie,然后通过ST中的URL转到要访问系统的服务器,进入该服务器后,仍然后检测有无ST,现在又ST,那末他会拿着这个ST中的唯1凭证去认证中心服务器进行比对,看看该唯1凭证是否是认证中心服务器端生成的,如果是的话啊,那末就返回用户信息!并展现页面给用户!。

             说了这么多,给大家看1个例子。先看1下这个例子的结构!

            

             这个例子中有3个项目,分别为:MasterSite,site1,site2.MasterSite 摹拟认证中心服务器,site1摹拟其中的1个利用,site2摹拟另外一个利用!

    让我们看1下主要的代码!

    验证服务器真个登录页的后台代码。首先检查发过来的要求中包括的信息都有甚么,

    using System;
    using System.Data;
    using System.Configuration;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
    using System.Text;

    public partial class _Default : System.Web.UI.Page
    {
    /// <summary>
    /// 服务器真个登录页面的类
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected void Page_Load(object sender, EventArgs e)
    {
    if (!IsPostBack)
    {
    SSORequest ssoRequest = new SSORequest();

    #region 验证 Post 过来的参数
    //——————————–
    // 要求注销
    if (!string.IsNullOrEmpty(Request["Logout"]))
    {
    Authentication.Logout();
    return;
    }
    //——————————–
    // 各独立站点标识
    if (string.IsNullOrEmpty(Request["IASID"]))
    {
    return;
    }
    else
    {
    ssoRequest.IASID = Request["IASID"];
    }

    //——————————–
    // 时间戳
    if (string.IsNullOrEmpty(Request["TimeStamp"]))
    {
    return;
    }
    else
    {
    ssoRequest.TimeStamp = Request["TimeStamp"];
    }

    //——————————–
    // 各独立站点的访问地址
    if (string.IsNullOrEmpty(Request["AppUrl"]))
    {
    return;
    }
    else
    {
    ssoRequest.AppUrl = Request["AppUrl"];
    }

    //——————————–
    // 各独立站点的 Token
    if (string.IsNullOrEmpty(Request["Authenticator"]))
    {
    return;
    }
    else
    {
    ssoRequest.Authenticator = Request["Authenticator"];
    }

    ViewState["SSORequest"] = ssoRequest;

    #endregion

    //验证从分站发过来的Token
    if (Authentication.ValidateAppToken(ssoRequest))
    {
    string userAccount = null;

    // 验证用户之前是不是登录过
    //验证 EAC 认证中心的 Cookie,验证通过时获得用户登录账号
    if (Authentication.ValidateEACCookie(out userAccount))
    {
    ssoRequest.UserAccount = userAccount;

    //创建认证中心发往各分站的 Token
    if (Authentication.CreateEACToken(ssoRequest))
    {
    Post(ssoRequest);
    }
    }
    else
    {
    return;
    }
    }
    else
    {
    return;
    }
    }
    }

    //post要求
    void Post(SSORequest ssoRequest)
    {
    PostService ps = new PostService();

    ps.Url = ssoRequest.AppUrl;

    ps.Add("UserAccount", ssoRequest.UserAccount);
    ps.Add("IASID", ssoRequest.IASID);
    ps.Add("TimeStamp", ssoRequest.TimeStamp);
    ps.Add("AppUrl", ssoRequest.AppUrl);
    ps.Add("Authenticator", ssoRequest.Authenticator);

    ps.Post();
    }

    /// <summary>
    /// 验证登录账号和密码是不是正确
    /// </summary>
    /// <param name="userName">登录账号</param>
    /// <param name="userPwd">登录密码</param>
    /// <returns></returns>
    private bool ValidateUserInfo(string userName, string userPwd)
    {
    //从数据库中读取,验证登录账号和密码
    //略…
    return true;
    }

    protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
    {
    if (string.IsNullOrEmpty(Login1.UserName) || string.IsNullOrEmpty(Login1.Password))
    {
    Page.RegisterClientScriptBlock("Add", "<script lanuage="javascript">alert('用户名密码不能为空!');</script>");
    return;
    }
    else if (ValidateUserInfo(Login1.UserName, Login1.Password) == false)
    {
    Page.RegisterClientScriptBlock("Add", "<script lanuage="javascript">alert('用户名密码毛病!');</script>");
    return;
    }
    else
    {
    //保存当前用户的用户名
    Session["CurrUserName"] = Login1.UserName;
    //设置过期时间 单位为分钟
    Session.Timeout = 120;

    SSORequest ssoRequest = ViewState["SSORequest"] as SSORequest;

    // 如果不是从各分站 Post 过来的要求,则默许登录主站
    if (ssoRequest == null)
    {
    FormsAuthentication.SetAuthCookie(Login1.UserName, false);

    ssoRequest = new SSORequest();
    //主站标识ID
    ssoRequest.IASID = "00";
    ssoRequest.AppUrl = "SiteList.aspx";
    ssoRequest.TimeStamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
    ssoRequest.Authenticator = string.Empty;

    Response.Redirect("SiteList.aspx");
    }
    ssoRequest.UserAccount = Login1.UserName;

    //创建Token
    if (Authentication.CreateEACToken(ssoRequest))
    {
    string expireTime = DateTime.Now.AddHours(3).ToString("yyyy-MM-dd HH:mm");

    Authentication.CreatEACCookie(ssoRequest.UserAccount, ssoRequest.TimeStamp, expireTime);

    Post(ssoRequest);
    }

    }
    }
    }

    </pre><p></p><p>        <span style="font-size:18px">  该类主要是将要发送的要求封装起来成为1个类进行发送!</span></p><p><span style="font-size:18px"></span></p><pre name="code" class="html">using System;
    using System.Data;
    using System.Configuration;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;

    /// <summary>
    /// SSORequest 该类主要是设置要求包,要求包中都包括哪些信息!
    /// 继承MarshalByRefObject ,主要解决跨域访问的问题!
    /// </summary>
    [Serializable]
    public class SSORequest : MarshalByRefObject
    {
    public string IASID; //各独立站点标识ID
    public string TimeStamp; //时间戳
    public string AppUrl; //各独立站点的访问地址
    public string Authenticator; //各独立站点的 Token

    public string UserAccount; //账号
    public string Password; //密码

    public string IPAddress; //IP地址

    //为ssresponse对象做准备
    public string ErrorDescription = "认证失败"; //用户认证通过,认证失败,包数据格式不正确,数据校验不正确
    public int Result = ⑴;

    public SSORequest()
    {

    }

    /// <summary>
    /// 获得当前页面上的SSORequest对象
    /// </summary>
    /// <param name="CurrentPage"></param>
    /// <returns></returns>
    public static SSORequest GetRequest(Page CurrentPage)
    {
    SSORequest request = new SSORequest();
    request.IPAddress = CurrentPage.Request.UserHostAddress;
    request.IASID = CurrentPage.Request["IASID"].ToString();// Request本身会Decode
    request.UserAccount = CurrentPage.Request["UserAccount"].ToString();//this.Text
    request.Password = CurrentPage.Request["Password"].ToString();
    request.AppUrl = CurrentPage.Request["AppUrl"].ToString();
    request.Authenticator = CurrentPage.Request["Authenticator"].ToString();
    request.TimeStamp = CurrentPage.Request["TimeStamp"].ToString();
    return request;
    }
    }

           该类的主要作用是发送要求,或将遭到的要求进行处理,发后回送要求信息!

    <span style="font-size:18px;">using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Web;

    /// <summary>
    /// 该类的主要作用是发送要求,
    /// 或将遭到的要求进行处理,发后回送要求信息!
    /// </summary>
    public class PostService
    {
    private System.Collections.Specialized.NameValueCollection Inputs = new System.Collections.Specialized.NameValueCollection();
    public string Url = "";
    public string Method = "post";
    public string FormName = "form1";

    /// <summary>
    /// 添加需要提交的名和值
    /// </summary>
    /// <param name="name"></param>
    /// <param name="value"></param>
    public void Add(string name, string value)
    {
    Inputs.Add(name, value);
    }

    /// <summary>
    /// 以输出Html方式POST
    /// </summary>
    public void Post()
    {
    System.Web.HttpContext.Current.Response.Clear();

    string html = string.Empty;

    html += ("<html><head>");
    html += (string.Format("</head><body onload="document.{0}.submit()">", FormName));
    html += (string.Format("<form name="{0}" method="{1}" action="{2}" >", FormName, Method, Url));
    try
    {
    for (int i = 0; i < Inputs.Keys.Count; i++)
    {
    html += (string.Format("<input name="{0}" type="hidden" value="{1}">", Inputs.Keys[i], Inputs[Inputs.Keys[i]]));
    }
    html += ("</form>");
    html += ("</body></html>");

    System.Web.HttpContext.Current.Response.Write(html);
    System.Web.HttpContext.Current.Response.End();
    }
    catch (Exception e)
    {

    }
    }
    }</span>

            安全验证类,主要对发送过来的票据进行验证!

    <span style="font-size:18px;">using System;
    using System.Data;
    using System.Configuration;
    using System.Web;
    using System.Web.Security;
    using System.Collections.Generic;
    using System.Text;

    /// <summary>
    /// 安全验证类
    /// </summary>
    public class Authentication
    {
    static readonly string cookieName = "EACToken";
    static readonly string hashSplitter = "|";

    public Authentication()
    {
    }

    public static string GetAppKey(int appID)
    {
    //string cmdText = @"select * from ";
    return string.Empty;
    }

    public static string GetAppKey()
    {
    return "22362E7A9285DD53A0BBC2932F9733C505DC04EDBFE00D70";
    }

    public static string GetAppIV()
    {
    return "1E7FA9231E7FA923";
    }

    /// <summary>
    /// 获得加密服务
    /// </summary>
    /// <returns></returns>
    static CryptoService GetCryptoService()
    {
    string key = GetAppKey();
    string IV = GetAppIV();

    CryptoService cs = new CryptoService(key, IV);
    return cs;
    }

    /// <summary>
    /// 创建各分站发往认证中心的 Token
    /// </summary>
    /// <param name="ssoRequest"></param>
    /// <returns></returns>
    public static bool CreateAppToken(SSORequest ssoRequest)
    {
    string OriginalAuthenticator = ssoRequest.IASID + ssoRequest.TimeStamp + ssoRequest.AppUrl;
    string AuthenticatorDigest = CryptoHelper.ComputeHashString(OriginalAuthenticator);
    string sToEncrypt = OriginalAuthenticator + AuthenticatorDigest;
    byte[] bToEncrypt = CryptoHelper.ConvertStringToByteArray(sToEncrypt);

    CryptoService cs = GetCryptoService();

    byte[] encrypted;

    if (cs.Encrypt(bToEncrypt, out encrypted))
    {
    ssoRequest.Authenticator = CryptoHelper.ToBase64String(encrypted);

    return true;
    }
    else
    {
    return false;
    }
    }

    /// <summary>
    /// 验证从各分站发送过来的 Token
    /// </summary>
    /// <param name="ssoRequest"></param>
    /// <returns></returns>
    public static bool ValidateAppToken(SSORequest ssoRequest)
    {
    string Authenticator = ssoRequest.Authenticator;

    string OriginalAuthenticator = ssoRequest.IASID + ssoRequest.TimeStamp + ssoRequest.AppUrl;
    string AuthenticatorDigest = CryptoHelper.ComputeHashString(OriginalAuthenticator);
    string sToEncrypt = OriginalAuthenticator + AuthenticatorDigest;
    byte[] bToEncrypt = CryptoHelper.ConvertStringToByteArray(sToEncrypt);

    CryptoService cs = GetCryptoService();
    byte[] encrypted;

    if (cs.Encrypt(bToEncrypt, out encrypted))
    {
    return Authenticator == CryptoHelper.ToBase64String(encrypted);
    }
    else
    {
    return false;
    }
    }

    /// <summary>
    /// 创建认证中心发往各分站的 Token
    /// </summary>
    /// <param name="ssoRequest"></param>
    /// <returns></returns>
    public static bool CreateEACToken(SSORequest ssoRequest)
    {
    string OriginalAuthenticator = ssoRequest.UserAccount + ssoRequest.IASID + ssoRequest.TimeStamp + ssoRequest.AppUrl;
    string AuthenticatorDigest = CryptoHelper.ComputeHashString(OriginalAuthenticator);
    string sToEncrypt = OriginalAuthenticator + AuthenticatorDigest;
    byte[] bToEncrypt = CryptoHelper.ConvertStringToByteArray(sToEncrypt);

    CryptoService cs = GetCryptoService();
    byte[] encrypted;

    if (cs.Encrypt(bToEncrypt, out encrypted))
    {
    ssoRequest.Authenticator = CryptoHelper.ToBase64String(encrypted);

    return true;
    }
    else
    {
    return false;
    }
    }

    /// <summary>
    /// 验证从认证中心发送过来的 Token
    /// </summary>
    /// <param name="ssoRequest"></param>
    /// <returns></returns>
    public static bool ValidateEACToken(SSORequest ssoRequest)
    {
    string Authenticator = ssoRequest.Authenticator;

    string OriginalAuthenticator = ssoRequest.UserAccount + ssoRequest.IASID + ssoRequest.TimeStamp + ssoRequest.AppUrl;
    string AuthenticatorDigest = CryptoHelper.ComputeHashString(OriginalAuthenticator);
    string sToEncrypt = OriginalAuthenticator + AuthenticatorDigest;
    byte[] bToEncrypt = CryptoHelper.ConvertStringToByteArray(sToEncrypt);

    string EncryCurrentAuthenticator = string.Empty;
    CryptoService cs = GetCryptoService();
    byte[] encrypted;

    if (cs.Encrypt(bToEncrypt, out encrypted))
    {
    EncryCurrentAuthenticator = CryptoHelper.ToBase64String(encrypted);

    return Authenticator == EncryCurrentAuthenticator;
    }
    else
    {
    return false;
    }
    }

    /// <summary>
    /// 创建 EAC 认证中心的 Cookie
    /// </summary>
    /// <param name="userAccount"></param>
    /// <param name="timeStamp"></param>
    /// <param name="expireTime"></param>
    /// <param name="cookieValue"></param>
    /// <returns></returns>
    public static bool CreatEACCookie(string userAccount, string timeStamp, string expireTime)
    {
    string plainText = "UserAccount=" + userAccount + ";TimeStamp=" + timeStamp + ";ExpireTime=" + expireTime;
    plainText += hashSplitter + CryptoHelper.ComputeHashString(plainText);

    CryptoService cs = GetCryptoService();
    byte[] encrypted;

    if (cs.Encrypt(CryptoHelper.ConvertStringToByteArray(plainText), out encrypted))
    {
    string cookieValue = CryptoHelper.ToBase64String(encrypted);
    SetCookie(cookieValue);

    return true;
    }
    else
    {
    return false;
    }
    }

    /// <summary>
    /// 验证 EAC 认证中心的 Cookie,验证通过时获得用户登录账号
    /// </summary>
    /// <param name="userAccount">输出用户登录账号</param>
    /// <returns></returns>
    public static bool ValidateEACCookie(out string userAccount)
    {
    userAccount = string.Empty;
    try
    {

    string cookieValue = GetCookie().Value;
    byte[] toDecrypt = CryptoHelper.FromBase64String(cookieValue);
    CryptoService cs = GetCryptoService();

    string decrypted = string.Empty;
    if (cs.Decrypt(toDecrypt, out decrypted))
    {

    string[] arrTemp = decrypted.Split(Convert.ToChar(hashSplitter));
    string plainText = arrTemp[0];
    string hashedText = arrTemp[1];

    userAccount = plainText.Split(Convert.ToChar(";"))[0].Split(Convert.ToChar("="))[1];

    return hashedText.Replace("", string.Empty) == CryptoHelper.ComputeHashString(plainText);

    }
    else
    {
    return false;
    }
    }
    catch (Exception e)
    {
    return false;
    }
    }

    public static void Logout()
    {
    HttpContext.Current.Response.Cookies[cookieName].Expires = DateTime.Parse("1900⑴⑴");
    HttpContext.Current.Response.Cookies[cookieName].Path = "/";
    }

    private static void SetCookie(string cookieValue)
    {
    HttpContext.Current.Response.Cookies[cookieName].Value = cookieValue;
    HttpContext.Current.Response.Cookies[cookieName].Expires = DateTime.Now.AddHours(24);
    HttpContext.Current.Response.Cookies[cookieName].Path = "/";
    }

    private static HttpCookie GetCookie()
    {
    HttpCookie cookie = HttpContext.Current.Request.Cookies["EACToken"];
    return cookie;
    }
    }</span>

             上边是主站中主要的类的代码,APP_CODE下边的其他两个类主要是进行数据加密用的,这里不再列出!下边看1下分站的代码,大部份的都和主站的代码1样,也包括1个SSORequest和1个PostServer还有另外3个类,下边看1下主页的后台代码

           

    using System;
    using System.Data;
    using System.Configuration;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
    using System.Text;

    public partial class _Default : System.Web.UI.Page
    {
    protected void Page_Load(object sender, EventArgs e)
    {
    if (!IsPostBack)
    {
    #region SSO 部份代码
    SSORequest ssoRequest = new SSORequest();

    if (string.IsNullOrEmpty(Request["IASID"]))
    {
    ssoRequest.IASID = "01";
    ssoRequest.TimeStamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
    ssoRequest.AppUrl = Request.Url.ToString();
    Authentication.CreateAppToken(ssoRequest);

    Post(ssoRequest);
    }
    else if (!string.IsNullOrEmpty(Request["IASID"])
    && !string.IsNullOrEmpty(Request["TimeStamp"])
    && !string.IsNullOrEmpty(Request["AppUrl"])
    && !string.IsNullOrEmpty(Request["UserAccount"])
    && !string.IsNullOrEmpty(Request["Authenticator"]))
    {
    ssoRequest.IASID = Request["IASID"];
    ssoRequest.TimeStamp = Request["TimeStamp"];
    ssoRequest.AppUrl = Request["AppUrl"];
    ssoRequest.UserAccount = Request["UserAccount"];
    ssoRequest.Authenticator = Request["Authenticator"];

    if (Authentication.ValidateEACToken(ssoRequest))
    {
    //从数据库中获得UserId
    Session["CurrUserName"] = Request["UserAccount"];
    Session.Timeout = 120;
    FormsAuthentication.SetAuthCookie(Request["UserAccount"], false);
    Response.Write(string.Format("{0},您好!欢迎来到site1, >> 访问<a href="http://localhost:6332/Site2/Default.aspx">site2</a>", ssoRequest.UserAccount));
    }
    }

    ViewState["SSORequest"] = ssoRequest;

    #endregion
    }
    }

    void Post(SSORequest ssoRequest)
    {
    PostService ps = new PostService();
    //认证中心(主站)地址
    string EACUrl = "http://192.168.24.89:8085";
    ps.Url = EACUrl;
    //ps.Add("UserAccount", ssoRequest.UserAccount);
    ps.Add("IASID", ssoRequest.IASID);
    ps.Add("TimeStamp", ssoRequest.TimeStamp);
    ps.Add("AppUrl", ssoRequest.AppUrl);
    ps.Add("Authenticator", ssoRequest.Authenticator);

    ps.Post();
    }

    //注销登录
    protected void LinkButton2_Click(object sender, EventArgs e)
    {
    FormsAuthentication.SignOut();

    SSORequest ssoRequest = new SSORequest();

    ssoRequest.IASID = "01";
    ssoRequest.TimeStamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
    ssoRequest.AppUrl = Request.Url.ToString();

    Authentication.CreateAppToken(ssoRequest);

    PostService ps = new PostService();

    //认证中心(主站)地址
    string EACUrl = "http://192.168.24.89:8085";
    ps.Url = EACUrl;

    ps.Add("IASID", ssoRequest.IASID);
    ps.Add("TimeStamp", ssoRequest.TimeStamp);
    ps.Add("AppUrl", ssoRequest.AppUrl);
    ps.Add("Authenticator", ssoRequest.Authenticator);

    ps.Add("Logout", "true");

    ps.Post();
    }

    //返回主站
    protected void LinkButton1_Click(object sender, EventArgs e)
    {
    if (Session["CurrUserName"] != null)
    {
    Response.Redirect("http://192.168.24.89:8085/MasterSite/SiteList.aspx");
    }
    }
    }

          可以看到有1个注销登录,他会清空主站的Cookie和分站的Cookie中的信息!  

          这里仅写了1些主要的类的代码,可以来这里下载DEMO。

          在计算机发展的愈来愈快的时期,好多事情都要放到计算机中进行处理,那末系统也就会随之增多。所以单点登录也就会变得愈来愈重要!

    波比源码 – 精品源码模版分享 | www.bobi11.com
    1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
    2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
    3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
    4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
    5. 如有链接无法下载、失效或广告,请联系管理员处理!
    6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
    7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!

    波比源码 » .Net单点登录详解 (SSO)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    波比源码
    一个高级程序员模板开发平台
    升级波友尊享更多特权立即升级