feat: 添加OAuth2认证配置和实现

添加OAuth2认证相关配置文件和服务实现,包括环境变量配置、PKCE流程支持、token管理等功能。主要变更:
- 新增OAuth2配置文件
- 实现OAuth2服务层
- 更新请求拦截器支持token自动刷新
- 修改认证API和store以支持OAuth2流程
This commit is contained in:
Sam 2026-02-07 17:47:11 +08:00
parent 1a0c18c198
commit 0c5bd5e647
21 changed files with 1332 additions and 372 deletions

View File

@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
namespace Fengling.AuthService.Configuration;
public sealed class FormValueRequiredAttribute(string name) : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(RouteContext context, ActionDescriptor action)
{
if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType))
{
return false;
}
if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded",
StringComparison.OrdinalIgnoreCase))
{
return false;
}
return !string.IsNullOrEmpty(context.HttpContext.Request.Form[name]);
}
}

View File

@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
using Quartz;
namespace Fengling.AuthService.Configuration; namespace Fengling.AuthService.Configuration;
@ -11,31 +14,84 @@ public static class OpenIddictSetup
IConfiguration configuration IConfiguration configuration
) )
{ {
services.Configure<IdentityOptions>(options =>
{
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role;
options.ClaimsIdentity.EmailClaimType = OpenIddictConstants.Claims.Email;
// Note: to require account confirmation before login,
// register an email sender service (IEmailSender) and
// set options.SignIn.RequireConfirmedAccount to true.
//
// For more information, visit https://aka.ms/aspaccountconf.
options.SignIn.RequireConfirmedAccount = false;
});
services.AddQuartz(options =>
{
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
var isTesting = configuration.GetValue<bool>("Testing", false); var isTesting = configuration.GetValue<bool>("Testing", false);
var builder = services.AddOpenIddict(); var builder = services.AddOpenIddict();
builder.AddCore(options => builder.AddCore(options =>
{ {
options.UseEntityFrameworkCore().UseDbContext<Data.ApplicationDbContext>(); options.UseEntityFrameworkCore()
.UseDbContext<Data.ApplicationDbContext>();
options.UseQuartz();
}); });
if (!isTesting) if (!isTesting)
{ {
builder.AddServer(options => builder.AddServer(options =>
{ {
options.SetIssuer(configuration["OpenIddict:Issuer"] ?? "https://auth.fengling.local"); options.SetIssuer(configuration["OpenIddict:Issuer"] ?? "http://localhost:5132");
options.SetAuthorizationEndpointUris("connect/authorize")
//.SetDeviceEndpointUris("connect/device")
.SetIntrospectionEndpointUris("connect/introspect")
.SetEndSessionEndpointUris("connect/endsession")
.SetTokenEndpointUris("connect/token")
.SetUserInfoEndpointUris("connect/userinfo")
.SetEndUserVerificationEndpointUris("connect/verify");
options.AllowAuthorizationCodeFlow()
.AllowHybridFlow()
.AllowClientCredentialsFlow()
.AllowRefreshTokenFlow();
options.AddDevelopmentEncryptionCertificate() options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate(); .AddDevelopmentSigningCertificate();
options.RegisterScopes(
"openid", options.DisableAccessTokenEncryption();
"profile",
"email",
options.RegisterScopes(OpenIddictConstants.Scopes.OfflineAccess, OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Profile, OpenIddictConstants.Scopes.OpenId,
OpenIddictConstants.Permissions.Scopes.Roles,
"api", "api",
"offline_access" "auth_server_admin");
);
options
.UseReferenceAccessTokens()
.UseReferenceRefreshTokens()
.UseAspNetCore()
.DisableTransportSecurityRequirement()
.EnableAuthorizationEndpointPassthrough()
.EnableEndSessionEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserInfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
}); });
} }
@ -45,10 +101,7 @@ public static class OpenIddictSetup
options.UseAspNetCore(); options.UseAspNetCore();
}); });
services.AddAuthentication(options => services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
return services; return services;
} }

View File

