diff --git a/FastTunnel.Api/Controllers/AccountController.cs b/FastTunnel.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..ba26e55 --- /dev/null +++ b/FastTunnel.Api/Controllers/AccountController.cs @@ -0,0 +1,87 @@ +using FastTunnel.Api.Models; +using FastTunnel.Core.Client; +using FastTunnel.Core.Config; +using FastTunnel.Server.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace FastTunnel.Api.Controllers +{ + public class AccountController : BaseController + { + IOptionsMonitor serverOptionsMonitor; + + public AccountController(IOptionsMonitor optionsMonitor) + { + serverOptionsMonitor = optionsMonitor; + } + + /// + /// 获取Token + /// + /// + /// + [AllowAnonymous] + [HttpPost] + public ApiResponse GetToken(GetTokenRequest request) + { + if ((serverOptionsMonitor.CurrentValue?.Api?.Accounts?.Length ?? 0) == 0) + { + ApiResponse.errorCode = ErrorCodeEnum.NoAccount; + ApiResponse.errorMessage = "账号或密码错误"; + return ApiResponse; + } + + var account = serverOptionsMonitor.CurrentValue.Api.Accounts.FirstOrDefault((x) => + { + return x.Name.Equals(request.name) && x.Password.Equals(request.password); + }); + + if (account == null) + { + ApiResponse.errorCode = ErrorCodeEnum.NoAccount; + ApiResponse.errorMessage = "账号或密码错误"; + return ApiResponse; + } + + // 生成Token + var claims = new[] { + new Claim("Name", account.Name) + }; + + ApiResponse.data = GenerateToken( + claims, + serverOptionsMonitor.CurrentValue.Api.JWT.IssuerSigningKey, + serverOptionsMonitor.CurrentValue.Api.JWT.Expires, + serverOptionsMonitor.CurrentValue.Api.JWT.ValidIssuer, + serverOptionsMonitor.CurrentValue.Api.JWT.ValidAudience); + + return ApiResponse; + } + + public static string GenerateToken( + IEnumerable claims, string Secret, int expiresMinutes = 60, string issuer = null, string audience = null) + { + var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Secret)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var securityToken = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: DateTime.Now.AddMinutes(expiresMinutes), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(securityToken); + } + } +} diff --git a/FastTunnel.Api/Controllers/BaseController.cs b/FastTunnel.Api/Controllers/BaseController.cs index 5916825..cff5134 100644 --- a/FastTunnel.Api/Controllers/BaseController.cs +++ b/FastTunnel.Api/Controllers/BaseController.cs @@ -1,4 +1,5 @@ using FastTunnel.Server.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; @@ -8,6 +9,7 @@ using System.Threading.Tasks; namespace FastTunnel.Api.Controllers { + [Authorize] [Route("api/[controller]/[action]")] [ApiController] public class BaseController : ControllerBase diff --git a/FastTunnel.Api/FastTunnel.Api.csproj b/FastTunnel.Api/FastTunnel.Api.csproj index 1a20f89..e1b27c2 100644 --- a/FastTunnel.Api/FastTunnel.Api.csproj +++ b/FastTunnel.Api/FastTunnel.Api.csproj @@ -8,6 +8,8 @@ + + diff --git a/FastTunnel.Api/Filters/CustomExceptionFilterAttribute.cs b/FastTunnel.Api/Filters/CustomExceptionFilterAttribute.cs new file mode 100644 index 0000000..e119012 --- /dev/null +++ b/FastTunnel.Api/Filters/CustomExceptionFilterAttribute.cs @@ -0,0 +1,38 @@ +using FastTunnel.Server.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FastTunnel.Api.Filters +{ + public class CustomExceptionFilterAttribute : ExceptionFilterAttribute + { + readonly ILogger _logger; + + public CustomExceptionFilterAttribute(ILogger logger) + { + _logger = logger; + } + + public override void OnException(ExceptionContext context) + { + _logger.LogError(context.Exception, "【全局异常捕获】"); + var res = new ApiResponse() + { + errorCode = ErrorCodeEnum.Exception, + data = null, + errorMessage = context.Exception.Message, + }; + + var result = new JsonResult(res) { StatusCode = 200 }; + + context.Result = result; + context.ExceptionHandled = true; + } + } +} diff --git a/FastTunnel.Api/Models/ApiResponse.cs b/FastTunnel.Api/Models/ApiResponse.cs index bd86d72..55e70c4 100644 --- a/FastTunnel.Api/Models/ApiResponse.cs +++ b/FastTunnel.Api/Models/ApiResponse.cs @@ -21,5 +21,11 @@ namespace FastTunnel.Server.Models public enum ErrorCodeEnum { NONE = 0, + + AuthError = 1, + + Exception = 2, + + NoAccount = 3, } } diff --git a/FastTunnel.Api/Models/GetTokenRequest.cs b/FastTunnel.Api/Models/GetTokenRequest.cs new file mode 100644 index 0000000..2aae47f --- /dev/null +++ b/FastTunnel.Api/Models/GetTokenRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FastTunnel.Api.Models +{ + public class GetTokenRequest + { + [Required] + public string name { get; set; } + + [Required] + public string password { get; set; } + } +} diff --git a/FastTunnel.Client/appsettings.json b/FastTunnel.Client/appsettings.json index 404ac02..1665117 100644 --- a/FastTunnel.Client/appsettings.json +++ b/FastTunnel.Client/appsettings.json @@ -7,54 +7,58 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + // 是否启用文件日志输出 "EnableFileLog": false, "ClientSettings": { "Server": { - // 与服务端通讯协议 - "Protocol": "ws", // ws(http)或wss(https) - // 服务端ip/域名 + // [必选] 与服务端通讯协议(来自服务端配置文件的urls参数) + // 可选参数:ws(http)或wss(https) + "Protocol": "ws", + // [必选] 服务端ip/域名(来自服务端配置文件的urls参数) "ServerAddr": "test.cc", - // 服务端监听的通信端口 + // [必选] 服务端监听的通信端口(来自服务端配置文件的urls参数) "ServerPort": 1270 }, - // 服务端Token验证 + // [可选],服务端Token,必须与服务端配置一致,否则拒绝登录。 "Token": "TOKEN_FOR_CLIENT_AUTHENTICATION", /** - * 通过自定义域名访问内网服务,需要有自己的域名 - * 可穿透所有TCP上层协议 + * [可选] 内网web节点配置 */ "Webs": [ { - // 本地站点所在内网的ip + // [必选] 内网站点所在内网的ip "LocalIp": "127.0.0.1", - // 站点监听的端口号 + // [必选] 内网站点监听的端口号 "LocalPort": 8090, - // 子域名, 访问本站点时的url为 http://{SubDomain}.{WebDomain}:{WebProxyPort}/ - "SubDomain": "test" // test.test.cc + // [必选] 子域名, 访问本站点时的url为 http://${SubDomain}.${WebDomain}:${ServerPort} + "SubDomain": "test" - // 附加域名,需要解析CNAME或A记录) + // [可选] 附加域名,需要解析CNAME或A记录至当前子域名 // "WWW": [ "www.abc.com", "test111.test.cc" ] } ], /** - * 端口转发 通过专用端口代理,不需要有自己的域名 + * [可选] 端口转发 通过专用端口代理,不需要有自己的域名 * 可穿透所有TCP上层协议 * 远程linux示例:#ssh -oPort=12701 {root}@{ServerAddr} ServerAddr 填入服务端ip,root对应内网用户名 * 通过服务端返回的访问方式进行访问即可 */ "Forwards": [ { + // [必选] 内网服务所在主机ip "LocalIp": "127.0.0.1", - "LocalPort": 3389, // windows远程桌面端口为3389 - "RemotePort": 1274 // 访问 服务端ip:1274 即可实现远程window桌面 + // [必选] 内网服务监听端口 windows远程桌面端口为3389 + "LocalPort": 3389, + // [必选] 服务端端口 访问 服务端ip:1274 即可实现远程window桌面 + "RemotePort": 1274 }, { "LocalIp": "127.0.0.1", - "LocalPort": 8090, // windows远程桌面端口为3389 - "RemotePort": 1275 // 访问 服务端ip:1274 即可实现远程window桌面 + "LocalPort": 3306, // mysql数据库默认端口 + "RemotePort": 1275 // 访问 服务端ip:1275 即可连接内网的mysql服务 } ] } diff --git a/FastTunnel.Core/Config/DefaultServerConfig.cs b/FastTunnel.Core/Config/DefaultServerConfig.cs index b84a857..ab470d5 100644 --- a/FastTunnel.Core/Config/DefaultServerConfig.cs +++ b/FastTunnel.Core/Config/DefaultServerConfig.cs @@ -14,5 +14,34 @@ namespace FastTunnel.Core.Config public bool EnableForward { get; set; } = false; public string Token { get; set; } + + public ApiOptions Api { get; set; } + + public class ApiOptions + { + public JWTOptions JWT { get; set; } + + public Account[] Accounts { get; set; } + } + + public class JWTOptions + { + public int ClockSkew { get; set; } + + public string ValidAudience { get; set; } + + public string ValidIssuer { get; set; } + + public string IssuerSigningKey { get; set; } + + public int Expires { get; set; } + } + + public class Account + { + public string Name { get; set; } + + public string Password { get; set; } + } } } diff --git a/FastTunnel.Server/FastTunnel.Server.csproj b/FastTunnel.Server/FastTunnel.Server.csproj index 8f08688..3cb1386 100644 --- a/FastTunnel.Server/FastTunnel.Server.csproj +++ b/FastTunnel.Server/FastTunnel.Server.csproj @@ -16,6 +16,7 @@ + diff --git a/FastTunnel.Server/Properties/PublishProfiles/FolderProfile2.pubxml b/FastTunnel.Server/Properties/PublishProfiles/FolderProfile2.pubxml new file mode 100644 index 0000000..040b013 --- /dev/null +++ b/FastTunnel.Server/Properties/PublishProfiles/FolderProfile2.pubxml @@ -0,0 +1,16 @@ + + + + + False + False + True + Release + Any CPU + FileSystem + bin\Release\net5.0\publish\ + FileSystem + + \ No newline at end of file diff --git a/FastTunnel.Server/Startup.cs b/FastTunnel.Server/Startup.cs index e8019ea..c249e01 100644 --- a/FastTunnel.Server/Startup.cs +++ b/FastTunnel.Server/Startup.cs @@ -1,17 +1,21 @@ using FastTunnel.Core; -using FastTunnel.Core.Config; +using FastTunnel.Core.Extensions; +using FastTunnel.Server.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using FastTunnel.Core.Config; +using System.Text; + #if DEBUG using Microsoft.OpenApi.Models; #endif -using System.Threading; -using System.Threading.Tasks; namespace FastTunnel.Server { @@ -27,6 +31,43 @@ namespace FastTunnel.Server // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + var serverOptions = Configuration.GetSection("FastTunnel").Get(); + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromSeconds(serverOptions.Api.JWT.ClockSkew), + ValidateIssuerSigningKey = true, + ValidAudience = serverOptions.Api.JWT.ValidAudience, + ValidIssuer = serverOptions.Api.JWT.ValidIssuer, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(serverOptions.Api.JWT.IssuerSigningKey)) + }; + + options.Events = new JwtBearerEvents + { + OnChallenge = async context => + { + context.HandleResponse(); + + context.Response.ContentType = "application/json;charset=utf-8"; + context.Response.StatusCode = StatusCodes.Status200OK; + + await context.Response.WriteAsync(new ApiResponse + { + errorCode = ErrorCodeEnum.AuthError, + errorMessage = context.Error ?? "Token is Required" + }.ToJson()); + }, + }; + }); + + services.AddAuthorization(); + services.AddControllers(); #if DEBUG @@ -37,8 +78,8 @@ namespace FastTunnel.Server #endif // -------------------FastTunnel STEP1 OF 3------------------ - services.AddFastTunnelServer(Configuration.GetSection("ServerSettings")); - // -------------------FastTunnel STEP1 END-------------------- + services.AddFastTunnelServer(Configuration.GetSection("FastTunnel")); + // -------------------FastTunnel STEP1 END------------------- } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -55,16 +96,22 @@ namespace FastTunnel.Server // -------------------FastTunnel STEP2 OF 3------------------ app.UseFastTunnelServer(); - // -------------------FastTunnel STEP2 END-------------------- + // -------------------FastTunnel STEP2 END------------------- app.UseRouting(); + // --------------------- Custom UI ---------------- + app.UseStaticFiles(); + app.UseAuthentication(); + app.UseAuthorization(); + // --------------------- Custom UI ---------------- + app.UseEndpoints(endpoints => { endpoints.MapControllers(); // -------------------FastTunnel STEP3 OF 3------------------ endpoints.MapFastTunnelServer(); - // -------------------FastTunnel STEP3 END-------------------- + // -------------------FastTunnel STEP3 END------------------- }); } } diff --git a/FastTunnel.Server/config/appsettings.json b/FastTunnel.Server/config/appsettings.json index 989359a..50b06dc 100644 --- a/FastTunnel.Server/config/appsettings.json +++ b/FastTunnel.Server/config/appsettings.json @@ -8,19 +8,41 @@ } }, "AllowedHosts": "*", - "urls": "http://*:1270;https://*:4443;", // Http&ͻͨѶ˿ + // Http&ͻͨѶ˿ + "urls": "http://*:1270", + // Ƿļ־ "EnableFileLog": false, - "ServerSettings": { - // 󶨵ĸ + "FastTunnel": { + // ѡ󶨵ĸ + // ͻSubDomainʵ ${SubDomain}.${WebDomain}վ㣬ע⣺Ҫͨվʱѡ "WebDomain": "test.cc", - // ѡʰڰipܾΪʱȨ޷ + // ѡʰΪʱȨ޷ʣΪʱڰipܾ "WebAllowAccessIps": [ "192.168.0.101" ], - // ѡǷSSHú󲻴ForwardͶ˿ת.Ĭfalse + // ѡǷ˿תú󲻴ForwardͶ˿ת.Ĭfalse "EnableForward": true, - // ѡΪʱͻҲЯͬToken,ܾ¼ - "Token": "TOKEN_FOR_CLIENT_AUTHENTICATION" + // ѡΪʱͻҲЯͬTokenܾ¼ + "Token": "TOKEN_FOR_CLIENT_AUTHENTICATION", + + /** + * apiӿڵJWT + */ + "Api": { + "JWT": { + "ClockSkew": 10, + "ValidAudience": "https://suidao.io", + "ValidIssuer": "FastTunnel", + "IssuerSigningKey": "This is IssuerSigningKey", + "Expires": 120 + }, + "Accounts": [ + { + "Name": "admin", + "Password": "admin123" + } + ] + } } } \ No newline at end of file diff --git a/FastTunnel.Server/wwwroot/index.html b/FastTunnel.Server/wwwroot/index.html new file mode 100644 index 0000000..b2d525b --- /dev/null +++ b/FastTunnel.Server/wwwroot/index.html @@ -0,0 +1 @@ +index \ No newline at end of file