出售域名 11365.com.cn
有需要请联系 16826375@qq.com
在手机上浏览
在手机上浏览

IdentityServer4实现单点登录

发布日期:2020-08-25

       单点登录,顾名思义就是一处登录,处处登录的意思,比如我在OA登录了,我就可以在电商、论坛、博客等网站保持登录状态。可能对于一个需要用户的网站来说并不太受欢迎。但对于企业网站群来说,却是非常有用,谁也不愿意不断的重复输入相同的用户名和密码。
这里介绍用IdentityServer4来实现单点登录。

       首先要明白两个协议,即OAuth2.0和OpenID,它们是用来授权的,现在很多的第三方登录(微信登录等),就是采用的OAuth2.0协议,具体更详情的协议内容请自行百度。


一、    创建授权中心
1)新建一个项目SinGooCMS.Passports,做为授权服务器(授权中心),打开nuget管理器,添加IdentityServer4。

在Startup.cs文件中配置服务

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer(options => options.UserInteraction = new IdentityServer4.Configuration.UserInteractionOptions
    {
        LoginUrl = "/passports/login", //认证地址
        LogoutUrl = "/passports/logout", //登出地址
        LoginReturnUrlParameter = "returnUrl", //跳转的url参数名
        LogoutIdParameter = "logoutid"

    })
    .AddDeveloperSigningCredential()
    .AddInMemoryIdentityResources(Config.GetIdentityResources())
    .AddInMemoryClients(Config.GetClients());//把配置文件的Client配置资源放到内存

    // 配置cookie策略
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Lax; //宽松策略
    });

//...
}

需要特别注意的是,如果没有https环境,要把cookie配置成Lax(宽松)策略,否则不能写入cookie,造成认证无法跳转。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
    app.UseIdentityServer(); //使用id4
    app.UseCookiePolicy(); //使用上面的cookie宽松策略
//...
}

2)客户端配置

编写一个接受请求的客户端信息文件 Clients.json

[
  {
    "ClientID": "cmssite",
    "ClientName": "SinGooCMS内容管理系统",
    "LoginUrl": "http://localhost:5002/signin-oidc",
    "LogoutUrl": "http://localhost:5002/signout-callback-oidc"
  },
  {
    "ClientID": "client2",
    "ClientName": "客户端2",
    "LoginUrl": "http://localhost:5003/signin-oidc",
    "LogoutUrl": "http://localhost:5003/signout-callback-oidc"
  }
]

配置信息实例化

using System;
using System.Collections.Generic;
using IdentityServer4;
using IdentityServer4.Models;

namespace SinGooCMS.Passports
{
    public class Config
    {
        // scopes define the resources in your system
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
        }

        // clients want to access resources (aka scopes)
        public static IEnumerable<Client> GetClients()
        {
            string txt = System.IO.File.ReadAllText(SinGooBase.GetMapPath("/config/Clients.json"));
            var clients = txt.JsonToObject<List<ClientInfo>>();

            if (clients != null && clients.Count > 0)
            {
                List<Client> result = new List<Client>();
                foreach (var item in clients)
                {
                    result.Add(new Client()
                    {
                        ClientId = item.ClientID,
                        ClientName = item.ClientName,
                        AllowedGrantTypes = GrantTypes.Implicit,//隐式方式
                        ClientSecrets ={//私钥
                            new Secret("secret".Sha256())
                        },
                        RequireConsent = false,//如果不需要显示否同意授权 页面 这里就设置为false
                        RedirectUris = { item.LoginUrl },//登录成功后返回的客户端地址
                        PostLogoutRedirectUris = { item.LogoutUrl },//注销登录后返回的客户端地址

                        AllowedScopes =
                        {
                            IdentityServerConstants.StandardScopes.OpenId,
                            IdentityServerConstants.StandardScopes.Profile
                        }
                    });
                }

                return result;
            }
            else
                throw new Exception("找不到Clients的配置信息:/Config/Clients.json");
        }
    }

    public class ClientInfo
    {
        /// <summary>
        /// 客户端ID
        /// </summary>
        public string ClientID { get; set; }
        /// <summary>
        /// 客户端名称
        /// </summary>
        public string ClientName { get; set; }
        /// <summary>
        /// 认证登录返回客户端地址
        /// </summary>
        public string LoginUrl { get; set; }
        /// <summary>
        /// 登出返回客户端地址
        /// </summary>
        public string LogoutUrl { get; set; }
    }
}

