IdentityServer4实现单点登录统一认证

Posted butterflyeffect

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IdentityServer4实现单点登录统一认证相关的知识,希望对你有一定的参考价值。

什么是单点登录同一认证?
多个应用共用一套登录系统,用户只需要在其中一个应用登录就获得了ids4授权,无需多次登录其它应用。
技术图片

下面介绍简单的demo实现:
1.数据库
新建database为blogcore,然后在下面新建Admin表,表结构如下图所示
技术图片

2.搭建IdentityServer4服务端
准备工具: VS2017以上、.net core2.1
新建项目技术图片
名字为IdentityS4
技术图片
依赖项-管理NuGet程序包,找到IdentityServer4包,选择版本为2.2.0
技术图片

设置该项目地址为:http://localhost:5000
新建一个cs文件为Config代码如下:

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()
        {
            return new List<Client>
            {
                // OpenID Connect隐式流客户端(MVC)
                new Client
                {
                    ClientId = "mvc",
                    ClientName = "MVC Client",
                    AllowedGrantTypes = GrantTypes.Implicit,//隐式方式
                    RequireConsent=false,//如果不需要显示否同意授权 页面 这里就设置为false
                    RedirectUris = { "http://localhost:5002/signin-oidc" },//登录成功后返回的客户端地址
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },//注销登录后返回的客户端地址

                    AllowedScopes =//下面这两个必须要加吧 不太明白啥意思
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                    }
                }
            };
        }
    }

在Startup.cs的ConfigureServices方法中注入Ids4服务,如下面部分代码:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<EFContext>(options=>options.UseSqlServer(Configuration.GetConnectionString("conn")));//注入DbContext
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

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

            services.AddTransient<IAdminService,AdminService>();//service注入
        }

在Startup.cs的Configure方法中添加ids4服务中间件(注意要放在UseMvc之前就可以):

app.UseIdentityServer();

至此ids4配置好,接下来是数据库连接,添加DbContext类名字为:EFContext.cs

public class EFContext : DbContext
    {
        public EFContext(DbContextOptions<EFContext> options) : base(options)
        {

        }

        #region 实体集

        public DbSet<Admin> Admin { get; set; }//注意 这里这个Blog不能写成Blogs否则会报错找不到Blogs 因为我们现在数据库和表是现成的 这里就相当于实体对应的数据库是Blog

        #endregion
    }

添加一个Admin.cs的实体类,对应数据库里面的用户表Admin

