如何使用Windows Identity Foundation(WTF)实现单点登录
资料参考来源 : 我姓区不姓区
有关于WIF的介绍以及环境配置在此不多说,可以去网上搜索,或者点击上方链接前往查看,以下所述都基于WIF配置完成的条件上;
以下很多东西都是从 我姓区不姓区 的博客直接copy过来的,我另外加的就是我跟着他的博客一路中所踩的坑以及我自己的理解;
开始单点登录踩坑之旅:
我们接下来的demo将包括以下的工程:
- SiteA —— 基于.net framework 4.5的MVC 4程序,使用WIF 4.5的SDK,第一个RP
- SiteB —— 基于.net framework 4.5的MVC 4程序,使用WIF 3.5的SDK,第二个RP
- SiteC —— 基于.net framework 4.0的MVC 4程序,使用WIF 3.5的SDK,第三个RP
- SiteD —— 基于.net framework 4.0 的WebApplication程序,使用WIF 3.5的SDK,第四个RP
- STS —— 基于.net framework 4.5 的MVC 4程序,作为IP
一、创建第一个RP
以管理员身份打开vs2012,在起始页上点击“新建项目”,在左边的“模板”树下,展开“其它项目类型”,然后选择“Visual Studio解决方案”,“名称”输入框里输入WIFSSO,然后选择解决方案的路径后点击”确定“,如图:
在”解决方案资源管理器“中,在新建好的解决方案上点右键,选择”添加“->”新建项目“。在弹出的对话框中选择”ASP.NET MVC 4 Web应用程序“,记得.Net Framework版本选4.5,名称起名为”SiteA“,然后点确定,如图:
在弹出的“新ASP.NET MVC 4项目”对话框中直接点“确定”,第一个RP项目新建完成后,添加以下两个引用:System.IdentityModel和System.IdentityModel.Services。这次的教程不使用Identity and Access Tool,而是直接修改web.config文件,这样能使大家对WIF的配置有更深入的了解。
打开web.config文件,将configSections节里的entityFramework配置节点删掉,因为我们不需要用到Entity Framework。最好把web.config中关于Entity Framework相关的配置全都删掉,因为我们都用不上。然后加上以下这两个节点:
<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" /> <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
将authentication节的mode属性设为None,并把里面的form节点删掉,因为我们采用的是WIF的身份验证方式,而不是传统的Forms身份验证。然后增加authorization节点,不允许匿名用户访问站点:
<authorization> <deny users="?"/> </authorization>
在system.webServer节点下增加2个HttpModule的配置节点:
<modules> <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" /> <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" /> </modules>
最后,增加WIF的配置节点:
<system.identityModel> <identityConfiguration> <audienceUris mode="Always"> <add value="http://www.sitea.com" /> </audienceUris> <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <trustedIssuers> <add name="http://www.sts.com" thumbprint="FD1425A2F30937786F46E52E43B01AFD54E5D64D"/> </trustedIssuers> </issuerNameRegistry> </identityConfiguration> </system.identityModel> <system.identityModel.services> <federationConfiguration> <cookieHandler requireSsl="false" /> <wsFederation passiveRedirectEnabled="true" issuer="http://www.sts.com" realm="http://www.sitea.com" reply="http://www.sitea.com" requireHttps="false"/> </federationConfiguration> </system.identityModel.services>
我来详细解释一下这些节点的意义。audienceUris指定了一组可以被RP接受的身份标识URI,只有这些配置中的URI范围内的令牌才可以被接受。这里,我把siteA配置在这里。trustedIssuers就是受信任的发行者,由于我们这个demo没有用到SSL,所以这里我指定的thumbprint是IIS Express的指纹,这个指纹在哪里可以获得呢?打开IIS管理器,在左侧树点击根节点,然后在“功能视图”里双击“服务器证书",如下图:
在打开的证书列表里,找到IIS Express Development Certificate,双击,在弹出的”证书“对话框中点击“详细信息”页签,找到“指纹”然后点击,把框里的指纹拷下来,全都改成大写后粘贴到thumbnail的值里去:
接下来配置federationConfiguration节点,它表示配置WSFederationAuthenticationModule (WSFAM) 和SessionAuthenticationModule (SAM) 时使用联合身份验证通过的 WS 联合身份验证协议。这里我们使用WS 联合身份验证的身份验证模块 (WSFAM),关于该节点的详细配置信息,请参考:http://msdn.microsoft.com/zh-cn/library/office/apps/hh568665.aspx
好,这样一来,SiteA的配置就已经完成了,然后我们来加点代码。
打开/Views/Home/Index.cshtml,将原有的代码删掉,改为如下代码:
@using System.Security.Claims @{ ViewBag.Title = "SiteA主页"; ClaimsIdentity ci = User.Identity as ClaimsIdentity; if(ci!=null) { <h2>@ci.FindFirst(ClaimTypes.Name).Value</h2> <h2>@ci.FindFirst(ClaimTypes.Email).Value</h2> } } <a href="http://www.sts.com/Account/LogOff">退出</a>
代码很简单,只要当前用户处于已登录状态,就把用户的名称和Email显示在页面上。
至此,SiteA就已经完成了。你是不是迫不及待的想要运行了呢?别急,虽然有SiteA了,但还没有STS呢,现在启动SiteA,由于没登录,所以它会跳转到STS,但STS还不存在,所以会出错的。
二、创建STS
接下来我们来创建STS,在解决方案上新建项目,新建一个名为STS的MVC 4应用程序,.Net Framework选择4.5,项目模板选择“Internet应用程序",确定。
添加System.IdentityModel和System.IdentityModel.Services这两个引用,打开web.config,为forms节点添加两个属性:
<forms loginUrl="~/Account/Login" timeout="2880" slidingExpiration="true" name=".STSASPAUTH" />
在AppSettings里增加如下三个节点:
<add key="IssuerName" value="PassiveSigninSTS" /> <add key="SigningCertificateName" value="CN=localhost" /> <add key="EncryptingCertificateName" value="" />
同样禁止匿名用户访问:
<authorization> <deny users="?"/> </authorization>
在应用程序下新建一个名为Services的文件夹,在里面新建一个类文件,名为:CertificateUtil,用于获取证书,具体代码如下:
public class CertificateUtil { public static X509Certificate2 GetCertificate(StoreName name, StoreLocation location, string subjectName) { X509Store store = new X509Store(name, location); X509Certificate2Collection certificates = null; store.Open(OpenFlags.ReadOnly); try { X509Certificate2 result = null; certificates = store.Certificates; for (int i = 0; i < certificates.Count; i++) { X509Certificate2 cert = certificates[i]; if (cert.SubjectName.Name.ToLower() == subjectName.ToLower()) { if (result != null) throw new ApplicationException(string.Format("subject Name {0}存在多个证书", subjectName)); result = new X509Certificate2(cert); } } if (result == null) { throw new ApplicationException(string.Format("没有找到用于 subject Name {0} 的证书", subjectName)); } return result; } finally { if (certificates != null) { for (int i = 0; i < certificates.Count; i++) { certificates[i].Reset(); } } store.Close(); } } }
创建新类,名为Common,存放几个常量:
public class Common { public const string IssuerName = "IssuerName"; public const string SigningCertificateName = "SigningCertificateName"; public const string EncryptingCertificateName = "EncryptingCertificateName"; }
创建新类,名为SingleSignOnManager,用于注册RP以及获取RP列表:
public class SingleSignOnManager { const string SITECOOKIENAME = "StsSiteCookie"; const string SITENAME = "StsSite"; /// <summary> /// Returns a list of sites the user is logged in via the STS /// </summary> /// <returns></returns> public static string[] SignOut() { if (HttpContext.Current != null && HttpContext.Current.Request != null && HttpContext.Current.Request.Cookies != null ) { HttpCookie siteCookie = HttpContext.Current.Request.Cookies[SITECOOKIENAME]; if (siteCookie != null) return siteCookie.Values.GetValues(SITENAME); } return new string[0]; } public static void RegisterRP(string SiteUrl) { if (HttpContext.Current != null && HttpContext.Current.Request != null && HttpContext.Current.Request.Cookies != null ) { // get an existing cookie or create a new one HttpCookie siteCookie = HttpContext.Current.Request.Cookies[SITECOOKIENAME]; if (siteCookie == null) siteCookie = new HttpCookie(SITECOOKIENAME); siteCookie.Values.Add(SITENAME, SiteUrl); HttpContext.Current.Response.AppendCookie(siteCookie); } } }
创建新类,CustomSecurityTokenService,自定义令牌服务,继承SecurityTokenService,用于返回需要的声明令牌:
public class CustomSecurityTokenService : SecurityTokenService { private readonly SigningCredentials signingCreds; private readonly EncryptingCredentials encryptingCreds; public CustomSecurityTokenService(SecurityTokenServiceConfiguration config) : base(config) { this.signingCreds = new X509SigningCredentials( CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.SigningCertificateName])); if (!string.IsNullOrWhiteSpace(WebConfigurationManager.AppSettings[Common.EncryptingCertificateName])) { this.encryptingCreds = new X509EncryptingCredentials( CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.EncryptingCertificateName])); } } /// <summary> /// 此方法返回要发布的令牌内容。内容由一组ClaimsIdentity实例来表示,每一个实例对应了一个要发布的令牌。当前Windows Identity Foundation只支持单个令牌发布,因此返回的集合必须总是只包含单个实例。 /// </summary> /// <param name="principal">调用方的principal</param> /// <param name="request">进入的 RST,我们这里不用它</param> /// <param name="scope">由之前通过GetScope方法返回的范围</param> /// <returns></returns> protected override ClaimsIdentity GetOutputClaimsIdentity(ClaimsPrincipal principal, RequestSecurityToken request, Scope scope) { //返回一个默认声明集,里面了包含自己想要的声明 //这里你可以通过ClaimsPrincipal来验证用户,并通过它来返回正确的声明。 string identityName = principal.Identity.Name; string[] temp = identityName.Split('|'); ClaimsIdentity outgoingIdentity = new ClaimsIdentity(); outgoingIdentity.AddClaim(new Claim(ClaimTypes.Email, temp[0])); outgoingIdentity.AddClaim(new Claim(ClaimTypes.DateOfBirth, temp[1])); outgoingIdentity.AddClaim(new Claim(ClaimTypes.Name, temp[2])); SingleSignOnManager.RegisterRP(scope.AppliesToAddress); return outgoingIdentity; } /// <summary> /// 此方法返回用于令牌发布请求的配置。配置由Scope类表示。在这里,我们只发布令牌到一个由encryptingCreds字段表示的RP标识 /// </summary> /// <param name="principal"></param> /// <param name="request"></param> /// <returns></returns> protected override Scope GetScope(ClaimsPrincipal principal, RequestSecurityToken request) { // 使用request的AppliesTo属性和RP标识来创建Scope Scope scope = new Scope(request.AppliesTo.Uri.AbsoluteUri, this.signingCreds); if (Uri.IsWellFormedUriString(request.ReplyTo, UriKind.Absolute)) { if (request.AppliesTo.Uri.Host != new Uri(request.ReplyTo).Host) scope.ReplyToAddress = request.AppliesTo.Uri.AbsoluteUri; else scope.ReplyToAddress = request.ReplyTo; } else { Uri resultUri = null; if (Uri.TryCreate(request.AppliesTo.Uri, request.ReplyTo, out resultUri)) scope.ReplyToAddress = resultUri.AbsoluteUri; else scope.ReplyToAddress = request.AppliesTo.Uri.ToString(); } if (this.encryptingCreds != null) { // 如果STS对应多个RP,要选择证书指定到请求令牌的RP,然后再用 encryptingCreds scope.EncryptingCredentials = this.encryptingCreds; } else scope.TokenEncryptionRequired = false; return scope; } }
最后添加新类CustomSecurityTokenServiceConfiguration,继承SecurityTokenServiceConfiguration:
public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration { private static readonly object syncRoot = new object(); private const string CustomSecurityTokenServiceConfigurationKey = "CustomSecurityTokenServiceConfigurationKey"; public CustomSecurityTokenServiceConfiguration() : base(WebConfigurationManager.AppSettings[Common.IssuerName]) { this.SecurityTokenService = typeof(CustomSecurityTokenService); } public static CustomSecurityTokenServiceConfiguration Current { get { HttpApplicationState app = HttpContext.Current.Application; CustomSecurityTokenServiceConfiguration config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration; if (config != null) return config; lock (syncRoot) { config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration; if (config == null) { config = new CustomSecurityTokenServiceConfiguration(); app.Add(CustomSecurityTokenServiceConfigurationKey, config); } return config; } } } }
打开/Controllers/HomeController.cs,将Index()方法修改如下:
public ActionResult Index() { FederatedPassiveSecurityTokenServiceOperations.ProcessRequest( System.Web.HttpContext.Current.Request, User as ClaimsPrincipal, CustomSecurityTokenServiceConfiguration.Current.CreateSecurityTokenService(), System.Web.HttpContext.Current.Response); return View(); }
打开/Controllers/AccountController.cs,将Login(LoginModel model, string returnUrl)方法修改如下:
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Login(LoginModel model, string returnUrl) { var query = HttpUtility.ParseQueryString(Request.UrlReferrer.Query); if (model.UserName == "ojlovecd@csdn.net" && model.Password == "123456") { FormsAuthentication.SetAuthCookie("ojlovecd@csdn.net|1983-10-22|oujian", false); if (!string.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); return RedirectToAction("Index", "Home"); } return View(model); }
LogOff方法修改如下:
public ActionResult LogOff() { FormsAuthentication.SignOut(); ViewData["AddressesExpected"] = SingleSignOnManager.SignOut().Distinct().ToArray(); return View("Login"); }
打开/Views/Account/Login.cshtml,添加以下代码:
@{ ViewBag.Title = "登录"; var addressesExpected = ViewData["AddressesExpected"] as string[]; if (addressesExpected != null) { foreach (var address in addressesExpected) { <img src="@(address)?wa=wsignoutcleanup1.0" style="display:none;" /> } } }
OK,至此STS也已经完成了。把SiteA和STS都部署到IIS上,然后打开C:\Windows\System32\Drivers\etc\hosts文件,添加几个站点:
注意:更改host文件需要管理员权限,否则是改动不了的;这个更改的作用是:将域名指向的网址变成本地,有喜欢恶作剧的朋友可以把别人最喜欢的域名网站指向到本地或者其他网站等等,哈哈,不知道这个东西的人会懵比的,哈哈;
127.0.0.1 www.sitea.com 127.0.0.1 www.siteb.com 127.0.0.1 www.sitec.com 127.0.0.1 www.sited.com 127.0.0.1 www.sts.com
好了,在浏览器输入www.sitea.com,看看如何,它马上跳转到了www.sts.com的登录页面,输入ojlovecd@csdn.net,密码123456,确定,登录成功,跳回到了www.sitea.com,并显示出了用户名和Email:
点击退出,将注销当前用户,并跳转到登录页。
注意:以上是原博客中的原文,我在实践的过程中曾报出一个问题:
错误:X.509 证书 CN=localhost 不在被信任的人的存储中。 X.509 certificate CN=localhost 链生成失败。所使用的证书具有无法验证的信任链。请替换该证书或更改 certificateValidationMode。已处理证书链,但是在不受信任提供程序信任的根证书中终止;
开始看到这个问题是懵逼的,在网上搜索了好久都没找到答案,多般曲折,最终还是找到了,http://www.cnblogs.com/pangguoming/p/5833009.html。我将localhost证书导出,然后在导入到受信任的根证书颁发机构,文件名写CN=后面的东西,然后运行成功;
三、创建其它RP
OK,站点A搞定了,那其它站点如何呢?现在只是最简单的登录退出功能而已,说好的单点登录呢?
别急,接下来就一一实现。
新建基于.NET Framework4.5的MVC4程序,添加Microsoft.IdentityModel引用。修改web.config,configSections里添加如下节点:
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
Compilation里增加Microsoft.IdentityModel的程序集:
<compilation debug="true" targetFramework="4.5" > <assemblies> <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> </assemblies> </compilation>
身份验证改为None,添加authorization节点,禁止匿名用户访问:
<authentication mode="None"> </authentication> <authorization> <deny users="?" /> </authorization>
添加三个httpModules:
<httpModules> <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </httpModules> system.webServer里添加以下三个modules: <modules > <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" /> </modules>
最后增加microsoft.identityModel节点:
<microsoft.identityModel> <service> <audienceUris mode="Always"> <add value="http://www.siteb.com" /> </audienceUris> <federatedAuthentication> <wsFederation passiveRedirectEnabled="true" issuer="http://www.sts.com" realm="http://www.siteb.com" reply="http://www.siteb.com" requireHttps="false" /> <cookieHandler requireSsl="false" /> </federatedAuthentication> <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <trustedIssuers> <add thumbprint="FD1425A2F30937786F46E52E43B01AFD54E5D64D" name="http://www.sts.com" /> </trustedIssuers> </issuerNameRegistry> </service> </microsoft.identityModel>
以上配置跟SIteA差不多,只是WIF3.5和4.5的区别而已,在这里就不赘述了,要获取详细信息,请参考微软官方网站。
打开/Views/Home/Index.cshtml,将代码修改如下,在SiteB里我们显示Email和生日:
@using Microsoft.IdentityModel.Claims @{ ViewBag.Title = "SiteB主页"; ClaimsIdentity ci = User.Identity as ClaimsIdentity; if(ci!=null) { <h2>@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.Email).Value</h2> <h2>@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.DateOfBirth).Value</h2> } } <a href="http://www.sts.com/Account/LogOff">退出</a>
OK,部署到IIS上,然后运行,页面跳转到了sts的登录页面,输入用户名和密码,跳转,哎哟我去,怎么报错了:
原因是从sts返回来的数据里有<>这种标签,于是asp.net认为那是有危险的,于是抛出了异常,这个异常大家估计以前也碰到过,最简单粗暴的方法就是把验证请求的配置改为false,但这里我不建议这么干, 为此,我们专门用一个类来处理这种情况。
在SiteB目录下新建一个文件夹名为Services,然后添加一个类,名为SampleRequestValidator:
/// <summary> /// This SampleRequestValidator validates the wresult parameter of the /// WS-Federation passive protocol by checking for a SignInResponse message /// in the form post. The SignInResponse message contents are verified later by /// the WSFederationPassiveAuthenticationModule or the WIF signin controls. /// </summary> public class SampleRequestValidator : RequestValidator { protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex) { validationFailureIndex = 0; if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal)) { return true; } return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex); } }
然后在web.config里加入这个类的配置:
<httpRuntime targetFramework="4.5" requestValidationType="SiteC.Services.SampleRequestValidator" />
重新运行程序,非常完美:
这时候再打开SIteA,发现也已经处于了登录状态,这时候在SiteA点击退出,跳转到了登录页,再看看这时候的SiteB呢,刷新SiteB首页,发现也跳转到了登录页,证明在SiteA的退出操作对SiteB也起了作用,确实是单点登录了!
SiteC和SiteD的配置与SiteB类似,这里我就不重复了,留给大家自己练习一下,等所有的项目都配置好以后,在任意站点登录,发现其它站点也是登录状态;在任意站点退出,发现其它站点也已经退出。利用WIF,单点登录变的如此简单~~
当我按照上面的教程完成了之后,确实能够实现了单点登录,但是我产生了以下几个疑惑:
- sitea站点是如何去验证它有没有登录过,这其实也是在询问在sitea中配置web.config是如何生效的?
- 但sitea站点退出了登录状态,siteb站点刷新后也跟着退出了登录状态,但是假设我给siteb的cookie生命周期设置为一天(或者更久),此时再次在sitea站点进行退出操作,siteb是否还会退出?
- 教程中,退出后会跳转到sts登录页面,这个时候再次登录就不会在次进行跳转,sts登录后能够跳转的原因是因为有个returnUrl参数,而点击退出时如何把url参数也传过来呢?直接拼接url吗?这是个可行方案,但是我觉得这样做不是最佳方案!
- 我发现在创建其他RP网站的web.config配置和sitea站点的web.config配置不一致,一个用的system.IdentityModel,一个用的是micrsoft.IdentityModel,很奇怪 原博客作者( 我姓区不姓区 )为什么要这么做?于是我选择将siteb站点的webconfig配置得和sitea一样,结果siteb运行成功,sitea报错了,我暂时没有时间去验证,姑且先把问题记录在这里?
- 目前的代码还只是在本地电脑中进行,尚未在服务器中运行,会出现什么状况还不清楚,我猜测在服务器上应该就不能用localhost证书了吧,毕竟这个是本地测试证书;
本文来自博客园,作者:武韬君,转载请注明原文链接:https://www.cnblogs.com/JETSh/p/8583866.html