@ -51,7 +51,7 @@ public class AccountController : Controller
return View(model); return View(model);
} }
var result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, false); var result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, true);
if (!result.Succeeded) if (!result.Succeeded)
{ {
if (result.IsLockedOut) if (result.IsLockedOut)
@ -117,13 +117,13 @@ public class AccountController : Controller
[HttpGet("profile")] [HttpGet("profile")]
[HttpGet("settings")] [HttpGet("settings")]
[HttpGet("logout")] [HttpGet("/connect/logout")]
public IActionResult NotImplemented() public IActionResult NotImplemented()
{ {
return RedirectToAction("Index", "Dashboard"); return RedirectToAction("Index", "Dashboard");
} }
[HttpPost("logout")] [HttpPost("/connect/logout")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> LogoutPost() public async Task<IActionResult> LogoutPost()
{ {

View File

@ -7,15 +7,16 @@ using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore; using OpenIddict.Server.AspNetCore;
using System.Security.Claims; using System.Security.Claims;
using Fengling.AuthService.Configuration;
using Fengling.AuthService.ViewModels; using Fengling.AuthService.ViewModels;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Swashbuckle.AspNetCore.Annotations;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers; namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("connect")]
public class AuthorizationController( public class AuthorizationController(
IOpenIddictApplicationManager applicationManager, IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager, IOpenIddictAuthorizationManager authorizationManager,
@ -26,8 +27,99 @@ public class AuthorizationController(
: Controller : Controller
{ {
[HttpGet("authorize")] [Authorize, FormValueRequired("submit.Accept")]
[HttpPost("authorize")] [Tags("submit.Accept")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
[SwaggerIgnore]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the profile of the logged in user.
var user = await userManager.GetUserAsync(User) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
if (user == null)
{
throw new InvalidOperationException("The user details cannot be retrieved.");
}
// Retrieve the application details from the database.
var application = await applicationManager.FindByClientIdAsync(request.ClientId!) ??
throw new InvalidOperationException(
"Details concerning the calling client application cannot be found.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await authorizationManager.FindAsync(
subject: await userManager.GetUserIdAsync(user),
client: await applicationManager.GetIdAsync(application),
status: OpenIddictConstants.Statuses.Valid,
type: OpenIddictConstants.AuthorizationTypes.Permanent,
scopes: request.GetScopes()).ToListAsync();
// Note: the same check is already made in the other action but is repeated
// here to ensure a malicious user can't abuse this POST-only endpoint and
// force it to return a valid response without the external authorization.
if (!authorizations.Any() &&
await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}!));
}
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
var principal = result.Principal!;
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
if (authorization == null)
{
authorization = await authorizationManager.CreateAsync(
principal: principal,
subject: await userManager.GetUserIdAsync(user)!,
client: await applicationManager.GetIdAsync(application) ?? string.Empty,
type: OpenIddictConstants.AuthorizationTypes.Permanent,
scopes: principal.GetScopes());
}
principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
[Authorize, FormValueRequired("submit.Deny")]
[Tags("submit.Deny")]
[SwaggerIgnore]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[Tags("Authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize() public async Task<IActionResult> Authorize()
{ {
var request = HttpContext.GetOpenIddictServerRequest() ?? var request = HttpContext.GetOpenIddictServerRequest() ??
@ -156,8 +248,10 @@ public class AuthorizationController(
// At this point, no authorization was found in the database and an error must be returned // 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. // if the client application specified prompt=none in the authorization request.
case OpenIddictConstants.ConsentTypes.Explicit when request.HasPromptValue(OpenIddictConstants.PromptValues.None): case OpenIddictConstants.ConsentTypes.Explicit
case OpenIddictConstants.ConsentTypes.Systematic when request.HasPromptValue(OpenIddictConstants.PromptValues.None): when request.HasPromptValue(OpenIddictConstants.PromptValues.None):
case OpenIddictConstants.ConsentTypes.Systematic
when request.HasPromptValue(OpenIddictConstants.PromptValues.None):
return Forbid( return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string> properties: new AuthenticationProperties(new Dictionary<string, string>
@ -170,13 +264,17 @@ public class AuthorizationController(
// In every other case, render the consent form. // In every other case, render the consent form.
default: default:
return View(new AuthorizeViewModel(await applicationManager.GetDisplayNameAsync(application),request.Scope)); return View(new AuthorizeViewModel(await applicationManager.GetDisplayNameAsync(application),
request.Scope));
} }
} }
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal) private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{ {
// Note: by default, claims are NOT automatically included in the access and identity tokens. // Note: by default, claims are NOT automatically included in access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies // 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. // whether they should be included in access tokens, in identity tokens or in both.
@ -214,4 +312,56 @@ public class AuthorizationController(
yield break; yield break;
} }
} }
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> UserInfo()
{
var user = await userManager.GetUserAsync(User) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// 获取用户的角色
var roles = await userManager.GetRolesAsync(user);
// 获取用户的租户信息
var tenantInfo = user.TenantInfo;
var claims = new List<Claim>
{
new(OpenIddictConstants.Claims.Subject, await userManager.GetUserIdAsync(user)),
new(OpenIddictConstants.Claims.Name, user.UserName!),
new(OpenIddictConstants.Claims.PreferredUsername, user.UserName!)
};
if (!string.IsNullOrEmpty(user.Email))
{
claims.Add(new(OpenIddictConstants.Claims.Email, user.Email!));
}
// 添加角色 claims
foreach (var role in roles)
{
claims.Add(new(OpenIddictConstants.Claims.Role, role));
}
// 添加自定义 tenant 相关 claims
if (tenantInfo != null)
{
claims.Add(new Claim("tenant_id", tenantInfo.Id.ToString()));
claims.Add(new Claim("tenant_code", tenantInfo.TenantId));
claims.Add(new Claim("tenant_name", tenantInfo.Name));
}
return Ok(new Dictionary<string, object>
{
["sub"] = await userManager.GetUserIdAsync(user),
["name"] = user.UserName,
["preferred_username"] = user.UserName,
["email"] = user.Email ?? "",
["role"] = roles.ToArray(),
["tenant_id"] = tenantInfo != null ? tenantInfo.Id.ToString() : "",
["tenant_code"] = tenantInfo?.TenantId ?? "",
["tenant_name"] = tenantInfo?.Name ?? ""
});
}
} }

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore; using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using System.Linq;
namespace Fengling.AuthService.Controllers; namespace Fengling.AuthService.Controllers;
@ -43,29 +44,87 @@ public class LogoutController : ControllerBase
var request = HttpContext.GetOpenIddictServerRequest() ?? var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("OpenIddict request is null"); throw new InvalidOperationException("OpenIddict request is null");
// 标准做法:先尝试从 id_token_hint 中提取客户端信息
string? clientId = request.ClientId;
if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(request.IdTokenHint))
{
try
{
// 从 id_token_hint 中提取 client_id (azp claim 或 aud claim)
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
if (principal != null)
{
// 尝试从 azp (authorized party) claim 获取
clientId = principal.GetClaim(Claims.AuthorizedParty);
// 如果没有 azp尝试从 aud (audience) claim 获取
if (string.IsNullOrEmpty(clientId))
{
clientId = principal.GetClaim(Claims.Audience);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to extract client_id from id_token_hint");
}
}
// 执行登出
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result.Succeeded) if (result.Succeeded)
{ {
await _signInManager.SignOutAsync(); await _signInManager.SignOutAsync();
} }
if (request.ClientId != null) // 处理 post_logout_redirect_uri
if (!string.IsNullOrEmpty(clientId))
{ {
var application = await _applicationManager.FindByClientIdAsync(request.ClientId); var application = await _applicationManager.FindByClientIdAsync(clientId);
if (application != null) if (application != null)
{ {
var postLogoutRedirectUri = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); var registeredUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
// 如果提供了 post_logout_redirect_uri验证它是否在注册的 URI 列表中
if (!string.IsNullOrEmpty(request.PostLogoutRedirectUri)) if (!string.IsNullOrEmpty(request.PostLogoutRedirectUri))
{ {
if (postLogoutRedirectUri.Contains(request.PostLogoutRedirectUri)) if (registeredUris.Contains(request.PostLogoutRedirectUri))
{ {
return Redirect(request.PostLogoutRedirectUri); // 如果提供了 state需要附加到重定向 URI
var redirectUri = request.PostLogoutRedirectUri;
if (!string.IsNullOrEmpty(request.State))
{
var separator = redirectUri.Contains('?') ? "&" : "?";
redirectUri = $"{redirectUri}{separator}state={Uri.EscapeDataString(request.State)}";
}
return Redirect(redirectUri);
}
else
{
_logger.LogWarning(
"Post-logout redirect URI {Uri} is not registered for client {ClientId}",
request.PostLogoutRedirectUri,
clientId);
}
}
else
{
// 如果没有提供 post_logout_redirect_uri使用第一个注册的 URI
var defaultUri = registeredUris.FirstOrDefault();
if (!string.IsNullOrEmpty(defaultUri))
{
_logger.LogInformation(
"Using default post-logout redirect URI for client {ClientId}",
clientId);
return Redirect(defaultUri);
} }
} }
} }
} }
// 如果无法确定重定向地址,返回默认页面
return Redirect("/"); return Redirect("/");
} }
} }

View File

@ -1,10 +1,6 @@
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
namespace Fengling.AuthService.Controllers; namespace Fengling.AuthService.Controllers;
@ -14,16 +10,13 @@ namespace Fengling.AuthService.Controllers;
[Authorize] [Authorize]
public class OAuthClientsController : ControllerBase public class OAuthClientsController : ControllerBase
{ {
private readonly ApplicationDbContext _context;
private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictApplicationManager _applicationManager;
private readonly ILogger<OAuthClientsController> _logger; private readonly ILogger<OAuthClientsController> _logger;
public OAuthClientsController( public OAuthClientsController(
ApplicationDbContext context,
IOpenIddictApplicationManager applicationManager, IOpenIddictApplicationManager applicationManager,
ILogger<OAuthClientsController> logger) ILogger<OAuthClientsController> logger)
{ {
_context = context;
_applicationManager = applicationManager; _applicationManager = applicationManager;
_logger = logger; _logger = logger;
} }
@ -33,54 +26,60 @@ public class OAuthClientsController : ControllerBase
[FromQuery] int page = 1, [FromQuery] int page = 1,
[FromQuery] int pageSize = 10, [FromQuery] int pageSize = 10,
[FromQuery] string? displayName = null, [FromQuery] string? displayName = null,
[FromQuery] string? clientId = null, [FromQuery] string? clientId = null)
[FromQuery] string? status = null)
{ {
var query = _context.OAuthApplications.AsQueryable(); var applications = _applicationManager.ListAsync();
var clientList = new List<object>();
if (!string.IsNullOrEmpty(displayName)) await foreach (var application in applications)
{ {
query = query.Where(c => c.DisplayName.Contains(displayName)); var clientIdValue = await _applicationManager.GetClientIdAsync(application);
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
if (!string.IsNullOrEmpty(displayName) && !displayNameValue.Contains(displayName, StringComparison.OrdinalIgnoreCase))
continue;
if (!string.IsNullOrEmpty(clientId) && !clientIdValue.Contains(clientId, StringComparison.OrdinalIgnoreCase))
continue;
var clientType = await _applicationManager.GetClientTypeAsync(application);
var consentType = await _applicationManager.GetConsentTypeAsync(application);
var permissions = await _applicationManager.GetPermissionsAsync(application);
var redirectUrisStrings = await _applicationManager.GetRedirectUrisAsync(application);
var postLogoutRedirectUrisStrings = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
var redirectUris = redirectUrisStrings;
var postLogoutRedirectUris = postLogoutRedirectUrisStrings;
clientList.Add(new
{
id = application,
clientId = clientIdValue,
displayName = displayNameValue,
redirectUris = redirectUris.Select(u => u.ToString()).ToArray(),
postLogoutRedirectUris = postLogoutRedirectUris.Select(u => u.ToString()).ToArray(),
scopes = permissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)),
grantTypes = permissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)),
clientType = clientType?.ToString(),
consentType = consentType?.ToString(),
status = "active",
permissions
});
} }
if (!string.IsNullOrEmpty(clientId)) var sortedClients = clientList
{ .OrderByDescending(c => (c as dynamic).clientId)
query = query.Where(c => c.ClientId.Contains(clientId));
}
if (!string.IsNullOrEmpty(status))
{
query = query.Where(c => c.Status == status);
}
var totalCount = await query.CountAsync();
var clients = await query
.OrderByDescending(c => c.CreatedAt)
.Skip((page - 1) * pageSize) .Skip((page - 1) * pageSize)
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToList();
var result = clients.Select(c => new
{
id = c.Id,
clientId = c.ClientId,
displayName = c.DisplayName,
redirectUris = c.RedirectUris,
postLogoutRedirectUris = c.PostLogoutRedirectUris,
scopes = c.Scopes,
grantTypes = c.GrantTypes,
clientType = c.ClientType,
consentType = c.ConsentType,
status = c.Status,
description = c.Description,
createdAt = c.CreatedAt,
updatedAt = c.UpdatedAt,
});
return Ok(new return Ok(new
{ {
items = result, items = sortedClients,
totalCount, totalCount = clientList.Count,
page, page,
pageSize pageSize
}); });
@ -127,36 +126,46 @@ public class OAuthClientsController : ControllerBase
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<ActionResult<OAuthApplication>> GetClient(long id) public async Task<ActionResult<object>> GetClient(string id)
{ {
var client = await _context.OAuthApplications.FindAsync(id); var application = await _applicationManager.FindByIdAsync(id);
if (client == null) if (application == null)
{ {
return NotFound(); return NotFound();
} }
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
var clientType = await _applicationManager.GetClientTypeAsync(application);
var consentType = await _applicationManager.GetConsentTypeAsync(application);
var permissions = await _applicationManager.GetPermissionsAsync(application);
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
return Ok(new return Ok(new
{ {
id = client.Id, id = id,
clientId = client.ClientId, clientId = clientIdValue,
displayName = client.DisplayName, displayName = displayNameValue,
redirectUris = client.RedirectUris, redirectUris = redirectUris.Select(u => u.ToString()).ToArray(),
postLogoutRedirectUris = client.PostLogoutRedirectUris, postLogoutRedirectUris = postLogoutRedirectUris.Select(u => u.ToString()).ToArray(),
scopes = client.Scopes, scopes = permissions
grantTypes = client.GrantTypes, .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
clientType = client.ClientType, .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)),
consentType = client.ConsentType, grantTypes = permissions
status = client.Status, .Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
description = client.Description, .Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)),
createdAt = client.CreatedAt, clientType = clientType?.ToString(),
updatedAt = client.UpdatedAt, consentType = consentType?.ToString(),
status = "active",
permissions
}); });
} }
[HttpPost] [HttpPost]
public async Task<ActionResult<object>> CreateClient([FromBody] CreateOAuthClientDto dto) public async Task<ActionResult<object>> CreateClient([FromBody] CreateOAuthClientDto dto)
{ {
var existingClient = await _context.OAuthApplications.FirstOrDefaultAsync(c => c.ClientId == dto.ClientId); var existingClient = await _applicationManager.FindByClientIdAsync(dto.ClientId);
if (existingClient != null) if (existingClient != null)
{ {
return BadRequest(new { message = "Client ID 已存在", clientId = dto.ClientId }); return BadRequest(new { message = "Client ID 已存在", clientId = dto.ClientId });
@ -168,9 +177,16 @@ public class OAuthClientsController : ControllerBase
{ {
ClientId = dto.ClientId, ClientId = dto.ClientId,
ClientSecret = clientSecret, ClientSecret = clientSecret,
DisplayName = dto.DisplayName DisplayName = dto.DisplayName,
ClientType = string.IsNullOrEmpty(dto.ClientType) ? "confidential" : dto.ClientType,
ConsentType = string.IsNullOrEmpty(dto.ConsentType) ? "explicit" : dto.ConsentType,
}; };
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
foreach (var uri in (dto.RedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _))) foreach (var uri in (dto.RedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
{ {
descriptor.RedirectUris.Add(new Uri(uri)); descriptor.RedirectUris.Add(new Uri(uri));
@ -183,205 +199,196 @@ public class OAuthClientsController : ControllerBase
foreach (var grantType in dto.GrantTypes ?? Array.Empty<string>()) foreach (var grantType in dto.GrantTypes ?? Array.Empty<string>())
{ {
descriptor.Permissions.Add(grantType); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.GrantType + grantType);
} }
foreach (var scope in dto.Scopes ?? Array.Empty<string>()) foreach (var scope in dto.Scopes ?? Array.Empty<string>())
{ {
descriptor.Permissions.Add(scope); descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
} }
await _applicationManager.CreateAsync(descriptor); var application = await _applicationManager.CreateAsync(descriptor);
var applicationId = await _applicationManager.GetIdAsync(application);
var client = new OAuthApplication return CreatedAtAction(nameof(GetClient), new { id = applicationId }, new
{ {
ClientId = dto.ClientId, id = applicationId,
ClientSecret = clientSecret, clientId = dto.ClientId,
DisplayName = dto.DisplayName, clientSecret = clientSecret,
RedirectUris = dto.RedirectUris ?? Array.Empty<string>(), displayName = dto.DisplayName,
PostLogoutRedirectUris = dto.PostLogoutRedirectUris ?? Array.Empty<string>(), status = "active"
Scopes = dto.Scopes ?? new[] { "openid", "profile", "email", "api" },
GrantTypes = dto.GrantTypes ?? new[] { "authorization_code", "refresh_token" },
ClientType = dto.ClientType ?? "confidential",
ConsentType = dto.ConsentType ?? "explicit",
Status = dto.Status ?? "active",
Description = dto.Description,
CreatedAt = DateTime.UtcNow,
};
_context.OAuthApplications.Add(client);
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "create", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(dto));
return CreatedAtAction(nameof(GetClient), new { id = client.Id }, new
{
client.Id,
client.ClientId,
client.ClientSecret,
client.DisplayName,
client.Status,
client.CreatedAt
}); });
} }
[HttpPost("{id}/generate-secret")] [HttpPost("{id}/generate-secret")]
public async Task<ActionResult<object>> GenerateSecret(long id) public async Task<ActionResult<object>> GenerateSecret(string id)
{ {
var client = await _context.OAuthApplications.FindAsync(id); var application = await _applicationManager.FindByIdAsync(id);
if (client == null)
{
return NotFound();
}
var newSecret = GenerateSecureSecret();
client.ClientSecret = newSecret;
client.UpdatedAt = DateTime.UtcNow;
var application = await _applicationManager.FindByClientIdAsync(client.ClientId);
if (application != null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = client.ClientId,
ClientSecret = newSecret,
DisplayName = client.DisplayName
};
await _applicationManager.UpdateAsync(application, descriptor);
}
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "generate_secret", "OAuthClient", client.Id, client.DisplayName, "[REDACTED]", "[REDACTED]");
return Ok(new
{
clientId = client.ClientId,
clientSecret = newSecret,
message = "新密钥已生成,请妥善保管,刷新后将无法再次查看"
});
}
[HttpPost("{id}/toggle-status")]
public async Task<ActionResult<object>> ToggleStatus(long id)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
var oldStatus = client.Status;
client.Status = client.Status == "active" ? "inactive" : "active";
client.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "toggle_status", "OAuthClient", client.Id, client.DisplayName, oldStatus, client.Status);
return Ok(new
{
clientId = client.ClientId,
oldStatus,
newStatus = client.Status,
message = $"客户端状态已从 {oldStatus} 更改为 {client.Status}"
});
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateClient(long id, [FromBody] UpdateOAuthClientDto dto)
{
var client = await _context.OAuthApplications.FindAsync(id);
if (client == null)
{
return NotFound();
}
var application = await _applicationManager.FindByClientIdAsync(client.ClientId);
if (application == null) if (application == null)
{ {
return NotFound(); return NotFound();
} }
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
var clientType = await _applicationManager.GetClientTypeAsync(application);
var consentType = await _applicationManager.GetConsentTypeAsync(application);
var permissions = await _applicationManager.GetPermissionsAsync(application);
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
var newSecret = GenerateSecureSecret();
var descriptor = new OpenIddictApplicationDescriptor var descriptor = new OpenIddictApplicationDescriptor
{ {
ClientId = client.ClientId, ClientId = clientIdValue,
ClientSecret = client.ClientSecret, ClientSecret = newSecret,
DisplayName = dto.DisplayName ?? client.DisplayName DisplayName = displayNameValue,
ClientType = clientType,
ConsentType = consentType,
}; };
var redirectUris = dto.RedirectUris ?? client.RedirectUris; foreach (var permission in permissions)
foreach (var uri in redirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
{ {
descriptor.RedirectUris.Add(new Uri(uri)); descriptor.Permissions.Add(permission);
} }
var postLogoutUris = dto.PostLogoutRedirectUris ?? client.PostLogoutRedirectUris; foreach (var uriString in redirectUris)
foreach (var uri in postLogoutUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
{ {
descriptor.PostLogoutRedirectUris.Add(new Uri(uri)); if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.RedirectUris.Add(uri);
}
} }
var grantTypes = dto.GrantTypes ?? client.GrantTypes; foreach (var uriString in postLogoutRedirectUris)
foreach (var grantType in grantTypes)
{ {
descriptor.Permissions.Add(grantType); if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.PostLogoutRedirectUris.Add(uri);
} }
var scopes = dto.Scopes ?? client.Scopes;
foreach (var scope in scopes)
{
descriptor.Permissions.Add(scope);
} }
await _applicationManager.UpdateAsync(application, descriptor); await _applicationManager.UpdateAsync(application, descriptor);
client.DisplayName = dto.DisplayName ?? client.DisplayName; return Ok(new
client.RedirectUris = redirectUris; {
client.PostLogoutRedirectUris = postLogoutUris; clientId = clientIdValue,
client.Scopes = scopes; clientSecret = newSecret,
client.GrantTypes = grantTypes; message = "新密钥已生成,请妥善保管,刷新后将无法再次查看"
client.ClientType = dto.ClientType ?? client.ClientType; });
client.ConsentType = dto.ConsentType ?? client.ConsentType;
client.Status = dto.Status ?? client.Status;
client.Description = dto.Description ?? client.Description;
client.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await CreateAuditLog("oauth", "update", "OAuthClient", client.Id, client.DisplayName, null, SerializeToJson(client));
return NoContent();
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> DeleteClient(long id) public async Task<IActionResult> DeleteClient(string id)
{ {
var client = await _context.OAuthApplications.FindAsync(id); var application = await _applicationManager.FindByIdAsync(id);
if (client == null) if (application == null)
{ {
return NotFound(); return NotFound();
} }
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
try try
{
var application = await _applicationManager.FindByClientIdAsync(client.ClientId);
if (application != null)
{ {
await _applicationManager.DeleteAsync(application); await _applicationManager.DeleteAsync(application);
_logger.LogInformation("Deleted OpenIddict application {ClientId}", client.ClientId); _logger.LogInformation("Deleted OpenIddict application {ClientId}", clientIdValue);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Failed to delete OpenIddict application for client {ClientId}", client.ClientId); _logger.LogWarning(ex, "Failed to delete OpenIddict application for client {ClientId}", clientIdValue);
return BadRequest(new { message = ex.Message });
} }
_context.OAuthApplications.Remove(client); return NoContent();
await _context.SaveChangesAsync(); }
await CreateAuditLog("oauth", "delete", "OAuthClient", client.Id, client.DisplayName, SerializeToJson(client)); [HttpPut("{id}")]
public async Task<IActionResult> UpdateClient(string id, [FromBody] UpdateOAuthClientDto dto)
{
var application = await _applicationManager.FindByIdAsync(id);
if (application == null)
{
return NotFound();
}
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
var clientType = await _applicationManager.GetClientTypeAsync(application);
var consentType = await _applicationManager.GetConsentTypeAsync(application);
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
var existingPermissions = await _applicationManager.GetPermissionsAsync(application);
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = clientIdValue,
DisplayName = dto.DisplayName ?? displayNameValue,
ClientType = string.IsNullOrEmpty(dto.ClientType) ? clientType?.ToString() : dto.ClientType,
ConsentType = string.IsNullOrEmpty(dto.ConsentType) ? consentType?.ToString() : dto.ConsentType,
};
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
if (dto.RedirectUris != null)
{
foreach (var uri in dto.RedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
{
descriptor.RedirectUris.Add(new Uri(uri));
}
}
else
{
foreach (var uriString in redirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.RedirectUris.Add(uri);
}
}
}
if (dto.PostLogoutRedirectUris != null)
{
foreach (var uri in dto.PostLogoutRedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
{
descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
}
}
else
{
foreach (var uriString in postLogoutRedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.PostLogoutRedirectUris.Add(uri);
}
}
}
var grantTypes = dto.GrantTypes ?? existingPermissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length));
var scopes = dto.Scopes ?? existingPermissions
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length));
foreach (var grantType in grantTypes)
{
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.GrantType + grantType);
}
foreach (var scope in scopes)
{
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
}
await _applicationManager.UpdateAsync(application, descriptor);
return NoContent(); return NoContent();
} }
@ -392,38 +399,6 @@ public class OAuthClientsController : ControllerBase
var bytes = RandomNumberGenerator.GetBytes(length); var bytes = RandomNumberGenerator.GetBytes(length);
return new string(bytes.Select(b => chars[b % chars.Length]).ToArray()); return new string(bytes.Select(b => chars[b % chars.Length]).ToArray());
} }
private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null)
{
var userName = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.Identity?.Name ?? "system";
var tenantId = User.FindFirstValue("TenantId");
var log = new AuditLog
{
Operator = userName,
TenantId = tenantId,
Operation = operation,
Action = action,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
Status = "success",
OldValue = oldValue,
NewValue = newValue,
};
_context.AuditLogs.Add(log);
await _context.SaveChangesAsync();
}
private static string SerializeToJson(object obj)
{
return System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = false
});
}
} }
public class CreateOAuthClientDto public class CreateOAuthClientDto

