IdentityServer4+.net core+Docker认证授权和单点登录
操作环境:Centos7.8+.net Core3.1+Docker
由于IdentityServer4的认证授权功能太过强大和复杂,实现了OAuth2.0的四种授权模式——隐式模式(implicit)、授权码模式(Authorization Code)、密码凭证模式(Resource Owner Password Credentials)、客户端凭证模式(Client Credentials),本文仅以其中的客户端凭证模式和隐式模式,实现两种不同场景作为示例,供大家参考学习。
一、客户端凭证模式——实现接口资源认证授权
示例场景:Web应用(MVC)请求微服务接口资源(WebAPI)需要通过idetityserver认证中心授权(采用客户端凭证模式实现)
项目整体结构:

1、认证中心IdentityServer项目
使用Nuget引入IdentityServer4组件

创建Config配置类
using IdentityServer4;
using IdentityServer4.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace HPM.IdentityServer4
{
public class Config
{
/// <summary>
/// 获取自定义API资源列表
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources()
{
List<ApiResource> apiResources = new List<ApiResource>();
apiResources.Add(new ApiResource() { Name = "ProductMicroService", DisplayName = "产品微服务",Scopes = { "ProductMicroService" } });//定义资源名称
apiResources.Add(new ApiResource() { Name = "UserMicroService", DisplayName = "用户微服务", Scopes = { "UserMicroService" } });//定义资源名称
return apiResources;
}
/// <summary>
/// 获取自定义客户端配置列表
/// 支持 授权码,隐藏式,密码式,凭证式、混合式
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients()
{
List<Client> clients = new List<Client>();
//客户端凭证授权模式(用于客户端访问微服务API资源授权认证)
clients.Add(new Client()
{
ClientId="client",//定义客户端获取Token时指定的client_id值
AllowedGrantTypes=GrantTypes.ClientCredentials,//指定grant_type为client_credentials客户端授权模式
ClientSecrets = {new Secret("Secret".Sha256())},//定义客户端获取token时指定的client_secret值
AllowOfflineAccess = true,//如果要获取refresh_tokens ,必须把AllowOfflineAccess设置为true
AccessTokenLifetime = 3600,//token有效期,默认3600秒
AllowedScopes = { "ProductMicroService", "UserMicroService" }//设置可访问范围的资源名称
});return clients;
}
/// <summary>
/// 获取IdentityServer4自身API资源列表
/// </summary>
/// <returns></returns>
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile() };
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope> { new ApiScope("ProductMicroService"), new ApiScope("UserMicroService") };
}
}
}
修改StartUp类
在ConfigureServices方法中添加IdentityServer的依赖注入
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiScopes(Config.GetApiScopes())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
在Configure方法中启用IdentityServer中间件及认证授权
// 配置管道中间件
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//暂时禁用Https
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
//身份认证中间件(身份验证必须在授权的前面)
app.UseAuthentication();
//授权中间件
app.UseAuthorization();
//这个必须在UseRouting和UseEndpoints中间。如果IdentityServer服务端和API端要写在一起,
//那么这个必须在UseAuthorization和UseAuthentication的上面。
//通过访问https://localhost:端口/.well-known/openid-configuration默认配置地址可以查看IdentityServer提供的endpoint
app.UseIdentityServer();//使用IdentityServer中间件
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
2、微服务ProductMicroService接口资源项目(其他微服务同样调整)
修改StartUp类
在ConfigureServices方法中添加IdentityServer的认证授权依赖注入
//指定使用IdentityServer作为API资源的授权模式
services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options => {
options.Authority = "https://www.aaa.vip:5050";//设置IdentityServer授权端口地址
options.ApiName = "ProductMicroService";//设置要开放访问的资源名称
options.RequireHttpsMetadata = false;
});
在Configure方法中启用认证授权中间件
//身份验证中间件 (身份验证必须在授权的前面)
app.UseAuthentication();
//授权验证中间件
app.UseAuthorization();
然后在需要通过授权验证才能访问的API加上[Authorize]特性保护起来

