soulsoft
我們正在探索 Cangjie 與 ASP.NET Core 的深度融合,致力于打造:
? 現(xiàn)代化:基于最新 .NET 技術(shù)棧的開發(fā)范式
? 輕量級(jí):低侵入設(shè)計(jì),零冗余依賴
? 可擴(kuò)展:模塊化架構(gòu),按需組合功能
? 可插拔:通過 gitcode 配置快速集成
誠邀志同道合的開發(fā)者加入 Soulsoft 組織,共同構(gòu)建:
?? 標(biāo)準(zhǔn)化組件庫
?? 統(tǒng)一技術(shù)生態(tài)
?? 開源社區(qū)協(xié)作平臺(tái)
內(nèi)置模塊
| 模塊名稱 | 描述 | 必要性 | 鏈接 |
|---|---|---|---|
| soulsoft_asp_http | HTTP核心功能 | 必需 | 鏈接 |
| soulsoft_asp_mvc | MVC | 可選 | 鏈接 |
| soulsoft_asp_routing | 路由與終結(jié)點(diǎn) | 必需 | 鏈接 |
| soulsoft_asp_hosting | Web主機(jī) | 必需 | 鏈接 |
| soulsoft_asp_staticfiles | 靜態(tài)文件支持 | 可選 | 鏈接 |
| soulsoft_asp_healthchecks | 健康檢查中間件 | 可選 | 鏈接 |
| soulsoft_asp_authentication | 身份認(rèn)證中間件 | 可選 | 鏈接 |
| soulsoft_asp_authentication_jwtbearer | Jwt身份認(rèn)證方案 | 可選 | 鏈接 |
| soulsoft_asp_authorization | 授權(quán)中間件 | 可選 | 鏈接 |
| soulsoft_security_claims | 身份聲明 | 可選 | 鏈接 |
| soulsoft_extensions_hosting | 通用主機(jī) | 可選 | 鏈接 |
| soulsoft_extensions_logging | 日志 | 可選 | 鏈接 |
| soulsoft_extensions_options | 選項(xiàng) | 必需 | 鏈接 |
| soulsoft_extensions_injection | 依賴注入 | 可選 | 鏈接 |
| soulsoft_extensions_healthchecks | 健康檢查服務(wù) | 可選 | 鏈接 |
| soulsoft_extensions_configuration | 配置管理 | 可選 | 鏈接 |
《依賴注入》
- 基本使用
//創(chuàng)建容器構(gòu)建器
let services = ServiceCollection()
//注冊(cè)單例服務(wù)
services.addSingleton<IDbConnection, DbConnection>()
//可以使用工廠模式,來規(guī)避反射
services.addSingleton<IDbConnection>(){ sp =>
DbConnection()
}
//構(gòu)建容器
let provider = services.build()
//解析服務(wù)
let connection = provider.getOrThrow<IDbConnection>()
//解析未注冊(cè)的服務(wù),但是依賴容器中的服務(wù)
let context = ActivatorUtilities.createInstance<DbContext>(provider)
- 生命周期
| 周期 | 說明 |
|---|---|
| Singleton | 單例:一個(gè)根容器及其子容器,只創(chuàng)建一個(gè)實(shí)列 |
| Scoped | 作用域:同一個(gè)作用域只創(chuàng)建一個(gè)實(shí)列,生命周期由業(yè)務(wù)定義 |
| Transient | 瞬時(shí):每次解析都是一個(gè)新的實(shí)列 |
let services = ServiceCollection()
services.addScoped<IDbConnection, DbConnection>()
let provider = services.build()//根容器
//創(chuàng)建作用域
try (scope = provider.createScope()){
//子容器
let connection = scope.services.getOrThrow<IDbConnection>()
}
//作用域結(jié)束時(shí)會(huì)釋放該作用域解析的非單例的實(shí)列(需要實(shí)現(xiàn)Resource接口)。
注意:通過
ServiceCollection直接構(gòu)建的稱為根容器,通過ServiceProvider創(chuàng)建的scope關(guān)聯(lián)的容器稱為子容器。根容器無法解析非單例的服務(wù)。
《選項(xiàng)》
選項(xiàng)是對(duì)依賴注入模塊的擴(kuò)展和補(bǔ)充,用于統(tǒng)一框架設(shè)計(jì)者和使用者之間的約定,設(shè)計(jì)者通過configure方法設(shè)置默認(rèn)值,使用者使用者通過configureAfter方法修改默認(rèn)值
- 基本使用
//定義一個(gè)選項(xiàng)
public class DbConnectionOptions {
var connectionString = "default"
}
//定義容器
let services = ServiceCollection()
services.configureAfter<DbConnectionOptions>{configureOptions =>
configureOptions.connectionString = "2.1"
}
services.configureAfter<DbConnectionOptions>{configureOptions =>
configureOptions.connectionString = "2.2"
}
services.configure<DbConnectionOptions>{configureOptions =>
configureOptions.connectionString = "1.1"
}
services.configure<DbConnectionOptions>{configureOptions =>
configureOptions.connectionString = "1.2"
}
//構(gòu)建容器
let provider = services.build()
//解析選項(xiàng)
let options = provider.getOrThrow<IOptions<DbConnectionOptions>>()
println(options.value.connectionString)//輸出2.2
注意:上面的版本號(hào)即執(zhí)行順序,無論解析多少次每個(gè)lambda函數(shù)只執(zhí)行一次
- 命名選項(xiàng)
let services = ServiceCollection()
services.configure<DbConnectionOptions>("tenant1"){configureOptions =>
configureOptions.connectionString = "1.1"
}
services.configure<DbConnectionOptions>("tenant2"){configureOptions =>
configureOptions.connectionString = "1.2"
}
let provider = services.build()
let options = provider.getOrThrow<IOptions<DbConnectionOptions>>()
println(options.value.connectionString)
println(options.get("tenant1").connectionString)
println(options.get("tenant2").connectionString)
命名選項(xiàng)在多租戶,多架構(gòu)場景下非常有用
《配置》
配置支持多數(shù)據(jù)源(命令行參數(shù),環(huán)境變量,json)和自定義數(shù)據(jù)來源。
main(args: Array<String>) {
let configurationBuilder = ConfigurationManager()
//添加命令行參數(shù)
configurationBuilder.addArgVars(args)
//添加“asp_”開頭的環(huán)境變量
configurationBuilder.addEnvVars("asp")
//添加json配置
configurationBuilder.addJsonFile("./appsettings.json", true)
let configuration = configurationBuilder.build()
println(configuration["help"])
println(configuration["port"])
//循環(huán)處理所有l(wèi)ogging:logLevel節(jié)點(diǎn)下的直接屬性
for (pattern in configuration.getSection("logging:logLevel").getChildren()) {
println("${pattern.key}=${pattern.value}")
}
return 0
}
appsettings.json
{
"logging": {
"logLevel": {
"default": "Info",
"soulsoft": "Error"
}
}
}
《日志》
日志模塊也是應(yīng)用開發(fā)過程中必備可卻的組件,日志模塊內(nèi)置了控制臺(tái)和文件提供程序,同樣也支持自定義日志提供程序
- 基本使用
let logFactory = LoggingBuilder()
.addFile()
.addConsole()
.build()
let logger = logFactory.createLogger("soulsoft.logging.test")
logger.info("hello")
- 使用日志過濾器
let logFactory = LoggingBuilder()
.addFile()
.addConsole()
.addFilter{providerName, categoryName, logLevel =>
(providerName == "file" && logLevel >= LogLevel.Error) ||
(providerName == "console" && logLevel >= LogLevel.Info)
}
.build()
let logger = logFactory.createLogger("soulsoft.logging.test")
logger.info("hello")//文件中不打印,控制臺(tái)中打印
logger.error("hello")//文件中打印,控制臺(tái)中打印
- 使用配置文件過濾
- file提供程序過濾規(guī)則:以
asp結(jié)尾的日志,只打印Warn及以上級(jí)別的日志 - console提供程序過濾規(guī)則:以
asp開頭的日志,只打印Info及以上級(jí)別的日志 - 默認(rèn)過濾規(guī)則:除上述之外打印Info及以上級(jí)別的日志
//創(chuàng)建配置
let configurationBuilder = ConfigurationManager()
configurationBuilder.addJsonFile("./appsettings.json", true)
let configuration = configurationBuilder.build()
//創(chuàng)建日志工廠
let logFactory = LoggingBuilder()
.addFile()
.addConsole()
.addConfiguration(configuration.getSection("logging"))
.build()
//測試
logFactory.createLogger("soulsoft.asp").info("hello")
logFactory.createLogger("asp.soulsoft").info("hello")
logFactory.createLogger("cangjie").info("hello")
./appsettings.json
{
"logging": {
"logLevel": {
"default": "Info"
},
"file": {
"logLevel": {
"*.asp": "Warn"
}
},
"console": {
"logLevel": {
"asp.*": "Info"
}
}
}
}
《通用主機(jī)》
通用主機(jī)整合了上述所有模塊,用于處理定時(shí)任務(wù)和消息隊(duì)列
- 創(chuàng)建一個(gè)工人和選項(xiàng)
public class TestWorkerOptions {
public var delay = 10
}
public class TestWorker <: BackgroundService {
private let _logger: ILogger
public TestWorker(let _options: IOptions<TestWorkerOptions>, let _env: IHostEnvironment, let _logFactory: ILoggerFactory) {
_logger = _logFactory.createLogger<TestWorker>()
}
public func run() {
while (!Thread.currentThread.hasPendingCancellation) {
//不同環(huán)境,執(zhí)行不同邏輯
if(_env.environmentName == "prod") {
logger.info("working...")
}else {
logger.info("hello...")
}
sleep(_options.value.delay * Duration.second)
}
}
}
- 定義一個(gè)擴(kuò)展
extend ServiceCollection{
public func addTestWorker(configureOptions: (TestWorkerOptions) -> Unit): ServiceCollection {
this.addHostedService<TestWorker>()
this.configure<TestWorkerOptions>(configureOptions)
}
}
- 啟動(dòng)主機(jī)
main(args: Array<String>) {
let builder = Host.createBuilder(args)
//注冊(cè)我們的后臺(tái)服務(wù)
builder.services.addTestWorker()
let host = builder.build()
host.run()
return 0
}
- 主機(jī)內(nèi)置了
IHostEnvironment服務(wù),可以通過解析它來區(qū)分開發(fā)環(huán)境還是生成環(huán)境- 可以通過
asp_environment環(huán)境變量或者--environment=test命令行參數(shù)來修改環(huán)境名
《Web主機(jī)》
web主機(jī)實(shí)現(xiàn)了通用主機(jī),并且在此基礎(chǔ)上擴(kuò)展了http協(xié)議,內(nèi)置請(qǐng)求管道來處理請(qǐng)求邏輯。soulsoft組織提供了了大量的中間件供開發(fā)者使用
啟動(dòng)一個(gè)支持靜態(tài)文件的web主機(jī)
main(args: Array<String>) {
let builder = WebHost.createBuilder(args)
let host = builder.build()
//當(dāng)請(qǐng)求網(wǎng)站根路徑(/)時(shí),負(fù)責(zé)查找并返回index.html頁面
host.useDefaultFiles()
//該中間件負(fù)責(zé)去wwwroot中查找并返回/xxx.(html|css|js|...)文件
host.useStaticFiles()
host.run()
return 0
}
啟動(dòng)一個(gè)支持動(dòng)態(tài)資源的web主機(jī)
main(args: Array<String>) {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()//注冊(cè)路由中間件需要的服務(wù)
let host = builder.build()
//useEndpoints:會(huì)注冊(cè)兩個(gè)中間件,一個(gè)負(fù)責(zé)路由,一個(gè)負(fù)責(zé)執(zhí)行終結(jié)點(diǎn)
host.useEndpoints { endpoints =>
endpoints.mapGet("hello") {
context => context.response.write("hello:soulsoft")
}
}
host.run()
return 0
}
- 路由中間件(EndpointRoutingMiddleware):負(fù)責(zé)根據(jù)用戶輸入的
uri查找對(duì)應(yīng)的Endpoint并放到HttpContext上(調(diào)用setEndpoint)- 終結(jié)點(diǎn)中間件(EndpointMiddleware):通過調(diào)用
HttpContext上面的getEndpoint()方法獲取終結(jié)點(diǎn),如果存在Endpoint將會(huì)執(zhí)行它
健康檢查中間件
main(args: Array<String>) {
let builder = WebHost.createBuilder(args)
builder.services.addHealthChecks()
//添加一個(gè)健康檢查項(xiàng)
.addCheck("self") {
//模擬隨機(jī)不健康效果
let random = Random()
if (random.nextInt32(10) % 2 == 0) {
HealthCheckResult.healthy()
} else {
HealthCheckResult.unhealthy()
}
}
let host = builder.build()
host.useHealthChecks("/health")
host.run()
return 0
}
《身份認(rèn)證》
Basic認(rèn)證方案
我們?cè)谏矸菡J(rèn)證模塊下可以非常方便的實(shí)現(xiàn)一個(gè)認(rèn)證方案,比如Basic認(rèn)證方案。身份認(rèn)證模塊為我們處理好了認(rèn)證和授權(quán)流程
public class BasicAuthenticationDefault {
public static let scheme = "basic"
}
public class BasicAuthenticationOptions <: AuthenticationSchemeOptions {
public var realm = "basic"
}
//定義basic認(rèn)證方案
public class BasicAuthenticationHandler <: AuthenticationHandler<BasicAuthenticationOptions> {
public init(options: IOptions<BasicAuthenticationOptions>, logger: ILoggerFactory) {
super(options, logger)
}
public func handleAuthenticate() {
if (let Some(authorization) <- this.context.request.headers.getFirst("Authorization")
.flatMap{f => fromBase64String(f.replace("Basic ", "")).flatMap{f=> String.fromUtf8(f)} }) {
let secrets = authorization.split(":")
if (secrets.size == 2) {
let username = secrets[0]
let password = secrets[1]
this.logger.info("username:${username},password:${password}")
//通過子容器來解析非單例的服務(wù):IResourceOwnerPasswordValidator
let validator = this.context.services.getOrThrow<IResourceOwnerPasswordValidator>()
if (!validator.validate(username, password)) {
return AuthenticateResult.fail(Exception("Invalid username or password"))
}
let subject = ClaimsPrincipal()
let identity = ClaimsIdentity(this.scheme.name)
identity.addClaim(Claim("username",username))
identity.addClaim(Claim("password",password))
subject.addIdentity(identity)
let ticket = AuthenticationTicket(subject, BasicAuthenticationDefault.scheme)
return AuthenticateResult.success(ticket)
}
}
return AuthenticateResult.noResult()
}
/*
重寫調(diào)戰(zhàn)處理邏輯:返回401狀態(tài)碼和Basic協(xié)議頭
*/
protected override func handleChallenge(properties: ?AuthenticationProperties): Unit {
this.context.response.addHeader("WWW-Authenticate", "Basic realm=\"${this.options.realm}\", charset=\"UTF-8\"")
super.handleChallenge(properties)
}
}
//定義資源所有者憑據(jù)驗(yàn)證
public interface IResourceOwnerPasswordValidator {
func validate(username: String, password: String): Bool
}
//實(shí)現(xiàn)資源所有者憑據(jù)驗(yàn)證
public class ResourceOwnerPasswordValidator <: IResourceOwnerPasswordValidator {
let users = HashMap<String, String>([("soulsoft", "soulsoft")])
public func validate(username: String, password: String) {
if (!users.contains(username)) {
return false
}
return users[username] == password
}
}
啟動(dòng)web主機(jī)來運(yùn)行
main (args: Array<String>) {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()
//注冊(cè)資源所有者驗(yàn)證器
builder.services.addTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>()
//注冊(cè)身份認(rèn)證服務(wù)
builder.services.addAuthentication(BasicAuthenticationDefault.scheme)
//注冊(cè)basic認(rèn)證方案
.addScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>(BasicAuthenticationDefault.scheme)
//注冊(cè)授權(quán)服務(wù)
builder.services.addAuthorizationBuilder()
//定義授權(quán)策略
.addPolicy("base-policy"){ policy =>
//必須包含username
policy.requireClaim("username")
//基本要求,具體參考源碼
policy.requireAuthenticatedUser()
}
let host = builder.build()
//啟用認(rèn)證中間件
host.useAuthentication()
host.useRouting()
//啟用授權(quán)中間件
host.useAuthorization()
host.run()
host.useEndpoints { endpoints =>
endpoints.mapGet("logout") {
context => context.response.write("logout succeeded")
}
//啟用認(rèn)證策略
.requireAuthorization("base-policy")
}
return 0
}
- 由于
授權(quán)中間件需要使用路由到的Endpoint,對(duì)終結(jié)點(diǎn)授權(quán),因此授權(quán)中間件必須放到useRouting后面- 注意:認(rèn)證是確定你是誰,無論成果與否都不影響流程,而授權(quán),需要驗(yàn)證你的身份,如果身份認(rèn)證不通過,那么將會(huì)發(fā)起
challenge(挑戰(zhàn)),并返回401狀態(tài)碼。如果身份認(rèn)證通過,但是不滿足授權(quán)策略將會(huì)發(fā)起forbid(禁止)返回403狀態(tài)碼。你可以通過override來重寫挑戰(zhàn)和禁止的邏輯- web主機(jī)在分發(fā)請(qǐng)求的時(shí)候,創(chuàng)建了一個(gè)子容器放到
HttpContext的services字段上,進(jìn)而實(shí)現(xiàn)請(qǐng)求scope級(jí)別的生命周期
Jwt認(rèn)證方案
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
//==============服務(wù)注冊(cè)==================
//注冊(cè)路由
builder.services.AddRouting()
//注冊(cè)身份認(rèn)證方案
builder.services.addAuthentication(JwtBearerAuthenticationDefaults.Scheme)
//注冊(cè)basic認(rèn)證方案
.addScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>(BasicAuthenticationDefault.Scheme)
//注冊(cè)jwtBearer認(rèn)證方案
.addJwtBearer(JwtBearerAuthenticationDefaults.Scheme) { configureOptions =>
let securityKey = SymmetricSecurityKey(builder.configuration["authentication:securityKey"].getOrThrow().toArray())
configureOptions.tokenValidationParameters = TokenValidationParameters(securityKey)
}
//注冊(cè)授權(quán)服務(wù)
builder.services.addAuthorizationBuilder()
.addPolicy("default"){ policy =>
//必須包含username
policy.requireClaim("username")
//基本要求,具體參考源碼
policy.requireAuthenticatedUser()
}
//==============請(qǐng)求管道==================
let host = builder.build()
//使用身份認(rèn)證
host.useAuthentication()
//動(dòng)態(tài)資源路由(負(fù)責(zé)路由,并放到HttpContext上)
host.useRouting()
//由于該中間件需要使用路由到的endpoint,因此必須放到useRouting后面
host.useAuthorization()
//動(dòng)態(tài)資源(負(fù)責(zé)注冊(cè)和執(zhí)行)
host.useEndpoints { endpoints =>
//創(chuàng)建jwt token
endpoints.mapGet("connect/token"){ context =>
let securityKey = SymmetricSecurityKey(host.configuration["authentication:securityKey"].getOrThrow().toArray())
let jwtHeader = JwtHeader(SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256))
let jwtPayload = JwtPayload([("sub", "1024"), ("username", "soulsoft")])
let jwtTokenHander = JwtSecurityTokenHandler()
let accessToken = jwtTokenHander.writeToken(JwtSecurityToken(jwtHeader, jwtPayload))
context.response.write(accessToken)
}
//登入接口需要授權(quán)
endpoints.mapGet("connect/logout") {
context => context.response.write("logout succeeded")
}.requireAuthorization("default")
}
host.run()
return 0
}
浙公網(wǎng)安備 33010602011771號(hào)