我认为对于一个普遍问题,必有对应的一个简洁优美的解决方案。当然这也许只是我的一厢情愿,因为根据宇宙法则,所有事物总归趋于混沌,而OAuth协议就是混沌中的产物,不管是1.、.0a还是2.,单看版本号就让人神伤。

对接过各类开放平台的朋友对OAuth应该不会陌生。当年我小试了下淘宝API,各种token、key、secret、code、id,让我眼花缭乱,不明所以,虽然最终调通,但那种照猫画虎的感觉颇不好受。最近公司计划,开放接口的授权协议从1.0升到2.,这个任务不巧就落在了我的头上。

声明:我并没有认真阅读过OAuth2.0协议规范,本文对OAuth2.0的阐述或有不当之处,请谅解。本文亦不保证叙述的正确性,欢迎指正。 

OAuth2.0包含四种角色:

用户,又叫资源所有者
客户端,俗称第三方商户
授权服务端,颁发AccessToken
资源服务端,根据AccessToken开放相应的资源访问权限
本文涉及到三种授权模式: Authorization Code模式:这是现在互联网应用中最常见的授权模式。客户端引导用户在授权服务端输入凭证获取用户授权(AccessToken),进而访问用户资源。需要注意的是,在用户授权后,授权服务端先回传客户端授权码,然后客户端再使用授权码换取AccessToken。为什么不直接返回AccessToken呢?主要是由于用户授权后,授权服务端重定向到客户端地址,此时数据只能通过QueryString方式向客户端传递,在地址栏URL中可见,不安全,于是分成了两步,第二步由客户端主动请求获取最终的令牌。
Client Credentials Flow:客户端乃是授权服务端的信任合作方,不需要用户参与授权,事先就约定向其开放指定资源(不特定于用户)的访问权限。客户端通过证书或密钥(或其它约定形式)证明自己的身份,获取AccessToken,用于后续访问。
Username and Password Flow:客户端被用户和授权服务端高度信任,用户直接在客户端中输入用户名密码,然后客户端传递用户名密码至授权服务端获取AccessToken,便可访问相应的用户资源。这在内部多系统资源共享、同源系统资源共享等场景下常用,比如单点登录,在登录时就获取了其它系统的AccessToken,避免后续授权,提高了用户体验。
上述模式涉及到三类凭证: AuthorizationCode:授权码,授权服务端和客户端之间传输。
AccessToken:访问令牌,授权服务端发给客户端,客户端用它去到资源服务端请求资源。
RefreshToken:刷新令牌,授权服务端和客户端之间传输。
对客户端来说,授权的过程就是获取AccessToken的过程。 总的来说,OAuth并没有新鲜玩意,仍是基于加密、证书诸如此类的技术,在OAuth出来之前,这些东东就已经被大伙玩的差不多了。OAuth给到我们的最大好处就是统一了流程标准,一定程度上促进了互联网的繁荣。 我接到任务后,本着善假于物的理念,先去网上搜了一遍,原本以为有很多资源,结果只搜到DotNetOpenAuth这个开源组件。更让人失望的是,官方API文档没找到(可能是我找的姿势不对,有知道的兄弟告知一声),网上其它资料也少的可怜,其间发现一篇 OAuth2学习及DotNetOpenAuth部分源码研究 ,欣喜若狂,粗粗浏览一遍,有收获,却觉得该组件未免过于繁杂(由于时间紧迫,我并没有深入研究,只是当前观点)。DotNetOpenAuth包含OpenID、OAuth1.[a]/2.0,自带的例子有几处暗坑,不易(能)调通。下面介绍我在搭建基于该组件的OAuth2.0授权框架时的一些心得体会。 本文介绍的DotNetOpenAuth乃是对应.Net4.0的版本。 授权服务端 授权服务端交道打的最多的就是客户端,于是定义一个Client类,实现DotNetOpenAuth.OAuth2.IClientDescription接口,下面我们来看IClientDescription的定义: public interface IClientDescription { Uri DefaultCallback { get; } //0:有secret 1:没有secret
ClientType ClientType { get; } //该client的secret是否为空
bool HasNonEmptySecret { get; } //检查传入的callback与该client的callback是否一致
bool IsCallbackAllowed(Uri callback); //检查传入的secret与该client的secret是否一致
bool IsValidClientSecret(string secret);
}
其中隐含了许多信息。DefaultCallback表示客户端的默认回调地址(假如有的话),在接收客户端请求时,使用IsCallbackAllowed判断回调地址是否合法(比如查看该次回调地址和默认地址是否属于同一个域),过滤其它应用的恶意请求。若ClientType 为0,则表示客户端需持密钥(secret)表明自己的身份,授权服务端可以据此赋予此类客户端相对更多的权限。自定义的Client类一般需要多定义一个ClientSecret属性。DefaultCallback和ClientSecret在下文常有涉及。 DotNetOpenAuth预定义了一个接口——IAuthorizationServerHost,这是个重要的接口,定义如下: public interface IAuthorizationServerHost
{
ICryptoKeyStore CryptoKeyStore { get; }
INonceStore NonceStore { get; } AutomatedAuthorizationCheckResponse CheckAuthorizeClientCredentialsGrant(IAccessTokenRequest accessRequest);
AutomatedUserAuthorizationCheckResponse CheckAuthorizeResourceOwnerCredentialGrant(string userName, string password, IAccessTokenRequest accessRequest);
AccessTokenResult CreateAccessToken(IAccessTokenRequest accessTokenRequestMessage);
IClientDescription GetClient(string clientIdentifier);
bool IsAuthorizationValid(IAuthorizationDescription authorization);
}
简单地说,CryptoKeyStore用于存取对称加密密钥,用于授权码和刷新令牌的加密,由于客户端不需要对它们进行解密,所以密钥只存于授权服务端;关于AccessToken的传输则略有不同,关于这点我们待会说。理解NonceStore 属性需要知道 N once和 Timestamp的概念,Nonce与消息合并加密可防止重放攻击,Timestamp是为了避免可能的Nonce重复问题,也将一同参与加密,具体参看 nonce和timestamp在Http安全协议中的作用 ;这项技术放在这里主要是为了确保一个授权码只能被使用一次。 CheckAuthorizeClientCredentialsGrant方法在客户端凭证模式下使用,CheckAuthorizeResourceOwnerCredentialGrant在用户名密码模式下使用,经测试,IsAuthorizationValid方法只在授权码模式下被调用,这三个方法的返回值标示是否通过授权。 当授权通过后,通过CreateAccessToken生成AccessToken并返回给客户端,客户端于是就可以用AccessToken访问资源服务端了。那当资源服务端接收到AccessToken时,需要做什么工作呢?首先,它要确认这个AccessToken是由合法的授权服务端颁发的,否则,攻击者就能使用DotNetOpenAuth另外建一个授权服务端,后果可想而知。说到身份认证,最成熟的就是RSA签名技术,即授权服务端私钥对AccessToken签名,资源服务端接收后使用授权服务端的公钥验证。我们还可以使用 资源服务器公/私钥对来加解密AccessToken(签名在加密后),这对于OAuth2.0来说没任何意义,而是为OAuth1.0服务的。 public AccessTokenResult CreateAccessToken(IAccessTokenRequest accessTokenRequestMessage)
{
var accessToken = new AuthorizationServerAccessToken();
int minutes = ;
string setting = ConfigurationManager.AppSettings["AccessTokenLifeTime"];
minutes = int.TryParse(setting, out minutes) ? minutes : ;//10分钟
accessToken.Lifetime = TimeSpan.FromMinutes(minutes); //这里设置加密公钥
//accessToken.ResourceServerEncryptionKey = new RSACryptoServiceProvider();
//accessToken.ResourceServerEncryptionKey.ImportParameters(ResourceServerEncryptionPublicKey); //签名私钥,这是必须的(在后续版本中可以设置accessToken.SymmetricKeyStore替代)
accessToken.AccessTokenSigningKey = CreateRSA(); var result = new AccessTokenResult(accessToken);
return result;
}
前面说了,所有授权模式都是为了获取AccessToken,授权码模式和用户名密码模式还有个RefreshToken,当然授权码模式独有Authorization Code。一般来说,这三个东西,对于客户端是一个经过加密编码的字符串,对于服务端是可序列化的对象,存储相关授权信息。需要注意的是客户端证书模式没有RefreshToken,这是为什么呢?我们不妨想想为什么授权码模式和用户名密码模式有个RefreshToken,或者说RefreshToken的作用是什么。以下是我个人推测: 首先要明确,AccessToken一般是不会永久有效的。因为,AccessToken并没有承载可以验证客户端身份的完备信息,并且资源服务端也不承担验证客户端身份的职责,一旦AccessToken被他人获取,那么就有可能被恶意使用。失效机制有效减少了产生此类事故可能造成的损失。当AccessToken失效后,需要重新获取。对于授权码模式和用户名密码模式来说,假如没有RefreshToken,就意味这需要用户重新输入用户名密码进行再次授权。如果AccessToken有效期够长,比如几天,倒不觉得有何不妥,有些敏感应用只设置数分钟,就显得不够人性化了。为了解决这个问题,引入RefreshToken,它会在AccessToken失效后,在不需要用户参与的情况下,重新获取新的AccessToken,这里有个前提就是RefreshToken的有效期(如果有的话)要比AccessToken长,可设为永久有效。那么,RefreshToken泄露了会带来问题吗?答案是不会,除非你同时泄露了客户端身份凭证。需要同时具备RefreshToken和客户端凭证信息,才能获取新的AccessToken,我们甚至可以将旧的AccessToken当作RefreshToken。同理可推,由于不需要用户参与授权,在客户端证书模式下,客户端在AccessToken失效后只需提交自己的身份凭证重新请求新AccessToken即可,根本不需要RefreshToken。 授权码模式,用户授权后(此时并生成返回AccessToken,而是返回授权码),授权服务端要保存相关的授权信息,为此定义一个ClientAuthorization类: public class ClientAuthorization
{
public int ClientId { get; set; } public string UserId { get; set; } public string Scope { get; set; } public DateTime? ExpirationDateUtc { get; set; }
}
ClientId和UserId就不说了,Scope是授权范围,可以是一串Uri,也可以是其它标识,只要后台代码能通过它来判断待访问资源是否属于授权范围即可。ExpirationDateUtc乃是授权过期时间,即当该时间到期后,需要用户重新授权(有RefreshToken)也没用,为null表示永不过期。 资源服务端 在所有的授权模式下,资源服务端都只专注一件和OAuth相关的事情——验证AccessToken。这个步骤相对来说就简单很多,以Asp.net WebAPI为例。在此之前建议对Asp.net WebAPI消息拦截机制不熟悉的朋友浏览一遍 ASP.NET Web API之消息[拦截]处理 。这里我们新建一个继承自DelegatingHandler的类作为例子: public class BearerTokenHandler : DelegatingHandler
{
/// <summary>
/// 验证访问令牌合法性,由授权服务器私钥签名,资源服务器通过对应的公钥验证
/// </summary>
private static readonly RSAParameters AuthorizationServerSigningPublicKey = new RSAParameters();//just a 例子 private RSACryptoServiceProvider CreateAuthorizationServerSigningServiceProvider()
{
var authorizationServerSigningServiceProvider = new RSACryptoServiceProvider();
authorizationServerSigningServiceProvider.ImportParameters(AuthorizationServerSigningPublicKey);
return authorizationServerSigningServiceProvider;
} protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Headers.Authorization != null)
{
if (request.Headers.Authorization.Scheme == "Bearer")
{
var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(this.CreateAuthorizationServerSigningServiceProvider(), null));
var principal = resourceServer.GetPrincipal(request);//可以在此传入待访问资源标识参与验证
HttpContext.Current.User = principal;
Thread.CurrentPrincipal = principal;
}
} return base.SendAsync(request, cancellationToken);
}
}
需要注意,AccessToken乃是从头信息Authorization获取,格式为“Bearer:AccessToken”,在下文“ 原生方式获取AccessToken ”中有进一步描述。ResourceServer.GetPrincipal方法使用授权服务端的公钥验证AccessToken的合法性,同时解密AccessToken,若传入参数有scope,则还会判断scope是否属于授权范围内,通过后将会话标识赋给当前会话,该会话标识乃是当初用户授权时的用户信息,这样就实现了用户信息的传递。一般来说若返回的principal为null,就可以不必执行后续逻辑了。 客户端 可以认为DotNetOpenAuth.OAuth2.Client是DotNetOpenAuth给C#客户端提供的默认SDK。我们以授权码模式为例。先声明一个IAuthorizationState接口对象,IAuthorizationState接口是用来保存最终换取AccessToken成功后授权服务端返回的信息,其部分定义如下: public interface IAuthorizationState {
Uri Callback { get; set; }
string RefreshToken { get; set; }
string AccessToken { get; set; }
DateTime? AccessTokenIssueDateUtc { get; set; }
DateTime? AccessTokenExpirationUtc { get; set; }
HashSet<string> Scope { get; }
}
AccessTokenExpirationUtc是AccessToken过期时间,以Utc时间为准。若该对象为null,则表示尚未授权,我们需要去授权服务端请求。 private static AuthorizationServerDescription _authServerDescription = new AuthorizationServerDescription
{
TokenEndpoint = new Uri(MvcApplication.TokenEndpoint),
AuthorizationEndpoint = new Uri(MvcApplication.AuthorizationEndpoint),
}; private static WebServerClient _client = new WebServerClient(_authServerDescription, "democlient", "samplesecret"); [HttpPost]
public ActionResult Index()
{
if (Authorization == null)
{
return _client.PrepareRequestUserAuthorization().AsActionResult();
}
return View();
}
AuthorizationServerDescription包含两个属性,AuthorizationEndpoint是用户显式授权的地址,一般即用户输用户名密码的地;TokenEndpoint是用授权码换取AccessToken的地址,注意该地址须用POST请求。“democlient”和“samplesecret”是示例用的客户端ID和客户端Secret。WebServerClient.PrepareRequestUserAuthorization方法将会首先返回code和state到当前url,以querystring的形式(若用户授权的话)。 code即是授权码,state参数不好理解,这涉及到CSRF,可参看浅谈CSRF攻击方式,state就是为了预防CSRF而引入的随机数。客户端生成该值,将其附加到state参数的同时,存入用户Cookie中,用户授权完毕后,该参数会同授权码一起返回到客户端,然后客户端将其值同Cookie中的值比较,若一样则表示该次授权为当前用户操作,视为有效。由于不同域的cookie无法共享,因此其它站点并不能知道state的确切的值,CSRF攻击也就无从谈起了。简单地说,state参数起到一个标示消息是否合法的作用。结合获取授权码这步来说,授权服务端返回的url为 http://localhost:22187/?code=xxxxxxxxx&state=_PzGpfJzyQI9DkdoyWeWr 格式,若忽略state,那么攻击方将code替换成自己的授权码,最终客户端获取的AccessToken是攻击方的AccessToken,由于AccessToken同用户关联,也就是说,后续客户端做的其实是另一个用户资源(也许是攻击方注册的虚拟用户),如果操作中包括新增或更新,那么真实用户信息就会被攻击方获取到。可参看 OAuth2 Cross Site Request Forgery, and state parameter 。 有了code就可以去换取AccessToken了: public ActionResult Index(string code,string state)
{
if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(state))
{
var authorization = _client.ProcessUserAuthorization(Request);
Authorization = authorization;
return View(authorization);
}
return View();
}
如前所述,Authorization不为null即表示整个授权流程成功完成。然后就可以用它来请求资源了。 public ActionResult Invoke()
{
var request = new HttpRequestMessage(new HttpMethod("GET"), "http://demo.openapi.cn/bookcates");
using (var httpClient = new HttpClient(_client.CreateAuthorizingHandler(Authorization)))
{
using (var resourceResponse = httpClient.SendAsync(request))
{
ViewBag.Result = resourceResponse.Result.Content.ReadAsStringAsync().Result;
}
}
return View(Authorization);
}
WebServerClient.CreateAuthorizingHandler方法返回一个DelegatingHandler,主要用来当AccessToken过期时,使用RefreshToken刷新换取新的AccessToken;并设置Authorization头信息,下文有进一步说明。 原生方式获取AccessToken 既然是开放平台,面对的客户端种类自然多种多样,DotNetOpenAuth.OAuth2.Client显然就不够用了,我也不打算为了这个学遍所有程序语言。所幸OAuth基于http,不管任何语言开发的客户端,获取AccessToken的步骤本质上就是提交http请求和接收http响应的过程,客户端SDK只是将这个过程封装得更易用一些。下面就让我们以授权码模式为例,一窥究竟。 参照前述事例,当我们第一次(新的浏览器会话)在客户端点击“请求授权”按钮后,会跳转到授权服务端的授权界面。 可以看到,url中带了client_id、redirect_uri、state、response_type四个参数,若要请求限定的授权范围,还可以传入scope参数。其中response_type设为code表示请求的是授权码。 private string GetNonCryptoRandomDataAsBase64(int binaryLength)
{
byte[] buffer = new byte[binaryLength];
_random.NextBytes(buffer);
string uniq = Convert.ToBase64String(buffer);
return uniq;
} public ActionResult DemoRequestCode()
{
string xsrfKey = this.GetNonCryptoRandomDataAsBase64();//生成随机数
string url = MvcApplication.AuthorizationEndpoint + "?" +
string.Format("client_id={0}&redirect_uri={1}&response_type={2}&state={3}",
"democlient", "http://localhost:22187/", "code", xsrfKey);
HttpCookie xsrfKeyCookie = new HttpCookie(XsrfCookieName, xsrfKey);
xsrfKeyCookie.HttpOnly = true;
xsrfKeyCookie.Secure = FormsAuthentication.RequireSSL;
Response.Cookies.Add(xsrfKeyCookie); return Redirect(url);
}
授权码返回后,先检查state参数,若通过则换取AccessToken: private bool VerifyState(string state)
{
var cookie = Request.Cookies[XsrfCookieName];
if (cookie == null)
return false; var xsrfCookieValue = cookie.Value;
return xsrfCookieValue == state;
} private AuthenticationHeaderValue SetAuthorizationHeader()
{
string concat = "democlient:samplesecret";
byte[] bits = Encoding.UTF8.GetBytes(concat);
string base64 = Convert.ToBase64String(bits);
return new AuthenticationHeaderValue("Basic", base64);
} public ActionResult Demo(string code, string state)
{
if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(state) && VerifyState(state))
{
var httpClient = new HttpClient();
var httpContent = new FormUrlEncodedContent(new Dictionary<string, string>()
{
{"code", code},
{"redirect_uri", "http://localhost:22187/"},
{"grant_type","authorization_code"}
});
httpClient.DefaultRequestHeaders.Authorization = this.SetAuthorizationHeader(); var response = httpClient.PostAsync(MvcApplication.TokenEndpoint, httpContent).Result;
Authorization = response.Content.ReadAsAsync<AuthorizationState>().Result;
return View(Authorization);
}
return View();
}
如上所示,以Post方式提交,三个参数,code即是授权码,redirect_uri和获取授权码时传递的redirect_uri要保持一致,grant_type设置为“authorization_code”。注意 SetAuthorizationHeader方法 ,需要设置请求头的Authorization属性,key为“Basic”,值为以Base64编码的“客户端ID:客户端Secret”字符串, 至于为何要如此规定,暂时没有探究 。 成功后返回的信息可以转为前面说的IAuthorizationState接口对象。 如前所述,当AccessToken过期后,需要用RefreshToken刷新。 private void RefreshAccessToken()
{
var httpClient = new HttpClient();
var httpContent = new FormUrlEncodedContent(new Dictionary<string, string>()
{
{"refresh_token", Authorization.RefreshToken},
{"grant_type","refresh_token"}
});
httpClient.DefaultRequestHeaders.Authorization = this.SetAuthorizationHeader(); var response = httpClient.PostAsync(MvcApplication.TokenEndpoint, httpContent).Result;
Authorization = response.Content.ReadAsAsync<AuthorizationState>().Result;
}
其中grant_type须设置为”refresh_token”,请求头信息设置同前。 获取AccessToken后, 就可以用于访问用户资源了。 public ActionResult DemoInvoke()
{
var httpClient = new HttpClient();
if (this.Authorization.AccessTokenExpirationUtc.HasValue && this.Authorization.AccessTokenExpirationUtc.Value < DateTime.UtcNow)
{
this.RefreshAccessToken();
}
var bearerToken = this.Authorization.AccessToken; httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
var request = new HttpRequestMessage(new HttpMethod("GET"), "http://demo.openapi.cn/bookcates");
using (var resourceResponse = httpClient.SendAsync(request))
{
ViewBag.Result = resourceResponse.Result.Content.ReadAsStringAsync().Result;
}
return View(Authorization);
}
用法很简单, Authorization 请求头,key设为“Bearer”,值为AccessToken即可。