3、Web应用客户端项目
Web客户端应用想要请求到微服务中带有[Authorize]特性标识的API资源,则必须在请求的同时,提供IdentityServer认证中心颁发的令牌,因此最好封装一个获取Token令牌的方法,如下
public async Task<string> GetAccessToken(string client_id,string client_secret,string scope)
{
//获取Redis中缓存的Token
string accessToken = _redisService.GetTokenValue(scope);
//string accessToken = _redisService.GetValue(scope + "_access_token");
if (string.IsNullOrEmpty(accessToken))
{
//重新从Identityserver中获取token
var tokenClient = new HttpClient();
var tokenResponse = await tokenClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = "https://www.aaa.vip:5100" + "/connect/token",
ClientId = client_id,
ClientSecret = client_secret,
Scope = scope
});
accessToken = tokenResponse.AccessToken;
//设置redis中的token字符串600秒过期
_redisService.SetTokenValue(scope, accessToken);
//_redisService.SetValue(scope + "_access_token", accessToken, 600);
}
////获取Refresh_Token
//tokenResponse = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
//{
// Address = identityServerUrl + "connect/token",
// ClientId = client_id,
// ClientSecret = client_secret,
// Scope = scope
//});
return accessToken;
}
其中client_id、client_secret、scope参数分别是IdentityServer项目中配置类Config中定义的Client客户端信息,讲这些客户端ID和密钥信息保持一致传入即可获取AccessToken授权令牌。
然后在请求微服务接口资源时,将令牌带入请求头部即可,如下

二、隐式模式——实现单点登录
首先要想使用IdentityServer实现单点登录,必须要满足一个条件,那就是应用站点和认证站点必须是HTTPS,重要的事情说三遍,必须是HTTPS!必须是HTTPS!必须是HTTPS!否则登录认证回调环节会有问题,过不去。
至于怎么搭建.net core的HTTPS站点,我在之后其他的文章中会讲解,并不复杂,主要是要花钱,没有氪金心理准备的就别玩IdentityServer的单点登录了。
示例场景:多个Web应用(mvc)通过请求IdentityServer认证中心实现单点登录(本文暂时仅以单个客户端应用进行演示)
项目整体结构:

1、认证中心IdentityServer项目
使用Nuget引入IdentityServer4组件