View File

@ -3,6 +3,7 @@ using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
namespace Fengling.AuthService.Controllers; namespace Fengling.AuthService.Controllers;
@ -12,13 +13,16 @@ namespace Fengling.AuthService.Controllers;
public class StatsController : ControllerBase public class StatsController : ControllerBase
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly ILogger<StatsController> _logger; private readonly ILogger<StatsController> _logger;
public StatsController( public StatsController(
ApplicationDbContext context, ApplicationDbContext context,
IOpenIddictApplicationManager applicationManager,
ILogger<StatsController> logger) ILogger<StatsController> logger)
{ {
_context = context; _context = context;
_applicationManager = applicationManager;
_logger = logger; _logger = logger;
} }
@ -30,7 +34,7 @@ public class StatsController : ControllerBase
var userCount = await _context.Users.CountAsync(u => !u.IsDeleted); var userCount = await _context.Users.CountAsync(u => !u.IsDeleted);
var tenantCount = await _context.Tenants.CountAsync(t => !t.IsDeleted); var tenantCount = await _context.Tenants.CountAsync(t => !t.IsDeleted);
var oauthClientCount = await _context.OAuthApplications.CountAsync(); var oauthClientCount = await CountOAuthClientsAsync();
var todayAccessCount = await _context.AccessLogs var todayAccessCount = await _context.AccessLogs
.CountAsync(l => l.CreatedAt >= today && l.CreatedAt < tomorrow); .CountAsync(l => l.CreatedAt >= today && l.CreatedAt < tomorrow);
@ -43,6 +47,17 @@ public class StatsController : ControllerBase
}); });
} }
private async Task<int> CountOAuthClientsAsync()
{
var count = 0;
var applications = _applicationManager.ListAsync();
await foreach (var _ in applications)
{
count++;
}
return count;
}
[HttpGet("system")] [HttpGet("system")]
public ActionResult<object> GetSystemStats() public ActionResult<object> GetSystemStats()
{ {

View File

@ -23,14 +23,13 @@ public class TokenController(
ILogger<TokenController> logger) ILogger<TokenController> logger)
: ControllerBase : ControllerBase
{ {
private readonly ILogger<TokenController> _logger = logger;
[HttpPost("token")] [HttpPost("token")]
public async Task<IActionResult> Exchange() public async Task<IActionResult> Exchange()
{ {
var request = HttpContext.GetOpenIddictServerRequest() ?? var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("OpenIddict request is null"); throw new InvalidOperationException("OpenIddict request is null");
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
if (request.IsAuthorizationCodeGrantType()) if (request.IsAuthorizationCodeGrantType())
{ {
@ -110,6 +109,12 @@ public class TokenController(
var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity); var principal = new ClaimsPrincipal(identity);
// 设置 Claim Destinations
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetScopes(request.GetScopes()); principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
@ -199,8 +204,14 @@ public class TokenController(
yield break; yield break;
// 明确处理租户 ID - 这是业务关键信息
case "tenant_id":
yield return OpenIddictConstants.Destinations.AccessToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value. // Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break; case "AspNet.Identity.SecurityStamp":
yield break;
default: default:
yield return OpenIddictConstants.Destinations.AccessToken; yield return OpenIddictConstants.Destinations.AccessToken;
@ -247,6 +258,12 @@ public class TokenController(
var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity); var principal = new ClaimsPrincipal(identity);
// 设置 Claim Destinations
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
principal.SetScopes(request.GetScopes()); principal.SetScopes(request.GetScopes());
principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync()); principal.SetResources(await scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());

View File

@ -11,7 +11,6 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, Applicati
{ {
} }
public DbSet<OAuthApplication> OAuthApplications { get; set; }
public DbSet<Tenant> Tenants { get; set; } public DbSet<Tenant> Tenants { get; set; }
public DbSet<AccessLog> AccessLogs { get; set; } public DbSet<AccessLog> AccessLogs { get; set; }
public DbSet<AuditLog> AuditLogs { get; set; } public DbSet<AuditLog> AuditLogs { get; set; }
@ -35,23 +34,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, Applicati
}); });
}); });
builder.Entity<ApplicationRole>(entity => builder.Entity<ApplicationRole>(entity => { entity.Property(e => e.Description).HasMaxLength(200); });
{
entity.Property(e => e.Description).HasMaxLength(200);
});
builder.Entity<OAuthApplication>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ClientId).IsUnique();
entity.Property(e => e.ClientId).HasMaxLength(100);
entity.Property(e => e.ClientSecret).HasMaxLength(200);
entity.Property(e => e.DisplayName).HasMaxLength(100);
entity.Property(e => e.ClientType).HasMaxLength(20);
entity.Property(e => e.ConsentType).HasMaxLength(20);
entity.Property(e => e.Status).HasMaxLength(20);
entity.Property(e => e.Description).HasMaxLength(500);
});
builder.Entity<Tenant>(entity => builder.Entity<Tenant>(entity =>
{ {

View File

@ -10,7 +10,7 @@ public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<Applicati
{ {
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseNpgsql("Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542"); optionsBuilder.UseNpgsql("Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542");
optionsBuilder.UseOpenIddict();
return new ApplicationDbContext(optionsBuilder.Options); return new ApplicationDbContext(optionsBuilder.Options);
} }
} }

View File

@ -1,6 +1,7 @@
using Fengling.AuthService.Models; using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
namespace Fengling.AuthService.Data; namespace Fengling.AuthService.Data;
@ -12,6 +13,8 @@ public static class SeedData
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>(); var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
var applicationManager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var scopeManager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
await context.Database.EnsureCreatedAsync(); await context.Database.EnsureCreatedAsync();
@ -119,32 +122,70 @@ public static class SeedData
} }
} }
var consoleClient = await context.OAuthApplications var consoleClient = await applicationManager.FindByClientIdAsync("fengling-console");
.FirstOrDefaultAsync(c => c.ClientId == "fengling-console");
if (consoleClient == null) if (consoleClient == null)
{ {
consoleClient = new OAuthApplication var descriptor = new OpenIddictApplicationDescriptor
{ {
ClientId = "fengling-console", ClientId = "fengling-console",
ClientSecret = null,
DisplayName = "Fengling Console", DisplayName = "Fengling Console",
RedirectUris = new[] { Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.EndSession,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Introspection
}
};
foreach (var uri in new[]
{
"http://localhost:5777/auth/callback", "http://localhost:5777/auth/callback",
"https://console.fengling.local/auth/callback" "https://console.fengling.local/auth/callback"
}, })
PostLogoutRedirectUris = new[] { {
descriptor.RedirectUris.Add(new Uri(uri));
}
foreach (var uri in new[]
{
"http://localhost:5777/", "http://localhost:5777/",
"https://console.fengling.local/" "https://console.fengling.local/"
}, })
Scopes = new[] { "api", "offline_access", "openid", "profile", "email" }, {
GrantTypes = new[] { "authorization_code", "refresh_token" }, descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
ClientType = "public", }
ConsentType = "implicit",
Status = "active", descriptor.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
CreatedAt = DateTime.UtcNow
var scopes = new[]
{
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
OpenIddictConstants.Permissions.Prefixes.Scope + OpenIddictConstants.Scopes.OfflineAccess,
OpenIddictConstants.Permissions.Prefixes.Scope + OpenIddictConstants.Scopes.OpenId,
OpenIddictConstants.Permissions.Prefixes.Scope + OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Permissions.Prefixes.Scope + OpenIddictConstants.Scopes.Roles,
OpenIddictConstants.Permissions.Prefixes.Scope + OpenIddictConstants.Scopes.Email
}; };
context.OAuthApplications.Add(consoleClient);
await context.SaveChangesAsync(); foreach (var permissionScope in scopes)
{
descriptor.Permissions.Add(permissionScope);
}
var grantTypes = new[]
{
OpenIddictConstants.Permissions.Prefixes.GrantType + OpenIddictConstants.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.Prefixes.GrantType + OpenIddictConstants.GrantTypes.RefreshToken
};
foreach (var grantType in grantTypes)
{
descriptor.Permissions.Add(grantType);
}
await applicationManager.CreateAsync(descriptor);
} }
} }
} }

