Browse Source

feat: 后台登录页面支持多租户

9.3.4.8
zzzwangjun@gmail.com 5 months ago
parent
commit
68cb4ef31f
  1. 2
      aspnet-core/frameworks/src/Lion.AbpPro.AspNetCore/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs
  2. 7
      aspnet-core/modules/BasicManagement/src/Lion.AbpPro.BasicManagement.Application/Roles/RoleAppService.cs
  3. 204
      aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml
  4. 60
      aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml.cs
  5. 274
      aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Monitor.cshtml
  6. 4
      vben28/.env.production

2
aspnet-core/frameworks/src/Lion.AbpPro.AspNetCore/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs

@ -62,7 +62,7 @@ public static class ServiceCollectionExtensions
// options.TenantResolvers.Add(new QueryStringTenantResolveContributor());
// options.TenantResolvers.Add(new RouteTenantResolveContributor());
options.TenantResolvers.Add(new HeaderTenantResolveContributor());
// options.TenantResolvers.Add(new CookieTenantResolveContributor());
options.TenantResolvers.Add(new CookieTenantResolveContributor());
});
return service;

7
aspnet-core/modules/BasicManagement/src/Lion.AbpPro.BasicManagement.Application/Roles/RoleAppService.cs

@ -4,16 +4,18 @@ namespace Lion.AbpPro.BasicManagement.Roles;
public class RoleAppService : BasicManagementAppService, IRoleAppService
{
private readonly IIdentityRoleAppService _identityRoleAppService;
private readonly IIdentityRoleRepository _roleRepository;
private readonly ICurrentTenant _currentTenant;
public RoleAppService(
IIdentityRoleAppService identityRoleAppService,
IIdentityRoleRepository roleRepository)
IIdentityRoleRepository roleRepository,
ICurrentTenant currentTenant)
{
_identityRoleAppService = identityRoleAppService;
_roleRepository = roleRepository;
_currentTenant = currentTenant;
}
/// <summary>
@ -60,6 +62,7 @@ public class RoleAppService : BasicManagementAppService, IRoleAppService
[Authorize(IdentityPermissions.Roles.Create)]
public virtual async Task<IdentityRoleDto> CreateAsync(IdentityRoleCreateDto input)
{
var s = _currentTenant;
return await _identityRoleAppService.CreateAsync(input);
}

204
aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml

@ -1,3 +1,4 @@
@page
@model Lion.AbpPro.Pages.Login
@ -11,19 +12,43 @@
<head>
<title>后台服务登录</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-offset-3 col-md-6">
<form class="form-horizontal" method="post">
<div class="container-fluid d-flex align-items-center justify-content-center min-vh-100">
<div class="row w-100 justify-content-center">
<div class="col-md-6">
<form class="form-horizontal" method="post" novalidate>
@Html.AntiForgeryToken()
<!-- 错误提示区域 -->
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>@TempData["ErrorMessage"]
</div>
}
<span class="heading">后台服务登录</span>
@if (TempData["EnableTenant"] != null && (bool)TempData["EnableTenant"])
{
<div class="form-group">
<input type="text" class="form-control" name="userName" placeholder="用户名">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-building"></i></span>
<input type="text" class="form-control" name="tenantName" placeholder="租户名称(可选)">
</div>
</div>
}
<div class="form-group">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" name="userName" placeholder="用户名" required>
</div>
</div>
<div class="form-group help">
<input type="password" class="form-control" name="password" placeholder="密码">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" name="password" placeholder="密码" required>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">登录</button>
@ -34,141 +59,126 @@
</div>
</body>
</html>
<style>
.row {
width: 800px;
height: auto;
margin: auto;
box-sizing: border-box;
transform: translate(0, 50%);
<style> body {
background: linear-gradient(120deg, #3498db, #8e44ad);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.form-horizontal {
background: #fff;
background: rgba(255, 255, 255, 0.9);
padding-bottom: 40px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 400px;
margin: 0 auto;
}
.form-horizontal .heading {
display: block;
font-size: 35px;
font-size: 32px;
font-weight: 700;
padding: 35px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 30px;
color: #2c3e50;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.form-horizontal .form-group {
padding: 0 40px;
.form-group {
padding: 0 30px;
margin: 0 0 25px 0;
position: relative;
}
.form-horizontal .form-control {
background: #f0f0f0;
border: none;
border-radius: 20px;
box-shadow: none;
padding: 0 20px 0 45px;
height: 40px;
transition: all 0.3s ease 0s;
.input-group {
border-radius: 30px;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.form-horizontal .form-control:focus {
background: #e0e0e0;
box-shadow: none;
outline: 0 none;
.input-group:focus-within {
box-shadow: 0 6px 15px rgba(52, 152, 219, 0.4);
transform: translateY(-2px);
}
.form-horizontal .form-group i {
position: absolute;
top: 12px;
left: 60px;
font-size: 17px;
color: #c8c8c8;
transition: all 0.5s ease 0s;
.input-group-text {
background: #3498db;
border: none;
color: white;
border-radius: 0;
width: 45px;
font-size: 16px;
}
.form-horizontal .form-control:focus + i {
color: #00b4ef;
.form-control {
border: none;
padding: 12px 15px;
height: 45px;
font-size: 15px;
box-shadow: none;
}
.form-horizontal .fa-question-circle {
display: inline-block;
position: absolute;
top: 12px;
right: 60px;
font-size: 20px;
color: #808080;
transition: all 0.5s ease 0s;
.form-control:focus {
box-shadow: none;
outline: 0 none;
background-color: #f8f9fa;
}
.form-horizontal .fa-question-circle:hover {
color: #000;
.form-control:required:valid {
border-left: 3px solid #28a745;
}
.form-horizontal .main-checkbox {
float: left;
width: 20px;
height: 20px;
background: #11a3fc;
border-radius: 50%;
position: relative;
margin: 5px 0 0 5px;
border: 1px solid #11a3fc;
.form-control:required:invalid:not(:placeholder-shown) {
border-left: 3px solid #dc3545;
}
.form-horizontal .main-checkbox label {
width: 20px;
height: 20px;
position: absolute;
top: 0;
left: 0;
.form-horizontal .btn {
text-align: center;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(to right, #3498db, #8e44ad);
border-radius: 30px;
padding: 10px 25px;
border: none;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.4s ease 0s;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.4);
width: 100%;
cursor: pointer;
}
.form-horizontal .main-checkbox label:after {
content: "";
width: 10px;
height: 5px;
position: absolute;
top: 5px;
left: 4px;
border: 3px solid #fff;
border-top: none;
border-right: none;
background: transparent;
opacity: 0;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
.form-horizontal .btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.6);
}
.form-horizontal .main-checkbox input[type="checkbox"] {
visibility: hidden;
.form-horizontal .btn:active {
transform: translateY(0);
}
.form-horizontal .main-checkbox input[type="checkbox"]:checked + label:after {
opacity: 1;
.container-fluid {
padding: 10px;
}
.form-horizontal .text {
float: left;
margin-left: 7px;
line-height: 20px;
padding-top: 5px;
text-transform: capitalize;
.form-group {
padding: 0 15px;
margin: 0 0 20px 0;
}
.form-horizontal .btn {
text-align: center;
font-size: 14px;
color: #fff;
background: #00b4ef;
border-radius: 30px;
padding: 10px 25px;
border: none;
text-transform: capitalize;
transition: all 0.5s ease 0s;
.form-horizontal .heading {
font-size: 26px;
padding: 25px 0;
margin-bottom: 25px;
}
</style>

60
aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml.cs

@ -1,5 +1,7 @@
using Lion.AbpPro.BasicManagement.ConfigurationOptions;
using Lion.AbpPro.BasicManagement.Tenants;
using Lion.AbpPro.BasicManagement.Tenants.Dtos;
using Lion.AbpPro.BasicManagement.Users;
using Lion.AbpPro.BasicManagement.Users.Dtos;
using Microsoft.AspNetCore.Mvc.RazorPages;
@ -15,58 +17,94 @@ namespace Lion.AbpPro.Pages
private readonly IHostEnvironment _hostEnvironment;
private readonly JwtOptions _jwtOptions;
private readonly IClock _clock;
private readonly AbpAspNetCoreMultiTenancyOptions _abpAspNetCoreMultiTenancyOptions;
private readonly IVoloTenantAppService _voloTenantAppService;
private readonly AbpProMultiTenancyOptions _abpProMultiTenancyOptions;
public Login(IAccountAppService accountAppService,
ILogger<Login> logger,
IHostEnvironment hostEnvironment,
IOptionsSnapshot<JwtOptions> jwtOptions,
IClock clock)
IClock clock,
IOptions<AbpAspNetCoreMultiTenancyOptions> abpAspNetCoreMultiTenancyOptions,
IVoloTenantAppService voloTenantAppService,
IOptions<AbpProMultiTenancyOptions> abpProMultiTenancyOptions)
{
_accountAppService = accountAppService;
_logger = logger;
_hostEnvironment = hostEnvironment;
_clock = clock;
_voloTenantAppService = voloTenantAppService;
_abpProMultiTenancyOptions = abpProMultiTenancyOptions.Value;
_abpAspNetCoreMultiTenancyOptions = abpAspNetCoreMultiTenancyOptions.Value;
_jwtOptions = jwtOptions.Value;
}
public void OnGet()
{
ViewData["ErrorMessage"] = null;
TempData["EnableTenant"] = _abpProMultiTenancyOptions.Enabled;
}
public async Task OnPost()
{
TempData["EnableTenant"] = _abpProMultiTenancyOptions.Enabled;
string tenantName = Request.Form["tenantName"];
string userName = Request.Form["userName"];
string password = Request.Form["password"];
if (userName.IsNullOrWhiteSpace() || password.IsNullOrWhiteSpace())
{
Response.Redirect("/Login");
// 添加错误提示信息
TempData["ErrorMessage"] = "用户名和密码不能为空";
return;
}
try
{
Guid? tenantId = null;
// 判断租户是否存在
if (tenantName.IsNotNullOrWhiteSpace())
{
var tenant = await _voloTenantAppService.FindTenantByNameAsync(new FindTenantByNameInput() { Name = tenantName });
if (!tenant.Success)
{
TempData["ErrorMessage"] = $"租户[{tenantName}]不存在";
return;
}
tenantId = tenant.TenantId;
}
var options = new CookieOptions
{
Expires = _clock.Now.AddHours(_jwtOptions.ExpirationTime),
SameSite = SameSiteMode.Unspecified,
};
var result = await _accountAppService.LoginAsync(new LoginInput() { Name = userName, Password = password });
// 设置cookies domain
//options.Domain = "AbpPro.cn";
// 清除现有的认证 cookies
Response.Cookies.Delete(AbpProAspNetCoreConsts.DefaultCookieName);
Response.Cookies.Delete(_abpAspNetCoreMultiTenancyOptions.TenantKey);
Response.Cookies.Append(AbpProAspNetCoreConsts.DefaultCookieName, result.Token, options);
if (tenantId.HasValue)
{
Response.Cookies.Append(_abpAspNetCoreMultiTenancyOptions.TenantKey, tenantId.ToString(), options);
}
var result = await _accountAppService.LoginAsync(new LoginInput()
{ Name = userName, Password = password });
Response.Cookies.Append(AbpProAspNetCoreConsts.DefaultCookieName,
result.Token, options);
}
catch (BusinessException e)
{
_logger.LogError($"登录失败:{e.Message}");
TempData["ErrorMessage"] = $"用户名或者密码错误";
return;
}
catch (Exception e)
{
_logger.LogError($"登录失败:{e.Message}");
Response.Redirect("/Login");
TempData["ErrorMessage"] = $"登录失败:{e.Message}";
return;
}
Response.Redirect("/monitor");
}
}

274
aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Monitor.cshtml

@ -1,204 +1,228 @@
@page
@using Lion.AbpPro
@model Lion.AbpPro.Pages.Monitor
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<title>后端服务</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<title>后端服务监控</title>
</head>
<body>
<div class="container projects ">
<div class="projects-header page-header">
<h2>后端服务列表</h2>
@* <p>这些项目或者是对Bootstrap进行了有益的补充,或者是基于Bootstrap开发的</p> *@
</div>
<div class="row">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="thumbnail" style="height: 180px">
<a href="@AbpProHttpApiHostConst.SwaggerUiEndPoint" target="_blank">
<img class="lazy" src="/images/swagger.png" width="300" height="150"/>
</a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.SwaggerUiEndPoint" target="_blank">SwaggerUI</a>
</h3>
<div class="page-wrapper">
<div class="container">
<div class="page-header">
<h1>后端服务监控面板</h1>
<p class="subtitle">请选择您要访问的服务面板</p>
</div>
<div class="services-grid">
<div class="service-item">
<a href="@AbpProHttpApiHostConst.SwaggerUiEndPoint" target="_blank" class="service-link">
<div class="service-icon">
<i class="fas fa-book"></i>
</div>
<div class="service-info">
<h3>SwaggerUI</h3>
<p class="description">API文档与测试</p>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="thumbnail" style="height: 180px">
<a href="@AbpProHttpApiHostConst.CapDashboardEndPoint" target="_blank">
<img class="lazy" src="/images/cap.png" width="300" height="150"/>
</a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.CapDashboardEndPoint" target="_blank">CAP面板</a>
</h3>
</div>
<div class="service-item">
<a href="@AbpProHttpApiHostConst.CapDashboardEndPoint" target="_blank" class="service-link">
<div class="service-icon">
<i class="fas fa-exchange-alt"></i>
</div>
<div class="service-info">
<h3>CAP面板</h3>
<p class="description">事件总线监控</p>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="thumbnail" style="height: 180px">
<a href="@AbpProHttpApiHostConst.HangfireDashboardEndPoint" target="_blank">
<img class="lazy" src="/images/hangfire.png" width="300" height="150"/>
</a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.HangfireDashboardEndPoint" target="_blank">Hangfire面板</a>
</h3>
</div>
<div class="service-item">
<a href="@AbpProHttpApiHostConst.HangfireDashboardEndPoint" target="_blank" class="service-link">
<div class="service-icon">
<i class="fas fa-tasks"></i>
</div>
<div class="service-info">
<h3>Hangfire面板</h3>
<p class="description">后台任务管理</p>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="thumbnail" style="height: 180px">
<a href="@AbpProHttpApiHostConst.MiniprofilerEndPoint" target="_blank">
<img class="lazy" src="/images/miniprofiler.png" width="300" height="150"/>
</a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.MiniprofilerEndPoint" target="_blank">Miniprofiler</a>
</h3>
</div>
<div class="service-item">
<a href="@AbpProHttpApiHostConst.MiniprofilerEndPoint" target="_blank" class="service-link">
<div class="service-icon">
<i class="fas fa-chart-line"></i>
</div>
<div class="service-info">
<h3>Miniprofiler</h3>
<p class="description">性能分析工具</p>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="thumbnail" style="height: 180px">
<a href="@AbpProHttpApiHostConst.MoreEndPoint" target="_blank">
<img class="lazy" src="/images/more.png" width="300" height="150"/>
</a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.MoreEndPoint" target="_blank">了解更多...</a>
</h3>
</div>
<div class="service-item">
<a href="@AbpProHttpApiHostConst.MoreEndPoint" target="_blank" class="service-link">
<div class="service-icon">
<i class="fas fa-ellipsis-h"></i>
</div>
<div class="service-info">
<h3>了解更多...</h3>
<p class="description">更多服务选项</p>
</div>
</a>
</div>
</div>
</div>
</div>
</body>
</html>
<style>
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
min-height: 100vh;
padding: 20px;
}
.page-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 40px);
}
.container {
width: 1170px;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
max-width: 1000px;
width: 100%;
background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
padding: 40px;
}
.projects-header {
width: 60%;
.page-header {
text-align: center;
font-weight: 200;
display: block;
margin: 60px auto 40px !important;
margin-bottom: 40px;
}
.page-header {
padding-bottom: 9px;
margin: 40px auto;
border-bottom: 1px solid #eee;
.page-header h1 {
font-size: 28px;
font-weight: 600;
color: #2d3748;
margin-bottom: 10px;
}
.projects-header h2 {
font-size: 42px;
letter-spacing: -1px;
.subtitle {
font-size: 16px;
color: #718096;
}
h2 {
margin-top: 20px;
margin-bottom: 10px;
font-weight: 500;
line-height: 1.1;
color: inherit;
/* text-align: center; */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 25px;
}
.service-item {
border-radius: 10px;
overflow: hidden;
transition: all 0.3s ease;
background: #ffffff;
border: 1px solid #e2e8f0;
}
p {
margin: 0 0 10px;
.service-item:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
border-color: #cbd5e0;
}
.row {
margin-right: -15px;
margin-left: -15px;
.service-link {
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
padding: 25px;
}
.col-lg-3 {
width: 25%;
.service-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background: #edf2f7;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
flex-shrink: 0;
}
.projects .thumbnail {
display: block;
margin-right: auto;
margin-left: auto;
text-align: center;
margin-bottom: 30px;
border-radius: 0;
.service-icon i {
font-size: 24px;
color: #4299e1;
}
.thumbnail {
display: block;
padding: 4px;
line-height: 1.42857143;
background-color: #fff;
border: 1px solid #ddd;
.service-info h3 {
font-size: 18px;
font-weight: 600;
color: #2d3748;
margin-bottom: 5px;
}
.
.description {
font-size: 14px;
color: #718096;
margin: 0;
}
transition(border 0.2 s ease-in-out);
/* 使用响应式单位和弹性布局替代媒体查询 */
.container {
padding: clamp(20px, 5vw, 40px);
}
a {
color: #337ab7;
text-decoration: none;
background-color: transparent;
.page-header h1 {
font-size: clamp(24px, 4vw, 28px);
}
.projects .thumbnail img {
max-width: 100%;
height: auto;
.service-link {
padding: clamp(15px, 3vw, 25px);
}
.thumbnail a > img,
.thumbnail > img {
margin-right: auto;
margin-left: auto;
.services-grid {
gap: clamp(15px, 3vw, 25px);
}
img {
vertical-align: middle;
.service-icon {
width: clamp(50px, 8vw, 60px);
height: clamp(50px, 8vw, 60px);
}
/* .projects .thumbnail .caption {
overflow-y: hidden;
color: #555;
} */
.caption {
padding: 9px;
overflow-y: hidden;
color: #555;
.service-icon i {
font-size: clamp(20px, 4vw, 24px);
}
</style>
</html>

4
vben28/.env.production

@ -36,8 +36,8 @@ VITE_LEGACY = false
# 接口地址
VITE_API_URL= http://43.139.143.143:8080
VITE_API_URL= http://139.155.114.244:44315
# WEBSOCKE 地址
VITE_WEBSOCKE_URL= http://43.139.143.143:8080/signalr/notification
VITE_WEBSOCKE_URL= http://139.155.114.244:44315/signalr/notification

Loading…
Cancel
Save