创建Config配置类
using IdentityServer4;
using IdentityServer4.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace HPM.IdentityServer4
{
public class Config
{
/// <summary>
/// 获取自定义客户端配置列表
/// 支持 授权码,隐藏式,密码式,凭证式、混合式
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients()
{
List<Client> clients = new List<Client>();
//隐藏式授权模式(用于多客户端单点登录)
clients.Add(new Client()
{
ClientId = "hpm_mvc_imp",
ClientName = "hpm",
AllowedGrantTypes = GrantTypes.Implicit,
//设置是否显示授权提示界面
//RequireConsent=true,
//指定允许令牌或授权码返回的地址(URL)-登录成功后重定向地址
//RedirectUris = { "http://www.b.net:5001/signin-oidc", "http://www.a.cn:5002/signin-oidc" },
RedirectUris = { "https://www.aaa.vip:5050/signin-oidc" },
//指定允许注销后返回的地址(URL),这里写两个客户端-注销成功后的重定向地址
//PostLogoutRedirectUris = { "http://www.b.net:5001/signout-callback-oidc", "http://www.a.cn:5002/signout-callback-oidc" },
PostLogoutRedirectUris = { "https://www.aaa.vip:5050/signout-callback-oidc" },
ClientSecrets = { new Secret("SSOSecret".Sha256()) },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
}
});
return clients;
}
/// <summary>
/// 获取IdentityServer4自身API资源列表
/// </summary>
/// <returns></returns>
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile() };
}
}
}
修改StartUp类
在ConfigureServices方法中添加IdentityServer的依赖注入
#region 添加IdentityServer单点登录依赖注入,要使用IdentityServer的单点登录,则必须启用https
//我们使用的IdentityModel这个组件,它默认限制了当 Ids4 非localhost地址时,必须启用HTTPS。详情见https://ask.csdn.net/questions/753220
//同时谷歌浏览器,对于cookies的写入,也必须要求https
services.AddIdentityServer(options =>
{
//可以通过此设置来指定登录路径,默认的登陆路径是/account/login
options.UserInteraction.LoginUrl = "/Account/Login";//【必备】登录地址
options.UserInteraction.LogoutUrl = "/Account/Logout";//【必备】退出地址
//options.UserInteraction.ConsentUrl = "/Account/Consent";//【必备】允许授权同意页面地址
//options.UserInteraction.ErrorUrl = "/Account/Error";//【必备】错误页面地址
options.UserInteraction.LoginReturnUrlParameter = "ReturnUrl";//【必备】设置传递给登录页面的返回URL参数的名称。默认为returnUrl
options.UserInteraction.LogoutIdParameter = "logoutId"; //【必备】设置传递给注销页面的注销消息ID参数的名称。缺省为logoutId
//options.UserInteraction.ConsentReturnUrlParameter = "ReturnUrl"; //【必备】设置传递给同意页面的返回URL参数的名称。默认为returnUrl
//options.UserInteraction.ErrorIdParameter = "errorId"; //【必备】设置传递给错误页面的错误消息ID参数的名称。缺省为errorId
//options.UserInteraction.CustomRedirectReturnUrlParameter = "ReturnUrl"; //【必备】设置从授权端点传递给自定义重定向的返回URL参数的名称。默认为returnUrl
//options.UserInteraction.CookieMessageThreshold = 5; //【必备】由于浏览器对Cookie的大小有限制,设置Cookies数量的限制,有效的保证了浏览器打开多个选项卡,一旦超出了Cookies限制就会清除以前的Cookies值
}).AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients());
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{ // all your options
options.Cookie.HttpOnly = false;
// in dev
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
//options.Cookie.SecurePolicy = CookieSecurePolicy.None;
//谷歌浏览器对SameSite有安全要求,否则无法写入Cookie,详情见https://stackoverflow.com/questions/51912757/identity-server-is-keep-showing-showing-login-user-is-not-authenticated-in-c
//options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SameSite = SameSiteMode.Lax;
});
#endregion
在Configure方法中启用IdentityServer中间件及认证授权
// 配置管道中间件
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//暂时禁用Https
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
//身份认证中间件(身份验证必须在授权的前面)
app.UseAuthentication();
//授权中间件
app.UseAuthorization();
//这个必须在UseRouting和UseEndpoints中间。如果IdentityServer服务端和API端要写在一起,
//那么这个必须在UseAuthorization和UseAuthentication的上面。
//通过访问https://localhost:端口/.well-known/openid-configuration默认配置地址可以查看IdentityServer提供的endpoint
app.UseIdentityServer();//使用IdentityServer中间件
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
创建登录及注销控制器,其中/Account/Login和/Account/Logout分别是StartUp的ConfigureServices中给IdentityServer设定好的登入和登出时自动回调的地址(即MVC里的Action)。这2个地址(Action)非常重要。除此之外,一般还会有一个处理登录信息提交的Action,也很重要。由于太过重要,我就直接将整段代码贴出来
public class AccountController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IUserServiceClient _userServiceClient;//自定义用户信息服务
public AccountController(IIdentityServerInteractionService interaction, IUserServiceClient userServiceClient)
{
this._interaction = interaction;
this._userServiceClient = userServiceClient;
}
/// <summary>
/// 登录页加载
/// 登录时IdentityServer自动跳转到此登录页
/// </summary>
/// <param name="ReturnUrl"></param>
/// <returns></returns>
public IActionResult Login(string ReturnUrl)
{
if (HttpContext.User.Identity.IsAuthenticated)
{
return Redirect(ReturnUrl);
}
LoginInputViewModel vm = new LoginInputViewModel()
{
ReturnUrl = ReturnUrl
};
return View(vm);
}
/// <summary>
/// 提交登录信息操作
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Login(LoginInputViewModel model)
{
//当登录提交给后台的model为null,则返回错误信息给前台
if (model == null)
{
ViewData["TipMessage"] = "登录失败,数据为空!";
return View(model);
}
//这里同理,当信息不完整的时候,返回错误信息给前台
if (string.IsNullOrEmpty(model.Username) || string.IsNullOrEmpty(model.Password))
{
//这里只是简单处理了
ViewData["TipMessage"] = "登录失败,用户名和密码不能为空!";
return View(model);
}
//自定义的方法获取数据库信息验证用户名和密码
BaseResultModel<UserInfoForLoginDto> loginResult = await _userServiceClient.GetLoginUserInfo(model.Username, model.Password);
if (loginResult.success)
{
//登录用户信息
UserInfoForLoginDto loginUserInfo = loginResult.data;
//配置Cookie
AuthenticationProperties properties = new AuthenticationProperties()
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(30))
//ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) //记住登录
};
//使用IdentityServerUser来进行SignInAsync
// issue authentication cookie with subject ID and username
var isuser = new IdentityServerUser(loginUserInfo.Id.ToString())
{
DisplayName = loginUserInfo.UserName
};
await HttpContext.SignInAsync(isuser, properties);
//使用IIdentityServerInteractionService的IsValidReturnUrl来验证ReturnUrl是否有问题
if (_interaction.IsValidReturnUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else
{
ViewData["TipMessage"] = "跳转地址有误:" + model.ReturnUrl;
return View(model);
}
}
else
{
//return Content("<script >alert('"+ loginResult.message + "');</script>", "text/html");
ViewData["TipMessage"] = loginResult.message;
return View(model);
}
return View();
}
/// <summary>
/// 登录注销
/// 注销登录时IdentityServer会自动回调此路径
/// </summary>
/// <param name="logoutId"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
//var logoutId = await _interaction.CreateLogoutContextAsync();
Console.WriteLine("准备注销登录,logoutId为:" + logoutId);
var logout= await _interaction.GetLogoutContextAsync(logoutId);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (logout.PostLogoutRedirectUri != null)
{
Console.WriteLine("注销登录,跳转PostLogoutRedirectUri地址:" + logout.PostLogoutRedirectUri);
return Redirect(logout.PostLogoutRedirectUri);
}
var refererUrl = Request.Headers["Referer"].ToString();
return Redirect(refererUrl);
}
}
这其中登录Login的ReturnUrl参数,以及注销Logout的logoutId参数,可以不用管,因为它们是IdentityServer回调地址,IdentityServer会自动带上这2个参数跳转到这2个地址。
至于是从何处触发的IdentityServer回调跳转?当然是在客户端Web应用的控制器中触发,下面会慢慢讲到。
为了便于大家理解,这里我将登录页、登录页的视图模型类ViewModel也一并贴出来
/// <summary>
/// 登录页的视图模型
/// </summary>
public class LoginInputViewModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
public bool RememberLogin { get; set; }
public string ReturnUrl { get; set; }
}
<div class="login_con">
<h1>登录/LOGIN</h1>
@using (Html.BeginForm("Login", "Account"))
{
<div class="text_box">
<div>
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />
<div><i><img src="/login/images/icon01.png"></i><input id="name" type="text" name="Username" placeholder="请输入注册邮箱"></div>
<div><i><img src="/login/images/icon02.png"></i><input id="pwd" type="password" name="Password" placeholder="请输入密码"></div>
<button type="submit">登 录</button>
</div>
</div>
}
</div>
2、Web应用客户端项目
修改StartUp类
在ConfigureServices方法中添加IdentityServer的依赖注入
//注入IdentityServer单点登录授权认证
//DefaultChallengeScheme的名字要和下面AddOpenIdConnect方法第一个参数名字保持一致
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//默认的身份验证方案名
options.DefaultChallengeScheme = "oidc";//用来跳转到ids登录页的身份验证方案名
//注意这俩配置与下面注册的身份验证方案的名字对应
})
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.Cookie.Name = CookieAuthenticationDefaults.AuthenticationScheme;//注册asp.net core 默认的基于cookie的身份验证方案
})
.AddOpenIdConnect("oidc", options =>//注册ids为我们提供的oidc身份验证方案
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://www.aaa.vip:5100";
options.RequireHttpsMetadata = false;
//指定允许服务端返回的地址,默认是new PathString("/signin-oidc")
//如果这里地址进行了自定义,那么服务端也要进行修改
//options.CallbackPath = new PathString("/signin-oidc");
//指定用户注销后,服务端可以调用客户端注销的地址,默认是new PathString("signout-callback-oidc")
//options.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
options.ClientId = "hpm_mvc_imp";
options.ClientSecret = "SSOSecret";
//options.ResponseType = "code id_token";//授权模式
options.SaveTokens = true;//是否将最后获取的idToken和accessToken存储到默认身份验证方案中
//布尔值来设置处理程序是否应该转到用户信息端点检索。额外索赔或不在id_token创建一个身份收到令牌端点。默认为“false”
//options.GetClaimsFromUserInfoEndpoint = true;
//事件
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents()
{
//远程故障
OnRemoteFailure = context =>
{
context.Response.Redirect("/SSO/Error");
context.HandleResponse();
return Task.FromResult(0);
},
//访问拒绝
OnAccessDenied = context =>
{
//重定向到指定页面
context.Response.Redirect("/SSO/Denied");
//停止此请求的所有处理并返回给客户端
context.HandleResponse();
return Task.FromResult(0);
},
};
});
在Configure方法中启用认证授权中间件及Cookie策略中间件
//使用Cookie
app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax });
app.UseRouting();
//身份认证中间件(身份验证必须在授权的前面)
app.UseAuthentication();
app.UseAuthorization();
接着,在任何需要登录验证的Action操作上,添加[Authorize]特性标识,则未登录状态下系统会触发认证,自动跳转到identityServer项目的登录页/Account/Login并带上相应ReturnUrl参数,如下

