• 一次曲折的单点集成之旅


    原有的系统是mvc 4.6的,要加一个简单的单点系统。经简单比较好,决定选用ids3做service。
    集成的方法直接看官方的示例即可:https://github.com/IdentityServer/IdentityServer3.Samples

    改造涉及到的要点有:

    • 使用授权码+PCKE码校验 (有点坑)
    • 本地已有用户密码验证
    • 定制登录页面,欢迎页面
    • nginx反向代理的坑

    第1点搜一下,还是能找到资料的。
    第2,3点直接看示例代码即可。
    第4点真的是一言难尽,以下是详细的跳坑经历。

    由于后台服务是部署在IIS上,再通过nginx通过反向代理,配置https证书。
    一开始我以为是没办法通过https代理https,所以只是在nignx的启用https。
    但这里会有一个很矛盾的地方,整体的站点是https的,但IIS里没有启动用https,直接启用RequireSsl = true会报错。
    但如果不设置ssl启用,则客户端登录访问的授权点与验证点不一致,直接报404错误。
    嗯……头大。

    解决办法:
    iis配置好https,使用相同的域名,如果是相同服务器,则使用不同的端口,如1443。
    nginx配置https,使用upstream的方式进行反向代理,而不是直接反向代理至服务器端口。
    配置如下:
    `
    upstream portal_server {
    server 127.0.0.1:1443;
    }

    server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name xxx.lennon.cn;

    # SSL
    ssl_certificate        D:/nginx/conf/xxx.lennon.cn.pem;
    ssl_certificate_key  D:/nginx/conf/xxx.lennon.cn.key;
    include default/ssl.conf;
    
    location / {
        proxy_pass https://test1_server;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme; #实际的协议 http还 https
        proxy_next_upstream error timeout http_404 http_403;
    }
    # index.php
    index index.html index.htm index.php;
    

    }
    `

    带pcke的授权码验证方式:
    `
    using System;
    using System.Collections.Generic;
    using IdentityModel.Client;
    using Microsoft.Owin.Security;
    using Owin;
    using Qianchen.Application.Organization;
    using System.Configuration;
    using System.Linq;
    using System.Net.Http;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web.Helpers;
    using IdentityModel;
    using Microsoft.IdentityModel.Protocols;
    using Microsoft.Owin;
    using Microsoft.Owin.Security.Cookies;
    using Microsoft.Owin.Security.Notifications;
    using Microsoft.Owin.Security.OpenIdConnect;
    using System.Security.Claims;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;

    namespace Lennon.Application.Auth
    {
    public static class LennonAuthExtension
    {
    private static string OIDC_ClientId = ConfigurationManager.AppSettings["oidc:ClientId"];
    private static string OIDC_ClientSecret = ConfigurationManager.AppSettings["oidc:ClientSecret"];
    private static string OIDC_Authority = ConfigurationManager.AppSettings["oidc:Authority"];
    private static string OIDC_RedirectUri = ConfigurationManager.AppSettings["oidc:RedirectUri"];
    private static string OIDC_PostLogoutRedirectUri = ConfigurationManager.AppSettings["oidc:PostLogoutRedirectUri"];
    private static string OIDC_ResponseType = ConfigurationManager.AppSettings["oidc:ResponseType"];
    private static string OIDC_RequireHttpsMeta = ConfigurationManager.AppSettings["oidc:RequireHttpsMeta"];
    private static string OIDC_Scope = ConfigurationManager.AppSettings["oidc:Scope"];

        private static string OIDC_RequestTokenUrl = ConfigurationManager.AppSettings["oidc:Authority"] + "/connect/token";
        private static string OIDC_RequestUserinfoUrl = ConfigurationManager.AppSettings["oidc:Authority"] + "/connect/userinfo";
        private static string APPID = ConfigurationManager.AppSettings["APPID"];
        private static IdentityUserBLL identity = new IdentityUserBLL();
        private static UserIBLL userBLL = new UserBLL();
    
        /// <summary>
        /// 用于保存数据
        /// 只能读取一次,读取即删除
        /// </summary>
        private static Dictionary<string, string> UserAuthenticationDic = new Dictionary<string, string>();
        /// <summary>
        /// 只能读取一次,读取即删除
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        private static string GetAuthenticationValue(string key)
        {
            if (UserAuthenticationDic.ContainsKey(key))
            {
                var val = UserAuthenticationDic[key];
                UserAuthenticationDic.Remove(key);
                return val;
            }
            else
            {
                return null;
            }
        }
    
        /// <summary>
        /// 保存登录过程中的key
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        private static void SetAuthenticationValue(string key, string value)
        {
            if (UserAuthenticationDic.ContainsKey(key))
            {
                UserAuthenticationDic.Remove(key);
            }
            UserAuthenticationDic.Add(key, value);
        }
        private static void RememberCodeVerifier(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n, string codeVerifier)
        {
            var properties = new AuthenticationProperties();
            properties.Dictionary.Add("cv", codeVerifier);
    
            string key = GetCodeVerifierKey(n.ProtocolMessage.State);
            string value = Convert.ToBase64String(Encoding.UTF8.GetBytes(n.Options.StateDataFormat.Protect(properties)));
            SetAuthenticationValue(key, value);
    
            n.Options.CookieManager.AppendResponseCookie(
                n.OwinContext,
                key,
                value,
                new CookieOptions
                {
                    //SameSite = SameSiteMode.None,
                    HttpOnly = true,
                    Secure = n.Request.IsSecure,
                    Expires = DateTime.UtcNow + n.Options.ProtocolValidator.NonceLifetime
                });
        }
    
        private static string RetrieveCodeVerifier(AuthorizationCodeReceivedNotification n)
        {
            string key = GetCodeVerifierKey(n.ProtocolMessage.State);
            string codeVerifier = GetAuthenticationValue(key);
    
            string codeVerifierCookie = n.Options.CookieManager.GetRequestCookie(n.OwinContext, key);
            if (codeVerifierCookie != null)
            {
                var cookieOptions = new CookieOptions
                {
                    //SameSite = SameSiteMode.None,
                    HttpOnly = true,
                    Secure = n.Request.IsSecure
                };
    
                n.Options.CookieManager.DeleteCookie(n.OwinContext, key, cookieOptions);
    
                var cookieProperties = n.Options.StateDataFormat.Unprotect(Encoding.UTF8.GetString(Convert.FromBase64String(codeVerifierCookie)));
                cookieProperties.Dictionary.TryGetValue("cv", out codeVerifier);
            }
    
            return codeVerifier;
        }
        private static string GetCodeVerifierKey(string state)
        {
            using (var hash = SHA256.Create())
            {
                return OpenIdConnectAuthenticationDefaults.CookiePrefix + "cv." + Convert.ToBase64String(hash.ComputeHash(Encoding.UTF8.GetBytes(state)));
            }
        }
    
    
        public static void UseGMDIAuthentication(this IAppBuilder app)
        {
            AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
    
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions()
            {
                AuthenticationType = "Cookies",
            });
    
            //默认不需要Https
            bool requireHttpMeta;
            if (!bool.TryParse(OIDC_RequireHttpsMeta, out requireHttpMeta))
            {
                requireHttpMeta = false;
            }
    
            app.UseOpenIdConnectAuthentication(new Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions
            {
                SignInAsAuthenticationType = Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationDefaults.AuthenticationType,
                Authority = OIDC_Authority, // 建议通过配置文件读取            
                ClientId = OIDC_ClientId, // 向单点登录服务注册时分配的客户端 Id
                ClientSecret = OIDC_ClientSecret,
                RedirectUri = OIDC_RedirectUri, // 回调地址
                PostLogoutRedirectUri = OIDC_PostLogoutRedirectUri,
                ResponseType = OIDC_ResponseType,
                Scope = OIDC_Scope, // 根据实际要请求的资源服务API设置,如果不需要请求其它资源服务API则保持不变
                RequireHttpsMetadata = requireHttpMeta,
                UsePkce = true,
                UseTokenLifetime = false,
                //RedeemCode = true,
                //SaveTokens = true,
                Notifications = new Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = async n =>
                    {
                        if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
                        {
                            // generate code verifier and code challenge
                            var codeVerifier = CryptoRandom.CreateUniqueId(32);
    
                            string codeChallenge;
                            using (var sha256 = SHA256.Create())
                            {
                                var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
                                codeChallenge = Base64Url.Encode(challengeBytes);
                            }
    
                            // set code_challenge parameter on authorization request
                            n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
                            n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
                            RememberCodeVerifier(n, codeVerifier);
                        }
                    },
                    AuthorizationCodeReceived = async context =>
                    {
    
                        var client = new HttpClient();
                        //var disco = await client.GetDiscoveryDocumentAsync(OIDC_Authority);
                        //if (disco.IsError)
                        //    throw new Exception(disco.Error);
    
                        var codeVerifier = RetrieveCodeVerifier(context);
    
                        // attach code_verifier on token request
                        //context.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
    
                        var req = new AuthorizationCodeTokenRequest
                        {
                            Address = OIDC_RequestTokenUrl,//disco.TokenEndpoint
                            ClientId = OIDC_ClientId,
                            ClientSecret = OIDC_ClientSecret,
                            Code = context.Code,
                            RedirectUri = OIDC_RedirectUri,
                            // optional PKCE parameter
                            CodeVerifier = codeVerifier
                        };
                        var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(req);
                        if (tokenResponse != null && !tokenResponse.IsError)
                        {
                            var userreq = new UserInfoRequest
                            {
                                Address = OIDC_RequestUserinfoUrl,//disco.UserInfoEndpoint
                                Token = tokenResponse.AccessToken
                            };
    
                            var userInfoResponse = await client.GetUserInfoAsync(userreq);
    
                            if (userInfoResponse.IsError)
                                throw new Exception(userInfoResponse.Error);
    
                            // create a new identity using the claims from the user info endpoint (including tokens)
                            var claims = userInfoResponse.Claims;
    
    
                            var account = claims.FirstOrDefault(x => x.Type == "preferred_username");
                            if (account != null)
                            {
                                var loginAccount = account.Value;
    
                            }
    
                            var authuser = GetAuthUser(claims);
    
                            //先检查是否存在,再保存
                            SaveUser(authuser);
                            //然后模拟登录
                            AutoLogin(authuser);
    
                            #region 使用页面也登录
                            var id = new ClaimsIdentity(OIDC_ResponseType);
                            id.AddClaims(userInfoResponse.Claims);
                            id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                            id.AddClaim(new Claim("id_token", tokenResponse.IdentityToken));
                            //id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
                            id.AddClaim(new Claim("email", authuser.email));
                            id.AddClaim(new Claim("preferred_username", authuser.preferredusername));
                            id.AddClaim(new Claim("sub", authuser.preferredusername));
                            id.AddClaim(new Claim("name", authuser.name));
                            context.AuthenticationTicket = new AuthenticationTicket(new ClaimsIdentity(id.Claims, AuthenticationTypes.Password, "name", "role"),
                                new AuthenticationProperties { IsPersistent = true }); 
                            #endregion
                        }
                    },
                }
            });
        }
        private static void AutoLoginQC(AuthUserQCModel authUser)
        {
            if (authUser == null) return;
            //本地模拟登录
        }
        private static void AutoLogin(AuthUserModel authUser)
        {
            if (authUser == null) return;
    
            OperatorHelper.Instance.AddLoginUser(authUser.sub, APPID, null);
            var userInfo = new UserInfo();
            var loginAccount = authUser.name;
            identity.IdentityLogin(ref userInfo, ref loginAccount);
        }
    
        public static void SaveUser(AuthUserModel authUser)
        {
        }
        public static AuthUserModel GetAuthUser(IEnumerable<Claim> claims)
        {
            AuthUserModel user = null;
            if (claims != null)
            {
                user = new AuthUserModel();
                user.sub = getClaimsValue(claims, "sub");
                user.name = getClaimsValue(claims, "name");
                user.given_name = getClaimsValue(claims, "given_name");
                user.departmentId = getClaimsValue(claims, "departmentId");
                user.departmentName = getClaimsValue(claims, "departmentName");
                user.email = getClaimsValue(claims, "email");
                user.post = getClaimsValue(claims, "post");
                user.rank = getClaimsValue(claims, "rank");
                user.officeType = getClaimsValue(claims, "officeType");
            }
    
            return user;
        }
        public static AuthUserQCModel GetQCAuthUser(IEnumerable<Claim> claims)
        {
            AuthUserQCModel user = null;
            if (claims != null)
            {
                user = new AuthUserQCModel();
                user.preferredusername = getClaimsValue(claims, "preferred_username");
                user.name = getClaimsValue(claims, "name");
                user.nickname = getClaimsValue(claims, "nickname");
                user.gender = getClaimsValue(claims, "gender");
                user.phonenumber = getClaimsValue(claims, "phonenumber");
                user.email = getClaimsValue(claims, "email");
            }
    
            return user;
        }
        public static string getClaimsValue(IEnumerable<Claim> claims, string key)
        {
            if (claims != null && !string.IsNullOrEmpty(key))
            {
                var claim = claims.FirstOrDefault(x => x.Type.Equals(key, StringComparison.OrdinalIgnoreCase));
                if (claim != null)
                {
                    return claim.Value;
                }
            }
    
            return "";
        }
    }
    public class AuthUserQCModel
    {
        /// <summary>
        /// sub
        /// </summary>
        public string preferredusername { get; set; }
        /// <summary>
        /// 工号
        /// </summary>
        public string name { get; set; }
        /// <summary>
        /// 姓名
        /// </summary>
        public string nickname { get; set; }
        /// <summary>
        /// 性别
        /// </summary>
        public string gender { get; set; }
    
        /// <summary>
        /// 邮箱
        /// </summary>
        public string email { get; set; }
        /// <summary>
        /// 职位
        /// </summary>
        public string phonenumber { get; set; }
    
    }
    

    }

    `

  • 相关阅读:
    mac单机 k8s minikube ELK yaml 详细配置 踩坑
    springboot es 配置, ElasticsearchRepository接口使用
    Docker 搭建 ELK 日志记录
    空杯心态
    与友人谈
    mac单机, jenkins-master在集群k8s外, k8s内部署动态jenkins-slave, jnlp方式. 踩坑+吐血详细总结
    Anyproxy 代理前端请求并mock返回 二次开发 持续集成
    Oracle 设置TO_DATE('13-OCT-20', 'dd-MON-yy'), 报错 ORA-01843: 无效的月份
    allure-java 二次开发 添加自定义注解, 并修改@step相关aop问题
    Appium添加Listener运行报错
  • 原文地址:https://www.cnblogs.com/seamusic/p/15586199.html
Copyright © 2020-2023  润新知