.net OWIN 实现 OAuth2.0

OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

网上有关OAuth的文章一大堆,这里就不多说了,直接进行项目配置

首先,在根目录创建Startup.cs 文件,代码如下:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Web.Http;

[assembly: OwinStartup(typeof(Currency.Startup))]
namespace Currency
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);
            ConfigureOAuth(app);
            //位置很关键
            app.UseWebApi(config);
        }
        public void ConfigureOAuth(IAppBuilder app)
        {
           
            var expires = 120;//单位分钟
            OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
            {
                AllowInsecureHttp = true,
                AuthorizeEndpointPath = new PathString("/oauth2/authorize"), //mvc 授权页面
                TokenEndpointPath = new PathString("/oauth2/token"),  //api 获取token
                AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(expires),
                //AuthorizationCodeProvider = new AuthorizationCodeProvider(),//授权码模式,暂不使用
                Provider = new SimpleAuthorizationServerProvider(),//认证策略
                RefreshTokenProvider = new SimpleRefreshTokenProvider(),//RefreshToken刷新token
                AccessTokenFormat = new MyJwtFormat() //token格式,使用jwt
            };

            // Token Generation
            app.UseOAuthAuthorizationServer(OAuthServerOptions);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());


            //配置cookie
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
            });
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
            app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

            // Enables the application to remember the second login verification factor such as phone or email.
            // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
            // This is similar to the RememberMe option when you log in.
            app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);


        }
    }


}

接下来,配置认证策略,创建 SimpleAuthorizationServerProvider.cs 文件,代码如下:

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using System.Web;

namespace Currency
{
    /// 
    /// 生成token
    /// 
    public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        /// 
        /// 验证clientID
        /// 
        /// 
        /// 
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {

            string clientId;
            string clientSecret;
            if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
            {
                context.TryGetFormCredentials(out clientId, out clientSecret);
            }

            if (context.ClientId == null)
            {
                context.SetError("invalid_clientId", "client_Id is not set");
                return Task.FromResult(null);
            }
            if (!string.IsNullOrEmpty(clientSecret))
            {
                context.OwinContext.Set("clientSecret", clientSecret);
            }


            //我这里是查询所有客户端列表,查找是否存在当前clientId
            var qlist = CommonSiteHelper.GetPublicQuotaList();


            var client = qlist.Where(m => m.LoginUrl == clientId || m.AppCode == clientId).FirstOrDefault();
            if (client != null)
            {
                context.Validated();
            }
            else
            {
                context.SetError("invalid_clientId", string.Format("Invalid client_id '{0}'", context.ClientId));
                return Task.FromResult(null);
            }
            return Task.FromResult(null);
        }
        /// 
        /// 验证账号密码
        /// 
        /// 
        /// 
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {


            UserInfoDal dal = new UserInfoDal();
            var umodel = new UserInfo()
            {
                UserAccount = context.UserName,
                UserPassword = context.Password
            };


            var qlist = CommonSiteHelper.GetPublicQuotaList();
            var client = qlist.Where(m => m.LoginUrl == context.ClientId || m.AppCode == context.ClientId).FirstOrDefault();
            if (client != null)
            {
                //登录操作,主要是下面的操作
                var jsonm = CommonSiteHelper.LoginSubmit(umodel, client);
                if (jsonm.status != 200)
                {
                    context.SetError("error", jsonm.msg);
                    return;
                }
                else
                {
                    //这里是关键,首先登录成功后获取用户对象
                    umodel = jsonm.data as UserInfo;
                    if (umodel != null)
                    {
                        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                        identity.AddClaim(new Claim(ClaimTypes.Name, umodel.NickName));
                        identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
                        identity.AddClaim(new Claim("sub", umodel.UserID));//这里是存储UserID,也可以储存你自己的内容
                        identity.AddClaim(new Claim("aud", context.ClientId));
                        identity.AddClaim(new Claim("portrait", umodel.Portrait));
                        identity.AddClaim(new Claim("useraccount", umodel.UserAccount));
                        
                        var props = new AuthenticationProperties(new Dictionary
                    {
                        {
                            "as:client_id", context.ClientId ?? string.Empty //这里存储客户端id,方便数据统计
                        },
                        {
                            "nickname", umodel.NickName    //这里存用户的基本信息
                        },
                        {
                            "portrait", FytRequest.GetUrlDomain()+ umodel.Portrait
                        }
                    });
                        var ticket = new AuthenticationTicket(identity, props);
                        
                        context.Validated(ticket);
                    }
                }
            }

        }
        /// 
        /// 生成 authorization_code(authorization code 授权方式)、生成 access_token (implicit 授权模式)
        /// 
        public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
        {
            if (context.AuthorizeRequest.IsImplicitGrantType)
            {
                //implicit 授权方式
                //var identity = new ClaimsIdentity("Bearer");
                //context.OwinContext.Authentication.SignIn(identity);
                //context.RequestCompleted();
            }
            else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
            {
                //authorization code 授权方式
                var redirectUri = context.Request.Query["redirect_uri"];
                var clientId = context.Request.Query["client_id"];
                var identity = new ClaimsIdentity(new GenericIdentity(
                    clientId, OAuthDefaults.AuthenticationType));

                var authorizeCodeContext = new AuthenticationTokenCreateContext(
                    context.OwinContext,
                    context.Options.AuthorizationCodeFormat,
                    new AuthenticationTicket(
                        identity,
                        new AuthenticationProperties(new Dictionary
                        {
                            {"client_id", clientId},
                            {"redirect_uri", redirectUri}
                        })
                        {
                            IssuedUtc = DateTimeOffset.UtcNow,
                            ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AuthorizationCodeExpireTimeSpan)
                        }));

                await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
                context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
                context.RequestCompleted();
            }
        }
        /// 
        /// 验证回调地址
        /// 
        /// 
        /// 
        public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
        {
            var qlist = CommonSiteHelper.GetPublicQuotaList();
            var client = qlist.Where(m => m.LoginUrl == context.ClientId || m.AppCode == context.ClientId).FirstOrDefault();
            if (client != null)
            {
                //我这里因为没有配置客户端回调,只验证了id,所以对客户端传过来的RedirectUri进行了检查
                if (!string.IsNullOrEmpty(context.RedirectUri))
                {
                    context.Validated(context.RedirectUri);
                }
                else
                {
                    context.Validated(client.HomeUrl);
                }
            }
            //return base.ValidateClientRedirectUri(context);
        }
        public override Task TokenEndpoint(OAuthTokenEndpointContext context)
        {
            foreach (KeyValuePair property in context.Properties.Dictionary)
            {
                context.AdditionalResponseParameters.Add(property.Key, property.Value);
            }

            return Task.FromResult(null);
        }

    }
} 
  