View File

@ -6,6 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="OpenIddict.Quartz" />
<PackageReference Include="Swashbuckle.AspNetCore" /> <PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Fengling.AuthService.Migrations namespace Fengling.AuthService.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
[Migration("20260205165820_InitialCreate")] [Migration("20260206142720_inital")]
partial class InitialCreate partial class inital
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -550,6 +550,214 @@ namespace Fengling.AuthService.Migrations
b.ToTable("AspNetUserTokens", (string)null); b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasColumnType("text");
b.Property<string>("ClientType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("JsonWebKeySet")
.HasColumnType("text");
b.Property<string>("Permissions")
.HasColumnType("text");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("RedirectUris")
.HasColumnType("text");
b.Property<string>("Requirements")
.HasColumnType("text");
b.Property<string>("Settings")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Scopes")
.HasColumnType("text");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Descriptions")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Resources")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("OpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("AuthorizationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Payload")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("Type")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique();
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictTokens", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b => modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{ {
b.OwnsOne("Fengling.AuthService.Models.TenantInfo", "TenantInfo", b1 => b.OwnsOne("Fengling.AuthService.Models.TenantInfo", "TenantInfo", b1 =>
@ -633,6 +841,42 @@ namespace Fengling.AuthService.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Fengling.AuthService.Migrations namespace Fengling.AuthService.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class InitialCreate : Migration public partial class inital : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@ -144,6 +144,51 @@ namespace Fengling.AuthService.Migrations
table.PrimaryKey("PK_OAuthApplications", x => x.Id); table.PrimaryKey("PK_OAuthApplications", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "OpenIddictApplications",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ApplicationType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ClientId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ClientSecret = table.Column<string>(type: "text", nullable: true),
ClientType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ConsentType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: true),
DisplayNames = table.Column<string>(type: "text", nullable: true),
JsonWebKeySet = table.Column<string>(type: "text", nullable: true),
Permissions = table.Column<string>(type: "text", nullable: true),
PostLogoutRedirectUris = table.Column<string>(type: "text", nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
RedirectUris = table.Column<string>(type: "text", nullable: true),
Requirements = table.Column<string>(type: "text", nullable: true),
Settings = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OpenIddictScopes",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Descriptions = table.Column<string>(type: "text", nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: true),
DisplayNames = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
Resources = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Tenants", name: "Tenants",
columns: table => new columns: table => new
@ -274,6 +319,63 @@ namespace Fengling.AuthService.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "OpenIddictAuthorizations",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ApplicationId = table.Column<string>(type: "text", nullable: true),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
Scopes = table.Column<string>(type: "text", nullable: true),
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_Application~",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "OpenIddictTokens",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ApplicationId = table.Column<string>(type: "text", nullable: true),
AuthorizationId = table.Column<string>(type: "text", nullable: true),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ExpirationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Payload = table.Column<string>(type: "text", nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
RedemptionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ReferenceId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id");
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "OpenIddictAuthorizations",
principalColumn: "Id");
});
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_AccessLogs_Action", name: "IX_AccessLogs_Action",
table: "AccessLogs", table: "AccessLogs",
@ -373,6 +475,39 @@ namespace Fengling.AuthService.Migrations
column: "ClientId", column: "ClientId",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenIddictApplications_ClientId",
table: "OpenIddictApplications",
column: "ClientId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
table: "OpenIddictAuthorizations",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_OpenIddictScopes_Name",
table: "OpenIddictScopes",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type",
table: "OpenIddictTokens",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_AuthorizationId",
table: "OpenIddictTokens",
column: "AuthorizationId");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_ReferenceId",
table: "OpenIddictTokens",
column: "ReferenceId",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Tenants_TenantId", name: "IX_Tenants_TenantId",
table: "Tenants", table: "Tenants",
@ -407,6 +542,12 @@ namespace Fengling.AuthService.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "OAuthApplications"); name: "OAuthApplications");
migrationBuilder.DropTable(
name: "OpenIddictScopes");
migrationBuilder.DropTable(
name: "OpenIddictTokens");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Tenants"); name: "Tenants");
@ -415,6 +556,12 @@ namespace Fengling.AuthService.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AspNetUsers"); name: "AspNetUsers");
migrationBuilder.DropTable(
name: "OpenIddictAuthorizations");
migrationBuilder.DropTable(
name: "OpenIddictApplications");
} }
} }
} }