如果不想这样被动的触发登录跳转,想要实现主动点击【登录】按钮跳转到登录页,可以创建一个用户控制器,将登录和注销功能都写在里面,其中登录Action带上[Authorize]特性标识,这样就能巧妙实现触发登录页跳转。
public class UserController : Controller
{/// <summary>
/// 登录
/// </summary>
/// <param name="pageUrl"></param>
/// <returns></returns>
[Authorize]
public IActionResult Login(string pageUrl=null)
{
//在认证中心登录页登录后,回到此处再跳转到其他页面
if(pageUrl == null)
{
return RedirectToAction("Index", "Home");
}
else
{
return Redirect(pageUrl);
}
}
/// <summary>
/// 注销登录
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult Logout()
{
//this.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
//this.HttpContext.SignOutAsync("oidc");
//return RedirectToAction("Index", "Home");
//这里会触发identityServer对/Account/Logout的回调,并带上logoutId参数
return SignOut("Cookies", "oidc");
}
}
客户端主动触发登录注销操作的位置
@if (string.IsNullOrEmpty(Model.UserName))
{
<div id="loginBox">
<a style="cursor: pointer;" href="/User/Login">登录</a><span> | </span><a style="cursor: pointer;" href="login/register.html">注册</a>
</div>
}
else
{
<span>Hi @Model.UserName 欢迎回来~<br /><a style="cursor: pointer;" href="/User/Logout">退出登录</a></span>
}
当在IdentityServer认证中心的登录页登录成功后,会自动跳转回Web客户端项目,这时客户端需要获取登录后的用户信息,获取方式如下:
if(HttpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier)!=null)
{
//Type:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier Value:1
viewModel.UserId = Convert.ToInt32(HttpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value);//获取用户ID
//Type:name Value:一人之下
viewModel.UserName = HttpContext.User.Claims.SingleOrDefault(s => s.Type == "name").Value;//获取用户姓名
}
直接在客户端的控制器中获取即可,这里由于我在登录时设定的IdentityServerUser参数Type是这2个,所以就用这2个Type来获取对应的信息,实际开发过程中大家可以根据自己的需要去设置,不一定要按照我的Type来取。
至此,费了我九牛二虎之力,苦心钻研一周的IdentityServer单点登录算是讲完了,最后还是要提醒大家一句,玩IdentityServer单点登录的前提是,你的站点必须是HTTPS!!!
本人虽然在钻研过程中参考了不少资料,但此文基本也算是原创,麻烦大家转载时注明出处,祝各位工作顺利,少死脑细胞。
转 https://www.cnblogs.com/mooncher/p/16328987.html

浙公网安备 33010602011771号