C#搭建Oauth2.0认证流程以及代码示例的更多相关文章

  1. 使用Owin中间件搭建OAuth2.0认证授权服务器

    前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OA ...

  2. Owin中间件搭建OAuth2.0认证授权服务体会

    继两篇转载的Owin搭建OAuth 2.0的文章,使用Owin中间件搭建OAuth2.0认证授权服务器和理解OAuth 2.0之后,我想把最近整理的资料做一下总结. 前两篇主要是介绍概念和一个基本的D ...

  3. [2014-11-11]使用Owin中间件搭建OAuth2.0认证授权服务器

    前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OA ...

  4. OAuth2.0认证流程是如何实现的?

    导读 大家也许都有过这样的体验,我们登录一些不是特别常用的软件或网站的时候可以使用QQ.微信或者微博等账号进行授权登陆.例如我们登陆豆瓣网的时候,如果不想单独注册豆瓣网账号的话,就可以选择用微博或者微 ...

  5. Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务

    在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka ...

  6. 使用DotNetOpenAuth搭建OAuth2.0授权框架——Demo代码简单说明

    前段时间随意抽离了一部分代码作为OAuth2的示例代码,若干处会造成困扰,现说明如下: public class OAuthController : Controller { private stat ...

  7. Oauth2.0认证流程

  8. Spring Security OAuth2.0认证授权一:框架搭建和认证测试

    一.OAuth2.0介绍 OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容. 1.s ...

  9. 使用DotNetOpenAuth搭建OAuth2.0授权框架

    标题还是一如既往的难取. 我认为对于一个普遍问题,必有对应的一个简洁优美的解决方案.当然这也许只是我的一厢情愿,因为根据宇宙法则,所有事物总归趋于混沌,而OAuth协议就是混沌中的产物,不管是1.0. ...

随机推荐

  1. AviSynth AVS Importer Plugin for Adobe Premiere Pro CC 2015 x64

    Premiere CS AVS Importer x64.prm copy to Adobe\Adobe Premiere Pro CC 2015\Plug-Ins\Common\ VSFilterM ...

  2. 012_k8s专题系列一之进入容器日常op

    一.下面列出如何进入正在运行的k8s容器 <1> kubectl get pods #查看所有正在运行的pod NAME READY STATUS RESTARTS AGE nginx-5 ...

  3. Linq与Lambda常用查询语法

    1.查询全部 2.按条件查询全部 3.去除重复 4.连接查询    between and 5.排序 6.分组

  4. chrome:禁用缓存

    F12->Network 非常好用!!!!!!!!!!

  5. AMQP消息队列之RabbitMQ简单示例

    前面一篇文章讲了如何快速搭建一个ActiveMQ的示例程序,ActiveMQ是JMS的实现,那这篇文章就再看下另外一种消息队列AMQP的代表实现RabbitMQ的简单示例吧.在具体讲解之前,先通过一个 ...

  6. django.db.utils.OperationalError: (1045, "Access denied for user 'ODBC'@'localhost' (using password)

    错误描述: 从SQLLITE数据库换为MYSQL数据库,执行 python manage.py migrate 命令时,报错:django.db.utils.OperationalError: (10 ...

  7. [C]关于extern与struct

    问题 我曾经很困惑,就是在两个编译单元当中,如何把一个单元中声明的struct结构引入到另外一个单元中来,折腾了很久,后来发现这位大神的留言 不是这么用的…… 类型的定义和类型变量的定义不同,类型定义 ...

  8. 面向对象(metaclass继承高级用法)

    方法一:# class MyType(type):# def __init__(self,*args,**kwargs):# print('132')# super(MyType,self).__in ...

  9. CSS入门(二)

    一.组合选择器 每个选择器位可以是任意基础选择器或选择器组合 1.群组选择器 可以一次性控制多个选择器 选择器之间用逗号(,)隔开 div,.d1,#div{ color:red; } 2.子代(后代 ...

  10. 《Oracle DBA工作笔记:运维、数据迁移与性能调优》 PDF 下载

    一:下载途径 二:本书图样 三:本书目录 第1篇 数据库运维篇第1章 数据库安装配置1.1 安装前的准备 11.2 安装数据库软件 51.2.1 方法1:OUI安装 61.2.2 方法2:静默安装 8 ...