View File

@ -547,6 +547,214 @@ namespace Fengling.AuthService.Migrations
b.ToTable("AspNetUserTokens", (string)null); b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasColumnType("text");
b.Property<string>("ClientType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("JsonWebKeySet")
.HasColumnType("text");
b.Property<string>("Permissions")
.HasColumnType("text");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("RedirectUris")
.HasColumnType("text");
b.Property<string>("Requirements")
.HasColumnType("text");
b.Property<string>("Settings")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Scopes")
.HasColumnType("text");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Descriptions")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Resources")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("OpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("AuthorizationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Payload")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("Type")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique();
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictTokens", (string)null);
});
modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b => modelBuilder.Entity("Fengling.AuthService.Models.ApplicationUser", b =>
{ {
b.OwnsOne("Fengling.AuthService.Models.TenantInfo", "TenantInfo", b1 => b.OwnsOne("Fengling.AuthService.Models.TenantInfo", "TenantInfo", b1 =>
@ -630,6 +838,42 @@ namespace Fengling.AuthService.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Fengling.AuthService.Models;
public class OAuthApplication
{
public long Id { get; set; }
public string ClientId { get; set; } = string.Empty;
public string? ClientSecret { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string[] RedirectUris { get; set; } = Array.Empty<string>();
public string[] PostLogoutRedirectUris { get; set; } = Array.Empty<string>();
public string[] Scopes { get; set; } = Array.Empty<string>();
public string[] GrantTypes { get; set; } = Array.Empty<string>();
public string ClientType { get; set; } = "public";
public string ConsentType { get; set; } = "implicit";
public string Status { get; set; } = "active";
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}

View File

@ -22,15 +22,9 @@ builder.Host.UseSerilog();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options => builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
if (connectionString.StartsWith("DataSource="))
{
options.UseInMemoryDatabase(connectionString);
}
else
{ {
options.UseNpgsql(connectionString); options.UseNpgsql(connectionString);
} options.UseOpenIddict();
}); });
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
@ -96,6 +90,14 @@ using (var scope = app.Services.CreateScope())
await SeedData.Initialize(scope.ServiceProvider); await SeedData.Initialize(scope.ServiceProvider);
} }
app.UseCors(x =>
{
x.SetIsOriginAllowed(origin => true)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.Build();
});
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();

View File

@ -6,21 +6,21 @@ public class RegisterViewModel
{ {
[Required(ErrorMessage = "用户名不能为空")] [Required(ErrorMessage = "用户名不能为空")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-50个字符之间")] [StringLength(50, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-50个字符之间")]
public string Username { get; set; } public string Username { get; set; } = default!;
[Required(ErrorMessage = "邮箱不能为空")] [Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "请输入有效的邮箱地址")] [EmailAddress(ErrorMessage = "请输入有效的邮箱地址")]
public string Email { get; set; } public string Email { get; set; }= default!;
[Required(ErrorMessage = "密码不能为空")] [Required(ErrorMessage = "密码不能为空")]
[StringLength(100, MinimumLength = 6, ErrorMessage = "密码长度必须在6-100个字符之间")] [StringLength(100, MinimumLength = 6, ErrorMessage = "密码长度必须在6-100个字符之间")]
[DataType(DataType.Password)] [DataType(DataType.Password)]
public string Password { get; set; } public string Password { get; set; }= default!;
[Required(ErrorMessage = "确认密码不能为空")] [Required(ErrorMessage = "确认密码不能为空")]
[DataType(DataType.Password)] [DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "两次输入的密码不一致")] [Compare("Password", ErrorMessage = "两次输入的密码不一致")]
public string ConfirmPassword { get; set; } public string ConfirmPassword { get; set; }= default!;
public string ReturnUrl { get; set; } public string ReturnUrl { get; set; }= default!;
} }

