增加github oauth登录
This commit is contained in:
		@@ -1,40 +0,0 @@
 | 
				
			|||||||
using Microsoft.AspNetCore.Authentication.OAuth;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
using System.Text.Json;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace ThingsGateway.Admin.Application;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// <summary>OAuthOptions 配置类</summary>
 | 
					 | 
				
			||||||
public abstract class AdminOAuthOptions : OAuthOptions
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    /// <summary>默认构造函数</summary>
 | 
					 | 
				
			||||||
    protected AdminOAuthOptions()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        ConfigureClaims();
 | 
					 | 
				
			||||||
        this.Events.OnRemoteFailure = context =>
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            var redirectUri = string.IsNullOrEmpty(HomePath) ? "/" : HomePath;
 | 
					 | 
				
			||||||
            context.Response.Redirect(redirectUri);
 | 
					 | 
				
			||||||
            context.HandleResponse();
 | 
					 | 
				
			||||||
            return Task.CompletedTask;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>配置 Claims 映射</summary>
 | 
					 | 
				
			||||||
    protected virtual void ConfigureClaims()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public virtual string GetName(JsonElement element)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        JsonElement.ObjectEnumerator target = element.EnumerateObject();
 | 
					 | 
				
			||||||
        return target.TryGetValue("name");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>获得/设置 登陆后首页</summary>
 | 
					 | 
				
			||||||
    public string HomePath { get; set; } = "/";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
namespace ThingsGateway.Admin.Application;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class GiteeOAuthUser
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public string Id { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public string Login { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public string Name { get; set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public string Avatar_Url { get; set; }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
using System.Text.Json;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace ThingsGateway.Admin.Application;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public static class OAuthUserExtensions
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public static GiteeOAuthUser ToAuthUser(this JsonElement element)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        GiteeOAuthUser authUser = new GiteeOAuthUser();
 | 
					 | 
				
			||||||
        JsonElement.ObjectEnumerator target = element.EnumerateObject();
 | 
					 | 
				
			||||||
        authUser.Id = target.TryGetValue("id");
 | 
					 | 
				
			||||||
        authUser.Login = target.TryGetValue("login");
 | 
					 | 
				
			||||||
        authUser.Name = target.TryGetValue("name");
 | 
					 | 
				
			||||||
        authUser.Avatar_Url = target.TryGetValue("avatar_url");
 | 
					 | 
				
			||||||
        return authUser;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static string TryGetValue(this JsonElement.ObjectEnumerator target, string propertyName)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return target.FirstOrDefault<JsonProperty>((Func<JsonProperty, bool>)(t => t.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))).Value.ToString() ?? string.Empty;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,18 +1,14 @@
 | 
				
			|||||||
using Microsoft.AspNetCore.Authentication;
 | 
					using Microsoft.AspNetCore.Authentication;
 | 
				
			||||||
using Microsoft.AspNetCore.Authentication.OAuth;
 | 
					using Microsoft.AspNetCore.Authentication.OAuth;
 | 
				
			||||||
using Microsoft.AspNetCore.Http;
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
using Microsoft.AspNetCore.WebUtilities;
 | 
					 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					using Microsoft.Extensions.DependencyInjection;
 | 
				
			||||||
using Microsoft.Extensions.Hosting;
 | 
					using Microsoft.Extensions.Hosting;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using Microsoft.Extensions.Options;
 | 
					using Microsoft.Extensions.Options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System.Collections.Concurrent;
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
using System.Net.Http.Headers;
 | 
					 | 
				
			||||||
using System.Security.Claims;
 | 
					using System.Security.Claims;
 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Text.Encodings.Web;
 | 
					using System.Text.Encodings.Web;
 | 
				
			||||||
using System.Text.Json;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
using ThingsGateway.Extension;
 | 
					using ThingsGateway.Extension;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,6 +76,7 @@ public class AdminOAuthHandler<TOptions>(
 | 
				
			|||||||
        AuthenticationProperties properties,
 | 
					        AuthenticationProperties properties,
 | 
				
			||||||
        OAuthTokenResponse tokens)
 | 
					        OAuthTokenResponse tokens)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        Backchannel.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokens.AccessToken);
 | 
				
			||||||
        properties.RedirectUri = Options.HomePath;
 | 
					        properties.RedirectUri = Options.HomePath;
 | 
				
			||||||
        properties.IsPersistent = true;
 | 
					        properties.IsPersistent = true;
 | 
				
			||||||
        var appConfig = await configService.GetAppConfigAsync().ConfigureAwait(false);
 | 
					        var appConfig = await configService.GetAppConfigAsync().ConfigureAwait(false);
 | 
				
			||||||
