This commit is contained in:
2023-05-31 10:19:05 +08:00
parent 1b65a7a9e5
commit 9c621c75cd
238 changed files with 9905 additions and 4034 deletions

View File

@@ -35,6 +35,12 @@ using JNPF.Systems.Entitys.Model.Permission.SocialsUser;
using JNPF.Systems.Interfaces.Permission;
using JNPF.Extras.CollectiveOAuth.Models;
using JNPF.Common.Models;
using JNPF.Common.Options;
using Microsoft.CodeAnalysis;
using JNPF.Common.Core.Handlers;
using JNPF.Message.Interfaces.Message;
using JNPF.Extras.DatabaseAccessor.SqlSugar.Models;
using Aop.Api.Domain;
namespace JNPF.OAuth;
@@ -45,6 +51,11 @@ namespace JNPF.OAuth;
[Route("api/[controller]")]
public class OAuthService : IDynamicApiController, ITransient
{
/// <summary>
/// 配置文档.
/// </summary>
private readonly OauthOptions _oauthOptions = App.GetConfig<OauthOptions>("OAuth", true);
/// <summary>
/// 用户仓储.
/// </summary>
@@ -125,6 +136,10 @@ public class OAuthService : IDynamicApiController, ITransient
/// </summary>
private SqlSugarScope _sqlSugarClient;
private readonly ISendMessageService _sendMessageService;
private readonly IMHandler _imHandler;
/// <summary>
/// 初始化一个<see cref="OAuthService"/>类型的新实例.
/// </summary>
@@ -138,13 +153,15 @@ public class OAuthService : IDynamicApiController, ITransient
IModuleFormService formService,
ISysConfigService sysConfigService,
ISocialsUserService socialsUserService,
ISendMessageService sendMessageService,
IOptions<ConnectionStringsOptions> connectionOptions,
IOptions<TenantOptions> tenantOptions,
ISqlSugarClient sqlSugarClient,
IHttpContextAccessor httpContextAccessor,
ICacheManager cacheManager,
IUserManager userManager,
IEventPublisher eventPublisher)
IEventPublisher eventPublisher,
IMHandler imHandler)
{
_captchaHandler = captchaHandler;
_userRepository = userRepository;
@@ -155,6 +172,7 @@ public class OAuthService : IDynamicApiController, ITransient
_formService = formService;
_sysConfigService = sysConfigService;
_socialsUserService = socialsUserService;
_sendMessageService = sendMessageService;
_connectionStrings = connectionOptions.Value;
_tenant = tenantOptions.Value;
_sqlSugarClient = (SqlSugarScope)sqlSugarClient;
@@ -162,6 +180,7 @@ public class OAuthService : IDynamicApiController, ITransient
_cacheManager = cacheManager;
_userManager = userManager;
_eventPublisher = eventPublisher;
_imHandler = imHandler;
}
#region Get
@@ -290,12 +309,15 @@ public class OAuthService : IDynamicApiController, ITransient
if ((loginOutput.userInfo.systemIds.Any() && !loginOutput.userInfo.systemIds.Any(x => x.id.Equals(loginOutput.userInfo.systemId))) || loginOutput.userInfo.systemId.IsNullOrEmpty())
{
var defaultItem = loginOutput.userInfo.systemIds.Find(x => x.enCode.Equals("mainSystem"));
if (defaultItem == null) defaultItem = loginOutput.userInfo.systemIds.FirstOrDefault();
loginOutput.userInfo.systemId = defaultItem.id;
defaultItem.currentSystem = true;
await _userRepository.AsUpdateable().SetColumns(x => x.SystemId == loginOutput.userInfo.systemId).Where(x => x.Id.Equals(userId)).ExecuteCommandAsync();
loginOutput.menuList = await _moduleService.GetUserTreeModuleList(type);
if (loginOutput.userInfo.systemIds.Any())
{
var defaultItem = loginOutput.userInfo.systemIds.Find(x => x.enCode.Equals("mainSystem"));
if (defaultItem == null) defaultItem = loginOutput.userInfo.systemIds.FirstOrDefault();
loginOutput.userInfo.systemId = defaultItem.id;
defaultItem.currentSystem = true;
await _userRepository.AsUpdateable().SetColumns(x => x.SystemId == loginOutput.userInfo.systemId).Where(x => x.Id.Equals(userId)).ExecuteCommandAsync();
loginOutput.menuList = await _moduleService.GetUserTreeModuleList(type);
}
}
if (!loginOutput.userInfo.systemIds.Any()) loginOutput.menuList.Clear();
@@ -357,24 +379,26 @@ public class OAuthService : IDynamicApiController, ITransient
/// </summary>
/// <returns></returns>
[HttpGet("Logout")]
public async Task Logout()
public async Task Logout([FromQuery] string ticket)
{
var tenantId = _userManager.TenantId ?? "default";
var userId = _userManager.UserId ?? "admim";
var httpContext = _httpContextAccessor.HttpContext;
httpContext.SignoutToSwagger();
// 清除IM中的webSocket, modified by PhilPan 改为在IMHandle中处理
//var list = await GetOnlineUserList();
//if (list != null)
//{
// var onlineUser = list.Find(it => it.tenantId == _userManager.TenantId && it.userId == _userManager.UserId);
// if (onlineUser != null)
// {
// list.RemoveAll((x) => x.connectionId == onlineUser.connectionId);
// await SetOnlineUserList(list);
// }
//}
// 清除IM中的webSocket
var list = await GetOnlineUserList(tenantId);
if (list != null)
{
var onlineUser = list.Find(it => it.tenantId == tenantId && it.userId == userId);
if (onlineUser != null)
{
list.RemoveAll((x) => x.connectionId == onlineUser.connectionId);
await SetOnlineUserList(list, tenantId);
}
}
await DelUserInfo();
await DelUserInfo(tenantId, userId);
}
#endregion
@@ -431,9 +455,15 @@ public class OAuthService : IDynamicApiController, ITransient
options = JNPFTenantExtensions.GetLinkToCustom(tenantId, result.data.linkList);
}
}
_sqlSugarClient.AddConnection(JNPFTenantExtensions.GetConfig(options));
_sqlSugarClient.ChangeDatabase(tenantId);
if (!"default".Equals(tenantId) && _tenant.MultiTenancyType.Equals("COLUMN"))
{
_sqlSugarClient.QueryFilter.AddTableFilter<ITenantFilter>(it => it.TenantId == tenantId);
}
else
{
_sqlSugarClient.AddConnection(JNPFTenantExtensions.GetConfig(options));
_sqlSugarClient.ChangeDatabase(tenantId);
}
}
else
{
@@ -563,9 +593,13 @@ public class OAuthService : IDynamicApiController, ITransient
{ ClaimConst.CLAINMADMINISTRATOR, userAnyPwd.IsAdministrator },
{ ClaimConst.TENANTID, options.ConfigId },
{ ClaimConst.CONNECTIONCONFIG, options},
{ ClaimConst.SINGLELOGIN, (int)sysConfig.singleLogin }
{ ClaimConst.SINGLELOGIN, (int)sysConfig.singleLogin },
{ ClaimConst.OnlineTicket, input.online_ticket }
}, tokenTimeout);
// 单点登录标识缓存
if (_oauthOptions.Enabled) _cacheManager.Set("OnlineTicket_" + input.online_ticket, options.ConfigId);
// 设置Swagger自动登录
_httpContextAccessor.HttpContext.SigninToSwagger(accessToken);
@@ -635,8 +669,7 @@ public class OAuthService : IDynamicApiController, ITransient
}
}
return new
{
return new {
theme = user.Theme == null ? "classic" : user.Theme,
token = string.Format("Bearer {0}", accessToken)
};
@@ -676,6 +709,63 @@ public class OAuthService : IDynamicApiController, ITransient
await _userRepository.AsUpdateable(userInfo).ExecuteCommandAsync();
return new { code = 200, msg = "注销成功" };
}
/// <summary>
/// 单点登录退出.
/// </summary>
/// <returns></returns>
[HttpPost("Logout/auth2")]
[AllowAnonymous]
public async Task OnlineLogout()
{
var ticket = _httpContextAccessor.HttpContext.Request.Form["ticket"];
var tenantId = await _cacheManager.GetAsync("OnlineTicket_" + ticket);
if (ticket.IsNotEmptyOrNull())
{
await _cacheManager.DelAsync("OnlineTicket_" + ticket);
var userId = _userManager.GetAdminUserId();
var userOnlineList = new List<UserOnlineModel>();
userOnlineList = await GetOnlineUserList(tenantId);
var userOnline = userOnlineList.Find(x => x.onlineTicket.Equals(ticket));
if (userOnline != null)
{
userId = userOnline.userId;
await _imHandler.SendMessageAsync(userOnline.connectionId, new { method = "logout", msg = "此账号已在其他地方登陆" }.ToJsonString());
}
// 清除IM中的webSocket
if (userOnlineList != null)
{
var onlineUser = userOnlineList.Find(it => it.tenantId == tenantId && it.userId == userId);
if (onlineUser != null)
{
userOnlineList.RemoveAll((x) => x.connectionId == onlineUser.connectionId);
await SetOnlineUserList(userOnlineList, tenantId);
}
}
await DelUserInfo(tenantId, userId);
}
}
/// <summary>
/// 密码过期提醒.
/// </summary>
/// <returns></returns>
[HttpPost("updatePasswordMessage")]
public async Task PwdMessage()
{
var sysConfigInfo = await _sysConfigService.GetInfo();
// 密码修改时间.
var changePasswordDate = _userManager.User.ChangePasswordDate.IsNullOrEmpty() ? _userManager.User.CreatorTime : _userManager.User.ChangePasswordDate;
// 提醒时间
var remindDate = changePasswordDate.ParseToDateTime().AddDays(sysConfigInfo.updateCycle - sysConfigInfo.updateInAdvance);
if (sysConfigInfo.passwordIsUpdatedRegularly == 1 && remindDate < DateTime.Now)
{
await _sendMessageService.SendMessageSystem("XTXXTX001");
}
}
#endregion
#region PrivateMethod
@@ -740,9 +830,9 @@ public class OAuthService : IDynamicApiController, ITransient
/// 获取在线用户列表.
/// </summary>
/// <returns></returns>
private async Task<List<UserOnlineModel>> GetOnlineUserList()
private async Task<List<UserOnlineModel>> GetOnlineUserList(string tenantId)
{
string cacheKey = string.Format("{0}{1}", CommonConst.CACHEKEYONLINEUSER, _userManager.ConnectionConfig.ConfigId);
string cacheKey = string.Format("{0}{1}", CommonConst.CACHEKEYONLINEUSER, tenantId);
return await _cacheManager.GetAsync<List<UserOnlineModel>>(cacheKey);
}
@@ -751,18 +841,18 @@ public class OAuthService : IDynamicApiController, ITransient
/// </summary>
/// <param name="onlineList">在线用户列表.</param>
/// <returns></returns>
private async Task<bool> SetOnlineUserList(List<UserOnlineModel> onlineList)
private async Task<bool> SetOnlineUserList(List<UserOnlineModel> onlineList, string tenantId)
{
string cacheKey = string.Format("{0}{1}", CommonConst.CACHEKEYONLINEUSER, _userManager.ConnectionConfig.ConfigId);
string cacheKey = string.Format("{0}{1}", CommonConst.CACHEKEYONLINEUSER, tenantId);
return await _cacheManager.SetAsync(cacheKey, onlineList);
}
/// <summary>
/// 删除用户登录信息缓存.
/// </summary>
private async Task<bool> DelUserInfo()
private async Task<bool> DelUserInfo(string tenantId, string userId)
{
string cacheKey = string.Format("{0}{1}_{2}", CommonConst.CACHEKEYUSER, _userManager.ConnectionConfig.ConfigId, _userManager.UserId);
string cacheKey = string.Format("{0}:{1}:{2}", tenantId, CommonConst.CACHEKEYUSER, userId);
return await _cacheManager.DelAsync(cacheKey);
}
@@ -1109,15 +1199,25 @@ public class OAuthService : IDynamicApiController, ITransient
{
var loginConfigModel = new SocialsLoginConfigModel();
// 追加第三方登录配置
var loginList = _socialsUserService.GetLoginList(CommonConst.PARAMS_JNPF_TICKET.ToUpper());
if (loginList == null) return loginConfigModel;
if (loginList.Any())
if (_oauthOptions.Enabled)
{
loginConfigModel.socialsList = loginList.ToObject<List<object>>();
loginConfigModel.redirect = false;
var url = _oauthOptions.LoginPath + "/" + _oauthOptions.DefaultSSO;
loginConfigModel.redirect = true;
loginConfigModel.url = url;
loginConfigModel.ticketParams = CommonConst.PARAMS_JNPF_TICKET;
}
else
{
// 追加第三方登录配置
var loginList = _socialsUserService.GetLoginList(CommonConst.PARAMS_JNPF_TICKET.ToUpper());
if (loginList == null) return loginConfigModel;
if (loginList.Any())
{
loginConfigModel.socialsList = loginList.ToObject<List<object>>();
loginConfigModel.redirect = false;
loginConfigModel.ticketParams = CommonConst.PARAMS_JNPF_TICKET;
}
}
return loginConfigModel;
}
@@ -1132,10 +1232,10 @@ public class OAuthService : IDynamicApiController, ITransient
public dynamic GetTicket()
{
SocialsLoginTicketModel ticketModel = new SocialsLoginTicketModel();
var curDate = DateTime.Now.AddMinutes(5); // 默认过期5分钟.
var curDate = DateTime.Now.AddMinutes(_oauthOptions.TicketTimeout); // 默认过期5分钟.
ticketModel.ticketTimeout = curDate.ParseToUnixTime();
var key = "SocialsLogin_" + Yitter.IdGenerator.YitIdHelper.NextId().ToString();
_cacheManager.Set(key, ticketModel.ToJsonString(), TimeSpan.FromMinutes(5));
var key = "SocialsLogin_" + SnowflakeIdHelper.NextId();
_cacheManager.Set(key, ticketModel.ToJsonString(), TimeSpan.FromMinutes(_oauthOptions.TicketTimeout));
return key;
}
@@ -1158,4 +1258,264 @@ public class OAuthService : IDynamicApiController, ITransient
}
#endregion
#region .
/// <summary>
/// 单点登录接口.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpGet("Login/{type}")]
[AllowAnonymous]
[IgnoreLog]
[NonUnify]
public async Task<dynamic> LoginByType(string type, [FromQuery] Dictionary<string, string> input)
{
#region Cas
//if (type.ToLower().Equals("cas"))
//{
// var ticket = input.ContainsKey(CommonConst.PARAMS_JNPF_TICKET) ? input[CommonConst.PARAMS_JNPF_TICKET].ToString() : string.Empty;
// var ticketModel = _cacheManager.Get<SocialsLoginTicketModel>(ticket);
// if (ticketModel == null) return "登录票据已失效";
// var casTicket = input.ContainsKey(CommonConst.CAS_Ticket) ? input[CommonConst.CAS_Ticket].ToString() : string.Empty;
// if (casTicket.IsNotEmptyOrNull())
// {
// }
// else
// {
// var loginUrl = _oauthOptions.SSO.Cas.ServerLoginUrl;
// //http://sso.maxkey.top:8527/sign/authz/cas/?service=http://sa-oauth-client.demo.maxkey.top:8002
// loginUrl = Extras.CollectiveOAuth.Utils.UrlBuilder.fromBaseUrl(loginUrl)
// .queryParam("service", _oauthOptions.LoginPath + "/cas")
// .queryParam(CommonConst.PARAMS_JNPF_TICKET, ticket)
// .build();
// _httpContextAccessor.HttpContext.Response.Redirect(loginUrl);
// }
//}
#endregion
if (type.ToLower().Equals("auth2"))
{
var ticket = string.Empty;
if (input.ContainsKey(CommonConst.PARAMS_JNPF_TICKET) && input[CommonConst.PARAMS_JNPF_TICKET].IsNotEmptyOrNull())
{
ticket = input[CommonConst.PARAMS_JNPF_TICKET];
var ticketModel = _cacheManager.Get<SocialsLoginTicketModel>(ticket);
if (ticketModel == null) return "登录票据已失效";
}
var code = input.ContainsKey(CommonConst.Code) ? input[CommonConst.Code] : string.Empty;
// 接受CODE 进行登录
if (code.IsNotEmptyOrNull())
{
try
{
await loginByCode(code, ticket);
}
catch (Exception e)
{
// 更新登录结果
return e.Message;
}
}
else
{
redirectLogin(ticket);
}
}
return null;
}
/// <summary>
/// 跳转单点登录页面.
/// </summary>
protected void redirectLogin(string ticket)
{
var loginUrl = _oauthOptions.SSO.Auth2.AuthorizeUrl;
var tmpAuthCallbackUrl = _oauthOptions.LoginPath + "/auth2";
//http://sso.maxkey.top:8527/sign/authz/oauth/v20/authorize?response_type=code&client_id=745057899234983936&redirect_uri=http://sa-oauth-client.demo.maxkey.top:8002/&scope=all
if (ticket.IsNotEmptyOrNull())
{
tmpAuthCallbackUrl = Extras.CollectiveOAuth.Utils.UrlBuilder.fromBaseUrl(tmpAuthCallbackUrl)
.queryParam(CommonConst.PARAMS_JNPF_TICKET, ticket)
.build();
}
loginUrl = Extras.CollectiveOAuth.Utils.UrlBuilder.fromBaseUrl(loginUrl)
.queryParam("response_type", CommonConst.Code)
.queryParam("client_id", _oauthOptions.SSO.Auth2.ClientId)
.queryParam("scope", "read")
.queryParam("redirect_uri", tmpAuthCallbackUrl)
.build();
_httpContextAccessor.HttpContext.Response.Redirect(loginUrl);
}
/// <summary>
/// Oauth2登录.
/// </summary>
/// <param name="code"></param>
/// <param name="ticket"></param>
protected async Task loginByCode(string code, string ticket)
{
var token = await getAccessToken(code);
var remoteUserInfo = await getRemoteInfo(token);
//var userId = remoteUserInfo.getOrDefault("accounts.username", remoteUserInfo["username"]).ToString();
var userId = remoteUserInfo.ContainsKey("accounts.username") ? remoteUserInfo["accounts.username"].ToString() : remoteUserInfo["username"].ToString();
var userAccount = string.Empty;
if (_tenant.MultiTenancy)
{
var instId = remoteUserInfo["institution"].ToString();
userAccount = instId + "@" + userId;
}
else
{
userAccount = userId;
}
// 登录账号
var loginInput = await GetUserInfoByUserAccount(userAccount);
loginInput.online_ticket = remoteUserInfo["online_ticket"].ToString();
var loginRes = await Login(loginInput);
var jnpfTicket = _cacheManager.Get<SocialsLoginTicketModel>(ticket);
if (jnpfTicket.IsNotEmptyOrNull())
{
// 修改 缓存 状态
jnpfTicket.status = (int)SocialsLoginTicketStatus.Success;
jnpfTicket.value = loginRes.token;
_cacheManager.Set(ticket, jnpfTicket.ToJsonString(), TimeSpan.FromMinutes(_oauthOptions.TicketTimeout));
}
else
{
var url = string.Format("{0}?token={1}&theme={2}", _oauthOptions.SucessFrontUrl, loginRes.token, loginRes.theme);
_httpContextAccessor.HttpContext.Response.Redirect(url);
}
}
/// <summary>
/// 获取OAUTH2 AccessToken.
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
private async Task<string> getAccessToken(string code)
{
var reqUrl = _oauthOptions.SSO.Auth2.AccessTokenUrl
.AddUrlQuery(string.Format("grant_type={0}", "authorization_code"))
.AddUrlQuery(string.Format("client_id={0}", _oauthOptions.SSO.Auth2.ClientId))
.AddUrlQuery(string.Format("client_secret={0}", _oauthOptions.SSO.Auth2.ClientSecret))
.AddUrlQuery(string.Format("redirect_uri={0}", _oauthOptions.LoginPath + "/auth2"))
.AddUrlQuery(string.Format("code={0}", code));
var response = await reqUrl.GetAsStringAsync();
Dictionary<string, object> result = null;
try
{
result = response.ToObject<Dictionary<string, object>>();
}
catch (Exception e)
{
// log.error("解析Auth2 access_token失败", e);
}
if (result == null || !result.ContainsKey("access_token"))
{
throw new Exception("Auth2: 获取access_token失败");
}
var access_token = result["access_token"].ToString();
// log.debug("Auth2 Token: {}", access_token);
return access_token;
}
/// <summary>
/// 获取用户信息.
/// </summary>
/// <param name="access_token"></param>
/// <returns></returns>
private async Task<Dictionary<string, object>> getRemoteInfo(string access_token)
{
var reqUrl = _oauthOptions.SSO.Auth2.UserInfoUrl
.AddUrlQuery(string.Format("access_token={0}", access_token));
var response = await reqUrl.GetAsStringAsync();
Dictionary<string, object> result = null;
try
{
// log.debug("Auth2 User: {}", response);
result = response.ToObject<Dictionary<string, object>>();
}
catch (Exception e)
{
// log.error("解析Auth2 用户信息失败", e);
}
if (result == null || !result.ContainsKey("username"))
{
// log.error(response);
throw new Exception("Auth2: 获取远程用户信息失败");
}
return result;
}
private async Task<LoginInput> GetUserInfoByUserAccount(string account)
{
ConnectionConfigOptions options = JNPFTenantExtensions.GetLinkToOrdinary(_connectionStrings.ConfigId, _connectionStrings.DBName);
UserAgent userAgent = new UserAgent(App.HttpContext);
if (_tenant.MultiTenancy)
{
// 分割账号
var tenantAccount = account.Split('@');
var tenantId = tenantAccount.FirstOrDefault();
if (tenantAccount.Length == 1)
account = "admin";
else
account = tenantAccount[1];
var interFace = string.Format("{0}{1}", _tenant.MultiTenancyDBInterFace, tenantId);
var response = await interFace.GetAsStringAsync();
var result = response.ToObject<RESTfulResult<TenantInterFaceOutput>>();
if (result.code != 200)
{
throw Oops.Oh(result.msg);
}
else if (result.data.dotnet == null && result.data.linkList == null)
{
throw Oops.Oh(ErrorCode.D1025);
}
else
{
if (result.data.linkList == null || result.data.linkList?.Count == 0)
{
options = JNPFTenantExtensions.GetLinkToOrdinary(tenantId, result.data.dotnet);
}
else if (result.data.dotnet == null)
{
options = JNPFTenantExtensions.GetLinkToCustom(tenantId, result.data.linkList);
}
}
_sqlSugarClient.AddConnection(JNPFTenantExtensions.GetConfig(options));
_sqlSugarClient.ChangeDatabase(tenantId);
}
var userEntity = _sqlSugarClient.Queryable<UserEntity>().Single(u => u.Account == account && u.DeleteMark == null);
return new LoginInput()
{
account = userEntity.Account,
password = userEntity.Password,
isSocialsLoginCallBack = true
};
}
#endregion
}