3)编写登录与登出代码

using System;
using System.Text;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SinGooCMS.Application.Interface;
using SinGooCMS.Application.ViewModels;
using SinGooCMS.Domain.Interface;
using SinGooCMS.Utility;

namespace SinGooCMS.Passports.Controllers
{
    public class PassportsController : Controller
    {
        private readonly IUserRepository userRepository;
        private readonly IUserService userService;
        private readonly IIdentityServerInteractionService interaction;

        public PassportsController(
                IUserRepository _userRepository,
                IUserService _userService,
                IIdentityServerInteractionService _interaction
            )
        {
            this.userRepository = _userRepository;
            this.userService = _userService;
            this.interaction = _interaction;
        }

        /// <summary>
        /// 登录页面
        /// </summary>
        [HttpGet]
        public async Task<IActionResult> Login(string returnUrl = null)
        {
            ViewBag.RemeberUserName = CookieUtils.GetCookie("_remeberusername"); //记住的会员名称
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        [HttpGet]
        public async Task<IActionResult> Logout(string logoutId)
        {
            #region 登出并跳转
            
            var logout = await interaction.GetLogoutContextAsync(logoutId);
            await HttpContext.SignOutAsync();

            //获取客户端点击注销登录的地址
            var refererUrl = Request.Headers["Referer"].ToString();
            if (!string.IsNullOrWhiteSpace(refererUrl))
            {
                return Redirect(refererUrl);
            }
            else
            {
                //获取配置的默认的注销登录后的跳转地址
                if (logout.PostLogoutRedirectUri != null)
                {
                    return Redirect(logout.PostLogoutRedirectUri);
                }
            }
            return View();

            #endregion
        }

        /// <summary>
        /// 登录
        /// </summary>
        [HttpPost]
        public async Task<IActionResult> Login(IFormCollection form)
        {
            string returnUrl = WebUtils.GetFormString("returnUrl");
            ViewBag.ReturnUrl = returnUrl;
            bool boolIsRemeber = WebUtils.GetFormInt("_loginremeber").Equals(1); //记住会员名称
            var viewModel = new LoginViewModel()
            {
                UserName = WebUtils.GetFormString("_loginname"),
                Password = userRepository.GetEncodePwd(WebUtils.GetFormString("_loginpwd")),
                ValidateCode = WebUtils.GetFormString("_loginyzm")
            };

            var user = new SinGooCMS.Domain.Models.UserInfo();
            OperateResult loginResult = userRepository.UserLogin(viewModel.UserName, viewModel.Password, ref user);
            if (loginResult.ret == ResultType.Success)
            {
                //记住会员名称
                if (boolIsRemeber)
                    CookieUtils.SetCookie("_remeberusername", viewModel.UserName, 3600 * 24 * 365);

                AuthenticationProperties props = new AuthenticationProperties
                {
                    IsPersistent = true,
                    ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(1))
                };
                var idUser = new IdentityServerUser(user.AutoID.ToString()) { DisplayName = viewModel.UserName };
                await HttpContext.SignInAsync(idUser, props);

                if (!string.IsNullOrEmpty(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                else
                {
                    return Redirect("/");
                }
            }
            else if (loginResult.code == "MutilLoginFail")
                return Content("多次登录失败,5分钟内禁止登录!");
            else
                return Content("登录失败,账号或者密码错误!");
        }
    }
}

这里的核心代码是把登录的用户ID和用户名写进了cookie

AuthenticationProperties props = new AuthenticationProperties
{
    IsPersistent = true,
    ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(1))
};
var idUser = new IdentityServerUser(user.AutoID.ToString()) { DisplayName = viewModel.UserName };
await HttpContext.SignInAsync(idUser, props);

 

二、创建客户端(请求授权的网站)

1)同样在Nuget中引用IdentityServer4

