You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
18 KiB
357 lines
18 KiB
using Microsoft.AspNetCore;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Primitives;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using Newtonsoft.Json;
|
|
using OpenIddict.Abstractions;
|
|
using OpenIddict.Server.AspNetCore;
|
|
using System.Collections.Immutable;
|
|
using System.Security.Claims;
|
|
using static OpenIddict.Abstractions.OpenIddictConstants;
|
|
|
|
namespace HuiXin.Identity.OpenIddict.Controllers
|
|
{
|
|
[ApiController]
|
|
[Route("auth")]
|
|
public class AuthorizationController : Controller
|
|
{
|
|
private readonly IOpenIddictApplicationManager _applicationManager;
|
|
private readonly IOpenIddictAuthorizationManager _authorizationManager;
|
|
private readonly IOpenIddictScopeManager _scopeManager;
|
|
|
|
public AuthorizationController(IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager)
|
|
{
|
|
_applicationManager = applicationManager;
|
|
_scopeManager = scopeManager;
|
|
_authorizationManager = authorizationManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 登录权限校验
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
/// <exception cref="Exception"></exception>
|
|
[HttpGet("connect/authorize")]
|
|
[IgnoreAntiforgeryToken]
|
|
public async Task<IActionResult> Authorize()
|
|
{
|
|
// var s=await _schemeProvider.GetAllSchemesAsync();
|
|
|
|
//通过扩展的获取自定义的参数校验
|
|
var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("未获取到相关认证情况");
|
|
|
|
#region 存在登录凭证且明确了登录请求的行为
|
|
// 存在登录凭证且明确了登录请求的行为
|
|
if (request.HasPrompt(Prompts.Login))
|
|
{
|
|
//这里有个小坑,在Challenge之前必须把这个行为去掉 不然 Challenge 进入 /connect/authorize 路由陷入死循环
|
|
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
|
|
var parameters = Request.HasFormContentType ?
|
|
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
|
|
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
|
|
|
|
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
|
|
|
|
return Challenge(
|
|
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, // IdentityConstants.ApplicationScheme,
|
|
properties: new AuthenticationProperties
|
|
{
|
|
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
|
|
});
|
|
}
|
|
#endregion
|
|
//检索本地的Cookies信息 确定重定向页面 这里都是UTC时间来设置的过期情况 这里没有用Identity 所以这里可以指定自己的应用名称
|
|
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); //IdentityConstants.ApplicationScheme
|
|
#region 未获取本地Cookies信息或者 cookie过期的情况
|
|
if (request == null || !result.Succeeded || (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
|
|
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
|
|
{
|
|
//是否是无效授权
|
|
if (request.HasPrompt(Prompts.None))
|
|
{
|
|
return Forbid(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户未登录."
|
|
}));
|
|
}
|
|
return Challenge(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties
|
|
{
|
|
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
|
|
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
|
|
});
|
|
}
|
|
#endregion
|
|
|
|
|
|
//var resultdata = _userService.GetLoginInfo(new Guid(result.Principal.GetClaim(Claims.Subject) ?? throw new Exception("用户标识存在"))) ?? throw new Exception("用户详细信息不存在");
|
|
var resultdata = new UserInfo()
|
|
{
|
|
Id = "user1",
|
|
UserName = "测试用户名",
|
|
NickName = "测试昵称",
|
|
};
|
|
|
|
// 获取客户端详细信息 验证其他数据
|
|
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ?? throw new InvalidOperationException("未查找到该客户端的应用详细信息");
|
|
|
|
//查找当前情况客户端下请求用户的持久化授权数据信息
|
|
var authorizations = _authorizationManager.FindAsync(
|
|
subject: resultdata.Id.ToString(),
|
|
client: await _applicationManager.GetIdAsync(application) ?? throw new Exception("没有找到客户端的应用信息"), //这里区分下 是application的Id而不是ClientId
|
|
status: Statuses.Valid,
|
|
type: AuthorizationTypes.Permanent,
|
|
scopes: request.GetScopes()
|
|
).ToBlockingEnumerable();//.ToListAsync();
|
|
|
|
var consenttype = await _applicationManager.GetConsentTypeAsync(application);
|
|
//获取授权同意确认页面
|
|
switch (consenttype)
|
|
{
|
|
//判断授权同意的类型
|
|
|
|
//1 外部允许的且没有任何授权项
|
|
case ConsentTypes.External when !authorizations.Any():
|
|
return Forbid(
|
|
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
|
|
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
|
"登录用户没用访问该客户端应用的权限"
|
|
}));
|
|
|
|
// 隐式、外部授权、显示模式模式
|
|
case ConsentTypes.Implicit:
|
|
case ConsentTypes.External when authorizations.Any():
|
|
case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
|
|
|
|
ClaimsPrincipal principal = CreateUserPrincpal(resultdata);
|
|
//设置请求的范围
|
|
principal.SetScopes(request.GetScopes());
|
|
|
|
//查找scope允许访问的资源
|
|
var resources = _scopeManager.ListResourcesAsync(principal.GetScopes()).ToBlockingEnumerable();
|
|
//通过扩展设置不同的资源访问 其实本质都是设置Claims 只是 key 在 scope以及Resource上不同
|
|
//Resource = "oi_rsrc";
|
|
// Scope = "oi_scp";
|
|
|
|
principal.SetResources(resources);
|
|
|
|
// 自动创建一个永久授权,以避免需要明确的同意 用于包含相同范围的未来授权或令牌请求
|
|
var authorization = authorizations.LastOrDefault();
|
|
if (authorization is null)
|
|
{
|
|
authorization = await _authorizationManager.CreateAsync(
|
|
principal: principal,
|
|
subject: resultdata.Id.ToString(),
|
|
client: await _applicationManager.GetIdAsync(application),
|
|
type: AuthorizationTypes.Permanent,
|
|
scopes: principal.GetScopes());
|
|
}
|
|
|
|
principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
|
|
|
|
foreach (var claim in principal.Claims)
|
|
{
|
|
claim.SetDestinations(GetDestinations(claim, principal));
|
|
}
|
|
//登录 OpenIddict签发令牌
|
|
return SignIn(principal, null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
|
|
|
// At this point, no authorization was found in the database and an error must be returned
|
|
// if the client application specified prompt=none in the authorization request.
|
|
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
|
|
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
|
|
return Forbid(
|
|
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
|
|
properties: new AuthenticationProperties(new Dictionary<string, string?>
|
|
{
|
|
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
|
|
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
|
|
"Interactive user consent is required."
|
|
}));
|
|
|
|
// In every other case, render the consent form.
|
|
//default:
|
|
// return View(new AuthorizeViewModel
|
|
// {
|
|
// ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application),
|
|
// Scope = request.Scope
|
|
// });
|
|
default:
|
|
return Challenge(
|
|
authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
|
|
properties: new AuthenticationProperties { RedirectUri = "/" }
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
[HttpPost("connect/token"), Produces("application/json")]
|
|
public async Task<IActionResult> ConnectToken()
|
|
{
|
|
var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OIDC请求不存在");
|
|
|
|
if (request.IsClientCredentialsGrantType())
|
|
{
|
|
// Note: the client credentials are automatically validated by OpenIddict:
|
|
// if client_id or client_secret are invalid, this action won't be invoked.
|
|
|
|
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
|
|
if (application is null)
|
|
{
|
|
Console.WriteLine($"client_id未注册:{request.ClientId}");
|
|
throw new InvalidOperationException("The application cannot be found.");
|
|
}
|
|
Console.WriteLine($"获取到客户信息:{JsonConvert.SerializeObject(application)}");
|
|
|
|
// Create a new ClaimsIdentity containing the claims that
|
|
// will be used to create an id_token, a token or a code.
|
|
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role);
|
|
|
|
// Use the client_id as the subject identifier.
|
|
identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application));
|
|
identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));
|
|
|
|
identity.SetDestinations(static claim => claim.Type switch
|
|
{
|
|
// Allow the "name" claim to be stored in both the access and identity tokens
|
|
// when the "profile" scope was granted (by calling principal.SetScopes(...)).
|
|
Claims.Name when claim.Subject.HasScope(Scopes.Profile)
|
|
=> new[] { Destinations.AccessToken, Destinations.IdentityToken },
|
|
|
|
// Otherwise, only store the claim in the access tokens.
|
|
_ => new[] { Destinations.AccessToken }
|
|
});
|
|
//Console.WriteLine($"identity信息:{JsonConvert.SerializeObject(identity)}");
|
|
|
|
var result = SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
|
//Console.WriteLine($"signIn信息:{JsonConvert.SerializeObject(result)}");
|
|
return result;
|
|
}
|
|
else if (request.IsPasswordGrantType())
|
|
{
|
|
//数据库获取用户信息
|
|
var princpal = CreateUserPrincpal(new UserInfo()
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
UserName = "测试用户名",
|
|
NickName = "测试昵称",
|
|
});
|
|
//princpal.SetResources(_scopeManager.ListResourcesAsync(princpal.GetScopes()));
|
|
|
|
foreach (var claim in princpal.Claims)
|
|
{
|
|
claim.SetDestinations(GetDestinations(claim, princpal));
|
|
}
|
|
return SignIn(princpal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
|
}
|
|
else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType())
|
|
{
|
|
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
|
|
//var user = _userService.GetLoginInfo(new Guid(principal?.GetClaim(Claims.Subject) ?? throw new Exception("用户标识存在")));
|
|
//UserInfo user = null;
|
|
//if (user == null)
|
|
//{
|
|
// return Forbid(
|
|
// authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
// properties: new AuthenticationProperties(new Dictionary<string, string?>
|
|
// {
|
|
// [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
|
|
// [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "令牌已失效"
|
|
// })
|
|
// );
|
|
//}
|
|
foreach (var claim in principal.Claims)
|
|
{
|
|
claim.SetDestinations(GetDestinations(claim, principal));
|
|
}
|
|
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
|
}
|
|
throw new InvalidOperationException($"不支持的grant_type:{request.GrantType}");
|
|
}
|
|
|
|
[HttpPost("connect/logout")]
|
|
public async Task<IActionResult> Logout()
|
|
{
|
|
await HttpContext.SignOutAsync();
|
|
return SignOut(
|
|
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
|
|
properties: new AuthenticationProperties
|
|
{
|
|
RedirectUri = "/"
|
|
});
|
|
}
|
|
|
|
public static ClaimsPrincipal CreateUserPrincpal(UserInfo user, string claimsIdentityName = "USERINFO")
|
|
{
|
|
//登录成功流程
|
|
ClaimsIdentity identity = new ClaimsIdentity(claimsIdentityName);
|
|
identity.AddClaim(new Claim(Claims.Subject, user.Id));
|
|
identity.AddClaim(new Claim(Claims.Name, user.UserName));
|
|
identity.AddClaim(new Claim(Claims.Nickname, user.NickName));
|
|
return new ClaimsPrincipal(identity);
|
|
}
|
|
|
|
private IEnumerable<string> GetDestinations(Claim claim)
|
|
{
|
|
// Note: by default, claims are NOT automatically included in the access and identity tokens.
|
|
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
|
|
// whether they should be included in access tokens, in identity tokens or in both.
|
|
|
|
return claim.Type switch
|
|
{
|
|
Claims.Name or
|
|
Claims.Subject
|
|
=> ImmutableArray.Create(Destinations.AccessToken, Destinations.IdentityToken),
|
|
|
|
_ => ImmutableArray.Create(Destinations.AccessToken),
|
|
};
|
|
}
|
|
|
|
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
|
|
{
|
|
|
|
switch (claim.Type)
|
|
{
|
|
case Claims.Name:
|
|
yield return Destinations.AccessToken;
|
|
|
|
if (principal.HasScope(Scopes.Profile))
|
|
yield return Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
case Claims.Email:
|
|
yield return Destinations.AccessToken;
|
|
|
|
if (principal.HasScope(Scopes.Email))
|
|
yield return Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
case Claims.Role:
|
|
yield return Destinations.AccessToken;
|
|
|
|
if (principal.HasScope(Scopes.Roles))
|
|
yield return Destinations.IdentityToken;
|
|
|
|
yield break;
|
|
|
|
|
|
case "AspNet.Identity.SecurityStamp": yield break;
|
|
|
|
default:
|
|
yield return Destinations.AccessToken;
|
|
yield break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|