1. 程式人生 > 其它 >一次曲折的單點整合之旅

一次曲折的單點整合之旅

原有的系統是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; }

}

}

`