@@ -90,7 +87,7 @@ public class AdminOAuthHandler<TOptions>(
 | 
				
			|||||||
            properties.ExpiresUtc = TimeProvider.System.GetUtcNow().AddSeconds(result);
 | 
					            properties.ExpiresUtc = TimeProvider.System.GetUtcNow().AddSeconds(result);
 | 
				
			||||||
            expire = (int)(result / 60.0);
 | 
					            expire = (int)(result / 60.0);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        var user = await HandleUserInfoAsync(tokens).ConfigureAwait(false);
 | 
					        var user = await Options.HandleUserInfoAsync(Context, tokens).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var loginEvent = await GetLogin(expire).ConfigureAwait(false);
 | 
					        var loginEvent = await GetLogin(expire).ConfigureAwait(false);
 | 
				
			||||||
        await UpdateUser(loginEvent).ConfigureAwait(false);
 | 
					        await UpdateUser(loginEvent).ConfigureAwait(false);
 | 
				
			||||||
@@ -148,43 +145,8 @@ public class AdminOAuthHandler<TOptions>(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>处理用户信息方法</summary>
 | 
					 | 
				
			||||||
    protected virtual async Task<JsonElement> HandleUserInfoAsync(OAuthTokenResponse tokens)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        var request = new HttpRequestMessage(HttpMethod.Get, BuildUserInfoUrl(tokens));
 | 
					 | 
				
			||||||
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var response = await Backchannel.SendAsync(request, Context.RequestAborted).ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (response.IsSuccessStatusCode)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return JsonDocument.Parse(content).RootElement;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>生成用户信息请求地址方法</summary>
 | 
					 | 
				
			||||||
    protected virtual string BuildUserInfoUrl(OAuthTokenResponse tokens)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string>
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            { "access_token", tokens.AccessToken }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// <summary>生成错误信息方法</summary>
 | 
					 | 
				
			||||||
    protected static async Task<string> Display(HttpResponseMessage response)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        var output = new StringBuilder();
 | 
					 | 
				
			||||||
        output.Append($"Status: {response.StatusCode}; ");
 | 
					 | 
				
			||||||
        output.Append($"Headers: {response.Headers}; ");
 | 
					 | 
				
			||||||
        output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return output.ToString();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async Task<LoginEvent> GetLogin(int expire)
 | 
					    private async Task<LoginEvent> GetLogin(int expire)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					using Microsoft.AspNetCore.Authentication.OAuth;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.WebUtilities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using System.Net.Http.Headers;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace ThingsGateway.Admin.Application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>OAuthOptions 配置类</summary>
 | 
				
			||||||
 | 
					public abstract class AdminOAuthOptions : OAuthOptions
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>默认构造函数</summary>
 | 
				
			||||||
 | 
					    protected AdminOAuthOptions()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ConfigureClaims();
 | 
				
			||||||
 | 
					        this.Events.OnRemoteFailure = context =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var redirectUri = string.IsNullOrEmpty(HomePath) ? "/" : HomePath;
 | 
				
			||||||
 | 
					            context.Response.Redirect(redirectUri);
 | 
				
			||||||
 | 
					            context.HandleResponse();
 | 
				
			||||||
 | 
					            return Task.CompletedTask;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Backchannel = new HttpClient(new HttpClientHandler
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator // 若测试用
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        Backchannel.DefaultRequestHeaders.UserAgent.Add(
 | 
				
			||||||
 | 
					            new ProductInfoHeaderValue("ThingsGateway", "1.0"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>配置 Claims 映射</summary>
 | 
				
			||||||
 | 
					    protected virtual void ConfigureClaims()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public virtual string GetName(JsonElement element)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        JsonElement.ObjectEnumerator target = element.EnumerateObject();
 | 
				
			||||||
 | 
					        return target.TryGetValue("name");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>获得/设置 登陆后首页</summary>
 | 
				
			||||||
 | 
					    public string HomePath { get; set; } = "/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>处理用户信息方法</summary>
 | 
				
			||||||
 | 
					    public virtual async Task<JsonElement> HandleUserInfoAsync(HttpContext context, OAuthTokenResponse tokens)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var request = new HttpRequestMessage(HttpMethod.Get, BuildUserInfoUrl(tokens));
 | 
				
			||||||
 | 
					        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var response = await Backchannel.SendAsync(request, context.RequestAborted).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.IsSuccessStatusCode)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return JsonDocument.Parse(content).RootElement;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>生成用户信息请求地址方法</summary>
 | 
				
			||||||
 | 
					    protected virtual string BuildUserInfoUrl(OAuthTokenResponse tokens)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return QueryHelpers.AddQueryString(UserInformationEndpoint, new Dictionary<string, string>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            { "access_token", tokens.AccessToken }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /// <summary>生成错误信息方法</summary>
 | 
				
			||||||
 | 
					    protected async Task<string> Display(HttpResponseMessage response)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var output = new StringBuilder();
 | 
				
			||||||
 | 
					        output.Append($"Status: {response.StatusCode}; ");
 | 
				
			||||||
 | 
					        output.Append($"Headers: {response.Headers}; ");
 | 
				
			||||||
 | 
					        output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return output.ToString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,16 +3,20 @@ using Microsoft.AspNetCore.Authentication.OAuth;
 | 
				
			|||||||
using Microsoft.AspNetCore.WebUtilities;
 | 
					using Microsoft.AspNetCore.WebUtilities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System.Net.Http.Headers;
 | 
					using System.Net.Http.Headers;
 | 
				
			||||||
using System.Text;
 | 
					 | 
				
			||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using ThingsGateway.NewLife.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace ThingsGateway.Admin.Application;
 | 
					namespace ThingsGateway.Admin.Application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class GiteeOAuthOptions : AdminOAuthOptions
 | 
					public class GiteeOAuthOptions : AdminOAuthOptions
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    INoticeService _noticeService;
 | 
				
			||||||
 | 
					    IVerificatInfoService _verificatInfoService;
 | 
				
			||||||
    public GiteeOAuthOptions() : base()
 | 
					    public GiteeOAuthOptions() : base()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        _noticeService = App.GetService<INoticeService>();
 | 
				
			||||||
 | 
					        _verificatInfoService = App.GetService<IVerificatInfoService>();
 | 
				
			||||||
        this.SignInScheme = ClaimConst.Scheme;
 | 
					        this.SignInScheme = ClaimConst.Scheme;
 | 
				
			||||||
        this.AuthorizationEndpoint = "https://gitee.com/oauth/authorize";
 | 
					        this.AuthorizationEndpoint = "https://gitee.com/oauth/authorize";
 | 
				
			||||||
        this.TokenEndpoint = "https://gitee.com/oauth/token";
 | 
					        this.TokenEndpoint = "https://gitee.com/oauth/token";
 | 
				
			||||||
@@ -29,11 +33,14 @@ public class GiteeOAuthOptions : AdminOAuthOptions
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Events.OnRedirectToAuthorizationEndpoint = context =>
 | 
					        Events.OnRedirectToAuthorizationEndpoint = context =>
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            //context.RedirectUri = context.RedirectUri.Replace("http%3A%2F%2F", "https%3A%2F%2F"); // 强制替换
 | 
					 | 
				
			||||||
            context.Response.Redirect(context.RedirectUri);
 | 
					            context.Response.Redirect(context.RedirectUri);
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					            return Task.CompletedTask;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        Events.OnRemoteFailure = context =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            XTrace.WriteException(context.Failure);
 | 
				
			||||||
 | 
					            return Task.CompletedTask;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>刷新 Token 方法</summary>
 | 
					    /// <summary>刷新 Token 方法</summary>
 | 
				
			||||||
@@ -60,16 +67,7 @@ public class GiteeOAuthOptions : AdminOAuthOptions
 | 
				
			|||||||
        return OAuthTokenResponse.Failed(new OAuthTokenException($"OAuth token endpoint failure: {await Display(response).ConfigureAwait(false)}"));
 | 
					        return OAuthTokenResponse.Failed(new OAuthTokenException($"OAuth token endpoint failure: {await Display(response).ConfigureAwait(false)}"));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>生成错误信息方法</summary>
 | 
					 | 
				
			||||||
    protected static async Task<string> Display(HttpResponseMessage response)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        var output = new StringBuilder();
 | 
					 | 
				
			||||||
        output.Append($"Status: {response.StatusCode}; ");
 | 
					 | 
				
			||||||
        output.Append($"Headers: {response.Headers}; ");
 | 
					 | 
				
			||||||
        output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return output.ToString();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override string GetName(JsonElement element)
 | 
					    public override string GetName(JsonElement element)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -77,7 +75,7 @@ public class GiteeOAuthOptions : AdminOAuthOptions
 | 
				
			|||||||
        return target.TryGetValue("name");
 | 
					        return target.TryGetValue("name");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static async Task HandlerGiteeStarredUrl(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
 | 
					    private async Task HandlerGiteeStarredUrl(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (string.IsNullOrWhiteSpace(context.AccessToken))
 | 
					        if (string.IsNullOrWhiteSpace(context.AccessToken))
 | 
				
			||||||
            throw new InvalidOperationException("Access token is missing.");
 | 
					            throw new InvalidOperationException("Access token is missing.");
 | 
				
			||||||
@@ -89,7 +87,7 @@ public class GiteeOAuthOptions : AdminOAuthOptions
 | 
				
			|||||||
            { "access_token", context.AccessToken }
 | 
					            { "access_token", context.AccessToken }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var request = new HttpRequestMessage(HttpMethod.Put, QueryHelpers.AddQueryString(uri, queryString))
 | 
					        var request = new HttpRequestMessage(HttpMethod.Get, QueryHelpers.AddQueryString(uri, queryString))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Headers = { Accept = { new MediaTypeWithQualityHeaderValue("application/json") } }
 | 
					            Headers = { Accept = { new MediaTypeWithQualityHeaderValue("application/json") } }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@@ -99,7 +97,17 @@ public class GiteeOAuthOptions : AdminOAuthOptions
 | 
				
			|||||||
        if (!response.IsSuccessStatusCode)
 | 
					        if (!response.IsSuccessStatusCode)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
					            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
				
			||||||
            throw new Exception($"Failed to star repository: {response.StatusCode}, {content}");
 | 
					
 | 
				
			||||||
 | 
					            var id = context.Identity.Claims.FirstOrDefault(a => a.Type == ClaimConst.VerificatId).Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var verificatInfoIds = _verificatInfoService.GetOne(id.ToLong());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _ = Task.Run(async () =>
 | 
				
			||||||
 | 
					             {
 | 
				
			||||||
 | 
					                 await Task.Delay(5000).ConfigureAwait(false);
 | 
				
			||||||
 | 
					                 await _noticeService.NavigationMesage(verificatInfoIds.ClientIds, "https://gitee.com/ThingsGateway/ThingsGateway", "创作不易,如有帮助请star仓库").ConfigureAwait(false);
 | 
				
			||||||
 | 
					             });
 | 
				
			||||||
 | 
					            //throw new Exception($"Failed to star repository: {response.StatusCode}, {content}");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -0,0 +1,122 @@
 | 
				
			|||||||
 | 
					using Microsoft.AspNetCore.Authentication;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Authentication.OAuth;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using System.Net.Http.Headers;
 | 
				
			||||||
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using ThingsGateway.NewLife.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace ThingsGateway.Admin.Application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class GitHubOAuthOptions : AdminOAuthOptions
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    INoticeService _noticeService;
 | 
				
			||||||
 | 
					    IVerificatInfoService _verificatInfoService;
 | 
				
			||||||
 | 
					    public GitHubOAuthOptions() : base()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        _noticeService = App.GetService<INoticeService>();
 | 
				
			||||||
 | 
					        _verificatInfoService = App.GetService<IVerificatInfoService>();
 | 
				
			||||||
 | 
					        SignInScheme = ClaimConst.Scheme;
 | 
				
			||||||
 | 
					        AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
 | 
				
			||||||
 | 
					        TokenEndpoint = "https://github.com/login/oauth/access_token";
 | 
				
			||||||
 | 
					        UserInformationEndpoint = "https://api.github.com/user";
 | 
				
			||||||
 | 
					        HomePath = "/";
 | 
				
			||||||
 | 
					        CallbackPath = "/signin-github";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Scope.Add("read:user");
 | 
				
			||||||
 | 
					        Scope.Add("public_repo"); // 需要用于 Star 仓库
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Events.OnCreatingTicket = async context =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            await HandleGitHubStarAsync(context).ConfigureAwait(false);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Events.OnRedirectToAuthorizationEndpoint = context =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            context.Response.Redirect(context.RedirectUri);
 | 
				
			||||||
 | 
					            return Task.CompletedTask;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Events.OnRemoteFailure = context =>
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            XTrace.WriteException(context.Failure);
 | 
				
			||||||
 | 
					            return Task.CompletedTask;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected override void ConfigureClaims()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ClaimActions.MapJsonKey(ClaimConst.AvatarUrl, "avatar_url");
 | 
				
			||||||
 | 
					        ClaimActions.MapJsonKey(ClaimConst.Account, "login");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        base.ConfigureClaims();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public override string GetName(JsonElement element)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (element.TryGetProperty("login", out var loginProp))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return loginProp.GetString() ?? string.Empty;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return string.Empty;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async Task HandleGitHubStarAsync(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (string.IsNullOrWhiteSpace(context.AccessToken))
 | 
				
			||||||
 | 
					            throw new InvalidOperationException("Access token is missing.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var request = new HttpRequestMessage(HttpMethod.Put, $"https://api.github.com/user/starred/{repoFullName}")
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Headers =
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Accept = { new MediaTypeWithQualityHeaderValue("application/vnd.github+json") },
 | 
				
			||||||
 | 
					                Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Content = new StringContent(string.Empty) // GitHub Star 接口需要空内容
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        request.Headers.UserAgent.Add(new ProductInfoHeaderValue("ThingsGateway", "1.0")); // GitHub API 要求 User-Agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!response.IsSuccessStatusCode)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var id = context.Identity.Claims.FirstOrDefault(a => a.Type == ClaimConst.VerificatId).Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var verificatInfoIds = _verificatInfoService.GetOne(id.ToLong());
 | 
				
			||||||
 | 
					            _ = Task.Run(async () =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                await Task.Delay(5000).ConfigureAwait(false);
 | 
				
			||||||
 | 
					                await _noticeService.NavigationMesage(verificatInfoIds.ClientIds, "https://github.com/ThingsGateway/ThingsGateway", "创作不易,如有帮助请star仓库").ConfigureAwait(false);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>处理用户信息方法</summary>
 | 
				
			||||||
 | 
					    public override async Task<JsonElement> HandleUserInfoAsync(HttpContext context, OAuthTokenResponse tokens)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var request = new HttpRequestMessage(HttpMethod.Get, UserInformationEndpoint);
 | 
				
			||||||
 | 
					        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
 | 
				
			||||||
 | 
					        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
 | 
				
			||||||
 | 
					        request.Headers.UserAgent.Add(new ProductInfoHeaderValue("ThingsGateway", "1.0")); // GitHub API 要求 User-Agent
 | 
				
			||||||
 | 
					        var response = await Backchannel.SendAsync(request, context.RequestAborted).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.IsSuccessStatusCode)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return JsonDocument.Parse(content).RootElement;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					namespace ThingsGateway.Admin.Application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class GithubOAuthSettings : GiteeOAuthSettings
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					using System.Text.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace ThingsGateway.Admin.Application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public static class OAuthUserExtensions
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public static string TryGetValue(this JsonElement.ObjectEnumerator target, string propertyName)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return target.FirstOrDefault<JsonProperty>((Func<JsonProperty, bool>)(t => t.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))).Value.ToString() ?? string.Empty;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,6 +5,9 @@
 | 
				
			|||||||
    @<div>
 | 
					    @<div>
 | 
				
			||||||
        <span class="mx-3">@item.ConfirmMessage</span>
 | 
					        <span class="mx-3">@item.ConfirmMessage</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Button Text=@Localizers["Jump"] Color="Color.Link" OnClick="()=>NavigationManager.NavigateTo(item.Uri)"></Button>
 | 
					        <a href=@item.Uri target="_blank">
 | 
				
			||||||
 | 
					            @item.Uri
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>;
 | 
					    </div>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
<Project>
 | 
					<Project>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<PropertyGroup>
 | 
						<PropertyGroup>
 | 
				
			||||||
		<PluginVersion>10.7.57</PluginVersion>
 | 
							<PluginVersion>10.7.58</PluginVersion>
 | 
				
			||||||
		<ProPluginVersion>10.7.57</ProPluginVersion>
 | 
							<ProPluginVersion>10.7.58</ProPluginVersion>
 | 
				
			||||||
		<AuthenticationVersion>2.6.0</AuthenticationVersion>
 | 
							<AuthenticationVersion>2.6.0</AuthenticationVersion>
 | 
				
			||||||
		<NET8Version>8.0.17</NET8Version>
 | 
							<NET8Version>8.0.17</NET8Version>
 | 
				
			||||||
		<NET9Version>9.0.6</NET9Version>
 | 
							<NET9Version>9.0.6</NET9Version>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/ThingsGateway.Server/Configuration/OAuthSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/ThingsGateway.Server/Configuration/OAuthSettings.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "GiteeOAuthSettings": {
 | 
				
			||||||
 | 
					    "ClientId": "237adcc9fd39a71d53691d96642e1c2432db90ad93c02f72604897a348f347c6",
 | 
				
			||||||
 | 
					    "ClientSecret": "4650577daf4cb02b325f3ae163e6706e87c4dc2ad615b035821b21116b279bca"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "GithubOAuthSettings": {
 | 
				
			||||||
 | 
					    "ClientId": "Ov23lijDvpmRPawSDyWN",
 | 
				
			||||||
 | 
					    "ClientSecret": "1f4aab562651dbe5d6a637afc57a5ca9a397ac07"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -48,7 +48,8 @@
 | 
				
			|||||||
                    <h5 class="mt-2 mb-12 ">@Localizer["Welcome"] 👋</h5>
 | 
					                    <h5 class="mt-2 mb-12 ">@Localizer["Welcome"] 👋</h5>
 | 
				
			||||||
                    @if(WebsiteOption.Value.Demo)
 | 
					                    @if(WebsiteOption.Value.Demo)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        <Button Class="btn-block mt-5" IsAsync OnClick=GiteeLogin>@Localizer["GiteeLogin"]</Button>
 | 
					                        <Button class="btn-block mt-5" IsAsync OnClick=GithubLogin>@Localizer["GithubLogin"]</Button>
 | 
				
			||||||
 | 
					                        <Button class="btn-block mt-5" IsAsync OnClick=GiteeLogin>@Localizer["GiteeLogin"]</Button>
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    else
 | 
					                    else
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,14 +64,25 @@ public partial class Login
 | 
				
			|||||||
        _versionString = $"v{VersionService.Version}";
 | 
					        _versionString = $"v{VersionService.Version}";
 | 
				
			||||||
        return base.OnInitializedAsync();
 | 
					        return base.OnInitializedAsync();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void GiteeLogin()
 | 
					    private void GiteeLogin()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var websiteOptions = App.GetOptions<WebsiteOptions>()!;
 | 
					        var websiteOptions = App.GetOptions<WebsiteOptions>()!;
 | 
				
			||||||
        if (websiteOptions.Demo)
 | 
					        if (websiteOptions.Demo)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            NavigationManager.NavigateTo("/api/auth/oauth-login", forceLoad: true);
 | 
					            NavigationManager.NavigateTo("/api/auth/oauth-login?scheme=Gitee", forceLoad: true);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void GithubLogin()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        var websiteOptions = App.GetOptions<WebsiteOptions>()!;
 | 
				
			||||||
 | 
					        if (websiteOptions.Demo)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            NavigationManager.NavigateTo("/api/auth/oauth-login?scheme=Github", forceLoad: true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [Inject]
 | 
					    [Inject]
 | 
				
			||||||
    NavigationManager NavigationManager { get; set; }
 | 
					    NavigationManager NavigationManager { get; set; }
 | 
				
			||||||
    private async Task LoginAsync(EditContext context)
 | 
					    private async Task LoginAsync(EditContext context)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,6 +35,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "ThingsGateway.Server.Login": {
 | 
					  "ThingsGateway.Server.Login": {
 | 
				
			||||||
    "GiteeLogin": "Gitee OAuth",
 | 
					    "GiteeLogin": "Gitee OAuth",
 | 
				
			||||||
 | 
					    "GithubLogin": "Github OAuth",
 | 
				
			||||||
    "LoginErrorc2": "Please contact the administrator!",
 | 
					    "LoginErrorc2": "Please contact the administrator!",
 | 
				
			||||||
    "LoginErrorh1": "Login Error",
 | 
					    "LoginErrorh1": "Login Error",
 | 
				
			||||||
    "LoginErrorh2": "Login Failed",
 | 
					    "LoginErrorh2": "Login Failed",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,6 +35,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "ThingsGateway.Server.Login": {
 | 
					  "ThingsGateway.Server.Login": {
 | 
				
			||||||
    "GiteeLogin": "Gitee授权登录",
 | 
					    "GiteeLogin": "Gitee授权登录",
 | 
				
			||||||
 | 
					    "GithubLogin": "Github授权登录",
 | 
				
			||||||
    "LoginErrorc2": "请联系管理员!",
 | 
					    "LoginErrorc2": "请联系管理员!",
 | 
				
			||||||
    "LoginErrorh1": "登录异常",
 | 
					    "LoginErrorh1": "登录异常",
 | 
				
			||||||
    "LoginErrorh2": "登录失败",
 | 
					    "LoginErrorh2": "登录失败",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -296,6 +296,14 @@ public class Startup : AppStartup
 | 
				
			|||||||
            options.ClientSecret = data.ClientSecret;
 | 
					            options.ClientSecret = data.ClientSecret;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            authenticationBuilder.AddOAuth<GitHubOAuthOptions, AdminOAuthHandler<GitHubOAuthOptions>>("Github", "Github", options =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var data = App.GetConfig<GithubOAuthSettings>("GithubOAuthSettings");
 | 
				
			||||||
 | 
					                options.ClientId = data.ClientId;
 | 
				
			||||||
 | 
					                options.ClientSecret = data.ClientSecret;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 添加jwt授权
 | 
					        // 添加jwt授权
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
<Project>
 | 
					<Project>
 | 
				
			||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <Version>10.7.57</Version>
 | 
					    <Version>10.7.58</Version>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user