2)在Startup.cs中配置服务

appsettings.json配置appsetting:

//是否统一(单点)登录
"IsUnificationLogin": "True",
//单点登录客户端ID
"OAuthClientID": "cmssite",
//单点登录客户端名称
"OAuthClientName": "singoocms内容管理系统",
public void ConfigureServices(IServiceCollection services)
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "Cookies";

        options.Authority = ConfigurtaionServices.GetAppSetting<string>("OAuthUrl");
        options.RequireHttpsMetadata = false;

        options.ClientId = ConfigurtaionServices.GetAppSetting<string>("OAuthClientID");
        options.SaveTokens = true;
    });

    // 配置cookie策略
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Lax;
    });

同样配置cookie为宽松策略,这些配置要写在server.AddControllersWithViews之前

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
    app.UseCookiePolicy();

//...

    app.UseRouting();

    app.UseAuthorization(); //统一认证 注意要写在app.UseRouting和app.UseEndpoints之间

    app.UseEndpoints(endpoints =>
}

3)请求登录

[HttpGet]
[Authorize] //测试的,将跳转到统一授权入口
public IActionResult Login()
{
    ViewBag.ReturnUrl = Context.AbsoluteUrl.IndexOf("?returnurl=") == -1
        ? ""
        : Context.AbsoluteUrl.Substring(Context.AbsoluteUrl.IndexOf("?returnurl=") + "?returnurl=".Length);

    if (ConfigurtaionServices.GetAppSetting<bool>("IsUnificationLogin"))
    {
        /*
            * 统一登录
            * 跳转到认证中心 passports/login 进行认证
            * 认证成功后 返回 UserID 和 UserName
            */
        if (HttpContext.User.Identity.IsAuthenticated)
        {
            var temp1 = HttpContext.User.Claims.Where(p => p.Type == "sub").FirstOrDefault();
            int userId = temp1 == null ? -1 : temp1.Value.ToInt(-1);
            var temp2 = HttpContext.User.Claims.Where(p => p.Type == "name").FirstOrDefault();
            string userName = temp2 == null ? "游客" : temp2.Value;

            int expire = 0;
            switch (Context.SiteConfig.CookieTime) //如果为空时效为 "浏览器关闭即失效":
            {
                case "一周": //系统默认为1周
                    expire = 7 * 24 * 60;
                    break;
                case "一年":
                    expire = 365 * 24 * 60;
                    break;
            }

            CookieUtils.SetCookie("singoocms_uid", userId.ToString(), expire);
            CookieUtils.SetCookie("singoocms_username", HttpUtility.UrlEncode(userName), expire);

            if (string.IsNullOrEmpty(ViewBag.ReturnUrl))
                return Redirect("/member/infocenter");
            else
                return Redirect(ViewBag.ReturnUrl);
        }
        else
        {
            return Content("认证失败,请联系管理员");
        }
    }
    else
    {
        //本地登录
        ViewBag.RemeberUserName = CookieUtils.GetCookie("_remeberusername"); //记住的会员名称                
        ViewBag.Thirdlogin = Plugin.ThirdLogin.OAuthConfig.Load();

        return View("user/登录.cshtml");
    }
}

在方法加上[Authorize]特性,登录将跳转到授权服务器。为了兼容本地系统,把返回的id,user写入到cookie。

 

三、授权流程

“请求授权网站”和“授权服务器”都已经创建完成了,我们可以做个测试,

在项目调试选项卡中,分别设置“授权服务器”网址为“http://localhost:5000”,设置“请求授权网站”网址为“http://localhost:5002”,并在解决方案中设置同时启动。

1)输入http://localhost:5002/user/login 触发【Authorize】认证请求

2)跳转到登录窗口(用户授权)

3)登录界面

登录成功后,又有许多步骤,跳转到原网站。

登出和登录是一样的步骤,这里就不多说了,大家可以亲自跑一遍!