根据 refreshToken 刷新Token,添加 SimpleRefreshTokenProvider.cs ,代码如下:

using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace Currency
{
    /// 
    /// 根据 refreshToken 刷新Token
    /// 
    public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    {
        /// 
        /// 首次生成refreshToken
        /// 
        /// 
        /// 
        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var refreshTokenId = Guid.NewGuid().ToString("n");
            var expires = 120;
            var day = DateTime.Now.AddMinutes(expires);

            var token = new RefreshToken()
            {
                Id = refreshTokenId.GetHashCode(),
                Subject = context.Ticket.Identity.Name,
                IssuedUtc = DateTime.UtcNow,
                ExpiresUtc = DateTime.UtcNow.AddMinutes(expires)
            };

            context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
            context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;

            token.ProtectedTicket = context.SerializeTicket();
            var uCla = context.Ticket.Identity.Claims.FirstOrDefault(m => m.Type == "sub");
            var result = false;
            if (uCla != null)
            {
                token.Subject = uCla.Value;
                if (!RedisHelper.KeyExists("account:refreshTokenId:" + token.Id.ToString()))
                {
                    //这里存一下refreshToken
                    result = await RedisHelper.StringSetAsync("account:refreshTokenId:" + token.Id.ToString(), token);
                }
            }


            if (result)
            {
                context.SetToken(refreshTokenId);
            }


        }
        /// 
        /// 根据refreshToken刷新token
        /// 
        /// 
        /// 

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {

            var hashedTokenId = context.Token.GetHashCode();

            var result = false;
            //根据存储的refreshToken,刷新token,并且删除存储的refreshToken
            if (RedisHelper.KeyExists("account:refreshTokenId:" + hashedTokenId.ToString()))
            {
                var refreshToken = await RedisHelper.StringGetAsync("account:refreshTokenId:" + hashedTokenId.ToString());
                context.DeserializeTicket(refreshToken.ProtectedTicket);
                result = RedisHelper.KeyDelete("account:refreshTokenId:" + hashedTokenId.ToString());
            }
        }

        public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

        public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }

    }

    internal class RefreshToken
    {
        public RefreshToken()
        {
        }

        public int Id { get; set; }
        public string Subject { get; set; }
        public string Aud { get; set; }
        public DateTime IssuedUtc { get; set; }
        public DateTime ExpiresUtc { get; set; }
        public string ProtectedTicket { get; set; }
    }
}

返回的token格式,默认是有,但是我这里需要存储一下信息,所以用了jwt,创建 MyJwtFormat.cs,代码如下:

using Microsoft.Owin.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Currency
{
    public class MyJwtFormat : ISecureDataFormat
    {
        //用于从AuthenticationTicket中获取Audience信息
        private const string AudiencePropertyKey = "aud";
        private const string AudiencePropertySub = "sub";

        private readonly string _issuer = string.Empty;
        //Jwt的发布者和用于数字签名的密钥
        //public MyJwtFormat(string issuer)
        //{
        //    _issuer = issuer;
        //}

        public string Protect(AuthenticationTicket data)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }
            var expires = 120;

            //获取Audience名称及其信息
            //string audienceId = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ?
            //    data.Properties.Dictionary[AudiencePropertyKey] : null;
            string client_id = data.Properties.Dictionary.ContainsKey("client_id") ?
                data.Properties.Dictionary["client_id"] : null;
            //获取Audience名称及其信息
            var aCla = data.Identity.Claims.FirstOrDefault(m => m.Type == "aud");
            var sCla = data.Identity.Claims.FirstOrDefault(m => m.Type == "sub");
            var uCla = data.Identity.Claims.FirstOrDefault(m => m.Type == "useraccount");
            string audienceId = aCla != null ? aCla.Value : "";
            string subId = sCla != null ? sCla.Value : "";
            string UserAccount = uCla != null ? uCla.Value : "";

         
            if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience");

            //获取发布时间和过期时间
            var issued = data.Properties.IssuedUtc;
            var daySpan = TimeSpan.FromMinutes(expires);

            var issuedTime = issued.Value.DateTime;

            //这里是我需要存储的数据,可忽略
            SSOToken token = new SSOToken()
            {
                User = new SSOUser()
                {
                    UserId = subId,
                    UserAccount = UserAccount
                },
                Status = "oauth_login",
                TimeOut = expires * 60
            };

            JWTPlayloadInfo playload = new JWTPlayloadInfo
            {
                sub = subId,
                aud = audienceId,
                iat = Utils.FormatDate(issuedTime, "1"),
                daySpan = daySpan,
                data = token
            };

            var jwt = JWTHelper.GetToken(playload);
            return jwt;
        }

        public AuthenticationTicket Unprotect(string protectedText)
        {
            throw new NotImplementedException();
        }
    }
}

这里产生一个JWTHelper.cs,这个是自己写的jwt的帮助类,参考另一篇文章 https://blog.csdn.net/u013608482/article/details/99863233

接下来是授权页面,创建OAuth2Controller,创建Authorize

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using Currency.Common;
using Currency.Model;
using Microsoft.Owin.Security;

namespace Currency.Controllers
{
    public class OAuth2Controller : Controller
    {
        public IAuthenticationManager _AuthenticationManager
        {
            get
            {
                return HttpContext.GetOwinContext().Authentication;
            }
        }

        // GET: OAuth2
        [Authorize]    //这里加上验证,如果没有登录,会跳到Startup配置的LoginPath地址
        public ActionResult Authorize()
        {
            var ClientId = FytRequest.GetQueryString("client_id");
            var identity = this.User.Identity as ClaimsIdentity;
            identity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);
            var uCla = identity.Claims.FirstOrDefault(m => m.Type == "sub");
            var pCla = identity.Claims.FirstOrDefault(m => m.Type == "portrait");
            if (pCla != null)
            {
                ViewBag.Portrait = pCla.Value;
            }
            ViewBag.NickName = identity.Name;

            //我这里需要显示一下是哪个应用请求的
            var qlist = CommonSiteHelper.GetPublicQuotaList();
            var client = qlist.Where(m => m.LoginUrl == ClientId || m.AppCode == ClientId).FirstOrDefault();
            


            ViewBag.Client = client;


            if (Request.HttpMethod == "POST")
            {
                //这是前段点击授权的处理事件,
                identity = this.User.Identity as ClaimsIdentity;
                identity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);
                //这里是授权操作,会根据redirect_uri,会加上hash值 access_token、token_type、expires_in,进行回调
                _AuthenticationManager.SignIn(identity);
            }
            return View();
        }

    }
}

到此,服务端创建完成,接下来是客户端的调用

1、首先是api调用,/oauth2/token

.net OWIN 实现 OAuth2.0_第1张图片

 2、授权,访问

/oauth2/authorize?response_type=token&client_id=【client_id】&redirect_uri=【redirect_uri】

response_type 固定 token,其他根据现有条件进行配置,点击授权后,会重定向 redirect_uri

#access_token=eyJ0eXAiOiJKV1QiLCJhb.......Xiop0D4&token_type=bearer&expires_in=172800

你可能感兴趣的:(c#)