Identity Server4 笔记
文章目录
一.角色
1.身份认证服务器
简而言之,他就是一个向客户端发送安全令牌的软件。
身份认证服务器的主要功能
- 保护您的资源
- 使用本地帐户存储或通过外部身份提供商对用户进行身份验证(例如微信登录)
- 提供会话管理和单点登录
- 管理和认证客户
- 向客户端发布身份和访问令牌
- 验证令牌
主要支持的协议
- OpenID Connect
- OAuth 2.0
2.用户
使用客户端的人
3.客户端
客户端是一种从IdentityServer请求令牌的软件-用于认证用户(请求身份令牌)或访问资源(请求访问令牌)。客户端必须先向授权服务器注册,然后才能请求令牌。
客户端可以是 web 应用程序,或者桌面应用程序,或移动程序
4. 资源
ID4 要保护的 API
或 身份数据
(因为我主要是webapi,所以后边主要使用 webapi 举例子)
每一个 API
都有一个独一无二的名字,客户端使用这个名字来指定他要访问的是哪个 API
。
5.Identity Token(身份令牌)
表示身份认证完成后的结果
他至少包括用户的身份信息,什么时候和怎么样去使用权限。
6.Access Token(访问令牌)
Access Token 是用来获取 API 的。客户端接受 access token,并将将她专送给 受保护的 API。
Access Token 里面包含客户端和使用者的信息(如果有的话),
如何理解 Identity token 和 Access Token 的关系?
可以把 Identity token 想象成在门卫那里领到的,证明你身份的条,而 Access Token是你可以 要进入 API 的钥匙 。(这是我自己的理解,不一定准确)
id4官方给的例子 https://demo.identityserver.io/
网址 https://gitee.com/rush_peng/identity-server4-example.git
二.客户端凭据的认证方式
没有用户参与的程序。使用的是 Client Credentials(客户端证书)这种授权方式。
里面用到的三个项目的 nuget 包是不一样的。
- IdentityServer : IdentityServer4
- API :JwtBearer (解析jwt文件)
- Client : IdentityModel (方便寻找 identityserver 的 api 接口的)
1.建立 IdentityServer4
1.用类的方式,声明配置文佳
public static class Config
{
//定义API
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api1", "My API")
};
//定义客户端
// 定义访问 API 的客户端应用
// 这种情况下,客户端没有交互式的用户,只能通过客户端密钥进行身份验证。
// ClientId 和 Client Secret 可以视为登录名和密码。让身份认证服务器知道是哪个用户。
public static IEnumerable<Client> Clients => new List<Client>
{
new Client
{
// 定义一个客户端
ClientId = "client",
// 认证类型。
//使用客户端密钥的方式进行验证。
AllowedGrantTypes = GrantTypes.ClientCredentials,
// 认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 这个客户端端可以访问的 API
AllowedScopes = { "api1" }
}
};
2.在 Startup 里面做调用设置
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var builder = services.AddIdentityServer()
.AddInMemoryApiScopes(Config.ApiScopes) // 那个api可以使用
.AddInMemoryClients(Config.Clients)// 哪个client可以使用
.AddDeveloperSigningCredential() ;//解决连接不上的问题,实际中,签名需要一对公钥和私钥,他会帮我们将公钥和私钥存储到硬盘上
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseIdentityServer();//使用 Identity 4
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
重生成,而使用命令行运行一下
dotnet … --urls http://*:5001
2.建立受保护的API
1.建立要访问受保护的 Action
[ApiController]
public class IdentityController : ControllerBase
{
[HttpGet("identity")]
[Authorize]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
[Authorize]
[HttpGet("test")]
public string Getst()
{
return "受保护的 API 访问成功";
}
}
2.在startup 里面进行配置
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//接受授服务器发送的任何访问令牌。
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
// 允许检测客户端请求的令牌中是否存在作用域
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthentication();//每次都要执行身份验证.
app.UseAuthorization(); //授权,确保匿名客户端无法访问我们的 api 端点
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
3.建立用来访问的客户端
public class Program
{
// IdentityModel包括一个与发现端点一起使用的客户端库。这样,您只需要知道IdentityServer的基地址-可以从元数据中读取实际的端点地址:
//找到授权的服务器
public static async System.Threading.Tasks.Task Main(string[] args)
{
//找到授权服务器
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5001"); //异步的方法名前边必须声明 async
if (disco.IsError)
{
Console.WriteLine($"连接失败****{disco.Error}");
return;
}
//向授权服务器请求 token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine("token返回错误", tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
//将访问令牌发送给 API
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:6001/test");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine("访问失败");
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine("访问成功");
// Console.WriteLine(JArray.Parse(content));
Console.WriteLine(content);
}
Console.ReadLine();
}
具体查看gitee代码:
4.保护WPF客户端
这种是有用户参与的,使用的是 Password Grant (用名名密码)这种授权方式。
1.流程
运行的顺序和字母的顺序是一致的:
- 浏览器要将请求传到用户代理
- 用户代理再连接 身份认证服务器,并将返回信息返回给用户代理,由用户决定要不要授权。
- 如果用户点了同意,那么,用户代理 就会再跳回浏览器,并带有 Authorization Code 。
- 然后浏览器带着 Authorization code 到 身份认证服务器。请求 access token。
所以在这个过程之中,即对用户进行身份认证,也对浏览器进行身份认证。
具体操作看代码吧。我也没用过MVC
2.为MVC刷新 token
使用 Refresh Token 刷新 Access Token。
1.在MVC 客户端进行设置
new Client
{
ClientId = "mvc client",
ClientName = "ASP.NET Core MVC Client", // 随便写
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,//允许两种授权方式.
ClientSecrets = { new Secret("mvc secret".Sha256()) },//密码.
//下边都是固定的地址。
RedirectUris = { "http://localhost:5002/signin-oidc" },
FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
PostLogoutRedirectUris =
{ "http://localhost:5002/signout-callback-oidc" },
AlwaysIncludeUserClaimsInIdToken = true,
AllowOfflineAccess = true, // offline_access
//默认是一个小时。现在改成 60s 。
AccessTokenLifetime = 60, // 60 seconds
AllowedScopes =
{
"api1",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
IdentityServerConstants.StandardScopes.Phone,
IdentityServerConstants.StandardScopes.Profile
}
},
但是仅仅这样设置,过了一分钟以后,进入 MVC ,依然还能得到 API 的相关信息 。这是因为我们的 API 没有设置对于 对于token检验。
2.对受保护的 API进行设置
services.AddAuthentication("Bearer")//bearer 的授权方式。
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = false;//不需要https
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
//每隔一分钟,验证一次这个token
options.TokenValidationParameters.RequireExpirationTime = true;
//必须要有超时时间这个参数。
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1);
});
3.刷新 Token
在 MVC 的控制器里面,再添加一个 action ,用来刷新 Token。
//刷新 token
private async Task<string> RenewTokensAsync()
{
//发现文档
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
{
throw new Exception(disco.Error);
}
//获取 refreshToken
var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
//请求token
var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "mvc client",
ClientSecret = "mvc secret",
Scope = "api1 openid profile email phone address",
GrantType = OpenIdConnectGrantTypes.RefreshToken,
RefreshToken = refreshToken
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
//超时时间,使用的是 UTC 时间。
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
// 把 token 数组全部集中起来,做成一个数组。
var tokens = new[]
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = tokenResponse.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = tokenResponse.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResponse.RefreshToken
},
new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
}
};
// 获取身份认证的结果,包含当前的pricipal和properties
var currentAuthenticateResult =
await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// 把获得的token再更新一遍。
currentAuthenticateResult.Properties.StoreTokens(tokens);
// 再进行以下登录动作.
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);
// 再把 access token 返回过去.
return tokenResponse.AccessToken;
}
在前边用到token的地方,再更新一下这个方法:
var response = await client.GetAsync("http://localhost:5001/identity");
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await RenewTokensAsync();
return RedirectToAction();
}
throw new Exception(response.ReasonPhrase);
}
5.Implicit Flow
- 用于公共客户端 ,比如用 Angular 这种做的。CS 的文件。如果有些保密文件,可以很容易的获取。
- 最好是客户端可以直接从浏览器 访问资源。
- 没有显示的客户端身份认证。
6. Hybrid Flow
- 访问被保护资源
- 刷新 Access Token
- 处理 Claims
- 基于策略的权限
使用 Hybrid 保护 api 资源。
之前使用的 OpenId Connect 隐式流,所有的令牌都是通过浏览器来传输,这对于 ID token 来说当然没有问题,但是我们还想请求一个 access token,accesstoken 更加敏感,所以在没有必要的时候,我们不会想把他暴露给外部世界, OpenID Connect 包含了一个叫做 “混合流(Hybrid flowe)”,它为我们提供了两方面的优点,身份令牌通过浏览器频道来传输,这样客户端就能能够在做任何工作前验证他,如果验证成功,客户端就会打开一个后端通道,来链接令牌,以检索访问令牌。
1.客户端类型
- 机密客户端:这种客户端有能力维护其凭据的机密性。位于服务端,例如服务器端的 Web 应用,例如 MVC。
- 公开客户端:这种客户端无法维持其凭据的机密性,位于客户端设备,例如 JS 应用,移动应用,原生应用和软件等。
OAuth2.和相比OpenID 就是相当于多出来一个 hybirid Flow
2.返回类型
根据 respons_type 的不同,分为三种情况:
- response_type = code Id_token
- response_type = code token
- response_type = code id_token token
3.角色和策略
- 角色:规定几个角色,然后每个角色里面添加若干个用户。一个用户可以处于不同的角色。
- 通过策略授予权限,可以将多个属性后者
3.1基于角色
在注册的用户里面直接声明 管理员角色
使用的时候,直接声明;
[Authorize(Roles ="管理员,普通用户")]
[HttpGet("test")]
public string Getst()
{
return "受保护的 API 访问成功";
}
3.2基于策略
比如:定义把名字为Smith,地址为Somewhere的作为条件。
总的来讲,基于策略的更加的灵活,我们可以自己定义:比如只让 firstname== “ 张 ”的人登录
在客户端声明:
services.AddAuthorization(options =>
{
options.AddPolicy("SmithInSomewhere", builder =>
{
builder.RequireAuthenticatedUser();
builder.RequireClaim(JwtClaimTypes.FamilyName, "Smith");
builder.RequireClaim("location", "somewhere");
});
在API调用
[Authorize(Policy ="sithInSomewhere")]
public IActionResult Index()
{
var user = User.Identity;
return View();
}
4.使用混合流保护客户端
参考文献
[1] Identity4 官方文档
[2] identityserver4
[3] 杨旭视频
[4] identityServer4 中文文档