View File

@ -1,3 +1,4 @@
@using Microsoft.Extensions.Primitives
@model Fengling.AuthService.ViewModels.AuthorizeViewModel @model Fengling.AuthService.ViewModels.AuthorizeViewModel
@{ @{
@ -10,7 +11,8 @@
<!-- Header --> <!-- Header -->
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 mb-4"> <div class="inline-flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 mb-4">
<svg class="h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg> </svg>
</div> </div>
@ -27,7 +29,8 @@
<div class="p-6 border-b border-border"> <div class="p-6 border-b border-border">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="h-12 w-12 rounded-lg bg-primary flex items-center justify-center text-primary-foreground text-lg font-semibold"> <div
class="h-12 w-12 rounded-lg bg-primary flex items-center justify-center text-primary-foreground text-lg font-semibold">
@(Model.ApplicationName?.Substring(0, Math.Min(1, Model.ApplicationName.Length)).ToUpper() ?? "A") @(Model.ApplicationName?.Substring(0, Math.Min(1, Model.ApplicationName.Length)).ToUpper() ?? "A")
</div> </div>
</div> </div>
@ -49,7 +52,10 @@
@foreach (var scope in Model.Scopes) @foreach (var scope in Model.Scopes)
{ {
<div class="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> <div class="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
<svg class="h-5 w-5 text-primary mt-0.5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="h-5 w-5 text-primary mt-0.5 flex-shrink-0"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/> <polyline points="22 4 12 14.01 9 11.01"/>
</svg> </svg>
@ -70,7 +76,9 @@
<!-- Warning Section --> <!-- Warning Section -->
<div class="p-4 bg-destructive/5 border-t border-border"> <div class="p-4 bg-destructive/5 border-t border-border">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<svg class="h-4 w-4 text-destructive mt-0.5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="h-4 w-4 text-destructive mt-0.5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/> <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/>
<path d="M12 9v4"/> <path d="M12 9v4"/>
<path d="M12 17h.01"/> <path d="M12 17h.01"/>
@ -83,19 +91,28 @@
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<form method="post" class="mt-6 space-y-3"> <form asp-controller="Authorization" asp-action="Authorize" method="post" class="mt-6 space-y-3">
@* Flow the request parameters so they can be received by the Accept/Reject actions: *@
@foreach (var parameter in Context.Request.HasFormContentType ?
(IEnumerable<KeyValuePair<string, StringValues>>) Context.Request.Form : Context.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<button type="submit" name="action" value="accept" <button type="submit" name="submit.Accept" value="Yes"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 shadow"> class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 shadow">
<svg class="mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/> <polyline points="22 4 12 14.01 9 11.01"/>
</svg> </svg>
授权 授权
</button> </button>
<button type="submit" name="action" value="deny" <button type="submit" name="submit.Deny" value="No"
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-11 px-8"> class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-11 px-8">
<svg class="mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg class="mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/> <line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/> <line x1="6" y1="6" x2="18" y2="18"/>
</svg> </svg>
@ -114,6 +131,7 @@
</div> </div>
@functions { @functions {
private string GetScopeDisplayName(string scope) private string GetScopeDisplayName(string scope)
{ {
return scope switch return scope switch
@ -143,4 +161,5 @@
_ => "自定义权限范围" _ => "自定义权限范围"
}; };
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "DataSource=:memory:" "DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View File

@ -3,12 +3,12 @@
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542" "DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542"
}, },
"Jwt": { "Jwt": {
"Issuer": "https://auth.fengling.local", "Issuer": "http://localhost:5132",
"Audience": "fengling-api", "Audience": "fengling-api",
"Secret": "FenglingAuthSecretKey2024!ChangeThisInProduction!" "Secret": "FenglingAuthSecretKey2024!ChangeThisInProduction!"
}, },
"OpenIddict": { "OpenIddict": {
"Issuer": "https://auth.fengling.local", "Issuer": "http://localhost:5132",
"Audience": "fengling-api" "Audience": "fengling-api"
}, },
"Logging": { "Logging": {