public class Admin
    {
        public int Id { get; set; }
        public DateTime CreateDate { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Remark { get; set; }
    }

接下来写service层,.net core为依赖注入,所以要以接口的形式在service层
新建一个接口:IAdminService.cs 代码如下:

public interface IAdminService : IBaseService<Admin>
    {
        Task<Admin> GetByStr(string username, string pwd);
    }

新建实现该接口的类AdminService.cs 代码如下:

public class AdminService: BaseService<Admin>, IAdminService
    {
        public AdminService(EFContext _efContext)
        {
            db = _efContext;
        }

        public async Task<Admin> GetByStr(string username, string pwd)
        {
            Admin m=await db.Admin.Where(a => a.UserName == username && a.Password == pwd).SingleOrDefaultAsync();
            if (m!=null)
            {
                return m;
            }
            else
            {
                return null;
            }
        }
    }

BaseService代码如下:

public class BaseService<T> where T : class, new()
    {
        public EFContext db;

        /// <summary>
        /// 增
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public async Task<bool> Add(T entity)
        {
            await db.Set<T>().AddAsync(entity);
            return db.SaveChanges() > 0;
        }
        /// <summary>
        /// 删
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public async Task<bool> Del(T entity)
        {
            db.Entry(entity).State = EntityState.Deleted;
            int i = await db.SaveChangesAsync();
            return i > 0;
        }
        /// <summary>
        /// 改
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public async Task<bool> Edit(T entity)
        {
            db.Entry(entity).State = EntityState.Modified;
            int i = await db.SaveChangesAsync();
            return i > 0;
        }
        /// <summary>
        /// 查-根据传进来的lambda表达式查询一条数据
        /// </summary>
        /// <param name="whereLambda"></param>
        /// <returns></returns>
        public async Task<T> Get(System.Linq.Expressions.Expression<Func<T, bool>> whereLambda)
        {
            T t = await db.Set<T>().Where(whereLambda).SingleOrDefaultAsync();
            return t;
        }

        /// <summary>
        /// 查询多条数据-根据传进来的lambda和排序的lambda查询
        /// </summary>
        /// <typeparam name="s"></typeparam>
        /// <param name="pageIndex"></param>
        /// <param name="pageSize"></param>
        /// <param name="whereLambda"></param>
        /// <param name="orderbyLambda"></param>
        /// <param name="isAsc"></param>
        /// <returns></returns>
        public async Task<List<T>> GetList<s>(int pageIndex, int pageSize,
            System.Linq.Expressions.Expression<Func<T, bool>> whereLambda,
            System.Linq.Expressions.Expression<Func<T, s>> orderbyLambda, bool isAsc)
        {
            var temp = db.Set<T>().Where(whereLambda);
            List<T> list = null;
            if (isAsc)//升序
            {
                list = await temp.OrderBy(orderbyLambda).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
            }
            else//降序
            {
                list = await temp.OrderByDescending(orderbyLambda).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
            }
            return list;
        }

        /// <summary>
        /// 获取总条数
        /// </summary>
        /// <param name="whereLambda"></param>
        /// <returns></returns>
        public async Task<int> GetTotalCount(Expression<Func<T, bool>> whereLambda)
        {
            return await db.Set<T>().Where(whereLambda).CountAsync();
        }
    }

IBaseService代码如下:

public interface IBaseService<T> where T : class, new()
    {
        //增
        Task<bool> Add(T entity);


        //删
        Task<bool> Del(T entity);

        //改
        Task<bool> Edit(T entity);

        //查
        Task<T> Get(System.Linq.Expressions.Expression<Func<T, bool>> whereLambda);


        //查询分页
        Task<List<T>> GetList<s>(int pageIndex, int pageSize, System.Linq.Expressions.Expression<Func<T, bool>> whereLambda, System.Linq.Expressions.Expression<Func<T, s>> orderbyLambda, bool isAsc);

        //获取总条数
        Task<int> GetTotalCount(Expression<Func<T, bool>> whereLambda);
    }

在Startup.cs的ConfigureServices方法中注入 service层 上面已经注入过...
在配置文件appsettings.json中添加数据库连接字符串如下代码:

{
  "ConnectionStrings": { "conn": "server=.;database=blogcore;uid=sa;pwd=123456" }, 
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

接下来做统一认证服务的登录页,新增一个控制器AccountController,代码如下:

public class AccountController : Controller
    {
        private IAdminService _adminService;//自己写的操作数据库Admin表的service
        private readonly IIdentityServerInteractionService _interaction;
        private readonly IClientStore _clientStore;
        private readonly IAuthenticationSchemeProvider _schemeProvider;
        private readonly IEventService _events;
        public AccountController(IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IAuthenticationSchemeProvider schemeProvider,
            IEventService events,
            IAdminService adminService)
        {
            _interaction = interaction;
            _clientStore = clientStore;
            _schemeProvider = schemeProvider;
            _events = events;
            _adminService = adminService;
        }

        /// <summary>
        /// 登录页面
        /// </summary>
        [HttpGet]
        public async Task<IActionResult> Login(string returnUrl=null)
        {
            ViewData["returnUrl"] = returnUrl;
            return View();
        }

        /// <summary>
        /// 登录post回发处理
        /// </summary>
        [HttpPost]
        public async Task<IActionResult> Login(string userName, string password,string returnUrl=null)
        {
            ViewData["returnUrl"] = returnUrl;
            Admin user = await _adminService.GetByStr(userName, password);
            if (user!=null)
            {
                AuthenticationProperties props= new AuthenticationProperties
                {
                    IsPersistent = true,
                    ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(1))
                };
                await HttpContext.SignInAsync(user.Id.ToString(), user.UserName, props);
                if (returnUrl!=null)
                {
                    return Redirect(returnUrl);
                }

                return View();
            }
            else
            {
                return View();
            }
        }
    }

Login视图代码如下:


@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
</head>
<body>

    <div align="center">
        <h1>统一认证登录中心</h1>
        <form action="/Account/Login" method="post">
            用户名:<input type="text" name="userName" /><br />
            密 码:<input type="password" name="password" /><input type="hidden" name="returnUrl" value="@ViewData["returnUrl"]" /> <br />
            <input type="submit" value="登录" />
        </form>
    </div>
</body>
</html>

至此,IdentityServer4服务端的工作完成,接下来我们要开始建客户端了,也就是需要保护的MVC网站

3.搭建客户端
新建一个名为 MvcClient 的ASP.Net Core Web应用程序
技术图片
把地址设置为:http://localhost:5002

在Startup.cs的ConfigureServices方法中添加如下部分代码(主要用来配置认证中心ids4的及自己作为客户端的认证信息):

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 = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;

                    options.ClientId = "mvc";
                    options.SaveTokens = true;
                });


            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

在 Configure方法中添加这行代码(需要在UseMvc之前就可以):

app.UseAuthentication();

到此,客户端跟统一认证的信息就配置完了。接下来我们把Home控制器上面加上需要验证的标志:[Authorize]
技术图片

我们把默认的Index视图页面html代码去掉,改成如下(主要用来显示下授权后拿到的用户信息):

@{
    ViewData["Title"] = "Home Page";
}

<div align="center"><h1>这里是受保护的客户端首页</h1></div>
<h3>User claims</h3>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>

    }
</dl>

到此,客户端的工作也做完了,下面我们要开始启动项目了,设置项目为多项目启动:解决方案上右键-属性
技术图片

现在我们启动项目:服务器项目和 客户端都运行了,但是客户端会直接跳转到服务端登录页面
比如我们如果访问受保护的5002端口资源,如果用户没有登录会直接跳到5000端口的统一认证服务登录页
技术图片
技术图片
技术图片

以上是关于IdentityServer4实现单点登录统一认证的主要内容,如果未能解决你的问题,请参考以下文章

单点登录让多系统统一认证不再难

统一认证授权及单点登录的技术选择

springboot实现用户统一认证管理(单点登录)

IdentityServer4目录

SSO单点登录统一身份认证系统

SSO统一身份认证——CAS Client客户端创建(九)