Repository: dromara/Sa-Token Branch: dev Commit: 47dff7059dab Files: 1492 Total size: 4.8 MB Directory structure: gitextract_sl8kal2d/ ├── .agents/ │ └── skills/ │ ├── README.md │ ├── commit-message/ │ │ ├── SKILL.md │ │ ├── examples.md │ │ └── reference.md │ ├── organize-update-log/ │ │ ├── SKILL.md │ │ └── format-reference.md │ ├── remove-redundancy-import/ │ │ ├── SKILL.md │ │ ├── reference.md │ │ └── scan_redundant_imports.py │ └── upgrade-version/ │ └── SKILL.md ├── .gitee/ │ └── ISSUE_TEMPLATE.md ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug反馈.md │ ├── 功能提问.md │ ├── 建议增加新功能.md │ └── 预期不符.md ├── .gitignore ├── LICENSE ├── MEMO/ │ ├── 1--统一定义properties尝试失败.md │ ├── 2--2026-3-1_诡异调试记录.txt │ └── 3--sa-token_最新版所有依赖.txt ├── README.md ├── mvn clean.bat ├── mvn test.bat ├── pom.xml ├── preview-doc.bat ├── sa-token-bom/ │ └── pom.xml ├── sa-token-core/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── cn/ │ └── dev33/ │ └── satoken/ │ ├── SaManager.java │ ├── annotation/ │ │ ├── SaCheckDisable.java │ │ ├── SaCheckHttpBasic.java │ │ ├── SaCheckHttpDigest.java │ │ ├── SaCheckLogin.java │ │ ├── SaCheckOr.java │ │ ├── SaCheckPermission.java │ │ ├── SaCheckRole.java │ │ ├── SaCheckSafe.java │ │ ├── SaIgnore.java │ │ ├── SaMode.java │ │ └── handler/ │ │ ├── SaAnnotationHandlerInterface.java │ │ ├── SaCheckDisableHandler.java │ │ ├── SaCheckHttpBasicHandler.java │ │ ├── SaCheckHttpDigestHandler.java │ │ ├── SaCheckLoginHandler.java │ │ ├── SaCheckOrHandler.java │ │ ├── SaCheckPermissionHandler.java │ │ ├── SaCheckRoleHandler.java │ │ ├── SaCheckSafeHandler.java │ │ └── SaIgnoreHandler.java │ ├── application/ │ │ ├── ApplicationInfo.java │ │ ├── SaApplication.java │ │ ├── SaGetValueInterface.java │ │ └── SaSetValueInterface.java │ ├── config/ │ │ ├── SaCookieConfig.java │ │ ├── SaTokenConfig.java │ │ └── SaTokenConfigFactory.java │ ├── context/ │ │ ├── SaHolder.java │ │ ├── SaTokenContext.java │ │ ├── SaTokenContextDefaultImpl.java │ │ ├── SaTokenContextForReadOnly.java │ │ ├── SaTokenContextForThreadLocal.java │ │ ├── SaTokenContextForThreadLocalStaff.java │ │ ├── mock/ │ │ │ ├── SaRequestForMock.java │ │ │ ├── SaResponseForMock.java │ │ │ ├── SaStorageForMock.java │ │ │ └── SaTokenContextMockUtil.java │ │ └── model/ │ │ ├── SaCookie.java │ │ ├── SaRequest.java │ │ ├── SaResponse.java │ │ ├── SaStorage.java │ │ ├── SaTokenContextModelBox.java │ │ └── package-info.java │ ├── dao/ │ │ ├── SaTokenDao.java │ │ ├── SaTokenDaoDefaultImpl.java │ │ ├── auto/ │ │ │ ├── SaTokenDaoByObjectFollowString.java │ │ │ ├── SaTokenDaoBySessionFollowObject.java │ │ │ └── SaTokenDaoByStringFollowObject.java │ │ └── timedcache/ │ │ ├── SaMapPackage.java │ │ ├── SaMapPackageForConcurrentHashMap.java │ │ └── SaTimedCache.java │ ├── error/ │ │ └── SaErrorCode.java │ ├── exception/ │ │ ├── ApiDisabledException.java │ │ ├── BackResultException.java │ │ ├── DisableServiceException.java │ │ ├── FirewallCheckException.java │ │ ├── InvalidContextException.java │ │ ├── NotHttpBasicAuthException.java │ │ ├── NotHttpDigestAuthException.java │ │ ├── NotImplException.java │ │ ├── NotLoginException.java │ │ ├── NotPermissionException.java │ │ ├── NotRoleException.java │ │ ├── NotSafeException.java │ │ ├── NotWebContextException.java │ │ ├── RequestPathInvalidException.java │ │ ├── SaJsonConvertException.java │ │ ├── SaTokenContextException.java │ │ ├── SaTokenException.java │ │ ├── SaTokenPluginException.java │ │ ├── SameTokenInvalidException.java │ │ ├── StopMatchException.java │ │ └── TotpAuthException.java │ ├── filter/ │ │ ├── SaFilter.java │ │ ├── SaFilterAuthStrategy.java │ │ └── SaFilterErrorStrategy.java │ ├── fun/ │ │ ├── IsRunFunction.java │ │ ├── SaFunction.java │ │ ├── SaParamFunction.java │ │ ├── SaParamRetFunction.java │ │ ├── SaRetFunction.java │ │ ├── SaRetGenericFunction.java │ │ ├── SaRouteFunction.java │ │ ├── SaTwoParamFunction.java │ │ ├── hooks/ │ │ │ └── SaTokenPluginHookFunction.java │ │ └── strategy/ │ │ ├── SaAutoRenewFunction.java │ │ ├── SaCheckELRootMapExtendFunction.java │ │ ├── SaCheckElementAnnotationFunction.java │ │ ├── SaCheckMethodAnnotationFunction.java │ │ ├── SaCheckOrAnnotationFunction.java │ │ ├── SaCorsHandleFunction.java │ │ ├── SaCreateSessionFunction.java │ │ ├── SaCreateStpLogicFunction.java │ │ ├── SaCreateTokenFunction.java │ │ ├── SaFirewallCheckFailHandleFunction.java │ │ ├── SaFirewallCheckFunction.java │ │ ├── SaGenerateUniqueTokenFunction.java │ │ ├── SaGetAnnotationFunction.java │ │ ├── SaHasElementFunction.java │ │ ├── SaIsAnnotationPresentFunction.java │ │ └── SaRouteMatchFunction.java │ ├── http/ │ │ ├── SaHttpTemplate.java │ │ ├── SaHttpTemplateDefaultImpl.java │ │ └── SaHttpUtil.java │ ├── httpauth/ │ │ ├── basic/ │ │ │ ├── SaHttpBasicAccount.java │ │ │ ├── SaHttpBasicTemplate.java │ │ │ └── SaHttpBasicUtil.java │ │ └── digest/ │ │ ├── SaHttpDigestModel.java │ │ ├── SaHttpDigestTemplate.java │ │ └── SaHttpDigestUtil.java │ ├── json/ │ │ ├── SaJsonTemplate.java │ │ └── SaJsonTemplateDefaultImpl.java │ ├── listener/ │ │ ├── SaTokenEventCenter.java │ │ ├── SaTokenListener.java │ │ ├── SaTokenListenerForLog.java │ │ └── SaTokenListenerForSimple.java │ ├── log/ │ │ ├── SaLog.java │ │ └── SaLogForConsole.java │ ├── model/ │ │ └── wrapperInfo/ │ │ └── SaDisableWrapperInfo.java │ ├── plugin/ │ │ ├── SaTokenPlugin.java │ │ ├── SaTokenPluginHolder.java │ │ └── SaTokenPluginHookModel.java │ ├── router/ │ │ ├── SaHttpMethod.java │ │ ├── SaRouter.java │ │ └── SaRouterStaff.java │ ├── same/ │ │ ├── SaSameTemplate.java │ │ └── SaSameUtil.java │ ├── secure/ │ │ ├── BCrypt.java │ │ ├── SaBase32Util.java │ │ ├── SaBase64Util.java │ │ ├── SaSecureUtil.java │ │ └── totp/ │ │ ├── SaTotpTemplate.java │ │ └── SaTotpUtil.java │ ├── serializer/ │ │ ├── SaSerializerTemplate.java │ │ └── impl/ │ │ ├── SaSerializerTemplateForJdk.java │ │ ├── SaSerializerTemplateForJdkUseBase64.java │ │ ├── SaSerializerTemplateForJdkUseHex.java │ │ ├── SaSerializerTemplateForJdkUseISO_8859_1.java │ │ └── SaSerializerTemplateForJson.java │ ├── session/ │ │ ├── SaSession.java │ │ ├── SaSessionCustomUtil.java │ │ ├── SaTerminalInfo.java │ │ └── raw/ │ │ ├── SaRawSessionDelegator.java │ │ └── SaRawSessionUtil.java │ ├── stp/ │ │ ├── SaLoginConfig.java │ │ ├── SaLoginModel.java │ │ ├── SaTokenInfo.java │ │ ├── StpInterface.java │ │ ├── StpInterfaceDefaultImpl.java │ │ ├── StpLogic.java │ │ ├── StpUtil.java │ │ └── parameter/ │ │ ├── SaLoginParameter.java │ │ ├── SaLogoutParameter.java │ │ └── enums/ │ │ ├── SaLogoutMode.java │ │ ├── SaLogoutRange.java │ │ ├── SaReplacedLoginExitMode.java │ │ └── SaReplacedRange.java │ ├── strategy/ │ │ ├── SaAnnotationStrategy.java │ │ ├── SaFirewallStrategy.java │ │ ├── SaStrategy.java │ │ └── hooks/ │ │ ├── SaFirewallCheckHook.java │ │ ├── SaFirewallCheckHookForBlackPath.java │ │ ├── SaFirewallCheckHookForDirectoryTraversal.java │ │ ├── SaFirewallCheckHookForHeader.java │ │ ├── SaFirewallCheckHookForHost.java │ │ ├── SaFirewallCheckHookForHttpMethod.java │ │ ├── SaFirewallCheckHookForParameter.java │ │ ├── SaFirewallCheckHookForPathBannedCharacter.java │ │ ├── SaFirewallCheckHookForPathDangerCharacter.java │ │ └── SaFirewallCheckHookForWhitePath.java │ ├── temp/ │ │ ├── SaTempTemplate.java │ │ └── SaTempUtil.java │ └── util/ │ ├── SaFoxUtil.java │ ├── SaHexUtil.java │ ├── SaResult.java │ ├── SaSugar.java │ ├── SaTokenConsts.java │ ├── SaTtlMethods.java │ ├── SaValue2Box.java │ └── StrFormatter.java ├── sa-token-demo/ │ ├── pom.xml │ ├── sa-token-demo-alone-redis/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenAloneRedisApplication.java │ │ │ └── test/ │ │ │ ├── AjaxJson.java │ │ │ └── TestController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-alone-redis-cluster/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenAloneRedisClusterApplication.java │ │ │ └── test/ │ │ │ ├── AjaxJson.java │ │ │ └── TestController.java │ │ └── resources/ │ │ ├── application.yml │ │ └── application_sentinel.yml │ ├── sa-token-demo-alone-redis-sb4/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenAloneRedisSb4Application.java │ │ │ └── test/ │ │ │ └── TestController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-apikey/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenApiKeyApplication.java │ │ │ ├── mock/ │ │ │ │ ├── SaApiKeyDataLoaderImpl.java │ │ │ │ └── SaApiKeyMockMapper.java │ │ │ ├── satoken/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── SaTokenConfigure.java │ │ │ └── test/ │ │ │ ├── ApiKeyController.java │ │ │ ├── ApiKeyResourcesController.java │ │ │ └── LoginController.java │ │ └── resources/ │ │ ├── application.yml │ │ └── static/ │ │ ├── common.js │ │ └── index.html │ ├── sa-token-demo-async/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenAsyncApplication.java │ │ │ ├── current/ │ │ │ │ └── GlobalException.java │ │ │ ├── satoken/ │ │ │ │ └── SaTokenConfigure.java │ │ │ └── test/ │ │ │ └── TestController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-beetl/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenBeetlDemoApplication.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── GlobalException.java │ │ │ └── TestController.java │ │ └── resources/ │ │ ├── application.yml │ │ └── templates/ │ │ └── index.btl │ ├── sa-token-demo-bom-import/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApplication.java │ │ │ ├── current/ │ │ │ │ └── GlobalException.java │ │ │ └── test/ │ │ │ └── TestController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-caffeine/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApplication.java │ │ │ ├── current/ │ │ │ │ └── GlobalException.java │ │ │ └── test/ │ │ │ └── LoginController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-case/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenCaseApplication.java │ │ │ ├── cases/ │ │ │ │ ├── more/ │ │ │ │ │ └── SaCheckELController.java │ │ │ │ ├── plugin/ │ │ │ │ │ └── TempTokenController.java │ │ │ │ ├── test/ │ │ │ │ │ └── TestController.java │ │ │ │ ├── up/ │ │ │ │ │ ├── DisableController.java │ │ │ │ │ ├── HttpBasicController.java │ │ │ │ │ ├── MutexLoginController.java │ │ │ │ │ ├── NotCookieController.java │ │ │ │ │ ├── RememberMeController.java │ │ │ │ │ ├── SafeAuthController.java │ │ │ │ │ ├── SearchSessionController.java │ │ │ │ │ ├── SecureController.java │ │ │ │ │ └── SwitchToController.java │ │ │ │ └── use/ │ │ │ │ ├── AtCheckController.java │ │ │ │ ├── JurAuthController.java │ │ │ │ ├── KickoutController.java │ │ │ │ ├── LoginAuthController.java │ │ │ │ ├── RouterCheckController.java │ │ │ │ └── SaSessionController.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── model/ │ │ │ │ └── SysUser.java │ │ │ └── satoken/ │ │ │ ├── MySaTempTemplate.java │ │ │ ├── MySaTokenListener.java │ │ │ ├── SaLogForSlf4j.java │ │ │ ├── SaTokenConfigure.java │ │ │ ├── StpInterfaceImpl.java │ │ │ ├── StpUserUtil.java │ │ │ ├── custom_annotation/ │ │ │ │ ├── CheckAccount.java │ │ │ │ ├── SaUserCheckLogin.java │ │ │ │ ├── SaUserCheckPermission.java │ │ │ │ ├── SaUserCheckRole.java │ │ │ │ ├── SaUserCheckSafe.java │ │ │ │ └── handler/ │ │ │ │ ├── CheckAccountHandler.java │ │ │ │ ├── SaUserCheckLoginHandler.java │ │ │ │ ├── SaUserCheckPermissionHandler.java │ │ │ │ ├── SaUserCheckRoleHandler.java │ │ │ │ └── SaUserCheckSafeHandler.java │ │ │ └── merge_annotation/ │ │ │ ├── SaUserCheckLogin.java │ │ │ ├── SaUserCheckPermission.java │ │ │ ├── SaUserCheckRole.java │ │ │ └── SaUserCheckSafe.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-device-lock/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDeviceLockApplication.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── satoken/ │ │ │ │ └── SaTokenConfigure.java │ │ │ ├── test/ │ │ │ │ ├── LoginController.java │ │ │ │ └── SysUserMockDao.java │ │ │ └── util/ │ │ │ ├── DeviceLockCheckUtil.java │ │ │ └── PhoneCodeUtil.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-device-lock-h5/ │ │ ├── common.js │ │ ├── device-lock-auth.html │ │ ├── index.html │ │ └── login.html │ ├── sa-token-demo-dubbo/ │ │ ├── sa-token-demo-dubbo-consumer/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── DubboConsumerApplication.java │ │ │ │ ├── controller/ │ │ │ │ │ └── TestController.java │ │ │ │ └── service/ │ │ │ │ └── DemoService.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── sa-token-demo-dubbo-provider/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── DubboProviderApplication.java │ │ │ │ ├── controller/ │ │ │ │ │ └── TestController.java │ │ │ │ └── service/ │ │ │ │ ├── DemoService.java │ │ │ │ └── DemoServiceImpl.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── sa-token-demo-dubbo3-consumer/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── Dubbo3ConsumerApplication.java │ │ │ │ ├── controller/ │ │ │ │ │ └── TestController.java │ │ │ │ └── service/ │ │ │ │ └── DemoService.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ └── sa-token-demo-dubbo3-provider/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── Dubbo3ProviderApplication.java │ │ │ ├── controller/ │ │ │ │ └── TestController.java │ │ │ └── service/ │ │ │ ├── DemoService.java │ │ │ └── DemoServiceImpl.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-freemarker/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenFreemarkerDemoApplication.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── GlobalException.java │ │ │ └── TestController.java │ │ └── resources/ │ │ ├── application.yml │ │ └── templates/ │ │ └── index.ftl │ ├── sa-token-demo-grpc/ │ │ ├── client/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── lym/ │ │ │ │ ├── Client.java │ │ │ │ ├── controller/ │ │ │ │ │ └── TestController.java │ │ │ │ └── grpc/ │ │ │ │ └── client/ │ │ │ │ └── GrpcAuthService.java │ │ │ ├── proto/ │ │ │ │ └── auth.proto │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── pom.xml │ │ └── server/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── lym/ │ │ │ ├── Server.java │ │ │ ├── grpc/ │ │ │ │ └── server/ │ │ │ │ └── GrpcAuthService.java │ │ │ └── service/ │ │ │ └── AuthService.java │ │ ├── proto/ │ │ │ └── auth.proto │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-hutool-timed-cache/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApplication.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-jwt/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenJwtDemoApplication.java │ │ │ ├── satoken/ │ │ │ │ └── SaTokenConfigure.java │ │ │ ├── test/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── TestJwtController.java │ │ │ └── util/ │ │ │ └── AjaxJson.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-loveqq-boot/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenLoveqqApplication.java │ │ │ ├── satoken/ │ │ │ │ ├── MyFilter.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── GlobalException.java │ │ │ ├── TestController.java │ │ │ └── UserService.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-oauth2/ │ │ ├── sa-token-demo-oauth2-client/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaOAuth2ClientApplication.java │ │ │ │ ├── oauth2/ │ │ │ │ │ └── SaOAuthClientController.java │ │ │ │ └── utils/ │ │ │ │ └── SoMap.java │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ └── templates/ │ │ │ └── index.html │ │ ├── sa-token-demo-oauth2-client-h5/ │ │ │ └── index.html │ │ ├── sa-token-demo-oauth2-server/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaOAuth2ServerApplication.java │ │ │ │ ├── mock/ │ │ │ │ │ └── SaClientMockDao.java │ │ │ │ ├── oauth2/ │ │ │ │ │ ├── SaOAuth2DataLoaderImpl.java │ │ │ │ │ ├── SaOAuth2ResourcesController.java │ │ │ │ │ ├── SaOAuth2ServerController.java │ │ │ │ │ ├── custom_grant_type/ │ │ │ │ │ │ ├── CustomPasswordGrantTypeHandler.java │ │ │ │ │ │ ├── PhoneCodeGrantTypeHandler.java │ │ │ │ │ │ └── PhoneLoginController.java │ │ │ │ │ ├── custom_scope/ │ │ │ │ │ │ ├── CustomOidcScopeHandler.java │ │ │ │ │ │ └── UserinfoScopeHandler.java │ │ │ │ │ └── h5/ │ │ │ │ │ └── SaOAuth2ServerH5Controller.java │ │ │ │ ├── satoken/ │ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ │ └── SaTokenConfigure.java │ │ │ │ └── test/ │ │ │ │ ├── Test2Controller.java │ │ │ │ └── TestController.java │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ └── templates/ │ │ │ ├── confirm.html │ │ │ └── login.html │ │ └── sa-token-demo-oauth2-server-h5/ │ │ ├── login.css │ │ ├── login.js │ │ └── oauth2-authorize.html │ ├── sa-token-demo-quick-login/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaQuicikStartup.java │ │ │ ├── SaTokenQuickDemoApplication.java │ │ │ └── test/ │ │ │ └── TestController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-quick-login-sb3/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaQuicikStartup.java │ │ │ ├── SaTokenQuickSb3DemoApplication.java │ │ │ └── test/ │ │ │ └── TestController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-remember-me/ │ │ ├── page_project/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.vue │ │ │ │ └── main.js │ │ │ └── vite.config.js │ │ └── sa-token-demo-remember-me-server/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cc/ │ │ │ └── sa_token/ │ │ │ ├── RememberMeApplication.java │ │ │ └── controller/ │ │ │ └── UserLoginController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-solon/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApp.java │ │ │ ├── satoken/ │ │ │ │ ├── SaLogForSlf4j.java │ │ │ │ ├── SaLogForSolon.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ ├── StpInterfaceImpl.java │ │ │ │ └── custom_annotation/ │ │ │ │ ├── CheckAccount.java │ │ │ │ └── handler/ │ │ │ │ └── CheckAccountHandler.java │ │ │ ├── test/ │ │ │ │ ├── GlobalExceptionFilter.java │ │ │ │ ├── StressTestController.java │ │ │ │ ├── TestController.java │ │ │ │ └── UserController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── app.yml │ ├── sa-token-demo-solon-redisson/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApp.java │ │ │ ├── satoken/ │ │ │ │ ├── SaLogForSlf4j.java │ │ │ │ ├── SaLogForSolon.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── GlobalExceptionFilter.java │ │ │ │ ├── SSOController.java │ │ │ │ ├── StressTestController.java │ │ │ │ ├── TestController.java │ │ │ │ └── UserController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── app.yml │ ├── sa-token-demo-springboot/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApplication.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── AtController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-springboot-low-version/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenApplication.java │ │ │ ├── current/ │ │ │ │ └── GlobalException.java │ │ │ ├── satoken/ │ │ │ │ └── SaTokenConfigure.java │ │ │ └── test/ │ │ │ └── LoginController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-springboot-redis/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApplication.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── AtController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-springboot-redisson/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenDemoApplication.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── redisson/ │ │ │ │ ├── RedissonConfig.java │ │ │ │ └── RedissonProperties.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── AtController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-springboot3-redis/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenSpringBoot3Application.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── AtController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-springboot4-redis/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenSpringBoot4Application.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── test/ │ │ │ │ ├── AtController.java │ │ │ │ ├── FaviconController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-sse/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenSseApplication.java │ │ │ ├── current/ │ │ │ │ └── GlobalException.java │ │ │ ├── satoken/ │ │ │ │ └── SaTokenConfigure.java │ │ │ ├── test/ │ │ │ │ ├── LoginController.java │ │ │ │ ├── SseAdminController.java │ │ │ │ └── SseController.java │ │ │ └── util/ │ │ │ └── SseEmitterHolder.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-ssm/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── controller/ │ │ │ │ ├── AtController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── PageController.java │ │ │ │ └── TestController.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── model/ │ │ │ │ └── SysUser.java │ │ │ └── satoken/ │ │ │ ├── SaInterceptorImpl.java │ │ │ ├── SaTokenBeanInjection.java │ │ │ └── StpInterfaceImpl.java │ │ ├── resources/ │ │ │ ├── application.yml │ │ │ ├── applicationContext.xml │ │ │ ├── spring-mvc.xml │ │ │ ├── spring-redis.xml │ │ │ └── spring-sa-token.xml │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── jsp/ │ │ │ │ ├── admin.jsp │ │ │ │ ├── home.jsp │ │ │ │ └── user.jsp │ │ │ └── web.xml │ │ └── index.jsp │ ├── sa-token-demo-sso/ │ │ ├── sa-token-demo-sso-client-h5/ │ │ │ ├── index.html │ │ │ ├── sso-common.js │ │ │ └── sso-login.html │ │ ├── sa-token-demo-sso-client-vue2/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── babel.config.js │ │ │ ├── jsconfig.json │ │ │ ├── package.json │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ ├── src/ │ │ │ │ ├── App.vue │ │ │ │ ├── main.js │ │ │ │ ├── router/ │ │ │ │ │ └── index.js │ │ │ │ └── views/ │ │ │ │ ├── sso-common.js │ │ │ │ ├── sso-index.vue │ │ │ │ └── sso-login.vue │ │ │ └── vue.config.js │ │ ├── sa-token-demo-sso-client-vue3/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.vue │ │ │ │ ├── main.js │ │ │ │ ├── router/ │ │ │ │ │ └── index.js │ │ │ │ └── views/ │ │ │ │ ├── sso-common.js │ │ │ │ ├── sso-index.vue │ │ │ │ └── sso-login.vue │ │ │ └── vite.config.js │ │ ├── sa-token-demo-sso-server/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaSsoServerApplication.java │ │ │ │ ├── h5/ │ │ │ │ │ ├── H5Controller.java │ │ │ │ │ └── SaTokenConfigure.java │ │ │ │ └── sso/ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ ├── HomeController.java │ │ │ │ └── SsoServerController.java │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ ├── static/ │ │ │ │ └── sa-res/ │ │ │ │ ├── layer/ │ │ │ │ │ ├── layer.js │ │ │ │ │ ├── mobile/ │ │ │ │ │ │ ├── layer.js │ │ │ │ │ │ └── need/ │ │ │ │ │ │ └── layer.css │ │ │ │ │ └── theme/ │ │ │ │ │ └── default/ │ │ │ │ │ └── layer.css │ │ │ │ ├── login.css │ │ │ │ └── login.js │ │ │ └── templates/ │ │ │ └── sa-login.html │ │ ├── sa-token-demo-sso-server-h5/ │ │ │ ├── common.js │ │ │ ├── home.html │ │ │ ├── sso-auth.css │ │ │ ├── sso-auth.html │ │ │ └── sso-auth.js │ │ ├── sa-token-demo-sso1-client/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaSso1ClientApplication.java │ │ │ │ └── sso/ │ │ │ │ └── SsoClientController.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── sa-token-demo-sso2-client/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaSso2ClientApplication.java │ │ │ │ ├── h5/ │ │ │ │ │ ├── H5Controller.java │ │ │ │ │ └── SaTokenConfigure.java │ │ │ │ └── sso/ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ └── SsoClientController.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── sa-token-demo-sso3-client/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaSso3ClientApplication.java │ │ │ │ ├── h5/ │ │ │ │ │ ├── H5Controller.java │ │ │ │ │ └── SaTokenConfigure.java │ │ │ │ └── sso/ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ └── SsoClientController.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── sa-token-demo-sso3-client-anon/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaSso3ClientAnonApplication.java │ │ │ │ └── sso/ │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ └── SsoClientController.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ ├── sa-token-demo-sso3-client-nosdk/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaSsoClientNoSdkApplication.java │ │ │ │ └── sso/ │ │ │ │ ├── SsoClientController.java │ │ │ │ ├── SsoRequestUtil.java │ │ │ │ └── util/ │ │ │ │ ├── AjaxJson.java │ │ │ │ └── MyHttpSessionHolder.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ └── sa-token-demo-sso3-client-resdk/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaSsoClientReSdkApplication.java │ │ │ ├── resdk/ │ │ │ │ ├── MyHttpSessionHolder.java │ │ │ │ └── StpLogicForHttpSession.java │ │ │ └── sso/ │ │ │ ├── GlobalExceptionHandler.java │ │ │ └── SsoClientController.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-sso-for-solon/ │ │ ├── sa-token-demo-sso-server-solon/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaConfig.java │ │ │ │ ├── SaSsoServerApp.java │ │ │ │ ├── h5/ │ │ │ │ │ ├── H5Controller.java │ │ │ │ │ └── SaTokenConfigure.java │ │ │ │ └── sso/ │ │ │ │ ├── GlobalExceptionFilter.java │ │ │ │ ├── HomeController.java │ │ │ │ └── SsoServerController.java │ │ │ └── resources/ │ │ │ ├── WEB-INF/ │ │ │ │ ├── static/ │ │ │ │ │ └── sa-res/ │ │ │ │ │ ├── layer/ │ │ │ │ │ │ ├── layer.js │ │ │ │ │ │ ├── mobile/ │ │ │ │ │ │ │ ├── layer.js │ │ │ │ │ │ │ └── need/ │ │ │ │ │ │ │ └── layer.css │ │ │ │ │ │ └── theme/ │ │ │ │ │ │ └── default/ │ │ │ │ │ │ └── layer.css │ │ │ │ │ ├── login.css │ │ │ │ │ └── login.js │ │ │ │ └── view/ │ │ │ │ └── sa-login.html │ │ │ └── app.yml │ │ ├── sa-token-demo-sso1-client-solon/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaConfig.java │ │ │ │ ├── SaSso1ClientApp.java │ │ │ │ └── sso/ │ │ │ │ └── SsoClientController.java │ │ │ └── resources/ │ │ │ └── app.yml │ │ ├── sa-token-demo-sso2-client-solon/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── pj/ │ │ │ │ ├── SaConfig.java │ │ │ │ ├── SaSso2ClientApp.java │ │ │ │ ├── h5/ │ │ │ │ │ ├── H5Controller.java │ │ │ │ │ └── SaTokenConfigure.java │ │ │ │ └── sso/ │ │ │ │ ├── GlobalExceptionFilter.java │ │ │ │ └── SsoClientController.java │ │ │ └── resources/ │ │ │ └── app.yml │ │ └── sa-token-demo-sso3-client-solon/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaConfig.java │ │ │ ├── SaSso3ClientApp.java │ │ │ ├── h5/ │ │ │ │ ├── H5Controller.java │ │ │ │ └── SaTokenConfigure.java │ │ │ └── sso/ │ │ │ ├── GlobalExceptionFilter.java │ │ │ └── SsoClientController.java │ │ └── resources/ │ │ └── app.yml │ ├── sa-token-demo-test/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenApplication.java │ │ │ ├── current/ │ │ │ │ ├── GlobalException.java │ │ │ │ └── NotFoundHandle.java │ │ │ ├── model/ │ │ │ │ ├── SysRole.java │ │ │ │ └── SysUser.java │ │ │ ├── satoken/ │ │ │ │ ├── SaLogForSlf4j.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ ├── StpInterfaceImpl.java │ │ │ │ └── StpUserUtil.java │ │ │ ├── test/ │ │ │ │ ├── AtController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── StressTestController.java │ │ │ │ ├── Test2Controller.java │ │ │ │ └── TestController.java │ │ │ └── util/ │ │ │ ├── AjaxJson.java │ │ │ └── Ttime.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-thymeleaf/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenThymeleafDemoApplication.java │ │ │ ├── satoken/ │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── GlobalException.java │ │ │ └── TestController.java │ │ └── resources/ │ │ ├── application.yml │ │ └── templates/ │ │ └── index.html │ ├── sa-token-demo-webflux/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenWebfluxApplication.java │ │ │ ├── satoken/ │ │ │ │ ├── MyFilter.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── DefineRoutes.java │ │ │ ├── GlobalException.java │ │ │ ├── TestController.java │ │ │ └── UserService.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-webflux-springboot3/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenWebfluxSpringboot3Application.java │ │ │ ├── satoken/ │ │ │ │ ├── MyFilter.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── DefineRoutes.java │ │ │ ├── GlobalException.java │ │ │ ├── TestController.java │ │ │ └── UserService.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-webflux-springboot4/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenWebfluxSpringboot4Application.java │ │ │ ├── satoken/ │ │ │ │ ├── MyFilter.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ └── test/ │ │ │ ├── DefineRoutes.java │ │ │ ├── GlobalException.java │ │ │ ├── TestController.java │ │ │ └── UserService.java │ │ └── resources/ │ │ └── application.yml │ ├── sa-token-demo-websocket/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── pj/ │ │ │ ├── SaTokenWebSocketApplication.java │ │ │ ├── test/ │ │ │ │ └── LoginController.java │ │ │ └── ws/ │ │ │ ├── WebSocketConfig.java │ │ │ └── WebSocketConnect.java │ │ └── resources/ │ │ └── application.yml │ └── sa-token-demo-websocket-spring/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── com/ │ │ └── pj/ │ │ ├── SaTokenWebSocketSpringApplication.java │ │ ├── test/ │ │ │ └── LoginController.java │ │ └── ws/ │ │ ├── MyWebSocketHandler.java │ │ ├── WebSocketConfig.java │ │ └── WebSocketInterceptor.java │ └── resources/ │ └── application.yml ├── sa-token-dependencies/ │ └── pom.xml ├── sa-token-doc/ │ ├── README.md │ ├── _sidebar.md │ ├── api/ │ │ ├── sa-session.md │ │ ├── sa-strategy.md │ │ ├── sa-token-dao.md │ │ └── stp-util.md │ ├── arch/ │ │ ├── data-structure.md │ │ └── dir-intro.md │ ├── doc/ │ │ ├── index-backup.html │ │ └── index.html │ ├── doc.html │ ├── fun/ │ │ ├── async--mock.md │ │ ├── auth-flow.md │ │ ├── auth-framework-function-test.md │ │ ├── cors-filter.md │ │ ├── curr-domain.md │ │ ├── custom-annotations.md │ │ ├── dynamic-router-check.md │ │ ├── exception-code.md │ │ ├── firewall.md │ │ ├── git-pr.md │ │ ├── issue-template.md │ │ ├── jur-cache.md │ │ ├── log.md │ │ ├── not-login-scene.md │ │ ├── plugin-dev.md │ │ ├── refer-info.md │ │ ├── sa-token-context--backup.md │ │ ├── sa-token-context.md │ │ ├── sa-token-test.md │ │ ├── session-model.md │ │ ├── sso-vs-oauth2.md │ │ ├── team.md │ │ ├── tech-stack.md │ │ ├── three-scope.md │ │ ├── timeline.md │ │ ├── token-info.md │ │ └── token-timeout.md │ ├── include/ │ │ └── include-qa.md │ ├── index.html │ ├── micro/ │ │ ├── dcs-session.md │ │ ├── gateway-auth.md │ │ ├── import-intro.md │ │ └── same-token.md │ ├── more/ │ │ ├── blog.md │ │ ├── common-action.md │ │ ├── common-questions.md │ │ ├── content-cooperation.md │ │ ├── demand-commit.md │ │ ├── join-group.md │ │ ├── link.md │ │ ├── noun-intro.md │ │ ├── sa-token-donate-old.md │ │ ├── sa-token-donate.md │ │ ├── tj-gzh-hz.md │ │ ├── tj-gzh.md │ │ ├── update-log.md │ │ └── wenjuan.md │ ├── oauth2/ │ │ ├── oauth2-apidoc.md │ │ ├── oauth2-at-check.md │ │ ├── oauth2-check-domain.md │ │ ├── oauth2-custom-api.md │ │ ├── oauth2-custom-grant_type.md │ │ ├── oauth2-custom-login.md │ │ ├── oauth2-custom-scope.md │ │ ├── oauth2-data-loader.md │ │ ├── oauth2-dev.md │ │ ├── oauth2-h5.md │ │ ├── oauth2-interworking.md │ │ ├── oauth2-oidc.md │ │ ├── oauth2-openid.md │ │ ├── oauth2-questions.md │ │ ├── oauth2-scope-level.md │ │ ├── oauth2-server.md │ │ └── readme.md │ ├── plugin/ │ │ ├── alone-redis.md │ │ ├── aop-at.md │ │ ├── api-key.md │ │ ├── api-sign.md │ │ ├── custom-serializer.md │ │ ├── dao-extend.md │ │ ├── dubbo-extend.md │ │ ├── freemarker-extend.md │ │ ├── grpc-extend.md │ │ ├── json-extend.md │ │ ├── jwt-extend.md │ │ ├── plugin-dev.md │ │ ├── quick-login.md │ │ ├── spel-at.md │ │ ├── temp-token.md │ │ └── thymeleaf-extend.md │ ├── pro/ │ │ ├── st_doc_top.md │ │ ├── st_index_top.md │ │ ├── st_oauth2.md │ │ └── st_sso.md │ ├── sso/ │ │ ├── anon-client.md │ │ ├── message-push.md │ │ ├── readme.md │ │ ├── signout.md │ │ ├── sso-apidoc.md │ │ ├── sso-check-domain.md │ │ ├── sso-custom-api.md │ │ ├── sso-custom-login.md │ │ ├── sso-dev.md │ │ ├── sso-diff-key.md │ │ ├── sso-h5.md │ │ ├── sso-home-jump.md │ │ ├── sso-nosdk.md │ │ ├── sso-pro.md │ │ ├── sso-questions.md │ │ ├── sso-server.md │ │ ├── sso-type1.md │ │ ├── sso-type2.md │ │ ├── sso-type3.md │ │ └── user-data-sync.md │ ├── start/ │ │ ├── download.md │ │ ├── example.md │ │ ├── maven-pull.md │ │ ├── new-version.md │ │ ├── solon-example.md │ │ └── webflux-example.md │ ├── static/ │ │ ├── custom-docsify-plugins/ │ │ │ ├── doc-lock-by-gzh-plugin.js │ │ │ ├── doc-lock-plugin.css │ │ │ └── doc-lock-plugin.js │ │ ├── doc.css │ │ ├── docsify-plugin.js │ │ ├── docsify-plugins/ │ │ │ ├── docsify-betterembed-1.1.1.js │ │ │ ├── docsify-plugin-flexible-alerts.min-1.1.1.js │ │ │ ├── progress.update.js │ │ │ └── sub-nav-draw.js │ │ ├── donate/ │ │ │ ├── donate-fun.js │ │ │ └── donate-list.js │ │ ├── index.css │ │ ├── is-fill-in-wj-plugin.js │ │ ├── is-star-plugin.js │ │ ├── jquery.lazyload-1.9.3.js │ │ ├── layer-v3.1.1/ │ │ │ ├── layer.js │ │ │ ├── mobile/ │ │ │ │ ├── layer.js │ │ │ │ └── need/ │ │ │ │ └── layer.css │ │ │ └── theme/ │ │ │ └── default/ │ │ │ └── layer.css │ │ ├── page-com/ │ │ │ └── github-stars-vs/ │ │ │ ├── echarts.min-5.4.3.js │ │ │ └── github-stars-vs.html │ │ ├── swiper/ │ │ │ ├── index-swiper.css │ │ │ └── index-swiper.js │ │ ├── vue.css │ │ └── water-change-theme/ │ │ ├── water-change-theme.css │ │ └── water-change-theme.js │ ├── up/ │ │ ├── basic-auth.md │ │ ├── disable.md │ │ ├── global-filter.md │ │ ├── global-listener.md │ │ ├── integ-redis.md │ │ ├── integ-spring-mongod-1.md │ │ ├── integ-spring-mongod-2.md │ │ ├── login-parameter.md │ │ ├── many-account.md │ │ ├── mock-person.md │ │ ├── mutex-login.md │ │ ├── not-cookie.md │ │ ├── password-secure.md │ │ ├── remember-me.md │ │ ├── safe-auth.md │ │ ├── search-session.md │ │ ├── token-prefix.md │ │ └── token-style.md │ └── use/ │ ├── at-check.md │ ├── config.md │ ├── dao-extend.md │ ├── jur-auth.md │ ├── kick.md │ ├── login-auth.md │ ├── route-check.md │ └── session.md ├── sa-token-plugin/ │ ├── pom.xml │ ├── sa-token-alone-redis/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── dao/ │ │ │ └── alone/ │ │ │ └── SaAloneRedisInject.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-alone-redis-by-spring-boot4/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── dao/ │ │ │ └── alone/ │ │ │ └── SaAloneRedisInject.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ ├── sa-token-apikey/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── apikey/ │ │ │ │ ├── SaApiKeyManager.java │ │ │ │ ├── annotation/ │ │ │ │ │ ├── SaCheckApiKey.java │ │ │ │ │ └── handle/ │ │ │ │ │ └── SaCheckApiKeyHandler.java │ │ │ │ ├── config/ │ │ │ │ │ └── SaApiKeyConfig.java │ │ │ │ ├── error/ │ │ │ │ │ └── SaApiKeyErrorCode.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── ApiKeyException.java │ │ │ │ │ └── ApiKeyScopeException.java │ │ │ │ ├── loader/ │ │ │ │ │ ├── SaApiKeyDataLoader.java │ │ │ │ │ └── SaApiKeyDataLoaderDefaultImpl.java │ │ │ │ ├── model/ │ │ │ │ │ └── ApiKeyModel.java │ │ │ │ └── template/ │ │ │ │ ├── SaApiKeyTemplate.java │ │ │ │ └── SaApiKeyUtil.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForApiKey.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-caffeine/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── dao/ │ │ │ │ ├── SaMapPackageForCaffeine.java │ │ │ │ └── SaTokenDaoForCaffeine.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForCaffeine.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-dubbo/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── context/ │ │ │ └── dubbo/ │ │ │ ├── filter/ │ │ │ │ ├── SaTokenDubboConsumerFilter.java │ │ │ │ ├── SaTokenDubboContextFilter.java │ │ │ │ └── SaTokenDubboProviderFilter.java │ │ │ ├── model/ │ │ │ │ ├── SaRequestForDubbo.java │ │ │ │ ├── SaResponseForDubbo.java │ │ │ │ └── SaStorageForDubbo.java │ │ │ └── util/ │ │ │ └── SaTokenContextDubboUtil.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── dubbo/ │ │ └── org.apache.dubbo.rpc.Filter │ ├── sa-token-dubbo3/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── context/ │ │ │ └── dubbo3/ │ │ │ ├── filter/ │ │ │ │ ├── SaTokenDubbo3ConsumerFilter.java │ │ │ │ ├── SaTokenDubbo3ContextFilter.java │ │ │ │ └── SaTokenDubbo3ProviderFilter.java │ │ │ ├── model/ │ │ │ │ ├── SaRequestForDubbo3.java │ │ │ │ ├── SaResponseForDubbo3.java │ │ │ │ └── SaStorageForDubbo3.java │ │ │ └── util/ │ │ │ └── SaTokenContextDubbo3Util.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── dubbo/ │ │ └── org.apache.dubbo.rpc.Filter │ ├── sa-token-fastjson/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateForFastjson.java │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForFastjson.java │ │ │ └── session/ │ │ │ └── SaSessionForFastjsonCustomized.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-fastjson2/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateForFastjson2.java │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForFastjson2.java │ │ │ └── session/ │ │ │ └── SaSessionForFastjson2Customized.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-forest/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── http/ │ │ │ │ └── SaHttpTemplateForForest.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForForest.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-freemarker/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── freemarker/ │ │ └── dialect/ │ │ ├── SaTokenTemplateDirectiveModel.java │ │ └── SaTokenTemplateModel.java │ ├── sa-token-grpc/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── context/ │ │ │ └── grpc/ │ │ │ ├── constants/ │ │ │ │ └── GrpcContextConstants.java │ │ │ ├── context/ │ │ │ │ └── SaTokenGrpcContext.java │ │ │ ├── interceptor/ │ │ │ │ ├── SaTokenContextGrpcServerInterceptor.java │ │ │ │ ├── SaTokenGrpcClientInterceptor.java │ │ │ │ └── SaTokenGrpcServerInterceptor.java │ │ │ ├── model/ │ │ │ │ ├── SaRequestForGrpc.java │ │ │ │ ├── SaResponseForGrpc.java │ │ │ │ └── SaStorageForGrpc.java │ │ │ └── util/ │ │ │ └── SaTokenContextGrpcUtil.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-hutool-timed-cache/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── dao/ │ │ │ │ └── SaTokenDaoForHutoolTimedCache.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForHutoolCache.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-jackson/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateForJackson.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForJackson.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-jackson3/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateForJackson3.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForJackson3.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-jwt/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── jwt/ │ │ ├── SaJwtTemplate.java │ │ ├── SaJwtUtil.java │ │ ├── StpLogicJwtForMixin.java │ │ ├── StpLogicJwtForSimple.java │ │ ├── StpLogicJwtForStateless.java │ │ ├── error/ │ │ │ └── SaJwtErrorCode.java │ │ └── exception/ │ │ └── SaJwtException.java │ ├── sa-token-oauth2/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── oauth2/ │ │ │ │ ├── SaOAuth2Manager.java │ │ │ │ ├── annotation/ │ │ │ │ │ ├── SaCheckAccessToken.java │ │ │ │ │ ├── SaCheckClientIdSecret.java │ │ │ │ │ ├── SaCheckClientToken.java │ │ │ │ │ └── handler/ │ │ │ │ │ ├── SaCheckAccessTokenHandler.java │ │ │ │ │ ├── SaCheckClientIdSecretHandler.java │ │ │ │ │ └── SaCheckClientTokenHandler.java │ │ │ │ ├── config/ │ │ │ │ │ ├── SaOAuth2OidcConfig.java │ │ │ │ │ └── SaOAuth2ServerConfig.java │ │ │ │ ├── consts/ │ │ │ │ │ ├── GrantType.java │ │ │ │ │ └── SaOAuth2Consts.java │ │ │ │ ├── dao/ │ │ │ │ │ └── SaOAuth2Dao.java │ │ │ │ ├── data/ │ │ │ │ │ ├── convert/ │ │ │ │ │ │ ├── SaOAuth2DataConverter.java │ │ │ │ │ │ └── SaOAuth2DataConverterDefaultImpl.java │ │ │ │ │ ├── generate/ │ │ │ │ │ │ ├── SaOAuth2DataGenerate.java │ │ │ │ │ │ └── SaOAuth2DataGenerateDefaultImpl.java │ │ │ │ │ ├── loader/ │ │ │ │ │ │ ├── SaOAuth2DataLoader.java │ │ │ │ │ │ └── SaOAuth2DataLoaderDefaultImpl.java │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── AccessTokenModel.java │ │ │ │ │ │ ├── ClientTokenModel.java │ │ │ │ │ │ ├── CodeModel.java │ │ │ │ │ │ ├── RefreshTokenModel.java │ │ │ │ │ │ ├── loader/ │ │ │ │ │ │ │ └── SaClientModel.java │ │ │ │ │ │ ├── oidc/ │ │ │ │ │ │ │ └── IdTokenModel.java │ │ │ │ │ │ └── request/ │ │ │ │ │ │ ├── ClientIdAndSecretModel.java │ │ │ │ │ │ └── RequestAuthModel.java │ │ │ │ │ └── resolver/ │ │ │ │ │ ├── SaOAuth2DataResolver.java │ │ │ │ │ └── SaOAuth2DataResolverDefaultImpl.java │ │ │ │ ├── error/ │ │ │ │ │ └── SaOAuth2ErrorCode.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── SaOAuth2AccessTokenException.java │ │ │ │ │ ├── SaOAuth2AccessTokenScopeException.java │ │ │ │ │ ├── SaOAuth2AuthorizationCodeException.java │ │ │ │ │ ├── SaOAuth2ClientModelException.java │ │ │ │ │ ├── SaOAuth2ClientModelScopeException.java │ │ │ │ │ ├── SaOAuth2ClientTokenException.java │ │ │ │ │ ├── SaOAuth2ClientTokenScopeException.java │ │ │ │ │ ├── SaOAuth2Exception.java │ │ │ │ │ └── SaOAuth2RefreshTokenException.java │ │ │ │ ├── function/ │ │ │ │ │ ├── SaOAuth2ConfirmViewFunction.java │ │ │ │ │ ├── SaOAuth2DoLoginHandleFunction.java │ │ │ │ │ ├── SaOAuth2NotLoginViewFunction.java │ │ │ │ │ └── strategy/ │ │ │ │ │ ├── SaOAuth2CreateAccessTokenValueFunction.java │ │ │ │ │ ├── SaOAuth2CreateClientTokenValueFunction.java │ │ │ │ │ ├── SaOAuth2CreateCodeValueFunction.java │ │ │ │ │ ├── SaOAuth2CreateRefreshTokenValueFunction.java │ │ │ │ │ ├── SaOAuth2GrantTypeAuthFunction.java │ │ │ │ │ ├── SaOAuth2ScopeWorkAccessTokenFunction.java │ │ │ │ │ └── SaOAuth2ScopeWorkClientTokenFunction.java │ │ │ │ ├── granttype/ │ │ │ │ │ └── handler/ │ │ │ │ │ ├── AuthorizationCodeGrantTypeHandler.java │ │ │ │ │ ├── PasswordGrantTypeHandler.java │ │ │ │ │ ├── RefreshTokenGrantTypeHandler.java │ │ │ │ │ ├── SaOAuth2GrantTypeHandlerInterface.java │ │ │ │ │ └── model/ │ │ │ │ │ └── PasswordAuthResult.java │ │ │ │ ├── processor/ │ │ │ │ │ └── SaOAuth2ServerProcessor.java │ │ │ │ ├── scope/ │ │ │ │ │ ├── CommonScope.java │ │ │ │ │ └── handler/ │ │ │ │ │ ├── OidcScopeHandler.java │ │ │ │ │ ├── OpenIdScopeHandler.java │ │ │ │ │ ├── SaOAuth2ScopeHandlerInterface.java │ │ │ │ │ ├── UnionIdScopeHandler.java │ │ │ │ │ └── UserIdScopeHandler.java │ │ │ │ ├── strategy/ │ │ │ │ │ └── SaOAuth2Strategy.java │ │ │ │ └── template/ │ │ │ │ ├── SaOAuth2Template.java │ │ │ │ └── SaOAuth2Util.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForOAuth2.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-okhttps/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── http/ │ │ │ │ └── SaHttpTemplateForOkHttps.java │ │ │ └── plugin/ │ │ │ └── SaTokenPluginForOkHttps.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-quick-login/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── quick/ │ │ │ ├── SaQuickInject.java │ │ │ ├── SaQuickManager.java │ │ │ ├── SaQuickRegister.java │ │ │ ├── config/ │ │ │ │ └── SaQuickConfig.java │ │ │ ├── function/ │ │ │ │ └── DoLoginHandleFunction.java │ │ │ └── web/ │ │ │ └── SaQuickController.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ ├── spring/ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ │ ├── static/ │ │ │ └── sa-res/ │ │ │ ├── layer/ │ │ │ │ ├── layer.js │ │ │ │ ├── mobile/ │ │ │ │ │ ├── layer.js │ │ │ │ │ └── need/ │ │ │ │ │ └── layer.css │ │ │ │ └── theme/ │ │ │ │ └── default/ │ │ │ │ └── layer.css │ │ │ ├── login.css │ │ │ └── login.js │ │ └── templates/ │ │ └── sa-login.html │ ├── sa-token-redis-jackson/ │ │ └── pom.xml │ ├── sa-token-redis-template/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── dao/ │ │ │ └── SaTokenDaoForRedisTemplate.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-redis-template-jdk-serializer/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── dao/ │ │ │ ├── SaTokenDaoForRedisTemplate.java │ │ │ └── SaTokenDaoForRedisTemplateUseJdkSerializer.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-redisson/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── dao/ │ │ └── SaTokenDaoForRedisson.java │ ├── sa-token-redisson-spring-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── spring/ │ │ │ └── SaTokenDaoForRedissonBeanRegister.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-redisx/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── dao/ │ │ │ └── SaTokenDaoForRedisx.java │ │ └── test/ │ │ ├── java/ │ │ │ └── demo/ │ │ │ ├── App.java │ │ │ └── Config.java │ │ └── resources/ │ │ └── app.yml │ ├── sa-token-serializer-features/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForSerializerFeatures.java │ │ │ └── serializer/ │ │ │ ├── SaSerializerForBase64UseCustomCharacters.java │ │ │ ├── SaSerializerForBase64UseEmoji.java │ │ │ ├── SaSerializerForBase64UsePeriodicTable.java │ │ │ ├── SaSerializerForBase64UseSpecialSymbols.java │ │ │ └── SaSerializerForBase64UseTianGan.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-sign/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForSign.java │ │ │ └── sign/ │ │ │ ├── SaSignManager.java │ │ │ ├── annotation/ │ │ │ │ ├── SaCheckSign.java │ │ │ │ └── handle/ │ │ │ │ └── SaCheckSignHandler.java │ │ │ ├── config/ │ │ │ │ ├── SaSignConfig.java │ │ │ │ └── SaSignManyConfigWrapper.java │ │ │ ├── error/ │ │ │ │ └── SaSignErrorCode.java │ │ │ ├── exception/ │ │ │ │ └── SaSignException.java │ │ │ └── template/ │ │ │ ├── SaSignMany.java │ │ │ ├── SaSignTemplate.java │ │ │ └── SaSignUtil.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-snack3/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateForSnack3.java │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForSnack3.java │ │ │ └── session/ │ │ │ └── SaSessionForSnack3Customized.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-snack4/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateForSnack4.java │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForSnack4.java │ │ │ └── session/ │ │ │ └── SaSessionForSnack4Customized.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ ├── sa-token-spring-aop/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── aop/ │ │ │ ├── SaAopPointcutAdvisorBeanRegister.java │ │ │ ├── SaAroundAnnotationMethodInterceptor.java │ │ │ └── SaAroundAnnotationPointcutAdvisor.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-spring-el/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── annotation/ │ │ │ │ └── SaCheckEL.java │ │ │ └── aop/ │ │ │ ├── SaCheckELAspect.java │ │ │ └── SaCheckELRootMap.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ ├── spring/ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ │ └── spel-extension.json │ ├── sa-token-sso/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── sso/ │ │ ├── SaSsoManager.java │ │ ├── config/ │ │ │ ├── SaSsoClientConfig.java │ │ │ ├── SaSsoClientModel.java │ │ │ └── SaSsoServerConfig.java │ │ ├── error/ │ │ │ └── SaSsoErrorCode.java │ │ ├── exception/ │ │ │ └── SaSsoException.java │ │ ├── function/ │ │ │ ├── CheckTicketAppendDataFunction.java │ │ │ ├── DoLoginHandleFunction.java │ │ │ ├── NotLoginViewFunction.java │ │ │ ├── SaSsoMessageHandleFunction.java │ │ │ ├── SendRequestFunction.java │ │ │ └── TicketResultHandleFunction.java │ │ ├── message/ │ │ │ ├── SaSsoMessage.java │ │ │ ├── SaSsoMessageHolder.java │ │ │ └── handle/ │ │ │ ├── SaSsoMessageHandle.java │ │ │ ├── SaSsoMessageSimpleHandle.java │ │ │ ├── client/ │ │ │ │ └── SaSsoMessageLogoutCallHandle.java │ │ │ └── server/ │ │ │ ├── SaSsoMessageCheckTicketHandle.java │ │ │ └── SaSsoMessageSignoutHandle.java │ │ ├── model/ │ │ │ ├── SaCheckTicketResult.java │ │ │ ├── SaSsoClientInfo.java │ │ │ ├── SaSsoClientModel.java │ │ │ └── TicketModel.java │ │ ├── name/ │ │ │ ├── ApiName.java │ │ │ └── ParamName.java │ │ ├── processor/ │ │ │ ├── SaSsoClientProcessor.java │ │ │ ├── SaSsoProcessorHelper.java │ │ │ └── SaSsoServerProcessor.java │ │ ├── strategy/ │ │ │ ├── SaSsoClientStrategy.java │ │ │ └── SaSsoServerStrategy.java │ │ ├── template/ │ │ │ ├── SaSsoClientTemplate.java │ │ │ ├── SaSsoClientUtil.java │ │ │ ├── SaSsoServerTemplate.java │ │ │ ├── SaSsoServerUtil.java │ │ │ ├── SaSsoTemplate.java │ │ │ └── SaSsoUtil.java │ │ └── util/ │ │ └── SaSsoConsts.java │ ├── sa-token-temp-jwt/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── plugin/ │ │ │ │ └── SaTokenPluginForTempForJwt.java │ │ │ └── temp/ │ │ │ └── jwt/ │ │ │ ├── SaJwtUtil.java │ │ │ ├── SaTempTemplateForJwt.java │ │ │ └── error/ │ │ │ └── SaTempJwtErrorCode.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── satoken/ │ │ └── cn.dev33.satoken.plugin.SaTokenPlugin │ └── sa-token-thymeleaf/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── cn/ │ └── dev33/ │ └── satoken/ │ └── thymeleaf/ │ └── dialect/ │ ├── Sa-Token-Dialect.xml │ ├── SaTokenDialect.java │ └── SaTokenTagProcessor.java ├── sa-token-special-dependencies/ │ ├── pom.xml │ ├── sa-token-spring-boot2-dependencies/ │ │ └── pom.xml │ ├── sa-token-spring-boot3-dependencies/ │ │ └── pom.xml │ └── sa-token-spring-boot4-dependencies/ │ └── pom.xml ├── sa-token-starter/ │ ├── pom.xml │ ├── sa-token-jakarta-servlet/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── servlet/ │ │ ├── error/ │ │ │ └── SaServletErrorCode.java │ │ ├── model/ │ │ │ ├── SaRequestForServlet.java │ │ │ ├── SaResponseForServlet.java │ │ │ └── SaStorageForServlet.java │ │ ├── package-info.java │ │ └── util/ │ │ ├── SaJakartaServletOperateUtil.java │ │ └── SaTokenContextJakartaServletUtil.java │ ├── sa-token-jboot-plugin/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── jboot/ │ │ │ ├── PathAnalyzer.java │ │ │ ├── SaAnnotationInterceptor.java │ │ │ ├── SaJdkSerializer.java │ │ │ ├── SaRedisCache.java │ │ │ ├── SaTokenCacheDao.java │ │ │ ├── SaTokenContextForJboot.java │ │ │ └── SaTokenPathFilter.java │ │ └── test/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── jboot/ │ │ │ └── test/ │ │ │ ├── AppRun.java │ │ │ ├── AtteStartListener.java │ │ │ └── StpInterfaceImpl.java │ │ └── resources/ │ │ └── jboot.properties │ ├── sa-token-jfinal-plugin/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── jfinal/ │ │ │ ├── PathAnalyzer.java │ │ │ ├── SaAnnotationInterceptor.java │ │ │ ├── SaControllerContext.java │ │ │ ├── SaJdkSerializer.java │ │ │ ├── SaTokenActionHandler.java │ │ │ ├── SaTokenContextForJfinal.java │ │ │ ├── SaTokenDaoRedis.java │ │ │ └── SaTokenPathFilter.java │ │ └── test/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── jfinal/ │ │ └── test/ │ │ ├── AppRun.java │ │ ├── Config.java │ │ └── StpInterfaceImpl.java │ ├── sa-token-loveqq-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── loveqq/ │ │ │ └── boot/ │ │ │ ├── SaBeanInject.java │ │ │ ├── SaBeanRegister.java │ │ │ ├── apiKey/ │ │ │ │ ├── SaApiKeyBeanInject.java │ │ │ │ └── SaApiKeyBeanRegister.java │ │ │ ├── context/ │ │ │ │ ├── SaReactorHolder.java │ │ │ │ └── path/ │ │ │ │ └── ApplicationContextPathLoading.java │ │ │ ├── filter/ │ │ │ │ ├── SaFirewallCheckFilter.java │ │ │ │ ├── SaRequestFilter.java │ │ │ │ ├── SaTokenContextFilter.java │ │ │ │ └── SaTokenCorsFilter.java │ │ │ ├── interceptor/ │ │ │ │ └── SaInterceptor.java │ │ │ ├── model/ │ │ │ │ ├── LoveqqSaRequest.java │ │ │ │ ├── LoveqqSaResponse.java │ │ │ │ └── LoveqqSaStorage.java │ │ │ ├── oauth2/ │ │ │ │ ├── SaOAuth2BeanInject.java │ │ │ │ └── SaOAuth2BeanRegister.java │ │ │ ├── package-info.java │ │ │ ├── sign/ │ │ │ │ ├── SaSignBeanInject.java │ │ │ │ └── SaSignBeanRegister.java │ │ │ ├── sso/ │ │ │ │ ├── SaSsoBeanInject.java │ │ │ │ └── SaSsoBeanRegister.java │ │ │ ├── support/ │ │ │ │ └── SaPathMatcherHolder.java │ │ │ └── utils/ │ │ │ ├── SaTokenContextUtil.java │ │ │ └── SaTokenOperateUtil.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── k.factories │ ├── sa-token-reactor-spring-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── reactor/ │ │ │ ├── package-info.java │ │ │ └── spring/ │ │ │ └── SpringBootVersionCompatibilityChecker.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-reactor-spring-boot3-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── reactor/ │ │ │ └── Placeholder.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ ├── sa-token-reactor-spring-boot4-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ └── reactor/ │ │ │ └── Placeholder.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ ├── sa-token-servlet/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── servlet/ │ │ ├── error/ │ │ │ └── SaServletErrorCode.java │ │ ├── model/ │ │ │ ├── SaRequestForServlet.java │ │ │ ├── SaResponseForServlet.java │ │ │ └── SaStorageForServlet.java │ │ ├── package-info.java │ │ └── util/ │ │ ├── SaServletOperateUtil.java │ │ └── SaTokenContextServletUtil.java │ ├── sa-token-solon-plugin/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── cn/ │ │ │ │ └── dev33/ │ │ │ │ └── satoken/ │ │ │ │ └── solon/ │ │ │ │ ├── SaBeanInject.java │ │ │ │ ├── SaBeanRegister.java │ │ │ │ ├── SaSolonPlugin.java │ │ │ │ ├── apikey/ │ │ │ │ │ ├── SaApiKeyBeanInject.java │ │ │ │ │ └── SaApiKeyBeanRegister.java │ │ │ │ ├── error/ │ │ │ │ │ └── SaSolonErrorCode.java │ │ │ │ ├── integration/ │ │ │ │ │ ├── SaFirewallCheckFilterForSolon.java │ │ │ │ │ ├── SaTokenContextFilterForSolon.java │ │ │ │ │ ├── SaTokenCorsFilterForSolon.java │ │ │ │ │ ├── SaTokenFilter.java │ │ │ │ │ └── SaTokenInterceptor.java │ │ │ │ ├── model/ │ │ │ │ │ ├── SaContextForSolon.java │ │ │ │ │ ├── SaRequestForSolon.java │ │ │ │ │ ├── SaResponseForSolon.java │ │ │ │ │ └── SaStorageForSolon.java │ │ │ │ ├── oauth2/ │ │ │ │ │ ├── SaOAuth2BeanInject.java │ │ │ │ │ └── SaOAuth2BeanRegister.java │ │ │ │ ├── package-info.java │ │ │ │ ├── sign/ │ │ │ │ │ ├── SaSignBeanInject.java │ │ │ │ │ └── SaSignBeanRegister.java │ │ │ │ ├── sso/ │ │ │ │ │ ├── SaSsoBeanInject.java │ │ │ │ │ └── SaSsoBeanRegister.java │ │ │ │ └── util/ │ │ │ │ ├── SaSolonOperateUtil.java │ │ │ │ └── SaTokenContextSolonUtil.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── solon/ │ │ │ └── cn.dev33.satoken.solon.properties │ │ └── test/ │ │ ├── java/ │ │ │ ├── demo/ │ │ │ │ ├── App.java │ │ │ │ └── Config.java │ │ │ └── demo2/ │ │ │ ├── App.java │ │ │ └── Config.java │ │ └── resources/ │ │ └── app.yml │ ├── sa-token-spring-boot-reactor-v2v3v4-common/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── reactor/ │ │ ├── context/ │ │ │ ├── SaReactorHolder.java │ │ │ └── SaReactorSyncHolder.java │ │ ├── filter/ │ │ │ ├── SaFirewallCheckFilterForReactor.java │ │ │ ├── SaReactorFilter.java │ │ │ ├── SaTokenContextFilterForReactor.java │ │ │ └── SaTokenCorsFilterForReactor.java │ │ ├── model/ │ │ │ ├── SaRequestForReactor.java │ │ │ ├── SaResponseForReactor.java │ │ │ └── SaStorageForReactor.java │ │ ├── package-info.java │ │ ├── spring/ │ │ │ ├── SaTokenContextForSpringReactor.java │ │ │ └── SaTokenContextRegister.java │ │ └── util/ │ │ └── SaReactorOperateUtil.java │ ├── sa-token-spring-boot-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── filter/ │ │ │ │ ├── SaFirewallCheckFilterForServlet.java │ │ │ │ ├── SaServletFilter.java │ │ │ │ ├── SaTokenContextFilterForServlet.java │ │ │ │ └── SaTokenCorsFilterForServlet.java │ │ │ ├── interceptor/ │ │ │ │ └── SaInterceptor.java │ │ │ ├── package-info.java │ │ │ └── spring/ │ │ │ ├── SaTokenContextForSpring.java │ │ │ ├── SaTokenContextRegister.java │ │ │ ├── SpringBootVersionCompatibilityChecker.java │ │ │ └── SpringMVCUtil.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-spring-boot-webmvc-reactor-v2v3v4-common/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── package-info.java │ │ │ └── spring/ │ │ │ ├── SaBeanInject.java │ │ │ ├── SaBeanRegister.java │ │ │ ├── apikey/ │ │ │ │ ├── SaApiKeyBeanInject.java │ │ │ │ ├── SaApiKeyBeanRegister.java │ │ │ │ └── package-info.java │ │ │ ├── context/ │ │ │ │ └── path/ │ │ │ │ └── ApplicationContextPathLoading.java │ │ │ ├── oauth2/ │ │ │ │ ├── SaOAuth2BeanInject.java │ │ │ │ ├── SaOAuth2BeanRegister.java │ │ │ │ └── package-info.java │ │ │ ├── pathmatch/ │ │ │ │ ├── SaPathMatcherHolder.java │ │ │ │ ├── SaPathPatternParserUtil.java │ │ │ │ └── SaPatternsRequestConditionHolder.java │ │ │ ├── sign/ │ │ │ │ ├── SaSignBeanInject.java │ │ │ │ ├── SaSignBeanRegister.java │ │ │ │ └── package-info.java │ │ │ └── sso/ │ │ │ ├── SaSsoBeanInject.java │ │ │ ├── SaSsoBeanRegister.java │ │ │ └── package-info.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ ├── sa-token-spring-boot-webmvc-v3v4-common/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── cn/ │ │ │ └── dev33/ │ │ │ └── satoken/ │ │ │ ├── filter/ │ │ │ │ ├── SaFirewallCheckFilterForJakartaServlet.java │ │ │ │ ├── SaServletFilter.java │ │ │ │ ├── SaTokenContextFilterForJakartaServlet.java │ │ │ │ └── SaTokenCorsFilterForJakartaServlet.java │ │ │ ├── interceptor/ │ │ │ │ └── SaInterceptor.java │ │ │ ├── package-info.java │ │ │ └── spring/ │ │ │ ├── SaTokenContextForSpringInJakartaServlet.java │ │ │ ├── SaTokenContextRegister.java │ │ │ └── SpringMVCUtil.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ ├── sa-token-spring-boot3-starter/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ └── Placeholder.java │ └── sa-token-spring-boot4-starter/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── cn/ │ └── dev33/ │ └── satoken/ │ └── Placeholder.java └── sa-token-test/ ├── pom.xml ├── sa-token-easy-test/ │ ├── pom.xml │ └── src/ │ └── test/ │ └── java/ │ └── com/ │ └── pj/ │ └── test/ │ ├── SaJsonTemplateTest.java │ └── model/ │ ├── SysRole.java │ └── SysUser.java ├── sa-token-jackson3-test/ │ ├── pom.xml │ └── src/ │ └── test/ │ └── java/ │ └── com/ │ └── pj/ │ └── test/ │ ├── SaJsonTemplateForJackson3Test.java │ └── model/ │ ├── SysRole.java │ └── SysUser.java ├── sa-token-json-test/ │ ├── pom.xml │ └── src/ │ └── test/ │ └── java/ │ └── com/ │ └── pj/ │ └── test/ │ ├── SaJsonTemplateTest.java │ └── model/ │ ├── SysRole.java │ └── SysUser.java ├── sa-token-jwt-test/ │ ├── pom.xml │ └── src/ │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── pj/ │ │ └── test/ │ │ ├── JwtForMixinTest.java │ │ ├── JwtForSimpleTest.java │ │ ├── JwtForStatelessTest.java │ │ ├── StartUpApplication.java │ │ └── satoken/ │ │ └── StpInterfaceImpl.java │ └── resources/ │ └── application.yml ├── sa-token-serializer-test/ │ ├── pom.xml │ └── src/ │ └── test/ │ └── java/ │ └── com/ │ └── pj/ │ └── test/ │ ├── SaSerializerTemplateTest.java │ └── model/ │ ├── SysRole.java │ └── SysUser.java ├── sa-token-springboot-test/ │ ├── pom.xml │ └── src/ │ └── test/ │ ├── java/ │ │ └── cn/ │ │ └── dev33/ │ │ └── satoken/ │ │ ├── core/ │ │ │ ├── application/ │ │ │ │ └── SaApplicationTest.java │ │ │ ├── config/ │ │ │ │ └── SaTokenConfigTest.java │ │ │ ├── context/ │ │ │ │ └── model/ │ │ │ │ ├── SaCookieTest.java │ │ │ │ └── SaTokenContextDefaultImplTest.java │ │ │ ├── dao/ │ │ │ │ └── SaTokenDaoTest.java │ │ │ ├── fun/ │ │ │ │ └── IsRunFunctionTest.java │ │ │ ├── json/ │ │ │ │ └── SaJsonTemplateDefaultImplTest.java │ │ │ ├── package-info.java │ │ │ ├── secure/ │ │ │ │ ├── BCryptTest.java │ │ │ │ ├── SaBase64UtilTest.java │ │ │ │ └── SaSecureUtilTest.java │ │ │ ├── session/ │ │ │ │ ├── SaSessionCustomUtilTest.java │ │ │ │ ├── SaSessionTest.java │ │ │ │ └── SaTerminalInfoTest.java │ │ │ ├── sign/ │ │ │ │ └── SaSignTemplateTest.java │ │ │ ├── stp/ │ │ │ │ └── TokenInfoTest.java │ │ │ ├── temp/ │ │ │ │ └── SaTempTokenTest.java │ │ │ └── util/ │ │ │ ├── SaFoxUtilTest.java │ │ │ └── SaResultTest.java │ │ ├── integrate/ │ │ │ ├── StartUpApplication.java │ │ │ ├── annotation/ │ │ │ │ ├── SaAnnotationController.java │ │ │ │ ├── SaAnnotationControllerTest.java │ │ │ │ └── SaAnnotationIgnoreController.java │ │ │ ├── configure/ │ │ │ │ ├── HandlerException.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ ├── StpInterfaceImpl.java │ │ │ │ └── inject/ │ │ │ │ ├── MySaBasicTemplate.java │ │ │ │ ├── MySaOAuth2Template.java │ │ │ │ ├── MySaSameTemplate.java │ │ │ │ ├── MySaSignTemplate.java │ │ │ │ ├── MySaSsoTemplate.java │ │ │ │ ├── MySaTempTemplate.java │ │ │ │ ├── MySaTokenDao.java │ │ │ │ ├── MySaTokenListener.java │ │ │ │ └── MyStpLogic.java │ │ │ ├── login/ │ │ │ │ ├── LoginController.java │ │ │ │ └── LoginControllerTest.java │ │ │ ├── more/ │ │ │ │ ├── MoreController.java │ │ │ │ └── MoreControllerTest.java │ │ │ ├── router/ │ │ │ │ ├── RouterController.java │ │ │ │ ├── RouterControllerTest.java │ │ │ │ └── SaTokenConfigure2.java │ │ │ └── same/ │ │ │ ├── SaSameTokenController.java │ │ │ └── SaSameTokenControllerTest.java │ │ ├── springboot/ │ │ │ ├── BasicsTest.java │ │ │ ├── ManyLoginTest.java │ │ │ ├── SaPathMatcherTest.java │ │ │ ├── SpringMVCUtilTest.java │ │ │ ├── StartUpApplication.java │ │ │ └── satoken/ │ │ │ └── StpInterfaceImpl.java │ │ └── util/ │ │ └── SoMap.java │ └── resources/ │ ├── application.yml │ └── sa-token2.properties └── sa-token-temp-jwt-test/ ├── pom.xml └── src/ └── test/ ├── java/ │ └── com/ │ └── pj/ │ └── test/ │ ├── SaTempTemplateForJwtTest.java │ └── StartUpApplication.java └── resources/ └── application.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .agents/skills/README.md ================================================ # Agent Skills 本目录存放用于辅助 Sa-Token 项目开发的 Agent Skills。这些 skills 封装了项目特定的知识和操作规范,可在对话中直接调用。 ## Skill 列表 | Skill 名称 | 功能描述 | 使用场景 | 入口文件 | |-----------|---------|---------|---------| | `commit-message` | 根据 git 变更生成符合 Sa-Token 项目风格的 commit message | 生成提交信息、写 commit message | [SKILL.md](commit-message/SKILL.md) | | `organize-update-log` | 根据 git 提交记录生成符合项目规范的更新日志内容 | 整理更新日志、分析版本变更 | [SKILL.md](organize-update-log/SKILL.md) | | `remove-redundancy-import` | 检查并移除 Java 类中未被引用的冗余 import | 清理冗余导包、优化 import | [SKILL.md](remove-redundancy-import/SKILL.md) | | `upgrade-version` | 将项目版本号从旧版本升级到新版本,批量修改 pom、常量、Demo 及文档 | 升级版本、修改版本号、version bump | [SKILL.md](upgrade-version/SKILL.md) | ### 详细说明 #### commit-message 根据当前 git 变更(staged 或 unstaged),生成符合 [Conventional Commits](https://www.conventionalcommits.org/) 格式、以中文为主的 commit message。支持识别新增文件、修复 bug、重构等多种变更类型。 #### organize-update-log 根据 git 提交记录,生成符合 `sa-token-doc/more/update-log.md` 格式的更新日志内容。自动分类到插件、starter、重构、Solon、示例、文档等版块。 #### remove-redundancy-import 扫描项目中所有 Java 类,检测未被引用的冗余 import,生成清理计划供审阅,确认后执行移除。支持通过内置 Python 脚本快速扫描。 #### upgrade-version 将 Sa-Token 项目版本号从旧版本升级到新版本。批量修改根 POM、BOM、SaTokenConsts、所有 Demo 子项目 pom.xml 及文档(README、index.html、doc.html、new-version.md 等)中的版本引用。明确排除历史记录、@since 标注、更新日志等不应修改的文件。 ## 快速使用 在 AI 对话中,直接描述你的需求即可自动触发相应 skill: ``` 用户:帮我生成 commit message → 自动使用 commit-message skill 分析 git 变更并生成提交信息 用户:整理一下更新日志 → 自动使用 organize-update-log skill 生成更新日志 用户:清理一下冗余 import → 自动使用 remove-redundancy-import skill 扫描并清理未使用的 import 用户:把版本从 v1.44.0 升级到 v1.45.0 → 自动使用 upgrade-version skill 批量修改版本号 ``` ## 新增 Skill 维护指南 当新增 skill 时,请同步更新本 README 文件,保持 skill 列表的完整性。 ### Skill 目录结构规范 每个 skill 应创建独立的子目录,结构如下: ``` .agents/skills/ ├── README.md # 本文件 └── skill-name/ # skill 目录(小写,短横线分隔) ├── SKILL.md # skill 主文件(必须包含 YAML 元数据和使用说明) ├── examples.md # 使用示例(可选) ├── reference.md # 参考文档(可选) └── scan_redundant_imports.py # 辅助脚本(如需要) ``` ### SKILL.md 文件格式 每个 `SKILL.md` 必须包含 YAML Front Matter: ```yaml --- name: skill-name description: 简要描述 skill 的功能和使用场景 --- ``` ### 更新 README 清单 新增 skill 后,请在本文件中: 1. 在 **Skill 列表** 表格中添加新行 2. 在 **详细说明** 小节添加对应的描述段落 3. (可选)在 **快速使用** 中添加使用示例 ## 注意事项 - 所有 skill 遵循 Sa-Token 项目特定的规范和风格 - 部分 skill(如 `remove-redundancy-import`)在执行前需要用户确认 - 可参考每个 skill 目录下的 `examples.md` 或 `reference.md` 获取更多使用帮助 ================================================ FILE: .agents/skills/commit-message/SKILL.md ================================================ --- name: commit-message description: 根据 git 变更生成符合 Sa-Token 项目风格的 commit message。遵循 Conventional Commits 格式,以中文为主。当用户要求生成提交信息、写 commit message、或根据变更生成提交说明时使用。 --- # 生成 Commit Message 根据当前 git 变更(staged 或 unstaged),生成符合 Sa-Token 项目规范的 commit message。 ## 使用时机 - 用户要求生成 commit message - 用户要求根据变更写提交说明 - 用户说「帮我写个 commit」「生成提交信息」等 ## 工作流程 ### 第一步:获取变更内容 ```bash git status git diff --staged git diff ``` **必须包含的变更范围**: - **staged 变更**:`git diff --staged` - **unstaged 变更**:若无 staged,则用 `git diff` 查看工作区修改 - **未跟踪文件**:`git status` 中的 Untracked files 也要纳入分析,生成 commit message 时需一并考虑 若存在未跟踪的新增文件(如新 skill、新配置等),应在 message 中体现,或给出「包含全部变更」与「仅已修改文件」两种方案供用户选择。 ### 第二步:分析变更类型 根据变更内容选择 type 前缀: | type | 适用场景 | |------|----------| | feat | 新增功能、新模块、新插件 | | fix | 修复 bug、修正错误 | | refactor | 重构、优化结构、重命名、移除冗余 | | perf | 性能优化(与 refactor 区分:侧重性能) | | docs | 文档更新、README、错别字、同步链接 | | style | 代码格式调整(缩进、空格等,不影响逻辑) | | chore | 构建配置、.gitignore、注释修复、依赖更新 | | test | 单元测试、测试用例 | | demo | 示例项目、demo 相关 | | memo | 备忘录、内部记录 | | revert | 回滚某次提交 | | AI | AI 创建的 skill、规则等 | ### 第三步:撰写描述 **基础格式**:`type: 简短描述` 或 `type(scope): 简短描述` **scope 可选**:涉及特定模块时使用,如 `feat(sign)`、`fix(oauth2)`、`refactor(dependencies)`。 **规范**: - **50 字规则**:subject 不超过 50 字符,保证在 git log 中完整显示 - **命令式语气**:用「修复」「新增」「优化」,不用「修复了」「新增了」 - **说明「做了什么」**:清晰表达变更内容,必要时说明「为什么」 - **以中文为主**:技术术语可保留英文(如 `StrFormatter`、`sa-token-jackson3`) - **动词开头**:新增、修复、优化、重构、移除、同步、订正 等 **可选 Body/Footer**(重要变更时): - Body:详细说明变更背景、动机,每行不超过 72 字符 - Footer:关联 Issue,如 `Fixes #123` 或 `merge: [pr N](url)` ### 第四步:输出 直接输出可复制的 commit message。若有多条合理方案,可给出 1~2 个备选。 ## 格式示例 **简单提交**(常用): ``` feat: 添加 sa-token-jackson3 插件 fix(sign): 修复签名校验在空参数时的空指针 ``` **带 scope**: ``` refactor(dependencies): 重构模块依赖层级 perf(oauth2): 优化 Client 信息读取算法 ``` **带 body**(复杂变更): ``` feat: 新增重复登录处理策略 当同一账号不允许多客户端同时登录时,支持选择踢人下线或拦截本次登录。 ``` ## 参考资源 - 示例:详见 [examples.md](examples.md) - 规范详解:详见 [reference.md](reference.md) ## 快速对照 | 变更内容 | 示例输出 | |----------|----------| | 新增插件 | `feat: 添加 sa-token-jackson3 插件` | | 修复 bug | `fix: 修复 StpUtil.getLoginIdByTokenNotThinkFreeze 方法缺少 static 的问题` | | 性能优化 | `perf: 优化 StrFormatter 常量封装` | | 重构模块 | `refactor: 重构模块依赖层级` | | 移除冗余 | `refactor: 移除冗余导包` | | 文档更新 | `docs: 同步最新文章列表、赞助者名单` | | 注释修复 | `chore: 修复注释错别字` | | 新增 skill | `AI: 新增 skills/commit-message/SKILL.md,用于根据 git 变更生成符合项目风格的 commit message` | ================================================ FILE: .agents/skills/commit-message/examples.md ================================================ # Commit Message 示例 基于 Sa-Token 项目近期提交整理,遵循 Conventional Commits + 50/72 规则。 ## feat - 新增功能 ``` feat: 添加 sa-token-jackson3 插件 feat: 新增 sa-token-spring-boot4-starter 集成包 feat: 新增 sa-token-reactor-spring-boot4-starter 集成包 feat(sign): 新增签名模板自定义能力 ``` ## fix - 修复问题 ``` fix: 修复 StpUtil.getLoginIdByTokenNotThinkFreeze 方法缺少 static 的问题 fix: 修正一处代码注释错误:SaTokenDao 注释中 数据有效期 应为 小于等于-2 (掉了等于) fix: Bearer 全局统一大小写 fix: SaOAuth2Strategy中removeGrantTypeHandler的引用有误 ``` ## refactor - 重构/优化 ``` refactor: 移除冗余导包 refactor: 重命名 SaRepeatLoginsMode -> SaReplacedLoginExitMode refactor: 优化项目构建配置 refactor: 优化 OAuth2 模块在请求中读取 Client 信息算法 refactor: 优化模块依赖关系 refactor: 重构模块依赖层级 refactor: sa-token-dependencies 重构为 sa-token-basic-dependencies refactor: SaTokenDubboContextFilter 改为使用 SaTokenContextDubboUtil 清理上下文 ``` ## perf - 性能优化 ``` perf: 优化 StrFormatter 常量规范与封装 perf: 优化 pattern 缓存,消除魔法值 ``` ## docs - 文档 ``` docs: 订正文档错别字 docs: 同步最新文章列表、赞助者名单 docs: 为 sa-token-sso 模块定义 STS 协议 docs: 优化 readme docs: 同步最新博客链接 ``` ## chore - 杂项 ``` chore: 修复注释错别字 chore: 增加忽略 .vscode 目录 ``` ## demo - 示例 ``` demo: 新增 sa-token-demo-webflux-springboot4 示例 demo: 新增 SpringBoot4 整合 demo 示例 ``` ## test - 测试 ``` test: 新增 sa-token-jackson3 单元测试 ``` ## memo - 备忘录 ``` memo: 备忘录重构为专门的文件夹 ``` ## style - 代码格式 ``` style: 统一代码缩进与空格 style: 修复 ESLint 警告 ``` ## revert - 回滚 ``` revert: feat(sign): 新增签名模板自定义能力 ``` ## AI - AI 相关 ``` AI: 新增 skills/remove-redundancy-import/SKILL.md,用于检查项目中的java类无效冗余导包信息并移除 AI: 新增 SKILL: organize-update-log ,用于格式化整理版本更新日志信息 ``` ================================================ FILE: .agents/skills/commit-message/reference.md ================================================ # Commit Message 规范参考 基于 Conventional Commits 与业界最佳实践整理。 ## 核心规则 | 规则 | 说明 | |------|------| | 50 字规则 | subject 不超过 50 字符,便于 git log 完整显示 | | 72 字规则 | body 每行不超过 72 字符,便于阅读与 diff | | 命令式语气 | 用「修复」「新增」而非「修复了」「新增了」 | | 说明动机 | 重要变更在 body 中说明「为什么」而不仅是「做了什么」 | ## 格式结构 ``` [()]: [optional body] [optional footer(s)] ``` - **subject**:必填,简明扼要 - **body**:可选,详细说明 - **footer**:可选,如 `Fixes #123`、`BREAKING CHANGE: xxx` ## 类型速查 - **feat**:新功能 - **fix**:修复 bug - **refactor**:重构(结构、逻辑) - **perf**:性能优化 - **docs**:文档 - **style**:格式(不影响逻辑) - **chore**:构建、配置、杂项 - **test**:测试 - **revert**:回滚 ================================================ FILE: .agents/skills/organize-update-log/SKILL.md ================================================ --- name: organize-update-log description: 根据 git 提交记录生成符合 Sa-Token 项目规范的更新日志内容。适用于分析指定版本之后的提交、提取变更并格式化为 update-log.md 风格。当用户需要生成更新日志、整理版本变更、或分析 release 之后的提交时使用。 --- # 整理更新日志 根据 git 提交记录,生成符合 `sa-token-doc/more/update-log.md` 格式的更新日志内容。 ## 使用时机 - 用户要求生成/整理更新日志 - 用户要求分析「某版本之后」的提交变更 - 用户要求将 git 提交格式化为更新日志风格 - 准备发布新版本前整理 changelog ## 工作流程 ### 第一步:确定基准版本 1. 询问用户基准版本(如 `v1.44.0`),或从上下文推断 2. 查找该版本的发布提交: - 在 `SaTokenConsts.java` 或 `pom.xml` 中搜索版本号 - 或执行:`git log --oneline --all -- sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java` 查找含 `release vX.X.X` 的提交 3. 记录基准提交 hash(如 `7bde74bc`) ### 第二步:获取提交列表 执行: ```bash git log <基准提交>..HEAD --oneline --format="%h %s" ``` 可选,获取更详细的变更文件: ```bash git log <基准提交>..HEAD --stat --format="=== %h %s ===" ``` ### 第三步:分类与映射 将每条提交按以下规则归类到对应板块: | 提交关键词/内容 | 归属板块 | |----------------|----------| | feat.*jackson、plugin、插件 | 插件 | | feat.*starter、spring-boot、reactor | starter | | refactor.*依赖、dependencies、模块 | 重构 | | refactor.*solon、gateway | Solon(单独列出) | | fix.*dubbo、dubbo3 | 插件 | | demo.*、示例 | 示例 或 starter | | docs.*、文档 | 文档 | | chore、.gitignore、.vscode | 其它 | | merge.*loveqq、maven-pull | 其它(含 PR 链接) | ### 第四步:动作词映射 根据提交类型选择正确的动作词: | 提交类型 | 动作词 | 示例 | |----------|--------|------| | feat、新增 | 新增 | 新增 `sa-token-jackson3` 插件 | | fix、修复 | 修复 | 修复 Maven 父子项目依赖下载问题 | | refactor、重构 | 重构 / 移除 | 重构模块依赖层级;移除 xxx 模块 | | 优化、perf | 优化 | 优化 Gateway 接口处理 | | 拆分 | 拆分 | (少见) | | 文档更新 | 同步/新增/优化/修复 | 按具体内容选择 | ### 第五步:按格式输出 使用下方模板生成最终内容。详见 [format-reference.md](format-reference.md)。 ## 输出模板 ```markdown ### vX.X.X @YYYY-M-D(或:开发中 / 未发布) - 插件: - 新增:xxx。 **[重要]**(如适用) - 修复:xxx。merge: [pr N](https://gitee.com/dromara/sa-token/pulls/N)(如适用) - starter: - 新增:xxx。 - 重构: - 重构:xxx。 - 移除:xxx。 - Solon:(如有) - 优化:xxx。merge: [pr N](url) - 示例:(如有) - 新增:xxx。 - 文档: - 同步:xxx。 - 新增:xxx。 - 优化:xxx。 - 修复:xxx。 - 其它: - 新增/修复/优化:xxx。 ``` ## 格式规则 1. **层级**:一级用 `-`,二级用 ` -`(Tab + 短横线) 2. **动作词**:每条以「新增」「修复」「重构」「优化」「移除」「同步」等开头 3. **重要标记**:对用户影响大的变更加 `**[重要]**` 4. **PR/Issue 链接**:提交信息含 `!358`、`pr 340` 等时,补充 `merge: [pr N](https://gitee.com/dromara/sa-token/pulls/N)` 5. **代码/模块名**:用反引号包裹,如 `` `sa-token-jackson3` `` 6. **合并同类**:多条相似文档类提交可合并为一条(如「同步公众号、博客、赞助者名单」) ## 常见板块 - **core**:核心逻辑、API、配置变更 - **SSO**:单点登录相关 - **OAuth2**:OAuth2 相关 - **插件**:插件包(jackson、dubbo、redis 等) - **starter**:Spring Boot / Reactor 等 starter - **示例**:demo 项目 - **文档**:文档、README、错别字 - **其它**:其它杂项 ## 注意事项 - 合并提交(Merge branch)可忽略,只保留实际变更的提交 - 纯文档/错别字可适度合并,避免条目过多 - 版本号未发布时,可写 `v1.45.0(开发中)` 或 `未发布` - 输出为可直接粘贴到 `update-log.md` 的 Markdown 片段 ================================================ FILE: .agents/skills/organize-update-log/format-reference.md ================================================ # 更新日志格式参考 本文档提供 `sa-token-doc/more/update-log.md` 的格式细节与示例,供生成更新日志时参考。 ## 版本标题格式 ```markdown ### v1.44.0 @2025-6-7 ``` - 版本号:`v` + 主.次.修订 - 日期:`@YYYY-M-D` 或 `@YYYY-M-DD` - 未发布时:`v1.45.0(开发中)` 或 `v1.45.0(未发布)` ## 板块结构 ``` - 板块名: - 动作词:具体描述。 **[重要]**(可选) merge: [pr N](url)(可选) ``` - 一级:`- 板块名:` - 二级:` - 动作词:描述。`(Tab 缩进) ## 动作词 | 动作词 | 含义 | 使用场景 | |--------|------|----------| | 新增 | 新功能、新模块 | feat、新增插件、新 starter | | 修复 | Bug 修复 | fix | | 重构 | 结构调整 | refactor | | 优化 | 改进、优化 | 优化、perf | | 移除 | 删除模块/功能 | 删除、移除 | | 拆分 | 模块拆分 | 拆分 | | 同步 | 内容同步 | 文档、赞助者、博客列表 | | 补全 | 补充内容 | 补全文档、测试 | | 升级 | 升级、变更 | 升级 API、模块 | ## 链接格式 **PR:** ```markdown merge: [pr 340](https://gitee.com/dromara/sa-token/pulls/340) ``` **Issue:** ```markdown fix: [#IA6ZK0](https://gitee.com/dromara/sa-token/issues/IA6ZK0) ``` ## 重要标记 对用户影响较大的变更加 `**[重要]**`,通常放在句末、链接前: ```markdown - 新增:新增 `sa-token-spring-boot4-starter` 集成包。 **[重要]** - 新增:loveqq-framework 启动器集成。merge: [pr 340](url) ``` ## 完整示例 ```markdown ### v1.45.0(开发中) - 插件: - 新增:新增 `sa-token-jackson3` 插件,用于 Jackson 3 的 JSON 解析。 **[重要]** - 新增:新增 `sa-token-jackson3` 单元测试。 - starter: - 新增:新增 `sa-token-spring-boot4-starter` 集成包,支持 Spring Boot 4。 **[重要]** - 新增:新增 `sa-token-reactor-spring-boot4-starter` 集成包,支持 WebFlux + Spring Boot 4。 **[重要]** - 新增:新增 `sa-token-demo-webflux-springboot4` 示例。 - 新增:新增 Spring Boot 4 整合 demo 示例。 - 重构: - 重构:`sa-token-dependencies` 重构为 `sa-token-basic-dependencies`。 **[重要]** - 重构:重构 Spring Boot 相关集成包,优化依赖关系。 - 移除:移除 `sa-token-spring-boot-autoconfig` 模块,相关逻辑迁移至各 starter 内。 **[重要]** - 重构:重构模块依赖层级,新增 `sa-token-special-dependencies`。 - Solon: - 优化:`sa-token-solon-plugin` 优化 Gateway 接口的处理,避免使用路由接口。merge: [pr 348](https://gitee.com/dromara/sa-token/pulls/348) - 其它: - 新增:loveqq-framework 启动器集成。merge: [pr 340](https://gitee.com/dromara/sa-token/pulls/340) - 修复:修复 Maven 父子项目无法下载依赖的问题。merge: [pr 358](https://gitee.com/dromara/sa-token/pulls/358) - 文档: - 同步:同步公众号文章列表、博客列表、赞助者名单。 - 新增:新增《Gitee 2025年度开源项目 Web应用开发 Top 2》证书展示。 - 优化:优化框架 Slogan、README、案例库展示。 - 修复:错别字修复;文档图片地址更换为本地文件。 - 其它: - 新增:增加忽略 .vscode 目录。 - 优化:注释优化。 ``` ## 提交信息到条目的映射示例 | 提交信息 | 生成条目 | |----------|----------| | `feat: 添加 sa-token-jackson3 插件` | 新增:新增 `sa-token-jackson3` 插件,用于 Jackson 3 的 JSON 解析。 **[重要]** | | `refactor: 移除 sa-token-spring-boot-autoconfig 模块` | 移除:移除 `sa-token-spring-boot-autoconfig` 模块,相关逻辑迁移至各 starter 内。 **[重要]** | | `docs: 同步最新赞助者名单` | 同步:同步赞助者名单。 | | `!358 update maven-pull.md` | 修复:修复 Maven 父子项目无法下载依赖的问题。merge: [pr 358](url) | ## 文档类合并建议 以下类型可合并为一条: - 同步公众号、博客、赞助者名单 → 「同步:同步公众号文章列表、博客列表、赞助者名单。」 - 多条例错别字修复 → 「修复:错别字修复。」 - 多篇文档图片本地化 → 「修复:文档图片地址更换为本地文件(基础篇、深入篇、SSO篇等)。」 ================================================ FILE: .agents/skills/remove-redundancy-import/SKILL.md ================================================ --- name: remove-redundancy-import description: 检查 Java 类中未被引用的冗余 import 并移除。先输出待审阅计划,用户确认后执行。适用于用户要求清理冗余导包、优化 import、或执行 remove-redundancy-import 时使用。 --- # 移除冗余 import 检查项目中所有 Java 类的未使用 import,生成清理计划供用户审阅,确认后执行移除。 ## 使用时机 - 用户要求清理冗余导包 - 用户要求优化 Java import - 用户明确执行 `remove-redundancy-import` 或提及本 Skill 名称 ## 强制流程 **必须先输出计划,用户确认后再执行移除。** 不得在未审阅的情况下直接修改文件。 ## 工作流程 ### 第一步:扫描与解析 **优先使用内置脚本**:在 Skill 目录下的 [scan_redundant_imports.py](scan_redundant_imports.py) 已实现完整扫描逻辑,可直接复用。 ```bash # 在项目根目录执行 python .agents/skills/remove-redundancy-import/scan_redundant_imports.py # 或指定扫描根路径 python .agents/skills/remove-redundancy-import/scan_redundant_imports.py . ``` 脚本输出格式:`文件路径 | 冗余import1; import2 | 数量`,末尾两行为 `TOTAL_FILES:N` 和 `TOTAL_IMPORTS:M`。 **若无 Python 环境**,可手动执行: 1. 使用 `Glob` 查找项目内所有 `**/*.java` 文件 2. 对每个文件:提取 `package`、`import`,按 [reference.md](reference.md) 判定是否被使用 3. 汇总存在冗余 import 的文件及列表 ### 第二步:输出计划 使用下方模板生成计划报告,等待用户确认: ```markdown ## 冗余 import 清理计划 | 文件 | 待移除 import | 数量 | |------|---------------|------| | path/to/Foo.java | `java.util.Date`, `java.sql.Timestamp` | 2 | | ... | ... | ... | **共 N 个文件,M 处冗余 import。确认后执行移除。** ``` ### 第三步:执行移除 用户确认后,对计划中的每个文件使用 `StrReplace` 移除对应 import 行: - 逐行移除,每行格式为 `import ...;` 或 `import static ...;` - 若某 import 后紧跟空行,可一并移除空行以保持格式整洁 - 移除后确认文件无语法错误 ## 检测规则概要 - **普通 import**:取最后一段类名(如 `java.util.List` → `List`),在类体中搜索 `\bList\b` - **static import**:取方法/字段名,在类体中搜索 - **同包冗余**:import 的包与当前文件 `package` 相同则视为冗余 - **通配符**:`import pkg.*` 跳过,不自动处理 详见 [reference.md](reference.md)。 ## 注意事项 - 通配符 import 无法可靠判断,一律跳过 - 注解中的类型引用采用保守策略,宁可漏检不误删 - 移除后建议用户运行 `mvn compile` 验证 ================================================ FILE: .agents/skills/remove-redundancy-import/reference.md ================================================ # 冗余 import 检测规则 ## 解析步骤 ### 1. 提取 package 匹配 `package\s+([\w.]+)\s*;`,得到当前文件所在包。 ### 2. 提取 import 匹配以下模式(每行一条): - `import\s+([\w.]+)\s*;` — 普通 import - `import\s+static\s+([\w.]+)\s*;` — static 导入类 - `import\s+static\s+([\w.]+)\.(\w+)\s*;` — static 导入成员(方法/字段) - `import\s+[\w.]+\s*\.\s*\*\s*;` — 通配符,**跳过不处理** ### 3. 确定简单名(Simple Name) | import 类型 | 示例 | 简单名 | |-------------|------|--------| | 普通类 | `import java.util.List;` | `List` | | 内部类 | `import pkg.Outer.Inner;` | `Inner` | | static 类 | `import static pkg.Utils;` | `Utils` | | static 成员 | `import static pkg.Utils.foo;` | `foo` | ### 4. 同包冗余 若 `import x.y.Z` 的包 `x.y` 与当前文件 `package x.y` 相同,则该 import 冗余(同包无需导入)。 ### 5. 使用检测 在**类体**(`package` 和所有 `import` 之后)中搜索: - 使用正则 `\bSimpleName\b` 匹配整词,避免误匹配子串 - 排除:注释、字符串字面量中的出现 - 若未找到匹配,则该 import 视为未使用 ## 边界情况 | 情况 | 处理方式 | |------|----------| | `import pkg.*;` | 跳过,不自动移除 | | 注解中的类型 `@Foo` | 若 `Foo` 为 import 的简单名,视为已使用 | | 泛型 `List` | `List` 会匹配,视为已使用 | | 同名类(如 `java.util.Date` 与 `java.sql.Date`) | 两 import 都保留;若仅一个被使用,只移除未使用的 | | Javadoc `@param` 中的类型 | 保守:若不确定则保留 | ## 正则参考 ``` // package package\s+([\w.]+)\s*; // 普通 import(非通配符) import\s+(?!static)([\w.]+)\s*; // static import 成员 import\s+static\s+[\w.]+\.(\w+)\s*; // static import 类 import\s+static\s+([\w.]+)\s*; ``` ================================================ FILE: .agents/skills/remove-redundancy-import/scan_redundant_imports.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 冗余 import 扫描脚本 按 reference.md 规则扫描项目内 Java 文件,输出待移除的冗余 import 列表。 用法:在项目根目录执行 python scan_redundant_imports.py """ import os import re import sys def get_simple_name(imp: str) -> str | None: """从 import 行提取简单名(用于类体搜索)""" m = re.match(r'import\s+static\s+[\w.]+\.(\w+)\s*;', imp) if m: return m.group(1) m = re.match(r'import\s+(?:static\s+)?([\w.]+)\s*;', imp) if m: return m.group(1).split('.')[-1] return None def get_import_full(imp: str) -> str: """提取 import 的完整限定名""" m = re.match(r'import\s+(?:static\s+)?([\w.]+)\s*;', imp) return m.group(1).strip() if m else '' def get_import_package(imp: str) -> str: """提取 import 所在包(用于同包冗余判断)""" m = re.match(r'import\s+(?:static\s+)?([\w.]+)\s*;', imp) if m: parts = m.group(1).split('.') return '.'.join(parts[:-1]) if len(parts) > 1 else '' return '' def find_class_body_start(content: str) -> int: """找到类体起始位置(最后一个 import 之后)""" last = 0 for m in re.finditer(r'import\s+(?:static\s+)?[\w.]+\s*;', content): last = m.end() return last def main() -> None: root = sys.argv[1] if len(sys.argv) > 1 else '.' skip_dirs = {'target', 'build', '.git', 'node_modules'} results = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in skip_dirs] for f in filenames: if not f.endswith('.java'): continue path = os.path.join(dirpath, f).replace('\\', '/') try: with open(path, 'r', encoding='utf-8', errors='ignore') as fp: content = fp.read() except OSError: continue pkg_match = re.search(r'package\s+([\w.]+)\s*;', content) file_pkg = pkg_match.group(1) if pkg_match else '' imports = re.findall(r'import\s+(?:static\s+)?[\w.]+\s*;', content) imports = [i for i in imports if '*;' not in i and '.*' not in i] body_start = find_class_body_start(content) body = content[body_start:] redundant = [] for imp in imports: simple = get_simple_name(imp) if not simple: continue imp_full = get_import_full(imp) imp_pkg = get_import_package(imp) if imp_pkg and imp_pkg == file_pkg: redundant.append(imp_full) continue if not re.search(r'\b' + re.escape(simple) + r'\b', body): redundant.append(imp_full) if redundant: results.append((path, redundant)) for path, red in results: print(f"{path} | {'; '.join(red)} | {len(red)}") print("TOTAL_FILES:" + str(len(results))) print("TOTAL_IMPORTS:" + str(sum(len(r[1]) for r in results))) if __name__ == '__main__': main() ================================================ FILE: .agents/skills/upgrade-version/SKILL.md ================================================ --- name: upgrade-version description: 将 Sa-Token 项目版本号升级到指定新版本。每次调用时先读取当前版本并提示用户,待用户输入目标版本后再执行批量修改。修改范围:pom.xml、核心常量、Demo 子项目及文档。当用户要求升级版本、修改版本号、或 version bump 时使用。 --- # Sa-Token 版本升级 将项目版本号升级到用户指定的新版本。每次调用时**先读取当前版本并询问目标版本**,用户确认后再批量修改核心构建、Demo 项目及文档中的版本引用。 ## 使用时机 - 用户要求升级项目版本、修改版本号 - 用户说「版本从 vX.Y.Z 升级到 vX.Y.Z」「bump version」等 ## 工作流程 ### 第零步:询问目标版本(必须执行,不得跳过) 1. **读取当前版本**:从 `pom.xml` 的 `` 或 `SaTokenConsts.java` 的 `VERSION_NO` 中读取当前版本号 2. **提示用户**:明确告知「当前版本号是:xxx」 3. **等待输入**:询问「请输入要升级到的目标版本号(如 1.46.0):」 4. **确认后再执行**:**必须**等用户明确回复目标版本号后,才能执行后续修改步骤。若用户仅说「升级版本」而未给出目标版本,先完成本步骤再继续 ### 第一步:核心构建配置(3 个文件) 使用「当前版本」「目标版本」进行替换: | 文件 | 修改内容 | |------|----------| | `pom.xml` | `当前版本` → 目标版本 | | `sa-token-bom/pom.xml` | `当前版本` → 目标版本 | | `sa-token-core/.../SaTokenConsts.java` | `VERSION_NO = "v当前版本"` → `"v目标版本"` | ### 第二步:Demo 项目(sa-token-demo 下所有 pom.xml) - 将 `当前版本` 改为目标版本 - **sa-token-demo-bom-import** 额外修改:`` 内 `sa-token-bom` 的 `当前版本` → 目标版本 **查找方式**:`grep "当前版本" sa-token-demo --output-mode files_with_matches` 定位所有需修改的 pom.xml。 ### 第三步:文档(6 个文件) | 文件 | 修改内容 | |------|----------| | `README.md` | 标题 `v当前版本` → `v目标版本`;Maven 依赖 `当前版本` → 目标版本 | | `sa-token-doc/README.md` | 标题 `v当前版本` → `v目标版本` | | `sa-token-doc/index.html` | `v当前版本` → `v目标版本` | | `sa-token-doc/doc.html` | `v当前版本` 和 `saTokenTopVersion = '当前版本'` → 目标版本 | | `sa-token-doc/start/new-version.md` | 文案及 Maven 示例中的当前版本 → 目标版本 | ### 第四步:不修改的文件 以下为历史记录或示例,**保持原样**: - `.agents/skills/` 下的示例(format-reference.md、SKILL.md 等) - `MEMO/` 下的历史备忘录 - `sa-token-core/.../*.java` 中的 `@since X.Y.Z`(表示 API 引入版本,不随发布升级) - `sa-token-doc/more/update-log.md`:更新日志应**新增**新版本条目,而非修改旧条目 - `sa-token-doc/more/blog.md`:历史博客链接 ## 替换规则 - **pom.xml**:`当前版本` → `目标版本` - **Java**:`"v当前版本"` → `"v目标版本"` - **HTML/MD**:`v当前版本` 和 `当前版本` 按上下文分别替换为目标版本 ## 执行顺序建议 1. 第零步:读取当前版本 → 提示用户 → 等待用户输入目标版本 2. 根 POM、sa-token-bom 3. SaTokenConsts.java 4. 批量修改 Demo pom.xml 5. 修改文档 ## 验证 执行完成后,用 `grep "被替换的版本号"` 在项目根目录搜索,确认仅剩「不修改」列表中的文件仍含该版本(即修改已生效)。 ================================================ FILE: .gitee/ISSUE_TEMPLATE.md ================================================ 请在以下地址复制 issue 模板进行提交: https://sa-token.cc/doc.html#/fun/issue-template ================================================ FILE: .github/ISSUE_TEMPLATE/bug反馈.md ================================================ --- name: bug反馈 about: 当你明确框架存在 bug 时,选择这个模板提交 title: '' labels: '' assignees: '' --- ### 使用版本: ### 报错信息: ### 希望结果: ### 复现步骤: < 备注:如果复现步骤比较复杂,请将 demo 上传到 gitee 并留下地址 > ================================================ FILE: .github/ISSUE_TEMPLATE/功能提问.md ================================================ --- name: 功能提问 about: 对框架的某个功能看不明白时,选择这个模板提交 title: '' labels: '' assignees: '' --- ### 对以下问题有疑问: < 备注:请尽量详细描述问题所在 > ================================================ FILE: .github/ISSUE_TEMPLATE/建议增加新功能.md ================================================ --- name: 建议增加新功能 about: 当你有一个好 idea 时,选择这个模板提交 title: '' labels: '' assignees: '' --- ### 建议增加的新功能: ### 应用场景阐述: < 备注:请尽量详细描述功能应用场景 > ================================================ FILE: .github/ISSUE_TEMPLATE/预期不符.md ================================================ --- name: 预期不符 about: 当框架的运行结果和你的预期不一致时,选择这个模板提交 title: '' labels: '' assignees: '' --- ### 使用版本: ### 涉及的功能模块: ### 测试步骤: + 我经过以下步骤测试: + 得出以下结果: + 其中第 xx 行的代码输出表现 和文档上描述的不一致: + 我的理解是: 请问,是我的理解不对,还是文档出了问题? ================================================ FILE: .gitignore ================================================ target/ node_modules/ bin/ .settings/ unpackage/ .classpath .project *.iml .factorypath /.factorypath .idea/ .vscode/ sa-token-three-plugin/ sa-token-doc/big-file/ .flattened-pom.xml ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2011-Present hubin. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MEMO/1--统一定义properties尝试失败.md ================================================ ## 未完成目标 ### 1、尝试将所有 `` 依赖版本号定义在同一个 pom.xml 里。 **[❌失败]** **尝试1:将所有 `` 定义在 `sa-token-dependencies` 里:** 结果: 无法在 `sa-token-spring-boot2/3/4-dependencies` 中引用这些 ``,因为 ` import` 只会导入目标的 `` 版本号定义,不会导入目标的 `` 属性。 `` 只会在 父子结构中向下传递,不会在 ` import` 中传递。 **尝试2:将所有 `` 定义在 `sa-token-parent` 里:** 结果:在 `sa-token-dependencies` 里无法引用这些 ``,因为 `sa-token-parent` 不是 `sa-token-dependencies` 的父模块。 将 `sa-token-parent` 定义为 `sa-token-dependencies` 的父模块行吗? 不行,因为在 `sa-token-parent` 通过 ` import` 导入了 `sa-token-dependencies`,如果再把 `sa-token-parent` 定义为 `sa-token-dependencies` 的父模块,会造成循环依赖。 执行 `mvn package` 打包时,maven 会直接报错: ``` [ERROR] [ERROR] Some problems were encountered while processing the POMs: [ERROR] The dependencies of type=pom and with scope=import form a cycle: cn.dev33:sa-token-parent:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 @ cn.dev33:sa-token-basic-dependencies:1.44.0 @ [ERROR] The build could not read 1 project -> [Help 1] [ERROR] [ERROR] The project cn.dev33:sa-token-parent:1.44.0 (E:\work\project-yun\sa-token\pom.xml) has 1 error [ERROR] The dependencies of type=pom and with scope=import form a cycle: cn.dev33:sa-token-parent:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 -> cn.dev33:sa-token-basic-dependencies:1.44.0 @ cn.dev33:sa-token-basic-dependencies:1.44.0 [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException ``` ================================================ FILE: MEMO/2--2026-3-1_诡异调试记录.txt ================================================ 2026-3-1 调试记录 启动 SaOAuth2ServerApplication,报错空指针:SaOAuth2ServerController 文件的 SaOAuth2Strategy.instance.notLoginView 空指针,SaOAuth2Strategy.instance 为 null SaOAuth2Strategy.instance 的定义为: public static final SaOAuth2Strategy instance = new SaOAuth2Strategy(); 看代码是无论如何也不可能空指针的,诡异。 在 main 方法第一句加上测试 @SpringBootApplication public class SaOAuth2ServerApplication { public static void main(String[] args) { System.out.println(SaOAuth2Strategy.instance); SpringApplication.run(SaOAuth2ServerApplication.class, args); System.out.println("\nSa-Token-OAuth2 Server端启动成功,配置如下:"); System.out.println(SaOAuth2Manager.getServerConfig()); } } 打印居然为 null。 询问 AI,解释的乱七八糟,没有参考价值。 然后在根目录执行 mvn clean,居然无法成功。sa-token-test 模块无法 clean 。 报错 test 依赖不存在 org.springframework.boot spring-boot-starter-test test 最后必须在 dependencyManagement 加上这个才行 org.springframework.boot spring-boot-starter-test 2.7.18 可是就算不加,我也已经在 sa-token-spring-boot2-dependencies 中定义这个依赖了呀,为什么 在 sa-token-test 中无法 import spring-boot-starter-test ? 加了后,mvn clean 执行成功了 但是 mvn package 又开始无法打包。 可以昨天我明明能打包成功的啊?今天好像就变动了一下 sa-token-test 中的依赖配置。这有什么影响吗? 而且打包报错信息居然是:sa-token-jboot-plugin 插件中 javax.servlet.http.HttpServletRequest 无法转换为 HttpServletRequest 什么东西啊。 抓头挠腮解决不了。 这个插件已经十几个版本没有变动过代码了,代码不变,打包环境不变,命令不变,今天就突然报这种莫名其妙的错误,无奈,只能先去除这个插件,不让它参与打包。 继续打包,又开始报错: sa-token-jfinal-plugin 中 cn.dev33.satoken.context.SaTokenContext 无法转换为 SaTokenContext。 这一瞬间我怀疑自己正处于梦中。 纠结了半分钟,继续去除此插件,继续打包。 打包成功了。 启动 SaOAuth2ServerApplication,启动成功,SaOAuth2ServerController 文件的 SaOAuth2Strategy.instance.notLoginView 空指针问题,消失了。 请问中间的这几个报错和这个空指针有任何关联吗?我请问呢? 注:以上所有叙述均为最后打包成功后进行回忆,可能细节上略有偏差。 两小时后: 本来可以运行成功的代码,只要一改子模块的代码就无法再运行成功,报错:java: 无法访问SaRequest。 试了好多解决方案,不行。 --- 吃了两份炉盖香酥鸡饼,原来人在压力大的时候真的需要补充能量。 --- 继续报错: Maven 资源编译器: 模块 'sa-token-oauth2' 所需的 Maven 项目配置不可用。仅当从 IDE 启动外部构建时,才支持 Maven 项目编译。 sa-token-jwt、sso、sign 等模块均出现此问题 最后: 把项目删掉,重新下载一份,导入 项目可以运行成功了,但是每次修改子模块,在 demo 示例里无法实时起作用。需要 mvn clean install 才能看到效果。 最后: 取消勾选 maven 配置项:Delegate IDE build/run actions to Maven 一切问题解决,包括最上面的诡异调试现象也消失了。 idea,你给老子爬 ================================================ FILE: MEMO/3--sa-token_最新版所有依赖.txt ================================================ cn.dev33 sa-token-bom 1.45.0 pom import cn.dev33 sa-token-core 1.45.0 cn.dev33 sa-token-spring-boot-starter 1.45.0 cn.dev33 sa-token-spring-boot3-starter 1.45.0 cn.dev33 sa-token-spring-boot4-starter 1.45.0 cn.dev33 sa-token-reactor-spring-boot-starter 1.45.0 cn.dev33 sa-token-reactor-spring-boot3-starter 1.45.0 cn.dev33 sa-token-reactor-spring-boot4-starter 1.45.0 cn.dev33 sa-token-jboot-plugin 1.45.0 cn.dev33 sa-token-jfinal-plugin 1.45.0 cn.dev33 sa-token-loveqq-boot-starter 1.45.0 cn.dev33 sa-token-servlet 1.45.0 cn.dev33 sa-token-jakarta-servlet 1.45.0 cn.dev33 sa-token-plugin 1.45.0 cn.dev33 sa-token-alone-redis 1.45.0 cn.dev33 sa-token-redis-template 1.45.0 cn.dev33 sa-token-redis-template-jdk-serializer 1.45.0 cn.dev33 sa-token-redis-jackson 1.45.0 cn.dev33 sa-token-redisson 1.45.0 cn.dev33 sa-token-redisson-spring-boot-starter 1.45.0 cn.dev33 sa-token-redisx 1.45.0 cn.dev33 sa-token-hutool-timed-cache 1.45.0 cn.dev33 sa-token-caffeine 1.45.0 cn.dev33 sa-token-jackson 1.45.0 cn.dev33 sa-token-jackson3 1.45.0 cn.dev33 sa-token-fastjson 1.45.0 cn.dev33 sa-token-fastjson2 1.45.0 cn.dev33 sa-token-snack3 1.45.0 cn.dev33 sa-token-snack4 1.45.0 cn.dev33 sa-token-serializer-features 1.45.0 cn.dev33 sa-token-thymeleaf 1.45.0 cn.dev33 sa-token-freemarker 1.45.0 cn.dev33 sa-token-dubbo 1.45.0 cn.dev33 sa-token-dubbo3 1.45.0 cn.dev33 sa-token-grpc 1.45.0 cn.dev33 sa-token-forest 1.45.0 cn.dev33 sa-token-okhttps 1.45.0 cn.dev33 sa-token-jwt 1.45.0 cn.dev33 sa-token-temp-jwt 1.45.0 cn.dev33 sa-token-oauth2 1.45.0 cn.dev33 sa-token-apikey 1.45.0 cn.dev33 sa-token-sign 1.45.0 cn.dev33 sa-token-quick-login 1.45.0 cn.dev33 sa-token-sso 1.45.0 cn.dev33 sa-token-spring-aop 1.45.0 cn.dev33 sa-token-spring-el 1.45.0 cn.dev33 sa-token-spring-boot-webmvc-reactor-v2v3v4-common 1.45.0 cn.dev33 sa-token-spring-boot-reactor-v2v3v4-common 1.45.0 cn.dev33 sa-token-spring-boot-webmvc-v3v4-common 1.45.0 ================================================ FILE: README.md ================================================

logo

Sa-Token v1.45.0

✨ 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!

在线文档:https://sa-token.cc

--- ### 📝 前言: 回望 2020 年初,我为 Sa-Token 提交第一行代码之际,彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架,更是一整套清晰、自洽的权限架构设计思想。 因此,这几年间我将大量时间倾注在 Sa-Token 的文档编写,几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨,以求做到最清晰、干练、易懂的表述。用心阅读文档,你学习到的将不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。 ### 🛠️ Sa-Token 介绍 Sa-Token 是一个轻量级 Java 权限认证框架,目前拥有五大核心模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。 ![sa-token-jss](https://sa-token.cc/big-file/index/intro/sa-token-jss--tran.png) 要在 SpringBoot 项目中使用 Sa-Token,你只需要在 pom.xml 中引入依赖: ``` xml cn.dev33 sa-token-spring-boot-starter 1.45.0 ``` 除了支持 SpringBoot2、Sa-Token 还为 SpringBoot3/4、Solon、JFinal 等常见 Web 框架提供集成包,做到真正的开箱即用。
简单示例展示:(点击展开 / 折叠) Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要: ``` java // 会话登录,参数填登录人的账号id StpUtil.login(10001); ``` 无需实现任何接口,无需创建任何配置文件,只需要这一句静态代码的调用,便可以完成会话登录认证。 如果一个接口需要登录后才能访问,我们只需调用以下代码: ``` java // 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常 StpUtil.checkLogin(); ``` 在 Sa-Token 中,大多数功能都可以一行代码解决: 踢人下线: ``` java // 将账号id为 10077 的会话踢下线 StpUtil.kickout(10077); ``` 权限认证: ``` java // 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法 @SaCheckPermission("user:add") public String insert(SysUser user) { // ... return "用户增加"; } ``` 路由拦截鉴权: ``` java // 根据路由划分模块,不同模块不同鉴权 registry.addInterceptor(new SaInterceptor(handler -> { SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); // 更多模块... })).addPathPatterns("/**"); ``` **如果您曾经使用过 Shiro、SpringSecurity,在切换到 Sa-Token 后,您将体会到质的飞跃。**
核心模块一览:(点击展开 / 折叠) - **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。 - **权限认证** —— 权限认证、角色认证、会话二级认证。 - **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。 - **注解式鉴权** —— 优雅的将鉴权与业务代码分离。 - **路由拦截式鉴权** —— 根据路由拦截鉴权,可适配 restful 模式。 - **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。 - **持久层扩展** —— 可集成 Redis,重启数据不丢失。 - **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。 - **Token风格定制** —— 内置六种 Token 风格,还可:自定义 Token 生成策略。 - **记住我模式** —— 适配 [记住我] 模式,重启浏览器免验证。 - **二级认证** —— 在已登录的基础上再次认证,保证安全性。 - **模拟他人账号** —— 实时操作任意用户状态数据。 - **临时身份切换** —— 将会话身份临时切换为其它账号。 - **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。 - **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。 - **密码加密** —— 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。 - **会话查询** —— 提供方便灵活的会话查询接口。 - **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。 - **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。 - **全局过滤器** —— 方便的处理跨域,全局设置安全响应头等操作。 - **多账号体系认证** —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表) - **单点登录** —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。 - **单点注销** —— 任意子系统内发起注销,即可全端下线。 - **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务,支持openid模式 。 - **分布式会话** —— 提供共享数据中心分布式会话方案。 - **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。 - **RPC调用鉴权** —— 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔 - **临时Token认证** —— 解决短时间的 Token 授权问题。 - **独立Redis** —— 将权限缓存与业务缓存分离。 - **Quick快速登录认证** —— 为项目零代码注入一个登录页面。 - **标签方言** —— 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。 - **jwt集成** —— 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。 - **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。 - **参数签名** —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。 - **自动续签** —— 提供两种Token过期策略,灵活搭配使用,还可自动续签。 - **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。 - **最新技术栈** —— 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。
### 🍃 SSO 单点登录 Sa-Token SSO 分为三种模式,可解决:`同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` 等架构下的 SSO 认证需求: ![sa-token-jss](https://sa-token.cc/big-file/doc/sso/sa-token-sso--white.png) | 系统架构 | 采用模式 | 简介 | 文档链接 | | :-------- | :-------- |:----------------| :-------- | | 前端同域 + 后端同 Redis | 模式一 | 共享Cookie同步会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client) | | 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client) | | 前端不同域 + 后端 不同Redis | 模式三 | HTTP请求获取会话 | [文档](https://sa-token.cc/doc.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso3-client) | 1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com` 2. 后端同 Redis:就是指多个系统可以连接同一个 Redis,共享会话数据。 3. 如果无法做到前端同域、后端同 Redis,可以走托底的模式三:Http请求校验 ticket 获取会话。 4. 提供:NoSdk 模式示例 + sso-server 接口文档,非 Sa-Token 项目、非 java 项目也可以对接。 5. 提供:多重安全校验:域名校验、ticket校验、参数签名校验,有效防 ticket 劫持,防请求重放等攻击。 6. 提供:大量实战痛点教学:sso-server 前后端分离设计、sso-client 前后端分离设计、用户数据同步/迁移方案设计。 7. 提供:直接可运行的 demo 示例,助你快速熟悉 SSO 大致登录流程。 8. 提供:深度细节优化,参数防丢:笔者曾试验多个SSO框架,均有参数丢失情况,比如登录前是:`http://a.com?id=1&name=2`,登录成功后就变成了:`http://a.com?id=1`,Sa-Token-SSO 内有专门算法保证了参数不丢失,登录成功后精准原路返回。 ### 🍂 OAuth2 授权认证 Sa-Token OAuth2 模块分为四种授权模式,解决不同场景下的授权需求 | 授权模式 | 简介 | | :-------- | :-------- | | 授权码式 | OAuth2 标准授权步骤,server 端下放 code,client 端获取 code 码兑换 access_token | | 隐藏式 | 备用选择,server 端使用 URL 重定向方式直接将 access_token 下放到 client 端页面 | | 密码式 | client 直接拿着用户的账号密码换取授权 access_token | | 客户端凭证式 | server 端针对 client 级别的 client_token,代表应用自身的资源授权 | 详细参考文档:[https://sa-token.cc/doc.html#/oauth2/readme](https://sa-token.cc/doc.html#/oauth2/readme) ### 📖❓ 疑问解答 **1、Sa-Token 功能全不全?** 七年磨一剑:五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证,我们提供的是一站式解决方案。 **2、Sa-Token 好不好学?** 中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。 **3、Sa-Token 用的人多不多?** 截止统计日 (2026-1-25) 起,Sa-Token 在: - Gitee 关注量达到 48627 Star,位列平台所有推荐项目排行榜第一名。 - GitHub 关注量达到 18523 Star,是主要竞争框架 Spring Security 的 1.97 倍,Apache Shiro 的 4.19 倍。 - 25+ 微信粉丝群 (500人),8+ QQ粉丝群 (1000人 or 2000人) ,在线文档访问量月PV 20万+。 这是众多开发者用脚投票的数据,相信这些数据比任何言语都能证明 Sa-Token 的热度。 **4、Sa-Token 有哪些权威认证?** 曾获荣誉包括但不限于:Gitee GVP 最有价值开源项目、GitCode G-Star 优质开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目、OSCHINA 2022 年度最火热中国开源项目社区之一、开放原子基金会2023快速成长开源项目、 Dromara 组织顶尖项目(之一)、可信开源社区共同体预备成员、所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。 Gitee High Star 计划项目(5000+star)。Gitee 2025年度开源项目 Web应用开发 Top 2。 **5、Sa-Token 收费吗?** Sa-Token 采用 Apache-2.0 开源协议,承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token,我们也不回避:[赞助链接](https://sa-token.cc/doc.html#/more/sa-token-donate)。 我们将定期同步赞助者名单到在线文档展示。(您需要注意的一点是:该赞助仅为友情赞助,不提供任何商业交换) **6、Sa-Token 是封装的 SpringSecurity 吗?是套壳 ApacheShiro 吗?** 不是。Sa-Token 不是一个后台模板,也不是针对 xx 框架的二次封装套壳,而是从 0 开始的纯血自研框架,核心包零依赖,完全自主可控的架构内核 + 众多主流框架的集成适配。 ### 🚀 优秀开源集成案例 - [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + Vite + SpringBoot + Mp + HuTool + SaToken。 - [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Xxl-Job、knife4j、OSS 定期同步。 - [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin):SmartAdmin 国内首个以「高质量代码」为核心,「简洁、高效、安全」中后台快速开发平台。 - [[ 橙单 ]](https://gitee.com/orangeform/orange-admin): 橙单中台化低代码生成器。可完整支持多应用、多租户、多渠道、工作流、框架技术栈自由组合等。 - [[ 灯灯 ]](https://gitee.com/dromara/lamp-cloud): 专注于多租户解决方案的中后台快速开发平台。支持独立数据库、共享数据架构 和 非租户模式 ✨ - [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog):一款 vue + springboot 前后端分离的博客系统。 还有更多优秀开源案例无法逐一展示,请参考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) ### 🔗 友情链接 - [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps):轻量级 http 通信框架,API无比优雅,支持 WebSocket、Stomp 协议 - [[ Forest ]](https://gitee.com/dromara/forest):声明式与编程式双修,让天下没有难以发送的 HTTP 请求 - [[ Bean Searcher ]](https://github.com/ejlchina/bean-searcher):专注高级查询的只读 ORM,使一行代码实现复杂列表检索! - [[ Jpom ]](https://gitee.com/dromara/Jpom):简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件。 - [[ TLog ]](https://gitee.com/dromara/TLog):一个轻量级的分布式日志标记追踪神器。 - [[ hippo4j ]](https://gitee.com/agentart/hippo4j):强大的动态线程池框架,附带监控报警功能。 - [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat):易用友好的开源实时监控告警系统,无需Agent,高性能集群,强大自定义监控能力。 - [[ Solon ]](https://gitee.com/noear/solon):一个更现代感的应用开发框架:更快、更小、更自由。 - [[ Chat2DB ]](https://github.com/chat2db/Chat2DB):一个AI驱动的数据库管理和BI工具,支持Mysql、pg、Oracle、Redis等22种数据库的管理。 ### 📦 代码托管 - Gitee:[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token) - GitHub:[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token) - AtomGit:[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token) ### 💬 交流群 QQ交流群:1081649142 [点击加入](https://qm.qq.com/q/SCAaZ6Ros2) 微信交流群: PS:扫码添加微信 (备注:sa-token),邀您加入群聊。
微信群 加入群聊的好处: - 第一时间收到框架更新通知。 - 第一时间收到框架 bug 通知。 - 第一时间收到新增开源案例通知。 - 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。 ================================================ FILE: mvn clean.bat ================================================ :: 整体clean call mvn clean :: demo模块clean cd sa-token-demo cd sa-token-demo-alone-redis & call mvn clean & cd .. cd sa-token-demo-alone-redis-cluster & call mvn clean & cd .. cd sa-token-demo-apikey & call mvn clean & cd .. cd sa-token-demo-async & call mvn clean & cd .. cd sa-token-demo-beetl & call mvn clean & cd .. cd sa-token-demo-bom-import & call mvn clean & cd .. cd sa-token-demo-case & call mvn clean & cd .. cd sa-token-demo-device-lock & call mvn clean & cd .. cd sa-token-demo-grpc & call mvn clean & cd .. cd sa-token-demo-hutool-timed-cache & call mvn clean & cd .. cd sa-token-demo-caffeine & call mvn clean & cd .. cd sa-token-demo-jwt & call mvn clean & cd .. cd sa-token-demo-quick-login & call mvn clean & cd .. cd sa-token-demo-quick-login-sb3 & call mvn clean & cd .. cd sa-token-demo-solon & call mvn clean & cd .. cd sa-token-demo-solon-redisson & call mvn clean & cd .. cd sa-token-demo-springboot & call mvn clean & cd .. cd sa-token-demo-springboot3-redis & call mvn clean & cd .. cd sa-token-demo-springboot4-redis & call mvn clean & cd .. cd sa-token-demo-springboot-low-version & call mvn clean & cd .. cd sa-token-demo-springboot-redis & call mvn clean & cd .. cd sa-token-demo-springboot-redisson & call mvn clean & cd .. cd sa-token-demo-sse & call mvn clean & cd .. cd sa-token-demo-ssm & call mvn clean & cd .. cd sa-token-demo-test & call mvn clean & cd .. cd sa-token-demo-thymeleaf & call mvn clean & cd .. cd sa-token-demo-freemarker & call mvn clean & cd .. cd sa-token-demo-webflux & call mvn clean & cd .. cd sa-token-demo-webflux-springboot3 & call mvn clean & cd .. cd sa-token-demo-websocket & call mvn clean & cd .. cd sa-token-demo-websocket-spring & call mvn clean & cd .. cd sa-token-demo-dubbo cd sa-token-demo-dubbo-consumer & call mvn clean & cd .. cd sa-token-demo-dubbo-provider & call mvn clean & cd .. cd sa-token-demo-dubbo3-consumer & call mvn clean & cd .. cd sa-token-demo-dubbo3-provider & call mvn clean & cd .. cd .. cd sa-token-demo-oauth2 cd sa-token-demo-oauth2-client & call mvn clean & cd .. cd sa-token-demo-oauth2-server & call mvn clean & cd .. cd .. cd sa-token-demo-remember-me cd sa-token-demo-remember-me-server & call mvn clean & cd .. cd .. cd sa-token-demo-sso cd sa-token-demo-sso-server & call mvn clean & cd .. cd sa-token-demo-sso1-client & call mvn clean & cd .. cd sa-token-demo-sso2-client & call mvn clean & cd .. cd sa-token-demo-sso3-client & call mvn clean & cd .. cd sa-token-demo-sso3-client-nosdk & call mvn clean & cd .. cd sa-token-demo-sso3-client-resdk & call mvn clean & cd .. cd sa-token-demo-sso3-client-anon & call mvn clean & cd .. cd .. cd sa-token-demo-sso-for-solon cd sa-token-demo-sso1-client-solon & call mvn clean & cd .. cd sa-token-demo-sso2-client-solon & call mvn clean & cd .. cd sa-token-demo-sso3-client-solon & call mvn clean & cd .. cd sa-token-demo-sso-server-solon & call mvn clean & cd .. cd .. cd .. :: test clean cd sa-token-test call mvn clean cd .. :: 最后打印 echo; echo; echo ----------- clean end ----------- echo; pause ================================================ FILE: mvn test.bat ================================================ :: 整体test call mvn clean test :: 最后打印 echo; echo; echo ----------- test end ----------- echo; pause ================================================ FILE: pom.xml ================================================ 4.0.0 cn.dev33 sa-token-parent pom ${revision} sa-token An open-source, free, and one-stop Java authentication framework that makes authentication simple and elegant! https://github.com/dromara/sa-token sa-token-dependencies sa-token-special-dependencies sa-token-bom sa-token-core sa-token-starter sa-token-plugin Apache 2 http://www.apache.org/licenses/LICENSE-2.0.txt repo A business-friendly OSS license 1.45.0 1.8 utf-8 utf-8 3.2.8 0.10.0 master https://github.com/dromara/sa-token.git scm:git:https://github.com/dromara/sa-token.git scm:git:https://github.com/dromara/sa-token.git click33 2393584716@qq.com cn.dev33 sa-token-dependencies ${project.version} pom import org.apache.maven.plugins maven-source-plugin 3.4.0 true compile jar org.apache.maven.plugins maven-compiler-plugin 3.15.0 1.8 1.8 UTF-8 org.apache.maven.plugins maven-javadoc-plugin 3.12.0 false false -Xdoclint:none false package aggregate attach-javadocs jar none org.codehaus.mojo flatten-maven-plugin 1.7.3 true resolveCiFriendliesOnly flatten process-resources flatten flatten.clean clean clean org.apache.maven.plugins maven-gpg-plugin ${maven-gpg-plugin.version} sign-artifacts verify sign org.sonatype.central central-publishing-maven-plugin ${central.publishing.maven.version} true central org.eclipse.m2e lifecycle-mapping 1.0.0 org.apache.maven.plugins maven-enforcer-plugin [1.0.0,) enforce ================================================ FILE: preview-doc.bat ================================================ :: 运行前需要安装 browser-sync: :: npm install -g browser-sync cd sa-token-doc & browser-sync start --server --files "" ================================================ FILE: sa-token-bom/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-bom ${revision} pom sa-token-bom Sa-Token Bom https://github.com/dromara/sa-token 1.45.0 3.2.8 0.10.0 Apache 2 http://www.apache.org/licenses/LICENSE-2.0.txt repo A business-friendly OSS license master https://github.com/dromara/sa-token.git scm:git:https://github.com/dromara/sa-token.git scm:git:https://github.com/dromara/sa-token.git click33 2393584716@qq.com cn.dev33 sa-token-core ${revision} cn.dev33 sa-token-starter ${revision} cn.dev33 sa-token-jboot-plugin ${revision} cn.dev33 sa-token-jfinal-plugin ${revision} cn.dev33 sa-token-reactor-spring-boot-starter ${revision} cn.dev33 sa-token-reactor-spring-boot3-starter ${revision} cn.dev33 sa-token-reactor-spring-boot4-starter ${revision} cn.dev33 sa-token-servlet ${revision} cn.dev33 sa-token-jakarta-servlet ${revision} cn.dev33 sa-token-solon-plugin ${revision} cn.dev33 sa-token-spring-boot-webmvc-reactor-v2v3v4-common ${revision} cn.dev33 sa-token-spring-boot-reactor-v2v3v4-common ${revision} cn.dev33 sa-token-spring-boot-webmvc-v3v4-common ${revision} cn.dev33 sa-token-spring-boot-starter ${revision} cn.dev33 sa-token-spring-boot3-starter ${revision} cn.dev33 sa-token-spring-boot4-starter ${revision} cn.dev33 sa-token-plugin ${revision} cn.dev33 sa-token-alone-redis ${revision} cn.dev33 sa-token-alone-redis-by-spring-boot4 ${revision} cn.dev33 sa-token-dubbo ${revision} cn.dev33 sa-token-dubbo3 ${revision} cn.dev33 sa-token-grpc ${revision} cn.dev33 sa-token-redis-template ${revision} cn.dev33 sa-token-jackson ${revision} cn.dev33 sa-token-jackson3 ${revision} cn.dev33 sa-token-fastjson ${revision} cn.dev33 sa-token-fastjson2 ${revision} cn.dev33 sa-token-snack3 ${revision} cn.dev33 sa-token-redis-jackson ${revision} cn.dev33 sa-token-forest ${revision} cn.dev33 sa-token-okhttps ${revision} cn.dev33 sa-token-redisson-spring-boot-starter ${revision} cn.dev33 sa-token-redisson ${revision} cn.dev33 sa-token-redisx ${revision} cn.dev33 sa-token-hutool-timed-cache ${revision} cn.dev33 sa-token-thymeleaf ${revision} cn.dev33 sa-token-freemarker ${revision} cn.dev33 sa-token-jwt ${revision} cn.dev33 sa-token-oauth2 ${revision} cn.dev33 sa-token-apikey ${revision} cn.dev33 sa-token-sign ${revision} cn.dev33 sa-token-quick-login ${revision} cn.dev33 sa-token-spring-aop ${revision} cn.dev33 sa-token-spring-el ${revision} cn.dev33 sa-token-sso ${revision} cn.dev33 sa-token-temp-jwt ${revision} cn.dev33 sa-token-redis-template-jdk-serializer ${revision} cn.dev33 sa-token-serializer-features ${revision} org.apache.maven.plugins maven-compiler-plugin 3.15.0 1.8 1.8 UTF-8 org.codehaus.mojo flatten-maven-plugin 1.7.3 true resolveCiFriendliesOnly flatten process-resources flatten flatten.clean clean clean org.apache.maven.plugins maven-gpg-plugin ${maven-gpg-plugin.version} sign-artifacts verify sign org.sonatype.central central-publishing-maven-plugin ${central.publishing.maven.version} true central org.eclipse.m2e lifecycle-mapping 1.0.0 org.apache.maven.plugins maven-enforcer-plugin [1.0.0,) enforce ================================================ FILE: sa-token-core/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-parent ${revision} ../pom.xml jar sa-token-core sa-token-core An open-source, free, and one-stop Java authentication framework that makes authentication simple and elegant! ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.config.SaTokenConfigFactory; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.context.SaTokenContextForThreadLocal; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.http.SaHttpTemplate; import cn.dev33.satoken.http.SaHttpTemplateDefaultImpl; import cn.dev33.satoken.json.SaJsonTemplate; import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.log.SaLogForConsole; import cn.dev33.satoken.same.SaSameTemplate; import cn.dev33.satoken.secure.totp.SaTotpTemplate; import cn.dev33.satoken.serializer.SaSerializerTemplate; import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJson; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.stp.StpInterfaceDefaultImpl; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.temp.SaTempTemplate; import cn.dev33.satoken.util.SaFoxUtil; import java.util.LinkedHashMap; import java.util.Map; /** * 管理 Sa-Token 所有全局组件,可通过此类快速获取、写入各种全局组件对象 * * @author click33 * @since 1.18.0 */ public class SaManager { /** * 全局配置对象 */ public volatile static SaTokenConfig config; public static void setConfig(SaTokenConfig config) { setConfigMethod(config); // 打印 banner if(config !=null && config.getIsPrint()) { SaFoxUtil.printSaToken(); } // 如果此 config 对象没有配置 isColorLog 的值,则框架为它自动判断一下 if(config != null && config.getIsLog() != null && config.getIsLog() && config.getIsColorLog() == null) { config.setIsColorLog(SaFoxUtil.isCanColorLog()); } // $$ 全局事件 SaTokenEventCenter.doSetConfig(config); // 调用一次 StpUtil 中的方法,保证其可以尽早的初始化 StpLogic StpUtil.getLoginType(); } private static void setConfigMethod(SaTokenConfig config) { SaManager.config = config; } /** * 获取 Sa-Token 的全局配置信息 * @return 全局配置信息 */ public static SaTokenConfig getConfig() { if (config == null) { synchronized (SaManager.class) { if (config == null) { setConfigMethod(SaTokenConfigFactory.createConfig()); } } } return config; } /** * 持久化组件 */ private volatile static SaTokenDao saTokenDao; public static void setSaTokenDao(SaTokenDao saTokenDao) { setSaTokenDaoMethod(saTokenDao); SaTokenEventCenter.doRegisterComponent("SaTokenDao", saTokenDao); } private static void setSaTokenDaoMethod(SaTokenDao saTokenDao) { if (SaManager.saTokenDao != null) { SaManager.saTokenDao.destroy(); } SaManager.saTokenDao = saTokenDao; if (SaManager.saTokenDao != null) { SaManager.saTokenDao.init(); } } public static SaTokenDao getSaTokenDao() { if (saTokenDao == null) { synchronized (SaManager.class) { if (saTokenDao == null) { setSaTokenDaoMethod(new SaTokenDaoDefaultImpl()); } } } return saTokenDao; } /** * 权限数据源组件 */ private volatile static StpInterface stpInterface; public static void setStpInterface(StpInterface stpInterface) { SaManager.stpInterface = stpInterface; SaTokenEventCenter.doRegisterComponent("StpInterface", stpInterface); } public static StpInterface getStpInterface() { if (stpInterface == null) { synchronized (SaManager.class) { if (stpInterface == null) { SaManager.stpInterface = new StpInterfaceDefaultImpl(); } } } return stpInterface; } /** * 上下文 SaTokenContext */ private volatile static SaTokenContext saTokenContext; public static void setSaTokenContext(SaTokenContext saTokenContext) { SaManager.saTokenContext = saTokenContext; SaTokenEventCenter.doRegisterComponent("SaTokenContext", saTokenContext); } public static SaTokenContext getSaTokenContext() { if (saTokenContext == null) { synchronized (SaManager.class) { if (saTokenContext == null) { SaManager.saTokenContext = new SaTokenContextForThreadLocal(); } } } return saTokenContext; } /** * 临时 token 认证模块 */ private volatile static SaTempTemplate saTempTemplate; public static void setSaTempTemplate(SaTempTemplate saTempTemplate) { SaManager.saTempTemplate = saTempTemplate; SaTokenEventCenter.doRegisterComponent("SaTempTemplate", saTempTemplate); } public static SaTempTemplate getSaTempTemplate() { if (saTempTemplate == null) { synchronized (SaManager.class) { if (saTempTemplate == null) { SaManager.saTempTemplate = new SaTempTemplate(); } } } return saTempTemplate; } /** * JSON 转换器 */ private volatile static SaJsonTemplate saJsonTemplate; public static void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { SaManager.saJsonTemplate = saJsonTemplate; SaTokenEventCenter.doRegisterComponent("SaJsonTemplate", saJsonTemplate); } public static SaJsonTemplate getSaJsonTemplate() { if (saJsonTemplate == null) { synchronized (SaManager.class) { if (saJsonTemplate == null) { SaManager.saJsonTemplate = new SaJsonTemplateDefaultImpl(); } } } return saJsonTemplate; } /** * HTTP 转换器 */ private volatile static SaHttpTemplate saHttpTemplate; public static void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { SaManager.saHttpTemplate = saHttpTemplate; SaTokenEventCenter.doRegisterComponent("SaHttpTemplate", saHttpTemplate); } public static SaHttpTemplate getSaHttpTemplate() { if (saHttpTemplate == null) { synchronized (SaManager.class) { if (saHttpTemplate == null) { SaManager.saHttpTemplate = new SaHttpTemplateDefaultImpl(); } } } return saHttpTemplate; } /** * 序列化器 */ private volatile static SaSerializerTemplate saSerializerTemplate; public static void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { SaManager.saSerializerTemplate = saSerializerTemplate; SaTokenEventCenter.doRegisterComponent("SaSerializerTemplate", saSerializerTemplate); } public static SaSerializerTemplate getSaSerializerTemplate() { if (saSerializerTemplate == null) { synchronized (SaManager.class) { if (saSerializerTemplate == null) { SaManager.saSerializerTemplate = new SaSerializerTemplateForJson(); } } } return saSerializerTemplate; } /** * Same-Token 同源系统认证模块 */ private volatile static SaSameTemplate saSameTemplate; public static void setSaSameTemplate(SaSameTemplate saSameTemplate) { SaManager.saSameTemplate = saSameTemplate; SaTokenEventCenter.doRegisterComponent("SaSameTemplate", saSameTemplate); } public static SaSameTemplate getSaSameTemplate() { if (saSameTemplate == null) { synchronized (SaManager.class) { if (saSameTemplate == null) { SaManager.saSameTemplate = new SaSameTemplate(); } } } return saSameTemplate; } /** * 日志输出器 */ public volatile static SaLog log = new SaLogForConsole(); public static void setLog(SaLog log) { SaManager.log = log; SaTokenEventCenter.doRegisterComponent("SaLog", log); } public static SaLog getLog() { return SaManager.log; } /** * TOTP 算法类,支持 生成/验证 动态一次性密码 */ private volatile static SaTotpTemplate totpTemplate; public static void setSaTotpTemplate(SaTotpTemplate totpTemplate) { SaManager.totpTemplate = totpTemplate; SaTokenEventCenter.doRegisterComponent("SaTotpTemplate", totpTemplate); } public static SaTotpTemplate getSaTotpTemplate() { if (totpTemplate == null) { synchronized (SaManager.class) { if (totpTemplate == null) { SaManager.totpTemplate = new SaTotpTemplate(); } } } return totpTemplate; } // ------------------- StpLogic 相关 ------------------- /** * StpLogic 集合, 记录框架所有成功初始化的 StpLogic */ public static Map stpLogicMap = new LinkedHashMap<>(); /** * 向全局集合中 put 一个 StpLogic * @param stpLogic StpLogic */ public static void putStpLogic(StpLogic stpLogic) { stpLogicMap.put(stpLogic.getLoginType(), stpLogic); } /** * 在全局集合中 移除 一个 StpLogic */ public static void removeStpLogic(String loginType) { stpLogicMap.remove(loginType); } /** * 根据 LoginType 获取对应的StpLogic,如果不存在则新建并返回 * @param loginType 对应的账号类型 * @return 对应的StpLogic */ public static StpLogic getStpLogic(String loginType) { return getStpLogic(loginType, true); } /** * 根据 LoginType 获取对应的StpLogic,如果不存在,isCreate = 是否自动创建并返回 * @param loginType 对应的账号类型 * @param isCreate 在 StpLogic 不存在时,true=新建并返回,false=抛出异常 * @return 对应的StpLogic */ public static StpLogic getStpLogic(String loginType, boolean isCreate) { // 如果type为空则返回框架默认内置的 if(loginType == null || loginType.isEmpty()) { return StpUtil.stpLogic; } // 从集合中获取 StpLogic stpLogic = stpLogicMap.get(loginType); if(stpLogic == null) { // isCreate=true时,自创建模式:自动创建并返回 if(isCreate) { synchronized (SaManager.class) { stpLogic = stpLogicMap.get(loginType); if(stpLogic == null) { stpLogic = SaStrategy.instance.createStpLogic.apply(loginType); } } } // isCreate=false时,严格校验模式:抛出异常 else { /* * 此时有两种情况会造成 StpLogic == null * 1. loginType拼写错误,请改正 (建议使用常量) * 2. 自定义StpUtil尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种 * (1) 从main方法里调用一次 * (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 */ throw new SaTokenException("未能获取对应StpLogic,type="+ loginType).setCode(SaErrorCode.CODE_10002); } } // 返回 return stpLogic; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckDisable.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import cn.dev33.satoken.util.SaTokenConsts; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 服务禁用校验:判断当前账号是否被禁用了指定服务,如果被禁用,会抛出异常,没有被禁用才能进入方法。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author videomonster * @since 1.31.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckDisable { /** * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 * * @return / */ String type() default ""; /** * 服务标识 (具体你要校验是否禁用的服务名称) * * @return / */ String[] value() default { SaTokenConsts.DEFAULT_DISABLE_SERVICE }; /** * 封禁等级(如果当前账号的被封禁等级 ≥ 此值,请求就无法进入方法) * * @return / */ int level() default SaTokenConsts.DEFAULT_DISABLE_LEVEL; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpBasic.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Http Basic 认证校验:只有通过 Http Basic 认证后才能进入该方法,否则抛出异常。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.26.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckHttpBasic { /** * 领域 * @return / */ String realm() default SaHttpBasicTemplate.DEFAULT_REALM; /** * 需要校验的账号密码,格式形如 sa:123456 * @return / */ String account() default ""; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import cn.dev33.satoken.httpauth.digest.SaHttpDigestModel; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Http Digest 认证校验:只有通过 Http Digest 认证后才能进入该方法,否则抛出异常。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.38.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckHttpDigest { /** * 用户名 * @return / */ String username() default ""; /** * 密码 * @return / */ String password() default ""; /** * 领域 * @return / */ String realm() default SaHttpDigestModel.DEFAULT_REALM; /** * 需要校验的用户名和密码,格式形如 sa:123456 * @return / */ String value() default ""; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckLogin.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 登录认证校验:只有登录之后才能进入该方法。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.10.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckLogin { /** * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 * * @return / */ String type() default ""; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.*; /** * 批量注解鉴权:只要满足其中一个注解即可通过验证 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.35.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckOr { /** * 设定 @SaCheckLogin,参考 {@link SaCheckLogin} * * @return / */ SaCheckLogin[] login() default {}; /** * 设定 @SaCheckRole,参考 {@link SaCheckRole} * * @return / */ SaCheckRole[] role() default {}; /** * 设定 @SaCheckPermission,参考 {@link SaCheckPermission} * * @return / */ SaCheckPermission[] permission() default {}; /** * 设定 @SaCheckSafe,参考 {@link SaCheckSafe} * * @return / */ SaCheckSafe[] safe() default {}; /** * 设定 @SaCheckHttpBasic,参考 {@link SaCheckHttpBasic} * * @return / */ SaCheckHttpBasic[] httpBasic() default {}; /** * 设定 @SaCheckBasic,参考 {@link SaCheckHttpDigest} * * @return / */ SaCheckHttpDigest[] httpDigest() default {}; /** * 设定 @SaCheckDisable,参考 {@link SaCheckDisable} * * @return / */ SaCheckDisable[] disable() default {}; /** * 需要追加抓取的注解 Class (只能填写 Sa-Token 相关注解类型) * * @return / */ Class[] append() default {}; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckPermission.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限认证校验:必须具有指定权限才能进入该方法。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.10.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckPermission { /** * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 * * @return / */ String type() default ""; /** * 需要校验的权限码 [ 数组 ] * * @return / */ String [] value() default {}; /** * 验证模式:AND | OR,默认AND * * @return / */ SaMode mode() default SaMode.AND; /** * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 * *

* 例1:@SaCheckPermission(value="user-add", orRole="admin"), * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。 *

* *

* 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。
* 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。 *

* * @return / */ String[] orRole() default {}; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckRole.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 角色认证校验:必须具有指定角色标识才能进入该方法。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.10.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckRole { /** * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 * * @return / */ String type() default ""; /** * 需要校验的角色标识 [ 数组 ] * * @return / */ String [] value() default {}; /** * 验证模式:AND | OR,默认AND * * @return / */ SaMode mode() default SaMode.AND; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckSafe.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import cn.dev33.satoken.util.SaTokenConsts; /** * 二级认证校验:客户端必须完成二级认证之后,才能进入该方法,否则将被抛出异常。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上)。 * * @author click33 * @since 1.21.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckSafe { /** * 多账号体系下所属的账号体系标识,非多账号体系无需关注此值 * * @return / */ String type() default ""; /** * 要校验的服务 * * @return / */ String value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaIgnore.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 忽略认证:表示被修饰的方法或类无需进行注解认证和路由拦截认证。 * *

请注意:此注解的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。

* * @author click33 * @since 1.31.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaIgnore { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaMode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; /** * 注解鉴权的验证模式 * * @author click33 * @since 1.10.0 */ public enum SaMode { /** * 必须具有所有的元素 */ AND, /** * 只需具有其中一个元素 */ OR } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaAnnotationHandlerInterface.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; /** * 所有注解处理器的父接口 * * @author click33 * @since 2024/8/2 */ public interface SaAnnotationHandlerInterface { /** * 获取所要处理的注解类型 * @return / */ Class getHandlerAnnotationClass(); /** * 所需要执行的校验方法 * @param at 注解对象 * @param element 被标注的注解的元素(方法/类)引用 */ @SuppressWarnings("unchecked") default void check(Annotation at, AnnotatedElement element) { checkMethod((T) at, element); } /** * 所需要执行的校验方法(转换类型后) * @param at 注解对象 * @param element 被标注的注解的元素(方法/类)引用 */ void checkMethod(T at, AnnotatedElement element); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckDisableHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.SaCheckDisable; import cn.dev33.satoken.stp.StpLogic; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckDisable 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckDisableHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckDisable.class; } @Override public void checkMethod(SaCheckDisable at, AnnotatedElement element) { _checkMethod(at.type(), at.value(), at.level()); } public static void _checkMethod(String type, String[] value, int level) { StpLogic stpLogic = SaManager.getStpLogic(type, false); Object loginId = stpLogic.getLoginId(); for (String service : value) { stpLogic.checkDisableLevel(loginId, service, level); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpBasicHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.annotation.SaCheckHttpBasic; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckHttpBasic 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckHttpBasicHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckHttpBasic.class; } @Override public void checkMethod(SaCheckHttpBasic at, AnnotatedElement element) { _checkMethod(at.realm(), at.account()); } public static void _checkMethod(String realm, String account) { SaHttpBasicUtil.check(realm, account); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpDigestHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.annotation.SaCheckHttpDigest; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; import cn.dev33.satoken.util.SaFoxUtil; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckHttpDigest 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckHttpDigestHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckHttpDigest.class; } @Override public void checkMethod(SaCheckHttpDigest at, AnnotatedElement element) { _checkMethod(at.username(), at.password(), at.realm(), at.value()); } public static void _checkMethod(String username, String password, String realm, String value) { // 如果配置了 value,则以 value 优先 if(SaFoxUtil.isNotEmpty(value)){ String[] arr = value.split(":"); if(arr.length != 2){ throw new SaTokenException("注解参数配置错误,格式应如:username:password"); } SaHttpDigestUtil.check(arr[0], arr[1]); return; } // 如果配置了 username,则分别获取参数 if(SaFoxUtil.isNotEmpty(username)){ SaHttpDigestUtil.check(username, password, realm); return; } // 都没有配置,则根据全局配置参数进行校验 SaHttpDigestUtil.check(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckLoginHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.stp.StpLogic; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckLogin 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckLoginHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckLogin.class; } @Override public void checkMethod(SaCheckLogin at, AnnotatedElement element) { _checkMethod(at.type()); } public static void _checkMethod(String type) { StpLogic stpLogic = SaManager.getStpLogic(type, false); stpLogic.checkLogin(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckOrHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.annotation.*; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 注解 SaCheckOr 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckOrHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckOr.class; } @Override public void checkMethod(SaCheckOr at, AnnotatedElement element) { _checkMethod(at.login(), at.role(), at.permission(), at.safe(), at.httpBasic(), at.httpDigest(), at.disable(), at.append(), element); } public static void _checkMethod( SaCheckLogin[] login, SaCheckRole[] role, SaCheckPermission[] permission, SaCheckSafe[] safe, SaCheckHttpBasic[] httpBasic, SaCheckHttpDigest[] httpDigest, SaCheckDisable[] disable, Class[] append, AnnotatedElement element ) { // 先把所有注解塞到一个 list 里 List annotationList = new ArrayList<>(); annotationList.addAll(Arrays.asList(login)); annotationList.addAll(Arrays.asList(role)); annotationList.addAll(Arrays.asList(permission)); annotationList.addAll(Arrays.asList(safe)); annotationList.addAll(Arrays.asList(disable)); annotationList.addAll(Arrays.asList(httpBasic)); annotationList.addAll(Arrays.asList(httpDigest)); for (Class annotationClass : append) { Annotation annotation = SaAnnotationStrategy.instance.getAnnotation.apply(element, annotationClass); if(annotation != null) { annotationList.add(annotation); } } // 如果 atList 为空,说明 SaCheckOr 上不包含任何注解校验,我们直接跳过即可 if(annotationList.isEmpty()) { return; } // 逐个开始校验 >>> List errorList = new ArrayList<>(); for (Annotation item : annotationList) { try { SaAnnotationStrategy.instance.annotationHandlerMap.get(item.annotationType()).check(item, element); // 只要有一个校验通过,就可以直接返回了 return; } catch (SaTokenException e) { errorList.add(e); } } // 执行至此,说明所有注解校验都通过不了,此时 errorList 里面会有多个异常,我们随便抛出一个即可 throw errorList.get(0); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckPermissionHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.util.SaFoxUtil; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckPermission 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckPermissionHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckPermission.class; } @Override public void checkMethod(SaCheckPermission at, AnnotatedElement element) { _checkMethod(at.type(), at.value(), at.mode(), at.orRole()); } public static void _checkMethod(String type, String[] value, SaMode mode, String[] orRole) { StpLogic stpLogic = SaManager.getStpLogic(type, false); String[] permissionArray = value; try { if(mode == SaMode.AND) { stpLogic.checkPermissionAnd(permissionArray); } else { stpLogic.checkPermissionOr(permissionArray); } } catch (NotPermissionException e) { // 权限认证校验未通过,再开始角色认证校验 for (String role : orRole) { String[] rArr = SaFoxUtil.convertStringToArray(role); // 某一项 role 认证通过,则可以提前退出了,代表通过 if (stpLogic.hasRoleAnd(rArr)) { return; } } throw e; } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckRoleHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.stp.StpLogic; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckRole 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckRoleHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckRole.class; } @Override public void checkMethod(SaCheckRole at, AnnotatedElement element) { _checkMethod(at.type(), at.value(), at.mode()); } public static void _checkMethod(String type, String[] value, SaMode mode) { StpLogic stpLogic = SaManager.getStpLogic(type, false); String[] roleArray = value; if(mode == SaMode.AND) { stpLogic.checkRoleAnd(roleArray); } else { stpLogic.checkRoleOr(roleArray); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckSafeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.stp.StpLogic; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckSafe 的处理器 * * @author click33 * @since 2024/8/2 */ public class SaCheckSafeHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckSafe.class; } @Override public void checkMethod(SaCheckSafe at, AnnotatedElement element) { _checkMethod(at.type(), at.value()); } public static void _checkMethod(String type, String value) { StpLogic stpLogic = SaManager.getStpLogic(type, false); stpLogic.checkSafe(value); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaIgnoreHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation.handler; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.router.SaRouter; import java.lang.reflect.AnnotatedElement; /** * 注解 SaIgnore 的处理器 *

v1.43.0 版本起,SaIgnore 注解处理逻辑已转移到全局策略中,此处理器代码仅做留档

* * @author click33 * @since 2024/8/2 */ public class SaIgnoreHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaIgnore.class; } @Override public void checkMethod(SaIgnore at, AnnotatedElement element) { _checkMethod(); } public static void _checkMethod() { SaRouter.stop(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/application/ApplicationInfo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.application; import cn.dev33.satoken.util.SaFoxUtil; /** * 应用全局信息 * * @author click33 * @since 1.31.0 */ public class ApplicationInfo { /** * 应用前缀 */ public static String routePrefix; /** * 为指定 path 裁剪掉 routePrefix 前缀 * @param path 指定 path * @return / */ public static String cutPathPrefix(String path) { if(! SaFoxUtil.isEmpty(routePrefix) && ! routePrefix.equals("/") && path.startsWith(routePrefix)){ path = path.substring(routePrefix.length()); } return path; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/application/SaApplication.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.application; import java.util.ArrayList; import java.util.List; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; /** * Application Model,全局作用域的读取值对象。 * *

在应用全局范围内: 存值、取值。数据在应用重启后失效,如果集成了 Redis,则在 Redis 重启后失效。 * * @author click33 * @since 1.31.0 */ public class SaApplication implements SaSetValueInterface { /** * 默认实例 */ public static SaApplication defaultInstance = new SaApplication(); // ---- 实现接口存取值方法 /** 取值 */ @Override public Object get(String key) { return SaManager.getSaTokenDao().getObject(splicingDataKey(key)); } /** 写值 */ @Override public SaApplication set(String key, Object value) { return set(key, value, SaTokenDao.NEVER_EXPIRE); } /** 删值 */ @Override public SaApplication delete(String key) { SaManager.getSaTokenDao().deleteObject(splicingDataKey(key)); return this; } // ---- 其它方法 /** * 写值 * @param key 名称 * @param value 值 * @param ttl 有效时间(单位:秒) * @return 对象自身 */ public SaApplication set(String key, Object value, long ttl) { SaManager.getSaTokenDao().setObject(splicingDataKey(key), value, ttl); return this; } /** * 返回当前存入的所有 key * @return / */ public List keys() { // 从缓存中查询出所有此前缀的 key String prefix = splicingDataKey(""); List list = SaManager.getSaTokenDao().searchData(prefix, "", 0, -1, true); // 裁减掉固定前缀,保留 key 名称,塞入新集合 int prefixLength = prefix.length(); List list2 = new ArrayList<>(); if(list != null) { for (String key : list) { list2.add(key.substring(prefixLength)); } } // 返回 return list2; } /** * 清空当前存入的所有 key */ public void clear() { List keys = keys(); for (String key : keys) { delete(key); } } /** * 拼接key:当存入一个变量时,应该使用的 key * * @param key 原始 key * @return 拼接后的 key 值 */ public String splicingDataKey(String key) { return SaManager.getConfig().getTokenName() + ":var:" + key; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/application/SaGetValueInterface.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.application; import cn.dev33.satoken.util.SaFoxUtil; /** * 对取值的一组方法封装 *

封装 SaStorage、SaSession、SaApplication 等存取值的一些固定方法,减少重复编码

* * @author click33 * @since 1.31.0 */ public interface SaGetValueInterface { // --------- 需要子类实现的方法 /** * 取值 * @param key key * @return 值 */ Object get(String key); // --------- 接口提供封装的方法 /** * 取值 (指定默认值) * * @param 默认值的类型 * @param key key * @param defaultValue 取不到值时返回的默认值 * @return 值 */ default T get(String key, T defaultValue) { return getValueByDefaultValue(get(key), defaultValue); } /** * 取值 (转String类型) * @param key key * @return 值 */ default String getString(String key) { Object value = get(key); if(value == null) { return null; } return String.valueOf(value); } /** * 取值 (转int类型) * @param key key * @return 值 */ default int getInt(String key) { return getValueByDefaultValue(get(key), 0); } /** * 取值 (转long类型) * @param key key * @return 值 */ default long getLong(String key) { return getValueByDefaultValue(get(key), 0L); } /** * 取值 (转double类型) * @param key key * @return 值 */ default double getDouble(String key) { return getValueByDefaultValue(get(key), 0.0); } /** * 取值 (转float类型) * @param key key * @return 值 */ default float getFloat(String key) { return getValueByDefaultValue(get(key), 0.0f); } /** * 取值 (指定转换类型) * @param 泛型 * @param key key * @param cs 指定转换类型 * @return 值 */ default T getModel(String key, Class cs) { return SaFoxUtil.getValueByType(get(key), cs); } /** * 取值 (指定转换类型, 并指定值为 null 时返回的默认值) * @param 泛型 * @param key key * @param cs 指定转换类型 * @param defaultValue 值为Null时返回的默认值 * @return 值 */ @SuppressWarnings("unchecked") default T getModel(String key, Class cs, Object defaultValue) { T model = getModel(key, cs); return valueIsNull(model) ? (T)defaultValue : model; } /** * 是否含有某个 key * @param key 指定 key * @return 是否含有 */ default boolean has(String key) { return !valueIsNull(get(key)); } // --------- 内部工具方法 /** * 判断一个值是否为null * @param value 指定值 * @return 此value是否为null */ default boolean valueIsNull(Object value) { return value == null || value.equals(""); } /** * 根据默认值来获取值 * @param 泛型 * @param value 值 * @param defaultValue 默认值 * @return 转换后的值 */ @SuppressWarnings("unchecked") default T getValueByDefaultValue(Object value, T defaultValue) { // 如果 obj 为 null,则直接返回默认值 if(valueIsNull(value)) { return defaultValue; } // 开始转换类型 Class cs = (Class) defaultValue.getClass(); return SaFoxUtil.getValueByType(value, cs); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.application; import cn.dev33.satoken.fun.SaRetGenericFunction; /** * 对写值的一组方法封装 *

封装 SaStorage、SaSession、SaApplication 等存取值的一些固定方法,减少重复编码

* * @author click33 * @since 1.31.0 */ public interface SaSetValueInterface extends SaGetValueInterface { // --------- 需要子类实现的方法 /** * 写值 * @param key 名称 * @param value 值 * @return 对象自身 */ SaSetValueInterface set(String key, Object value); /** * 删值 * @param key 要删除的key * @return 对象自身 */ SaSetValueInterface delete(String key); // --------- 接口提供封装的方法 /** * * 取值 (如果值为 null,则执行 fun 函数获取值,并把函数返回值写入缓存) * @param 返回值的类型 * @param key key * @param fun 值为null时执行的函数 * @return 值 */ @SuppressWarnings("unchecked") default T get(String key, SaRetGenericFunction fun) { Object value = get(key); if(value == null) { value = fun.run(); set(key, value); } return (T) value; } /** * 写值 (只有在此 key 原本无值的情况下才会写入) * @param key 名称 * @param value 值 * @return 对象自身 */ default SaSetValueInterface setByNull(String key, Object value) { if( ! has(key)) { set(key, value); } return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/config/SaCookieConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.config; import java.util.LinkedHashMap; import java.util.Map; /** * Sa-Token Cookie写入 相关配置 * * @author click33 * @since 1.27.0 */ public class SaCookieConfig { /* Cookie 功能为浏览器通用标准,建议大家自行搜索文章了解各个属性的功能含义,此处源码仅做简单解释。 */ /** * 作用域 *

写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。

*

一般情况下你不需要设置此值,因为浏览器默认会把 Cookie 写到当前域名下。

*/ private String domain; /** * 路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) */ private String path; /** * 是否只在 https 协议下有效 */ private Boolean secure = false; /** * 是否禁止 js 操作 Cookie */ private Boolean httpOnly = false; /** * 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) */ private String sameSite; /** * 额外扩展属性 */ private Map extraAttrs = new LinkedHashMap<>(); /** * 获取:Cookie 作用域 *

写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。

*

一般情况下你不需要设置此值,因为浏览器默认会把 Cookie 写到当前域名下。

* @return / */ public String getDomain() { return domain; } /** * 写入:Cookie 作用域 *

写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。

*

一般情况下你不需要设置此值,因为浏览器默认会把 Cookie 写到当前域名下。

* @param domain / * @return 对象自身 */ public SaCookieConfig setDomain(String domain) { this.domain = domain; return this; } /** * @return 路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) */ public String getPath() { return path; } /** * @param path 路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) * @return 对象自身 */ public SaCookieConfig setPath(String path) { this.path = path; return this; } /** * @return 是否只在 https 协议下有效 */ public Boolean getSecure() { return secure; } /** * @param secure 是否只在 https 协议下有效 * @return 对象自身 */ public SaCookieConfig setSecure(Boolean secure) { this.secure = secure; return this; } /** * @return 是否禁止 js 操作 Cookie */ public Boolean getHttpOnly() { return httpOnly; } /** * @param httpOnly 是否禁止 js 操作 Cookie * @return 对象自身 */ public SaCookieConfig setHttpOnly(Boolean httpOnly) { this.httpOnly = httpOnly; return this; } /** * @return 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) */ public String getSameSite() { return sameSite; } /** * @param sameSite 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) * @return 对象自身 */ public SaCookieConfig setSameSite(String sameSite) { this.sameSite = sameSite; return this; } /** * @return 获取额外扩展属性 */ public Map getExtraAttrs() { return extraAttrs; } /** * 写入额外扩展属性 * @param extraAttrs / * @return 对象自身 */ public SaCookieConfig setExtraAttrs(Map extraAttrs) { this.extraAttrs = extraAttrs; return this; } /** * 追加扩展属性 * @param name / * @param value / * @return 对象自身 */ public SaCookieConfig addExtraAttr(String name, String value) { if (extraAttrs == null) { extraAttrs = new LinkedHashMap<>(); } this.extraAttrs.put(name, value); return this; } /** * 追加扩展属性 * @param name / * @return 对象自身 */ public SaCookieConfig addExtraAttr(String name) { return this.addExtraAttr(name, null); } /** * 移除指定扩展属性 * @param name / * @return 对象自身 */ public SaCookieConfig removeExtraAttr(String name) { if(extraAttrs != null) { this.extraAttrs.remove(name); } return this; } // toString @Override public String toString() { return "SaCookieConfig [" + "domain=" + domain + ", path=" + path + ", secure=" + secure + ", httpOnly=" + httpOnly + ", sameSite=" + sameSite + ", extraAttrs=" + extraAttrs + "]"; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.config; import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; import cn.dev33.satoken.stp.parameter.enums.SaLogoutRange; import cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode; import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; /** * Sa-Token 配置类 Model * *

* 你可以通过yml、properties、java代码等形式配置本类参数,具体请查阅官方文档: * https://sa-token.cc *

* * @author click33 * @since 1.10.0 */ public class SaTokenConfig implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) */ private String tokenName = "satoken"; /** token 有效期(单位:秒) 默认30天,-1 代表永久有效 */ private long timeout = 60 * 60 * 24 * 30; /** * token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) */ private long activeTimeout = -1; /** * 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 */ private Boolean dynamicActiveTimeout = false; /** * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) */ private Boolean isConcurrent = true; /** * 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) */ private Boolean isShare = false; /** * 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) */ private SaReplacedLoginExitMode replacedLoginExitMode = SaReplacedLoginExitMode.OLD_DEVICE; /** * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) */ private SaReplacedRange replacedRange = SaReplacedRange.CURR_DEVICE_TYPE; /** * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) */ private int maxLoginCount = 12; /** * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) */ private SaLogoutMode overflowLogoutMode = SaLogoutMode.LOGOUT; /** * 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) */ private int maxTryTimes = 12; /** * 是否尝试从请求体里读取 token */ private Boolean isReadBody = true; /** * 是否尝试从 header 里读取 token */ private Boolean isReadHeader = true; /** * 是否尝试从 cookie 里读取 token */ private Boolean isReadCookie = true; /** * 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) */ private Boolean isLastingCookie = true; /** * 是否在登录后将 token 写入到响应头 */ private Boolean isWriteHeader = false; /** * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) *
(此参数只在调用 StpUtil.logout() 时有效) */ private SaLogoutRange logoutRange = SaLogoutRange.TOKEN; /** * 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效) */ private Boolean isLogoutKeepFreezeOps = false; /** * 在注销 token 后,是否保留其对应的 Token-Session */ private Boolean isLogoutKeepTokenSession = false; /** * 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) */ private Boolean rightNowCreateTokenSession = false; /** * token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */ private String tokenStyle = "uuid"; /** * 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理 */ private int dataRefreshPeriod = 30; /** * 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录) */ private Boolean tokenSessionCheckLogin = true; /** * 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) */ private Boolean autoRenew = true; /** * token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */ private String tokenPrefix; /** * cookie 模式是否自动填充 token 前缀 */ private Boolean cookieAutoFillPrefix = false; /** * 是否在初始化配置时在控制台打印版本字符画 */ private Boolean isPrint = true; /** * 是否打印操作日志 */ private Boolean isLog = false; /** * 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 */ private String logLevel = "trace"; /** * 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 */ private int logLevelInt = 1; /** * 是否打印彩色日志 */ private Boolean isColorLog = null; /** * jwt秘钥(只有集成 jwt 相关模块时此参数才会生效) */ private String jwtSecretKey; /** * Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 */ private String httpBasic = ""; /** * Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 */ private String httpDigest = ""; /** * 配置当前项目的网络访问地址 */ private String currDomain; /** * Same-Token 的有效期 (单位: 秒) */ private long sameTokenTimeout = 60 * 60 * 24; /** * 是否校验 Same-Token(部分rpc插件有效) */ private Boolean checkSameToken = false; /** * Cookie配置对象 */ public SaCookieConfig cookie = new SaCookieConfig(); /** * @return token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) */ public String getTokenName() { return tokenName; } /** * @param tokenName token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) * @return 对象自身 */ public SaTokenConfig setTokenName(String tokenName) { this.tokenName = tokenName; return this; } /** * @return token 有效期(单位:秒) 默认30天,-1 代表永久有效 */ public long getTimeout() { return timeout; } /** * @param timeout token 有效期(单位:秒) 默认30天,-1 代表永久有效 * @return 对象自身 */ public SaTokenConfig setTimeout(long timeout) { this.timeout = timeout; return this; } /** * @return token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) */ public long getActiveTimeout() { return activeTimeout; } /** * @param activeTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) * @return 对象自身 */ public SaTokenConfig setActiveTimeout(long activeTimeout) { this.activeTimeout = activeTimeout; return this; } /** * @return 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 */ public Boolean getDynamicActiveTimeout() { return dynamicActiveTimeout; } /** * @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 * @return 对象自身 */ public SaTokenConfig setDynamicActiveTimeout(Boolean dynamicActiveTimeout) { this.dynamicActiveTimeout = dynamicActiveTimeout; return this; } /** * @return 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) */ public Boolean getIsConcurrent() { return isConcurrent; } /** * @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) * @return 对象自身 */ public SaTokenConfig setIsConcurrent(Boolean isConcurrent) { this.isConcurrent = isConcurrent; return this; } /** * @return 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) */ public Boolean getIsShare() { return isShare; } /** * @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) * @return 对象自身 */ public SaTokenConfig setIsShare(Boolean isShare) { this.isShare = isShare; return this; } /** * @return 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) */ public int getMaxLoginCount() { return maxLoginCount; } /** * @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) * @return 对象自身 */ public SaTokenConfig setMaxLoginCount(int maxLoginCount) { this.maxLoginCount = maxLoginCount; return this; } /** * @return 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) */ public int getMaxTryTimes() { return maxTryTimes; } /** * @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) * @return 对象自身 */ public SaTokenConfig setMaxTryTimes(int maxTryTimes) { this.maxTryTimes = maxTryTimes; return this; } /** * @return 是否尝试从请求体里读取 token */ public Boolean getIsReadBody() { return isReadBody; } /** * @param isReadBody 是否尝试从请求体里读取 token * @return 对象自身 */ public SaTokenConfig setIsReadBody(Boolean isReadBody) { this.isReadBody = isReadBody; return this; } /** * @return 是否尝试从 header 里读取 token */ public Boolean getIsReadHeader() { return isReadHeader; } /** * @param isReadHeader 是否尝试从 header 里读取 token * @return 对象自身 */ public SaTokenConfig setIsReadHeader(Boolean isReadHeader) { this.isReadHeader = isReadHeader; return this; } /** * @return 是否尝试从 cookie 里读取 token */ public Boolean getIsReadCookie() { return isReadCookie; } /** * @param isReadCookie 是否尝试从 cookie 里读取 token * @return 对象自身 */ public SaTokenConfig setIsReadCookie(Boolean isReadCookie) { this.isReadCookie = isReadCookie; return this; } /** * 获取 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) * * @return isLastingCookie / */ public Boolean getIsLastingCookie() { return this.isLastingCookie; } /** * 设置 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) * * @param isLastingCookie / * @return 对象自身 */ public SaTokenConfig setIsLastingCookie(Boolean isLastingCookie) { this.isLastingCookie = isLastingCookie; return this; } /** * @return 是否在登录后将 token 写入到响应头 */ public Boolean getIsWriteHeader() { return isWriteHeader; } /** * @param isWriteHeader 是否在登录后将 token 写入到响应头 * @return 对象自身 */ public SaTokenConfig setIsWriteHeader(Boolean isWriteHeader) { this.isWriteHeader = isWriteHeader; return this; } /** * @return token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */ public String getTokenStyle() { return tokenStyle; } /** * @param tokenStyle token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) * @return 对象自身 */ public SaTokenConfig setTokenStyle(String tokenStyle) { this.tokenStyle = tokenStyle; return this; } /** * @return 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理 */ public int getDataRefreshPeriod() { return dataRefreshPeriod; } /** * @param dataRefreshPeriod 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理 * @return 对象自身 */ public SaTokenConfig setDataRefreshPeriod(int dataRefreshPeriod) { this.dataRefreshPeriod = dataRefreshPeriod; return this; } /** * @return 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录) */ public Boolean getTokenSessionCheckLogin() { return tokenSessionCheckLogin; } /** * @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录) * @return 对象自身 */ public SaTokenConfig setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) { this.tokenSessionCheckLogin = tokenSessionCheckLogin; return this; } /** * @return 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) */ public Boolean getAutoRenew() { return autoRenew; } /** * @param autoRenew 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) * @return 对象自身 */ public SaTokenConfig setAutoRenew(Boolean autoRenew) { this.autoRenew = autoRenew; return this; } /** * @return token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */ public String getTokenPrefix() { return tokenPrefix; } /** * @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) * @return 对象自身 */ public SaTokenConfig setTokenPrefix(String tokenPrefix) { this.tokenPrefix = tokenPrefix; return this; } /** * @return cookie 模式是否自动填充 token 前缀 */ public Boolean getCookieAutoFillPrefix() { return cookieAutoFillPrefix; } /** * @param cookieAutoFillPrefix cookie 模式是否自动填充 token 前缀 * @return 对象自身 */ public SaTokenConfig setCookieAutoFillPrefix(Boolean cookieAutoFillPrefix) { this.cookieAutoFillPrefix = cookieAutoFillPrefix; return this; } /** * @return 是否在初始化配置时在控制台打印版本字符画 */ public Boolean getIsPrint() { return isPrint; } /** * @param isPrint 是否在初始化配置时在控制台打印版本字符画 * @return 对象自身 */ public SaTokenConfig setIsPrint(Boolean isPrint) { this.isPrint = isPrint; return this; } /** * @return 是否打印操作日志 */ public Boolean getIsLog() { return isLog; } /** * @param isLog 是否打印操作日志 * @return 对象自身 */ public SaTokenConfig setIsLog(Boolean isLog) { this.isLog = isLog; return this; } /** * @return 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 */ public String getLogLevel() { return logLevel; } /** * @param logLevel 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 * @return 对象自身 */ public SaTokenConfig setLogLevel(String logLevel) { this.logLevel = logLevel; this.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel); return this; } /** * @return 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 */ public int getLogLevelInt() { return logLevelInt; } /** * @param logLevelInt 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 * @return 对象自身 */ public SaTokenConfig setLogLevelInt(int logLevelInt) { this.logLevelInt = logLevelInt; this.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt); return this; } /** * 获取:是否打印彩色日志 * * @return isColorLog 是否打印彩色日志 */ public Boolean getIsColorLog() { return this.isColorLog; } /** * 设置:是否打印彩色日志 * * @param isColorLog 是否打印彩色日志 * @return 对象自身 */ public SaTokenConfig setIsColorLog(Boolean isColorLog) { this.isColorLog = isColorLog; return this; } /** * @return jwt秘钥(只有集成 jwt 相关模块时此参数才会生效) */ public String getJwtSecretKey() { return jwtSecretKey; } /** * @param jwtSecretKey jwt秘钥(只有集成 jwt 相关模块时此参数才会生效) * @return 对象自身 */ public SaTokenConfig setJwtSecretKey(String jwtSecretKey) { this.jwtSecretKey = jwtSecretKey; return this; } /** * @return Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 */ public String getHttpBasic() { return httpBasic; } /** * @param httpBasic Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 * @return 对象自身 */ public SaTokenConfig setHttpBasic(String httpBasic) { this.httpBasic = httpBasic; return this; } /** * @return Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 */ public String getHttpDigest() { return httpDigest; } /** * @param httpDigest Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 * @return 对象自身 */ public SaTokenConfig setHttpDigest(String httpDigest) { this.httpDigest = httpDigest; return this; } /** * @return 配置当前项目的网络访问地址 */ public String getCurrDomain() { return currDomain; } /** * @param currDomain 配置当前项目的网络访问地址 * @return 对象自身 */ public SaTokenConfig setCurrDomain(String currDomain) { this.currDomain = currDomain; return this; } /** * @return Same-Token 的有效期 (单位: 秒) */ public long getSameTokenTimeout() { return sameTokenTimeout; } /** * @param sameTokenTimeout Same-Token 的有效期 (单位: 秒) * @return 对象自身 */ public SaTokenConfig setSameTokenTimeout(long sameTokenTimeout) { this.sameTokenTimeout = sameTokenTimeout; return this; } /** * @return 是否校验Same-Token(部分rpc插件有效) */ public Boolean getCheckSameToken() { return checkSameToken; } /** * @param checkSameToken 是否校验Same-Token(部分rpc插件有效) * @return 对象自身 */ public SaTokenConfig setCheckSameToken(Boolean checkSameToken) { this.checkSameToken = checkSameToken; return this; } /** * @return 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) */ public SaReplacedLoginExitMode getReplacedLoginExitMode() { return replacedLoginExitMode; } /** * @param replacedLoginExitMode 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) * @return 对象自身 */ public SaTokenConfig setReplacedLoginExitMode(SaReplacedLoginExitMode replacedLoginExitMode) { this.replacedLoginExitMode = replacedLoginExitMode; return this; } /** * 获取 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端 ALL_DEVICE_TYPE=所有设备类型端) * * @return / */ public SaReplacedRange getReplacedRange() { return this.replacedRange; } /** * 设置 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端 ALL_DEVICE_TYPE=所有设备类型端) * * @param replacedRange / * @return 对象自身 */ public SaTokenConfig setReplacedRange(SaReplacedRange replacedRange) { this.replacedRange = replacedRange; return this; } /** * 获取 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) * * @return / */ public SaLogoutMode getOverflowLogoutMode() { return this.overflowLogoutMode; } /** * 设置 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) * * @param overflowLogoutMode / * @return 对象自身 */ public SaTokenConfig setOverflowLogoutMode(SaLogoutMode overflowLogoutMode) { this.overflowLogoutMode = overflowLogoutMode; return this; } /** * 获取 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)
(此参数只在调用 StpUtil.logout() 时有效) * * @return / */ public SaLogoutRange getLogoutRange() { return this.logoutRange; } /** * 设置 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)
(此参数只在调用 StpUtil.logout() 时有效) * * @param logoutRange / * @return 对象自身 */ public SaTokenConfig setLogoutRange(SaLogoutRange logoutRange) { this.logoutRange = logoutRange; return this; } /** * 获取 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)
(此参数只在调用 StpUtil.[logoutkickoutreplaced]ByTokenValue("token") 时有效) * * @return isLogoutKeepFreezeOps / */ public Boolean getIsLogoutKeepFreezeOps() { return this.isLogoutKeepFreezeOps; } /** * 设置 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)
(此参数只在调用 StpUtil.[logoutkickoutreplaced]ByTokenValue("token") 时有效) * * @param isLogoutKeepFreezeOps / * @return 对象自身 */ public SaTokenConfig setIsLogoutKeepFreezeOps(Boolean isLogoutKeepFreezeOps) { this.isLogoutKeepFreezeOps = isLogoutKeepFreezeOps; return this; } /** * 获取 在注销 token 后,是否保留其对应的 Token-Session * * @return isLogoutKeepTokenSession / */ public Boolean getIsLogoutKeepTokenSession() { return this.isLogoutKeepTokenSession; } /** * 设置 在注销 token 后,是否保留其对应的 Token-Session * * @param isLogoutKeepTokenSession / * @return 对象自身 */ public SaTokenConfig setIsLogoutKeepTokenSession(Boolean isLogoutKeepTokenSession) { this.isLogoutKeepTokenSession = isLogoutKeepTokenSession; return this; } /** * 获取 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) * * @return / */ public Boolean getRightNowCreateTokenSession() { return this.rightNowCreateTokenSession; } /** * 设置 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) * * @param rightNowCreateTokenSession / * @return 对象自身 */ public SaTokenConfig setRightNowCreateTokenSession(Boolean rightNowCreateTokenSession) { this.rightNowCreateTokenSession = rightNowCreateTokenSession; return this; } /** * @return Cookie 全局配置对象 */ public SaCookieConfig getCookie() { return cookie; } /** * @param cookie Cookie 全局配置对象 * @return 对象自身 */ public SaTokenConfig setCookie(SaCookieConfig cookie) { this.cookie = cookie; return this; } @Override public String toString() { return "SaTokenConfig [" + "tokenName=" + tokenName + ", timeout=" + timeout + ", activeTimeout=" + activeTimeout + ", dynamicActiveTimeout=" + dynamicActiveTimeout + ", isConcurrent=" + isConcurrent + ", isShare=" + isShare + ", replacedRange=" + replacedRange + ", replacedLoginExitMode=" + replacedLoginExitMode + ", maxLoginCount=" + maxLoginCount + ", overflowLogoutMode=" + overflowLogoutMode + ", maxTryTimes=" + maxTryTimes + ", isReadBody=" + isReadBody + ", isReadHeader=" + isReadHeader + ", isReadCookie=" + isReadCookie + ", isLastingCookie=" + isLastingCookie + ", isWriteHeader=" + isWriteHeader + ", logoutRange=" + logoutRange + ", isLogoutKeepFreezeOps=" + isLogoutKeepFreezeOps + ", isLogoutKeepTokenSession=" + isLogoutKeepTokenSession + ", rightNowCreateTokenSession=" + rightNowCreateTokenSession + ", tokenStyle=" + tokenStyle + ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin=" + tokenSessionCheckLogin + ", autoRenew=" + autoRenew + ", tokenPrefix=" + tokenPrefix + ", cookieAutoFillPrefix=" + cookieAutoFillPrefix + ", isPrint=" + isPrint + ", isLog=" + isLog + ", logLevel=" + logLevel + ", logLevelInt=" + logLevelInt + ", isColorLog=" + isColorLog + ", jwtSecretKey=" + jwtSecretKey + ", httpBasic=" + httpBasic + ", httpDigest=" + httpDigest + ", currDomain=" + currDomain + ", sameTokenTimeout=" + sameTokenTimeout + ", checkSameToken=" + checkSameToken + ", cookie=" + cookie + "]"; } // ------------------- 过期方法 ------------------- /** *

请更改为 getActiveTimeout()

* @return token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) */ @Deprecated public long getActivityTimeout() { // System.err.println("配置项已过期,请更换:sa-token.activity-timeout -> sa-token.active-timeout"); return activeTimeout; } /** *

请更改为 setActiveTimeout()

* @param activityTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 * (例如可以设置为 1800 代表 30 分钟内无操作就冻结) * @return 对象自身 */ @Deprecated public SaTokenConfig setActivityTimeout(long activityTimeout) { System.err.println("配置项已过期,请更换:sa-token.activity-timeout -> sa-token.active-timeout"); this.activeTimeout = activityTimeout; return this; } /** *

请更改为 getHttpBasic()

* @return Http Basic 认证的默认账号和密码 */ @Deprecated public String getBasic() { return httpBasic; } /** *

请更改为 setHttpBasic()

* @param basic Http Basic 认证的默认账号和密码 * @return 对象自身 */ @Deprecated public SaTokenConfig setBasic(String basic) { this.httpBasic = basic; return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfigFactory.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.config; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import java.util.Properties; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; /** * Sa-Token配置文件的构建工厂类 * *

用于手动读取配置文件初始化 SaTokenConfig 对象,只有在非IOC环境下你才会用到此类

* * @author click33 * @since 1.10.0 */ public class SaTokenConfigFactory { private SaTokenConfigFactory() { } /** * 配置文件地址 */ public static String configPath = "sa-token.properties"; /** * 根据 configPath 路径获取配置信息 * * @return 一个SaTokenConfig对象 */ public static SaTokenConfig createConfig() { return createConfig(configPath); } /** * 根据指定路径路径获取配置信息 * * @param path 配置文件路径 * @return 一个 SaTokenConfig 对象 */ public static SaTokenConfig createConfig(String path) { Map map = readPropToMap(path); // if (map == null) { // throw new RuntimeException("找不到配置文件:" + configPath, null); // } return (SaTokenConfig) initPropByMap(map, new SaTokenConfig()); } /** * 工具方法: 将指定路径的properties配置文件读取到Map中 * * @param propertiesPath 配置文件地址 * @return 一个Map */ private static Map readPropToMap(String propertiesPath) { Map map = new HashMap<>(16); try { InputStream is = SaTokenConfigFactory.class.getClassLoader().getResourceAsStream(propertiesPath); if (is == null) { return null; } Properties prop = new Properties(); prop.load(is); for (String key : prop.stringPropertyNames()) { map.put(key, prop.getProperty(key)); } } catch (IOException e) { throw new SaTokenException("配置文件(" + propertiesPath + ")加载失败", e).setCode(SaErrorCode.CODE_10021); } return map; } /** * 工具方法: 将 Map 的值映射到一个 Model 上 * * @param map 属性集合 * @param obj 对象, 或类型 * @return 返回实例化后的对象 */ private static Object initPropByMap(Map map, Object obj) { if (map == null) { map = new HashMap<>(16); } // 1、取出类型 Class cs; if (obj instanceof Class) { // 如果是一个类型,则将obj=null,以便完成静态属性反射赋值 cs = (Class) obj; obj = null; } else { // 如果是一个对象,则取出其类型 cs = obj.getClass(); } // 2、遍历类型属性,反射赋值 for (Field field : cs.getDeclaredFields()) { String value = map.get(field.getName()); if (value == null) { // 如果为空代表没有配置此项 continue; } try { Object valueConvert = SaFoxUtil.getValueByType(value, field.getType()); field.setAccessible(true); field.set(obj, valueConvert); } catch (IllegalArgumentException | IllegalAccessException e) { throw new SaTokenException("属性赋值出错:" + field.getName(), e).setCode(SaErrorCode.CODE_10022); } } return obj; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/SaHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.application.SaApplication; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; /** * Sa-Token 上下文持有类,你可以通过此类快速获取当前环境下的 SaRequest、SaResponse、SaStorage、SaApplication 对象。 * * @author click33 * @since 1.18.0 */ public class SaHolder { /** * 获取当前请求的 SaTokenContext 上下文对象 * @see SaTokenContext * * @return / */ public static SaTokenContext getContext() { return SaManager.getSaTokenContext(); } /** * 获取当前请求的 Request 包装对象 * @see SaRequest * * @return / */ public static SaRequest getRequest() { return SaManager.getSaTokenContext().getRequest(); } /** * 获取当前请求的 Response 包装对象 * @see SaResponse * * @return / */ public static SaResponse getResponse() { return SaManager.getSaTokenContext().getResponse(); } /** * 获取当前请求的 Storage 包装对象 * @see SaStorage * * @return / */ public static SaStorage getStorage() { return SaManager.getSaTokenContext().getStorage(); } /** * 获取全局 SaApplication 对象 * @see SaApplication * * @return / */ public static SaApplication getApplication() { return SaApplication.defaultInstance; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContext.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; /** * Sa-Token 上下文处理器 * *

上下文处理器封装了当前应用环境的底层操作,是 Sa-Token 对接不同 web 框架的关键,详细可参考在线文档 “自定义 SaTokenContext 指南”章节

* * @author click33 * @since 1.16.0 */ public interface SaTokenContext { /** * 初始化上下文 * * @param req / * @param res / * @param stg / */ void setContext(SaRequest req, SaResponse res, SaStorage stg); /** * 清除化上下文 */ void clearContext(); /** * 判断当前上下文是否可用 * * @return / */ boolean isValid(); /** * 获取 Box 对象 */ SaTokenContextModelBox getModelBox(); /** * 获取当前上下文的 Request 包装对象 * @see SaRequest * * @return / */ default SaRequest getRequest() { return getModelBox().getRequest(); } /** * 获取当前上下文的 Response 包装对象 * @see SaResponse * * @return / */ default SaResponse getResponse(){ return getModelBox().getResponse(); } /** * 获取当前上下文的 Storage 包装对象 * @see SaStorage * * @return / */ default SaStorage getStorage(){ return getModelBox().getStorage(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenContextException; /** * Sa-Token 上下文处理器 [ 默认实现类 ] * *

* 一般情况下框架会为你自动注入合适的上下文处理器,如果代码断点走到了此默认实现类, * 说明你引入的依赖有问题或者错误的调用了 Sa-Token 的API, 请在 [ 在线开发文档 → 附录 → 常见问题排查 ] 中按照提示进行排查 *

* * @author click33 * @since 1.16.0 */ public class SaTokenContextDefaultImpl implements SaTokenContext { /** * 默认的上下文处理器对象 */ public static SaTokenContextDefaultImpl defaultContext = new SaTokenContextDefaultImpl(); /** * 错误提示语 */ public static final String ERROR_MESSAGE = "未能获取有效的上下文处理器"; @Override public void setContext(SaRequest req, SaResponse res, SaStorage stg) { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } @Override public void clearContext() { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } @Override public boolean isValid() { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } @Override public SaTokenContextModelBox getModelBox() { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } @Override public SaRequest getRequest() { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } @Override public SaResponse getResponse() { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } @Override public SaStorage getStorage() { throw new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForReadOnly.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; /** * Sa-Token 上下文处理器次级实现:只读上下文 * * @author click33 * @since 1.42.0 */ public interface SaTokenContextForReadOnly extends SaTokenContext { @Override default void setContext(SaRequest req, SaResponse res, SaStorage stg) { } @Override default void clearContext() { } @Override default SaTokenContextModelBox getModelBox() { return null; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocal.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; /** * Sa-Token 上下文处理器 [ ThreadLocal 版本 ] * *

* 使用 [ ThreadLocal 版本 ] 上下文处理器需要在全局过滤器或者拦截器内率先调用 * SaTokenContextForThreadLocalStaff.setBox(req, res, sto) 初始化上下文 *

* *

一般情况下你不需要直接操作此类,因为框架的 starter 集成包里已经封装了完整的上下文操作

* * @author click33 * @since 1.16.0 */ public class SaTokenContextForThreadLocal implements SaTokenContext { @Override public void setContext(SaRequest req, SaResponse res, SaStorage stg) { SaTokenContextForThreadLocalStaff.setModelBox(req, res, stg); } @Override public void clearContext() { SaTokenContextForThreadLocalStaff.clearModelBox(); } @Override public boolean isValid() { return SaTokenContextForThreadLocalStaff.getModelBoxOrNull() != null; } @Override public SaTokenContextModelBox getModelBox() { return SaTokenContextForThreadLocalStaff.getModelBox(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenContextException; /** * Sa-Token 上下文处理器 [ThreadLocal 版本] ---- 对象存储器 * *

一般情况下你不需要直接操作此类,因为框架的 starter 集成包里已经封装了完整的上下文操作

* * @author click33 * @since 1.16.0 */ public class SaTokenContextForThreadLocalStaff { /** * 基于 ThreadLocal 的 [ Box 存储器 ] */ public static ThreadLocal modelBoxThreadLocal = new ThreadLocal<>(); /** * 初始化当前线程的 [ Box 存储器 ] * @param request {@link SaRequest} * @param response {@link SaResponse} * @param storage {@link SaStorage} */ public static void setModelBox(SaRequest request, SaResponse response, SaStorage storage) { SaTokenContextModelBox bok = new SaTokenContextModelBox(request, response, storage); modelBoxThreadLocal.set(bok); } /** * 清除当前线程的 [ Box 存储器 ] */ public static void clearModelBox() { modelBoxThreadLocal.remove(); } /** * 获取当前线程的 [ Box 存储器 ] * @return / */ public static SaTokenContextModelBox getModelBoxOrNull() { return modelBoxThreadLocal.get(); } /** * 获取当前线程的 [ Box 存储器 ], 如果为空则抛出异常 * @return / */ public static SaTokenContextModelBox getModelBox() { SaTokenContextModelBox box = modelBoxThreadLocal.get(); if(box == null) { throw new SaTokenContextException("SaTokenContext 上下文尚未初始化").setCode(SaErrorCode.CODE_10002); } return box; } /** * 在当前线程的 SaRequest 包装对象 * * @return / */ public static SaRequest getRequest() { return getModelBox().getRequest(); } /** * 在当前线程的 SaResponse 包装对象 * * @return / */ public static SaResponse getResponse() { return getModelBox().getResponse(); } /** * 在当前线程的 SaStorage 存储器包装对象 * * @return / */ public static SaStorage getStorage() { return getModelBox().getStorage(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaRequestForMock.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.mock; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; /** * 对 SaRequest 包装类的实现(Mock 版) * * @author click33 * @since 1.42.0 */ public class SaRequestForMock implements SaRequest { public Map parameterMap = new LinkedHashMap<>(); public Map headerMap = new LinkedHashMap<>(); public Map cookieMap = new LinkedHashMap<>(); public String requestPath; public String url; public String method; public String host; public String forwardTo; /** * 获取底层源对象 */ @Override public Object getSource() { return null; } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { return parameterMap.get(name); } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return parameterMap.keySet(); } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ return parameterMap; } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { return headerMap.get(name); } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { return getCookieLastValue(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ return cookieMap.get(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ return cookieMap.get(name); } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { return ApplicationInfo.cutPathPrefix(requestPath); } /** * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { return url; } /** * 返回当前请求的类型 */ @Override public String getMethod() { return method; } /** * 查询请求 host */ @Override public String getHost() { return host; } /** * 转发请求 */ @Override public Object forward(String path) { this.forwardTo = path; return null; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaResponseForMock.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.mock; import cn.dev33.satoken.context.model.SaResponse; import java.util.LinkedHashMap; import java.util.Map; /** * 对 SaResponse 包装类的实现(Mock 版) * * @author click33 * @since 1.42.0 */ public class SaResponseForMock implements SaResponse { public int status; public Map headerMap = new LinkedHashMap<>(); public String redirectTo; /** * 获取底层源对象 */ @Override public Object getSource() { return null; } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { this.status = sc; return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { headerMap.put(name, value); return this; } /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { headerMap.put(name, value); return this; } /** * 重定向 */ @Override public Object redirect(String url) { this.redirectTo = url; return null; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaStorageForMock.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.mock; import cn.dev33.satoken.context.model.SaStorage; import java.util.LinkedHashMap; import java.util.Map; /** * 对 SaStorage 包装类的实现(Mock 版) * * @author click33 * @since 1.42.0 */ public class SaStorageForMock implements SaStorage { public Map dataMap = new LinkedHashMap<>(); /** * 获取底层源对象 */ @Override public Object getSource() { return null; } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorageForMock set(String key, Object value) { dataMap.put(key, value); return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return dataMap.get(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorageForMock delete(String key) { dataMap.remove(key); return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaTokenContextMockUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.mock; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaRetGenericFunction; /** * Sa-Token Mock 上下文 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextMockUtil { /** * 写入 Mock 上下文 */ public static void setMockContext() { SaRequestForMock request = new SaRequestForMock(); SaResponseForMock response = new SaResponseForMock(); SaStorageForMock storage = new SaStorageForMock(); SaManager.getSaTokenContext().setContext(request, response, storage); } /** * 写入 Mock 上下文,并执行一段代码,执行完毕后清除上下文 * * @param fun / */ public static void setMockContext(SaFunction fun) { try { setMockContext(); fun.run(); } finally { clearContext(); } } /** * 写入 Mock 上下文,并执行一段代码,执行完毕后清除上下文 * * @param fun / */ public static T setMockContext(SaRetGenericFunction fun) { try { setMockContext(); return fun.run(); } finally { clearContext(); } } /** * 清除上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaCookie.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.model; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; /** * Cookie Model,代表一个 Cookie 应该具有的所有参数 * * @author click33 * @since 1.16.0 */ public class SaCookie { /** * 写入响应头时使用的key */ public static final String HEADER_NAME = "Set-Cookie"; /** * 名称 */ private String name; /** * 值 */ private String value; /** * 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除 */ private int maxAge = -1; /** * 域 */ private String domain; /** * 路径 */ private String path; /** * 是否只在 https 协议下有效 */ private Boolean secure = false; /** * 是否禁止 js 操作 Cookie */ private Boolean httpOnly = false; /** * 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) */ private String sameSite; // Cookie 属性参考文章:https://blog.csdn.net/fengbin2005/article/details/136544226 /** * 额外扩展属性 */ private Map extraAttrs = new LinkedHashMap<>(); /** * 构造一个 */ public SaCookie() { } /** * 构造一个 * @param name 名字 * @param value 值 */ public SaCookie(String name, String value) { this.name = name; this.value = value; } /** * @return 名称 */ public String getName() { return name; } /** * @param name 名称 * @return 对象自身 */ public SaCookie setName(String name) { this.name = name; return this; } /** * @return 值 */ public String getValue() { return value; } /** * @param value 值 * @return 对象自身 */ public SaCookie setValue(String value) { this.value = value; return this; } /** * @return 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除 */ public int getMaxAge() { return maxAge; } /** * @param maxAge 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除 * @return 对象自身 */ public SaCookie setMaxAge(int maxAge) { this.maxAge = maxAge; return this; } /** * @return 域 */ public String getDomain() { return domain; } /** * @param domain 域 * @return 对象自身 */ public SaCookie setDomain(String domain) { this.domain = domain; return this; } /** * @return 路径 */ public String getPath() { return path; } /** * @param path 路径 * @return 对象自身 */ public SaCookie setPath(String path) { this.path = path; return this; } /** * @return 是否只在 https 协议下有效 */ public Boolean getSecure() { return secure; } /** * @param secure 是否只在 https 协议下有效 * @return 对象自身 */ public SaCookie setSecure(Boolean secure) { this.secure = secure; return this; } /** * @return 是否禁止 js 操作 Cookie */ public Boolean getHttpOnly() { return httpOnly; } /** * @param httpOnly 是否禁止 js 操作 Cookie * @return 对象自身 */ public SaCookie setHttpOnly(Boolean httpOnly) { this.httpOnly = httpOnly; return this; } /** * @return 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) */ public String getSameSite() { return sameSite; } /** * @param sameSite 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) * @return 对象自身 */ public SaCookie setSameSite(String sameSite) { this.sameSite = sameSite; return this; } /** * @return 获取额外扩展属性 */ public Map getExtraAttrs() { return extraAttrs; } /** * 写入额外扩展属性 * @param extraAttrs / * @return 对象自身 */ public SaCookie setExtraAttrs(Map extraAttrs) { this.extraAttrs = extraAttrs; return this; } /** * 追加扩展属性 * @param name / * @param value / * @return 对象自身 */ public SaCookie addExtraAttr(String name, String value) { if (extraAttrs == null) { extraAttrs = new LinkedHashMap<>(); } this.extraAttrs.put(name, value); return this; } /** * 追加扩展属性 * @param name / * @return 对象自身 */ public SaCookie addExtraAttr(String name) { return this.addExtraAttr(name, null); } /** * 移除指定扩展属性 * @param name / * @return 对象自身 */ public SaCookie removeExtraAttr(String name) { if(extraAttrs != null) { this.extraAttrs.remove(name); } return this; } // toString @Override public String toString() { return "SaCookie [" + "name=" + name + ", value=" + value + ", maxAge=" + maxAge + ", domain=" + domain + ", path=" + path + ", secure=" + secure + ", httpOnly=" + httpOnly + ", sameSite=" + sameSite + ", extraAttrs=" + extraAttrs + "]"; } /** * 构建一下 */ public void builder() { if(path == null) { path = "/"; } } /** * 转换为响应头 Set-Cookie 参数需要的值 * @return / */ public String toHeaderValue() { this.builder(); if(SaFoxUtil.isEmpty(name)) { throw new SaTokenException("name不能为空").setCode(SaErrorCode.CODE_12002); } if(value != null && value.contains(";")) { throw new SaTokenException("无效Value:" + value).setCode(SaErrorCode.CODE_12003); } // example: // Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax StringBuilder sb = new StringBuilder(); sb.append(name).append("=").append(value); if(maxAge >= 0) { sb.append("; Max-Age=").append(maxAge); String expires; if(maxAge == 0) { expires = Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME); } else { expires = OffsetDateTime.now().plusSeconds(maxAge).format(DateTimeFormatter.RFC_1123_DATE_TIME); } sb.append("; Expires=").append(expires); } if(!SaFoxUtil.isEmpty(domain)) { sb.append("; Domain=").append(domain); } if(!SaFoxUtil.isEmpty(path)) { sb.append("; Path=").append(path); } if(secure) { sb.append("; Secure"); } if(httpOnly) { sb.append("; HttpOnly"); } if(!SaFoxUtil.isEmpty(sameSite)) { sb.append("; SameSite=").append(sameSite); } // 扩展属性 if(extraAttrs != null) { extraAttrs.forEach((k, v) -> { if(SaFoxUtil.isEmpty(v)) { sb.append("; ").append(k); } else { sb.append("; ").append(k).append("=").append(v); } }); } return sb.toString(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.model; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.util.SaFoxUtil; import java.util.Collection; import java.util.Map; /** * Request 请求对象 包装类 * * @author click33 * @since 1.16.0 */ public interface SaRequest { /** * 获取底层被包装的源对象 * @return / */ Object getSource(); /** * 在 [ 请求体 ] 里获取一个参数值 * @param name 键 * @return 值 */ String getParam(String name); /** * 在 [ 请求体 ] 里获取一个参数值,值为空时返回默认值 * @param name 键 * @param defaultValue 值为空时的默认值 * @return 值 */ default String getParam(String name, String defaultValue) { String value = getParam(name); if(SaFoxUtil.isEmpty(value)) { return defaultValue; } return value; } /** * 在 [ 请求体 ] 里检测提供的参数是否为指定值 * @param name 键 * @param value 值 * @return 是否相等 */ default boolean isParam(String name, String value) { String paramValue = getParam(name); return SaFoxUtil.isNotEmpty(paramValue) && paramValue.equals(value); } /** * 在 [ 请求体 ] 里检测请求是否提供了指定参数 * @param name 参数名称 * @return 是否提供 */ default boolean hasParam(String name) { return SaFoxUtil.isNotEmpty(getParam(name)); } /** * 在 [ 请求体 ] 里获取一个值 (此值必须存在,否则抛出异常 ) * @param name 键 * @return 参数值 */ default String getParamNotNull(String name) { String paramValue = getParam(name); if(SaFoxUtil.isEmpty(paramValue)) { throw new SaTokenException("缺少参数:" + name).setCode(SaErrorCode.CODE_12001); } return paramValue; } /** * 获取 [ 请求体 ] 里提交的所有参数名称 * @return 参数名称列表 */ Collection getParamNames(); /** * 获取 [ 请求体 ] 里提交的所有参数 * @return 参数列表 */ Map getParamMap(); /** * 在 [ 请求头 ] 里获取一个值 * @param name 键 * @return 值 */ String getHeader(String name); /** * 在 [ 请求头 ] 里获取一个值 * @param name 键 * @param defaultValue 值为空时的默认值 * @return 值 */ default String getHeader(String name, String defaultValue) { String value = getHeader(name); if(SaFoxUtil.isEmpty(value)) { return defaultValue; } return value; } /** * 在 [ Cookie作用域 ] 里获取一个值 * @param name 键 * @return 值 */ String getCookieValue(String name); /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) * @param name 键 * @return 值 */ String getCookieFirstValue(String name); /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ String getCookieLastValue(String name); /** * 返回当前请求path (不包括上下文名称) * @return / */ String getRequestPath(); /** * 返回当前请求 path 是否为指定值 * @param path path * @return / */ default boolean isPath(String path) { return getRequestPath().equals(path); } /** * 返回当前请求的url,不带query参数,例:http://xxx.com/test * @return / */ String getUrl(); /** * 返回当前请求的类型 * @return / */ String getMethod(); /** * 返回当前请求 Method 是否为指定值 * @param method method * @return / */ default boolean isMethod(String method) { return getMethod().equals(method); } /** * 返回当前请求 Method 是否为指定值 * @param method method * @return / */ default boolean isMethod(SaHttpMethod method) { return getMethod().equals(method.name()); } /** * 查询请求 host * @return / */ String getHost(); /** * 判断此请求是否为 Ajax 异步请求 * @return / */ default boolean isAjax() { return "XMLHttpRequest".equalsIgnoreCase(getHeader("X-Requested-With")) || isParam("_ajax", "true"); } /** * 转发请求 * @param path 转发地址 * @return 任意值 */ Object forward(String path); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.model; /** * Response 响应对象 包装类 * * @author click33 * @since 1.16.0 */ public interface SaResponse { /** * 指定前端可以获取到哪些响应头时使用的参数名 */ String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; /** * 获取底层被包装的源对象 * @return / */ Object getSource(); /** * 删除指定Cookie * @param name Cookie名称 */ default void deleteCookie(String name) { addCookie(name, null, null, null, 0); } /** * 删除指定Cookie * @param name Cookie名称 * @param path Cookie 路径 * @param domain Cookie 作用域 */ default void deleteCookie(String name, String path, String domain) { addCookie(name, null, path, domain, 0); } /** * 写入指定Cookie * @param name Cookie名称 * @param value Cookie值 * @param path Cookie路径 * @param domain Cookie的作用域 * @param timeout 过期时间 (秒) */ default void addCookie(String name, String value, String path, String domain, int timeout) { this.addCookie(new SaCookie(name, value).setPath(path).setDomain(domain).setMaxAge(timeout)); } /** * 写入指定Cookie * @param cookie Cookie-Model */ default void addCookie(SaCookie cookie) { this.addHeader(SaCookie.HEADER_NAME, cookie.toHeaderValue()); } /** * 设置响应状态码 * @param sc 响应状态码 * @return 对象自身 */ SaResponse setStatus(int sc); /** * 在响应头里写入一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ SaResponse setHeader(String name, String value); /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ SaResponse addHeader(String name, String value); /** * 在响应头写入 [Server] 服务器名称 * @param value 服务器名称 * @return 对象自身 */ default SaResponse setServer(String value) { return this.setHeader("Server", value); } /** * 重定向 * @param url 重定向地址 * @return 任意值 */ Object redirect(String url); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.model; import cn.dev33.satoken.application.SaSetValueInterface; /** * Storage Model,请求作用域的读取值对象。 * *

在一次请求范围内: 存值、取值。数据在请求结束后失效。 * * @author click33 * @since 1.16.0 */ public interface SaStorage extends SaSetValueInterface { /** * 获取底层被包装的源对象 * @return / */ Object getSource(); // ---- 实现接口存取值方法 /** 取值 */ @Override Object get(String key); /** 写值 */ @Override SaStorage set(String key, Object value); /** 删值 */ @Override SaStorage delete(String key); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaTokenContextModelBox.java ================================================ package cn.dev33.satoken.context.model; /** * Box 盒子类,用于存储 [ SaRequest、SaResponse、SaStorage ] 三个包装对象 * * @author click33 * @since 1.16.0 */ public class SaTokenContextModelBox { public SaRequest request; public SaResponse response; public SaStorage storage; public SaTokenContextModelBox(SaRequest request, SaResponse response, SaStorage storage) { this.request = request; this.response = response; this.storage = storage; } public SaRequest getRequest() { return request; } public void setRequest(SaRequest request) { this.request = request; } public SaResponse getResponse() { return response; } public void setResponse(SaResponse response) { this.response = response; } public SaStorage getStorage() { return storage; } public void setStorage(SaStorage storage) { this.storage = storage; } @Override public String toString() { return "Box [request=" + request + ", response=" + response + ", storage=" + storage + "]"; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/context/model/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * 因为不能确定最终运行的 web 容器属于标准 Servlet 模型还是非 Servlet 模型,特封装此包下的包装类进行对接。 * 调用路径为:Sa-Token 功能函数 -> SaRequest 封装接口 -> SaRequest 具体实现类。 */ package cn.dev33.satoken.context.model; ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.session.SaSession; import java.util.List; /** * Sa-Token 持久层接口 * *

* 此接口的不同实现类可将数据存储至不同位置,如:内存Map、Redis 等等。 * 如果你要自定义数据存储策略,也需通过实现此接口来完成。 *

* * @author click33 * @since 1.10.0 */ public interface SaTokenDao { /** 常量,表示一个 key 永不过期 (在一个 key 被标注为永远不过期时返回此值) */ long NEVER_EXPIRE = -1; /** 常量,表示系统中不存在这个缓存(在对不存在的 key 获取剩余存活时间时返回此值) */ long NOT_VALUE_EXPIRE = -2; // --------------------- 字符串读写 --------------------- /** * 获取 value,如无返空 * * @param key 键名称 * @return value */ String get(String key); /** * 写入 value,并设定存活时间(单位: 秒) * * @param key 键名称 * @param value 值 * @param timeout 数据有效期(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ void set(String key, String value, long timeout); /** * 更新 value (过期时间不变) * @param key 键名称 * @param value 值 */ void update(String key, String value); /** * 删除 value * @param key 键名称 */ void delete(String key); /** * 获取 value 的剩余存活时间(单位: 秒) * @param key 指定 key * @return 这个 key 的剩余存活时间 */ long getTimeout(String key); /** * 修改 value 的剩余存活时间(单位: 秒) * @param key 指定 key * @param timeout 过期时间(单位: 秒) */ void updateTimeout(String key, long timeout); // --------------------- 对象读写 --------------------- /** * 获取 Object,如无返空 * * @param key 键名称 * @return object */ Object getObject(String key); /** * 获取 Object (指定反序列化类型),如无返空 * * @param key 键名称 * @return object */ T getObject(String key, Class classType); /** * 写入 Object,并设定存活时间 (单位: 秒) * * @param key 键名称 * @param object 值 * @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ void setObject(String key, Object object, long timeout); /** * 更新 Object (过期时间不变) * @param key 键名称 * @param object 值 */ void updateObject(String key, Object object); /** * 删除 Object * @param key 键名称 */ void deleteObject(String key); /** * 获取 Object 的剩余存活时间 (单位: 秒) * @param key 指定 key * @return 这个 key 的剩余存活时间 */ long getObjectTimeout(String key); /** * 修改 Object 的剩余存活时间(单位: 秒) * @param key 指定 key * @param timeout 剩余存活时间 */ void updateObjectTimeout(String key, long timeout); // --------------------- SaSession 读写 (默认复用 Object 读写方法) --------------------- /** * 获取 SaSession,如无返空 * @param sessionId sessionId * @return SaSession */ SaSession getSession(String sessionId); /** * 写入 SaSession,并设定存活时间(单位: 秒) * @param session 要保存的 SaSession 对象 * @param timeout 过期时间(单位: 秒) */ void setSession(SaSession session, long timeout); /** * 更新 SaSession * @param session 要更新的 SaSession 对象 */ void updateSession(SaSession session); /** * 删除 SaSession * @param sessionId sessionId */ void deleteSession(String sessionId); /** * 获取 SaSession 剩余存活时间(单位: 秒) * @param sessionId 指定 SaSession * @return 这个 SaSession 的剩余存活时间 */ long getSessionTimeout(String sessionId); /** * 修改 SaSession 剩余存活时间(单位: 秒) * @param sessionId 指定 SaSession * @param timeout 剩余存活时间 */ void updateSessionTimeout(String sessionId, long timeout); // --------------------- 会话管理 --------------------- /** * 搜索数据 * @param prefix 前缀 * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表从 start 处一直取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return 查询到的数据集合 */ List searchData(String prefix, String keyword, int start, int size, boolean sortType); // --------------------- 生命周期 --------------------- /** * 当此 SaTokenDao 实例被装载时触发 */ default void init() { } /** * 当此 SaTokenDao 实例被卸载时触发 */ default void destroy() { } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDaoDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject; import cn.dev33.satoken.dao.timedcache.SaMapPackageForConcurrentHashMap; import cn.dev33.satoken.dao.timedcache.SaTimedCache; import cn.dev33.satoken.util.SaFoxUtil; import java.util.List; /** * Sa-Token 持久层接口,默认实现类,基于 SaTimedCache - ConcurrentHashMap (内存缓存,系统重启后数据丢失) * * @author click33 * @since 1.10.0 */ public class SaTokenDaoDefaultImpl implements SaTokenDaoByStringFollowObject { public SaTimedCache timedCache = new SaTimedCache( new SaMapPackageForConcurrentHashMap<>(), new SaMapPackageForConcurrentHashMap<>() ); // ------------------------ Object 读写操作 @Override public Object getObject(String key) { return timedCache.getObject(key); } @Override @SuppressWarnings("unchecked") public T getObject(String key, Class classType){ return (T) getObject(key); } @Override public void setObject(String key, Object object, long timeout) { timedCache.setObject(key, object, timeout); } @Override public void updateObject(String key, Object object) { timedCache.updateObject(key, object); } @Override public void deleteObject(String key) { timedCache.deleteObject(key); } @Override public long getObjectTimeout(String key) { return timedCache.getObjectTimeout(key); } @Override public void updateObjectTimeout(String key, long timeout) { timedCache.updateObjectTimeout(key, timeout); } // --------- 会话管理 @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { return SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType); } // --------- 组件生命周期 /** * 组件被安装时,开始刷新数据线程 */ @Override public void init() { timedCache.initRefreshThread(); } /** * 组件被卸载时,结束定时任务,不再定时清理过期数据 */ @Override public void destroy() { timedCache.endRefreshThread(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByObjectFollowString.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.auto; import cn.dev33.satoken.SaManager; /** * SaTokenDao 次级实现,Object 读写跟随 String 读写 (推荐中间件型缓存实现 implements 此接口) * * @author click33 * @since 1.41.0 */ public interface SaTokenDaoByObjectFollowString extends SaTokenDaoBySessionFollowObject { // --------------------- Object 读写 --------------------- /** * 获取 Object,如无返空 * * @param key 键名称 * @return object */ @Override default Object getObject(String key) { String jsonString = get(key); return SaManager.getSaSerializerTemplate().stringToObject(jsonString); } /** * 获取 Object (指定反序列化类型),如无返空 * * @param key 键名称 * @return object */ default T getObject(String key, Class classType) { String jsonString = get(key); return SaManager.getSaSerializerTemplate().stringToObject(jsonString, classType); } /** * 写入 Object,并设定存活时间 (单位: 秒) * * @param key 键名称 * @param object 值 * @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ @Override default void setObject(String key, Object object, long timeout) { String jsonString = SaManager.getSaSerializerTemplate().objectToString(object); set(key, jsonString, timeout); } /** * 更新 Object (过期时间不变) * @param key 键名称 * @param object 值 */ @Override default void updateObject(String key, Object object) { String jsonString = SaManager.getSaSerializerTemplate().objectToString(object); update(key, jsonString); } /** * 删除 Object * @param key 键名称 */ @Override default void deleteObject(String key) { delete(key); } /** * 获取 Object 的剩余存活时间 (单位: 秒) * @param key 指定 key * @return 这个 key 的剩余存活时间 */ @Override default long getObjectTimeout(String key) { return getTimeout(key); } /** * 修改 Object 的剩余存活时间(单位: 秒) * @param key 指定 key * @param timeout 剩余存活时间 */ @Override default void updateObjectTimeout(String key, long timeout) { updateTimeout(key, timeout); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoBySessionFollowObject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.auto; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.strategy.SaStrategy; /** * SaTokenDao 次级实现:SaSession 读写跟随 Object 读写 * * @author click33 * @since 1.41.0 */ public interface SaTokenDaoBySessionFollowObject extends SaTokenDao { // --------------------- SaSession 读写 (默认复用 Object 读写方法) --------------------- /** * 获取 SaSession,如无返空 * @param sessionId sessionId * @return SaSession */ default SaSession getSession(String sessionId) { return getObject(sessionId, SaStrategy.instance.sessionClassType); } /** * 写入 SaSession,并设定存活时间(单位: 秒) * @param session 要保存的 SaSession 对象 * @param timeout 过期时间(单位: 秒) */ default void setSession(SaSession session, long timeout) { setObject(session.getId(), session, timeout); } /** * 更新 SaSession * @param session 要更新的 SaSession 对象 */ default void updateSession(SaSession session) { updateObject(session.getId(), session); } /** * 删除 SaSession * @param sessionId sessionId */ default void deleteSession(String sessionId) { deleteObject(sessionId); } /** * 获取 SaSession 剩余存活时间(单位: 秒) * @param sessionId 指定 SaSession * @return 这个 SaSession 的剩余存活时间 */ default long getSessionTimeout(String sessionId) { return getObjectTimeout(sessionId); } /** * 修改 SaSession 剩余存活时间(单位: 秒) * @param sessionId 指定 SaSession * @param timeout 剩余存活时间 */ default void updateSessionTimeout(String sessionId, long timeout) { updateObjectTimeout(sessionId, timeout); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByStringFollowObject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.auto; /** * SaTokenDao 次级实现:String 读写跟随 Object 读写 (推荐内存型缓存实现 implements 此接口) * * @author click33 * @since 1.41.0 */ public interface SaTokenDaoByStringFollowObject extends SaTokenDaoBySessionFollowObject { // --------------------- String 读写 --------------------- @Override default String get(String key) { return (String) getObject(key); } @Override default void set(String key, String value, long timeout) { setObject(key, value, timeout); } @Override default void update(String key, String value) { updateObject(key, value); } @Override default void delete(String key) { deleteObject(key); } @Override default long getTimeout(String key) { return getObjectTimeout(key); } @Override default void updateTimeout(String key, long timeout) { updateObjectTimeout(key, timeout); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackage.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.timedcache; import java.util.Set; /** * Map 包装类 * * @author click33 * @since 1.41.0 */ public interface SaMapPackage { /** * 获取底层被包装的源对象 * * @return / */ Object getSource(); /** * 读 * * @param key / * @return / */ V get(String key); /** * 写 * * @param key / * @param value / */ void put(String key, V value); /** * 删 * @param key / */ void remove(String key); /** * 所有 key */ Set keySet(); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackageForConcurrentHashMap.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.timedcache; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * Map 包装类 (ConcurrentHashMap 版) * * @author click33 * @since 1.41.0 */ public class SaMapPackageForConcurrentHashMap implements SaMapPackage { private final ConcurrentHashMap map = new ConcurrentHashMap(); @Override public Object getSource() { return map; } @Override public V get(String key) { return map.get(key); } @Override public void put(String key, V value) { map.put(key, value); } @Override public void remove(String key) { map.remove(key); } @Override public Set keySet() { return map.keySet(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaTimedCache.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.timedcache; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import java.util.Set; /** * 一个定时缓存的简单实现,采用:惰性检查 + 异步循环扫描 * * @author click33 * @since 1.41.0 */ public class SaTimedCache { /** * 存储数据的集合 */ public SaMapPackage dataMap; /** * 存储数据过期时间的集合(单位: 毫秒), 记录所有 key 的到期时间 (注意存储的是到期时间,不是剩余存活时间) */ public SaMapPackage expireMap; public SaTimedCache(SaMapPackage dataMap, SaMapPackage expireMap) { this.dataMap = dataMap; this.expireMap = expireMap; } // ------------------------ 基础 API 读写操作 public Object getObject(String key) { clearKeyByTimeout(key); return dataMap.get(key); } public void setObject(String key, Object object, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } dataMap.put(key, object); expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); } public void updateObject(String key, Object object) { if(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) { return; } dataMap.put(key, object); } public void deleteObject(String key) { dataMap.remove(key); expireMap.remove(key); } public long getObjectTimeout(String key) { return getKeyTimeout(key); } public void updateObjectTimeout(String key, long timeout) { expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); } public Set keySet() { return dataMap.keySet(); } // --------- 过期时间相关操作 /** * 如果指定的 key 已经过期,则立即清除它 * @param key 指定 key */ void clearKeyByTimeout(String key) { Long expirationTime = expireMap.get(key); // 清除条件: // 1、数据存在。 // 2、不是 [ 永不过期 ]。 // 3、已经超过过期时间。 if(expirationTime != null && expirationTime != SaTokenDao.NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) { dataMap.remove(key); expireMap.remove(key); } } /** * 获取指定 key 的剩余存活时间 (单位:秒) * @param key 指定 key * @return 这个 key 的剩余存活时间 */ long getKeyTimeout(String key) { // 由于数据过期检测属于惰性扫描,很可能此时这个 key 已经是过期状态了,所以这里需要先检查一下 clearKeyByTimeout(key); // 获取这个 key 的过期时间 Long expire = expireMap.get(key); // 如果 expire 数据不存在,说明框架没有存储这个 key,此时返回 NOT_VALUE_EXPIRE if(expire == null) { return SaTokenDao.NOT_VALUE_EXPIRE; } // 如果 expire 被标注为永不过期,则返回 NEVER_EXPIRE if(expire == SaTokenDao.NEVER_EXPIRE) { return SaTokenDao.NEVER_EXPIRE; } // ---- 代码至此,说明这个 key 是有过期时间的,且未过期,那么: // 计算剩余时间并返回 (过期时间戳 - 当前时间戳) / 1000 转秒 long timeout = (expire - System.currentTimeMillis()) / 1000; // 小于零时,视为不存在 if(timeout < 0) { dataMap.remove(key); expireMap.remove(key); return SaTokenDao.NOT_VALUE_EXPIRE; } return timeout; } // --------- 定时清理过期数据 /** * 执行数据清理的线程引用 */ public Thread refreshThread; /** * 是否继续执行数据清理的线程标记 */ public volatile boolean refreshFlag; /** * 清理所有已经过期的 key */ public void refreshDataMap() { for (String s : expireMap.keySet()) { clearKeyByTimeout(s); } } /** * 初始化定时任务,定时清理过期数据 */ public void initRefreshThread() { // 如果开发者配置了 <=0 的值,则不启动定时清理 if(SaManager.getConfig().getDataRefreshPeriod() <= 0) { return; } // 启动定时刷新 this.refreshFlag = true; this.refreshThread = new Thread(() -> { for (;;) { try { try { // 如果已经被标记为结束 if( ! refreshFlag) { return; } // 执行清理 refreshDataMap(); } catch (Exception e) { e.printStackTrace(); } // 休眠N秒 int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); if(dataRefreshPeriod <= 0) { dataRefreshPeriod = 1; } Thread.sleep(dataRefreshPeriod * 1000L); } catch (Exception e) { e.printStackTrace(); } } }); this.refreshThread.start(); } /** * 结束定时任务 */ public void endRefreshThread() { this.refreshFlag = false; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.error; /** * 定义所有异常细分状态码 * * @author click33 * @since 1.33.0 */ public interface SaErrorCode { /** 代表这个异常在抛出时未指定异常细分状态码 */ int CODE_UNDEFINED = -1; // ------------ /** 未能获取有效的上下文处理器 */ int CODE_10001 = 10001; /** 未能获取有效的上下文 */ int CODE_10002 = 10002; /** JSON 转换器未实现 */ int CODE_10003 = 10003; /** HTTP 请求处理器未实现 */ int CODE_10004 = 10004; /** 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic */ int CODE_10011 = 10011; /** 指定的配置文件加载失败 */ int CODE_10021 = 10021; /** 配置文件属性无法正常读取 */ int CODE_10022 = 10022; /** 重置的侦听器集合不可以为空 */ int CODE_10031 = 10031; /** 注册的侦听器不可以为空 */ int CODE_10032 = 10032; // 1030x core模块 /** 提供的 Same-Token 是无效的 */ int CODE_10301 = 10301; /** 表示未能通过 Http Basic 认证校验 */ int CODE_10311 = 10311; /** 表示未能通过 Http Digest 认证校验 */ int CODE_10312 = 10312; /** 提供的 HttpMethod 是无效的 */ int CODE_10321 = 10321; // 1100x StpLogic /** 未能读取到有效Token */ int CODE_11001 = 11001; /** 登录时的账号id值为空 */ int CODE_11002 = 11002; /** 更改 Token 指向的 账号Id 时,账号Id值为空 */ int CODE_11003 = 11003; /** 登录失败:当前账号已在其它客户端登录 */ int CODE_11004 = 11004; /** 未能读取到有效Token */ int CODE_11011 = 11011; /** Token无效 */ int CODE_11012 = 11012; /** Token已过期 */ int CODE_11013 = 11013; /** Token已被顶下线 */ int CODE_11014 = 11014; /** Token已被踢下线 */ int CODE_11015 = 11015; /** Token已被冻结 */ int CODE_11016 = 11016; /** 前端未按照指定的前缀提交 token */ int CODE_11017 = 11017; /** 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常 */ int CODE_11031 = 11031; /** 缺少指定的角色 */ int CODE_11041 = 11041; /** 缺少指定的权限 */ int CODE_11051 = 11051; /** 当前账号未通过服务封禁校验 */ int CODE_11061 = 11061; /** 提供要解禁的账号无效 */ int CODE_11062 = 11062; /** 提供要解禁的服务无效 */ int CODE_11063 = 11063; /** 提供要解禁的等级无效 */ int CODE_11064 = 11064; /** 二级认证校验未通过 */ int CODE_11071 = 11071; /** 获取 SaSession 时提供的 SessionId 为空 */ int CODE_11072 = 11072; /** 获取 Token-Session 时提供的 token 为空 */ int CODE_11073 = 11073; /** 获取 Token-Session 时提供的 token 为无效 token */ int CODE_11074 = 11074; // ------------ /** 请求中缺少指定的参数 */ int CODE_12001 = 12001; /** 构建 Cookie 时缺少 name 参数 */ int CODE_12002 = 12002; /** 构建 Cookie 时缺少 value 参数 */ int CODE_12003 = 12003; // ------------ /** Base64 编码异常 */ int CODE_12101 = 12101; /** Base64 解码异常 */ int CODE_12102 = 12102; /** URL 编码异常 */ int CODE_12103 = 12103; /** URL 解码异常 */ int CODE_12104 = 12104; /** md5 加密异常 */ int CODE_12111 = 12111; /** sha1 加密异常 */ int CODE_12112 = 12112; /** sha256 加密异常 */ int CODE_12113 = 12113; /** sha384 加密异常 */ int CODE_121131 = 121131; /** sha512 加密异常 */ int CODE_121132 = 121132; /** AES 加密异常 */ int CODE_12114 = 12114; /** AES 解密异常 */ int CODE_12115 = 12115; /** RSA 公钥加密异常 */ int CODE_12116 = 12116; /** RSA 私钥加密异常 */ int CODE_12117 = 12117; /** RSA 公钥解密异常 */ int CODE_12118 = 12118; /** RSA 私钥解密异常 */ int CODE_12119 = 12119; // ------------ /** 未实现具体的路由匹配策略 */ int CODE_12401 = 12401; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/ApiDisabledException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表 API 已被禁用 * *

一般在 API 不合适调用的时候抛出,例如在集成 jwt 模块后调用数据持久化相关方法

* * @author click33 * @since 1.28.0 */ public class ApiDisabledException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130133L; /** 异常提示语 */ public static final String BE_MESSAGE = "this api is disabled"; /** * 一个异常:代表 API 已被禁用 */ public ApiDisabledException() { super(BE_MESSAGE); } /** * 一个异常:代表 API 已被禁用 * @param message 异常描述 */ public ApiDisabledException(String message) { super(message); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/BackResultException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表停止匹配,直接退出,向前端输出结果 (框架内部专属异常,一般情况下开发者无需关注) * * @author click33 * @since 1.21.0 */ public class BackResultException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130143L; /** * 要输出的结果 */ public Object result; /** * 构造 * @param result 要输出的结果 */ public BackResultException(Object result) { super(String.valueOf(result)); this.result = result; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableServiceException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表指定账号的指定服务已被封禁 * * @author click33 * @since 1.31.0 */ public class DisableServiceException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130143L; /** 异常标记值(已更改为 SaTokenConsts.DEFAULT_DISABLE_LEVEL) */ @Deprecated public static final String BE_VALUE = "disable"; /** 异常提示语 */ public static final String BE_MESSAGE = "此账号已被禁止访问服务:"; /** * 账号类型 */ private final String loginType; /** * 被封禁的账号id */ private final Object loginId; /** * 具体被封禁的服务 */ private final String service; /** * 具体被封禁的等级 */ private final int level; /** * 校验时要求低于的等级 */ private final int limitLevel; /** * 封禁剩余时间,单位:秒 */ private final long disableTime; /** * 获取:账号类型 * * @return / */ public String getLoginType() { return loginType; } /** * 获取: 被封禁的账号id * * @return / */ public Object getLoginId() { return loginId; } /** * 获取: 被封禁的服务 * * @return / */ public Object getService() { return service; } /** * 获取: 被封禁的等级 * * @return / */ public int getLevel() { return level; } /** * 获取: 校验时要求低于的等级 * * @return / */ public int getLimitLevel() { return limitLevel; } /** * 获取: 封禁剩余时间,单位:秒 * @return / */ public long getDisableTime() { return disableTime; } /** * 一个异常:代表指定账号指定服务已被封禁 * * @param loginType 账号类型 * @param loginId 被封禁的账号id * @param service 具体封禁的服务 * @param level 被封禁的等级 * @param limitLevel 校验时要求低于的等级 * @param disableTime 封禁剩余时间,单位:秒 */ public DisableServiceException(String loginType, Object loginId, String service, int level, int limitLevel, long disableTime) { super(BE_MESSAGE + service); this.loginId = loginId; this.loginType = loginType; this.service = service; this.level = level; this.limitLevel = limitLevel; this.disableTime = disableTime; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/FirewallCheckException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表防火墙检验未通过 * * @author click33 * @since 1.41.0 */ public class FirewallCheckException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 8243974276159004739L; public FirewallCheckException(String message) { super(message); } public FirewallCheckException(Throwable e) { super(e); } public FirewallCheckException(String message, Throwable e) { super(message, e); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/InvalidContextException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表框架未能获取有效的上下文 *

已过期:请更名为 SaTokenContextException 用法不变,未来版本将彻底删除此类

* * @author click33 * @since 1.33.0 */ @Deprecated public class InvalidContextException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** * 一个异常:代表框架未能获取有效的上下文 * @param message 异常描述 */ public InvalidContextException(String message) { super(message); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表会话未能通过 Http Basic 认证校验 * * @author click33 * @since 1.26.0 */ public class NotHttpBasicAuthException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** 异常提示语 */ public static final String BE_MESSAGE = "no basic auth"; /** * 一个异常:代表会话未通过 Http Basic 认证 */ public NotHttpBasicAuthException() { super(BE_MESSAGE); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表会话未能通过 Http Digest 认证校验 * * @author click33 * @since 1.38.0 */ public class NotHttpDigestAuthException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** 异常提示语 */ public static final String BE_MESSAGE = "no http digest auth"; /** * 一个异常:代表会话未通过 Http Digest 认证 */ public NotHttpDigestAuthException() { super(BE_MESSAGE); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotImplException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表组件或方法未被提供有效的实现 * * @author click33 * @since 1.33.0 */ public class NotImplException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** * 一个异常:代表组件或方法未被提供有效的实现 * @param message 异常描述 */ public NotImplException(String message) { super(message); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotLoginException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; import cn.dev33.satoken.util.SaFoxUtil; import java.util.Arrays; import java.util.List; /** * 一个异常:代表会话未能通过登录认证校验 * * @author click33 * @since 1.10.0 */ public class NotLoginException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130142L; // ------------------- 异常类型常量 -------------------- /* * 这里简述一下为什么要把常量设计为String类型 * 因为loginId刚取出的时候类型为String,为了避免两者相比较时不必要的类型转换带来的性能消耗,故在此直接将常量类型设计为String */ /** 表示未能读取到有效 token */ public static final String NOT_TOKEN = "-1"; public static final String NOT_TOKEN_MESSAGE = "未能读取到有效 token"; /** 表示 token 无效 */ public static final String INVALID_TOKEN = "-2"; public static final String INVALID_TOKEN_MESSAGE = "token 无效"; /** 表示 token 已过期 */ public static final String TOKEN_TIMEOUT = "-3"; public static final String TOKEN_TIMEOUT_MESSAGE = "token 已过期"; /** 表示 token 已被顶下线 */ public static final String BE_REPLACED = "-4"; public static final String BE_REPLACED_MESSAGE = "token 已被顶下线"; /** 表示 token 已被踢下线 */ public static final String KICK_OUT = "-5"; public static final String KICK_OUT_MESSAGE = "token 已被踢下线"; /** 表示 token 已被冻结 */ public static final String TOKEN_FREEZE = "-6"; public static final String TOKEN_FREEZE_MESSAGE = "token 已被冻结"; /** 表示 未按照指定前缀提交 token */ public static final String NO_PREFIX = "-7"; public static final String NO_PREFIX_MESSAGE = "未按照指定前缀提交 token"; /** 默认的提示语 */ public static final String DEFAULT_MESSAGE = "当前会话未登录"; /** * 代表异常 token 的标志集合 */ public static final List ABNORMAL_LIST = Arrays.asList(NOT_TOKEN, INVALID_TOKEN, TOKEN_TIMEOUT, BE_REPLACED, KICK_OUT, TOKEN_FREEZE, NO_PREFIX); /** * 异常类型 */ private final String type; /** * 获取异常类型 * @return 异常类型 */ public String getType() { return type; } /** * 账号类型 */ private final String loginType; /** * 获得账号类型 * @return 账号类型 */ public String getLoginType() { return loginType; } /** * 构造方法创建一个 * @param message 异常消息 * @param loginType 账号类型 * @param type 类型 */ public NotLoginException(String message, String loginType, String type) { super(message); this.loginType = loginType; this.type = type; } /** * 静态方法构建一个 NotLoginException * @param loginType 账号类型 * @param type 未登录场景值 * @param message 异常描述信息 * @param token 引起异常的 token 值,可不填,如果填了会拼接到异常描述信息后面 * @return 构建完毕的异常对象 */ public static NotLoginException newInstance(String loginType, String type, String message, String token) { if(SaFoxUtil.isNotEmpty(token)) { message = message + ":" + token; } return new NotLoginException(message, loginType, type); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotPermissionException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; import cn.dev33.satoken.stp.StpUtil; /** * 一个异常:代表会话未能通过权限认证校验 * * @author click33 * @since 1.10.0 */ public class NotPermissionException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130141L; /** 权限码 */ private final String permission; /** * @return 获得具体缺少的权限码 */ public String getPermission() { return permission; } /** * 账号类型 */ private final String loginType; /** * 获得账号类型 * * @return 账号类型 */ public String getLoginType() { return loginType; } public NotPermissionException(String permission) { this(permission, StpUtil.stpLogic.loginType); } public NotPermissionException(String permission, String loginType) { super("无此权限:" + permission); this.permission = permission; this.loginType = loginType; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotRoleException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; import cn.dev33.satoken.stp.StpUtil; /** * 一个异常:代表会话未能通过角色认证校验 * * @author click33 * @since 1.10.0 */ public class NotRoleException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 8243974276159004739L; /** 角色标识 */ private final String role; /** * @return 获得角色标识 */ public String getRole() { return role; } /** * 账号类型 */ private final String loginType; /** * 获得账号类型 * * @return 账号类型 */ public String getLoginType() { return loginType; } public NotRoleException(String role) { this(role, StpUtil.stpLogic.loginType); } public NotRoleException(String role, String loginType) { super("无此角色:" + role); this.role = role; this.loginType = loginType; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotSafeException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表会话未能通过二级认证校验 * * @author click33 * @since 1.21.0 */ public class NotSafeException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** 异常提示语 */ public static final String BE_MESSAGE = "二级认证校验失败"; /** * 账号类型 */ private final String loginType; /** * 未通过校验的 Token 值 */ private final Object tokenValue; /** * 未通过校验的服务 */ private final String service; /** * 获取:账号类型 * * @return / */ public String getLoginType() { return loginType; } /** * 获取: 未通过校验的 Token 值 * * @return / */ public Object getTokenValue() { return tokenValue; } /** * 获取: 未通过校验的服务 * * @return / */ public Object getService() { return service; } /** * 一个异常:代表会话未能通过二级认证校验 * * @param loginType 账号类型 * @param tokenValue 未通过校验的 Token 值 * @param service 未通过校验的服务 */ public NotSafeException(String loginType, String tokenValue, String service) { super(BE_MESSAGE + ":" + service); this.tokenValue = tokenValue; this.loginType = loginType; this.service = service; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/NotWebContextException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表当前不是 Web 上下文,无法调用某个 API * * @author click33 * @since 1.33.0 */ public class NotWebContextException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** * 一个异常:代表当前不是 Web 上下文,无法调用某个 API * @param message 异常描述 */ public NotWebContextException(String message) { super(message); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/RequestPathInvalidException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表请求 path 无效或非法 * * @author click33 * @since 1.37.0 */ public class RequestPathInvalidException extends FirewallCheckException { /** * 序列化版本号 */ private static final long serialVersionUID = 8243974276159004739L; /** 具体无效的 path */ private final String path; /** * @return 具体无效的 path */ public String getPath() { return path; } public RequestPathInvalidException(String message, String path) { super(message); this.path = path; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/SaJsonConvertException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表 JSON 转换失败 * * @author click33 * @since 1.30.0 */ public class SaJsonConvertException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290134144L; /** * 一个异常:代表 JSON 转换失败 * @param cause 异常对象 */ public SaJsonConvertException(Throwable cause) { super(cause); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenContextException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; import java.io.Serializable; /** * 一个异常:代表框架未能获取有效的上下文 * * @author click33 * @since 1.33.0 */ public class SaTokenContextException extends InvalidContextException implements Serializable { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** * 一个异常:代表框架未能获取有效的上下文 * @param message 异常描述 */ public SaTokenContextException(String message) { super(message); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.util.SaFoxUtil; /** * Sa-Token 框架内部逻辑发生错误抛出的异常 * *

框架其它异常均继承自此类,开发者可通过捕获此异常来捕获框架内部抛出的所有异常

* * @author click33 * @since 1.10.0 */ public class SaTokenException extends RuntimeException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130132L; /** * 异常细分状态码 */ private int code = SaErrorCode.CODE_UNDEFINED; /** * 构建一个异常 * * @param code 异常细分状态码 */ public SaTokenException(int code) { super(); this.code = code; } /** * 构建一个异常 * * @param message 异常描述信息 */ public SaTokenException(String message) { super(message); } /** * 构建一个异常 * * @param code 异常细分状态码 * @param message 异常信息 */ public SaTokenException(int code, String message) { super(message); this.code = code; } /** * 构建一个异常 * * @param cause 异常对象 */ public SaTokenException(Throwable cause) { super(cause); } /** * 构建一个异常 * * @param message 异常信息 * @param cause 异常对象 */ public SaTokenException(String message, Throwable cause) { super(message, cause); } /** * 获取异常细分状态码 * @return 异常细分状态码 */ public int getCode() { return code; } /** * 写入异常细分状态码 * @param code 异常细分状态码 * @return 对象自身 */ public SaTokenException setCode(int code) { this.code = code; return this; } /** * 断言 flag 不为 true,否则抛出 message 异常 * @param flag 标记 * @param message 异常信息 */ public static void notTrue(boolean flag, String message) { notTrue(flag, message, SaErrorCode.CODE_UNDEFINED); } /** * 断言 flag 不为 true,否则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分状态码 */ public static void notTrue(boolean flag, String message, int code) { if(flag) { throw new SaTokenException(message).setCode(code); } } /** * 断言 value 不为空,否则抛出 message 异常 * @param value 值 * @param message 异常信息 */ public static void notEmpty(Object value, String message) { notEmpty(value, message, SaErrorCode.CODE_UNDEFINED); } /** * 断言 value 不为空,否则抛出 message 异常 * @param value 值 * @param message 异常信息 * @param code 异常细分状态码 */ public static void notEmpty(Object value, String message, int code) { if(SaFoxUtil.isEmpty(value)) { throw new SaTokenException(message).setCode(code); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenPluginException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表插件安装过程中出现异常 * * @author click33 * @since 1.28.0 */ public class SaTokenPluginException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130131L; /** * 一个异常:代表插件安装过程中出现异常 * @param message 异常描述 */ public SaTokenPluginException(String message) { super(message); } /** * 一个异常:代表插件安装过程中出现异常 * * @param cause 异常对象 */ public SaTokenPluginException(Throwable cause) { super(cause); } /** * 一个异常:代表插件安装过程中出现异常 * * @param message 异常描述 * @param cause 异常对象 */ public SaTokenPluginException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/SameTokenInvalidException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表 Same-Token 校验未通过 * * @author click33 * @since 1.32.0 */ public class SameTokenInvalidException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** * 一个异常:代表 Same-Token 校验未通过 * @param message 异常描述 */ public SameTokenInvalidException(String message) { super(message); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/StopMatchException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表停止路由匹配,进入 Controller (框架内部专属异常,一般情况下开发者无需关注) * * @author click33 * @since 1.20.0 */ public class StopMatchException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130143L; /** * 构造 */ public StopMatchException() { super("stop match"); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/exception/TotpAuthException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.exception; /** * 一个异常:代表 TOTP 校验未通过 * * @author click33 * @since 1.41.0 */ public class TotpAuthException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** 异常提示语 */ public static final String BE_MESSAGE = "totp check fail"; /** * 一个异常:代表会话未通过 Totp 校验 */ public TotpAuthException() { super(BE_MESSAGE); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import java.util.List; /** * Sa-Token 过滤器接口,为不同版本的过滤器: * 1、封装共同代码。 * 2、定义统一的行为接口。 * * @author click33 * @since 1.34.0 */ public interface SaFilter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 添加 [ 拦截路由 ] * @param paths 路由 * @return 对象自身 */ SaFilter addInclude(String... paths); /** * 添加 [ 放行路由 ] * @param paths 路由 * @return 对象自身 */ SaFilter addExclude(String... paths); /** * 写入 [ 拦截路由 ] 集合 * @param pathList 路由集合 * @return 对象自身 */ SaFilter setIncludeList(List pathList); /** * 写入 [ 放行路由 ] 集合 * @param pathList 路由集合 * @return 对象自身 */ SaFilter setExcludeList(List pathList); // ------------------------ 钩子函数 /** * 写入[ 认证函数 ]: 每次请求执行 * @param auth see note * @return 对象自身 */ SaFilter setAuth(SaFilterAuthStrategy auth); /** * 写入[ 异常处理函数 ]:每次[ 认证函数 ]发生异常时执行此函数 * @param error see note * @return 对象自身 */ SaFilter setError(SaFilterErrorStrategy error); /** * 写入[ 前置函数 ]:在每次[ 认证函数 ]之前执行。 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth * @param beforeAuth / * @return 对象自身 */ SaFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterAuthStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; /** * Sa-Token 全局过滤器 - 认证策略封装,方便 lambda 表达式风格调用 * * @author click33 * @since 1.17.0 */ @FunctionalInterface public interface SaFilterAuthStrategy { /** * 执行方法 * @param obj 无含义参数,留作扩展 */ void run(Object obj); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterErrorStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; /** * Sa-Token 全局过滤器 - 异常处理策略封装,方便 lambda 表达式风格调用 * *

此方法的返回值将在 toString() 后返回给前端,如果你要返回 JSON 数据,需要在返回前自行序列化为 JSON 字符串

* * @author click33 * @since 1.16.0 */ @FunctionalInterface public interface SaFilterErrorStrategy { /** * 执行方法 * @param e 异常对象 * @return 输出对象,此返回值将在 toString() 后返回给前端,如果你要返回 JSON 数据,需要在返回前自行序列化为 JSON 字符串 */ Object run(Throwable e); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/IsRunFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * lambda 表达式辅助封装:根据 Boolean 变量,决定是否执行一个函数 * * @author click33 * @since 1.13.0 */ public class IsRunFunction { /** * 变量 */ public final Boolean isRun; /** * 设定一个变量,如果为true,则执行exe函数 * * @param isRun 变量 */ public IsRunFunction(boolean isRun) { this.isRun = isRun; } /** * 当 isRun == true 时执行此函数 * @param function 函数 * @return 对象自身 */ public IsRunFunction exe(SaFunction function) { if (isRun) { function.run(); } return this; } /** * 当 isRun == false 时执行此函数 * @param function 函数 * @return 对象自身 */ public IsRunFunction noExe(SaFunction function) { if (!isRun) { function.run(); } return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * 无形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.13.0 */ @FunctionalInterface public interface SaFunction { /** * 执行的方法 */ void run(); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * 单形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.27.0 */ @FunctionalInterface public interface SaParamFunction { /** * 执行的方法 * @param r 传入的参数 */ void run(T r); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamRetFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * 单形参、有返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.27.0 */ @FunctionalInterface public interface SaParamRetFunction { /** * 执行的方法 * @param param 传入的参数 * @return 返回值 */ R run(T param); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * 无形参、有返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.20.0 */ @FunctionalInterface public interface SaRetFunction { /** * 执行的方法 * @return 返回值 */ Object run(); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetGenericFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * 无形参、有返回值(泛型)的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.42.0 */ @FunctionalInterface public interface SaRetGenericFunction { /** * 执行的方法 * @return 返回值 */ T run(); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRouteFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; /** * 路由拦截器验证方法的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.34.0 */ @FunctionalInterface public interface SaRouteFunction { /** * 执行验证的方法 * * @param request Request 包装对象 * @param response Response 包装对象 * @param handler 处理对象 */ void run(SaRequest request, SaResponse response, Object handler); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/SaTwoParamFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun; /** * 双形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用 * * @author click33 * @since 1.41.0 */ @FunctionalInterface public interface SaTwoParamFunction { /** * 执行的方法 * @param r 传入的参数 * @param r2 传入的参数 2 */ void run(T r, T2 r2); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/hooks/SaTokenPluginHookFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.hooks; import cn.dev33.satoken.plugin.SaTokenPlugin; /** * SaTokenPlugin 钩子函数 * * @author click33 * @since 1.41.0 */ @FunctionalInterface public interface SaTokenPluginHookFunction { /** * 执行的方法 * @param plugin 插件实例 */ void execute(SaTokenPlugin plugin); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaAutoRenewFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.stp.StpLogic; import java.util.function.Function; /** * 函数式接口:自定义自动续期条件 * *

参数:StpLogic 实例

*

返回:Boolean 是否续期

* * @author fangzhengjin * @since 1.41.0 */ @FunctionalInterface public interface SaAutoRenewFunction extends Function { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckELRootMapExtendFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.util.Map; import java.util.function.Consumer; /** * 函数式接口:SaCheckELRootMap 扩展函数 * *

参数:SaCheckELRootMap 对象

* * @author click33 * @since 1.40.0 */ @FunctionalInterface public interface SaCheckELRootMapExtendFunction extends Consumer> { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckElementAnnotationFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.lang.reflect.AnnotatedElement; import java.util.function.Consumer; /** * 函数式接口:对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) * *

参数:element元素

*

返回:无

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaCheckElementAnnotationFunction extends Consumer { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckMethodAnnotationFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.lang.reflect.Method; import java.util.function.Consumer; /** * 函数式接口:对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) * *

参数:Method句柄

*

返回:无

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaCheckMethodAnnotationFunction extends Consumer { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckOrAnnotationFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.annotation.SaCheckOr; import java.util.function.Consumer; /** * 函数式接口:对一个 @SaCheckOr 进行注解校验 * *

参数:SaCheckOr 注解的实例

*

返回:无

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaCheckOrAnnotationFunction extends Consumer { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCorsHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; /** * CORS 跨域策略处理函数 * * @author click33 * @since 1.42.0 */ @FunctionalInterface public interface SaCorsHandleFunction { /** * CORS 策略处理函数 * * @param req 请求包装对象 * @param res 响应包装对象 * @param sto 数据读写对象 */ void execute( SaRequest req, SaResponse res, SaStorage sto ); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateSessionFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.session.SaSession; import java.util.function.Function; /** * 函数式接口:创建 SaSession 的策略 * *

参数:SessionId

*

返回:SaSession对象

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaCreateSessionFunction extends Function { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateStpLogicFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.stp.StpLogic; import java.util.function.Function; /** * 函数式接口:创建 StpLogic 的算法 * *

参数:账号体系标识

*

返回:创建好的 StpLogic 对象

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaCreateStpLogicFunction extends Function { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateTokenFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.util.function.BiFunction; /** * 函数式接口:创建 token 的策略 * *

参数:账号 id、账号类型

*

返回:token 值

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaCreateTokenFunction extends BiFunction { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFailHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.FirewallCheckException; /** * 函数式接口:当防火墙校验不通过时执行的函数 * * @author click33 * @since 1.37.0 */ @FunctionalInterface public interface SaFirewallCheckFailHandleFunction { /** * 执行函数 * @param e 防火墙校验异常 * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ void run(FirewallCheckException e, SaRequest req, SaResponse res, Object extArg); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; /** * 函数式接口:防火墙校验函数 * * @author click33 * @since 1.37.0 */ @FunctionalInterface public interface SaFirewallCheckFunction { /** * 执行函数 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ void execute(SaRequest req, SaResponse res, Object extArg); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGenerateUniqueTokenFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.util.function.Function; import java.util.function.Supplier; /** * 生成唯一式 token 的函数式接口,方便开发者进行 lambda 表达式风格调用 * *

参数:元素名称, 最大尝试次数, 创建 token 函数, 检查 token 函数

*

返回:生成的token

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaGenerateUniqueTokenFunction { /** * 封装 token 生成、校验的代码,生成唯一式 token * * @param elementName 要生成的元素名称,方便抛出异常时组织提示信息 * @param maxTryTimes 最大尝试次数 * @param createTokenFunction 创建 token 的函数 * @param checkTokenFunction 校验 token 是否唯一的函数(返回 true 表示唯一,可用) * @return 最终生成的唯一式 token */ String execute( String elementName, int maxTryTimes, Supplier createTokenFunction, Function checkTokenFunction ); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.util.function.BiFunction; /** * 函数式接口:从元素上获取注解 * *

参数:element元素,要获取的注解类型

*

返回:注解对象

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaGetAnnotationFunction extends BiFunction, Annotation> { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaHasElementFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.util.List; import java.util.function.BiFunction; /** * 函数式接口:判断集合中是否包含指定元素(模糊匹配) * *

参数:集合、元素

*

返回:是否包含

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaHasElementFunction extends BiFunction, String, Boolean> { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaIsAnnotationPresentFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.function.BiFunction; /** * 函数式接口:判断一个 Method 或其所属 Class 是否包含指定注解 * *

参数:Method、注解

*

返回:是否包含

* * @author click33 * @since 1.35.0 */ @FunctionalInterface public interface SaIsAnnotationPresentFunction extends BiFunction, Boolean> { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaRouteMatchFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.fun.strategy; import java.util.function.BiFunction; /** * 函数式接口:路由匹配策略 * *

参数:pattern, path

*

返回:是否匹配

* * @author click33 * @since 1.42.0 */ @FunctionalInterface public interface SaRouteMatchFunction extends BiFunction { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.http; import java.util.Map; /** * Http 请求处理器 * * @author click33 * @since 1.43.0 */ public interface SaHttpTemplate { /** * get 请求 * * @param url / * @return / */ String get(String url); /** * post 请求,form-data 格式参数 * * @param url / * @param params / * @return / */ String postByFormData(String url, Map params); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplateDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.http; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.NotImplException; import java.util.Map; /** * Http 请求处理器,默认实现类 * * @author click33 * @since 1.43.0 */ public class SaHttpTemplateDefaultImpl implements SaHttpTemplate { public static final String ERROR_MESSAGE = "HTTP 请求处理器未实现"; /** * get 请求 * * @param url / * @return / */ @Override public String get(String url) { throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10004); } /** * post 请求,form-data 格式参数 */ @Override public String postByFormData(String url, Map params) { throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10004); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.http; import cn.dev33.satoken.SaManager; import java.util.Map; /** * Http 请求处理器 工具类 * * @author click33 * @since 1.43.0 */ public class SaHttpUtil { /** * get 请求 * * @param url / * @return / */ public static String get(String url) { return SaManager.getSaHttpTemplate().get(url); } /** * post 请求,form-data 格式参数 * * @param url / * @param params / * @return / */ public static String postByFormData(String url, Map params) { return SaManager.getSaHttpTemplate().postByFormData(url, params); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicAccount.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.httpauth.basic; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; /** * Sa-Token Http Basic 账号 * * @author click33 * @since 1.41.0 */ public class SaHttpBasicAccount { /** * 账号 */ private String username; /** * 密码 */ private String password; /** * 构造函数 * @param username 账号 * @param password 密码 */ public SaHttpBasicAccount(String username, String password) { this.username = username; this.password = password; } /** * 构造函数 * @param usernameAndPassword 账号和密码,冒号隔开 */ public SaHttpBasicAccount(String usernameAndPassword) { if(SaFoxUtil.isEmpty(usernameAndPassword)) { throw new SaTokenException("UsernameAndPassword 不能为空"); } String[] arr = usernameAndPassword.split(":"); if(arr.length != 2) { throw new SaTokenException("UsernameAndPassword 格式错误,正确格式为:username:password"); } this.username = arr[0]; this.password = arr[1]; } /** * 获取 账号 * * @return username 账号 */ public String getUsername() { return this.username; } /** * 设置 账号 * * @param username 账号 */ public void setUsername(String username) { this.username = username; } /** * 获取 密码 * * @return password 密码 */ public String getPassword() { return this.password; } /** * 设置 密码 * * @param password 密码 */ public void setPassword(String password) { this.password = password; } @Override public String toString() { return "SaHttpBasicAccount{" + "username='" + username + '\'' + ", password='" + password + '\'' + '}'; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.httpauth.basic; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.NotHttpBasicAuthException; import cn.dev33.satoken.secure.SaBase64Util; import cn.dev33.satoken.util.SaFoxUtil; /** * Sa-Token Http Basic 认证模块 * * @author click33 * @since 1.26.0 */ public class SaHttpBasicTemplate { /** * 默认的 Realm 领域名称 */ public static final String DEFAULT_REALM = "Sa-Token"; /** * 在校验失败时,设置响应头,并抛出异常 * @param realm 领域 */ public void throwNotBasicAuthException(String realm) { SaHolder.getResponse().setStatus(401).setHeader("WWW-Authenticate", "Basic Realm=" + realm); throw new NotHttpBasicAuthException().setCode(SaErrorCode.CODE_10311); } /** * 获取浏览器提交的 Http Basic 参数 (裁剪掉前缀并解码) * @return 值 */ public String getAuthorizationValue() { // 获取前端提交的请求头 Authorization 参数 String authorization = SaHolder.getRequest().getHeader("Authorization"); // 如果不是以 Basic 作为前缀,则视为无效 if(authorization == null || ! authorization.startsWith("Basic ")) { return null; } // 裁剪前缀并解码 return SaBase64Util.decode(authorization.substring(6)); } /** * 获取 Http Basic 账号密码对象 * @return / */ public SaHttpBasicAccount getHttpBasicAccount() { String authorizationValue = getAuthorizationValue(); if(authorizationValue == null) { return null; } return new SaHttpBasicAccount(authorizationValue); } /** * 对当前会话进行 Basic 校验(使用全局配置的账号密码),校验不通过则抛出异常 */ public void check() { check(DEFAULT_REALM, SaManager.getConfig().getHttpBasic()); } /** * 对当前会话进行 Basic 校验(手动设置账号密码),校验不通过则抛出异常 * @param account 账号(格式为 user:password) */ public void check(String account) { check(DEFAULT_REALM, account); } /** * 对当前会话进行 Basic 校验(手动设置 Realm 和 账号密码),校验不通过则抛出异常 * @param realm 领域 * @param account 账号(格式为 user:password) */ public void check(String realm, String account) { if(SaFoxUtil.isEmpty(account)) { account = SaManager.getConfig().getHttpBasic(); } String authorization = getAuthorizationValue(); if(SaFoxUtil.isEmpty(authorization) || ! authorization.equals(account)) { throwNotBasicAuthException(realm); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.httpauth.basic; /** * Sa-Token Http Basic 认证模块,Util 工具类 * * @author click33 * @since 1.26.0 */ public class SaHttpBasicUtil { private SaHttpBasicUtil() { } /** * 底层使用的 SaBasicTemplate 对象 */ public static SaHttpBasicTemplate saHttpBasicTemplate = new SaHttpBasicTemplate(); /** * 获取浏览器提交的 Http Basic 参数 (裁剪掉前缀并解码) * @return 值 */ public static String getAuthorizationValue() { return saHttpBasicTemplate.getAuthorizationValue(); } /** * 获取 Http Basic 账号密码对象 * @return / */ public static SaHttpBasicAccount getHttpBasicAccount() { return saHttpBasicTemplate.getHttpBasicAccount(); } /** * 对当前会话进行 Basic 校验(使用全局配置的账号密码),校验不通过则抛出异常 */ public static void check() { saHttpBasicTemplate.check(); } /** * 对当前会话进行 Basic 校验(手动设置账号密码),校验不通过则抛出异常 * @param account 账号(格式为 user:password) */ public static void check(String account) { saHttpBasicTemplate.check(account); } /** * 对当前会话进行 Basic 校验(手动设置 Realm 和 账号密码),校验不通过则抛出异常 * @param realm 领域 * @param account 账号(格式为 user:password) */ public static void check(String realm, String account) { saHttpBasicTemplate.check(realm, account); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.httpauth.digest; /** * Sa-Token Http Digest 认证 - 参数实体类 * * @author click33 * @since 1.38.0 */ public class SaHttpDigestModel { /** * 默认的 Realm 领域名称 */ public static final String DEFAULT_REALM = "Sa-Token"; /** * 默认的 qop 值 */ public static final String DEFAULT_QOP = "auth"; /** * 用户名 */ public String username; /** * 密码 */ public String password; /** * 领域 */ public String realm = DEFAULT_REALM; /** * 随机数 */ public String nonce; /** * 请求 uri */ public String uri; /** * 请求方法 */ public String method; /** * 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 */ public String qop; /** * nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 */ public String nc; /** * 客户端随机数,由客户端提供 */ public String cnonce; /** * opaque */ public String opaque; /** * 请求摘要,最终计算的摘要结果 */ public String response; // ------------------- 构造函数 ------------------- public SaHttpDigestModel() { } public SaHttpDigestModel(String username, String password) { this.username = username; this.password = password; } public SaHttpDigestModel(String username, String password, String realm) { this.username = username; this.password = password; this.realm = realm; } // ------------------- get/set ------------------- /** * 获取 用户名 * * @return username 用户名 */ public String getUsername() { return this.username; } /** * 设置 用户名 * * @param username 用户名 * @return / */ public SaHttpDigestModel setUsername(String username) { this.username = username; return this; } /** * 获取 领域 * * @return realm 领域 */ public String getRealm() { return this.realm; } /** * 设置 领域 * * @param realm 领域 * @return / */ public SaHttpDigestModel setRealm(String realm) { this.realm = realm; return this; } /** * 获取 密码 * * @return password 密码 */ public String getPassword() { return this.password; } /** * 设置 密码 * * @param password 密码 * @return / */ public SaHttpDigestModel setPassword(String password) { this.password = password; return this; } /** * 获取 随机数 * * @return nonce 随机数 */ public String getNonce() { return this.nonce; } /** * 设置 随机数 * * @param nonce 随机数 * @return / */ public SaHttpDigestModel setNonce(String nonce) { this.nonce = nonce; return this; } /** * 获取 请求 uri * * @return uri 请求 uri */ public String getUri() { return this.uri; } /** * 设置 请求 uri * * @param uri 请求 uri * @return / */ public SaHttpDigestModel setUri(String uri) { this.uri = uri; return this; } /** * 获取 请求方法 * * @return method 请求方法 */ public String getMethod() { return this.method; } /** * 设置 请求方法 * * @param method 请求方法 * @return / */ public SaHttpDigestModel setMethod(String method) { this.method = method; return this; } /** * 获取 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 * * @return qop 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 */ public String getQop() { return this.qop; } /** * 设置 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 * * @param qop 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 * @return / */ public SaHttpDigestModel setQop(String qop) { this.qop = qop; return this; } /** * 获取 nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 * * @return nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 */ public String getNc() { return this.nc; } /** * 设置 nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 * * @param nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 * @return / */ public SaHttpDigestModel setNc(String nc) { this.nc = nc; return this; } /** * 获取 客户端随机数,由客户端提供 * * @return cnonce 客户端随机数,由客户端提供 */ public String getCnonce() { return this.cnonce; } /** * 设置 客户端随机数,由客户端提供 * * @param cnonce 客户端随机数,由客户端提供 * @return / */ public SaHttpDigestModel setCnonce(String cnonce) { this.cnonce = cnonce; return this; } /** * 获取 opaque * * @return opaque opaque */ public String getOpaque() { return this.opaque; } /** * 设置 opaque * * @param opaque opaque * @return / */ public SaHttpDigestModel setOpaque(String opaque) { this.opaque = opaque; return this; } /** * 获取 请求摘要,最终计算的摘要结果 * * @return response 请求摘要,最终计算的摘要结果 */ public String getResponse() { return this.response; } /** * 设置 请求摘要,最终计算的摘要结果 * * @param response 请求摘要,最终计算的摘要结果 * @return / */ public SaHttpDigestModel setResponse(String response) { this.response = response; return this; } @Override public String toString() { return "SaHttpDigestModel[" + "username=" + username + ", password=" + password + ", realm=" + realm + ", nonce=" + nonce + ", uri=" + uri + ", method=" + method + ", qop=" + qop + ", nc=" + nc + ", cnonce=" + cnonce + ", opaque=" + opaque + ", response=" + response + "]"; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.httpauth.digest; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.SaCheckHttpDigest; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.NotHttpDigestAuthException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.util.SaFoxUtil; import java.util.LinkedHashMap; import java.util.Map; /** * Sa-Token Http Digest 认证模块 - 模板方法类 * * @author click33 * @since 1.38.0 */ public class SaHttpDigestTemplate { /* 这里只是 Http Digest 认证的一个简单实现,待实现功能还有: 1、nonce 防重放攻击 2、nc 计数器 3、qop 保护质量=auth-int 4、opaque 透明值 5、algorithm 更多摘要算法 等等 */ /** * 构建认证失败的响应头参数 * @param model 参数对象 * @return 响应头值 */ public String buildResponseHeaderValue(SaHttpDigestModel model) { // 抛异常 String headerValue = "Digest " + "realm=\"" + model.realm + "\", " + "qop=\"" + model.qop + "\", " + "nonce=\"" + model.nonce + "\", " + "nc=" + model.nc + ", " + "opaque=\"" + model.opaque + "\""; return headerValue; } /** * 在校验失败时,设置响应头,并抛出异常 * @param model Digest 参数对象 */ public void throwNotHttpDigestAuthException(SaHttpDigestModel model) { // 补全一些必须的参数 model.realm = (model.realm != null) ? model.realm : SaHttpDigestModel.DEFAULT_REALM; model.qop = (model.qop != null) ? model.qop : SaHttpDigestModel.DEFAULT_QOP; model.nonce = (model.nonce != null) ? model.nonce : SaFoxUtil.getRandomString(32); model.opaque = (model.opaque != null) ? model.opaque : SaFoxUtil.getRandomString(32); model.nc = (model.nc != null) ? model.nc : "00000001"; // 设置响应头 SaHolder.getResponse() .setStatus(401) .setHeader("WWW-Authenticate", buildResponseHeaderValue(model)); // 抛异常 throw new NotHttpDigestAuthException().setCode(SaErrorCode.CODE_10312); } /** * 获取浏览器提交的 Digest 参数 (裁剪掉前缀) * @return 值 */ public String getAuthorizationValue() { // 获取前端提交的请求头 Authorization 参数 String authorization = SaHolder.getRequest().getHeader("Authorization"); // 如果不是以 Digest 作为前缀,则视为无效 if(authorization == null || ! authorization.startsWith("Digest ")) { return null; } // 裁剪前缀并解码 return authorization.substring(7); } /** * 获取浏览器提交的 Digest 参数,并转化为 Map * @return / */ public SaHttpDigestModel getAuthorizationValueToModel() { // 先获取字符串值 String authorization = getAuthorizationValue(); if(authorization == null) { // throw new SaTokenException("请求头中未携带 Digest 认证参数"); return null; } // 根据逗号分割,解析为 Map Map map = new LinkedHashMap<>(); String[] arr = authorization.split(","); for (String s : arr) { String[] kv = s.split("="); if (kv.length == 2) { map.put(kv[0].trim(), kv[1].trim().replace("\"", "")); } // 兼容字符串包含多个=的情况,如:uri 带参数的问题 // username="sa", realm="Sa-Token", nonce="IWlEwO23oCAbIAbHX1BYnX5ddKHUdsjW", uri="/test/testDigest?name=zhangsan&age=18", response="c4359210ccb23c985234ee6e02def88d", opaque="H6jPyjwfioc0oUbDE0OSmpX7wznfxxMo", qop=auth, nc=00000002, cnonce="46dd0073c981a9c7" else if (s.contains("=")) { map.put(kv[0].trim(), s.substring(kv[0].length() + 1).trim().replace("\"", "")); } } /* 参考样例: username=sa, realm=Sa-Token, nonce=dcd98b7102dd2f0e8b11d0f600bfb0c093, uri=/test/testDigest, response=a32023c128e142163dd4856a2f511c70, opaque=5ccc069c403ebaf9f0171e9517f40e41, qop=auth, nc=00000002, cnonce=f3ca6bfc0b2f59c4 */ // 转化为 Model SaHttpDigestModel model = new SaHttpDigestModel(); model.username = map.get("username"); model.realm = map.get("realm"); model.nonce = map.get("nonce"); model.uri = map.get("uri"); model.method = SaHolder.getRequest().getMethod(); model.qop = map.get("qop"); model.nc = map.get("nc"); model.cnonce = map.get("cnonce"); model.opaque = map.get("opaque"); model.response = map.get("response"); // return model; } /** * 计算:根据 Digest 参数计算 response * * @param model Digest 参数对象 * @return 计算出的 response */ public String calcResponse(SaHttpDigestModel model) { // frag1 = md5(username:realm:password) String frag1 = SaSecureUtil.md5(model.username + ":" + model.realm + ":" + model.password); // frag2 = nonce:nc:cnonce:qop String frag2 = model.nonce + ":" + model.nc + ":" + model.cnonce + ":" + model.qop; // frag3 = md5(method:uri) String frag3 = SaSecureUtil.md5(model.method + ":" + model.uri); // 最终结果 = md5(frag1:frag2:frag3) String response = SaSecureUtil.md5(frag1 + ":" + frag2 + ":" + frag3); // return response; } /** * 把 hopeModel 有的值都 copy 到 reqModel 中 */ public void copyHopeToReq(SaHttpDigestModel hopeModel, SaHttpDigestModel reqModel){ reqModel.username = hopeModel.username; reqModel.password = hopeModel.password; reqModel.realm = hopeModel.realm != null ? hopeModel.realm : reqModel.realm; reqModel.nonce = hopeModel.nonce != null ? hopeModel.nonce : reqModel.nonce; reqModel.uri = hopeModel.uri != null ? hopeModel.uri : reqModel.uri; reqModel.method = hopeModel.method != null ? hopeModel.method : reqModel.method; reqModel.qop = hopeModel.qop != null ? hopeModel.qop : reqModel.qop; reqModel.nc = hopeModel.nc != null ? hopeModel.nc : reqModel.nc; reqModel.opaque = hopeModel.opaque != null ? hopeModel.opaque : reqModel.opaque; // reqModel.cnonce = hopeModel.cnonce != null ? hopeModel.cnonce : reqModel.cnonce; // reqModel.response = hopeModel.response != null ? hopeModel.response : reqModel.response; } // ---------- 校验 ---------- /** * 校验:根据提供 Digest 参数计算 res,与 request 请求中的 Digest 参数进行校验,校验不通过则抛出异常 * @param hopeModel 提供的 Digest 参数对象 */ public void check(SaHttpDigestModel hopeModel) { // 先进行一些必须的希望参数校验 SaTokenException.notEmpty(hopeModel, "Digest参数对象不能为空"); SaTokenException.notEmpty(hopeModel.username, "必须提供希望的 username 参数"); SaTokenException.notEmpty(hopeModel.password, "必须提供希望的 password 参数"); // 获取 web 请求中的 Digest 参数 SaHttpDigestModel reqModel = getAuthorizationValueToModel(); // 为空代表前端根本没有提交 Digest 参数,直接抛异常 if(reqModel == null) { throwNotHttpDigestAuthException(hopeModel); } // 把 hopeModel 有的值都 copy 到 reqModel 中 copyHopeToReq(hopeModel, reqModel); // 计算 String cResponse = calcResponse(reqModel); // 比对,不一致就抛异常 if(! cResponse.equals(reqModel.response)) { throwNotHttpDigestAuthException(hopeModel); } // 认证通过 } /** * 校验:根据提供的参数,校验不通过抛出异常 * @param username 用户名 * @param password 密码 */ public void check(String username, String password) { check(new SaHttpDigestModel(username, password)); } /** * 校验:根据提供的参数,校验不通过抛出异常 * @param username 用户名 * @param password 密码 * @param realm 领域 */ public void check(String username, String password, String realm) { check(new SaHttpDigestModel(username, password, realm)); } /** * 校验:根据全局配置参数,校验不通过抛出异常 */ public void check() { String httpDigest = SaManager.getConfig().getHttpDigest(); if(SaFoxUtil.isEmpty(httpDigest)){ throw new SaTokenException("未配置全局 Http Digest 认证参数"); } String[] arr = httpDigest.split(":"); if(arr.length != 2){ throw new SaTokenException("全局 Http Digest 认证参数配置错误,格式应如:username:password"); } check(arr[0], arr[1]); } // ----------------- 过期方法 ----------------- /** * 根据注解 ( @SaCheckHttpDigest ) 鉴权 * * @param at 注解对象 */ @Deprecated public void checkByAnnotation(SaCheckHttpDigest at) { // 如果配置了 value,则以 value 优先 String value = at.value(); if(SaFoxUtil.isNotEmpty(value)){ String[] arr = value.split(":"); if(arr.length != 2){ throw new SaTokenException("注解参数配置错误,格式应如:username:password"); } check(arr[0], arr[1]); return; } // 如果配置了 username,则分别获取参数 String username = at.username(); if(SaFoxUtil.isNotEmpty(username)){ check(username, at.password(), at.realm()); return; } // 都没有配置,则根据全局配置参数进行校验 check(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.httpauth.digest; import cn.dev33.satoken.annotation.SaCheckHttpDigest; import java.util.Map; /** * Sa-Token Http Digest 认证模块,Util 工具类 * * @author click33 * @since 1.38.0 */ public class SaHttpDigestUtil { private SaHttpDigestUtil() { } /** * 底层使用的 SaHttpDigestTemplate 对象 */ public static SaHttpDigestTemplate saHttpDigestTemplate = new SaHttpDigestTemplate(); /** * 获取浏览器提交的 Digest 参数 (裁剪掉前缀) * @return 值 */ public static String getAuthorizationValue() { return saHttpDigestTemplate.getAuthorizationValue(); } /** * 获取浏览器提交的 Digest 参数,并转化为 Map * @return / */ public static SaHttpDigestModel getAuthorizationValueToModel() { return saHttpDigestTemplate.getAuthorizationValueToModel(); } // ---------- 校验 ---------- /** * 校验:根据提供 Digest 参数计算 res,与 request 请求中的 Digest 参数进行校验,校验不通过则抛出异常 * @param hopeModel 提供的 Digest 参数对象 */ public static void check(SaHttpDigestModel hopeModel) { saHttpDigestTemplate.check(hopeModel); } /** * 校验:根据提供的参数,校验不通过抛出异常 * @param username 用户名 * @param password 密码 */ public static void check(String username, String password) { saHttpDigestTemplate.check(username, password); } /** * 校验:根据提供的参数,校验不通过抛出异常 * @param username 用户名 * @param password 密码 * @param realm 领域 */ public static void check(String username, String password, String realm) { saHttpDigestTemplate.check(username, password, realm); } /** * 校验:根据全局配置参数,校验不通过抛出异常 */ public static void check() { saHttpDigestTemplate.check(); } // ----------------- 过期方法 ----------------- /** * 根据注解 ( @SaCheckHttpDigest ) 鉴权 * * @param at 注解对象 */ @Deprecated public static void checkByAnnotation(SaCheckHttpDigest at) { saHttpDigestTemplate.checkByAnnotation(at); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import java.util.Map; /** * JSON 转换器 * * @author click33 * @since 1.30.0 */ public interface SaJsonTemplate { /** * 序列化:对象 -> json 字符串 * * @param obj / * @return / */ String objectToJson(Object obj); /** * 反序列化:json 字符串 → 对象 * * @param jsonStr / * @param type / * @return / * @param / */ T jsonToObject(String jsonStr, Class type); /** * 反序列化:json 字符串 → 对象 (自动判断类型) * * @param jsonStr / * @return / */ default Object jsonToObject(String jsonStr) { return jsonToObject(jsonStr, Object.class); }; /** * 反序列化:json 字符串 → Map * * @param jsonStr / * @return / */ default Map jsonToMap(String jsonStr) { return jsonToObject(jsonStr, Map.class); }; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplateDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.NotImplException; import java.util.Map; /** * JSON 转换器,默认实现类 * *

如果代码断点走到了此默认实现类,说明框架没有注入有效的 JSON 转换器,需要开发者自行实现并注入

* * @author click33 * @since 1.30.0 */ public class SaJsonTemplateDefaultImpl implements SaJsonTemplate { public static final String ERROR_MESSAGE = "未实现具体的 json 转换器"; @Override public String objectToJson(Object obj) { throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); } @Override public Object jsonToObject(String jsonStr) { throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); } @Override public T jsonToObject(String jsonStr, Class type) { throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); } @Override public Map jsonToMap(String jsonStr) { throw new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.listener; import java.util.ArrayList; import java.util.List; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.StpLogic; /** * Sa-Token 事件中心 事件发布器 * *

提供侦听器注册、事件发布能力

* * @author click33 * @since 1.31.0 */ public class SaTokenEventCenter { // --------- 注册侦听器 private static List listenerList = new ArrayList<>(); static { // 默认添加控制台日志侦听器 listenerList.add(new SaTokenListenerForLog()); } /** * 获取已注册的所有侦听器 * @return / */ public static List getListenerList() { return listenerList; } /** * 重置侦听器集合 * @param listenerList / */ public static void setListenerList(List listenerList) { if(listenerList == null) { throw new SaTokenException("重置的侦听器集合不可以为空").setCode(SaErrorCode.CODE_10031); } SaTokenEventCenter.listenerList = listenerList; } /** * 注册一个侦听器 * @param listener / */ public static void registerListener(SaTokenListener listener) { if(listener == null) { throw new SaTokenException("注册的侦听器不可以为空").setCode(SaErrorCode.CODE_10032); } listenerList.add(listener); } /** * 注册一组侦听器 * @param listenerList / */ public static void registerListenerList(List listenerList) { if(listenerList == null) { throw new SaTokenException("注册的侦听器集合不可以为空").setCode(SaErrorCode.CODE_10031); } for (SaTokenListener listener : listenerList) { if(listener == null) { throw new SaTokenException("注册的侦听器不可以为空").setCode(SaErrorCode.CODE_10032); } } SaTokenEventCenter.listenerList.addAll(listenerList); } /** * 移除一个侦听器 * @param listener / */ public static void removeListener(SaTokenListener listener) { listenerList.remove(listener); } /** * 移除指定类型的所有侦听器 * @param cls / */ public static void removeListener(Class cls) { ArrayList listenerListCopy = new ArrayList<>(listenerList); for (SaTokenListener listener : listenerListCopy) { if(cls.isAssignableFrom(listener.getClass())) { listenerList.remove(listener); } } } /** * 清空所有已注册的侦听器 */ public static void clearListener() { listenerList.clear(); } /** * 判断是否已经注册了指定侦听器 * @param listener / * @return / */ public static boolean hasListener(SaTokenListener listener) { return listenerList.contains(listener); } /** * 判断是否已经注册了指定类型的侦听器 * @param cls / * @return / */ public static boolean hasListener(Class cls) { for (SaTokenListener listener : listenerList) { if(cls.isAssignableFrom(listener.getClass())) { return true; } } return false; } // --------- 事件发布 /** * 事件发布:xx 账号登录 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue 本次登录产生的 token 值 * @param loginParameter 登录参数 */ public static void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { for (SaTokenListener listener : listenerList) { listener.doLogin(loginType, loginId, tokenValue, loginParameter); } } /** * 事件发布:xx 账号注销 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 */ public static void doLogout(String loginType, Object loginId, String tokenValue) { for (SaTokenListener listener : listenerList) { listener.doLogout(loginType, loginId, tokenValue); } } /** * 事件发布:xx 账号被踢下线 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 */ public static void doKickout(String loginType, Object loginId, String tokenValue) { for (SaTokenListener listener : listenerList) { listener.doKickout(loginType, loginId, tokenValue); } } /** * 事件发布:xx 账号被顶下线 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 */ public static void doReplaced(String loginType, Object loginId, String tokenValue) { for (SaTokenListener listener : listenerList) { listener.doReplaced(loginType, loginId, tokenValue); } } /** * 事件发布:xx 账号被封禁 * @param loginType 账号类别 * @param loginId 账号id * @param service 指定服务 * @param level 封禁等级 * @param disableTime 封禁时长,单位: 秒 */ public static void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { for (SaTokenListener listener : listenerList) { listener.doDisable(loginType, loginId, service, level, disableTime); } } /** * 事件发布:xx 账号被解封 * @param loginType 账号类别 * @param loginId 账号id * @param service 指定服务 */ public static void doUntieDisable(String loginType, Object loginId, String service) { for (SaTokenListener listener : listenerList) { listener.doUntieDisable(loginType, loginId, service); } } /** * 事件发布:xx 账号完成二级认证 * @param loginType 账号类别 * @param tokenValue token值 * @param service 指定服务 * @param safeTime 认证时间,单位:秒 */ public static void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { for (SaTokenListener listener : listenerList) { listener.doOpenSafe(loginType, tokenValue, service, safeTime); } } /** * 事件发布:xx 账号关闭二级认证 * @param loginType 账号类别 * @param service 指定服务 * @param tokenValue token值 */ public static void doCloseSafe(String loginType, String tokenValue, String service) { for (SaTokenListener listener : listenerList) { listener.doCloseSafe(loginType, tokenValue, service); } } /** * 事件发布:创建了一个新的 SaSession * @param id SessionId */ public static void doCreateSession(String id) { for (SaTokenListener listener : listenerList) { listener.doCreateSession(id); } } /** * 事件发布:一个 SaSession 注销了 * @param id SessionId */ public static void doLogoutSession(String id) { for (SaTokenListener listener : listenerList) { listener.doLogoutSession(id); } } /** * 每次 Token 续期时触发(注意:是 timeout 续期,而不是 active-timeout 续期) * * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token 值 * @param timeout 续期时间 */ public static void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { for (SaTokenListener listener : listenerList) { listener.doRenewTimeout(loginType, loginId, tokenValue, timeout); } } /** * 事件发布:有新的全局组件载入到框架中 * @param compName 组件名称 * @param compObj 组件对象 */ public static void doRegisterComponent(String compName, Object compObj) { for (SaTokenListener listener : listenerList) { listener.doRegisterComponent(compName, compObj); } } /** * 事件发布:有新的注解处理器载入到框架中 * @param handler 注解处理器 */ public static void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) { for (SaTokenListener listener : listenerList) { listener.doRegisterAnnotationHandler(handler); } } /** * 事件发布:有新的 StpLogic 载入到框架中 * @param stpLogic / */ public static void doSetStpLogic(StpLogic stpLogic) { for (SaTokenListener listener : listenerList) { listener.doSetStpLogic(stpLogic); } } /** * 事件发布:有新的全局配置载入到框架中 * @param config / */ public static void doSetConfig(SaTokenConfig config) { for (SaTokenListener listener : listenerList) { listener.doSetConfig(config); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListener.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.listener; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.StpLogic; /** * Sa-Token 侦听器 * *

你可以通过实现此接口在用户登录、退出等关键性操作时进行一些AOP切面操作

* * @author click33 * @since 1.17.0 */ public interface SaTokenListener { /** * 每次登录时触发 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue 本次登录产生的 token 值 * @param loginParameter 登录参数 */ void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter); /** * 每次注销时触发 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 */ void doLogout(String loginType, Object loginId, String tokenValue); /** * 每次被踢下线时触发 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 */ void doKickout(String loginType, Object loginId, String tokenValue); /** * 每次被顶下线时触发 * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token值 */ void doReplaced(String loginType, Object loginId, String tokenValue); /** * 每次被封禁时触发 * @param loginType 账号类别 * @param loginId 账号id * @param service 指定服务 * @param level 封禁等级 * @param disableTime 封禁时长,单位: 秒 */ void doDisable(String loginType, Object loginId, String service, int level, long disableTime); /** * 每次被解封时触发 * @param loginType 账号类别 * @param loginId 账号id * @param service 指定服务 */ void doUntieDisable(String loginType, Object loginId, String service); /** * 每次打开二级认证时触发 * @param loginType 账号类别 * @param tokenValue token值 * @param service 指定服务 * @param safeTime 认证时间,单位:秒 */ void doOpenSafe(String loginType, String tokenValue, String service, long safeTime); /** * 每次关闭二级认证时触发 * @param loginType 账号类别 * @param tokenValue token值 * @param service 指定服务 */ void doCloseSafe(String loginType, String tokenValue, String service); /** * 每次创建 SaSession 时触发 * @param id SessionId */ void doCreateSession(String id); /** * 每次注销 SaSession 时触发 * @param id SessionId */ void doLogoutSession(String id); /** * 每次 Token 续期时触发(注意:是 timeout 续期,而不是 active-timeout 续期) * * @param loginType 账号类别 * @param loginId 账号id * @param tokenValue token 值 * @param timeout 续期时间 */ void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout); /** * 全局组件载入 * @param compName 组件名称 * @param compObj 组件对象 */ default void doRegisterComponent(String compName, Object compObj) {} /** * 注册了自定义注解处理器 * @param handler 注解处理器 */ default void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) {} /** * StpLogic 对象替换 * @param stpLogic / */ default void doSetStpLogic(StpLogic stpLogic) {} /** * 载入全局配置 * @param config / */ default void doSetConfig(SaTokenConfig config) {} } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.listener; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaFoxUtil; import static cn.dev33.satoken.SaManager.log; /** * Sa-Token 侦听器的一个实现:Log 打印 * * @author click33 * @since 1.33.0 */ public class SaTokenListenerForLog implements SaTokenListener { /** * 每次登录时触发 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { log.info("账号 {} 登录成功 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); } /** * 每次注销时触发 */ @Override public void doLogout(String loginType, Object loginId, String tokenValue) { log.info("账号 {} 注销登录 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); } /** * 每次被踢下线时触发 */ @Override public void doKickout(String loginType, Object loginId, String tokenValue) { log.info("账号 {} 被踢下线 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); } /** * 每次被顶下线时触发 */ @Override public void doReplaced(String loginType, Object loginId, String tokenValue) { log.info("账号 {} 被顶下线 (loginType={}), 会话凭证 token={}", loginId, loginType, tokenValue); } /** * 每次被封禁时触发 */ @Override public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { log.info("账号 {} [{}服务] 被封禁 (loginType={}), 封禁等级={}, 解封时间为 {}", loginId, loginType, service, level, SaFoxUtil.formatAfterDate(disableTime * 1000)); } /** * 每次被解封时触发 */ @Override public void doUntieDisable(String loginType, Object loginId, String service) { log.info("账号 {} [{}服务] 解封成功 (loginType={})", loginId, service, loginType); } /** * 每次打开二级认证时触发 */ @Override public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { log.info("token 二级认证成功, 业务标识={}, 有效期={}秒, Token值={}", service, safeTime, tokenValue); } /** * 每次关闭二级认证时触发 */ @Override public void doCloseSafe(String loginType, String tokenValue, String service) { log.info("token 二级认证关闭, 业务标识={}, Token值={}", service, tokenValue); } /** * 每次创建Session时触发 */ @Override public void doCreateSession(String id) { log.info("SaSession [{}] 创建成功", id); } /** * 每次注销Session时触发 */ @Override public void doLogoutSession(String id) { log.info("SaSession [{}] 注销成功", id); } /** * 每次 Token 续期时触发 */ @Override public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { log.info("token 续期成功, {} 秒后到期, 帐号={}, token值={} ", timeout, loginId, tokenValue); } /** * 全局组件载入 * @param compName 组件名称 * @param compObj 组件对象 */ @Override public void doRegisterComponent(String compName, Object compObj) { String canonicalName = compObj == null ? null : compObj.getClass().getCanonicalName(); log.info("全局组件 {} 载入成功: {}", compName, canonicalName); } /** * 注册了自定义注解处理器 * @param handler 注解处理器 */ @Override public void doRegisterAnnotationHandler(SaAnnotationHandlerInterface handler) { if(handler != null) { log.info("注解扩展 @{} (处理器: {})", handler.getHandlerAnnotationClass().getSimpleName(), handler.getClass().getCanonicalName()); } } /** * StpLogic 对象替换 * @param stpLogic / */ @Override public void doSetStpLogic(StpLogic stpLogic) { if(stpLogic != null) { log.info("会话组件 StpLogic(type={}) 重置成功: {}", stpLogic.getLoginType(), stpLogic.getClass()); } } /** * 载入全局配置 * @param config / */ @Override public void doSetConfig(SaTokenConfig config) { if(config != null) { log.info("全局配置 {} ", config); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForSimple.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.listener; import cn.dev33.satoken.stp.parameter.SaLoginParameter; /** * Sa-Token 侦听器,默认空实现 * *

对所有事件方法提供空实现,方便开发者通过继承此类快速实现一个可用的侦听器

* * @author click33 * @since 1.31.0 */ public class SaTokenListenerForSimple implements SaTokenListener { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { } @Override public void doLogout(String loginType, Object loginId, String tokenValue) { } @Override public void doKickout(String loginType, Object loginId, String tokenValue) { } @Override public void doReplaced(String loginType, Object loginId, String tokenValue) { } @Override public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { } @Override public void doUntieDisable(String loginType, Object loginId, String service) { } @Override public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { } @Override public void doCloseSafe(String loginType, String tokenValue, String service) { } @Override public void doCreateSession(String id) { } @Override public void doLogoutSession(String id) { } @Override public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/log/SaLog.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.log; /** * Sa-Token 日志输出接口 * * @author click33 * @since 1.33.0 */ public interface SaLog { /** * 输出 trace 日志 * @param str 日志内容 * @param args 参数列表 */ void trace(String str, Object ...args); /** * 输出 debug 日志 * @param str 日志内容 * @param args 参数列表 */ void debug(String str, Object ...args); /** * 输出 info 日志 * @param str 日志内容 * @param args 参数列表 */ void info(String str, Object ...args); /** * 输出 warn 日志 * @param str 日志内容 * @param args 参数列表 */ void warn(String str, Object ...args); /** * 输出 error 日志 * @param str 日志内容 * @param args 参数列表 */ void error(String str, Object ...args); /** * 输出 fatal 日志 * @param str 日志内容 * @param args 参数列表 */ void fatal(String str, Object ...args); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/log/SaLogForConsole.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.log; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.util.StrFormatter; /** * Sa-Token 日志实现类 [ 控制台打印 ] * * @author click33 * @since 1.33.0 */ public class SaLogForConsole implements SaLog { /** * 日志等级 */ public static final int trace = 1; public static final int debug = 2; public static final int info = 3; public static final int warn = 4; public static final int error = 5; public static final int fatal = 6; /** * 日志输出的前缀 */ public static String LOG_PREFIX = "SaLog -->: "; public static String TRACE_PREFIX = "SA [TRACE]-->: "; public static String DEBUG_PREFIX = "SA [DEBUG]-->: "; public static String INFO_PREFIX = "SA [INFO] -->: "; public static String WARN_PREFIX = "SA [WARN] -->: "; public static String ERROR_PREFIX = "SA [ERROR]-->: "; public static String FATAL_PREFIX = "SA [FATAL]-->: "; /** * 日志输出的颜色 */ public static String TRACE_COLOR = "\033[39m"; public static String DEBUG_COLOR = "\033[34m"; public static String INFO_COLOR = "\033[32m"; public static String WARN_COLOR = "\033[33m"; public static String ERROR_COLOR = "\033[31m"; public static String FATAL_COLOR = "\033[35m"; public static String DEFAULT_COLOR = "\033[39m"; @Override public void trace(String str, Object... args) { println(trace, TRACE_COLOR, TRACE_PREFIX, str, args); } @Override public void debug(String str, Object... args) { println(debug, DEBUG_COLOR, DEBUG_PREFIX, str, args); } @Override public void info(String str, Object... args) { println(info, INFO_COLOR, INFO_PREFIX, str, args); } @Override public void warn(String str, Object... args) { println(warn, WARN_COLOR, WARN_PREFIX, str, args); } @Override public void error(String str, Object... args) { println(error, ERROR_COLOR, ERROR_PREFIX, str, args); } @Override public void fatal(String str, Object... args) { println(fatal, FATAL_COLOR, FATAL_PREFIX, str, args); } /** * 打印日志到控制台 * @param level 日志等级 * @param color 颜色编码 * @param prefix 前缀 * @param str 字符串 * @param args 参数列表 */ public void println(int level, String color, String prefix, String str, Object... args) { SaTokenConfig config = SaManager.getConfig(); if(config.getIsLog() && level >= config.getLogLevelInt()) { if(config.getIsColorLog() == Boolean.TRUE) { // 彩色日志 System.out.println(color + prefix + StrFormatter.format(str, args) + DEFAULT_COLOR); } else { // 黑白日志 System.out.println(prefix + StrFormatter.format(str, args)); } } } /* // 三种写法速度对比 // if( config.getIsColorLog() != null && config.getIsColorLog() ) 10亿次,2058ms // if( config.getIsColorLog() == Boolean.TRUE ) 10亿次,1050ms 最快 // if( Objects.equals(config.getIsColorLog(), Boolean.TRUE) ) 10亿次,1543ms */ /* 颜色参考: DEFAULT 39 BLACK 30 RED 31 GREEN 32 YELLOW 33 BLUE 34 MAGENTA 35 CYAN 36 WHITE 37 BRIGHT_BLACK 90 BRIGHT_RED 91 BRIGHT_GREEN 92 BRIGHT_YELLOW 93 BRIGHT_BLUE 94 BRIGHT_MAGENTA 95 BRIGHT_CYAN 96 BRIGHT_WHITE 97 */ } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/model/wrapperInfo/SaDisableWrapperInfo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.model.wrapperInfo; import cn.dev33.satoken.util.SaTokenConsts; /** * 返回值包装类:描述一个账号是否已被封禁等信息 * * @author click33 * @since 1.40.0 */ public class SaDisableWrapperInfo { /** * 是否被封禁 */ public boolean isDisable; /** * 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁) */ public long disableTime; /** * 封禁等级(最小1级,0=未封禁) */ public int disableLevel; /** * 构建对象 * * @param isDisable 是否被封禁 * @param disableTime 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁) * @param disableLevel 封禁等级(最小1级,0=未封禁) */ public SaDisableWrapperInfo(boolean isDisable, long disableTime, int disableLevel) { this.isDisable = isDisable; this.disableTime = disableTime; this.disableLevel = disableLevel; } /** * 创建一个已封禁描述对象 * @param disableTime 封禁时间 * @param disableLevel 封禁等级 * @return / */ public static SaDisableWrapperInfo createDisabled(long disableTime, int disableLevel) { return new SaDisableWrapperInfo(true, disableTime, disableLevel); } /** * 创建一个未封禁描述对象 * @return / */ public static SaDisableWrapperInfo createNotDisabled() { return new SaDisableWrapperInfo(false, 0, SaTokenConsts.NOT_DISABLE_LEVEL); } /** * 创建一个未封禁描述对象,并指定缓存时间,指定时间内不再重复查询 * @param cacheTime 缓存时间(单位:秒) * @return / */ public static SaDisableWrapperInfo createNotDisabled(long cacheTime) { return new SaDisableWrapperInfo(false, cacheTime, SaTokenConsts.NOT_DISABLE_LEVEL); } @Override public String toString() { return "SaDisableWrapperInfo{" + "isDisable=" + isDisable + ", disableTime=" + disableTime + ", disableLevel=" + disableLevel + '}'; } // setter / getter 仅为兼容部分框架序列化操作,不建议调用 public boolean getIsDisable() { return isDisable; } public SaDisableWrapperInfo setIsDisable(boolean isDisable) { this.isDisable = isDisable; return this; } public long getDisableTime() { return disableTime; } public SaDisableWrapperInfo setDisableTime(long disableTime) { this.disableTime = disableTime; return this; } public int getDisableLevel() { return disableLevel; } public SaDisableWrapperInfo setDisableLevel(int disableLevel) { this.disableLevel = disableLevel; return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPlugin.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; /** * Sa-Token 插件总接口 * * @author click33 * @since 1.41.0 */ public interface SaTokenPlugin { /** * 安装插件 */ void install(); /** * 卸载插件 */ default void destroy(){ } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.SaTokenPluginException; import cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; /** * Sa-Token 插件管理器,管理所有插件的加载与卸载 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginHolder { /** * 默认实例,非单例模式,可替换 */ public static SaTokenPluginHolder instance = new SaTokenPluginHolder(); // ------------------- 插件管理器初始化相关 ------------------- /** * 是否已经加载过插件 */ public boolean isLoader = false; /** * SPI 文件所在目录名称 */ public String spiDir = "satoken"; /** * 初始化加载所有插件(多次调用只会执行一次) */ public synchronized void init() { if(isLoader) { return; } loaderPlugins(); isLoader = true; } /** * 根据 SPI 机制加载所有插件 *

* 加载所有 jar 下 /META-INF/satoken/ 目录下 cn.dev33.satoken.plugin.SaTokenPlugin 文件指定的实现类 *

*/ public synchronized void loaderPlugins() { SaManager.getLog().info("SPI plugin loading start ..."); List plugins = _loaderPluginsBySpi(SaTokenPlugin.class, spiDir); for (SaTokenPlugin plugin : plugins) { installPlugin(plugin); } SaManager.getLog().info("SPI plugin loading end ..."); } /** * 自定义 SPI 读取策略 (无状态函数) * @param serviceInterface SPI 接口 * @param dirName 目录名称 * @return / * @param / */ protected List _loaderPluginsBySpi(Class serviceInterface, String dirName) { String path = "META-INF/" + dirName + "/" + serviceInterface.getName(); List providers = new ArrayList<>(); try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Enumeration resources = classLoader.getResources(path); while (resources.hasMoreElements()) { URL url = resources.nextElement(); try (InputStream is = url.openStream()) { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { line = line.trim(); // 忽略空行和注释行 if (!line.isEmpty() && !line.startsWith("#")) { Class clazz = Class.forName(line, true, classLoader); T instance = serviceInterface.cast(clazz.getDeclaredConstructor().newInstance()); providers.add(instance); } } } catch (Exception e) { throw new SaTokenPluginException("SPI 插件加载失败: " + e.getMessage(), e); } } } catch (Exception e) { throw new SaTokenPluginException("SPI 插件加载失败: " + e.getMessage(), e); } return providers; } // ------------------- 插件管理 ------------------- /** * 所有插件的集合 */ private final List pluginList = new ArrayList<>(); /** * 获取插件集合副本 (拷贝插件集合,而非每个插件实例) * @return / */ public synchronized List getPluginListCopy() { return new ArrayList<>(pluginList); } /** * 判断是否已经安装了指定插件 * * @param pluginClass 插件类型 * @return / */ public synchronized boolean isInstalledPlugin(Class pluginClass) { for (SaTokenPlugin plugin : pluginList) { if (plugin.getClass().equals(pluginClass)) { return true; } } return false; } /** * 获取指定类型的插件 * @param pluginClass / * @return / * @param / */ public synchronized T getPlugin(Class pluginClass) { for (SaTokenPlugin plugin : pluginList) { if (plugin.getClass().equals(pluginClass)) { return (T) plugin; } } return null; } /** * 消费指定集合的钩子函数,返回消费的数量 * @param pluginClass / * @param hooks / * @param / */ protected synchronized int _consumeHooks(List> hooks, Class pluginClass) { int consumeCount = 0; for (int i = 0; i < hooks.size(); i++) { SaTokenPluginHookModel model = hooks.get(i); if(model.listenerClass.equals(pluginClass)) { model.executeFunction.execute(getPlugin(pluginClass)); hooks.remove(i); i--; consumeCount++; } } return consumeCount; } // ------------------- 插件 Install 与 Destroy ------------------- /** * 安装指定插件 * @param plugin / */ public synchronized SaTokenPluginHolder installPlugin(SaTokenPlugin plugin) { // 插件为空,拒绝安装 if (plugin == null) { throw new SaTokenPluginException("插件不可为空"); } // 插件已经被安装过了,拒绝再次安装 if (isInstalledPlugin(plugin.getClass())) { throw new SaTokenPluginException("插件 [ " + plugin.getClass().getCanonicalName() + " ] 已安装,不可重复安装"); } // 执行该插件的 install 前置钩子 _consumeHooks(beforeInstallHooks, plugin.getClass()); // 插件安装 int consumeCount = _consumeHooks(installHooks, plugin.getClass()); if (consumeCount == 0) { plugin.install(); } // 执行该插件的 install 后置钩子 _consumeHooks(afterInstallHooks, plugin.getClass()); // 添加到插件集合 pluginList.add(plugin); // 返回对象自身,支持连缀风格调用 return this; } /** * 安装指定插件,根据插件类型 * @param pluginClass / */ public synchronized SaTokenPluginHolder installPlugin(Class pluginClass) { try { T plugin = pluginClass.getDeclaredConstructor().newInstance(); return installPlugin(plugin); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new SaTokenPluginException(e); } } /** * 卸载指定插件 * @param plugin / */ public synchronized SaTokenPluginHolder destroyPlugin(SaTokenPlugin plugin) { // 插件为空,拒绝卸载 if (plugin == null) { throw new SaTokenPluginException("插件不可为空"); } // 插件未被安装,拒绝卸载 if (!isInstalledPlugin(plugin.getClass())) { throw new SaTokenPluginException("插件 [ " + plugin.getClass().getCanonicalName() + " ] 未安装,无法卸载"); } // 执行该插件的 destroy 前置钩子 _consumeHooks(beforeDestroyHooks, plugin.getClass()); // 插件卸载 int consumeCount = _consumeHooks(destroyHooks, plugin.getClass()); if (consumeCount == 0) { plugin.destroy(); } // 执行该插件的 destroy 后置钩子 _consumeHooks(afterDestroyHooks, plugin.getClass()); // 返回对象自身,支持连缀风格调用 return this; } /** * 卸载指定插件,根据插件类型 * @param pluginClass / */ public synchronized SaTokenPluginHolder destroyPlugin(Class pluginClass) { return destroyPlugin(getPlugin(pluginClass)); } // ------------------- 插件 Install 钩子 ------------------- /** * 插件 [ Install 钩子 ] 集合 */ private final List> installHooks = new ArrayList<>(); /** * 插件 [ Install 前置钩子 ] 集合 */ private final List> beforeInstallHooks = new ArrayList<>(); /** * 插件 [ Install 后置钩子 ] 集合 */ private final List> afterInstallHooks = new ArrayList<>(); /** * 注册指定插件的 [ Install 钩子 ],1、同插件支持多次注册。2、如果插件已经安装完毕,则抛出异常。3、注册 Install 钩子的插件默认安装行为将不再执行 * @param listenerClass / * @param executeFunction / * @param / */ public synchronized SaTokenPluginHolder onInstall(Class listenerClass, SaTokenPluginHookFunction executeFunction) { // 如果指定的插件已经安装完毕,则不再允许注册前置钩子函数 if(isInstalledPlugin(listenerClass)) { throw new SaTokenPluginException("插件 [ " + listenerClass.getCanonicalName() + " ] 已安装完毕,不允许再注册 Install 钩子函数"); } // 堆积到钩子函数集合 installHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); // 返回对象自身,支持连缀风格调用 return this; } /** * 注册指定插件的 [ Install 前置钩子 ],1、同插件支持多次注册。2、如果插件已经安装完毕,则抛出异常 * @param listenerClass / * @param executeFunction / * @param / */ public synchronized SaTokenPluginHolder onBeforeInstall(Class listenerClass, SaTokenPluginHookFunction executeFunction) { // 如果指定的插件已经安装完毕,则不再允许注册前置钩子函数 if(isInstalledPlugin(listenerClass)) { throw new SaTokenPluginException("插件 [ " + listenerClass.getCanonicalName() + " ] 已安装完毕,不允许再注册 Install 前置钩子函数"); } // 堆积到钩子函数集合 beforeInstallHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); // 返回对象自身,支持连缀风格调用 return this; } /** * 注册指定插件的 [ Install 后置钩子 ],1、同插件支持多次注册。2、如果插件已经安装完毕,则立即执行该钩子函数 * @param listenerClass / * @param executeFunction / * @param / */ public synchronized SaTokenPluginHolder onAfterInstall(Class listenerClass, SaTokenPluginHookFunction executeFunction) { // 如果指定的插件已经安装完毕,则立即执行该钩子函数 if(isInstalledPlugin(listenerClass)) { executeFunction.execute(getPlugin(listenerClass)); return this; } // 堆积到钩子函数集合 afterInstallHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); // 返回对象自身,支持连缀风格调用 return this; } // ------------------- 插件 Destroy 钩子 ------------------- /** * 插件 [ Destroy 钩子 ] 集合 */ private final List> destroyHooks = new ArrayList<>(); /** * 插件 [ Destroy 前置钩子 ] 集合 */ private final List> beforeDestroyHooks = new ArrayList<>(); /** * 插件 [ Destroy 后置钩子 ] 集合 */ private final List> afterDestroyHooks = new ArrayList<>(); /** * 注册指定插件的 [ Destroy 钩子 ],1、同插件支持多次注册。2、注册 Destroy 钩子的插件默认卸载行为将不再执行 * @param listenerClass / * @param executeFunction / * @param / */ public synchronized SaTokenPluginHolder onDestroy(Class listenerClass, SaTokenPluginHookFunction executeFunction) { destroyHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); // 返回对象自身,支持连缀风格调用 return this; } /** * 注册指定插件的 [ Destroy 前置钩子 ],同插件支持多次注册 * @param listenerClass / * @param executeFunction / * @param / */ public synchronized SaTokenPluginHolder onBeforeDestroy(Class listenerClass, SaTokenPluginHookFunction executeFunction) { beforeDestroyHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); // 返回对象自身,支持连缀风格调用 return this; } /** * 注册指定插件的 [ Destroy 后置钩子 ],同插件支持多次注册 * @param listenerClass / * @param executeFunction / * @param / */ public synchronized SaTokenPluginHolder onAfterDestroy(Class listenerClass, SaTokenPluginHookFunction executeFunction) { afterDestroyHooks.add(new SaTokenPluginHookModel(listenerClass, executeFunction)); // 返回对象自身,支持连缀风格调用 return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHookModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction; /** * Sa-Token 插件 Hook Model * * @author click33 * @since 1.41.0 */ public class SaTokenPluginHookModel { /** * 监听插件类型 */ public Class listenerClass; /** * 执行的方法 */ public SaTokenPluginHookFunction executeFunction; /** * 构造函数 * @param listenerClass / * @param executeFunction / */ public SaTokenPluginHookModel(Class listenerClass, SaTokenPluginHookFunction executeFunction) { this.listenerClass = listenerClass; this.executeFunction = executeFunction; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/router/SaHttpMethod.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.router; import java.util.HashMap; import java.util.Map; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; /** * Http 请求各种请求类型的枚举表示 * *

参考:Spring - HttpMethod * * @author click33 * @since 1.27.0 */ public enum SaHttpMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT, /** * 代表全部请求方式 */ ALL; private static final Map map = new HashMap<>(); static { for (SaHttpMethod reqMethod : values()) { map.put(reqMethod.name(), reqMethod); } } /** * String 转 enum * @param method 请求类型 * @return SaHttpMethod 对象 */ public static SaHttpMethod toEnum(String method) { if(method == null) { throw new SaTokenException("Method 不可以是 null").setCode(SaErrorCode.CODE_10321); } SaHttpMethod reqMethod = map.get(method.toUpperCase()); if(reqMethod == null) { throw new SaTokenException("无效Method:" + method).setCode(SaErrorCode.CODE_10321); } return reqMethod; } /** * String[] 转 enum[] * @param methods 请求类型数组 * @return SaHttpMethod 数组 */ public static SaHttpMethod[] toEnumArray(String... methods) { SaHttpMethod [] arr = new SaHttpMethod[methods.length]; for (int i = 0; i < methods.length; i++) { arr[i] = SaHttpMethod.toEnum(methods[i]); } return arr; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.router; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.fun.SaParamRetFunction; import cn.dev33.satoken.strategy.SaStrategy; import java.util.List; /** * 路由匹配操作工具类 * *

提供了一系列的路由匹配操作方法,一般用在全局拦截器、过滤器做路由拦截鉴权。

*

简单示例:

*
 *    	// 指定一条 match 规则
 *    	SaRouter
 *    	   	.match("/**")    // 拦截的 path 列表,可以写多个
 *   	   	.notMatch("/user/doLogin")        // 排除掉的 path 列表,可以写多个
 *   	   	.check(r->StpUtil.checkLogin());        // 要执行的校验动作,可以写完整的 lambda 表达式
 * 
* * @author click33 * @since 1.27.0 */ public class SaRouter { private SaRouter() { } // -------------------- 路由匹配相关 -------------------- /** * 路由匹配 * @param pattern 路由匹配符 * @param path 被匹配的路由 * @return 是否匹配成功 */ public static boolean isMatch(String pattern, String path) { return SaStrategy.instance.routeMatcher.apply(pattern, path); } /** * 路由匹配 * @param patterns 路由匹配符集合 * @param path 被匹配的路由 * @return 是否匹配成功 */ public static boolean isMatch(List patterns, String path) { if(patterns == null) { return false; } for (String pattern : patterns) { if(isMatch(pattern, path)) { return true; } } return false; } /** * 路由匹配 * @param patterns 路由匹配符数组 * @param path 被匹配的路由 * @return 是否匹配成功 */ public static boolean isMatch(String[] patterns, String path) { if(patterns == null) { return false; } for (String pattern : patterns) { if(isMatch(pattern, path)) { return true; } } return false; } /** * Http请求方法匹配 * @param methods Http请求方法断言数组 * @param methodString Http请求方法 * @return 是否匹配成功 */ public static boolean isMatch(SaHttpMethod[] methods, String methodString) { if(methods == null) { return false; } for (SaHttpMethod method : methods) { if(method == SaHttpMethod.ALL || (method != null && method.toString().equalsIgnoreCase(methodString))) { return true; } } return false; } // ------ 使用当前URI匹配 /** * 路由匹配 (使用当前URI) * @param pattern 路由匹配符 * @return 是否匹配成功 */ public static boolean isMatchCurrURI(String pattern) { return isMatch(pattern, SaHolder.getRequest().getRequestPath()); } /** * 路由匹配 (使用当前URI) * @param patterns 路由匹配符集合 * @return 是否匹配成功 */ public static boolean isMatchCurrURI(List patterns) { return isMatch(patterns, SaHolder.getRequest().getRequestPath()); } /** * 路由匹配 (使用当前URI) * @param patterns 路由匹配符数组 * @return 是否匹配成功 */ public static boolean isMatchCurrURI(String[] patterns) { return isMatch(patterns, SaHolder.getRequest().getRequestPath()); } /** * Http请求方法匹配 (使用当前请求方式) * @param methods Http请求方法断言数组 * @return 是否匹配成功 */ public static boolean isMatchCurrMethod(SaHttpMethod[] methods) { return isMatch(methods, SaHolder.getRequest().getMethod()); } // -------------------- 开始匹配 -------------------- /** * 初始化一个SaRouterStaff,开始匹配 * @return SaRouterStaff */ public static SaRouterStaff newMatch() { return new SaRouterStaff(); } // ----------------- path匹配 /** * 路由匹配 * @param patterns 路由匹配符集合 * @return SaRouterStaff */ public static SaRouterStaff match(String... patterns) { return new SaRouterStaff().match(patterns); } /** * 路由匹配排除 * @param patterns 路由匹配符排除数组 * @return SaRouterStaff */ public static SaRouterStaff notMatch(String... patterns) { return new SaRouterStaff().notMatch(patterns); } /** * 路由匹配 * @param patterns 路由匹配符集合 * @return 对象自身 */ public static SaRouterStaff match(List patterns) { return new SaRouterStaff().match(patterns); } /** * 路由匹配排除 * @param patterns 路由匹配符排除集合 * @return 对象自身 */ public static SaRouterStaff notMatch(List patterns) { return new SaRouterStaff().notMatch(patterns); } // ----------------- Method匹配 /** * Http请求方式匹配 (Enum) * @param methods Http请求方法断言数组 * @return SaRouterStaff */ public static SaRouterStaff match(SaHttpMethod... methods) { return new SaRouterStaff().match(methods); } /** * Http请求方法匹配排除 (Enum) * @param methods Http请求方法断言排除数组 * @return SaRouterStaff */ public static SaRouterStaff notMatch(SaHttpMethod... methods) { return new SaRouterStaff().notMatch(methods); } /** * Http请求方法匹配 (String) * @param methods Http请求方法断言数组 * @return SaRouterStaff */ public static SaRouterStaff matchMethod(String... methods) { return new SaRouterStaff().matchMethod(methods); } /** * Http请求方法匹配排除 (String) * @param methods Http请求方法断言排除数组 * @return SaRouterStaff */ public static SaRouterStaff notMatchMethod(String... methods) { return new SaRouterStaff().notMatchMethod(methods); } // ----------------- 条件匹配 /** * 根据 boolean 值进行匹配 * @param flag boolean值 * @return SaRouterStaff */ public static SaRouterStaff match(boolean flag) { return new SaRouterStaff().match(flag); } /** * 根据 boolean 值进行匹配排除 * @param flag boolean值 * @return SaRouterStaff */ public static SaRouterStaff notMatch(boolean flag) { return new SaRouterStaff().notMatch(flag); } /** * 根据自定义方法进行匹配 (lazy) * @param fun 自定义方法 * @return SaRouterStaff */ public static SaRouterStaff match(SaParamRetFunction fun) { return new SaRouterStaff().match(fun); } /** * 根据自定义方法进行匹配排除 (lazy) * @param fun 自定义排除方法 * @return SaRouterStaff */ public static SaRouterStaff notMatch(SaParamRetFunction fun) { return new SaRouterStaff().notMatch(fun); } // -------------------- 直接指定check函数 -------------------- /** * 路由匹配,如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param fun 要执行的校验方法 * @return / */ public static SaRouterStaff match(String pattern, SaFunction fun) { return new SaRouterStaff().match(pattern, fun); } /** * 路由匹配,如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param fun 要执行的校验方法 * @return / */ public static SaRouterStaff match(String pattern, SaParamFunction fun) { return new SaRouterStaff().match(pattern, fun); } /** * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param excludePattern 要排除的路由匹配符 * @param fun 要执行的方法 * @return / */ public static SaRouterStaff match(String pattern, String excludePattern, SaFunction fun) { return new SaRouterStaff().match(pattern, excludePattern, fun); } /** * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param excludePattern 要排除的路由匹配符 * @param fun 要执行的方法 * @return / */ public static SaRouterStaff match(String pattern, String excludePattern, SaParamFunction fun) { return new SaRouterStaff().match(pattern, excludePattern, fun); } // -------------------- 提前退出 -------------------- /** * 停止匹配,跳出函数 (在多个匹配链中一次性跳出Auth函数) * @return SaRouterStaff */ public static SaRouterStaff stop() { throw new StopMatchException(); } /** * 停止匹配,结束执行,向前端返回结果 * @return SaRouterStaff */ public static SaRouterStaff back() { throw new BackResultException(""); } /** * 停止匹配,结束执行,向前端返回结果 * @param result 要输出的结果 * @return SaRouterStaff */ public static SaRouterStaff back(Object result) { throw new BackResultException(result); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterStaff.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.router; import java.util.List; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.fun.SaParamRetFunction; /** * 路由匹配操作对象 * * @author click33 * @since 1.27.0 */ public class SaRouterStaff { /** * 是否命中的标记变量 */ public boolean isHit = true; /** * @return 是否命中 */ public boolean isHit() { return isHit; } /** * @param isHit 命中标记 * @return 对象自身 */ public SaRouterStaff setHit(boolean isHit) { this.isHit = isHit; return this; } /** * 重置命中标记为 true * @return 对象自身 */ public SaRouterStaff reset() { this.isHit = true; return this; } // ----------------- path匹配 /** * 路由匹配 * @param patterns 路由匹配符数组 * @return 对象自身 */ public SaRouterStaff match(String... patterns) { if(isHit) { isHit = SaRouter.isMatchCurrURI(patterns); } return this; } /** * 路由匹配排除 * @param patterns 路由匹配符排除数组 * @return 对象自身 */ public SaRouterStaff notMatch(String... patterns) { if(isHit) { isHit = !SaRouter.isMatchCurrURI(patterns); } return this; } /** * 路由匹配 * @param patterns 路由匹配符集合 * @return 对象自身 */ public SaRouterStaff match(List patterns) { if(isHit) { isHit = SaRouter.isMatchCurrURI(patterns); } return this; } /** * 路由匹配排除 * @param patterns 路由匹配符排除集合 * @return 对象自身 */ public SaRouterStaff notMatch(List patterns) { if(isHit) { isHit = !SaRouter.isMatchCurrURI(patterns); } return this; } // ----------------- Method匹配 /** * Http请求方法匹配 (Enum) * @param methods Http请求方法断言数组 * @return 对象自身 */ public SaRouterStaff match(SaHttpMethod... methods) { if(isHit) { isHit = SaRouter.isMatchCurrMethod(methods); } return this; } /** * Http请求方法匹配排除 (Enum) * @param methods Http请求方法断言排除数组 * @return 对象自身 */ public SaRouterStaff notMatch(SaHttpMethod... methods) { if(isHit) { isHit = !SaRouter.isMatchCurrMethod(methods); } return this; } /** * Http请求方法匹配 (String) * @param methods Http请求方法断言数组 * @return 对象自身 */ public SaRouterStaff matchMethod(String... methods) { if(isHit) { SaHttpMethod [] arr = SaHttpMethod.toEnumArray(methods); isHit = SaRouter.isMatchCurrMethod(arr); } return this; } /** * Http请求方法匹配排除 (String) * @param methods Http请求方法断言排除数组 * @return 对象自身 */ public SaRouterStaff notMatchMethod(String... methods) { if(isHit) { SaHttpMethod [] arr = SaHttpMethod.toEnumArray(methods); isHit = !SaRouter.isMatchCurrMethod(arr); } return this; } // ----------------- 条件匹配 /** * 根据 boolean 值进行匹配 * @param flag boolean值 * @return 对象自身 */ public SaRouterStaff match(boolean flag) { if(isHit) { isHit = flag; } return this; } /** * 根据 boolean 值进行匹配排除 * @param flag boolean值 * @return 对象自身 */ public SaRouterStaff notMatch(boolean flag) { if(isHit) { isHit = !flag; } return this; } /** * 根据自定义方法进行匹配 (lazy) * @param fun 自定义方法 * @return 对象自身 */ public SaRouterStaff match(SaParamRetFunction fun) { if(isHit) { isHit = fun.run(this); } return this; } /** * 根据自定义方法进行匹配排除 (lazy) * @param fun 自定义排除方法 * @return 对象自身 */ public SaRouterStaff notMatch(SaParamRetFunction fun) { if(isHit) { isHit = !fun.run(this); } return this; } // ----------------- 函数校验执行 /** * 执行校验函数 (无参) * @param fun 要执行的函数 * @return 对象自身 */ public SaRouterStaff check(SaFunction fun) { if(isHit) { fun.run(); } return this; } /** * 执行校验函数 (带参) * @param fun 要执行的函数 * @return 对象自身 */ public SaRouterStaff check(SaParamFunction fun) { if(isHit) { fun.run(this); } return this; } /** * 自由匹配 ( 在free作用域里执行stop()不会跳出Auth函数,而是仅仅跳出free代码块 ) * @param fun 要执行的函数 * @return 对象自身 */ public SaRouterStaff free(SaParamFunction fun) { if(isHit) { try { fun.run(this); } catch (StopMatchException e) { // 跳出 free自由匹配代码块 } } return this; } // ----------------- 直接指定check函数 /** * 路由匹配,如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param fun 要执行的校验方法 * @return / */ public SaRouterStaff match(String pattern, SaFunction fun) { return this.match(pattern).check(fun); } /** * 路由匹配,如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param fun 要执行的校验方法 * @return / */ public SaRouterStaff match(String pattern, SaParamFunction fun) { return this.match(pattern).check(fun); } /** * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param excludePattern 要排除的路由匹配符 * @param fun 要执行的方法 * @return / */ public SaRouterStaff match(String pattern, String excludePattern, SaFunction fun) { return this.match(pattern).notMatch(excludePattern).check(fun); } /** * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 * @param pattern 路由匹配符 * @param excludePattern 要排除的路由匹配符 * @param fun 要执行的方法 * @return / */ public SaRouterStaff match(String pattern, String excludePattern, SaParamFunction fun) { return this.match(pattern).notMatch(excludePattern).check(fun); } // ----------------- 提前退出 /** * 停止匹配,跳出函数 (在多个匹配链中一次性跳出Auth函数) * @return 对象自身 */ public SaRouterStaff stop() { if(isHit) { throw new StopMatchException(); } return this; } /** * 停止匹配,结束执行,向前端返回结果 * @return 对象自身 */ public SaRouterStaff back() { if(isHit) { throw new BackResultException(""); } return this; } /** * 停止匹配,结束执行,向前端返回结果 * @return 对象自身 * @param result 要输出的结果 */ public SaRouterStaff back(Object result) { if(isHit) { throw new BackResultException(result); } return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.same; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SameTokenInvalidException; import cn.dev33.satoken.util.SaFoxUtil; /** * Sa Same-Token 同源系统身份认证模块 - 模板方法类 * *

解决同源系统互相调用时的身份认证校验, 例如:微服务网关请求转发鉴权、微服务RPC调用鉴权 * * @author click33 * @since 1.32.0 */ public class SaSameTemplate { /** * 提交 Same-Token 时,建议使用的参数名称 */ public static final String SAME_TOKEN = "SA-SAME-TOKEN"; // -------------------- 获取 & 校验 /** * 获取当前 Same-Token, 如果不存在,则立即创建并返回 * @return / */ public String getToken() { String currentToken = getTokenNh(); if(SaFoxUtil.isEmpty(currentToken)) { // 注意这里的自刷新不能做到高并发可用 currentToken = refreshToken(); } return currentToken; } /** * 判断一个 Same-Token 是否有效 * @param token / * @return / */ public boolean isValid(String token) { // 1、 如果传入的 token 为空,则立即返回 false if(SaFoxUtil.isEmpty(token)) { return false; } // 2、 验证当前 Same-Token 及 Past-Same-Token return token.equals(getToken()) || token.equals(getPastTokenNh()); } /** * 校验一个 Same-Token 是否有效 (如果无效则抛出异常) * @param token / */ public void checkToken(String token) { if( ! isValid(token)) { token = (token == null ? "" : token); throw new SameTokenInvalidException("无效Same-Token:" + token).setCode(SaErrorCode.CODE_10301); } } /** * 校验当前 Request 上下文提供的 Same-Token 是否有效 (如果无效则抛出异常) */ public void checkCurrentRequestToken() { checkToken(SaHolder.getRequest().getHeader(SAME_TOKEN)); } /** * 刷新一次 Same-Token (注意集群环境中不要多个服务重复调用) * @return 刷新后产生的新 Same-Token */ public String refreshToken() { // 1. 先将当前 Same-Token 写入到 Past-Same-Token 中 String sameToken = getTokenNh(); if( ! SaFoxUtil.isEmpty(sameToken)) { savePastToken(sameToken, getTokenTimeout()); } // 2. 再刷新当前 Same-Token String newSameToken = createToken(); saveToken(newSameToken); // 3. 返回新的 Same-Token return newSameToken; } // ------------------------------ 保存Token /** * 保存 Same-Token * @param token / */ public void saveToken(String token) { if(SaFoxUtil.isEmpty(token)) { return; } SaManager.getSaTokenDao().set(splicingTokenSaveKey(), token, SaManager.getConfig().getSameTokenTimeout()); } /** * 保存 Past-Same-Token * @param token token * @param timeout 有效期(单位:秒) */ public void savePastToken(String token, long timeout){ if(SaFoxUtil.isEmpty(token)) { return; } SaManager.getSaTokenDao().set(splicingPastTokenSaveKey(), token, timeout); } // -------------------- 获取Token /** * 获取 Same-Token,不做任何处理 * @return / */ public String getTokenNh() { return SaManager.getSaTokenDao().get(splicingTokenSaveKey()); } /** * 获取 Past-Same-Token,不做任何处理 * @return / */ public String getPastTokenNh() { return SaManager.getSaTokenDao().get(splicingPastTokenSaveKey()); } /** * 获取 Same-Token 的剩余有效期 (单位:秒) * @return / */ public long getTokenTimeout() { return SaManager.getSaTokenDao().getTimeout(splicingTokenSaveKey()); } // -------------------- 创建Token /** * 创建一个 Same-Token * @return Token */ public String createToken() { return SaFoxUtil.getRandomString(64); } // -------------------- 拼接key /** * 拼接key:Same-Token 的存储 key * @return key */ public String splicingTokenSaveKey() { return SaManager.getConfig().getTokenName() + ":var:same-token"; } /** * 拼接key:次级 Same-Token 的存储 key * @return key */ public String splicingPastTokenSaveKey() { return SaManager.getConfig().getTokenName() + ":var:past-same-token"; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.same; import cn.dev33.satoken.SaManager; /** * Sa Same-Token 同源系统身份认证模块 - 工具类 * *

解决同源系统互相调用时的身份认证校验, 例如:微服务网关请求转发鉴权、微服务RPC调用鉴权 * * @author click33 * @since 1.32.0 */ public class SaSameUtil { private SaSameUtil(){} /** * 提交 Same-Token 时,建议使用的参数名称 */ public static final String SAME_TOKEN = SaSameTemplate.SAME_TOKEN; // -------------------- 获取 & 校验 /** * 获取当前 Same-Token, 如果不存在,则立即创建并返回 * @return / */ public static String getToken() { return SaManager.getSaSameTemplate().getToken(); } /** * 判断一个 Same-Token 是否有效 * @param token / * @return / */ public static boolean isValid(String token) { return SaManager.getSaSameTemplate().isValid(token); } /** * 校验一个 Same-Token 是否有效 (如果无效则抛出异常) * @param token / */ public static void checkToken(String token) { SaManager.getSaSameTemplate().checkToken(token); } /** * 校验当前 Request 上下文提供的 Same-Token 是否有效 (如果无效则抛出异常) */ public static void checkCurrentRequestToken() { SaManager.getSaSameTemplate().checkCurrentRequestToken(); } /** * 刷新一次 Same-Token (注意集群环境中不要多个服务重复调用) * @return 刷新后产生的新 Same-Token */ public static String refreshToken() { return SaManager.getSaSameTemplate().refreshToken(); } // -------------------- 获取Token /** * 获取 Same-Token,不做任何处理 * @return / */ public static String getTokenNh() { return SaManager.getSaSameTemplate().getTokenNh(); } /** * 获取 Past-Same-Token,不做任何处理 * @return / */ public static String getPastTokenNh() { return SaManager.getSaSameTemplate().getPastTokenNh(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/secure/BCrypt.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.secure; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; /** * BCrypt加密算法实现。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。 *

* 此类来自于https://github.com/jeremyh/jBCrypt/ *

* 使用方法如下: *

* {@code * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); * } *

* 使用checkpw方法检查被加密的字符串是否与原始字符串匹配: *

* {@code * BCrypt.checkpw(candidate_password, stored_hash); * } *

* gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度: *

* {@code * String strong_salt = BCrypt.gensalt(10); * String stronger_salt = BCrypt.gensalt(12); * } * * @author Damien Miller * @since 1.29.0 */ @Deprecated @SuppressWarnings("all") public class BCrypt { // BCrypt parameters private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; private static final int BCRYPT_SALT_LEN = 16; // Blowfish parameters private static final int BLOWFISH_NUM_ROUNDS = 16; // Initial contents of key schedule private static final int[] P_orig = {0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b}; private static final int[] S_orig = {0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6}; // bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls // this "ciphertext", but it is really plaintext or an IV. We keep // the name to make code comparison easier. static private final int[] bf_crypt_ciphertext = {0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274}; // Table for Base64 encoding static private final char[] base64_code = {'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; // Table for Base64 decoding static private final byte[] index_64 = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1}; // Expanded Blowfish key private int[] P; private int[] S; /** * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note that this is *not* compatible with the standard MIME-base64 encoding. * * @param d the byte array to encode * @param len the number of bytes to encode * @return base64-encoded string * @throws IllegalArgumentException if the length is invalid */ private static String encode_base64(byte[] d, int len) throws IllegalArgumentException { int off = 0; StringBuilder rs = new StringBuilder(); int c1, c2; if (len <= 0 || len > d.length) throw new IllegalArgumentException("Invalid len"); while (off < len) { c1 = d[off++] & 0xff; rs.append(base64_code[(c1 >> 2) & 0x3f]); c1 = (c1 & 0x03) << 4; if (off >= len) { rs.append(base64_code[c1 & 0x3f]); break; } c2 = d[off++] & 0xff; c1 |= (c2 >> 4) & 0x0f; rs.append(base64_code[c1 & 0x3f]); c1 = (c2 & 0x0f) << 2; if (off >= len) { rs.append(base64_code[c1 & 0x3f]); break; } c2 = d[off++] & 0xff; c1 |= (c2 >> 6) & 0x03; rs.append(base64_code[c1 & 0x3f]); rs.append(base64_code[c2 & 0x3f]); } return rs.toString(); } /** * Look up the 3 bits base64-encoded by the specified character, range-checking againt conversion table * * @param x the base64-encoded value * @return the decoded value of x */ private static byte char64(char x) { if ((int) x > index_64.length) return -1; return index_64[x]; } /** * Decode a string encoded using bcrypt's base64 scheme to a byte array.
* Note that this is *not* compatible with the standard MIME-base64 encoding. * * @param s the string to decode * @param maxolen the maximum number of bytes to decode * @return an array containing the decoded bytes * @throws IllegalArgumentException if maxolen is invalid */ private static byte[] decodeBase64(String s, int maxolen) throws IllegalArgumentException { final StringBuilder rs = new StringBuilder(); int off = 0, slen = s.length(), olen = 0; byte[] ret; byte c1, c2, c3, c4, o; if (maxolen <= 0) throw new IllegalArgumentException("Invalid maxolen"); while (off < slen - 1 && olen < maxolen) { c1 = char64(s.charAt(off++)); c2 = char64(s.charAt(off++)); if (c1 == -1 || c2 == -1) break; o = (byte) (c1 << 2); o |= (c2 & 0x30) >> 4; rs.append((char) o); if (++olen >= maxolen || off >= slen) break; c3 = char64(s.charAt(off++)); if (c3 == -1) break; o = (byte) ((c2 & 0x0f) << 4); o |= (c3 & 0x3c) >> 2; rs.append((char) o); if (++olen >= maxolen || off >= slen) break; c4 = char64(s.charAt(off++)); o = (byte) ((c3 & 0x03) << 6); o |= c4; rs.append((char) o); ++olen; } ret = new byte[olen]; for (off = 0; off < olen; off++) ret[off] = (byte) rs.charAt(off); return ret; } /** * Blowfish encipher a single 64-bit block encoded as two 32-bit halves * * @param lr an array containing the two 32-bit half blocks * @param off the position in the array of the blocks */ private void encipher(int[] lr, int off) { int i, n, l = lr[off], r = lr[off + 1]; l ^= P[0]; for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) { // Feistel substitution on left word n = S[(l >> 24) & 0xff]; n += S[0x100 | ((l >> 16) & 0xff)]; n ^= S[0x200 | ((l >> 8) & 0xff)]; n += S[0x300 | (l & 0xff)]; r ^= n ^ P[++i]; // Feistel substitution on right word n = S[(r >> 24) & 0xff]; n += S[0x100 | ((r >> 16) & 0xff)]; n ^= S[0x200 | ((r >> 8) & 0xff)]; n += S[0x300 | (r & 0xff)]; l ^= n ^ P[++i]; } lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; lr[off + 1] = l; } /** * Cycically extract a word of key material * * @param data the string to extract the data from * @param offp a "pointer" (as a one-entry array) to the current offset into data * @return the next word of material from data */ private static int streamToWord(byte[] data, int[] offp) { int i; int word = 0; int off = offp[0]; for (i = 0; i < 4; i++) { word = (word << 8) | (data[off] & 0xff); off = (off + 1) % data.length; } offp[0] = off; return word; } /** * Initialise the Blowfish key schedule */ private void init_key() { P = P_orig.clone(); S = S_orig.clone(); } /** * Key the Blowfish cipher * * @param key an array containing the key */ private void key(byte[] key) { int i; int[] koffp = {0}; int[] lr = {0, 0}; int plen = P.length, slen = S.length; for (i = 0; i < plen; i++) P[i] = P[i] ^ streamToWord(key, koffp); for (i = 0; i < plen; i += 2) { encipher(lr, 0); P[i] = lr[0]; P[i + 1] = lr[1]; } for (i = 0; i < slen; i += 2) { encipher(lr, 0); S[i] = lr[0]; S[i + 1] = lr[1]; } } /** * Perform the "enhanced key schedule" step described by Provos and Mazieres in "A Future-Adaptable Password Scheme" http://www.openbsd.org/papers/bcrypt-paper.ps * * @param data salt information * @param key password information */ private void ekskey(byte[] data, byte[] key) { int i; int[] koffp = {0}; int[] doffp = {0}; int[] lr = {0, 0}; int plen = P.length, slen = S.length; for (i = 0; i < plen; i++) P[i] = P[i] ^ streamToWord(key, koffp); for (i = 0; i < plen; i += 2) { lr[0] ^= streamToWord(data, doffp); lr[1] ^= streamToWord(data, doffp); encipher(lr, 0); P[i] = lr[0]; P[i + 1] = lr[1]; } for (i = 0; i < slen; i += 2) { lr[0] ^= streamToWord(data, doffp); lr[1] ^= streamToWord(data, doffp); encipher(lr, 0); S[i] = lr[0]; S[i + 1] = lr[1]; } } /** * 加密密文 * * @param password 明文密码 * @param salt 加盐 * @param log_rounds hash中叠加的对数 * @param cdata 加密数据 * @return 加密后的密文 */ public byte[] crypt(byte[] password, byte[] salt, int log_rounds, int[] cdata) { int rounds, i, j; int clen = cdata.length; byte[] ret; if (log_rounds < 4 || log_rounds > 30) throw new IllegalArgumentException("Bad number of rounds"); rounds = 1 << log_rounds; if (salt.length != BCRYPT_SALT_LEN) throw new IllegalArgumentException("Bad salt length"); init_key(); ekskey(salt, password); for (i = 0; i != rounds; i++) { key(password); key(salt); } for (i = 0; i < 64; i++) { for (j = 0; j < (clen >> 1); j++) encipher(cdata, j << 1); } ret = new byte[clen * 4]; for (i = 0, j = 0; i < clen; i++) { ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); ret[j++] = (byte) (cdata[i] & 0xff); } return ret; } /** * 生成密文,使用长度为10的加盐方式 * * @param password 需要加密的明文 * @return 密文 */ public static String hashpw(String password) { return hashpw(password, gensalt()); } /** * 生成密文 * * @param password 需要加密的明文 * @param salt 盐,使用{@link #gensalt()} 生成 * @return 密文 */ public static String hashpw(String password, String salt) { BCrypt bcrypt; String real_salt; byte[] saltb; byte[] hashed; char minor = (char) 0; int rounds, off; StringBuilder rs = new StringBuilder(); if (salt.charAt(0) != '$' || salt.charAt(1) != '2') throw new IllegalArgumentException("Invalid salt version"); if (salt.charAt(2) == '$') off = 3; else { minor = salt.charAt(2); // pr#1560@Github // 修正一个在Blowfish实现上的安全风险 if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') throw new IllegalArgumentException("Invalid salt revision"); off = 4; } // Extract number of rounds if (salt.charAt(off + 2) > '$') throw new IllegalArgumentException("Missing salt rounds"); rounds = Integer.parseInt(salt.substring(off, off + 2)); real_salt = salt.substring(off + 3, off + 25); byte[] passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes(StandardCharsets.UTF_8); saltb = decodeBase64(real_salt, BCRYPT_SALT_LEN); bcrypt = new BCrypt(); hashed = bcrypt.crypt(passwordb, saltb, rounds, bf_crypt_ciphertext.clone()); rs.append("$2"); if (minor >= 'a') rs.append(minor); rs.append("$"); if (rounds < 10) rs.append("0"); if (rounds > 30) { throw new IllegalArgumentException("rounds exceeds maximum (30)"); } rs.append(rounds); rs.append("$"); rs.append(encode_base64(saltb, saltb.length)); rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1)); return rs.toString(); } /** * 生成盐 * * @param log_rounds hash中叠加的2的对数 - the work factor therefore increases as 2**log_rounds. * @param random {@link SecureRandom} * @return an encoded salt value */ public static String gensalt(int log_rounds, SecureRandom random) { final StringBuilder rs = new StringBuilder(); byte[] rnd = new byte[BCRYPT_SALT_LEN]; random.nextBytes(rnd); rs.append("$2a$"); if (log_rounds < 10) rs.append("0"); if (log_rounds > 30) { throw new IllegalArgumentException("log_rounds exceeds maximum (30)"); } rs.append(log_rounds); rs.append("$"); rs.append(encode_base64(rnd, rnd.length)); return rs.toString(); } /** * 生成盐 * * @param log_rounds the log2 of the number of rounds of hashing to apply - the work factor therefore increases as 2**log_rounds. * @return 盐 */ public static String gensalt(int log_rounds) { return gensalt(log_rounds, new SecureRandom()); } /** * 生成盐 * * @return 盐 */ public static String gensalt() { return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); } /** * 检查明文密码文本是否匹配加密后的文本 * * @param plaintext 需要验证的明文密码 * @param hashed 密文 * @return 是否匹配 */ public static boolean checkpw(String plaintext, String hashed) { byte[] hashed_bytes; byte[] try_bytes; String try_pw; try { try_pw = hashpw(plaintext, hashed); } catch (Exception ignore) { // 生成密文时错误直接返回false issue#1377@Github return false; } hashed_bytes = hashed.getBytes(StandardCharsets.UTF_8); try_bytes = try_pw.getBytes(StandardCharsets.UTF_8); if (hashed_bytes.length != try_bytes.length) { return false; } byte ret = 0; for (int i = 0; i < try_bytes.length; i++) ret |= hashed_bytes[i] ^ try_bytes[i]; return ret == 0; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase32Util.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.secure; import java.nio.charset.StandardCharsets; /** * Sa-Token Base32 工具类 * * @author click33 * @since 1.42.0 */ public class SaBase32Util { private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; private static final int[] BASE32_LOOKUP = new int[256]; static { // 初始化解码查找表 for (int i = 0; i < BASE32_CHARS.length(); i++) { char c = BASE32_CHARS.charAt(i); BASE32_LOOKUP[c] = i; // 支持小写字母解码 if (c >= 'A' && c <= 'Z') { BASE32_LOOKUP[Character.toLowerCase(c)] = i; } } } /** * Base32 编码(byte[] 转 String) */ public static String encodeBytesToString(byte[] bytes) { if (bytes == null) return null; StringBuilder result = new StringBuilder(); int buffer = 0; int bufferSize = 0; for (byte b : bytes) { buffer = (buffer << 8) | (b & 0xFF); bufferSize += 8; while (bufferSize >= 5) { bufferSize -= 5; int index = (buffer >> bufferSize) & 0x1F; result.append(BASE32_CHARS.charAt(index)); } } // 处理剩余位 if (bufferSize > 0) { int index = (buffer << (5 - bufferSize)) & 0x1F; result.append(BASE32_CHARS.charAt(index)); } return result.toString(); } /** * Base32 解码(String 转 byte[]) */ public static byte[] decodeStringToBytes(String text) { if (text == null) return null; text = text.replaceAll("=", "").trim(); if (text.isEmpty()) return new byte[0]; int buffer = 0; int bufferSize = 0; int byteCount = (text.length() * 5 + 7) / 8; byte[] bytes = new byte[byteCount]; int byteIndex = 0; for (char c : text.toCharArray()) { int value = BASE32_LOOKUP[c]; if (value == 0 && c != 'A') continue; // 跳过非法字符 buffer = (buffer << 5) | value; bufferSize += 5; if (bufferSize >= 8) { bufferSize -= 8; bytes[byteIndex++] = (byte) ((buffer >> bufferSize) & 0xFF); } } // 处理最后一个字节 if (bufferSize > 0) { bytes[byteIndex] = (byte) ((buffer << (8 - bufferSize)) & 0xFF); } return bytes; } /** * Base32 编码(String 转 String) */ public static String encode(String text) { if (text == null) return null; return encodeBytesToString(text.getBytes(StandardCharsets.UTF_8)); } /** * Base32 解码(String 转 String) */ public static String decode(String base32Text) { if (base32Text == null) return null; byte[] bytes = decodeStringToBytes(base32Text); return new String(bytes, StandardCharsets.UTF_8); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase64Util.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.secure; import java.nio.charset.StandardCharsets; import java.util.Base64; /** * Sa-Token Base64 工具类 * * @author click33 * @since 1.14.0 */ public class SaBase64Util { private static final Base64.Encoder encoder = Base64.getEncoder(); private static final Base64.Decoder decoder = Base64.getDecoder(); /** * Base64编码,byte[] 转 String * @param bytes byte[] * @return 字符串 */ public static String encodeBytesToString(byte[] bytes){ return encoder.encodeToString(bytes); } /** * Base64解码,String 转 byte[] * @param text 字符串 * @return byte[] */ public static byte[] decodeStringToBytes(String text){ return decoder.decode(text); } /** * Base64编码,String 转 String * @param text 字符串 * @return Base64格式字符串 */ public static String encode(String text){ return encoder.encodeToString(text.getBytes(StandardCharsets.UTF_8)); } /** * Base64解码,String 转 String * @param base64Text Base64格式字符串 * @return 字符串 */ public static String decode(String base64Text){ return new String(decoder.decode(base64Text), StandardCharsets.UTF_8); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/secure/SaSecureUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.secure; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.HashMap; import java.util.UUID; /** * Sa-Token 常见加密算法工具类 * * @author click33 * @since 1.14.0 */ public class SaSecureUtil { private SaSecureUtil() { } /** * Base64编码 */ private static final Base64.Encoder encoder = Base64.getEncoder(); /** * Base64解码 */ private static final Base64.Decoder decoder = Base64.getDecoder(); // ----------------------- 摘要加密 ----------------------- /** * md5加密 * @param str 指定字符串 * @return 加密后的字符串 */ public static String md5(String str) { str = (str == null ? "" : str); char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; try { byte[] btInput = str.getBytes(); MessageDigest mdInst = MessageDigest.getInstance("MD5"); mdInst.update(btInput); byte[] md = mdInst.digest(); int j = md.length; char[] strA = new char[j * 2]; int k = 0; for (byte byte0 : md) { strA[k++] = hexDigits[byte0 >>> 4 & 0xf]; strA[k++] = hexDigits[byte0 & 0xf]; } return new String(strA); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12111); } } /** * sha1加密 * * @param str 指定字符串 * @return 加密后的字符串 */ public static String sha1(String str) { try { str = (str == null ? "" : str); MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); return getShaHexString(str, messageDigest); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12112); } } /** * sha256加密 * * @param str 指定字符串 * @return 加密后的字符串 */ public static String sha256(String str) { try { str = (str == null ? "" : str); MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return getShaHexString(str, messageDigest); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12113); } } /** * sha384加密 * * @param str 指定字符串 * @return 加密后的字符串 */ public static String sha384(String str) { try { str = (str == null ? "" : str); MessageDigest messageDigest = MessageDigest.getInstance("SHA-384"); return getShaHexString(str, messageDigest); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_121131); } } /** * sha512加密 * * @param str 指定字符串 * @return 加密后的字符串 */ public static String sha512(String str) { try { str = (str == null ? "" : str); MessageDigest messageDigest = MessageDigest.getInstance("SHA-512"); return getShaHexString(str, messageDigest); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_121132); } } /** * sha (Secure Hash Algorithm)加密 公共方法 * * @param str 指定字符串 * @param messageDigest 消息摘要 * @return 加密后的字符串 */ private static String getShaHexString(String str, MessageDigest messageDigest) { messageDigest.update(str.getBytes(StandardCharsets.UTF_8)); byte[] bytes = messageDigest.digest(); StringBuilder builder = new StringBuilder(); String temp; for (byte aByte : bytes) { temp = Integer.toHexString(aByte & 0xFF); // 获取无符号整数十六进制字符串 if (temp.length() == 1) { builder.append("0"); // 确保每个字节都用两个字符表示 } builder.append(temp); } return builder.toString(); } /** * md5加盐加密: md5(md5(str) + md5(salt)) * @param str 字符串 * @param salt 盐 * @return 加密后的字符串 */ @Deprecated public static String md5BySalt(String str, String salt) { return md5(md5(str) + md5(salt)); } /** * sha256加盐加密: sha256(sha256(str) + sha256(salt)) * @param str 字符串 * @param salt 盐 * @return 加密后的字符串 */ @Deprecated public static String sha256BySalt(String str, String salt) { return sha256(sha256(str) + sha256(salt)); } // ----------------------- 对称加密 AES ----------------------- /** * 默认密码算法 */ private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; /** * AES加密 * * @param key 加密的密钥 * @param text 需要加密的字符串 * @return 返回Base64转码后的加密数据 */ public static String aesEncrypt(String key, String text) { try { Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); byte[] byteContent = text.getBytes(StandardCharsets.UTF_8); cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); byte[] result = cipher.doFinal(byteContent); return encoder.encodeToString(result); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12114); } } /** * AES解密 * @param key 加密的密钥 * @param text 已加密的密文 * @return 返回解密后的数据 */ public static String aesDecrypt(String key, String text) { try { Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key)); byte[] result = cipher.doFinal(decoder.decode(text)); return new String(result, StandardCharsets.UTF_8); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12115); } } /** * 生成加密秘钥 * @param password 秘钥 * @return SecretKeySpec */ private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException { KeyGenerator kg = KeyGenerator.getInstance("AES"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(password.getBytes()); kg.init(128, random); SecretKey secretKey = kg.generateKey(); return new SecretKeySpec(secretKey.getEncoded(), "AES"); } // ----------------------- 非对称加密 RSA ----------------------- private static final String ALGORITHM = "RSA"; private static final int KEY_SIZE = 1024; // ---------- 5个常用方法 /** * 生成密钥对 * @return Map对象 (private=私钥, public=公钥) * @throws Exception 异常 */ @Deprecated public static HashMap rsaGenerateKeyPair() throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); KeyPair keyPair; keyPairGenerator.initialize(KEY_SIZE, new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes())); keyPair = keyPairGenerator.generateKeyPair(); RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); HashMap map = new HashMap<>(16); map.put("private", encoder.encodeToString(rsaPrivateKey.getEncoded())); map.put("public", encoder.encodeToString(rsaPublicKey.getEncoded())); return map; } /** * RSA公钥加密 * @param publicKeyString 公钥 * @param content 内容 * @return 加密后内容 */ @Deprecated public static String rsaEncryptByPublic(String publicKeyString, String content) { try { // 获得公钥对象 PublicKey publicKey = getPublicKeyFromString(publicKeyString); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); // 该密钥能够加密的最大字节长度 int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11; byte[][] arrays = splitBytes(content.getBytes(), splitLength); StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { stringBuilder.append(bytesToHexString(cipher.doFinal(array))); } return stringBuilder.toString(); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12116); } } /** * RSA私钥加密 * @param privateKeyString 私钥 * @param content 内容 * @return 加密后内容 */ @Deprecated public static String rsaEncryptByPrivate(String privateKeyString, String content) { try { PrivateKey privateKey = getPrivateKeyFromString(privateKeyString); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, privateKey); // 该密钥能够加密的最大字节长度 int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11; byte[][] arrays = splitBytes(content.getBytes(), splitLength); StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { stringBuilder.append(bytesToHexString(cipher.doFinal(array))); } return stringBuilder.toString(); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12117); } } /** * RSA公钥解密 * @param publicKeyString 公钥 * @param content 已加密内容 * @return 解密后内容 */ @Deprecated public static String rsaDecryptByPublic(String publicKeyString, String content) { try { PublicKey publicKey = getPublicKeyFromString(publicKeyString); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, publicKey); // 该密钥能够加密的最大字节长度 int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8; byte[] contentBytes = hexStringToBytes(content); byte[][] arrays = splitBytes(contentBytes, splitLength); StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { stringBuilder.append(new String(cipher.doFinal(array))); } return stringBuilder.toString(); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12118); } } /** * RSA私钥解密 * @param privateKeyString 公钥 * @param content 已加密内容 * @return 解密后内容 */ @Deprecated public static String rsaDecryptByPrivate(String privateKeyString, String content) { try { PrivateKey privateKey = getPrivateKeyFromString(privateKeyString); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); // 该密钥能够加密的最大字节长度 int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8; byte[] contentBytes = hexStringToBytes(content); byte[][] arrays = splitBytes(contentBytes, splitLength); StringBuilder stringBuilder = new StringBuilder(); for (byte[] array : arrays) { stringBuilder.append(new String(cipher.doFinal(array))); } return stringBuilder.toString(); } catch (Exception e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12119); } } // ---------- 获取*钥 /** 根据公钥字符串获取 公钥对象 */ private static PublicKey getPublicKeyFromString(String key) throws NoSuchAlgorithmException, InvalidKeySpecException { // 过滤掉\r\n key = key.replace("\r\n", ""); // 取得公钥 X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(decoder.decode(key)); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePublic(x509KeySpec); } /** 根据私钥字符串获取 私钥对象 */ private static PrivateKey getPrivateKeyFromString(String key) throws NoSuchAlgorithmException, InvalidKeySpecException { // 过滤掉\r\n key = key.replace("\r\n", ""); // 取得私钥 PKCS8EncodedKeySpec x509KeySpec = new PKCS8EncodedKeySpec(decoder.decode(key)); KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePrivate(x509KeySpec); } // ---------- 一些辅助方法 /** 根据限定的每组字节长度,将字节数组分组 */ private static byte[][] splitBytes(byte[] bytes, int splitLength) { // bytes与splitLength的余数 int remainder = bytes.length % splitLength; // 数据拆分后的组数,余数不为0时加1 int quotient = remainder != 0 ? bytes.length / splitLength + 1 : bytes.length / splitLength; byte[][] arrays = new byte[quotient][]; byte[] array; for (int i = 0; i < quotient; i++) { // 如果是最后一组(quotient-1),同时余数不等于0,就将最后一组设置为remainder的长度 if (i == quotient - 1 && remainder != 0) { array = new byte[remainder]; System.arraycopy(bytes, i * splitLength, array, 0, remainder); } else { array = new byte[splitLength]; System.arraycopy(bytes, i * splitLength, array, 0, splitLength); } arrays[i] = array; } return arrays; } /** 将字节数组转换成16进制字符串 */ private static String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(bytes.length); String temp; for (byte aByte : bytes) { temp = Integer.toHexString(0xFF & aByte); if (temp.length() < 2) { sb.append(0); } sb.append(temp); } return sb.toString(); } /** 将16进制字符串转换成字节数组 */ private static byte[] hexStringToBytes(String hex) { int len = (hex.length() / 2); hex = hex.toUpperCase(); byte[] result = new byte[len]; char[] chars = hex.toCharArray(); for (int i = 0; i < len; i++) { int pos = i * 2; result[i] = (byte) (toByte(chars[pos]) << 4 | toByte(chars[pos + 1])); } return result; } /** 将char转换为byte */ private static byte toByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.secure.totp; import cn.dev33.satoken.exception.TotpAuthException; import cn.dev33.satoken.secure.SaBase32Util; import cn.dev33.satoken.util.StrFormatter; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.time.Instant; /** * TOTP 算法类,支持 生成/验证 动态一次性密码 * * @author click33 * @since 1.42.0 */ public class SaTotpTemplate { /** * 时间窗口步长(秒) */ public int timeStep = 30; /** * 生成的验证码位数 */ public int codeDigits = 6; /** * 哈希算法(HmacSHA1、HmacSHA256等) */ public String hmacAlgorithm = "HmacSHA1"; /** * 密钥长度(字节,推荐16或32) */ public int secretKeyLength = 16; /** * 构造函数 (使用默认参数) */ public SaTotpTemplate() { } /** * 构造函数 (使用自定义参数) * * @param timeStep 时间窗口步长(秒) * @param codeDigits 生成的验证码位数 * @param hmacAlgorithm 哈希算法(HmacSHA1、HmacSHA256等) * @param secretKeyLength 密钥长度(字节,推荐16或32) */ public SaTotpTemplate(int timeStep, int codeDigits, String hmacAlgorithm, int secretKeyLength) { this.timeStep = timeStep; this.codeDigits = codeDigits; this.hmacAlgorithm = hmacAlgorithm; this.secretKeyLength = secretKeyLength; } /** * 生成随机密钥(Base32编码) * * @return / */ public String generateSecretKey() { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[secretKeyLength]; random.nextBytes(bytes); return SaBase32Util.encodeBytesToString(bytes).replace("=", ""); } /** * 生成当前时间的 TOTP 验证码 * * @param secretKey Base32 编码的密钥 * @return / */ public String _generateTOTP(String secretKey) { return _generateTOTP(secretKey, Instant.now().getEpochSecond()); } /** * 判断用户输入的 TOTP 是否有效 * * @param secretKey Base32编码的密钥 * @param code 用户输入的验证码 * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) * @return / */ public boolean validateTOTP(String secretKey, String code, int timeWindowOffset) { long currentWindow = Instant.now().getEpochSecond() / timeStep; for (int i = -timeWindowOffset; i <= timeWindowOffset; i++) { String calculatedCode = _generateTOTP(secretKey, (currentWindow + i) * timeStep); if (calculatedCode.equals(code)) { return true; } } return false; } /** * 校验用户输入的TOTP是否有效,如果无效则抛出异常 * * @param secretKey Base32编码的密钥 * @param code 用户输入的验证码 * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) */ public void checkTOTP(String secretKey, String code, int timeWindowOffset) { if (!validateTOTP(secretKey, code, timeWindowOffset)) { throw new TotpAuthException(); } } /** * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) * * @param account 账户名 * @return / */ public String generateGoogleSecretKey(String account) { return generateGoogleSecretKey(account, generateSecretKey()); } /** * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) * * @param account 账户名 * @param secretKey TOTP 秘钥 * @return / */ public String generateGoogleSecretKey(String account, String secretKey) { return StrFormatter.format("otpauth://totp/{}?secret={}", account, secretKey); } /** * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{issuer}:{account}?secret={secretKey}&issuer={issuer}) * * @param account 账户名 * @param issuer 签发者 * @param secretKey TOTP 秘钥 * @return / */ public String generateGoogleSecretKey(String account, String issuer, String secretKey) { return StrFormatter.format("otpauth://totp/{}:{}?secret={}&issuer={}", issuer, account, secretKey, issuer); } protected String _generateTOTP(String secretKey, long time) { // Base32解码密钥 byte[] keyBytes = SaBase32Util.decodeStringToBytes(secretKey); byte[] counterBytes = ByteBuffer.allocate(8).putLong(time / timeStep).array(); try { // 计算HMAC哈希 Mac hmac = Mac.getInstance(hmacAlgorithm); hmac.init(new SecretKeySpec(keyBytes, hmacAlgorithm)); byte[] hash = hmac.doFinal(counterBytes); // 动态截断(RFC 6238) int offset = hash[hash.length - 1] & 0xF; int binary = ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16) | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF); // 生成指定位数的验证码 int otp = binary % (int) Math.pow(10, codeDigits); return String.format("%0" + codeDigits + "d", otp); } catch (GeneralSecurityException e) { throw new RuntimeException("TOTP生成失败", e); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.secure.totp; import cn.dev33.satoken.SaManager; /** * TOTP 工具类,支持 生成/验证 动态一次性密码 * * @author click33 * @since 1.42.0 */ public class SaTotpUtil { /** * 生成随机密钥(Base32编码) * * @return / */ public static String generateSecretKey() { return SaManager.getSaTotpTemplate().generateSecretKey(); } /** * 生成当前时间的TOTP验证码 * * @param secretKey Base32编码的密钥 * @return / */ public static String generateTOTP(String secretKey) { return SaManager.getSaTotpTemplate()._generateTOTP(secretKey); } /** * 判断用户输入的TOTP是否有效 * * @param secretKey Base32编码的密钥 * @param code 用户输入的验证码 * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) * @return / */ public static boolean validateTOTP(String secretKey, String code, int timeWindowOffset) { return SaManager.getSaTotpTemplate().validateTOTP(secretKey, code, timeWindowOffset); } /** * 校验用户输入的TOTP是否有效,如果无效则抛出异常 * * @param secretKey Base32编码的密钥 * @param code 用户输入的验证码 * @param timeWindowOffset 允许的时间窗口偏移量(如1表示允许前后各1个时间窗口) */ public static void checkTOTP(String secretKey, String code, int timeWindowOffset) { SaManager.getSaTotpTemplate().checkTOTP(secretKey, code, timeWindowOffset); } /** * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) * * @param account 账户名 * @return / */ public static String generateGoogleSecretKey(String account) { return SaManager.getSaTotpTemplate().generateGoogleSecretKey(account); } /** * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{account}?secret={secretKey}) * * @param account 账户名 * @param secretKey TOTP 秘钥 * @return / */ public static String generateGoogleSecretKey(String account, String secretKey) { return SaManager.getSaTotpTemplate().generateGoogleSecretKey(account, secretKey); } /** * 生成谷歌认证器的扫码字符串 (形如:otpauth://totp/{issuer}:{account}?secret={secretKey}&issuer={issuer}) * * @param account 账户名 * @param issuer 签发者 * @param secretKey TOTP 秘钥 * @return / */ public static String generateGoogleSecretKey(String account, String issuer, String secretKey) { return SaManager.getSaTotpTemplate().generateGoogleSecretKey(account, issuer, secretKey); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/serializer/SaSerializerTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer; /** * 序列化器 * * @author click33 * @since 1.41.0 */ public interface SaSerializerTemplate { /** * 序列化:对象 -> 字符串 * * @param obj / * @return / */ String objectToString(Object obj); /** * 反序列化:字符串 → 对象 * * @param str / * @return / */ Object stringToObject(String str); /** * 反序列化:字符串 → 对象 (指定类型) *

* 此方法目前仅为 json 序列化实现类 在 反序列化对象 传递类型信息 *

* * @param str / * @return / */ @SuppressWarnings("unchecked") default T stringToObject(String str, Class type) { return (T)stringToObject(str); }; /** * 序列化:对象 -> 字节数组 * * @param obj / * @return / */ byte[] objectToBytes(Object obj); /** * 反序列化:字节数组 → 对象 * * @param bytes / * @return / */ Object bytesToObject(byte[] bytes); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdk.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer.impl; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.serializer.SaSerializerTemplate; import java.io.*; /** * 序列化器次级实现: jdk序列化 * * @author click33 * @since 1.41.0 */ public interface SaSerializerTemplateForJdk extends SaSerializerTemplate { @Override default String objectToString(Object obj) { byte[] bytes = objectToBytes(obj); if (bytes == null) { return null; } return bytesToString(bytes); } @Override default Object stringToObject(String str) { if(str == null) { return null; } byte[] bytes = stringToBytes(str); return bytesToObject(bytes); } @Override default byte[] objectToBytes(Object obj) { if (obj == null) { return null; } try ( ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos) ) { oos.writeObject(obj); return baos.toByteArray(); } catch (IOException e) { throw new SaTokenException(e); } } @Override default Object bytesToObject(byte[] bytes) { if(bytes == null) { return null; } try ( ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais) ) { return ois.readObject(); } catch (IOException | ClassNotFoundException e) { throw new SaTokenException(e); } } /** * byte[] 转换为 String * @param bytes / * @return / */ String bytesToString(byte[] bytes); /** * String 转换为 byte[] * @param str / * @return / */ byte[] stringToBytes(String str); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseBase64.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer.impl; import java.util.Base64; /** * 序列化器: jdk序列化、Base64 编码 (体积+33%) * * @author click33 * @since 1.41.0 */ public class SaSerializerTemplateForJdkUseBase64 implements SaSerializerTemplateForJdk { @Override public String bytesToString(byte[] bytes) { return Base64.getEncoder().encodeToString(bytes); } @Override public byte[] stringToBytes(String str) { return Base64.getDecoder().decode(str); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseHex.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer.impl; import cn.dev33.satoken.util.SaHexUtil; /** * 序列化器: jdk序列化、16 进制编码 (体积+100%) * * @author click33 * @since 1.41.0 */ public class SaSerializerTemplateForJdkUseHex implements SaSerializerTemplateForJdk { @Override public String bytesToString(byte[] bytes) { return SaHexUtil.bytesToHex(bytes); } @Override public byte[] stringToBytes(String str) { return SaHexUtil.hexToBytes(str); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseISO_8859_1.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer.impl; import java.nio.charset.StandardCharsets; /** * 序列化器: jdk序列化、ISO-8859-1 编码 (体积无变化) * * @author click33 * @since 1.41.0 */ public class SaSerializerTemplateForJdkUseISO_8859_1 implements SaSerializerTemplateForJdk { @Override public String bytesToString(byte[] bytes) { return new String(bytes, StandardCharsets.ISO_8859_1); } @Override public byte[] stringToBytes(String str) { return str.getBytes(StandardCharsets.ISO_8859_1); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJson.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer.impl; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.serializer.SaSerializerTemplate; /** * 序列化器: 使用 json 转换器 * * @author click33 * @since 1.41.0 */ public class SaSerializerTemplateForJson implements SaSerializerTemplate { @Override public String objectToString(Object obj) { return SaManager.getSaJsonTemplate().objectToJson(obj); } @Override public Object stringToObject(String str) { return SaManager.getSaJsonTemplate().jsonToObject(str); } @Override public T stringToObject(String str, Class type) { return SaManager.getSaJsonTemplate().jsonToObject(str, type); } @Override public byte[] objectToBytes(Object obj) { throw new ApiDisabledException("json 序列化器不支持 Object -> byte[]"); } @Override public Object bytesToObject(byte[] bytes) { throw new ApiDisabledException("json 序列化器不支持 byte[] -> Object"); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.application.SaSetValueInterface; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.fun.SaTwoParamFunction; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * Session Model,会话作用域的读取值对象 * *

在一次会话范围内: 存值、取值。数据在注销登录后失效。

*

* 在 Sa-Token 中,SaSession 分为三种,分别是:
* - Account-Session: 指的是框架为每个 账号id 分配的 SaSession。
* - Token-Session: 指的是框架为每个 token 分配的 SaSession。
* - Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 SaSession。
*
* 注意:以上分类仅为框架设计层面的概念区分,实际上它们的数据存储格式都是一致的。 *

* * @author click33 * @since 1.10.0 */ public class SaSession implements SaSetValueInterface, Serializable { /** * */ private static final long serialVersionUID = 1L; /** * 在 SaSession 上存储用户对象时建议使用的 key */ public static final String USER = "USER"; /** * 在 SaSession 上存储角色列表时建议使用的 key */ public static final String ROLE_LIST = "ROLE_LIST"; /** * 在 SaSession 上存储权限列表时建议使用的 key */ public static final String PERMISSION_LIST = "PERMISSION_LIST"; /** * 此 SaSession 的 id */ private String id; /** * 此 SaSession 的 类型 */ private String type; /** * 所属 loginType */ private String loginType; /** * 所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效) */ private Object loginId; /** * 所属 Token (当此 SaSession 属于 Token-Session 时,此值有效) */ private String token; /** * 当前账号历史总计登录设备数量 (当此 SaSession 属于 Account-Session 时,此值有效) */ private int historyTerminalCount; /** * 此 SaSession 的创建时间(13位时间戳) */ private long createTime; /** * 所有挂载数据 */ private Map dataMap = new ConcurrentHashMap<>(); // ----------------------- 构建相关 /** * 构建一个 Session 对象 */ public SaSession() { /* * 当 Session 从 Redis 中反序列化取出时,框架会误以为创建了新的Session, * 因此此处不可以调用this(null); 避免监听器收到错误的通知 */ // this(null); } /** * 构建一个 Session 对象 * @param id Session的id */ public SaSession(String id) { this.id = id; this.createTime = System.currentTimeMillis(); // $$ 发布事件 SaTokenEventCenter.doCreateSession(id); } /** * 获取:此 SaSession 的 id * @return / */ public String getId() { return this.id; } /** * 写入:此 SaSession 的 id * @param id / * @return 对象自身 */ public SaSession setId(String id) { this.id = id; return this; } /** * 获取:此 SaSession 的 类型 * * @return / */ public String getType() { return this.type; } /** * 设置:此 SaSession 的 类型 * * @param type / * @return 对象自身 */ public SaSession setType(String type) { this.type = type; return this; } /** * 获取:所属 loginType * @return / */ public String getLoginType() { return this.loginType; } /** * 设置:所属 loginType * @param loginType / * @return 对象自身 */ public SaSession setLoginType(String loginType) { this.loginType = loginType; return this; } /** * 获取:所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效) * @return / */ public Object getLoginId() { return this.loginId; } /** * 设置:所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效) * @param loginId / * @return 对象自身 */ public SaSession setLoginId(Object loginId) { this.loginId = loginId; return this; } /** * 获取:所属 Token (当此 SaSession 属于 Token-Session 时,此值有效) * @return / */ public String getToken() { return this.token; } /** * 设置:所属 Token (当此 SaSession 属于 Token-Session 时,此值有效) * @param token / * @return 对象自身 */ public SaSession setToken(String token) { this.token = token; return this; } /** * 返回:当前 SaSession 的创建时间(13位时间戳) * @return / */ public long getCreateTime() { return this.createTime; } /** * 写入:此 SaSession 的创建时间(13位时间戳) * @param createTime / * @return 对象自身 */ public SaSession setCreateTime(long createTime) { this.createTime = createTime; return this; } // ----------------------- SaTerminalInfo 相关 /** * 登录终端信息列表 */ private List terminalList = new Vector<>(); /** * 写入登录终端信息列表 * @param terminalList / */ public void setTerminalList(List terminalList) { this.terminalList = terminalList; } /** * 获取登录终端信息列表 * * @return / */ public List getTerminalList() { return terminalList; } /** * 获取 登录终端信息列表 (拷贝副本) * * @return / */ public List terminalListCopy() { return new ArrayList<>(terminalList); } /** * 获取 登录终端信息列表 (拷贝副本),根据 deviceType 筛选 * * @param deviceType 设备类型,填 null 代表不限设备类型 * @return / */ public List getTerminalListByDeviceType(String deviceType) { // 返回全部 if(deviceType == null) { return terminalListCopy(); } // 返回筛选后的 List copyList = terminalListCopy(); List newList = new ArrayList<>(); for (SaTerminalInfo terminal : copyList) { if(SaFoxUtil.equals(terminal.getDeviceType(), deviceType)) { newList.add(terminal); } } return newList; } /** * 获取 登录终端 token 列表 * * @param deviceType 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public List getTokenValueListByDeviceType(String deviceType) { List tokenValueList = new ArrayList<>(); for (SaTerminalInfo terminal : getTerminalListByDeviceType(deviceType)) { tokenValueList.add(terminal.getTokenValue()); } return tokenValueList; } /** * 查找一个终端信息,根据 tokenValue * * @param tokenValue / * @return / */ public SaTerminalInfo getTerminal(String tokenValue) { for (SaTerminalInfo terminal : terminalListCopy()) { if (SaFoxUtil.equals(terminal.getTokenValue(), tokenValue)) { return terminal; } } return null; } /** * 添加一个终端信息 * * @param terminalInfo / */ public void addTerminal(SaTerminalInfo terminalInfo) { // 根据 tokenValue 值查重,如果存在旧的,则先删除 SaTerminalInfo oldTerminal = getTerminal(terminalInfo.getTokenValue()); if(oldTerminal != null) { terminalList.remove(oldTerminal); } // 然后添加新的 this.historyTerminalCount++; terminalInfo.setIndex(this.historyTerminalCount); terminalList.add(terminalInfo); update(); } /** * 移除一个终端信息 * * @param tokenValue token值 */ public void removeTerminal(String tokenValue) { SaTerminalInfo terminalInfo = getTerminal(tokenValue); if (terminalList.remove(terminalInfo)) { update(); } } /** * 获取 当前账号历史总计登录设备数量 (当此 SaSession 属于 Account-Session 时,此值有效) * * @return / */ public int getHistoryTerminalCount() { return this.historyTerminalCount; } /** * 设置 当前账号历史总计登录设备数量 (当此 SaSession 属于 Account-Session 时,此值有效) * * @param historyTerminalCount / */ public void setHistoryTerminalCount(int historyTerminalCount) { this.historyTerminalCount = historyTerminalCount; } /** * 遍历 terminalList 列表,执行特定函数 * * @param function 需要执行的函数 */ public void forEachTerminalList(SaTwoParamFunction function) { for (SaTerminalInfo terminalInfo: terminalListCopy()) { function.run(this, terminalInfo); } } /** * 判断指定设备 id 是否为可信任设备 * @param deviceId / * @return / */ public boolean isTrustDeviceId(String deviceId) { if(SaFoxUtil.isEmpty(deviceId)) { return false; } for (SaTerminalInfo terminal : terminalListCopy()) { if (SaFoxUtil.equals(terminal.getDeviceId(), deviceId)) { return true; } } return false; } // ----------------------- 一些操作 /** * 更新Session(从持久库更新刷新一下) */ public void update() { SaManager.getSaTokenDao().updateSession(this); } /** 注销Session (从持久库删除) */ public void logout() { SaManager.getSaTokenDao().deleteSession(this.id); // $$ 发布事件 SaTokenEventCenter.doLogoutSession(id); } /** 当 Session 上的 SaTerminalInfo 数量为零时,注销会话 */ public void logoutByTerminalCountToZero() { if (terminalList.isEmpty()) { logout(); } } /** * 获取此Session的剩余存活时间 (单位: 秒) * @return 此Session的剩余存活时间 (单位: 秒) */ public long timeout() { return SaManager.getSaTokenDao().getSessionTimeout(this.id); } /** * 修改此Session的剩余存活时间 * @param timeout 过期时间 (单位: 秒) */ public void updateTimeout(long timeout) { SaManager.getSaTokenDao().updateSessionTimeout(this.id, timeout); } /** * 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改) * @param minTimeout 过期时间 (单位: 秒) */ public void updateMinTimeout(long minTimeout) { long min = trans(minTimeout); long curr = trans(timeout()); if(curr < min) { updateTimeout(minTimeout); } } /** * 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改) * @param maxTimeout 过期时间 (单位: 秒) */ public void updateMaxTimeout(long maxTimeout) { long max = trans(maxTimeout); long curr = trans(timeout()); if(curr > max) { updateTimeout(maxTimeout); } } /** * value为 -1 时返回 Long.MAX_VALUE,否则原样返回 * @param value / * @return / */ protected long trans(long value) { return value == SaTokenDao.NEVER_EXPIRE ? Long.MAX_VALUE : value; } // ----------------------- 存取值 (类型转换) // ---- 重写接口方法 /** * 取值 * @param key key * @return 值 */ @Override public Object get(String key) { return dataMap.get(key); } /** * 写值 * @param key 名称 * @param value 值 * @return 对象自身 */ @Override public SaSession set(String key, Object value) { dataMap.put(key, value); update(); return this; } /** * 写值 (只有在此 key 原本无值的情况下才会写入) * @param key 名称 * @param value 值 * @return 对象自身 */ @Override public SaSession setByNull(String key, Object value) { if( ! has(key)) { dataMap.put(key, value); update(); } return this; } /** * 删值 * @param key 要删除的key * @return 对象自身 */ @Override public SaSession delete(String key) { dataMap.remove(key); update(); return this; } // ----------------------- 其它方法 /** * 返回当前 Session 挂载数据的所有 key * * @return key 列表 */ public Set keys() { return dataMap.keySet(); } /** * 清空所有挂载数据 */ public void clear() { dataMap.clear(); update(); } /** * 获取数据挂载集合(如果更新map里的值,请调用 session.update() 方法避免产生脏数据 ) * * @return 返回底层储存值的map对象 */ public Map getDataMap() { return dataMap; } /** * 设置数据挂载集合 (改变底层对象引用,将 dataMap 整个对象替换) * @param dataMap 数据集合 * * @return 对象自身 */ public SaSession setDataMap(Map dataMap) { this.dataMap = dataMap; return this; } /** * 写入数据集合 (不改变底层对象引用,只将此 dataMap 所有数据进行替换) * @param dataMap 数据集合 */ public SaSession refreshDataMap(Map dataMap) { this.dataMap.clear(); this.dataMap.putAll(dataMap); this.update(); return this; } // } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionCustomUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaTokenConsts; /** * 自定义 SaSession 工具类,快捷的读取、操作自定义 SaSession * *

样例: *

 * 		// 在一处代码写入数据 
 * 		SaSession session = SaSessionCustomUtil.getSessionById("role-" + 1001);
 * 		session.set("count", 1);
 * 	
 * 		// 在另一处代码获取数据 
 * 		SaSession session = SaSessionCustomUtil.getSessionById("role-" + 1001);
 * 		int count = session.getInt("count");
 * 		System.out.println("count=" + count);
 * 
* * @author click33 * @since 1.10.0 */ public class SaSessionCustomUtil { private SaSessionCustomUtil() { } /** * 添加上指定前缀,防止恶意伪造数据 */ public static String sessionKey = "custom"; /** * 拼接Key: 在存储自定义 SaSession 时应该使用的 key * * @param sessionId 会话id * @return sessionId */ public static String splicingSessionKey(String sessionId) { return SaManager.getConfig().getTokenName() + ":" + sessionKey + ":session:" + sessionId; } /** * 判断:指定 key 的 SaSession 是否存在 * * @param sessionId SaSession 的 id * @return 是否存在 */ public static boolean isExists(String sessionId) { return SaManager.getSaTokenDao().getSession(splicingSessionKey(sessionId)) != null; } /** * 获取指定 key 的 SaSession 对象, 如果此 SaSession 尚未在 DB 创建,isCreate 参数代表是否则新建并返回 * * @param sessionId SaSession 的 id * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 * @return SaSession 对象 */ public static SaSession getSessionById(String sessionId, boolean isCreate) { SaSession session = SaManager.getSaTokenDao().getSession(splicingSessionKey(sessionId)); if (session == null && isCreate) { session = SaStrategy.instance.createSession.apply(splicingSessionKey(sessionId)); session.setType(SaTokenConsts.SESSION_TYPE__CUSTOM); SaManager.getSaTokenDao().setSession(session, SaManager.getConfig().getTimeout()); } return session; } /** * 获取指定 key 的 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 * * @param sessionId SaSession 的 id * @return SaSession 对象 */ public static SaSession getSessionById(String sessionId) { return getSessionById(sessionId, true); } /** * 删除指定 key 的 SaSession * * @param sessionId SaSession 的 id */ public static void deleteSessionById(String sessionId) { SaManager.getSaTokenDao().deleteSession(splicingSessionKey(sessionId)); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/session/SaTerminalInfo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; /** * 登录设备终端信息 Model * * @author click33 * @since 1.41.0 */ public class SaTerminalInfo implements Serializable { /** * */ private static final long serialVersionUID = 1406115065849845073L; /** * 登录会话索引值 (该账号第几个登录的设备, 从 1 开始) */ private int index; /** * Token 值 */ private String tokenValue; /** * 所属设备类型,例如:PC、WEB、HD、MOBILE、APP */ private String deviceType; /** * 登录设备唯一标识,例如:kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM */ private String deviceId; /** * 此次登录的自定义扩展数据 (只允许在登录前设定,登录后不建议更改) */ private Map extraData; /** * 创建时间 */ private long createTime; /** * 构建一个 */ public SaTerminalInfo() { } /** * 构建一个 * * @param index 登录会话索引值 (该账号第几个登录的设备) * @param tokenValue Token 值 * @param deviceType 所属设备类型 * @param extraData 此客户端登录的挂载数据 */ public SaTerminalInfo(int index, String tokenValue, String deviceType, Map extraData) { this.index = index; this.tokenValue = tokenValue; this.deviceType = deviceType; this.extraData = extraData; this.createTime = System.currentTimeMillis(); } // 扩展方法 /** * 此次登录的自定义扩展数据 (只允许在登录前设定,登录后不建议更改) * @param key 键 * @param value 值 * @return 对象自身 */ public SaTerminalInfo setExtra(String key, Object value) { if(this.extraData == null) { this.extraData = new LinkedHashMap<>(); } this.extraData.put(key, value); return this; } /** * 此次登录的自定义扩展数据 * @param key 键 * @return 扩展数据的值 */ public Object getExtra(String key) { if(this.extraData == null) { return null; } return this.extraData.get(key); } /** * 判断是否设置了扩展数据 * @return / */ public boolean haveExtraData() { return extraData != null && !extraData.isEmpty(); } // -------------------- get/set -------------------- /** * 获取 登录会话索引值 (该账号第几个登录的设备) * * @return index 登录会话索引值 (该账号第几个登录的设备) */ public int getIndex() { return this.index; } /** * 设置 登录会话索引值 (该账号第几个登录的设备) * * @param index 登录会话索引值 (该账号第几个登录的设备) * @return 对象自身 */ public SaTerminalInfo setIndex(int index) { this.index = index; return this; } /** * @return Token 值 */ public String getTokenValue() { return tokenValue; } /** * 写入 Token 值 * * @param tokenValue / * @return 对象自身 */ public SaTerminalInfo setTokenValue(String tokenValue) { this.tokenValue = tokenValue; return this; } /** * @return 所属设备类型 */ public String getDeviceType() { return deviceType; } /** * 写入所属设备类型 * * @param deviceType / * @return 对象自身 */ public SaTerminalInfo setDeviceType(String deviceType) { this.deviceType = deviceType; return this; } /** * 获取 登录设备唯一标识 * * @return deviceId 登录设备唯一标识 */ public String getDeviceId() { return this.deviceId; } /** * 设置 登录设备唯一标识,例如:kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM * * @param deviceId 登录设备唯一标识,例如:kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM * @return 对象自身 */ public SaTerminalInfo setDeviceId(String deviceId) { this.deviceId = deviceId; return this; } /** * 获取 此客户端登录的挂载数据 * * @return / */ public Map getExtraData() { return this.extraData; } /** * 设置 此客户端登录的挂载数据 * * @param extraData / * @return 对象自身 */ public SaTerminalInfo setExtraData(Map extraData) { this.extraData = extraData; return this; } /** * 获取 创建时间 * * @return createTime 创建时间 */ public long getCreateTime() { return this.createTime; } /** * 设置 创建时间 * * @param createTime 创建时间 * @return 对象自身 */ public SaTerminalInfo setCreateTime(long createTime) { this.createTime = createTime; return this; } // @Override public String toString() { return "SaTerminalInfo [" + "index=" + index + ", tokenValue='" + tokenValue + ", deviceType='" + deviceType + ", deviceId='" + deviceId + ", extraData=" + extraData + ", createTime=" + createTime + ']'; } /* * Expand in the future: * deviceName 登录设备端名称,一般为浏览器名称 * systemName 登录设备操作系统名称 * loginIp 登录IP地址 * address 登录设备地理位置 * loginTime 登录时间 */ } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session.raw; import cn.dev33.satoken.session.SaSession; /** * SaSession 读写工具类 委托 * * @author click33 * @since 1.42.0 */ public class SaRawSessionDelegator { /** * raw session 类型 */ public String type; public SaRawSessionDelegator(String type) { this.type = type; } /** * 判断:指定 SaSession 是否存在 * * @param valueId / * @return 是否存在 */ public boolean isExists(Object valueId) { return SaRawSessionUtil.isExists(type, valueId); } /** * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建,isCreate 参数代表是否则新建并返回 * * @param valueId / * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 * @return SaSession 对象 */ public SaSession getSessionById(Object valueId, boolean isCreate) { return SaRawSessionUtil.getSessionById(type, valueId, isCreate); } /** * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 * * @param valueId / * @return SaSession 对象 */ public SaSession getSessionById(Object valueId) { return SaRawSessionUtil.getSessionById(type, valueId); } /** * 删除指定 SaSession * * @param valueId / */ public void deleteSessionById(Object valueId) { SaRawSessionUtil.deleteSessionById(type, valueId); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session.raw; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.strategy.SaStrategy; /** * SaSession 读写工具类 * * @author click33 * @since 1.42.0 */ public class SaRawSessionUtil { private SaRawSessionUtil() { } /** * 拼接Key: 在存储 SaSession 时应该使用的 key * * @param type 类型 * @param valueId 唯一标识 * @return sessionId */ public static String splicingSessionKey(String type, Object valueId) { return SaManager.getConfig().getTokenName() + ":raw-session:" + type + ":" + valueId; } /** * 判断:指定 SaSession 是否存在 * * @param type / * @param valueId / * @return 是否存在 */ public static boolean isExists(String type, Object valueId) { return SaManager.getSaTokenDao().getSession(splicingSessionKey(type, valueId)) != null; } /** * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建,isCreate 参数代表是否则新建并返回 * * @param type / * @param valueId / * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 * @return SaSession 对象 */ public static SaSession getSessionById(String type, Object valueId, boolean isCreate) { String sessionId = splicingSessionKey(type, valueId); SaSession session = SaManager.getSaTokenDao().getSession(sessionId); if (session == null && isCreate) { session = SaStrategy.instance.createSession.apply(sessionId); session.setType(type); SaManager.getSaTokenDao().setSession(session, SaManager.getConfig().getTimeout()); } return session; } /** * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 * * @param type / * @param valueId / * @return SaSession 对象 */ public static SaSession getSessionById(String type, Object valueId) { return getSessionById(type, valueId, true); } /** * 删除指定 SaSession * * @param type / * @param valueId / */ public static void deleteSessionById(String type, Object valueId) { SaManager.getSaTokenDao().deleteSession(splicingSessionKey(type, valueId)); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import java.util.Map; /** *

请更换为 new SaLoginParameter()

* * 快速、简洁的构建:调用 `StpUtil.login()` 时的 [ 配置参数 SaLoginParameter ] * *
 *     	// 例如:在登录时指定 token 有效期为七天,代码如下:
 *     	StpUtil.login(10001, SaLoginConfig.setTimeout(60 * 60 * 24 * 7));
 *
 *     	// 上面的代码与下面的代码等价
 *     	StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));
 * 
* * @author click33 * @since 1.29.0 */ @Deprecated public class SaLoginConfig { private SaLoginConfig() { } /** * @param device 此次登录的客户端设备类型 * @return 登录参数 Model */ public static SaLoginParameter setDevice(String device) { return create().setDeviceType(device); } /** * @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) * @return 登录参数 Model */ public static SaLoginParameter setIsLastingCookie(Boolean isLastingCookie) { return create().setIsLastingCookie(isLastingCookie); } /** * @param timeout 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值) * @return 登录参数 Model */ public static SaLoginParameter setTimeout(long timeout) { return create().setTimeout(timeout); } /** * @param activeTimeout 指定此次登录 token 最低活跃频率,单位:秒(如未指定,自动取全局配置的 activeTimeout 值) * @return 对象自身 */ public static SaLoginParameter setActiveTimeout(long activeTimeout) { return create().setActiveTimeout(activeTimeout); } /** * @param extraData 扩展信息(只在jwt模式下生效) * @return 登录参数 Model */ public static SaLoginParameter setExtraData(Map extraData) { return create().setExtraData(extraData); } /** * @param token 预定Token(预定本次登录生成的Token值) * @return 登录参数 Model */ public static SaLoginParameter setToken(String token) { return create().setToken(token); } /** * 写入扩展数据(只在jwt模式下生效) * @param key 键 * @param value 值 * @return 登录参数 Model */ public static SaLoginParameter setExtra(String key, Object value) { return create().setExtra(key, value); } /** * @param isWriteHeader 是否在登录后将 Token 写入到响应头 * @return 登录参数 Model */ public static SaLoginParameter setIsWriteHeader(Boolean isWriteHeader) { return create().setIsWriteHeader(isWriteHeader); } /** * 设置 本次登录挂载到 TokenSign 的数据 * * @param tokenSignTag / * @return 登录参数 Model */ public static SaLoginParameter setTokenSignTag(Map tokenSignTag) { return create().setTerminalExtraData(tokenSignTag); } /** * 静态方法获取一个 SaLoginParameter 对象 * @return SaLoginParameter 对象 */ public static SaLoginParameter create() { return new SaLoginParameter(SaManager.getConfig()); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; import cn.dev33.satoken.stp.parameter.SaLoginParameter; /** *

请更改为 SaLoginParameter

* 在调用 `StpUtil.login()` 时的 配置参数 Model,决定登录的一些细节行为
* * @author click33 * @since 1.13.2 */ @Deprecated public class SaLoginModel extends SaLoginParameter { } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/SaTokenInfo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; /** * Token 信息 Model: 用来描述一个 Token 的常见参数。 * *

* 例如:
*

 *     {
 *         "tokenName": "satoken",           // token名称
 *         "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633",      // token值
 *         "isLogin": true,                  // 此token是否已经登录
 *         "loginId": "10001",               // 此token对应的LoginId,未登录时为null
 *         "loginType": "login",              // 账号类型标识
 *         "tokenTimeout": 2591977,          // token剩余有效期 (单位: 秒)
 *         "sessionTimeout": 2591977,        // Account-Session剩余有效时间 (单位: 秒)
 *         "tokenSessionTimeout": -2,        // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
 *         "tokenActiveTimeout": -1,       // Token 距离被冻结还剩多少时间 (单位: 秒)
 *         "loginDevice": "DEF"   // 登录设备类型
 *     }
 *     
*

* * @author click33 * @since 1.10.0 */ public class SaTokenInfo { /** token 名称 */ public String tokenName; /** token 值 */ public String tokenValue; /** 此 token 是否已经登录 */ public Boolean isLogin; /** 此 token 对应的 LoginId,未登录时为 null */ public Object loginId; /** 多账号体系下的账号类型 */ public String loginType; /** token 剩余有效期(单位: 秒) */ public long tokenTimeout; /** Account-Session 剩余有效时间(单位: 秒) */ public long sessionTimeout; /** Token-Session 剩余有效时间(单位: 秒) */ public long tokenSessionTimeout; /** token 距离被冻结还剩多少时间(单位: 秒) */ public long tokenActiveTimeout; /** 登录设备类型 */ public String loginDeviceType; /** 自定义数据(暂无意义,留作扩展) */ public String tag; /** * @return token 名称 */ public String getTokenName() { return tokenName; } /** * @param tokenName token 名称 */ public void setTokenName(String tokenName) { this.tokenName = tokenName; } /** * @return token 值 */ public String getTokenValue() { return tokenValue; } /** * @param tokenValue token 值 */ public void setTokenValue(String tokenValue) { this.tokenValue = tokenValue; } /** * @return 此 token 是否已经登录 */ public Boolean getIsLogin() { return isLogin; } /** * @param isLogin 此 token 是否已经登录 */ public void setIsLogin(Boolean isLogin) { this.isLogin = isLogin; } /** * @return 此 token 对应的LoginId,未登录时为null */ public Object getLoginId() { return loginId; } /** * @param loginId 此 token 对应的LoginId,未登录时为null */ public void setLoginId(Object loginId) { this.loginId = loginId; } /** * @return 多账号体系下的账号类型 */ public String getLoginType() { return loginType; } /** * @param loginType 多账号体系下的账号类型 */ public void setLoginType(String loginType) { this.loginType = loginType; } /** * @return token 剩余有效期(单位: 秒) */ public long getTokenTimeout() { return tokenTimeout; } /** * @param tokenTimeout token剩余有效期(单位: 秒) */ public void setTokenTimeout(long tokenTimeout) { this.tokenTimeout = tokenTimeout; } /** * @return Account-Session 剩余有效时间(单位: 秒) */ public long getSessionTimeout() { return sessionTimeout; } /** * @param sessionTimeout Account-Session剩余有效时间(单位: 秒) */ public void setSessionTimeout(long sessionTimeout) { this.sessionTimeout = sessionTimeout; } /** * @return Token-Session剩余有效时间(单位: 秒) */ public long getTokenSessionTimeout() { return tokenSessionTimeout; } /** * @param tokenSessionTimeout Token-Session剩余有效时间(单位: 秒) */ public void setTokenSessionTimeout(long tokenSessionTimeout) { this.tokenSessionTimeout = tokenSessionTimeout; } /** * @return token 距离被冻结还剩多少时间(单位: 秒) */ public long getTokenActiveTimeout() { return tokenActiveTimeout; } /** * @param tokenActiveTimeout token 距离被冻结还剩多少时间(单位: 秒) */ public void setTokenActiveTimeout(long tokenActiveTimeout) { this.tokenActiveTimeout = tokenActiveTimeout; } /** * @return 登录设备类型 */ public String getLoginDeviceType() { return loginDeviceType; } /** * @param loginDeviceType 登录设备类型 */ public void setLoginDeviceType(String loginDeviceType) { this.loginDeviceType = loginDeviceType; } /** * @return 自定义数据(暂无意义,留作扩展) */ public String getTag() { return tag; } /** * @param tag 自定义数据(暂无意义,留作扩展) */ public void setTag(String tag) { this.tag = tag; } /** * toString */ @Override public String toString() { return "SaTokenInfo [tokenName=" + tokenName + ", tokenValue=" + tokenValue + ", isLogin=" + isLogin + ", loginId=" + loginId + ", loginType=" + loginType + ", tokenTimeout=" + tokenTimeout + ", sessionTimeout=" + sessionTimeout + ", tokenSessionTimeout=" + tokenSessionTimeout + ", tokenActiveTimeout=" + tokenActiveTimeout + ", loginDeviceType=" + loginDeviceType + ", tag=" + tag + "]"; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterface.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo; import java.util.List; /** * 权限数据源加载接口 * *

* 在使用权限校验 API 之前,你必须实现此接口,告诉框架哪些用户拥有哪些权限。
* 框架默认不对数据进行缓存,如果你的数据是从数据库中读取的,一般情况下你需要手动实现数据的缓存读写。 *

* * @author click33 * @since 1.10.0 */ public interface StpInterface { /** * 返回指定账号id所拥有的权限码集合 * * @param loginId 账号id * @param loginType 账号类型 * @return 该账号id具有的权限码集合 */ List getPermissionList(Object loginId, String loginType); /** * 返回指定账号id所拥有的角色标识集合 * * @param loginId 账号id * @param loginType 账号类型 * @return 该账号id具有的角色标识集合 */ List getRoleList(Object loginId, String loginType); /** * 返回指定账号 id 是否被封禁 * * @param loginId 账号id * @param service 业务标识符 * @return 描述该账号是否封禁的包装信息对象 */ default SaDisableWrapperInfo isDisabled(Object loginId, String service) { return SaDisableWrapperInfo.createNotDisabled(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterfaceDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; import java.util.ArrayList; import java.util.List; /** * 对 {@link StpInterface} 接口默认的实现类 *

* 如果开发者没有实现 StpInterface 接口,则框架会使用此默认实现类,所有方法都返回空集合,即:用户不具有任何权限和角色。 * * @author click33 * @since 1.10.0 */ public class StpInterfaceDefaultImpl implements StpInterface { @Override public List getPermissionList(Object loginId, String loginType) { return new ArrayList<>(); } @Override public List getRoleList(Object loginId, String loginType) { return new ArrayList<>(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaCookieConfig; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaCookie; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.*; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaTwoParamFunction; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; import cn.dev33.satoken.stp.parameter.enums.SaLogoutRange; import cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode; import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTokenConsts; import cn.dev33.satoken.util.SaValue2Box; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; import static cn.dev33.satoken.exception.NotLoginException.*; /** * Sa-Token 权限认证,逻辑实现类 * *

* Sa-Token 的核心,框架大多数功能均由此类提供具体逻辑实现。 *

* * @author click33 * @since 1.10.0 */ public class StpLogic { /** * 账号类型标识,多账号体系时(一个系统多套用户表)用此值区分具体要校验的是哪套用户,比如:login、user、admin */ public String loginType; /** * 初始化 StpLogic, 并指定账号类型 * * @param loginType 账号类型标识 */ public StpLogic(String loginType) { setLoginType(loginType); } /** * 获取当前 StpLogic 账号类型标识 * * @return / */ public String getLoginType(){ return loginType; } /** * 安全的重置当前账号类型 * * @param loginType 账号类型标识 * @return 对象自身 */ public StpLogic setLoginType(String loginType){ // 先清除此 StpLogic 在全局 SaManager 中的记录 if(SaFoxUtil.isNotEmpty(this.loginType)) { SaManager.removeStpLogic(this.loginType); } // 赋值 this.loginType = loginType; // 将新的 loginType -> StpLogic 映射关系 put 到 SaManager 全局集合中,以便后续根据 LoginType 进行查找此对象 SaManager.putStpLogic(this); return this; } private SaTokenConfig config; /** * 写入当前 StpLogic 单独使用的配置对象 * * @param config 配置对象 * @return 对象自身 */ public StpLogic setConfig(SaTokenConfig config) { this.config = config; return this; } /** * 返回当前 StpLogic 使用的配置对象,如果当前 StpLogic 没有配置,则返回 null * * @return / */ public SaTokenConfig getConfig() { return config; } /** * 返回当前 StpLogic 使用的配置对象,如果当前 StpLogic 没有配置,则返回全局配置对象 * * @return / */ public SaTokenConfig getConfigOrGlobal() { SaTokenConfig cfg = getConfig(); if(cfg != null) { return cfg; } return SaManager.getConfig(); } // ------------------- 获取 token 相关 ------------------- /** * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 * * @return / */ public String getTokenName() { return splicingKeyTokenName(); } /** * 为指定账号创建一个 token (只是把 token 创建出来,并不持久化存储) * * @param loginId 账号id * @param deviceType 设备类型 * @param timeout 过期时间 * @param extraData 扩展信息 * @return 生成的tokenValue */ public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { return SaStrategy.instance.createToken.apply(loginId, loginType); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 */ public void setTokenValue(String tokenValue){ setTokenValue(tokenValue, createSaLoginParameter()); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param cookieTimeout Cookie存活时间(秒) */ public void setTokenValue(String tokenValue, int cookieTimeout){ setTokenValue(tokenValue, createSaLoginParameter().setTimeout(cookieTimeout)); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param loginParameter 登录参数 */ public void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ // 先判断一下,如果提供 token 为空,则不执行任何动作 if(SaFoxUtil.isEmpty(tokenValue)) { return; } // 1、将 token 写入到当前请求的 Storage 存储器里 setTokenValueToStorage(tokenValue); // 2. 将 token 写入到当前会话的 Cookie 里 if (getConfigOrGlobal().getIsReadCookie()) { setTokenValueToCookie(tokenValue, loginParameter.getCookie(), loginParameter.getCookieTimeout()); } // 3. 将 token 写入到当前请求的响应头中 if(loginParameter.getIsWriteHeader()) { setTokenValueToResponseHeader(tokenValue); } } /** * 将 token 写入到当前请求的 Storage 存储器里 * * @param tokenValue 要保存的 token 值 */ public void setTokenValueToStorage(String tokenValue){ // 1、获取当前请求的 Storage 存储器 SaStorage storage = SaHolder.getStorage(); // 2、保存 token // - 如果没有配置前缀模式,直接保存 // - 如果配置了前缀模式,则拼接上前缀保存 String tokenPrefix = getConfigOrGlobal().getTokenPrefix(); if( SaFoxUtil.isEmpty(tokenPrefix) ) { storage.set(splicingKeyJustCreatedSave(), tokenValue); } else { storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue); } // 3、以无前缀的方式再写入一次 storage.set(SaTokenConsts.JUST_CREATED_NOT_PREFIX, tokenValue); } /** * 将 token 写入到当前会话的 Cookie 里 * * @param tokenValue token 值 * @param cookieTimeout Cookie存活时间(单位:秒,填-1代表为内存Cookie,浏览器关闭后消失) */ public void setTokenValueToCookie(String tokenValue, int cookieTimeout){ setTokenValueToCookie(tokenValue, null, cookieTimeout); } /** * 将 token 写入到当前会话的 Cookie 里 * * @param tokenValue token 值 * @param cookieConfig Cookie 配置项 * @param cookieTimeout Cookie存活时间(单位:秒,填-1代表为内存Cookie,浏览器关闭后消失) */ public void setTokenValueToCookie(String tokenValue, SaCookieConfig cookieConfig, int cookieTimeout){ if(cookieConfig == null) { cookieConfig = getConfigOrGlobal().getCookie(); } SaCookie cookie = new SaCookie() .setName(getTokenName()) .setValue(tokenValue) .setMaxAge(cookieTimeout) .setDomain(cookieConfig.getDomain()) .setPath(cookieConfig.getPath()) .setSecure(cookieConfig.getSecure()) .setHttpOnly(cookieConfig.getHttpOnly()) .setSameSite(cookieConfig.getSameSite()) .setExtraAttrs(cookieConfig.getExtraAttrs()) ; SaHolder.getResponse().addCookie(cookie); } /** * 将 token 写入到当前请求的响应头中 * * @param tokenValue token 值 */ public void setTokenValueToResponseHeader(String tokenValue){ // 写入到响应头 String tokenName = getTokenName(); SaResponse response = SaHolder.getResponse(); response.setHeader(tokenName, tokenValue); // 此处必须在响应头里指定 Access-Control-Expose-Headers: token-name,否则前端无法读取到这个响应头 response.addHeader(SaResponse.ACCESS_CONTROL_EXPOSE_HEADERS, tokenName); } /** * 获取当前请求的 token 值 * * @return 当前tokenValue */ public String getTokenValue(){ return getTokenValue(false); } /** * 获取当前请求的 token 值 * * @param noPrefixThrowException 如果提交的 token 不带有指定的前缀,是否抛出异常 * @return 当前tokenValue */ public String getTokenValue(boolean noPrefixThrowException){ // 1、获取前端提交的 token (包含前缀值) String tokenValue = getTokenValueNotCut(); // 2、如果全局配置打开了前缀模式,则二次处理一下 String tokenPrefix = getConfigOrGlobal().getTokenPrefix(); if(SaFoxUtil.isNotEmpty(tokenPrefix)) { // 情况2.1:如果提交的 token 为空,则转为 null if(SaFoxUtil.isEmpty(tokenValue)) { tokenValue = null; } // 情况2.2:如果 token 有值,但是并不是以指定的前缀开头 else if(! tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT)) { if(noPrefixThrowException) { throw NotLoginException.newInstance(loginType, NO_PREFIX, NO_PREFIX_MESSAGE + ",prefix=" + tokenPrefix, null).setCode(SaErrorCode.CODE_11017); } else { tokenValue = null; } } // 情况2.3:代码至此,说明 token 有值,且是以指定的前缀开头的,现在裁剪掉前缀 else { tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length()); } } // 3、返回 return tokenValue; } /** * 获取当前请求的 token 值 (不裁剪前缀) * * @return / */ public String getTokenValueNotCut(){ // 获取相应对象 SaStorage storage = SaHolder.getStorage(); SaRequest request = SaHolder.getRequest(); SaTokenConfig config = getConfigOrGlobal(); String keyTokenName = getTokenName(); String tokenValue = null; // 1. 先尝试从 Storage 存储器里读取 if(storage.get(splicingKeyJustCreatedSave()) != null) { tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave())); } // 2. 再尝试从 请求体 里面读取 if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadBody()){ tokenValue = request.getParam(keyTokenName); } // 3. 再尝试从 header 头里读取 if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadHeader()){ tokenValue = request.getHeader(keyTokenName); } // 4. 最后尝试从 cookie 里读取 if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadCookie()){ tokenValue = request.getCookieValue(keyTokenName); if(SaFoxUtil.isNotEmpty(tokenValue) && config.getCookieAutoFillPrefix()) { tokenValue = config.getTokenPrefix() + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue; } } // 5. 至此,不管有没有读取到,都不再尝试了,直接返回 return tokenValue; } /** * 获取当前请求的 token 值,如果获取不到则抛出异常 * * @return / */ public String getTokenValueNotNull(){ String tokenValue = getTokenValue(true); if(SaFoxUtil.isEmpty(tokenValue)) { throw NotLoginException.newInstance(loginType, NOT_TOKEN, NOT_TOKEN_MESSAGE, null).setCode(SaErrorCode.CODE_11001); } return tokenValue; } /** * 获取当前会话的 token 参数信息 * * @return token 参数信息 */ public SaTokenInfo getTokenInfo() { SaTokenInfo info = new SaTokenInfo(); info.tokenName = getTokenName(); info.tokenValue = getTokenValue(); info.isLogin = isLogin(); info.loginId = getLoginIdDefaultNull(); info.loginType = getLoginType(); info.tokenTimeout = getTokenTimeout(); info.sessionTimeout = getSessionTimeout(); info.tokenSessionTimeout = getTokenSessionTimeout(); info.tokenActiveTimeout = getTokenActiveTimeout(); info.loginDeviceType = getLoginDeviceType(); return info; } // ------------------- 登录相关操作 ------------------- // --- 登录 /** * 会话登录 * * @param id 账号id,建议的类型:(long | int | String) */ public void login(Object id) { login(id, createSaLoginParameter()); } /** * 会话登录,并指定登录设备类型 * * @param id 账号id,建议的类型:(long | int | String) * @param deviceType 设备类型 */ public void login(Object id, String deviceType) { login(id, createSaLoginParameter().setDeviceType(deviceType)); } /** * 会话登录,并指定是否 [记住我] * * @param id 账号id,建议的类型:(long | int | String) * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public void login(Object id, boolean isLastingCookie) { login(id, createSaLoginParameter().setIsLastingCookie(isLastingCookie)); } /** * 会话登录,并指定此次登录 token 的有效期, 单位:秒 * * @param id 账号id,建议的类型:(long | int | String) * @param timeout 此次登录 token 的有效期, 单位:秒 */ public void login(Object id, long timeout) { login(id, createSaLoginParameter().setTimeout(timeout)); } /** * 会话登录,并指定所有登录参数 Model * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model */ public void login(Object id, SaLoginParameter loginParameter) { // 1、创建会话 String token = createLoginSession(id, loginParameter); // 2、在当前客户端注入 token setTokenValue(token, loginParameter); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public String createLoginSession(Object id) { return createLoginSession(id, createSaLoginParameter()); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model * @return 返回会话令牌 */ public String createLoginSession(Object id, SaLoginParameter loginParameter) { // 1、先检查一下,传入的参数是否有效 checkLoginArgs(id, loginParameter); // 2、给这个账号分配一个可用的 token String tokenValue = distUsableToken(id, loginParameter); // 3、获取此账号的 Account-Session , 续期 SaSession session = getSessionByLoginId(id, true, loginParameter.getTimeout()); session.updateMinTimeout(loginParameter.getTimeout()); // 4、在 Account-Session 上记录本次登录的终端信息 SaTerminalInfo terminalInfo = new SaTerminalInfo() .setDeviceType(loginParameter.getDeviceType()) .setDeviceId(loginParameter.getDeviceId()) .setTokenValue(tokenValue) .setExtraData(loginParameter.getTerminalExtraData()) .setCreateTime(System.currentTimeMillis()); session.addTerminal(terminalInfo); // 5、保存 token -> id 的映射关系,方便日后根据 token 找账号 id saveTokenToIdMapping(tokenValue, id, loginParameter.getTimeout()); // 6、写入这个 token 的最后活跃时间 token-last-active if(isOpenCheckActiveTimeout()) { setLastActiveToNow(tokenValue, loginParameter.getActiveTimeout(), loginParameter.getTimeout()); } // 7、如果该 token 对应的 Token-Session 已经存在,则需要给其续期 SaSession tokenSession = getTokenSessionByToken(tokenValue, loginParameter.getRightNowCreateTokenSession()); if(tokenSession != null) { tokenSession.updateMinTimeout(loginParameter.getTimeout()); } // 8、$$ 发布全局事件:账号 xxx 登录成功 SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginParameter); // 9、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉 if(loginParameter.getMaxLoginCount() != -1) { logoutByMaxLoginCount(id, session, null, loginParameter.getMaxLoginCount(), loginParameter.getOverflowLogoutMode()); } // 10、一切处理完毕,返回会话凭证 token return tokenValue; } /** * 为指定账号 id 的登录操作,分配一个可用的 token * * @param id 账号id * @param loginParameter 此次登录的参数Model * @return 返回 token */ protected String distUsableToken(Object id, SaLoginParameter loginParameter) { // 1、获取全局配置的 isConcurrent 参数 if( ! loginParameter.getIsConcurrent()) { // 如果配置为:不允许一个账号多地同时登录,则需要根据配置选择: // 一.将这个账号的历史登录会话标记为:被顶下线 // 二.提示错误并拒绝本次登录 if (loginParameter.getReplacedLoginExitMode() == SaReplacedLoginExitMode.OLD_DEVICE){ if(loginParameter.getReplacedRange() == SaReplacedRange.CURR_DEVICE_TYPE) { replaced(id, loginParameter.getDeviceType()); } if(loginParameter.getReplacedRange() == SaReplacedRange.ALL_DEVICE_TYPE) { replaced(id, createSaLogoutParameter()); } } else if (loginParameter.getReplacedLoginExitMode() == SaReplacedLoginExitMode.NEW_DEVICE){ List terminalListByLoginId = getTerminalListByLoginId(id); // 只有当存在有效地会话时才拒绝登录 boolean hasActiveSession = terminalListByLoginId.stream() .anyMatch(terminal -> isValidToken(terminal.getTokenValue())); if (hasActiveSession) { throw new SaTokenException("登录失败:当前账号已在其它客户端登录").setCode(SaErrorCode.CODE_11004); } } } // 2、如果调用者预定了要生成的 token,则直接返回这个预定的值,框架无需再操心了 if(SaFoxUtil.isNotEmpty(loginParameter.getToken())) { return loginParameter.getToken(); } // 3、只有在配置了 [ 允许一个账号多地同时登录 ] 时,才尝试复用旧 token,这样可以避免不必要地查询,节省开销 if(loginParameter.getIsConcurrent()) { // 3.1、如果配置了允许复用旧 token if(isSupportShareToken() && loginParameter.getIsShare()) { // 根据 账号id + 设备类型,尝试获取旧的 token String tokenValue = getTokenValueByLoginId(id, loginParameter.getDeviceType()); // 如果有值,那就直接复用 if(SaFoxUtil.isNotEmpty(tokenValue)) { return tokenValue; } // 如果没值,那还是要继续往下走,尝试新建 token // ↓↓↓ } } // 4、如果代码走到此处,说明未能成功复用旧 token,需要根据算法新建 token return SaStrategy.instance.generateUniqueToken.execute( "token", getConfigOfMaxTryTimes(loginParameter), () -> { return createTokenValue(id, loginParameter.getDeviceType(), loginParameter.getTimeout(), loginParameter.getExtraData()); }, tokenValue -> { return getLoginIdNotHandle(tokenValue) == null; } ); } /** * 校验登录时的参数有效性,如果有问题会打印警告或抛出异常 * * @param id 账号id * @param loginParameter 此次登录的参数Model */ protected void checkLoginArgs(Object id, SaLoginParameter loginParameter) { // 1、账号 id 不能为空 if(SaFoxUtil.isEmpty(id)) { throw new SaTokenException("loginId 不能为空").setCode(SaErrorCode.CODE_11002); } // 2、账号 id 不能是异常标记值 if(NotLoginException.ABNORMAL_LIST.contains(id.toString())) { throw new SaTokenException("loginId 不能为以下值:" + NotLoginException.ABNORMAL_LIST); } // 3、账号 id 不能是复杂类型 if( ! SaFoxUtil.isBasicType(id.getClass())) { SaManager.log.warn("loginId 应该为简单类型,例如:String | int | long,不推荐使用复杂类型:" + id.getClass()); } // 4、判断当前 StpLogic 是否支持 extra 扩展参数 if( ! isSupportExtra()) { // 如果不支持,开发者却传入了 extra 扩展参数,那么就打印警告信息 if(loginParameter.haveExtraData()) { SaManager.log.warn("当前 StpLogic 不支持 extra 扩展参数模式,传入的 extra 参数将被忽略"); } } // 5、如果全局配置未启动动态 activeTimeout 功能,但是此次登录却传入了 activeTimeout 参数,那么就打印警告信息 if( ! getConfigOrGlobal().getDynamicActiveTimeout() && loginParameter.getActiveTimeout() != null) { SaManager.log.warn("当前全局配置未开启动态 activeTimeout 功能,传入的 activeTimeout 参数将被忽略"); } } /** * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public String getOrCreateLoginSession(Object id) { String tokenValue = getTokenValueByLoginId(id); if(tokenValue == null) { tokenValue = createLoginSession(id, createSaLoginParameter()); } return tokenValue; } // --- 注销 (根据 token) /** * 在当前客户端会话注销 */ public void logout() { logout(createSaLogoutParameter()); } /** * 在当前客户端会话注销,根据注销参数 */ public void logout(SaLogoutParameter logoutParameter) { // 1、如果本次请求连 Token 都没有提交,那么它本身也不属于登录状态,此时无需执行任何操作 String tokenValue = getTokenValue(); if(SaFoxUtil.isEmpty(tokenValue)) { return; } // 2、如果打开了 Cookie 模式,则先把 Cookie 数据清除掉 if(getConfigOrGlobal().getIsReadCookie()){ SaCookieConfig cfg = getConfigOrGlobal().getCookie(); SaCookie cookie = new SaCookie() .setName(getTokenName()) .setValue(null) // 有效期指定为0,做到以增代删 .setMaxAge(0) .setDomain(cfg.getDomain()) .setPath(cfg.getPath()) .setSecure(cfg.getSecure()) .setHttpOnly(cfg.getHttpOnly()) .setSameSite(cfg.getSameSite()) ; SaHolder.getResponse().addCookie(cookie); } // 3、然后从当前 Storage 存储器里删除 Token SaStorage storage = SaHolder.getStorage(); storage.delete(splicingKeyJustCreatedSave()); // 4、清除当前上下文的 [ 活跃度校验 check 标记 ] storage.delete(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY); // 5、清除这个 token 的其它相关信息 if(logoutParameter.getRange() == SaLogoutRange.TOKEN) { logoutByTokenValue(tokenValue, logoutParameter); } else { Object loginId = getLoginIdByTokenNotThinkFreeze(tokenValue); if(loginId != null) { if(!logoutParameter.getIsKeepFreezeOps() && isFreeze(tokenValue)) { return; } logout(loginId, logoutParameter); } } } /** * 注销下线,根据指定 token * * @param tokenValue 指定 token */ public void logoutByTokenValue(String tokenValue) { logoutByTokenValue(tokenValue, createSaLogoutParameter()); } /** * 注销下线,根据指定 token、注销参数 * * @param tokenValue 指定 token * @param logoutParameter / */ public void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { _logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.LOGOUT)); } /** * 踢人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token */ public void kickoutByTokenValue(String tokenValue) { kickoutByTokenValue(tokenValue, createSaLogoutParameter()); } /** * 踢人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token * @param logoutParameter 注销参数 */ public void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { _logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.KICKOUT)); } /** * 顶人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token */ public void replacedByTokenValue(String tokenValue) { replacedByTokenValue(tokenValue, createSaLogoutParameter()); } /** * 顶人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token * @param logoutParameter / */ public void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { _logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.REPLACED)); } /** * [work] 注销下线,根据指定 token 、注销参数 * * @param tokenValue 指定 token * @param logoutParameter 注销参数 */ public void _logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { // 1、判断一下:如果此 token 映射的是一个无效 loginId,则此处立即返回,不需要再往下处理了 // 如果不提前截止,则后续的操作可能会写入意外数据 Object loginId = getLoginIdByTokenNotThinkFreeze(tokenValue); if( SaFoxUtil.isEmpty(loginId) ) { return; } if(!logoutParameter.getIsKeepFreezeOps() && isFreeze(tokenValue)) { return; } // 2、清除这个 token 的最后活跃时间记录 if(isOpenCheckActiveTimeout()) { clearLastActive(tokenValue); } // 3、清除 Token-Session if( ! logoutParameter.getIsKeepTokenSession()) { deleteTokenSession(tokenValue); } // 4、清理或更改 Token 映射 // 5、发布事件通知 // SaLogoutMode.LOGOUT:注销下线 if(logoutParameter.getMode() == SaLogoutMode.LOGOUT) { deleteTokenToIdMapping(tokenValue); SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); } // SaLogoutMode.LOGOUT:踢人下线 if(logoutParameter.getMode() == SaLogoutMode.KICKOUT) { updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT); SaTokenEventCenter.doKickout(loginType, loginId, tokenValue); } // SaLogoutMode.REPLACED:顶人下线 if(logoutParameter.getMode() == SaLogoutMode.REPLACED) { updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED); SaTokenEventCenter.doReplaced(loginType, loginId, tokenValue); } // 6、清理这个账号的 Account-Session 上的 terminal 信息,并且尝试注销掉 Account-Session SaSession session = getSessionByLoginId(loginId, false); if(session != null) { session.removeTerminal(tokenValue); session.logoutByTerminalCountToZero(); } } // --- 注销 (根据 loginId) /** * 会话注销,根据账号id * * @param loginId 账号id */ public void logout(Object loginId) { logout(loginId, createSaLogoutParameter()); } /** * 会话注销,根据账号id 和 设备类型 * * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) */ public void logout(Object loginId, String deviceType) { logout(loginId, createSaLogoutParameter().setDeviceType(deviceType)); } /** * 会话注销,根据账号id 和 注销参数 * * @param loginId 账号id * @param logoutParameter 注销参数 */ public void logout(Object loginId, SaLogoutParameter logoutParameter) { _logout(loginId, logoutParameter.setMode(SaLogoutMode.LOGOUT)); } /** * 踢人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id */ public void kickout(Object loginId) { kickout(loginId, createSaLogoutParameter()); } /** * 踢人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) */ public void kickout(Object loginId, String deviceType) { kickout(loginId, createSaLogoutParameter().setDeviceType(deviceType)); } /** * 踢人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public void kickout(Object loginId, SaLogoutParameter logoutParameter) { _logout(loginId, logoutParameter.setMode(SaLogoutMode.KICKOUT)); } /** * 顶人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id */ public void replaced(Object loginId) { replaced(loginId, createSaLogoutParameter()); } /** * 顶人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) */ public void replaced(Object loginId, String deviceType) { replaced(loginId, createSaLogoutParameter().setDeviceType(deviceType)); } /** * 顶人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public void replaced(Object loginId, SaLogoutParameter logoutParameter) { _logout(loginId, logoutParameter.setMode(SaLogoutMode.REPLACED)); } /** * [work] 会话注销,根据账号id 和 注销参数 * * @param loginId 账号id * @param logoutParameter 注销参数 */ public void _logout(Object loginId, SaLogoutParameter logoutParameter) { // 1、获取此账号的 Account-Session,上面记录了此账号的所有登录客户端数据 SaSession session = getSessionByLoginId(loginId, false); if(session != null) { // 2、遍历此 SaTerminalInfo 客户端列表,清除相关数据 List terminalList = session.terminalListCopy(); for (SaTerminalInfo terminal: terminalList) { // 不符合 deviceType 的跳过 if( ! SaFoxUtil.isEmpty(logoutParameter.getDeviceType()) && ! logoutParameter.getDeviceType().equals(terminal.getDeviceType())) { continue; } // 不符合 deviceId 的跳过 if( ! SaFoxUtil.isEmpty(logoutParameter.getDeviceId()) && ! logoutParameter.getDeviceId().equals(terminal.getDeviceId())) { continue; } _removeTerminal(session, terminal, logoutParameter); } // 3、如果代码走到这里的时候,此账号已经没有客户端在登录了,则直接注销掉这个 Account-Session if(logoutParameter.getMode() == SaLogoutMode.REPLACED) { // 因为调用顶替下线时,一般都是在新客户端正在登录,所以此种情况不需要清除该账号的 Account-Session // 如果清除了 Account-Session,将可能导致 Account-Session 被注销后又立刻创建出来,造成不必要的性能浪费 } else { session.logoutByTerminalCountToZero(); } } } // --- 注销 (会话管理辅助方法) /** * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) * @param session / * @param terminal / */ public void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { _removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.LOGOUT)); } /** * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) * @param session / * @param terminal / */ public void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { _removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.KICKOUT)); } /** * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) * @param session / * @param terminal / */ public void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { _removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.REPLACED)); } /** * 在 Account-Session 上移除 Terminal 信息 (内部方法,仅为减少重复代码,外部调用意义不大) * @param session Account-Session * @param terminal 设备信息 * @param logoutParameter 注销参数 */ public void _removeTerminal(SaSession session, SaTerminalInfo terminal, SaLogoutParameter logoutParameter) { Object loginId = session.getLoginId(); String tokenValue = terminal.getTokenValue(); // 1、从 Account-Session 上清除此设备信息 session.removeTerminal(tokenValue); // 2、清除这个 token 的最后活跃时间记录 if(isOpenCheckActiveTimeout()) { clearLastActive(tokenValue); } // 3、清除这个 token 的 Token-Session 对象 if( ! logoutParameter.getIsKeepTokenSession()) { deleteTokenSession(tokenValue); } // 4、清理或更改 Token 映射 // 5、发布事件通知 // SaLogoutMode.LOGOUT:注销下线 if(logoutParameter.getMode() == SaLogoutMode.LOGOUT) { deleteTokenToIdMapping(tokenValue); SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); } // SaLogoutMode.LOGOUT:踢人下线 if(logoutParameter.getMode() == SaLogoutMode.KICKOUT) { updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT); SaTokenEventCenter.doKickout(loginType, loginId, tokenValue); } // SaLogoutMode.REPLACED:顶人下线 if(logoutParameter.getMode() == SaLogoutMode.REPLACED) { updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED); SaTokenEventCenter.doReplaced(loginType, loginId, tokenValue); } } /** * 如果指定账号 id、设备类型的登录客户端已经超过了指定数量,则按照登录时间顺序,把最开始登录的给注销掉 * * @param loginId 账号id * @param session 此账号的 Account-Session 对象,可填写 null,框架将自动获取 * @param deviceType 设备类型(填 null 代表注销此账号所有设备类型的登录) * @param maxLoginCount 最大登录数量,超过此数量的将被注销 * @param logoutMode 超出的客户端将以何种方式被注销 */ public void logoutByMaxLoginCount(Object loginId, SaSession session, String deviceType, int maxLoginCount, SaLogoutMode logoutMode) { // 1、如果调用者提供的 Account-Session 对象为空,则我们先手动获取一下 if(session == null) { session = getSessionByLoginId(loginId, false); if(session == null) { return; } } // 2、获取这个账号指定设备类型下的所有登录客户端 List list = session.getTerminalListByDeviceType(deviceType); // 3、按照登录时间倒叙,超过 maxLoginCount 数量的,全部注销掉 for (int i = 0; i < list.size() - maxLoginCount; i++) { _removeTerminal(session, list.get(i), createSaLogoutParameter().setMode(logoutMode)); } // 4、如果代码走到这里的时候,此账号已经没有客户端在登录了,则直接注销掉这个 Account-Session session.logoutByTerminalCountToZero(); } // ---- 会话查询 /** * 判断当前会话是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public boolean isLogin() { // 判断条件: // 1、获取到的 loginId 不为 null, // 2、并且不在异常项集合里(此项在 getLoginIdDefaultNull() 方法里完成判断) return getLoginIdDefaultNull() != null; } /** * 判断指定账号是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public boolean isLogin(Object loginId) { // 判断条件:能否根据 loginId 查询到对应的 terminal 值 return !getTerminalListByLoginId(loginId, null).isEmpty(); } /** * 检验当前会话是否已经登录,如未登录,则抛出异常 */ public void checkLogin() { // 效果与 getLoginId() 相同,只是 checkLogin() 更加语义化一些 getLoginId(); } /** * 获取当前会话账号id,如果未登录,则抛出异常 * * @return 账号id */ public Object getLoginId() { // 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份 if(isSwitch()) { return getSwitchLoginId(); } // 2、如果前端没有提交 token,则抛出异常: 未能读取到有效 token String tokenValue = getTokenValue(true); if(SaFoxUtil.isEmpty(tokenValue)) { throw NotLoginException.newInstance(loginType, NOT_TOKEN, NOT_TOKEN_MESSAGE, null).setCode(SaErrorCode.CODE_11011); } // 3、查找此 token 对应的 loginId,如果找不到则抛出:token 无效 String loginId = getLoginIdNotHandle(tokenValue); if(SaFoxUtil.isEmpty(loginId)) { throw NotLoginException.newInstance(loginType, INVALID_TOKEN, INVALID_TOKEN_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11012); } // 4、如果这个 token 指向的是值是:过期标记,则抛出:token 已过期 if(loginId.equals(NotLoginException.TOKEN_TIMEOUT)) { throw NotLoginException.newInstance(loginType, TOKEN_TIMEOUT, TOKEN_TIMEOUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11013); } // 5、如果这个 token 指向的是值是:被顶替标记,则抛出:token 已被顶下线 if(loginId.equals(NotLoginException.BE_REPLACED)) { throw NotLoginException.newInstance(loginType, BE_REPLACED, BE_REPLACED_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11014); } // 6、如果这个 token 指向的是值是:被踢下线标记,则抛出:token 已被踢下线 if(loginId.equals(NotLoginException.KICK_OUT)) { throw NotLoginException.newInstance(loginType, KICK_OUT, KICK_OUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11015); } // 7、token 活跃频率检查 checkActiveTimeoutByConfig(tokenValue); // ------ 至此,loginId 已经是一个合法的值,代表当前会话是一个正常的登录状态了 // 8、返回 loginId return loginId; } /** * 获取当前会话账号id, 如果未登录,则返回默认值 * * @param 返回类型 * @param defaultValue 默认值 * @return 登录id */ @SuppressWarnings("unchecked") public T getLoginId(T defaultValue) { // 1、先正常获取一下当前会话的 loginId Object loginId = getLoginIdDefaultNull(); // 2、如果 loginId 为 null,则返回默认值 if(loginId == null) { return defaultValue; } // 3、loginId 不为 null,则开始尝试类型转换 if(defaultValue == null) { return (T) loginId; } return (T) SaFoxUtil.getValueByType(loginId, defaultValue.getClass()); } /** * 获取当前会话账号id, 如果未登录,则返回null * * @return 账号id */ public Object getLoginIdDefaultNull() { // 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份 if(isSwitch()) { return getSwitchLoginId(); } // 2、如果前端连 token 都没有提交,则直接返回 null String tokenValue = getTokenValue(); if(tokenValue == null) { return null; } // 3、根据 token 找到对应的 loginId,如果 loginId 为 null 或者属于异常标记里面,均视为未登录, 统一返回 null Object loginId = getLoginIdNotHandle(tokenValue); if( ! isValidLoginId(loginId) ) { return null; } // 4、如果 token 已被冻结,也返回 null if(getTokenActiveTimeoutByToken(tokenValue) == SaTokenDao.NOT_VALUE_EXPIRE) { return null; } // 5、执行到此,证明此 loginId 已经是个正常合法的账号id了,可以返回 return loginId; } /** * 获取当前会话账号id, 并转换为 String 类型 * * @return 账号id */ public String getLoginIdAsString() { return String.valueOf(getLoginId()); } /** * 获取当前会话账号id, 并转换为 int 类型 * * @return 账号id */ public int getLoginIdAsInt() { return Integer.parseInt(String.valueOf(getLoginId())); } /** * 获取当前会话账号id, 并转换为 long 类型 * * @return 账号id */ public long getLoginIdAsLong() { return Long.parseLong(String.valueOf(getLoginId())); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null * * @param tokenValue token * @return 账号id */ public Object getLoginIdByToken(String tokenValue) { Object loginId = getLoginIdByTokenNotThinkFreeze(tokenValue); if( SaFoxUtil.isNotEmpty(loginId) ) { // 如果 token 已被冻结,也返回 null long activeTimeout = getTokenActiveTimeoutByToken(tokenValue); if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) { return null; } } return loginId; } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null * * @param tokenValue token * @return 账号id */ public Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { // 1、如果提供的 token 为空,则直接返回 null if(SaFoxUtil.isEmpty(tokenValue)) { return null; } // 2、查找此 token 对应的 loginId,如果找不到或找的到但属于无效值,则返回 null String loginId = getLoginIdNotHandle(tokenValue); if( ! isValidLoginId(loginId) ) { return null; } // 3、返回 return loginId; } /** * 获取指定 token 对应的账号id (不做任何特殊处理) * * @param tokenValue token 值 * @return 账号id */ public String getLoginIdNotHandle(String tokenValue) { return getSaTokenDao().get(splicingKeyTokenValue(tokenValue)); } /** * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) * * @param key 键值 * @return 对应的扩展数据 */ public Object getExtra(String key) { throw new ApiDisabledException("只有在集成 sa-token-jwt 插件后才可以使用 extra 扩展参数").setCode(SaErrorCode.CODE_11031); } /** * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) * * @param tokenValue 指定的 Token 值 * @param key 键值 * @return 对应的扩展数据 */ public Object getExtra(String tokenValue, String key) { throw new ApiDisabledException("只有在集成 sa-token-jwt 插件后才可以使用 extra 扩展参数").setCode(SaErrorCode.CODE_11031); } // ---- 其它操作 /** * 判断一个 loginId 是否是有效的 (判断标准:不为 null、空字符串,且不在异常标记项里面) * * @param loginId 账号id * @return / */ public boolean isValidLoginId(Object loginId) { return SaFoxUtil.isNotEmpty(loginId) && !NotLoginException.ABNORMAL_LIST.contains(loginId.toString()); } /** * 判断一个 token 是否是有效的 (判断标准:使用此 token 查询到的 loginId 不为 Empty ) * * @param tokenValue / * @return / */ public boolean isValidToken(String tokenValue) { Object loginId = getLoginIdByToken(tokenValue); return SaFoxUtil.isNotEmpty(loginId); } /** * 存储 token - id 映射关系 * * @param tokenValue token值 * @param loginId 账号id * @param timeout 会话有效期 (单位: 秒) */ public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) { getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout); } /** * 更改 token - id 映射关系 * * @param tokenValue token值 * @param loginId 新的账号Id值 */ public void updateTokenToIdMapping(String tokenValue, Object loginId) { // 先判断一下,是否传入了空值 SaTokenException.notTrue(SaFoxUtil.isEmpty(loginId), "loginId 不能为空", SaErrorCode.CODE_11003); // 更新缓存中的 token 指向 getSaTokenDao().update(splicingKeyTokenValue(tokenValue), loginId.toString()); } /** * 删除 token - id 映射 * * @param tokenValue token值 */ public void deleteTokenToIdMapping(String tokenValue) { getSaTokenDao().delete(splicingKeyTokenValue(tokenValue)); } // ------------------- Account-Session 相关 ------------------- /** * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,isCreate = 是否立即新建并返回 * * @param sessionId SessionId * @param isCreate 是否新建 * @param timeout 如果这个 SaSession 是新建的,则使用此值作为过期值(单位:秒),可填 null,代表使用全局 timeout 值 * @param appendOperation 如果这个 SaSession 是新建的,则要追加执行的动作,可填 null,代表无追加动作 * @return Session对象 */ public SaSession getSessionBySessionId(String sessionId, boolean isCreate, Long timeout, Consumer appendOperation) { // 如果提供的 sessionId 为 null,则直接返回 null if(SaFoxUtil.isEmpty(sessionId)) { throw new SaTokenException("SessionId 不能为空").setCode(SaErrorCode.CODE_11072); } // 先检查这个 SaSession 是否已经存在,如果不存在且 isCreate=true,则新建并返回 SaSession session = getSaTokenDao().getSession(sessionId); if(session == null && isCreate) { // 创建这个 SaSession session = SaStrategy.instance.createSession.apply(sessionId); // 追加操作 if(appendOperation != null) { appendOperation.accept(session); } // 如果未提供 timeout,则根据相应规则设定默认的 timeout if(timeout == null) { // 如果是 Token-Session,则使用对用 token 的有效期,使 token 和 token-session 保持相同ttl,同步失效 if(SaTokenConsts.SESSION_TYPE__TOKEN.equals(session.getType())) { timeout = getTokenTimeout(session.getToken()); if(timeout == SaTokenDao.NOT_VALUE_EXPIRE) { timeout = getConfigOrGlobal().getTimeout(); } } else { // 否则使用全局配置的 timeout timeout = getConfigOrGlobal().getTimeout(); } } // 将这个 SaSession 入库 getSaTokenDao().setSession(session, timeout); } return session; } /** * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null * * @param sessionId SessionId * @return Session对象 */ public SaSession getSessionBySessionId(String sessionId) { return getSessionBySessionId(sessionId, false, null, null); } /** * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param loginId 账号id * @param isCreate 是否新建 * @param timeout 如果这个 SaSession 是新建的,则使用此值作为过期值(单位:秒),可填 null,代表使用全局 timeout 值 * @return SaSession 对象 */ public SaSession getSessionByLoginId(Object loginId, boolean isCreate, Long timeout) { if(SaFoxUtil.isEmpty(loginId)) { throw new SaTokenException("Account-Session 获取失败:loginId 不能为空"); } return getSessionBySessionId(splicingKeySession(loginId), isCreate, timeout, session -> { // 这里是该 Account-Session 首次创建时才会被执行的方法: // 设定这个 SaSession 的各种基础信息:类型、账号体系、账号id session.setType(SaTokenConsts.SESSION_TYPE__ACCOUNT); session.setLoginType(getLoginType()); session.setLoginId(loginId); }); } /** * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param loginId 账号id * @param isCreate 是否新建 * @return SaSession 对象 */ public SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return getSessionByLoginId(loginId, isCreate, null); } /** * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param loginId 账号id * @return SaSession 对象 */ public SaSession getSessionByLoginId(Object loginId) { return getSessionByLoginId(loginId, true, null); } /** * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param isCreate 是否新建 * @return Session对象 */ public SaSession getSession(boolean isCreate) { return getSessionByLoginId(getLoginId(), isCreate); } /** * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public SaSession getSession() { return getSession(true); } // ------------------- Token-Session 相关 ------------------- /** * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回 * * @param tokenValue token值 * @param isCreate 是否新建 * @return session对象 */ public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) { // 1、token 为空,不允许创建 if(SaFoxUtil.isEmpty(tokenValue)) { throw new SaTokenException("Token-Session 获取失败:token 为空").setCode(SaErrorCode.CODE_11073); } // 2、如果能查询到旧记录,则直接返回 String sessionId = splicingKeyTokenSession(tokenValue); SaSession tokenSession = getSaTokenDao().getSession(sessionId); if(tokenSession != null) { return tokenSession; } // 以下是查不到的情况 // 3、指定了不需要创建,返回 null if( ! isCreate) { return null; } // 以下是需要创建的情况 // 4、检查一下这个 token 是否为有效 token,无效 token 不允许创建 if(getConfigOrGlobal().getTokenSessionCheckLogin() && ! isValidToken(tokenValue)) { throw new SaTokenException("Token-Session 获取失败,token 无效: " + tokenValue).setCode(SaErrorCode.CODE_11074); } // 5、创建 Token-Session 并返回 return getSessionBySessionId(sessionId, true, null, session -> { // 这里是该 Token-Session 首次创建时才会被执行的方法: // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 session.setType(SaTokenConsts.SESSION_TYPE__TOKEN); session.setLoginType(getLoginType()); session.setToken(tokenValue); }); } /** * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param tokenValue Token值 * @return Session对象 */ public SaSession getTokenSessionByToken(String tokenValue) { return getTokenSessionByToken(tokenValue, true); } /** * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回 * * @param isCreate 是否新建 * @return Session对象 */ public SaSession getTokenSession(boolean isCreate) { String tokenValue = getTokenValue(); checkActiveTimeoutByConfig(tokenValue); return getTokenSessionByToken(tokenValue, isCreate); } /** * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public SaSession getTokenSession() { return getTokenSession(true); } /** * 获取当前匿名 Token-Session (可在未登录情况下使用的 Token-Session) * * @param isCreate 在 Token-Session 尚未创建的情况是否新建并返回 * @return Token-Session 对象 */ public SaSession getAnonTokenSession(boolean isCreate) { /* * 情况1、如果调用方提供了有效 Token,则:直接返回其 [Token-Session] * 情况2、如果调用方提供了无效 Token,或根本没有提供 Token,则:创建新 Token -> 返回 [ Token-Session ] */ String tokenValue = getTokenValue(); // q1 —— 判断这个 Token 是否有效,两个条件符合其一即可: /* * 条件1、能查出 Token-Session * 条件2、能查出 LoginId */ if(SaFoxUtil.isNotEmpty(tokenValue)) { // 符合条件1 SaSession session = getTokenSessionByToken(tokenValue, false); if(session != null) { return session; } // 符合条件2 String loginId = getLoginIdNotHandle(tokenValue); if(isValidLoginId(loginId)) { return getTokenSessionByToken(tokenValue, isCreate); } } // q2 —— 此时q2分两种情况: /* * 情况 2.1、isCreate=true:说明调用方想让框架帮其创建一个 SaSession,那框架就创建并返回 * 情况 2.2、isCreate=false:说明调用方并不想让框架帮其创建一个 SaSession,那框架就直接返回 null */ if(isCreate) { // 随机创建一个 Token tokenValue = SaStrategy.instance.generateUniqueToken.execute( "token", getConfigOfMaxTryTimes(createSaLoginParameter()), () -> { return createTokenValue(null, null, getConfigOrGlobal().getTimeout(), null); }, token -> { return getTokenSessionByToken(token, false) == null; } ); // 写入此 token 的最后活跃时间 if(isOpenCheckActiveTimeout()) { setLastActiveToNow(tokenValue, null, null); } // 在当前上下文写入此 TokenValue setTokenValue(tokenValue); // 返回其 Token-Session 对象 final String finalTokenValue = tokenValue; return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, getConfigOrGlobal().getTimeout(), session -> { // 这里是该 Anon-Token-Session 首次创建时才会被执行的方法: // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 session.setType(SaTokenConsts.SESSION_TYPE__ANON); session.setLoginType(getLoginType()); session.setToken(finalTokenValue); }); } else { return null; } } /** * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) * * @return Token-Session 对象 */ public SaSession getAnonTokenSession() { return getAnonTokenSession(true); } /** * 删除指定 token 的 Token-Session * * @param tokenValue token值 */ public void deleteTokenSession(String tokenValue) { getSaTokenDao().delete(splicingKeyTokenSession(tokenValue)); } // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- /** * 写入指定 token 的 [ 最后活跃时间 ] 为当前时间戳 √√√ * * @param tokenValue 指定token * @param activeTimeout 这个 token 的最低活跃频率,单位:秒,填 null 代表使用全局配置的 activeTimeout 值 * @param timeout 保存数据时使用的 ttl 值,单位:秒,填 null 代表使用全局配置的 timeout 值 */ protected void setLastActiveToNow(String tokenValue, Long activeTimeout, Long timeout) { // 如果提供的 timeout 为null,则使用全局配置的 timeout 值 SaTokenConfig config = getConfigOrGlobal(); if(timeout == null) { timeout = config.getTimeout(); } // activeTimeout 变量无需赋值默认值,因为当缓存中没有这个值时,会自动使用全局配置的值 // 将此 token 的 [ 最后活跃时间 ] 标记为当前时间戳 String key = splicingKeyLastActiveTime(tokenValue); String value = String.valueOf(System.currentTimeMillis()); if(config.getDynamicActiveTimeout() && activeTimeout != null) { value += "," + activeTimeout; } getSaTokenDao().set(key, value, timeout); } /** * 续签指定 token:将这个 token 的 [ 最后活跃时间 ] 更新为当前时间戳 * * @param tokenValue 指定token */ public void updateLastActiveToNow(String tokenValue) { String key = splicingKeyLastActiveTime(tokenValue); String value = new SaValue2Box(System.currentTimeMillis(), getTokenUseActiveTimeout(tokenValue)).toString(); getSaTokenDao().update(key, value); } /** * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) *

* 请注意: 即使 token 已被冻结 也可续签成功, * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 *

*/ public void updateLastActiveToNow() { updateLastActiveToNow(getTokenValue()); } /** * 清除指定 Token 的 [ 最后活跃时间记录 ] * * @param tokenValue 指定 token */ protected void clearLastActive(String tokenValue) { getSaTokenDao().delete(splicingKeyLastActiveTime(tokenValue)); } /** * 判断指定 token 是否已被冻结 * * @param tokenValue 指定 token */ public boolean isFreeze(String tokenValue) { // 1、获取这个 token 的剩余活跃有效期 long activeTimeout = getTokenActiveTimeoutByToken(tokenValue); // 2、值为 -1 代表此 token 已经被设置永不冻结 if(activeTimeout == SaTokenDao.NEVER_EXPIRE) { return false; } // 3、值为 -2 代表已被冻结 if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) { return true; } return false; } /** * 根据全局配置决定是否校验指定 token 的活跃度 * * @param tokenValue 指定 token */ public void checkActiveTimeoutByConfig(String tokenValue) { if(isOpenCheckActiveTimeout()) { // storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗 SaHolder.getStorage().get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> { // 1、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制,如果是则代表其已被冻结,需要抛出:token 已被冻结 checkActiveTimeout(tokenValue); // 2、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 (注意此处的续签是在续 active-timeout,而非 timeout) if(SaStrategy.instance.autoRenew.apply(this)) { updateLastActiveToNow(tokenValue); } return true; }); } } /** * 检查指定 token 是否已被冻结,如果是则抛出异常 * * @param tokenValue 指定 token */ public void checkActiveTimeout(String tokenValue) { if (isFreeze(tokenValue)) { throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016); } } /** * 检查当前 token 是否已被冻结,如果是则抛出异常 */ public void checkActiveTimeout() { checkActiveTimeout(getTokenValue()); } /** * 获取指定 token 在缓存中的 activeTimeout 值,如果不存在则返回 null * * @param tokenValue 指定token * @return / */ public Long getTokenUseActiveTimeout(String tokenValue) { // 在未启用动态 activeTimeout 功能时,直接返回 null if( ! getConfigOrGlobal().getDynamicActiveTimeout()) { return null; } // 先取出这个 token 的最后活跃时间值 String key = splicingKeyLastActiveTime(tokenValue); String value = getSaTokenDao().get(key); // 解析,无值的情况下返回 null SaValue2Box box = new SaValue2Box(value); return box.getValue2AsLong(null); } /** * 获取指定 token 在缓存中的 activeTimeout 值,如果不存在则返回全局配置的 activeTimeout 值 * * @param tokenValue 指定token * @return / */ public long getTokenUseActiveTimeoutOrGlobalConfig(String tokenValue) { Long activeTimeout = getTokenUseActiveTimeout(tokenValue); if(activeTimeout == null) { return getConfigOrGlobal().getActiveTimeout(); } return activeTimeout; } /** * 获取指定 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 * * @param tokenValue 指定token * @return / */ public long getTokenLastActiveTime(String tokenValue) { // 1、如果提供的 token 为 null,则返回 -2 if(SaFoxUtil.isEmpty(tokenValue)) { return SaTokenDao.NOT_VALUE_EXPIRE; } // 2、获取这个 token 的最后活跃时间,13位时间戳 String key = splicingKeyLastActiveTime(tokenValue); String lastActiveTimeString = getSaTokenDao().get(key); // 3、查不到,返回-2 if(lastActiveTimeString == null) { return SaTokenDao.NOT_VALUE_EXPIRE; } // 4、根据逗号切割字符串 return new SaValue2Box(lastActiveTimeString).getValue1AsLong(); } /** * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 * * @return / */ public long getTokenLastActiveTime() { return getTokenLastActiveTime(getTokenValue()); } // ------------------- 过期时间相关 ------------------- /** * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public long getTokenTimeout() { return getTokenTimeout(getTokenValue()); } /** * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param token 指定token * @return token剩余有效时间 */ public long getTokenTimeout(String token) { return getSaTokenDao().getTimeout(splicingKeyTokenValue(token)); } /** * 获取指定账号 id 的 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param loginId 指定loginId * @return token剩余有效时间 */ public long getTokenTimeoutByLoginId(Object loginId) { return getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValueByLoginId(loginId))); } /** * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public long getSessionTimeout() { return getSessionTimeoutByLoginId(getLoginIdDefaultNull()); } /** * 获取指定账号 id 的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param loginId 指定loginId * @return token剩余有效时间 */ public long getSessionTimeoutByLoginId(Object loginId) { return getSaTokenDao().getSessionTimeout(splicingKeySession(loginId)); } /** * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public long getTokenSessionTimeout() { return getTokenSessionTimeoutByTokenValue(getTokenValue()); } /** * 获取指定 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param tokenValue 指定token * @return token 剩余有效时间 */ public long getTokenSessionTimeoutByTokenValue(String tokenValue) { return getSaTokenDao().getSessionTimeout(splicingKeyTokenSession(tokenValue)); } /** * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) * * @return / */ public long getTokenActiveTimeout() { return getTokenActiveTimeoutByToken(getTokenValue()); } /** * 获取指定 token 剩余活跃有效期:这个 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) * * @param tokenValue 指定 token * @return / */ public long getTokenActiveTimeoutByToken(String tokenValue) { // 如果全局配置了永不冻结, 则返回 -1 if( ! isOpenCheckActiveTimeout() ) { return SaTokenDao.NEVER_EXPIRE; } // ------ 开始查询 // 先获取这个 token 的最后活跃时间,13位时间戳 long lastActiveTime = getTokenLastActiveTime(tokenValue); if(lastActiveTime == SaTokenDao.NOT_VALUE_EXPIRE) { return SaTokenDao.NOT_VALUE_EXPIRE; } // 实际时间差 long timeDiff = (System.currentTimeMillis() - lastActiveTime) / 1000; // 该 token 允许的时间差 long allowTimeDiff = getTokenUseActiveTimeoutOrGlobalConfig(tokenValue); if(allowTimeDiff == SaTokenDao.NEVER_EXPIRE) { // 如果允许的时间差为 -1 ,则代表永不冻结,此处需要立即返回 -1 ,无需后续计算 return SaTokenDao.NEVER_EXPIRE; } // 校验这个时间差是否超过了允许的值 // 计算公式为: 允许的最大时间差 - 实际时间差,判断是否 < 0, 如果是则代表已经被冻结 ,返回-2 long activeTimeout = allowTimeDiff - timeDiff; if(activeTimeout < 0) { return SaTokenDao.NOT_VALUE_EXPIRE; } else { // 否则代表没冻结,返回剩余有效时间 return activeTimeout; } } /** * 对当前 token 的 timeout 值进行续期 * * @param timeout 要修改成为的有效时间 (单位: 秒) */ public void renewTimeout(long timeout) { // 1、续期缓存数据 String tokenValue = getTokenValue(); renewTimeout(tokenValue, timeout); // 2、续期客户端 Cookie 有效期 if(getConfigOrGlobal().getIsReadCookie()) { // 如果 timeout = -1,代表永久,但是一般浏览器不支持永久 Cookie,所以此处设置为 int 最大值 // 如果 timeout 大于 int 最大值,会造成数据溢出,所以也要将其设置为 int 最大值 if(timeout == SaTokenDao.NEVER_EXPIRE || timeout > Integer.MAX_VALUE) { timeout = Integer.MAX_VALUE; } setTokenValueToCookie(tokenValue, (int)timeout); } } /** * 对指定 token 的 timeout 值进行续期 * * @param tokenValue 指定 token * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) */ public void renewTimeout(String tokenValue, long timeout) { // 1、如果 token 指向的 loginId 为空,或者属于异常项时,不进行续期操作 Object loginId = getLoginIdByToken(tokenValue); if(loginId == null) { return; } // 2、检查 token 合法性 SaSession session = getSessionByLoginId(loginId); if(session == null) { throw new SaTokenException("未能查询到对应 Access-Session 会话,无法续期"); } if(session.getTerminal(tokenValue) == null) { throw new SaTokenException("未能查询到对应终端信息,无法续期"); } // 3、续期此 token 本身的有效期 (改 ttl) SaTokenDao dao = getSaTokenDao(); dao.updateTimeout(splicingKeyTokenValue(tokenValue), timeout); // 4、续期此 token 的 Token-Session 有效期 SaSession tokenSession = getTokenSessionByToken(tokenValue, false); if(tokenSession != null) { tokenSession.updateTimeout(timeout); } // 5、续期此 token 指向的账号的 Account-Session 有效期 session.updateMinTimeout(timeout); // 6、更新此 token 的最后活跃时间 if(isOpenCheckActiveTimeout()) { dao.updateTimeout(splicingKeyLastActiveTime(tokenValue), timeout); } // 7、$$ 发布事件:某某 token 被续期了 SaTokenEventCenter.doRenewTimeout(loginType, loginId, tokenValue, timeout); } // ------------------- 角色认证操作 ------------------- /** * 获取:当前账号的角色集合 * * @return / */ public List getRoleList() { return getRoleList(getLoginId()); } /** * 获取:指定账号的角色集合 * * @param loginId 指定账号id * @return / */ public List getRoleList(Object loginId) { return SaManager.getStpInterface().getRoleList(loginId, loginType); } /** * 判断:当前账号是否拥有指定角色, 返回 true 或 false * * @param role 角色 * @return / */ public boolean hasRole(String role) { try { return hasRole(getLoginId(), role); } catch (NotLoginException e) { return false; } } /** * 判断:指定账号是否含有指定角色标识, 返回 true 或 false * * @param loginId 账号id * @param role 角色标识 * @return 是否含有指定角色标识 */ public boolean hasRole(Object loginId, String role) { return hasElement(getRoleList(loginId), role); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 * @return true或false */ public boolean hasRoleAnd(String... roleArray){ try { checkRoleAnd(roleArray); return true; } catch (NotLoginException | NotRoleException e) { return false; } } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 * @return true或false */ public boolean hasRoleOr(String... roleArray){ try { checkRoleOr(roleArray); return true; } catch (NotLoginException | NotRoleException e) { return false; } } /** * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException * * @param role 角色标识 */ public void checkRole(String role) { if( ! hasRole(getLoginId(), role)) { throw new NotRoleException(role, this.loginType).setCode(SaErrorCode.CODE_11041); } } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 */ public void checkRoleAnd(String... roleArray){ // 先获取当前是哪个账号id Object loginId = getLoginId(); // 如果没有指定要校验的角色,那么直接跳过 if(roleArray == null || roleArray.length == 0) { return; } // 开始校验 List roleList = getRoleList(loginId); for (String role : roleArray) { if(!hasElement(roleList, role)) { throw new NotRoleException(role, this.loginType).setCode(SaErrorCode.CODE_11041); } } } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 */ public void checkRoleOr(String... roleArray){ // 先获取当前是哪个账号id Object loginId = getLoginId(); // 如果没有指定权限,那么直接跳过 if(roleArray == null || roleArray.length == 0) { return; } // 开始校验 List roleList = getRoleList(loginId); for (String role : roleArray) { if(hasElement(roleList, role)) { // 有的话提前退出 return; } } // 代码至此,说明一个都没通过,需要抛出无角色异常 throw new NotRoleException(roleArray[0], this.loginType).setCode(SaErrorCode.CODE_11041); } // ------------------- 权限认证操作 ------------------- /** * 获取:当前账号的权限码集合 * * @return / */ public List getPermissionList() { return getPermissionList(getLoginId()); } /** * 获取:指定账号的权限码集合 * * @param loginId 指定账号id * @return / */ public List getPermissionList(Object loginId) { return SaManager.getStpInterface().getPermissionList(loginId, loginType); } /** * 判断:当前账号是否含有指定权限, 返回 true 或 false * * @param permission 权限码 * @return 是否含有指定权限 */ public boolean hasPermission(String permission) { try { return hasPermission(getLoginId(), permission); } catch (NotLoginException e) { return false; } } /** * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false * * @param loginId 账号 id * @param permission 权限码 * @return 是否含有指定权限 */ public boolean hasPermission(Object loginId, String permission) { return hasElement(getPermissionList(loginId), permission); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public boolean hasPermissionAnd(String... permissionArray){ try { checkPermissionAnd(permissionArray); return true; } catch (NotLoginException | NotPermissionException e) { return false; } } /** * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public boolean hasPermissionOr(String... permissionArray){ try { checkPermissionOr(permissionArray); return true; } catch (NotLoginException | NotPermissionException e) { return false; } } /** * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException * * @param permission 权限码 */ public void checkPermission(String permission) { if( ! hasPermission(getLoginId(), permission)) { throw new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051); } } /** * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] * * @param permissionArray 权限码数组 */ public void checkPermissionAnd(String... permissionArray){ // 先获取当前是哪个账号id Object loginId = getLoginId(); // 如果没有指定权限,那么直接跳过 if(permissionArray == null || permissionArray.length == 0) { return; } // 开始校验 List permissionList = getPermissionList(loginId); for (String permission : permissionArray) { if(!hasElement(permissionList, permission)) { throw new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051); } } } /** * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 */ public void checkPermissionOr(String... permissionArray){ // 先获取当前是哪个账号id Object loginId = getLoginId(); // 如果没有指定要校验的权限,那么直接跳过 if(permissionArray == null || permissionArray.length == 0) { return; } // 开始校验 List permissionList = getPermissionList(loginId); for (String permission : permissionArray) { if(hasElement(permissionList, permission)) { // 有的话提前退出 return; } } // 代码至此,说明一个都没通过,需要抛出无权限异常 throw new NotPermissionException(permissionArray[0], this.loginType).setCode(SaErrorCode.CODE_11051); } // ------------------- id 反查 token 相关操作 ------------------- /** * 获取指定账号 id 的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @return token值 */ public String getTokenValueByLoginId(Object loginId) { return getTokenValueByLoginId(loginId, null); } /** * 获取指定账号 id 指定设备类型端的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return token值 */ public String getTokenValueByLoginId(Object loginId, String deviceType) { List tokenValueList = getTokenValueListByLoginId(loginId, deviceType); return tokenValueList.isEmpty() ? null : tokenValueList.get(tokenValueList.size() - 1); } /** * 获取指定账号 id 的 token 集合 * * @param loginId 账号id * @return 此 loginId 的所有相关 token */ public List getTokenValueListByLoginId(Object loginId) { return getTokenValueListByLoginId(loginId, null); } /** * 获取指定账号 id 指定设备类型端的 token 集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public List getTokenValueListByLoginId(Object loginId, String deviceType) { // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时返回空集合 SaSession session = getSessionByLoginId(loginId, false); if(session == null) { return new ArrayList<>(); } // 按照设备类型进行筛选 return session.getTokenValueListByDeviceType(deviceType); } /** * 获取指定账号 id 已登录设备信息集合 * * @param loginId 账号id * @return 此 loginId 的所有登录 token */ public List getTerminalListByLoginId(Object loginId) { return getTerminalListByLoginId(loginId, null); } /** * 获取指定账号 id 指定设备类型端的已登录设备信息集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public List getTerminalListByLoginId(Object loginId, String deviceType) { // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时返回空集合 SaSession session = getSessionByLoginId(loginId, false); if(session == null) { return new ArrayList<>(); } // 按照设备类型进行筛选 return session.getTerminalListByDeviceType(deviceType); } /** * 获取指定账号 id 已登录设备信息集合,执行特定函数 * * @param loginId 账号id * @param function 需要执行的函数 */ public void forEachTerminalList(Object loginId, SaTwoParamFunction function) { // 如果该账号的 Account-Session 为 null,说明此账号尚没有客户端在登录,此时无需遍历 SaSession session = getSessionByLoginId(loginId, false); if(session == null) { return; } // 遍历 session.forEachTerminalList(function); } /** * 返回当前 token 指向的 SaTerminalInfo 设备信息,如果 token 无效则返回 null * * @return / */ public SaTerminalInfo getTerminalInfo() { return getTerminalInfoByToken(getTokenValue()); } /** * 返回指定 token 指向的 SaTerminalInfo 设备信息,如果 Token 无效则返回 null * * @param tokenValue 指定 token * @return / */ public SaTerminalInfo getTerminalInfoByToken(String tokenValue) { // 1、如果 token 为 null,直接提前返回 if(SaFoxUtil.isEmpty(tokenValue)) { return null; } // 2、判断 Token 是否有效 Object loginId = getLoginIdNotHandle(tokenValue); if( ! isValidLoginId(loginId)) { return null; } // 3、判断 Account-Session 是否存在 SaSession session = getSessionByLoginId(loginId, false); if(session == null) { return null; } // 4、判断 Token 是否已被冻结 if(isFreeze(tokenValue)) { return null; } // 5、遍历 Account-Session 上的客户端 token 列表,寻找当前 token 对应的设备类型 List terminalList = session.terminalListCopy(); for (SaTerminalInfo terminal : terminalList) { if(terminal.getTokenValue().equals(tokenValue)) { return terminal; } } // 6、没有找到,还是返回 null return null; } /** * 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ public String getLoginDeviceType() { return getLoginDeviceTypeByToken(getTokenValue()); } /** * 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ public String getLoginDeviceTypeByToken(String tokenValue) { SaTerminalInfo terminalInfo = getTerminalInfoByToken(tokenValue); return terminalInfo == null ? null : terminalInfo.getDeviceType(); } /** * 返回当前会话的登录设备 ID * * @return / */ public String getLoginDeviceId() { return getLoginDeviceIdByToken(getTokenValue()); } /** * 返回指定 token 会话的登录设备 ID * * @param tokenValue 指定token * @return / */ public String getLoginDeviceIdByToken(String tokenValue) { SaTerminalInfo terminalInfo = getTerminalInfoByToken(tokenValue); return terminalInfo == null ? null : terminalInfo.getDeviceId(); } /** * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 * @param deviceId / * @return / */ public boolean isTrustDeviceId(Object userId, String deviceId) { // 先查询此账号的 Account-Session,如果连 Account-Session 都没有,那么此账号尚未登录,直接返回 false SaSession session = getSessionByLoginId(userId, false); if(session == null) { return false; } // 判断 return session.isTrustDeviceId(deviceId); } // ------------------- 会话管理 ------------------- /** * 根据条件查询缓存中所有的 token * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return token集合 */ public List searchTokenValue(String keyword, int start, int size, boolean sortType) { return getSaTokenDao().searchData(splicingKeyTokenValue(""), (keyword == null ? "" : keyword), start, size, sortType); } /** * 根据条件查询缓存中所有的 SessionId * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public List searchSessionId(String keyword, int start, int size, boolean sortType) { return getSaTokenDao().searchData(splicingKeySession(""), (keyword == null ? "" : keyword), start, size, sortType); } /** * 根据条件查询缓存中所有的 Token-Session-Id * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { return getSaTokenDao().searchData(splicingKeyTokenSession(""), (keyword == null ? "" : keyword), start, size, sortType); } // ------------------- 账号封禁 ------------------- /** * 封禁:指定账号 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public void disable(Object loginId, long time) { disableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.DEFAULT_DISABLE_LEVEL, time); } /** * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) * * @param loginId 账号id * @return / */ public boolean isDisable(Object loginId) { return isDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.MIN_DISABLE_LEVEL); } /** * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id */ public void checkDisable(Object loginId) { checkDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.MIN_DISABLE_LEVEL); } /** * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @return / */ public long getDisableTime(Object loginId) { return getDisableTime(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE); } /** * 解封:指定账号 * * @param loginId 账号id */ public void untieDisable(Object loginId) { untieDisable(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE); } // ------------------- 分类封禁 ------------------- /** * 封禁:指定账号的指定服务 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param service 指定服务 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public void disable(Object loginId, String service, long time) { disableLevel(loginId, service, SaTokenConsts.DEFAULT_DISABLE_LEVEL, time); } /** * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return / */ public boolean isDisable(Object loginId, String service) { return isDisableLevel(loginId, service, SaTokenConsts.MIN_DISABLE_LEVEL); } /** * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public void checkDisable(Object loginId, String... services) { if(services != null) { for (String service : services) { checkDisableLevel(loginId, service, SaTokenConsts.MIN_DISABLE_LEVEL); } } } /** * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return see note */ public long getDisableTime(Object loginId, String service) { return getSaTokenDao().getTimeout(splicingKeyDisable(loginId, service)); } /** * 解封:指定账号、指定服务 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public void untieDisable(Object loginId, String... services) { // 先检查提供的参数是否有效 if(SaFoxUtil.isEmpty(loginId)) { throw new SaTokenException("请提供要解禁的账号").setCode(SaErrorCode.CODE_11062); } if(services == null || services.length == 0) { throw new SaTokenException("请提供要解禁的服务").setCode(SaErrorCode.CODE_11063); } // 遍历逐个解禁 for (String service : services) { // 解封 getSaTokenDao().delete(splicingKeyDisable(loginId, service)); // $$ 发布事件 SaTokenEventCenter.doUntieDisable(loginType, loginId, service); } } // ------------------- 阶梯封禁 ------------------- /** * 封禁:指定账号,并指定封禁等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public void disableLevel(Object loginId, int level, long time) { disableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level, time); } /** * 封禁:指定账号的指定服务,并指定封禁等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public void disableLevel(Object loginId, String service, int level, long time) { // 先检查提供的参数是否有效 if(SaFoxUtil.isEmpty(loginId)) { throw new SaTokenException("请提供要封禁的账号").setCode(SaErrorCode.CODE_11062); } if(SaFoxUtil.isEmpty(service)) { throw new SaTokenException("请提供要封禁的服务").setCode(SaErrorCode.CODE_11063); } if(level < SaTokenConsts.MIN_DISABLE_LEVEL && level != 0) { throw new SaTokenException("封禁等级不可以小于最小值:" + SaTokenConsts.MIN_DISABLE_LEVEL + " (0除外)").setCode(SaErrorCode.CODE_11064); } // 打上封禁标记 getSaTokenDao().set(splicingKeyDisable(loginId, service), String.valueOf(level), time); // $$ 发布事件 SaTokenEventCenter.doDisable(loginType, loginId, service, level, time); } /** * 判断:指定账号是否已被封禁到指定等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @return / */ public boolean isDisableLevel(Object loginId, int level) { return isDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level); } /** * 判断:指定账号的指定服务,是否已被封禁到指定等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @return / */ public boolean isDisableLevel(Object loginId, String service, int level) { // 1、先前置检查一下这个账号是否被封禁了 int disableLevel = getDisableLevel(loginId, service); if(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) { return false; } // 2、再判断被封禁的等级是否达到了指定级别 return disableLevel >= level; } /** * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public void checkDisableLevel(Object loginId, int level) { checkDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level); } /** * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public void checkDisableLevel(Object loginId, String service, int level) { // 1、先前置检查一下这个账号是否被封禁了 int disableLevel = getDisableLevel(loginId, service); if(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) { return; } // 2、再判断被封禁的等级是否达到了指定级别 if(disableLevel >= level) { throw new DisableServiceException(loginType, loginId, service, disableLevel, level, getDisableTime(loginId, service)) .setCode(SaErrorCode.CODE_11061); } } /** * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @return / */ public int getDisableLevel(Object loginId) { return getDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE); } /** * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @param service 指定封禁服务 * @return / */ public int getDisableLevel(Object loginId, String service) { // 1、先从缓存中查询数据,缓存中有值,以缓存值优先 String value = getSaTokenDao().get(splicingKeyDisable(loginId, service)); if(SaFoxUtil.isNotEmpty(value)) { return SaFoxUtil.getValueByType(value, int.class); } // 2、如果缓存中无数据,则从"数据加载器"中再次查询 SaDisableWrapperInfo disableWrapperInfo = SaManager.getStpInterface().isDisabled(loginId, service); // 如果返回值 disableTime 有效,则代表返回结果需要写入缓存 if(disableWrapperInfo.disableTime == SaTokenDao.NEVER_EXPIRE || disableWrapperInfo.disableTime > 0) { disableLevel(loginId, service, disableWrapperInfo.disableLevel, disableWrapperInfo.disableTime); } // 返回查询结果 return disableWrapperInfo.disableLevel; } // ------------------- 临时身份切换 ------------------- /** * 临时切换身份为指定账号id * * @param loginId 指定loginId */ public void switchTo(Object loginId) { SaHolder.getStorage().set(splicingKeySwitch(), loginId); } /** * 结束临时切换身份 */ public void endSwitch() { SaHolder.getStorage().delete(splicingKeySwitch()); } /** * 判断当前请求是否正处于 [ 身份临时切换 ] 中 * * @return / */ public boolean isSwitch() { return SaHolder.getStorage().get(splicingKeySwitch()) != null; } /** * 返回 [ 身份临时切换 ] 的 loginId * * @return / */ public Object getSwitchLoginId() { return SaHolder.getStorage().get(splicingKeySwitch()); } /** * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 * * @param loginId 指定账号id * @param function 要执行的方法 */ public void switchTo(Object loginId, SaFunction function) { try { switchTo(loginId); function.run(); } finally { endSwitch(); } } // ------------------- 二级认证 ------------------- /** * 在当前会话 开启二级认证 * * @param safeTime 维持时间 (单位: 秒) */ public void openSafe(long safeTime) { openSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE, safeTime); } /** * 在当前会话 开启二级认证 * * @param service 业务标识 * @param safeTime 维持时间 (单位: 秒) */ public void openSafe(String service, long safeTime) { // 1、开启二级认证前必须处于登录状态,否则抛出异常 checkLogin(); // 2、写入指定的 可以 标记,打开二级认证 String tokenValue = getTokenValueNotNull(); getSaTokenDao().set(splicingKeySafe(tokenValue, service), SaTokenConsts.SAFE_AUTH_SAVE_VALUE, safeTime); // 3、$$ 发布事件,某某 token 令牌开启了二级认证 SaTokenEventCenter.doOpenSafe(loginType, tokenValue, service, safeTime); } /** * 判断:当前会话是否处于二级认证时间内 * * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public boolean isSafe() { return isSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); } /** * 判断:当前会话 是否处于指定业务的二级认证时间内 * * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public boolean isSafe(String service) { return isSafe(getTokenValue(), service); } /** * 判断:指定 token 是否处于二级认证时间内 * * @param tokenValue Token 值 * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public boolean isSafe(String tokenValue, String service) { // 1、如果提供的 Token 为空,则直接视为未认证 if(SaFoxUtil.isEmpty(tokenValue)) { return false; } // 2、如果此 token 不处于登录状态,也将其视为未认证 Object loginId = getLoginIdNotHandle(tokenValue); if( ! isValidLoginId(loginId) ) { return false; } // 3、如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service)); return !(SaFoxUtil.isEmpty(value)); } /** * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 */ public void checkSafe() { checkSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); } /** * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 * * @param service 业务标识 */ public void checkSafe(String service) { // 1、必须先通过登录校验 checkLogin(); // 2、再进行二级认证校验 // 如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证 String tokenValue = getTokenValue(); String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service)); if(SaFoxUtil.isEmpty(value)) { throw new NotSafeException(loginType, tokenValue, service).setCode(SaErrorCode.CODE_11071); } } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @return 剩余有效时间 */ public long getSafeTime() { return getSafeTime(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @param service 业务标识 * @return 剩余有效时间 */ public long getSafeTime(String service) { // 1、如果前端没有提交 Token,则直接视为未认证 String tokenValue = getTokenValue(); if(SaFoxUtil.isEmpty(tokenValue)) { return SaTokenDao.NOT_VALUE_EXPIRE; } // 2、从缓存中查询这个 key 的剩余有效期 return getSaTokenDao().getTimeout(splicingKeySafe(tokenValue, service)); } /** * 在当前会话 结束二级认证 */ public void closeSafe() { closeSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE); } /** * 在当前会话 结束指定业务标识的二级认证 * * @param service 业务标识 */ public void closeSafe(String service) { // 1、如果前端没有提交 Token,则无需任何操作 String tokenValue = getTokenValue(); if(SaFoxUtil.isEmpty(tokenValue)) { return; } // 2、删除 key getSaTokenDao().delete(splicingKeySafe(tokenValue, service)); // 3、$$ 发布事件,某某 token 令牌关闭了二级认证 SaTokenEventCenter.doCloseSafe(loginType, tokenValue, service); } // ------------------- 拼接相应key ------------------- /** * 获取:客户端 tokenName * * @return key */ public String splicingKeyTokenName() { return getConfigOrGlobal().getTokenName(); } /** * 拼接: 在保存 token - id 映射关系时,应该使用的key * * @param tokenValue token值 * @return key */ public String splicingKeyTokenValue(String tokenValue) { return getConfigOrGlobal().getTokenName() + ":" + loginType + ":token:" + tokenValue; } /** * 拼接: 在保存 Account-Session 时,应该使用的 key * * @param loginId 账号id * @return key */ public String splicingKeySession(Object loginId) { return getConfigOrGlobal().getTokenName() + ":" + loginType + ":session:" + loginId; } /** * 拼接:在保存 Token-Session 时,应该使用的 key * * @param tokenValue token值 * @return key */ public String splicingKeyTokenSession(String tokenValue) { return getConfigOrGlobal().getTokenName() + ":" + loginType + ":token-session:" + tokenValue; } /** * 拼接: 在保存 token 最后活跃时间时,应该使用的 key * * @param tokenValue token值 * @return key */ public String splicingKeyLastActiveTime(String tokenValue) { return getConfigOrGlobal().getTokenName() + ":" + loginType + ":last-active:" + tokenValue; } /** * 拼接:在进行临时身份切换时,应该使用的 key * * @return key */ public String splicingKeySwitch() { return SaTokenConsts.SWITCH_TO_SAVE_KEY + loginType; } /** * 如果 token 为本次请求新创建的,则以此字符串为 key 存储在当前 request 中 * * @return key */ public String splicingKeyJustCreatedSave() { // return SaTokenConsts.JUST_CREATED_SAVE_KEY + loginType; return SaTokenConsts.JUST_CREATED; } /** * 拼接: 在保存服务封禁标记时,应该使用的 key * * @param loginId 账号id * @param service 具体封禁的服务 * @return key */ public String splicingKeyDisable(Object loginId, String service) { return getConfigOrGlobal().getTokenName() + ":" + loginType + ":disable:" + service + ":" + loginId; } /** * 拼接: 在保存业务二级认证标记时,应该使用的 key * * @param tokenValue 要认证的 Token * @param service 要认证的业务标识 * @return key */ public String splicingKeySafe(String tokenValue, String service) { // 格式::<账号类型>::<业务标识>: // 形如:satoken:login:safe:important:gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__ return getConfigOrGlobal().getTokenName() + ":" + loginType + ":safe:" + service + ":" + tokenValue; } // ------------------- Bean 对象、字段代理 ------------------- /** * 返回当前 StpLogic 使用的持久化对象 * * @return / */ public SaTokenDao getSaTokenDao() { return SaManager.getSaTokenDao(); } /** * 返回当前 StpLogic 是否支持共享 token 策略 * * @return / */ public boolean isSupportShareToken() { return getConfigOrGlobal().getIsShare(); } /** * 返回全局配置是否开启了 Token 活跃度校验,返回 true 代表已打开,返回 false 代表不打开,此时永不冻结 token * * @return / */ public boolean isOpenCheckActiveTimeout() { SaTokenConfig cfg = getConfigOrGlobal(); return cfg.getActiveTimeout() != SaTokenDao.NEVER_EXPIRE || cfg.getDynamicActiveTimeout(); } /** * 返回全局配置的 Cookie 保存时长,单位:秒 (根据全局 timeout 计算) * * @return Cookie 应该保存的时长 */ public int getConfigOfCookieTimeout() { long timeout = getConfigOrGlobal().getTimeout(); if(timeout == SaTokenDao.NEVER_EXPIRE) { return Integer.MAX_VALUE; } return (int) timeout; } /** * 返回全局配置的 maxTryTimes 值,在每次创建 token 时,对其唯一性测试的最高次数(-1=不测试) * * @param loginParameter / * @return / */ public int getConfigOfMaxTryTimes(SaLoginParameter loginParameter) { return loginParameter.getMaxTryTimes(); } /** * 判断:集合中是否包含指定元素(模糊匹配) * * @param list 集合 * @param element 元素 * @return / */ public boolean hasElement(List list, String element) { return SaStrategy.instance.hasElement.apply(list, element); } /** * 当前 StpLogic 对象是否支持 token 扩展参数 * * @return / */ public boolean isSupportExtra() { return false; } /** * 根据当前配置对象创建一个 SaLoginParameter 对象 * * @return / */ public SaLoginParameter createSaLoginParameter() { return new SaLoginParameter(getConfigOrGlobal()); } /** * 根据当前配置对象创建一个 SaLogoutParameter 对象 * * @return / */ public SaLogoutParameter createSaLogoutParameter() { return new SaLogoutParameter(getConfigOrGlobal()); } // ------------------- 过期方法 ------------------- /** *

请更换为 getLoginDeviceType

* 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ @Deprecated public String getLoginDevice() { return getLoginDeviceType(); } /** *

请更换为 getLoginDeviceTypeByToken

* 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ @Deprecated public String getLoginDeviceByToken(String tokenValue) { return getLoginDeviceTypeByToken(tokenValue); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaTwoParamFunction; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import java.util.List; /** * Sa-Token 权限认证工具类 * * @author click33 * @since 1.0.0 */ public class StpUtil { private StpUtil() {} /** * 多账号体系下的类型标识 */ public static final String TYPE = "login"; /** * 底层使用的 StpLogic 对象 */ public static StpLogic stpLogic = new StpLogic(TYPE); /** * 获取当前 StpLogic 的账号类型 * * @return / */ public static String getLoginType(){ return stpLogic.getLoginType(); } /** * 安全的重置 StpLogic 对象 * *
1、更改此账户的 StpLogic 对象 *
2、put 到全局 StpLogic 集合中 *
3、发送日志 * * @param newStpLogic / */ public static void setStpLogic(StpLogic newStpLogic) { // 1、重置此账户的 StpLogic 对象 stpLogic = newStpLogic; // 2、添加到全局 StpLogic 集合中 // 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic SaManager.putStpLogic(newStpLogic); // 3、$$ 发布事件:更新了 stpLogic 对象 SaTokenEventCenter.doSetStpLogic(stpLogic); } /** * 获取 StpLogic 对象 * * @return / */ public static StpLogic getStpLogic() { return stpLogic; } // ------------------- 获取 token 相关 ------------------- /** * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 * * @return / */ public static String getTokenName() { return stpLogic.getTokenName(); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 */ public static void setTokenValue(String tokenValue){ stpLogic.setTokenValue(tokenValue); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param cookieTimeout Cookie存活时间(秒) */ public static void setTokenValue(String tokenValue, int cookieTimeout){ stpLogic.setTokenValue(tokenValue, cookieTimeout); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param loginParameter 登录参数 */ public static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ stpLogic.setTokenValue(tokenValue, loginParameter); } /** * 将 token 写入到当前请求的 Storage 存储器里 * * @param tokenValue 要保存的 token 值 */ public static void setTokenValueToStorage(String tokenValue){ stpLogic.setTokenValueToStorage(tokenValue); } /** * 获取当前请求的 token 值 * * @return 当前tokenValue */ public static String getTokenValue() { return stpLogic.getTokenValue(); } /** * 获取当前请求的 token 值 (不裁剪前缀) * * @return / */ public static String getTokenValueNotCut(){ return stpLogic.getTokenValueNotCut(); } /** * 获取当前会话的 token 参数信息 * * @return token 参数信息 */ public static SaTokenInfo getTokenInfo() { return stpLogic.getTokenInfo(); } // ------------------- 登录相关操作 ------------------- // --- 登录 /** * 会话登录 * * @param id 账号id,建议的类型:(long | int | String) */ public static void login(Object id) { stpLogic.login(id); } /** * 会话登录,并指定登录设备类型 * * @param id 账号id,建议的类型:(long | int | String) * @param deviceType 设备类型 */ public static void login(Object id, String deviceType) { stpLogic.login(id, deviceType); } /** * 会话登录,并指定是否 [记住我] * * @param id 账号id,建议的类型:(long | int | String) * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public static void login(Object id, boolean isLastingCookie) { stpLogic.login(id, isLastingCookie); } /** * 会话登录,并指定此次登录 token 的有效期, 单位:秒 * * @param id 账号id,建议的类型:(long | int | String) * @param timeout 此次登录 token 的有效期, 单位:秒 */ public static void login(Object id, long timeout) { stpLogic.login(id, timeout); } /** * 会话登录,并指定所有登录参数 Model * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model */ public static void login(Object id, SaLoginParameter loginParameter) { stpLogic.login(id, loginParameter); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public static String createLoginSession(Object id) { return stpLogic.createLoginSession(id); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model * @return 返回会话令牌 */ public static String createLoginSession(Object id, SaLoginParameter loginParameter) { return stpLogic.createLoginSession(id, loginParameter); } /** * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public static String getOrCreateLoginSession(Object id) { return stpLogic.getOrCreateLoginSession(id); } // --- 注销 (根据 token) /** * 在当前客户端会话注销 */ public static void logout() { stpLogic.logout(); } /** * 在当前客户端会话注销,根据注销参数 */ public static void logout(SaLogoutParameter logoutParameter) { stpLogic.logout(logoutParameter); } /** * 注销下线,根据指定 token * * @param tokenValue 指定 token */ public static void logoutByTokenValue(String tokenValue) { stpLogic.logoutByTokenValue(tokenValue); } /** * 注销下线,根据指定 token、注销参数 * * @param tokenValue 指定 token * @param logoutParameter / */ public static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.logoutByTokenValue(tokenValue, logoutParameter); } /** * 踢人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token */ public static void kickoutByTokenValue(String tokenValue) { stpLogic.kickoutByTokenValue(tokenValue); } /** * 踢人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token * @param logoutParameter 注销参数 */ public static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.kickoutByTokenValue(tokenValue, logoutParameter); } /** * 顶人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token */ public static void replacedByTokenValue(String tokenValue) { stpLogic.replacedByTokenValue(tokenValue); } /** * 顶人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token * @param logoutParameter / */ public static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.replacedByTokenValue(tokenValue, logoutParameter); } // --- 注销 (根据 loginId) /** * 会话注销,根据账号id * * @param loginId 账号id */ public static void logout(Object loginId) { stpLogic.logout(loginId); } /** * 会话注销,根据账号id 和 设备类型 * * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) */ public static void logout(Object loginId, String deviceType) { stpLogic.logout(loginId, deviceType); } /** * 会话注销,根据账号id 和 注销参数 * * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void logout(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.logout(loginId, logoutParameter); } /** * 踢人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id */ public static void kickout(Object loginId) { stpLogic.kickout(loginId); } /** * 踢人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) */ public static void kickout(Object loginId, String deviceType) { stpLogic.kickout(loginId, deviceType); } /** * 踢人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void kickout(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.kickout(loginId, logoutParameter); } /** * 顶人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id */ public static void replaced(Object loginId) { stpLogic.replaced(loginId); } /** * 顶人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) */ public static void replaced(Object loginId, String deviceType) { stpLogic.replaced(loginId, deviceType); } /** * 顶人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void replaced(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.replaced(loginId, logoutParameter); } // --- 注销 (会话管理辅助方法) /** * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) * @param session / * @param terminal / */ public static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByLogout(session, terminal); } /** * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) * @param session / * @param terminal / */ public static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByKickout(session, terminal); } /** * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) * @param session / * @param terminal / */ public static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByReplaced(session, terminal); } // 会话查询 /** * 判断当前会话是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public static boolean isLogin() { return stpLogic.isLogin(); } /** * 判断指定账号是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public static boolean isLogin(Object loginId) { return stpLogic.isLogin(loginId); } /** * 检验当前会话是否已经登录,如未登录,则抛出异常 */ public static void checkLogin() { stpLogic.checkLogin(); } /** * 获取当前会话账号id,如果未登录,则抛出异常 * * @return 账号id */ public static Object getLoginId() { return stpLogic.getLoginId(); } /** * 获取当前会话账号id, 如果未登录,则返回默认值 * * @param 返回类型 * @param defaultValue 默认值 * @return 登录id */ public static T getLoginId(T defaultValue) { return stpLogic.getLoginId(defaultValue); } /** * 获取当前会话账号id, 如果未登录,则返回null * * @return 账号id */ public static Object getLoginIdDefaultNull() { return stpLogic.getLoginIdDefaultNull(); } /** * 获取当前会话账号id, 并转换为 String 类型 * * @return 账号id */ public static String getLoginIdAsString() { return stpLogic.getLoginIdAsString(); } /** * 获取当前会话账号id, 并转换为 int 类型 * * @return 账号id */ public static int getLoginIdAsInt() { return stpLogic.getLoginIdAsInt(); } /** * 获取当前会话账号id, 并转换为 long 类型 * * @return 账号id */ public static long getLoginIdAsLong() { return stpLogic.getLoginIdAsLong(); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null * * @param tokenValue token * @return 账号id */ public static Object getLoginIdByToken(String tokenValue) { return stpLogic.getLoginIdByToken(tokenValue); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null * * @param tokenValue token * @return 账号id */ public static Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { return stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue); } /** * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) * * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String key) { return stpLogic.getExtra(key); } /** * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) * * @param tokenValue 指定的 Token 值 * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String tokenValue, String key) { return stpLogic.getExtra(tokenValue, key); } // ------------------- Account-Session 相关 ------------------- /** * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param loginId 账号id * @param isCreate 是否新建 * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return stpLogic.getSessionByLoginId(loginId, isCreate); } /** * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null * * @param sessionId SessionId * @return Session对象 */ public static SaSession getSessionBySessionId(String sessionId) { return stpLogic.getSessionBySessionId(sessionId); } /** * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param loginId 账号id * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId) { return stpLogic.getSessionByLoginId(loginId); } /** * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param isCreate 是否新建 * @return Session对象 */ public static SaSession getSession(boolean isCreate) { return stpLogic.getSession(isCreate); } /** * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public static SaSession getSession() { return stpLogic.getSession(); } // ------------------- Token-Session 相关 ------------------- /** * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param tokenValue Token值 * @return Session对象 */ public static SaSession getTokenSessionByToken(String tokenValue) { return stpLogic.getTokenSessionByToken(tokenValue); } /** * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public static SaSession getTokenSession() { return stpLogic.getTokenSession(); } /** * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) * * @return Token-Session 对象 */ public static SaSession getAnonTokenSession() { return stpLogic.getAnonTokenSession(); } // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- /** * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) *

* 请注意: 即使 token 已被冻结 也可续签成功, * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 *

*/ public static void updateLastActiveToNow() { stpLogic.updateLastActiveToNow(); } /** * 检查当前 token 是否已被冻结,如果是则抛出异常 */ public static void checkActiveTimeout() { stpLogic.checkActiveTimeout(); } /** * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 * * @return / */ public static long getTokenLastActiveTime() { return stpLogic.getTokenLastActiveTime(); } // ------------------- 过期时间相关 ------------------- /** * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getTokenTimeout() { return stpLogic.getTokenTimeout(); } /** * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param token 指定token * @return token剩余有效时间 */ public static long getTokenTimeout(String token) { return stpLogic.getTokenTimeout(token); } /** * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getSessionTimeout() { return stpLogic.getSessionTimeout(); } /** * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getTokenSessionTimeout() { return stpLogic.getTokenSessionTimeout(); } /** * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) * * @return / */ public static long getTokenActiveTimeout() { return stpLogic.getTokenActiveTimeout(); } /** * 对当前 token 的 timeout 值进行续期 * * @param timeout 要修改成为的有效时间 (单位: 秒) */ public static void renewTimeout(long timeout) { stpLogic.renewTimeout(timeout); } /** * 对指定 token 的 timeout 值进行续期 * * @param tokenValue 指定 token * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) */ public static void renewTimeout(String tokenValue, long timeout) { stpLogic.renewTimeout(tokenValue, timeout); } // ------------------- 角色认证操作 ------------------- /** * 获取:当前账号的角色集合 * * @return / */ public static List getRoleList() { return stpLogic.getRoleList(); } /** * 获取:指定账号的角色集合 * * @param loginId 指定账号id * @return / */ public static List getRoleList(Object loginId) { return stpLogic.getRoleList(loginId); } /** * 判断:当前账号是否拥有指定角色, 返回 true 或 false * * @param role 角色 * @return / */ public static boolean hasRole(String role) { return stpLogic.hasRole(role); } /** * 判断:指定账号是否含有指定角色标识, 返回 true 或 false * * @param loginId 账号id * @param role 角色标识 * @return 是否含有指定角色标识 */ public static boolean hasRole(Object loginId, String role) { return stpLogic.hasRole(loginId, role); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 * @return true或false */ public static boolean hasRoleAnd(String... roleArray){ return stpLogic.hasRoleAnd(roleArray); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 * @return true或false */ public static boolean hasRoleOr(String... roleArray){ return stpLogic.hasRoleOr(roleArray); } /** * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException * * @param role 角色标识 */ public static void checkRole(String role) { stpLogic.checkRole(role); } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 */ public static void checkRoleAnd(String... roleArray){ stpLogic.checkRoleAnd(roleArray); } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 */ public static void checkRoleOr(String... roleArray){ stpLogic.checkRoleOr(roleArray); } // ------------------- 权限认证操作 ------------------- /** * 获取:当前账号的权限码集合 * * @return / */ public static List getPermissionList() { return stpLogic.getPermissionList(); } /** * 获取:指定账号的权限码集合 * * @param loginId 指定账号id * @return / */ public static List getPermissionList(Object loginId) { return stpLogic.getPermissionList(loginId); } /** * 判断:当前账号是否含有指定权限, 返回 true 或 false * * @param permission 权限码 * @return 是否含有指定权限 */ public static boolean hasPermission(String permission) { return stpLogic.hasPermission(permission); } /** * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false * * @param loginId 账号 id * @param permission 权限码 * @return 是否含有指定权限 */ public static boolean hasPermission(Object loginId, String permission) { return stpLogic.hasPermission(loginId, permission); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public static boolean hasPermissionAnd(String... permissionArray){ return stpLogic.hasPermissionAnd(permissionArray); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public static boolean hasPermissionOr(String... permissionArray){ return stpLogic.hasPermissionOr(permissionArray); } /** * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException * * @param permission 权限码 */ public static void checkPermission(String permission) { stpLogic.checkPermission(permission); } /** * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] * * @param permissionArray 权限码数组 */ public static void checkPermissionAnd(String... permissionArray) { stpLogic.checkPermissionAnd(permissionArray); } /** * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 */ public static void checkPermissionOr(String... permissionArray) { stpLogic.checkPermissionOr(permissionArray); } // ------------------- id 反查 token 相关操作 ------------------- /** * 获取指定账号 id 的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @return token值 */ public static String getTokenValueByLoginId(Object loginId) { return stpLogic.getTokenValueByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return token值 */ public static String getTokenValueByLoginId(Object loginId, String deviceType) { return stpLogic.getTokenValueByLoginId(loginId, deviceType); } /** * 获取指定账号 id 的 token 集合 * * @param loginId 账号id * @return 此 loginId 的所有相关 token */ public static List getTokenValueListByLoginId(Object loginId) { return stpLogic.getTokenValueListByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的 token 集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public static List getTokenValueListByLoginId(Object loginId, String deviceType) { return stpLogic.getTokenValueListByLoginId(loginId, deviceType); } /** * 获取指定账号 id 已登录设备信息集合 * * @param loginId 账号id * @return 此 loginId 的所有登录 token */ public static List getTerminalListByLoginId(Object loginId) { return stpLogic.getTerminalListByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的已登录设备信息集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return / */ public static List getTerminalListByLoginId(Object loginId, String deviceType) { return stpLogic.getTerminalListByLoginId(loginId, deviceType); } /** * 获取指定账号 id 已登录设备信息集合,执行特定函数 * * @param loginId 账号id * @param function 需要执行的函数 */ public static void forEachTerminalList(Object loginId, SaTwoParamFunction function) { stpLogic.forEachTerminalList(loginId, function); } /** * 返回当前 token 指向的 SaTerminalInfo 设备信息,如果 token 无效则返回 null * * @return / */ public static SaTerminalInfo getTerminalInfo() { return stpLogic.getTerminalInfo(); } /** * 返回指定 token 指向的 SaTerminalInfo 设备信息,如果 Token 无效则返回 null * * @param tokenValue 指定 token * @return / */ public static SaTerminalInfo getTerminalInfoByToken(String tokenValue) { return stpLogic.getTerminalInfoByToken(tokenValue); } /** * 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ public static String getLoginDeviceType() { return stpLogic.getLoginDeviceType(); } /** * 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ public static String getLoginDeviceTypeByToken(String tokenValue) { return stpLogic.getLoginDeviceTypeByToken(tokenValue); } /** * 返回当前会话的登录设备 ID * * @return / */ public static String getLoginDeviceId() { return stpLogic.getLoginDeviceId(); } /** * 返回指定 token 会话的登录设备 ID * * @param tokenValue 指定token * @return / */ public static String getLoginDeviceIdByToken(String tokenValue) { return stpLogic.getLoginDeviceIdByToken(tokenValue); } /** * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 * @param deviceId / * @return / */ public static boolean isTrustDeviceId(Object userId, String deviceId) { return stpLogic.isTrustDeviceId(userId, deviceId); } // ------------------- 会话管理 ------------------- /** * 根据条件查询缓存中所有的 token * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return token集合 */ public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { return stpLogic.searchTokenValue(keyword, start, size, sortType); } /** * 根据条件查询缓存中所有的 SessionId * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public static List searchSessionId(String keyword, int start, int size, boolean sortType) { return stpLogic.searchSessionId(keyword, start, size, sortType); } /** * 根据条件查询缓存中所有的 Token-Session-Id * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { return stpLogic.searchTokenSessionId(keyword, start, size, sortType); } // ------------------- 账号封禁 ------------------- /** * 封禁:指定账号 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, long time) { stpLogic.disable(loginId, time); } /** * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) * * @param loginId 账号id * @return / */ public static boolean isDisable(Object loginId) { return stpLogic.isDisable(loginId); } /** * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id */ public static void checkDisable(Object loginId) { stpLogic.checkDisable(loginId); } /** * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @return / */ public static long getDisableTime(Object loginId) { return stpLogic.getDisableTime(loginId); } /** * 解封:指定账号 * * @param loginId 账号id */ public static void untieDisable(Object loginId) { stpLogic.untieDisable(loginId); } // ------------------- 分类封禁 ------------------- /** * 封禁:指定账号的指定服务 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param service 指定服务 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, String service, long time) { stpLogic.disable(loginId, service, time); } /** * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return / */ public static boolean isDisable(Object loginId, String service) { return stpLogic.isDisable(loginId, service); } /** * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void checkDisable(Object loginId, String... services) { stpLogic.checkDisable(loginId, services); } /** * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return see note */ public static long getDisableTime(Object loginId, String service) { return stpLogic.getDisableTime(loginId, service); } /** * 解封:指定账号、指定服务 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void untieDisable(Object loginId, String... services) { stpLogic.untieDisable(loginId, services); } // ------------------- 阶梯封禁 ------------------- /** * 封禁:指定账号,并指定封禁等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, int level, long time) { stpLogic.disableLevel(loginId, level, time); } /** * 封禁:指定账号的指定服务,并指定封禁等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, String service, int level, long time) { stpLogic.disableLevel(loginId, service, level, time); } /** * 判断:指定账号是否已被封禁到指定等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, int level) { return stpLogic.isDisableLevel(loginId, level); } /** * 判断:指定账号的指定服务,是否已被封禁到指定等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, String service, int level) { return stpLogic.isDisableLevel(loginId, service, level); } /** * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, int level) { stpLogic.checkDisableLevel(loginId, level); } /** * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, String service, int level) { stpLogic.checkDisableLevel(loginId, service, level); } /** * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @return / */ public static int getDisableLevel(Object loginId) { return stpLogic.getDisableLevel(loginId); } /** * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @param service 指定封禁服务 * @return / */ public static int getDisableLevel(Object loginId, String service) { return stpLogic.getDisableLevel(loginId, service); } // ------------------- 临时身份切换 ------------------- /** * 临时切换身份为指定账号id * * @param loginId 指定loginId */ public static void switchTo(Object loginId) { stpLogic.switchTo(loginId); } /** * 结束临时切换身份 */ public static void endSwitch() { stpLogic.endSwitch(); } /** * 判断当前请求是否正处于 [ 身份临时切换 ] 中 * * @return / */ public static boolean isSwitch() { return stpLogic.isSwitch(); } /** * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 * * @param loginId 指定账号id * @param function 要执行的方法 */ public static void switchTo(Object loginId, SaFunction function) { stpLogic.switchTo(loginId, function); } // ------------------- 二级认证 ------------------- /** * 在当前会话 开启二级认证 * * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(long safeTime) { stpLogic.openSafe(safeTime); } /** * 在当前会话 开启二级认证 * * @param service 业务标识 * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(String service, long safeTime) { stpLogic.openSafe(service, safeTime); } /** * 判断:当前会话是否处于二级认证时间内 * * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe() { return stpLogic.isSafe(); } /** * 判断:当前会话 是否处于指定业务的二级认证时间内 * * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String service) { return stpLogic.isSafe(service); } /** * 判断:指定 token 是否处于二级认证时间内 * * @param tokenValue Token 值 * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String tokenValue, String service) { return stpLogic.isSafe(tokenValue, service); } /** * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 */ public static void checkSafe() { stpLogic.checkSafe(); } /** * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 * * @param service 业务标识 */ public static void checkSafe(String service) { stpLogic.checkSafe(service); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @return 剩余有效时间 */ public static long getSafeTime() { return stpLogic.getSafeTime(); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @param service 业务标识 * @return 剩余有效时间 */ public static long getSafeTime(String service) { return stpLogic.getSafeTime(service); } /** * 在当前会话 结束二级认证 */ public static void closeSafe() { stpLogic.closeSafe(); } /** * 在当前会话 结束指定业务标识的二级认证 * * @param service 业务标识 */ public static void closeSafe(String service) { stpLogic.closeSafe(service); } // ------------------- Bean 对象、字段代理 ------------------- /** * 根据当前配置对象创建一个 SaLoginParameter 对象 * * @return / */ public static SaLoginParameter createSaLoginParameter() { return stpLogic.createSaLoginParameter(); } // ------------------- 过期方法 ------------------- /** *

请更换为 getLoginDeviceType

* 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ @Deprecated public static String getLoginDevice() { return stpLogic.getLoginDevice(); } /** *

请更换为 getLoginDeviceTypeByToken

* 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ @Deprecated public static String getLoginDeviceByToken(String tokenValue) { return stpLogic.getLoginDeviceByToken(tokenValue); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLoginParameter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp.parameter; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaCookieConfig; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; import cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode; import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; import cn.dev33.satoken.util.SaTokenConsts; import java.util.LinkedHashMap; import java.util.Map; /** * 在调用 `StpUtil.login()` 时的 配置参数对象,决定登录的一些细节行为
* *
 *     	// 例如:在登录时指定 token 有效期为七天,代码如下:
 *     	StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));
 * 
* * @author click33 * @since 1.13.2 */ public class SaLoginParameter { // --------- 单独参数 /** * 此次登录的客户端设备类型 */ private String deviceType; /** * 此次登录的客户端设备id */ private String deviceId; /** * 扩展信息(只在 jwt 模式下生效) */ private Map extraData; /** * 预定Token(预定本次登录生成的Token值) */ private String token; /** * 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 */ private Map terminalExtraData; // --------- 覆盖性参数 /** * 指定此次登录 token 有效期,单位:秒 (如未指定,自动取全局配置的 timeout 值) */ private long timeout; /** * 指定此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) */ private Long activeTimeout; /** * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) */ private Boolean isConcurrent; /** * 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) */ private Boolean isShare; /** * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) */ private int maxLoginCount; /** * 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) */ private int maxTryTimes; /** * 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) */ private Boolean isLastingCookie; /** * 是否在登录后将 Token 写入到响应头 */ private Boolean isWriteHeader; /** * 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 */ private SaReplacedLoginExitMode replacedLoginExitMode; /** * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) */ private SaReplacedRange replacedRange; /** * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) */ private SaLogoutMode overflowLogoutMode; /** * 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) */ private Boolean rightNowCreateTokenSession; /** * Cookie 配置对象 */ public SaCookieConfig cookie = new SaCookieConfig(); // ------ 附加方法 public SaLoginParameter() { this(SaManager.getConfig()); } public SaLoginParameter(SaTokenConfig config) { setDefaultValues(config); } /** * 根据 SaTokenConfig 对象初始化默认值 * * @param config 使用的配置对象 * @return 对象自身 */ public SaLoginParameter setDefaultValues(SaTokenConfig config) { this.deviceType = SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE; this.timeout = config.getTimeout(); this.isConcurrent = config.getIsConcurrent(); this.isShare = config.getIsShare(); this.maxLoginCount = config.getMaxLoginCount(); this.maxTryTimes = config.getMaxTryTimes(); this.isLastingCookie = config.getIsLastingCookie(); this.isWriteHeader = config.getIsWriteHeader(); this.replacedRange = config.getReplacedRange(); this.overflowLogoutMode = config.getOverflowLogoutMode(); this.rightNowCreateTokenSession = config.getRightNowCreateTokenSession(); this.replacedLoginExitMode = config.getReplacedLoginExitMode(); this.setupCookieConfig(cookie -> { SaCookieConfig gCookie = config.getCookie(); cookie.setDomain(gCookie.getDomain()); cookie.setPath(gCookie.getPath()); cookie.setSecure(gCookie.getSecure()); cookie.setHttpOnly(gCookie.getHttpOnly()); cookie.setSameSite(gCookie.getSameSite()); cookie.setExtraAttrs(new LinkedHashMap<>(gCookie.getExtraAttrs())); }); return this; } /** * 写入扩展数据(只在jwt模式下生效) * @param key 键 * @param value 值 * @return 对象自身 */ public SaLoginParameter setExtra(String key, Object value) { if(this.extraData == null) { this.extraData = new LinkedHashMap<>(); } this.extraData.put(key, value); return this; } /** * 获取扩展数据(只在jwt模式下生效) * @param key 键 * @return 扩展数据的值 */ public Object getExtra(String key) { if(this.extraData == null) { return null; } return this.extraData.get(key); } /** * 判断是否设置了扩展数据(只在jwt模式下生效) * @return / */ public boolean haveExtraData() { return extraData != null && !extraData.isEmpty(); } /** * 写入本次登录挂载到 SaTerminalInfo 的自定义扩展数据 * @param key 键 * @param value 值 * @return 对象自身 */ public SaLoginParameter setTerminalExtra(String key, Object value) { if(this.terminalExtraData == null) { this.terminalExtraData = new LinkedHashMap<>(); } this.terminalExtraData.put(key, value); return this; } /** * 获取本次登录挂载到 SaTerminalInfo 的自定义扩展数据 * @param key 键 * @return 扩展数据的值 */ public Object getTerminalExtra(String key) { if(this.terminalExtraData == null) { return null; } return this.terminalExtraData.get(key); } /** * 判断是否设置了本次登录挂载到 SaTerminalInfo 的自定义扩展数据 * @return / */ public boolean haveTerminalExtraData() { return terminalExtraData != null && !terminalExtraData.isEmpty(); } /** * 计算 Cookie 时长 * @return / */ public int getCookieTimeout() { if( ! getIsLastingCookie()) { return -1; } long _timeout = getTimeout(); if(_timeout == SaTokenDao.NEVER_EXPIRE || _timeout > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } return (int)_timeout; } /** * 静态方法获取一个 SaLoginParameter 对象 * @return SaLoginParameter 对象 */ public static SaLoginParameter create() { return new SaLoginParameter(SaManager.getConfig()); } /** * 设置 Cookie 配置项 * @param fun / * @return 对象自身 */ public SaLoginParameter setupCookieConfig(SaParamFunction fun) { fun.run(this.cookie); return this; } // ---------------- get set /** * @return 此次登录的客户端设备类型 */ public String getDeviceType() { return deviceType; } /** * @param deviceType 此次登录的客户端设备类型 * @return 对象自身 */ public SaLoginParameter setDeviceType(String deviceType) { this.deviceType = deviceType; return this; } /** * 获取 此次登录的客户端设备id * * @return deviceId 此次登录的客户端设备id */ public String getDeviceId() { return this.deviceId; } /** * 设置 此次登录的客户端设备id * * @param deviceId 此次登录的客户端设备id */ public SaLoginParameter setDeviceId(String deviceId) { this.deviceId = deviceId; return this; } /** * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) * * @return replacedMode 顶人下线的范围 */ public SaReplacedRange getReplacedRange() { return this.replacedRange; } /** * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) * * @param replacedRange / * @return 对象自身 */ public SaLoginParameter setReplacedRange(SaReplacedRange replacedRange) { this.replacedRange = replacedRange; return this; } /** * 获取 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) * * @return overflowLogoutMode / */ public SaLogoutMode getOverflowLogoutMode() { return this.overflowLogoutMode; } /** * 设置 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) * * @param overflowLogoutMode / * @return 对象自身 */ public SaLoginParameter setOverflowLogoutMode(SaLogoutMode overflowLogoutMode) { this.overflowLogoutMode = overflowLogoutMode; return this; } /** * @return 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) */ public Boolean getIsLastingCookie() { return isLastingCookie; } /** * @param isLastingCookie 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) * @return 对象自身 */ public SaLoginParameter setIsLastingCookie(Boolean isLastingCookie) { this.isLastingCookie = isLastingCookie; return this; } /** * @return 指定此次登录 token 有效期,单位:秒 */ public long getTimeout() { return timeout; } /** * @param timeout 指定此次登录 token 有效期,单位:秒 (如未指定,自动取全局配置的 timeout 值) * @return 对象自身 */ public SaLoginParameter setTimeout(long timeout) { this.timeout = timeout; return this; } /** * @return 此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) */ public Long getActiveTimeout() { return activeTimeout; } /** * @param activeTimeout 指定此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) * @return 对象自身 */ public SaLoginParameter setActiveTimeout(long activeTimeout) { this.activeTimeout = activeTimeout; return this; } /** * @return 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) */ public Boolean getIsConcurrent() { return isConcurrent; } /** * @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) * @return 对象自身 */ public SaLoginParameter setIsConcurrent(Boolean isConcurrent) { this.isConcurrent = isConcurrent; return this; } /** * @return 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) */ public Boolean getIsShare() { return isShare; } /** * @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) * @return 对象自身 */ public SaLoginParameter setIsShare(Boolean isShare) { this.isShare = isShare; return this; } /** * @return 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) */ public int getMaxLoginCount() { return maxLoginCount; } /** * @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) * @return 对象自身 */ public SaLoginParameter setMaxLoginCount(int maxLoginCount) { this.maxLoginCount = maxLoginCount; return this; } /** * @return 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) */ public int getMaxTryTimes() { return maxTryTimes; } /** * @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) * @return 对象自身 */ public SaLoginParameter setMaxTryTimes(int maxTryTimes) { this.maxTryTimes = maxTryTimes; return this; } /** * @return 扩展信息(只在jwt模式下生效) */ public Map getExtraData() { return extraData; } /** * @param extraData 扩展信息(只在jwt模式下生效) * @return 对象自身 */ public SaLoginParameter setExtraData(Map extraData) { this.extraData = extraData; return this; } /** * @return 预定Token(预定本次登录生成的Token值) */ public String getToken() { return token; } /** * @param token 预定Token(预定本次登录生成的Token值) * @return 对象自身 */ public SaLoginParameter setToken(String token) { this.token = token; return this; } /** * @return 是否在登录后将 Token 写入到响应头 */ public Boolean getIsWriteHeader() { return isWriteHeader; } /** * @param isWriteHeader 是否在登录后将 Token 写入到响应头 * @return 对象自身 */ public SaLoginParameter setIsWriteHeader(Boolean isWriteHeader) { this.isWriteHeader = isWriteHeader; return this; } /** * 获取 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 * * @return / */ public Map getTerminalExtraData() { return this.terminalExtraData; } /** * 设置 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 * * @param terminalExtraData / * @return 对象自身 */ public SaLoginParameter setTerminalExtraData(Map terminalExtraData) { this.terminalExtraData = terminalExtraData; return this; } /** * 获取 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) * * @return / */ public Boolean getRightNowCreateTokenSession() { return this.rightNowCreateTokenSession; } /** * 设置 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) * * @param rightNowCreateTokenSession / * @return 对象自身 */ public SaLoginParameter setRightNowCreateTokenSession(Boolean rightNowCreateTokenSession) { this.rightNowCreateTokenSession = rightNowCreateTokenSession; return this; } /** * @return Cookie 配置对象 */ public SaCookieConfig getCookie() { return cookie; } /** * @param cookie Cookie 配置对象 * @return 对象自身 */ public SaLoginParameter setCookie(SaCookieConfig cookie) { this.cookie = cookie; return this; } /** * 获取:在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) * @return / */ public SaReplacedLoginExitMode getReplacedLoginExitMode() { return replacedLoginExitMode; } /** * 设置:在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) * @param replacedLoginExitMode / * @return 对象自身 */ public SaLoginParameter setReplacedLoginExitMode(SaReplacedLoginExitMode replacedLoginExitMode) { this.replacedLoginExitMode = replacedLoginExitMode; return this; } /* * toString */ @Override public String toString() { return "SaLoginParameter [" + "deviceType=" + deviceType + ", deviceId=" + deviceId + ", replacedRange=" + replacedRange + ", replacedLoginExitMode=" + replacedLoginExitMode + ", overflowLogoutMode=" + overflowLogoutMode + ", isLastingCookie=" + isLastingCookie + ", timeout=" + timeout + ", activeTimeout=" + activeTimeout + ", isConcurrent=" + isConcurrent + ", isShare=" + isShare + ", maxLoginCount=" + maxLoginCount + ", maxTryTimes=" + maxTryTimes + ", extraData=" + extraData + ", token=" + token + ", isWriteHeader=" + isWriteHeader + ", terminalTag=" + terminalExtraData + ", rightNowCreateTokenSession=" + rightNowCreateTokenSession + ", cookie=" + cookie + "]"; } /** *

请更换为 getDeviceType

* @return 此次登录的客户端设备类型 */ @Deprecated public String getDevice() { return deviceType; } /** *

请更换为 setDeviceType

* @param device 此次登录的客户端设备类型 * @return 对象自身 */ @Deprecated public SaLoginParameter setDevice(String device) { this.deviceType = device; return this; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLogoutParameter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp.parameter; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; import cn.dev33.satoken.stp.parameter.enums.SaLogoutRange; /** * 在会话注销时的 配置参数对象,决定注销时的一些细节行为
* *
 *     	// 例如:
 *     	StpUtil.logout(10001, new SaLogoutParameter());
 * 
* * @author click33 * @since 1.41.0 */ public class SaLogoutParameter { // --------- 单独参数 /** * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备类型的会话) *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) */ private String deviceType; /** * 需要注销的设备ID (为 null 代表不限制,为具体值代表只注销此设备ID的会话) *
(此参数只在调用 StpUtil.logout(id, param) 时有效) */ private String deviceId; /** * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线,REPLACED=顶人下线) */ private SaLogoutMode mode = SaLogoutMode.LOGOUT; // --------- 覆盖性参数 /** * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) *
(此参数只在调用 StpUtil.logout(param) 时有效) */ private SaLogoutRange range; /** * 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token", param) 时有效) */ private Boolean isKeepFreezeOps; /** * 在注销 token 后,是否保留其对应的 Token-Session */ private Boolean isKeepTokenSession; // ------ 附加方法 public SaLogoutParameter() { this(SaManager.getConfig()); } public SaLogoutParameter(SaTokenConfig config) { setDefaultValues(config); } /** * 根据 SaTokenConfig 对象初始化默认值 * * @param config 使用的配置对象 * @return 对象自身 */ public SaLogoutParameter setDefaultValues(SaTokenConfig config) { this.range = config.getLogoutRange(); this.isKeepFreezeOps = config.getIsLogoutKeepFreezeOps(); this.isKeepTokenSession = config.getIsLogoutKeepTokenSession(); return this; } /** * 静态方法获取一个 SaLoginParameter 对象 * @return SaLoginParameter 对象 */ public static SaLogoutParameter create() { return new SaLogoutParameter(SaManager.getConfig()); } // ---------------- get set /** * @return 在注销 token 后,是否保留其对应的 Token-Session */ public Boolean getIsKeepTokenSession() { return isKeepTokenSession; } /** * @param isKeepTokenSession 在注销 token 后,是否保留其对应的 Token-Session * * @return 对象自身 */ public SaLogoutParameter setIsKeepTokenSession(Boolean isKeepTokenSession) { this.isKeepTokenSession = isKeepTokenSession; return this; } /** * 获取 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token", param) 时有效) * * @return / */ public Boolean getIsKeepFreezeOps() { return this.isKeepFreezeOps; } /** * 设置 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) *
(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token", param) 时有效) * * @param isKeepFreezeOps / * @return 对象自身 */ public SaLogoutParameter setIsKeepFreezeOps(Boolean isKeepFreezeOps) { this.isKeepFreezeOps = isKeepFreezeOps; return this; } /** * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备类型的会话) *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) * * @return deviceType / */ public String getDeviceType() { return this.deviceType; } /** * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备类型的会话) *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) * * @param deviceType / * @return / */ public SaLogoutParameter setDeviceType(String deviceType) { this.deviceType = deviceType; return this; } /** * 需要注销的设备ID (为 null 代表不限制,为具体值代表只注销此设备 ID 的会话) *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) * * @return / */ public String getDeviceId() { return this.deviceId; } /** * 需要注销的设备类型 (为 null 代表不限制,为具体值代表只注销此设备 ID 的会话) *
(此参数只在调用 StpUtil.logout(id, parame) 时有效) * * @param deviceId / * @return / */ public SaLogoutParameter setDeviceId(String deviceId) { this.deviceId = deviceId; return this; } /** * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线,REPLACED=顶人下线) * * @return logoutMode 注销类型 */ public SaLogoutMode getMode() { return this.mode; } /** * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线,REPLACED=顶人下线) * * @param mode 注销类型 * @return / */ public SaLogoutParameter setMode(SaLogoutMode mode) { this.mode = mode; return this; } /** * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) *
(此参数只在调用 StpUtil.logout(param) 时有效) * * @return / */ public SaLogoutRange getRange() { return this.range; } /** * 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) *
(此参数只在调用 StpUtil.logout(param) 时有效) * * @param range / * @return / */ public SaLogoutParameter setRange(SaLogoutRange range) { this.range = range; return this; } /* * toString */ @Override public String toString() { return "SaLoginParameter [" + "deviceType=" + deviceType + ", deviceId=" + deviceId + ", isKeepTokenSession=" + isKeepTokenSession + ", isKeepFreezeOps=" + isKeepFreezeOps + ", mode=" + mode + ", range=" + range + "]"; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutMode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp.parameter.enums; /** * SaLogoutMode: 注销模式 * * @author click33 * @since 1.41.0 */ public enum SaLogoutMode { /** * 注销下线 */ LOGOUT, /** * 踢人下线 */ KICKOUT, /** * 顶人下线 */ REPLACED; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutRange.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp.parameter.enums; /** * SaLogoutMode: 注销范围 * * @author click33 * @since 1.41.0 */ public enum SaLogoutRange { /** * token 范围:只注销提供的 token 指向的会话 */ TOKEN, /** * 账号范围:注销 token 指向的 loginId 会话 */ ACCOUNT } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedLoginExitMode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp.parameter.enums; /** * 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 * @author 石泽旭 * @since 1.44.0 */ public enum SaReplacedLoginExitMode { /** * 旧设备下线,新设备登录成功 */ OLD_DEVICE, /** * 新设备登录失败,旧设备维持在线 */ NEW_DEVICE } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedRange.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.stp.parameter.enums; /** * 顶人下线的范围 * * @author click33 * @since 1.41.0 */ public enum SaReplacedRange { /** * 当前指定的设备类型端 */ CURR_DEVICE_TYPE, /** * 所有设备类型端 */ ALL_DEVICE_TYPE } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaAnnotationStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy; import cn.dev33.satoken.annotation.*; import cn.dev33.satoken.annotation.handler.*; import cn.dev33.satoken.fun.strategy.*; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.router.SaRouter; import java.lang.annotation.Annotation; import java.util.*; /** * Sa-Token 注解鉴权相关策略 * * @author click33 * @since 1.39.0 */ public final class SaAnnotationStrategy { private SaAnnotationStrategy() { registerDefaultAnnotationHandler(); } /** * 全局单例引用 */ public static final SaAnnotationStrategy instance = new SaAnnotationStrategy(); // ----------------------- 所有策略 /** * 注解处理器集合 */ public Map, SaAnnotationHandlerInterface> annotationHandlerMap = new LinkedHashMap<>(); /** * 注册所有默认的注解处理器 */ public void registerDefaultAnnotationHandler() { annotationHandlerMap.put(SaCheckLogin.class, new SaCheckLoginHandler()); annotationHandlerMap.put(SaCheckRole.class, new SaCheckRoleHandler()); annotationHandlerMap.put(SaCheckPermission.class, new SaCheckPermissionHandler()); annotationHandlerMap.put(SaCheckSafe.class, new SaCheckSafeHandler()); annotationHandlerMap.put(SaCheckDisable.class, new SaCheckDisableHandler()); annotationHandlerMap.put(SaCheckHttpBasic.class, new SaCheckHttpBasicHandler()); annotationHandlerMap.put(SaCheckHttpDigest.class, new SaCheckHttpDigestHandler()); annotationHandlerMap.put(SaCheckOr.class, new SaCheckOrHandler()); } /** * 注册一个注解处理器 */ public void registerAnnotationHandler(SaAnnotationHandlerInterface handler) { annotationHandlerMap.put(handler.getHandlerAnnotationClass(), handler); SaTokenEventCenter.doRegisterAnnotationHandler(handler); } /** * 注册一个注解处理器,到首位 */ public void registerAnnotationHandlerToFirst(SaAnnotationHandlerInterface handler) { Map, SaAnnotationHandlerInterface> newMap = new LinkedHashMap<>(); newMap.put(handler.getHandlerAnnotationClass(), handler); newMap.putAll(annotationHandlerMap); this.annotationHandlerMap = newMap; SaTokenEventCenter.doRegisterAnnotationHandler(handler); } /** * 移除一个注解处理器 */ public void removeAnnotationHandler(Class cls) { annotationHandlerMap.remove(cls); } /** * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) */ public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { // 如果 Method 或其所属 Class 上有 @SaIgnore 注解,则直接跳过整个校验过程 if(instance.isAnnotationPresent.apply(method, SaIgnore.class)) { SaRouter.stop(); } // 先校验 Method 所属 Class 上的注解 instance.checkElementAnnotation.accept(method.getDeclaringClass()); // 再校验 Method 上的注解 instance.checkElementAnnotation.accept(method); }; /** * 对一个 [Element] 对象进行注解校验 (注解鉴权内部实现) */ @SuppressWarnings("unchecked") public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> { // 如果此元素上标注了 @SaCheckOr,则必须在后续判断中忽略掉其指定的 append() 类型注解判断 List> ignoreClassList = new ArrayList<>(); SaCheckOr checkOr = (SaCheckOr)instance.getAnnotation.apply(element, SaCheckOr.class); if(checkOr != null) { ignoreClassList = Arrays.asList(checkOr.append()); } // 遍历所有的注解处理器,检查此 element 是否具有这些指定的注解 for (Map.Entry, SaAnnotationHandlerInterface> entry: annotationHandlerMap.entrySet()) { // 忽略掉在 @SaCheckOr 中 append 字段指定的注解 Class atClass = (Class)entry.getKey(); if(ignoreClassList.contains(atClass)) { continue; } Annotation annotation = instance.getAnnotation.apply(element, atClass); if(annotation != null) { entry.getValue().check(annotation, element); } } }; /** * 从元素上获取注解 */ public SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{ return element.getAnnotation(annotationClass); }; /** * 判断一个 Method 或其所属 Class 是否包含指定注解 */ public SaIsAnnotationPresentFunction isAnnotationPresent = (method, annotationClass) -> { return instance.getAnnotation.apply(method, annotationClass) != null || instance.getAnnotation.apply(method.getDeclaringClass(), annotationClass) != null; }; /** * SaCheckELRootMap 扩展函数 */ public SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> { // 默认不做任何处理 }; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaFirewallStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.strategy.SaFirewallCheckFailHandleFunction; import cn.dev33.satoken.fun.strategy.SaFirewallCheckFunction; import cn.dev33.satoken.strategy.hooks.*; import java.util.ArrayList; import java.util.List; /** * Sa-Token 防火墙策略 * * @author click33 * @since 1.40.0 */ public final class SaFirewallStrategy { /** * 全局单例引用 */ public static final SaFirewallStrategy instance = new SaFirewallStrategy(); /** * 防火墙校验钩子函数集合 */ public List checkHooks = new ArrayList<>(); private SaFirewallStrategy() { // 初始化默认的防火墙校验钩子函数集合 checkHooks.add(SaFirewallCheckHookForWhitePath.instance); checkHooks.add(SaFirewallCheckHookForBlackPath.instance); checkHooks.add(SaFirewallCheckHookForPathDangerCharacter.instance); checkHooks.add(SaFirewallCheckHookForPathBannedCharacter.instance); checkHooks.add(SaFirewallCheckHookForDirectoryTraversal.instance); checkHooks.add(SaFirewallCheckHookForHost.instance); checkHooks.add(SaFirewallCheckHookForHttpMethod.instance); checkHooks.add(SaFirewallCheckHookForHeader.instance); checkHooks.add(SaFirewallCheckHookForParameter.instance); } /** * 注册一个防火墙校验 hook * @param checkHook / */ public void registerHook(SaFirewallCheckHook checkHook) { SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass()); checkHooks.add(checkHook); } /** * 注册一个防火墙校验 hook 到第一位, * 请注意将 hook 注册到第一位将会优先于白名单的判断,如果您依然希望白名单 hook 保持最高优先级,请调用 registerHookToSecond * @param checkHook / */ public void registerHookToFirst(SaFirewallCheckHook checkHook) { SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass()); checkHooks.add(0, checkHook); } /** * 注册一个防火墙校验 hook 到第二位 * @param checkHook / */ public void registerHookToSecond(SaFirewallCheckHook checkHook) { SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass()); checkHooks.add(1, checkHook); } /** * 移除指定类型的防火墙校验 hook * @param hookClass / */ public void removeHook(Class hookClass) { for (SaFirewallCheckHook hook : checkHooks) { if (hook.getClass().equals(hookClass)) { checkHooks.remove(hook); SaManager.getLog().info("防火墙校验 hook 移除成功: " + hookClass); return; } } } /** * 防火墙校验函数 */ public SaFirewallCheckFunction check = (req, res, extArg) -> { for (SaFirewallCheckHook checkHook : checkHooks) { checkHook.execute(req, res, extArg); } }; /** * 当请求 path 校验不通过时地处理方案,自定义示例: *
	 * 		SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {
	 * 			// 自定义处理逻辑 ...
	 *      };
	 * 
*/ public SaFirewallCheckFailHandleFunction checkFailHandle = null; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.NotImplException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.fun.strategy.*; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTokenConsts; import java.util.UUID; /** * Sa-Token 策略对象 *

* 此类统一定义框架内的一些关键性逻辑算法,方便开发者进行按需重写,例: *

*
 // SaStrategy全局单例,所有方法都用以下形式重写
 SaStrategy.instance.setCreateToken((loginId, loginType) -》 {
 // 自定义Token生成的算法
 return "xxxx";
 });
 * 
* * @author click33 * @since 1.27.0 */ public final class SaStrategy { private SaStrategy() { } /** * 获取 SaStrategy 对象的单例引用 */ public static final SaStrategy instance = new SaStrategy(); // ----------------------- 所有策略 /** * 创建 Token 的策略 */ public SaCreateTokenFunction createToken = (loginId, loginType) -> { // 根据配置的tokenStyle生成不同风格的token String tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle(); switch (tokenStyle) { // uuid case SaTokenConsts.TOKEN_STYLE_UUID: return UUID.randomUUID().toString(); // 简单uuid (不带下划线) case SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID: return UUID.randomUUID().toString().replaceAll("-", ""); // 32位随机字符串 case SaTokenConsts.TOKEN_STYLE_RANDOM_32: return SaFoxUtil.getRandomString(32); // 64位随机字符串 case SaTokenConsts.TOKEN_STYLE_RANDOM_64: return SaFoxUtil.getRandomString(64); // 128位随机字符串 case SaTokenConsts.TOKEN_STYLE_RANDOM_128: return SaFoxUtil.getRandomString(128); // tik风格 (2_14_16) case SaTokenConsts.TOKEN_STYLE_TIK: return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__"; // 默认,还是uuid default: SaManager.getLog().warn("配置的 tokenStyle 值无效:{},仅允许以下取值: " + "uuid、simple-uuid、random-32、random-64、random-128、tik", tokenStyle); return UUID.randomUUID().toString(); } }; /** * 创建 Session 的策略 */ public SaCreateSessionFunction createSession = (sessionId) -> { return new SaSession(sessionId); }; /** * 反序列化 SaSession 时默认指定的类型 */ public volatile Class sessionClassType = SaSession.class; /** * 判断:集合中是否包含指定元素(模糊匹配) */ public SaHasElementFunction hasElement = (list, element) -> { // 空集合直接返回false if(list == null || list.size() == 0) { return false; } // 先尝试一下简单匹配,如果可以匹配成功则无需继续模糊匹配 if (list.contains(element)) { return true; } // 开始模糊匹配 for (String patt : list) { if(SaFoxUtil.vagueMatch(patt, element)) { return true; } } // 走出for循环说明没有一个元素可以匹配成功 return false; }; /** * 生成唯一式 token 的算法 */ public SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> { // 为方便叙述,以下代码注释均假设在处理生成 token 的场景,但实际上本方法也可能被用于生成 code、ticket 等 // 循环生成 for (int i = 1; ; i++) { // 生成 token String token = createTokenFunction.get(); // 如果 maxTryTimes == -1,表示不做唯一性验证,直接返回 if (maxTryTimes == -1) { return token; } // 如果 token 在DB库查询不到数据,说明是个可用的全新 token,直接返回 if (checkTokenFunction.apply(token)) { return token; } // 如果已经循环了 maxTryTimes 次,仍然没有创建出可用的 token,那么抛出异常 if (i >= maxTryTimes) { throw new SaTokenException(elementName + " 生成失败,已尝试" + i + "次,生成算法过于简单或资源池已耗尽"); } } }; /** * 是否自动续期 active-timeout */ public SaAutoRenewFunction autoRenew = (stpLogic) -> { return stpLogic.getConfigOrGlobal().getAutoRenew(); }; /** * 创建 StpLogic 的算法 */ public SaCreateStpLogicFunction createStpLogic = (loginType) -> { return new StpLogic(loginType); }; /** * 路由匹配策略 */ public SaRouteMatchFunction routeMatcher = (pattern, path) -> { throw new NotImplException("未实现具体路由匹配策略").setCode(SaErrorCode.CODE_12401); }; /** * CORS 策略处理函数 */ public SaCorsHandleFunction corsHandle = (req, res, sto) -> { }; // ----------------------- 重写策略 set连缀风格 /** * 重写创建 Token 的策略 * * @param createToken / * @return / */ public SaStrategy setCreateToken(SaCreateTokenFunction createToken) { this.createToken = createToken; return this; } /** * 重写创建 Session 的策略 * * @param createSession / * @return / */ public SaStrategy setCreateSession(SaCreateSessionFunction createSession) { this.createSession = createSession; return this; } /** * 判断:集合中是否包含指定元素(模糊匹配) * * @param hasElement / * @return / */ public SaStrategy setHasElement(SaHasElementFunction hasElement) { this.hasElement = hasElement; return this; } /** * 生成唯一式 token 的算法 * * @param generateUniqueToken / * @return / */ public SaStrategy setGenerateUniqueToken(SaGenerateUniqueTokenFunction generateUniqueToken) { this.generateUniqueToken = generateUniqueToken; return this; } /** * 创建 StpLogic 的算法 * * @param createStpLogic / * @return / */ public SaStrategy setCreateStpLogic(SaCreateStpLogicFunction createStpLogic) { this.createStpLogic = createStpLogic; return this; } /** * 是否自动续期 * * @param autoRenew / * @return / */ public SaStrategy setAutoRenew(SaAutoRenewFunction autoRenew) { this.autoRenew = autoRenew; return this; } // /** * 请更换为 instance */ @Deprecated public static final SaStrategy me = instance; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHook.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; /** * 防火墙策略校验钩子函数 - 接口 * * @author click33 * @since 1.41.0 */ @FunctionalInterface public interface SaFirewallCheckHook { /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ void execute(SaRequest req, SaResponse res, Object extArg); } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForBlackPath.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.RequestPathInvalidException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:请求 path 黑名单校验 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForBlackPath implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForBlackPath instance = new SaFirewallCheckHookForBlackPath(); /** * 请求 path 黑名单 */ public List blackPaths = new ArrayList<>(); /** * 重载配置 * @param paths 黑名单 path 列表 */ public void resetConfig(String... paths) { this.blackPaths.clear(); this.blackPaths.addAll(Arrays.asList(paths)); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 扩展预留参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { String requestPath = req.getRequestPath(); for (String item : blackPaths) { if (requestPath.equals(item)) { throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForDirectoryTraversal.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.RequestPathInvalidException; /** * 防火墙策略校验钩子函数:请求 path 目录遍历符检测 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForDirectoryTraversal implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForDirectoryTraversal instance = new SaFirewallCheckHookForDirectoryTraversal(); /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { String requestPath = req.getRequestPath(); if(!isPathValid(requestPath)) { throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); } } /** * 检查路径是否有效 * @param path / * @return / */ public static boolean isPathValid(String path) { if (path == null || path.isEmpty()) { return false; } // 必须以 '/' 开头 if (path.charAt(0) != '/') { return false; } // 特殊处理根路径 "/" if (path.equals("/")) { return true; } String[] components = path.split("/"); for (int i = 0; i < components.length; i++) { String component = components[i]; // 处理空组件 if (component.isEmpty()) { if (i == 0) { // 允许路径以 "/" 开头(第一个组件为空) continue; } else { // 其他位置的空组件(如中间或末尾的 "//")非法 return false; } } // 检查是否包含 "." 或 ".." 组件 if (component.equals(".") || component.equals("..")) { return false; } } return true; } // 测试 // public static void main(String[] args) { // test("/user/info", true); // 合法 // test("/user/info/.", false); // 末尾包含 /. // test("/user/info/..", false); // 末尾包含 /.. // test("/user/info/./get", false); // 中间包含 /./ // test("/user/info/../get", false); // 中间包含 /../ // test("/user/info/.js", true); // 合法后缀 // test("/.abcdef", true); // 合法隐藏文件 // test("//user", false); // 多余斜杠 // test("/user//info", false); // 中间多余斜杠 // test("/", true); // 根目录合法 // test("user/../info", false); // 不以 / 开头 // test("a/b/c/..", false); // 不以 / 开头 // test("test/.", false); // 不以 / 开头 // test("", true); // 空路径非法 // } // // private static void test(String path, boolean expected) { // boolean result = isPathValid(path); // System.out.printf("Path: %-20s Expected: %-5s Actual: %-5s %s%n", // path, expected, result, (result == expected) ? "✓" : "✗"); // } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHeader.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.FirewallCheckException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:请求头检测 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForHeader implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForHeader instance = new SaFirewallCheckHookForHeader(); /** * 不允许的请求头列表 */ public List notAllowHeaderNames = new ArrayList<>(); public SaFirewallCheckHookForHeader() { } /** * 配置 * @param notAllowHeaderNames 不允许的请求头列表 (先清空原来的,再添加上新的) */ public void resetConfig(String... notAllowHeaderNames) { this.notAllowHeaderNames.clear(); this.notAllowHeaderNames.addAll(Arrays.asList(notAllowHeaderNames)); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { for (String headerName : notAllowHeaderNames) { if(req.getHeader(headerName) != null) { throw new FirewallCheckException("非法请求头:" + headerName); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHost.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.strategy.SaStrategy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:Host 检测 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForHost implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForHost instance = new SaFirewallCheckHookForHost(); /** * 是否校验 host,默认关闭 */ public boolean isCheckHost = false; /** * 允许的 host 列表,允许通配符 */ public List allowHosts = new ArrayList<>(); /** * 重载配置 * @param isCheckHost 是否校验 host * @param allowHosts 允许的 host 列表 (先清空原来的,再添加上新的) */ public void resetConfig(boolean isCheckHost, String... allowHosts) { this.isCheckHost = isCheckHost; this.allowHosts.clear(); this.allowHosts.addAll(Arrays.asList(allowHosts)); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { if(isCheckHost) { String host = req.getHost(); if( ! SaStrategy.instance.hasElement.apply(allowHosts, host) ) { throw new FirewallCheckException("非法请求 host:" + host); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHttpMethod.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.router.SaHttpMethod; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:请求 Method 检测 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForHttpMethod implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForHttpMethod instance = new SaFirewallCheckHookForHttpMethod(); /** * 是否校验 请求Method,默认开启 */ public boolean isCheckMethod = true; /** * 允许的 HttpMethod 列表 */ public List allowMethods = new ArrayList<>(); public SaFirewallCheckHookForHttpMethod() { // 默认允许的 HttpMethod 列表 allowMethods.add(SaHttpMethod.GET.name()); allowMethods.add(SaHttpMethod.POST.name()); allowMethods.add(SaHttpMethod.PUT.name()); allowMethods.add(SaHttpMethod.DELETE.name()); allowMethods.add(SaHttpMethod.HEAD.name()); allowMethods.add(SaHttpMethod.OPTIONS.name()); allowMethods.add(SaHttpMethod.PATCH.name()); allowMethods.add(SaHttpMethod.TRACE.name()); allowMethods.add(SaHttpMethod.CONNECT.name()); } /** * 配置 * @param isCheckMethod 是否校验 Method * @param methods 允许的 HttpMethod 列表 (先清空原来的,再添加上新的) */ public void resetConfig(boolean isCheckMethod, String... methods) { this.isCheckMethod = isCheckMethod; this.allowMethods.clear(); this.allowMethods.addAll(Arrays.asList(methods)); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { if(isCheckMethod) { String method = req.getMethod(); if( ! allowMethods.contains(method) ) { throw new FirewallCheckException("非法请求 Method:" + method); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForParameter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.FirewallCheckException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:请求参数检测 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForParameter implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForParameter instance = new SaFirewallCheckHookForParameter(); /** * 不允许的请求参数列表 */ public List notAllowParameterNames = new ArrayList<>(); public SaFirewallCheckHookForParameter() { } /** * 配置 * @param notAllowParameterNames 不允许的请求参数列表 (先清空原来的,再添加上新的) */ public void resetConfig(String... notAllowParameterNames) { this.notAllowParameterNames.clear(); this.notAllowParameterNames.addAll(Arrays.asList(notAllowParameterNames)); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { for (String parameterName : notAllowParameterNames) { if(req.getParam(parameterName) != null) { throw new FirewallCheckException("非法请求参数:" + parameterName); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathBannedCharacter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.util.SaFoxUtil; /** * 防火墙策略校验钩子函数:请求 path 禁止字符校验 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForPathBannedCharacter implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForPathBannedCharacter instance = new SaFirewallCheckHookForPathBannedCharacter(); /** * 是否严格禁止出现百分号字符 % (默认:否) */ public boolean bannedPercentage = false; /** * 重载配置 * @param bannedPercentage 是否严格禁止出现百分号字符 % (默认:否) */ public void resetConfig(boolean bannedPercentage) { this.bannedPercentage = bannedPercentage; } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { // 非可打印 ASCII 字符检查 String requestPath = req.getRequestPath(); if(SaFoxUtil.hasNonPrintableASCII(requestPath)) { throw new RequestPathInvalidException("请求 path 包含禁止字符:" + requestPath, requestPath); } if(bannedPercentage && requestPath.contains("%")) { throw new RequestPathInvalidException("请求 path 包含禁止字符 %:" + requestPath, requestPath); } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathDangerCharacter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.RequestPathInvalidException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:请求 path 危险字符校验 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForPathDangerCharacter implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForPathDangerCharacter instance = new SaFirewallCheckHookForPathDangerCharacter(); /** * 请求 path 不允许出现的危险字符 */ public List dangerCharacter = new ArrayList<>(Arrays.asList( "//", // // "\\", // \ "%2e", "%2E", // . "%2f", "%2F", // / "%5c", "%5C", // \ ";", "%3b", "%3B", // ; // 参考资料:https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA "%25", // 空格 "\0", "%00", // 空字符 "\n", "%0a", "%0A", // 换行符 "\r", "%0d", "%0D", // 回车符 "\u2028", // 行分隔符 "\u2029" // 段分隔符 )); /** * 重载配置 * @param character 危险字符列表 */ public void resetConfig(String... character) { this.dangerCharacter = Arrays.asList(character); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { String requestPath = req.getRequestPath(); for (String item : dangerCharacter) { if (requestPath.contains(item)) { throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForWhitePath.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.strategy.hooks; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.StopMatchException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 防火墙策略校验钩子函数:请求 path 白名单放行 * * @author click33 * @since 1.41.0 */ public class SaFirewallCheckHookForWhitePath implements SaFirewallCheckHook { /** * 默认实例 */ public static SaFirewallCheckHookForWhitePath instance = new SaFirewallCheckHookForWhitePath(); /** * 请求 path 白名单 */ public List whitePaths = new ArrayList<>(); /** * 重载配置 * @param paths 白名单 path 列表 */ public void resetConfig(String... paths) { this.whitePaths.clear(); this.whitePaths.addAll(Arrays.asList(paths)); } /** * 执行的方法 * * @param req 请求对象 * @param res 响应对象 * @param extArg 预留扩展参数 */ @Override public void execute(SaRequest req, SaResponse res, Object extArg) { String requestPath = req.getRequestPath(); for (String item : whitePaths) { if (requestPath.equals(item)) { throw new StopMatchException(); } } } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.temp; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.raw.SaRawSessionDelegator; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTtlMethods; import java.util.*; /** * Sa-Token 临时 token 验证模块 * *

* 有效期很短的一种token,一般用于一次性接口防盗用、短时间资源访问等业务场景 *

* * @author click33 * @since 1.42.0 */ public class SaTempTemplate implements SaTtlMethods { /** *默认命名空间 */ public static final String DEFAULT_NAMESPACE = "temp-token"; /** * 命名空间 */ public String namespace; /** * Raw Session 读写委托 */ public SaRawSessionDelegator rawSessionDelegator; /** * 在 raw-session 中的保存索引列表使用的 key */ public static final String TEMP_TOKEN_MAP = "__HD_TEMP_TOKEN_MAP"; public SaTempTemplate(){ this(DEFAULT_NAMESPACE); } /** * 实例化 * @param namespace 命名空间,用于多实例隔离 */ public SaTempTemplate(String namespace){ if(SaFoxUtil.isEmpty(namespace)) { throw new SaTokenException("namespace 不能为空"); } this.namespace = namespace; this.rawSessionDelegator = new SaRawSessionDelegator(namespace); } // -------- 创建 /** * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token,请自行在 value 拼接不同前缀) * * @param value 指定值 * @param timeout 有效时间,单位:秒,-1 代表永久有效 * @return 生成的 token */ public String createToken(Object value, long timeout) { return createToken(value, timeout, false); } /** * 为指定 业务标识、指定 value 创建一个 Token * @param value 指定值 * @param timeout 有效期,单位:秒,-1 代表永久有效 * @param isRecordIndex 是否记录索引,以便后续使用 value 反查 token * @return 生成的token */ public String createToken(Object value, long timeout, boolean isRecordIndex) { // 生成 temp-token String tempToken = createTempTokenValue(value); // 持久化映射关系 saveToken(tempToken, value, timeout); // 记录索引 if(isRecordIndex) { SaSession session = rawSessionDelegator.getSessionById(value); addTempTokenIndex(session, tempToken, timeout); adjustIndex(value, session); } // 返回 return tempToken; } /** * 保存 token * @param token / * @param value / * @param timeout / */ public void saveToken(String token, Object value, long timeout) { String key = splicingTempTokenSaveKey(token); SaManager.getSaTokenDao().setObject(key, value, timeout); } /** * 创建一个 temp-token 值 * * @return / */ public String createTempTokenValue(Object value) { return SaStrategy.instance.generateUniqueToken.execute( "Temp Token", SaManager.getConfig().getMaxTryTimes(), () -> randomTempToken(value), _apiKey -> _getValue(_apiKey) == null ); } /** * 随机一个 temp-token * * @return / */ public String randomTempToken(Object value) { return UUID.randomUUID().toString().replace("-", ""); } // -------- 解析 /** * 解析 Token 获取 value * @param token 指定 Token * @return / */ public Object parseToken(String token) { return _getValue(token); } /** * 解析 Token 获取 value,并转换为指定类型 * * @param token 指定 Token * @param cs 指定类型 * @param 默认值的类型 * @return / */ public T parseToken(String token, Class cs) { return parseToken(token, null, cs); } /** * 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 *

* 请注意此方法在旧版本(<= v1.41.0) 时的三个参数为:service, token, class
* 新版本三个参数为:token, cutPrefix, class
* 请注意其中的逻辑变化 *

* * @param token 指定 Token * @param cs 指定类型 * @param 默认值的类型 * @return / */ public T parseToken(String token, String cutPrefix, Class cs) { // 解析值 Object value = parseToken(token); // 如果未指定裁剪前缀,则直接返回 if(SaFoxUtil.isEmpty(cutPrefix)) { return SaFoxUtil.getValueByType(value, cs); } // 如果符合前缀则裁剪并返回,如果不符合前缀则返回 null checkCutPrefixLength(cutPrefix); String str = SaFoxUtil.valueToString(value); if(str.startsWith(cutPrefix)) { return SaFoxUtil.getValueByType(str.substring(cutPrefix.length()), cs); } else { return null; } } /** * 获取指定指定 Token 的剩余有效期,单位:秒 *

返回值 -1 代表永久,-2 代表 token 无效 * * @param token / * @return / */ public long getTimeout(String token) { return _getTimeout(token); } // -------- 删除 /** * 删除一个 token * @param token 指定 Token */ public void deleteToken(String token) { // 如果无此数据,则直接返回 Object value = parseToken(token); if(SaFoxUtil.isEmpty(value)) { return; } // 删除 token 本身 _deleteToken(token); // 调整索引 SaSession session = rawSessionDelegator.getSessionById(value, false); if(session != null) { deleteTempTokenIndex(session, token); adjustIndex(value, null); } } // ------------------- 索引操作 /** * 调整索引 * * @param value 值 * @param session 可填写 null,代表使用 value 现场查询 * @return 调整后的索引列表 */ public Map adjustIndex(Object value, SaSession session) { // 未提供则现场查询 if(session == null) { session = rawSessionDelegator.getSessionById(value, false); if(session == null) { return newTokenIndexMap(); } } // 重新整理索引列表 Map tempTokenNewList = newTokenIndexMap(); ArrayList tempTokenTtlList = new ArrayList<>(); Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap); for (Map.Entry entry : tempTokenMap.entrySet()) { long ttl = expireTimeToTtl(entry.getValue()); if(ttl != SaTokenDao.NOT_VALUE_EXPIRE) { tempTokenNewList.put(entry.getKey(), entry.getValue()); tempTokenTtlList.add(ttl); } } // 有则保存,无则删除 if( ! tempTokenNewList.isEmpty()) { session.set(TEMP_TOKEN_MAP, tempTokenNewList); } else { rawSessionDelegator.deleteSessionById(value); return tempTokenNewList; } // 调整 SaSession TTL long maxTtl = getMaxTtl(tempTokenTtlList); if(maxTtl != 0) { session.updateTimeout(maxTtl); } return tempTokenNewList; } /** * 获取指定 value 的 temp-token 列表记录 * @param value / * @return / */ public List getTempTokenList(Object value) { // 先调增索引再获取,否则有可能获取到的不是最新有效数据 Map tempTokenMap = adjustIndex(value, null); return new ArrayList<>(tempTokenMap.keySet()); } /** * 在 SaSession 上添加临时 temp-token 索引 * @param session / * @param token / * @param timeout / */ protected void addTempTokenIndex(SaSession session, String token, long timeout) { Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap); if(! tempTokenMap.containsKey(token)) { tempTokenMap.put(token, ttlToExpireTime(timeout)); session.set(TEMP_TOKEN_MAP, tempTokenMap); } } /** * 在 SaSession 上删除临时 temp-token 索引 * @param session / * @param token / */ protected void deleteTempTokenIndex(SaSession session, String token) { Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap); if(tempTokenMap.containsKey(token)) { tempTokenMap.remove(token); session.set(TEMP_TOKEN_MAP, tempTokenMap); } } // -------- 元操作 protected Object _getValue(String token) { String key = splicingTempTokenSaveKey(token); return SaManager.getSaTokenDao().getObject(key); } protected void _deleteToken(String token) { String key = splicingTempTokenSaveKey(token); SaManager.getSaTokenDao().deleteObject(key); } protected long _getTimeout(String token) { String key = splicingTempTokenSaveKey(token); return SaManager.getSaTokenDao().getObjectTimeout(key); } // -------- 其它 /** * 检查裁剪前缀长度 * @param cutPrefix / */ protected static void checkCutPrefixLength(String cutPrefix) { if(cutPrefix.length() >= 32) { throw new SaTokenException("裁剪前缀长度必须小于 32 位"); } } /** * 获取:在存储临时 token 数据时,应该使用的 key * @param token token值 * @return key */ public String splicingTempTokenSaveKey(String token) { return SaManager.getConfig().getTokenName() + ":" + namespace + ":" + token; } /** * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) */ public String getJwtSecretKey() { return null; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.temp; import cn.dev33.satoken.SaManager; import java.util.List; /** * Sa-Token 临时 token 验证模块 - 工具类 * *

* 有效期很短的一种token,一般用于一次性接口防盗用、短时间资源访问等业务场景 *

* * @author click33 * @since 1.20.0 */ public class SaTempUtil { private SaTempUtil() { } // -------- 创建 /** * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token,请自行在 value 拼接不同前缀) * * @param value 指定值 * @param timeout 有效时间,单位:秒,-1 代表永久有效 * @return 生成的 token */ public static String createToken(Object value, long timeout) { return SaManager.getSaTempTemplate().createToken(value, timeout); } /** * 为指定 业务标识、指定 value 创建一个 Token * @param value 指定值 * @param timeout 有效期,单位:秒,-1 代表永久有效 * @param isRecordIndex 是否记录索引,以便后续使用 value 反查 token * @return 生成的token */ public static String createToken(Object value, long timeout, boolean isRecordIndex) { return SaManager.getSaTempTemplate().createToken(value, timeout, isRecordIndex); } /** * 保存 token * @param token / * @param value / * @param timeout / */ public static void saveToken(String token, Object value, long timeout) { SaManager.getSaTempTemplate().saveToken(token, value, timeout); } // -------- 解析 /** * 解析 Token 获取 value * @param token 指定 Token * @return / */ public static Object parseToken(String token) { return SaManager.getSaTempTemplate().parseToken(token); } /** * 解析 Token 获取 value,并转换为指定类型 * * @param token 指定 Token * @param cs 指定类型 * @param 默认值的类型 * @return / */ public static T parseToken(String token, Class cs) { return SaManager.getSaTempTemplate().parseToken(token, cs); } /** * 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 *

* 请注意此方法在旧版本(<= v1.41.0) 时的三个参数为:service, token, class
* 新版本三个参数为:token, cutPrefix, class
* 请注意其中的逻辑变化 *

* * @param token 指定 Token * @param cs 指定类型 * @param 默认值的类型 * @return / */ public static T parseToken(String token, String cutPrefix, Class cs) { return SaManager.getSaTempTemplate().parseToken(token, cutPrefix, cs); } /** * 获取指定指定 Token 的剩余有效期,单位:秒 *

返回值 -1 代表永久,-2 代表 token 无效 * * @param token / * @return / */ public static long getTimeout(String token) { return SaManager.getSaTempTemplate().getTimeout(token); } // -------- 删除 /** * 删除一个 token * @param token 指定 Token */ public static void deleteToken(String token) { SaManager.getSaTempTemplate().deleteToken(token); } // ------------------- 索引操作 /** * 获取指定 value 的 temp-token 列表记录 * @param value / * @return / */ public static List getTempTokenList(Object value) { return SaManager.getSaTempTemplate().getTempTokenList(value); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import java.io.Console; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.net.URLDecoder; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ThreadLocalRandom; /** * Sa-Token 内部工具类 * * @author click33 * @since 1.18.0 */ public class SaFoxUtil { private SaFoxUtil() { } /** * 打印 Sa-Token 版本字符画 */ public static void printSaToken() { String str = "" + "____ ____ ___ ____ _ _ ____ _ _ \r\n" + "[__ |__| __ | | | |_/ |___ |\\ | \r\n" + "___] | | | |__| | \\_ |___ | \\| " // + SaTokenConsts.VERSION_NO // + "sa-token:" // + "\r\n" + "DevDoc:" + SaTokenConsts.DEV_DOC_URL // + "\r\n"; + "\r\n" + SaTokenConsts.DEV_DOC_URL // + "\r\n"; + " (" + SaTokenConsts.VERSION_NO + ")" // + "\r\n" + "GitHub:" + SaTokenConsts.GITHUB_URL // + "\r\n"; ; System.out.println(str); } /** * 生成指定长度的随机字符串 * * @param length 字符串的长度 * @return 一个随机字符串 */ public static String getRandomString(int length) { String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { int number = ThreadLocalRandom.current().nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } /** * 生成指定区间的 int 值 * * @param min 最小值(包括) * @param max 最大值(包括) * @return / */ public static int getRandomNumber(int min, int max) { return ThreadLocalRandom.current().nextInt(min, max + 1); } /** * 指定元素是否为null或者空字符串 * @param str 指定元素 * @return 是否为null或者空字符串 */ public static boolean isEmpty(Object str) { return str == null || "".equals(str); } /** * 指定元素是否不为 (null或者空字符串) * @param str 指定元素 * @return 是否为null或者空字符串 */ public static boolean isNotEmpty(Object str) { return ! isEmpty(str); } /** * 指定数组是否为null或者空数组 *

该方法已过时,建议使用 isEmptyArray 方法

* @param / * @param array / * @return / */ @Deprecated public static boolean isEmpty(T[] array) { return isEmptyArray(array); } /** * 指定数组是否为null或者空数组 * @param / * @param array / * @return / */ public static boolean isEmptyArray(T[] array) { return array == null || array.length == 0; } /** * 指定集合是否为null或者空数组 * @param list / * @return / */ public static boolean isEmptyList(List list) { return list == null || list.isEmpty(); } /** * 比较两个对象是否相等 * @param a 第一个对象 * @param b 第二个对象 * @return 两个对象是否相等 */ public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); } /** * 比较两个对象是否不相等 * @param a 第一个对象 * @param b 第二个对象 * @return 两个对象是否不相等 */ public static boolean notEquals(Object a, Object b) { return !equals(a, b); } /** * 以当前时间戳和随机int数字拼接一个随机字符串 * * @return 随机字符串 */ public static String getMarking28() { return System.currentTimeMillis() + "" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE); } /** * 将日期格式化 (yyyy-MM-dd HH:mm:ss) * @param date 日期 * @return 格式化后的时间 */ public static String formatDate(Date date){ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); } /** * 将日期格式化 (yyyy-MM-dd HH:mm:ss) * @param zonedDateTime 日期 * @return 格式化后的时间 */ public static String formatDate(ZonedDateTime zonedDateTime) { return zonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } /** * 指定毫秒后的时间(格式化 :yyyy-MM-dd HH:mm:ss) * @param ms 指定毫秒后 * @return 格式化后的时间 */ public static String formatAfterDate(long ms) { Instant instant = Instant.ofEpochMilli(System.currentTimeMillis() + ms); ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); return formatDate(zonedDateTime); } /** * 从集合里查询数据 * * @param dataList 数据集合 * @param prefix 前缀 * @param keyword 关键字 * @param start 起始位置 (-1代表查询所有) * @param size 获取条数 * @param sortType 排序类型(true=正序,false=反序) * * @return 符合条件的新数据集合 */ public static List searchList(Collection dataList, String prefix, String keyword, int start, int size, boolean sortType) { if (prefix == null) { prefix = ""; } if (keyword == null) { keyword = ""; } // 挑选出所有符合条件的 List list = new ArrayList<>(); for (String key : dataList) { if (key.startsWith(prefix) && key.contains(keyword)) { list.add(key); } } // 取指定段数据 return searchList(list, start, size, sortType); } /** * 从集合里查询数据 * * @param list 数据集合 * @param start 起始位置 * @param size 获取条数 (-1代表从start处一直取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return 符合条件的新数据集合 */ public static List searchList(List list, int start, int size, boolean sortType) { // 如果是反序的话 if( ! sortType) { Collections.reverse(list); } // start 至少为0 if (start < 0) { start = 0; } // size为-1时,代表一直取到末尾,否则取到 start + size int end; if(size == -1) { end = list.size(); } else { end = start + size; } // 取出的数据放到新集合中 List list2 = new ArrayList<>(); for (int i = start; i < end; i++) { // 如果已经取到list的末尾,则直接退出 if (i >= list.size()) { return list2; } list2.add(list.get(i)); } return list2; } /** * 字符串模糊匹配 *

example: *

user* user-add -- true *

user* art-add -- false * @param patt 表达式 * @param str 待匹配的字符串 * @return 是否可以匹配 */ public static boolean vagueMatch(String patt, String str) { // 两者均为 null 时,直接返回 true if(patt == null && str == null) { return true; } // 两者其一为 null 时,直接返回 false if(patt == null || str == null) { return false; } // 如果表达式不带有*号,则只需简单equals即可 (这样可以使速度提升200倍左右) if( ! patt.contains("*")) { return patt.equals(str); } // 深入匹配 return vagueMatchMethod(patt, str); } /** * 字符串模糊匹配 * * @param pattern / * @param str / * @return / */ private static boolean vagueMatchMethod( String pattern, String str) { int m = str.length(); int n = pattern.length(); boolean[][] dp = new boolean[m + 1][n + 1]; dp[0][0] = true; for (int i = 1; i <= n; ++i) { if (pattern.charAt(i - 1) == '*') { dp[0][i] = true; } else { break; } } for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (pattern.charAt(j - 1) == '*') { dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; } else if (str.charAt(i - 1) == pattern.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } } } return dp[m][n]; } /** * 判断类型是否为8大包装类型 * @param cs / * @return / */ public static boolean isWrapperType(Class cs) { return cs == Integer.class || cs == Short.class || cs == Long.class || cs == Byte.class || cs == Float.class || cs == Double.class || cs == Boolean.class || cs == Character.class; } /** * 判断类型是否为基础类型:8大基本数据类型、8大包装类、String * @param cs / * @return / */ public static boolean isBasicType(Class cs) { return cs.isPrimitive() || isWrapperType(cs) || cs == String.class; } /** * 将指定值转化为指定类型 * @param 泛型 * @param obj 值 * @param cs 类型 * @return 转换后的值 */ @SuppressWarnings("unchecked") public static T getValueByType(Object obj, Class cs) { // 如果 obj 为 null 或者本来就是 cs 类型 if(obj == null || obj.getClass().equals(cs)) { return (T)obj; } // 开始转换 String obj2 = String.valueOf(obj); Object obj3; if (cs.equals(String.class)) { obj3 = obj2; } else if (cs.equals(int.class) || cs.equals(Integer.class)) { obj3 = Integer.valueOf(obj2); } else if (cs.equals(long.class) || cs.equals(Long.class)) { obj3 = Long.valueOf(obj2); } else if (cs.equals(short.class) || cs.equals(Short.class)) { obj3 = Short.valueOf(obj2); } else if (cs.equals(byte.class) || cs.equals(Byte.class)) { obj3 = Byte.valueOf(obj2); } else if (cs.equals(float.class) || cs.equals(Float.class)) { obj3 = Float.valueOf(obj2); } else if (cs.equals(double.class) || cs.equals(Double.class)) { obj3 = Double.valueOf(obj2); } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { obj3 = Boolean.valueOf(obj2); } else if (cs.equals(char.class) || cs.equals(Character.class)) { obj3 = obj2.charAt(0); } else { obj3 = obj; } return (T)obj3; } /** * 将 Map 转化为 Object * @param map / * @param clazz / * @return / * @param / */ public static T mapToObject(Map map, Class clazz) { if(map == null) { return null; } if(clazz == Map.class) { return (T) map; } try { T obj = clazz.getDeclaredConstructor().newInstance(); for (Field field : clazz.getDeclaredFields()) { String fieldName = field.getName(); if (map.containsKey(fieldName)) { field.setAccessible(true); field.set(obj, map.get(fieldName)); } } return obj; } catch (Exception e) { throw new RuntimeException("转换失败: " + e.getMessage(), e); } } /** * 在url上拼接上kv参数并返回 * @param url url * @param paramStr 参数, 例如 id=1001 * @return 拼接后的url字符串 */ public static String joinParam(String url, String paramStr) { // 如果参数为空, 直接返回 if(paramStr == null || paramStr.length() == 0) { return url; } if(url == null) { url = ""; } int index = url.lastIndexOf('?'); // ? 不存在 if(index == -1) { return url + '?' + paramStr; } // ? 是最后一位 if(index == url.length() - 1) { return url + paramStr; } // ? 是其中一位 if(index < url.length() - 1) { String separatorChar = "&"; // 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 & if(url.lastIndexOf(separatorChar) != url.length() - 1 && paramStr.indexOf(separatorChar) != 0) { return url + separatorChar + paramStr; } else { return url + paramStr; } } // 正常情况下, 代码不可能执行到此 return url; } /** * 在url上拼接上kv参数并返回 * @param url url * @param key 参数名称 * @param value 参数值 * @return 拼接后的url字符串 */ public static String joinParam(String url, String key, Object value) { // 如果url或者key为空, 直接返回 if(isEmpty(url) || isEmpty(key)) { return url; } return joinParam(url, key + "=" + value); } /** * 在url上拼接锚参数 * @param url url * @param paramStr 参数, 例如 id=1001 * @return 拼接后的url字符串 */ public static String joinSharpParam(String url, String paramStr) { // 如果参数为空, 直接返回 if(paramStr == null || paramStr.length() == 0) { return url; } if(url == null) { url = ""; } int index = url.lastIndexOf('#'); // # 不存在 if(index == -1) { return url + '#' + paramStr; } // # 是最后一位 if(index == url.length() - 1) { return url + paramStr; } // # 是其中一位 if(index < url.length() - 1) { String separatorChar = "&"; // 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 & if(url.lastIndexOf(separatorChar) != url.length() - 1 && paramStr.indexOf(separatorChar) != 0) { return url + separatorChar + paramStr; } else { return url + paramStr; } } // 正常情况下, 代码不可能执行到此 return url; } /** * 在url上拼接锚参数 * @param url url * @param key 参数名称 * @param value 参数值 * @return 拼接后的url字符串 */ public static String joinSharpParam(String url, String key, Object value) { // 如果url或者key为空, 直接返回 if(isEmpty(url) || isEmpty(key)) { return url; } return joinSharpParam(url, key + "=" + value); } /** * 拼接两个url *

例如:url1=http://domain.cn,url2=/sso/auth,则返回:http://domain.cn/sso/auth

* * @param url1 第一个url * @param url2 第二个url * @return 拼接完成的url */ public static String spliceTwoUrl(String url1, String url2) { // q1、任意一个为空,则直接返回另一个 if(url1 == null) { return url2; } if(url2 == null) { return url1; } // q2、如果 url2 以 http 开头,将其视为一个完整地址 if(url2.startsWith("http")) { return url2; } // q3、将两个地址拼接在一起 return url1 + url2; } /** * 将数组的所有元素使用逗号拼接在一起 * @param arr 数组 * @return 字符串,例: a,b,c */ public static String arrayJoin(String[] arr) { if(arr == null) { return ""; } StringBuilder str = new StringBuilder(); for (int i = 0; i < arr.length; i++) { str.append(arr[i]); if(i != arr.length - 1) { str.append(","); } } return str.toString(); } /** * 验证URL的正则表达式 */ public static String URL_REGEX = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"; /** * 使用正则表达式判断一个字符串是否为URL * @param str 字符串 * @return 拼接后的url字符串 */ public static boolean isUrl(String str) { if(isEmpty(str)) { return false; } return str.toLowerCase().matches(URL_REGEX); } /** * URL编码 * @param url see note * @return see note */ public static String encodeUrl(String url) { try { return URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12103); } } /** * URL解码 * @param url see note * @return see note */ public static String decoderUrl(String url) { try { return URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new SaTokenException(e).setCode(SaErrorCode.CODE_12104); } } /** * 将指定字符串按照逗号分隔符转化为字符串集合 * @param str 字符串 * @return 分割后的字符串集合 */ public static List convertStringToList(String str) { List list = new ArrayList<>(); if(isEmpty(str)) { return list; } String[] arr = str.split(","); for (String s : arr) { s = s.trim(); if(!isEmpty(s)) { list.add(s); } } return list; } /** * 将指定集合按照逗号连接成一个字符串 * @param list 集合 * @return 字符串 */ public static String convertListToString(List list) { if(list == null || list.isEmpty()) { return ""; } StringBuilder str = new StringBuilder(); for (int i = 0; i < list.size(); i++) { str.append(list.get(i)); if(i != list.size() - 1) { str.append(","); } } return str.toString(); } /** * String 转 Array,按照逗号切割 * @param str 字符串 * @return 数组 */ public static String[] convertStringToArray(String str) { List list = convertStringToList(str); return list.toArray(new String[0]); } /** * Array 转 String,按照逗号连接 * @param arr 数组 * @return 字符串 */ public static String convertArrayToString(String[] arr) { if(arr == null || arr.length == 0) { return ""; } return String.join(",", arr); } /** * 返回一个空集合 * @param 集合类型 * @return 空集合 */ public static List emptyList() { return new ArrayList<>(); } /** * String数组转集合 * @param str String数组 * @return 集合 */ public static List toList(String... str) { return new ArrayList<>(Arrays.asList(str)); } /** * String 集合转数组 * @param list 集合 * @return 数组 */ public static String[] toArray(List list) { return list.toArray(new String[0]); } public static List logLevelList = Arrays.asList("", "trace", "debug", "info", "warn", "error", "fatal"); /** * 将日志等级从 String 格式转化为 int 格式 * @param level / * @return / */ public static int translateLogLevelToInt(String level) { int levelInt = logLevelList.indexOf(level); if(levelInt <= 0 || levelInt >= logLevelList.size()) { levelInt = 1; } return levelInt; } /** * 将日志等级从 String 格式转化为 int 格式 * @param level / * @return / */ public static String translateLogLevelToString(int level) { if(level <= 0 || level >= logLevelList.size()) { level = 1; } return logLevelList.get(level); } /** * 判断当前系统是否可以打印彩色日志,判断准确率并非100%,但基本可以满足大部分场景 * @return / */ @SuppressWarnings("all") public static boolean isCanColorLog() { // 获取当前环境相关信息 Console console = System.console(); String term = System.getenv().get("TERM"); // 两者均为 null,一般是在 eclipse、idea 等 IDE 环境下运行的,此时可以打印彩色日志 if(console == null && term == null) { return true; } // 两者均不为 null,一般是在 linux 环境下控制台运行的,此时可以打印彩色日志 if(console != null && term != null) { return true; } // console 有值,term 为 null,一般是在 windows 的 cmd 控制台运行的,此时不可以打印彩色日志 if(console != null && term == null) { return false; } // console 为 null,term 有值,一般是在 linux 的 nohup 命令运行的,此时不可以打印彩色日志 // 此时也有可能是在 windows 的 git bash 环境下运行的,此时可以打印彩色日志,但此场景无法和上述场景区分,所以统一不打印彩色日志 if(console == null && term != null) { return false; } // 正常情况下,代码不会走到这里,但是方法又必须要有返回值,所以随便返回一个 return false; } /** * list1 是否完全包含 list2 中所有元素 * @param list1 集合1 * @param list2 集合2 * @return / */ public static boolean list1ContainList2AllElement(List list1, List list2){ if(list2 == null || list2.isEmpty()) { return true; } if(list1 == null || list1.isEmpty()) { return false; } for (String str : list2) { if(!list1.contains(str)) { return false; } } return true; } /** * list1 是否包含 list2 中任意一个元素 * @param list1 集合1 * @param list2 集合2 * @return / */ public static boolean list1ContainList2AnyElement(List list1, List list2){ if(list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) { return false; } for (String str : list2) { if(list1.contains(str)) { return true; } } return false; } /** * 从 list1 中剔除 list2 所包含的元素 (克隆副本操作,不影响 list1) * @param list1 集合1 * @param list2 集合2 * @return / */ public static List list1RemoveByList2(List list1, List list2){ if(list1 == null) { return null; } if(list1.isEmpty() || list2 == null || list2.isEmpty()) { return new ArrayList<>(list1); } List listX = new ArrayList<>(list1); for (String str : list2) { listX.remove(str); } return listX; } /** * 检查字符串是否包含非可打印 ASCII 字符 * @param str / * @return / */ public static boolean hasNonPrintableASCII(String str) { if (str == null) { return false; } for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); // ASCII 范围检查:0-31 或 127 if ((c <= 31) || (c == 127)) { return true; } } return false; } /** * 将 value 转化为 String,如果 value 为 null,则返回空字符串 * @param value / * @return / */ public static String valueToString(Object value) { if (value == null) { return ""; } return value.toString(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaHexUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; /** * 十六进制工具类 * * @author deepseek * @since 2025/2/24 */ public class SaHexUtil { // 十六进制字符表(大写) private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); /** * 将字节数组转换为十六进制字符串(JDK8兼容) * @param bytes 要转换的字节数组 * @return 十六进制字符串(大写) */ public static String bytesToHex(byte[] bytes) { if (bytes == null) return null; char[] hexChars = new char[bytes.length * 2]; for (int i = 0; i < bytes.length; i++) { int v = bytes[i] & 0xFF; hexChars[i * 2] = HEX_ARRAY[v >>> 4]; hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F]; } return new String(hexChars); } /** * 将十六进制字符串转换为字节数组(JDK8兼容) * @param hexString 有效的十六进制字符串(不区分大小写) * @return 对应的字节数组 * @throws IllegalArgumentException 输入字符串格式错误时抛出异常 */ public static byte[] hexToBytes(String hexString) { if (hexString == null) return null; int len = hexString.length(); if (len % 2 != 0) { throw new IllegalArgumentException("Hex string must have even length"); } byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { int high = Character.digit(hexString.charAt(i), 16); int low = Character.digit(hexString.charAt(i+1), 16); if (high == -1 || low == -1) { throw new IllegalArgumentException( "Invalid hex character at position " + i + " or " + (i+1) ); } data[i/2] = (byte) ((high << 4) + low); } return data; } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaResult.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; import cn.dev33.satoken.SaManager; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; /** * 对请求接口返回 Json 格式数据的简易封装。 * *

* 所有预留字段:
* code = 状态码
* msg = 描述信息
* data = 携带对象
*

* * @author click33 * @since 1.22.0 */ public class SaResult extends LinkedHashMap implements Serializable{ // 序列化版本号 private static final long serialVersionUID = 1L; // 预定的状态码 public static final int CODE_SUCCESS = 200; public static final int CODE_ERROR = 500; public static final int CODE_NOT_PERMISSION = 403; public static final int CODE_NOT_LOGIN = 401; /** * 构建 */ public SaResult() { } /** * 构建 * @param code 状态码 * @param msg 信息 * @param data 数据 */ public SaResult(int code, String msg, Object data) { this.setCode(code); this.setMsg(msg); this.setData(data); } /** * 根据 Map 快速构建 * @param map / */ public SaResult(Map map) { this.setMap(map); } /** * 获取code * @return code */ public Integer getCode() { return (Integer)this.get("code"); } /** * 获取msg * @return msg */ public String getMsg() { return (String)this.get("msg"); } /** * 获取data * @return data */ public Object getData() { return this.get("data"); } /** * 给code赋值,连缀风格 * @param code code * @return 对象自身 */ public SaResult setCode(int code) { this.put("code", code); return this; } /** * 给msg赋值,连缀风格 * @param msg msg * @return 对象自身 */ public SaResult setMsg(String msg) { this.put("msg", msg); return this; } /** * 给data赋值,连缀风格 * @param data data * @return 对象自身 */ public SaResult setData(Object data) { this.put("data", data); return this; } /** * 写入一个值 自定义key, 连缀风格 * @param key key * @param data data * @return 对象自身 */ public SaResult set(String key, Object data) { this.put(key, data); return this; } /** * 获取一个值 根据自定义key * @param 要转换为的类型 * @param key key * @param cs 要转换为的类型 * @return 值 */ public T get(String key, Class cs) { return SaFoxUtil.getValueByType(get(key), cs); } /** * 写入一个Map, 连缀风格 * @param map map * @return 对象自身 */ public SaResult setMap(Map map) { if(map != null) { for (String key : map.keySet()) { this.put(key, map.get(key)); } } return this; } /** * 写入一个 json 字符串, 连缀风格 * @param jsonString json 字符串 * @return 对象自身 */ public SaResult setJsonString(String jsonString) { Map map = SaManager.getSaJsonTemplate().jsonToMap(jsonString); return setMap(map); } /** * 移除默认属性(code、msg、data), 连缀风格 * @return 对象自身 */ public SaResult removeDefaultFields() { this.remove("code"); this.remove("msg"); this.remove("data"); return this; } /** * 移除非默认属性(code、msg、data), 连缀风格 * @return 对象自身 */ public SaResult removeNonDefaultFields() { for (String key : this.keySet()) { if("code".equals(key) || "msg".equals(key) || "data".equals(key)) { continue; } this.remove(key); } return this; } // ============================ 静态方法快速构建 ================================== // 构建成功 public static SaResult ok() { return new SaResult(CODE_SUCCESS, "ok", null); } public static SaResult ok(String msg) { return new SaResult(CODE_SUCCESS, msg, null); } public static SaResult code(int code) { return new SaResult(code, null, null); } public static SaResult data(Object data) { return new SaResult(CODE_SUCCESS, "ok", data); } // 构建失败 public static SaResult error() { return new SaResult(CODE_ERROR, "error", null); } public static SaResult error(String msg) { return new SaResult(CODE_ERROR, msg, null); } // 构建未登录 public static SaResult notLogin() { return new SaResult(CODE_NOT_LOGIN, "not login", null); } // 构建无权限 public static SaResult notPermission() { return new SaResult(CODE_NOT_PERMISSION, "not permission", null); } // 构建指定状态码 public static SaResult get(int code, String msg, Object data) { return new SaResult(code, msg, data); } // 构建一个空的 public static SaResult empty() { return new SaResult(); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "{" + "\"code\": " + this.getCode() + ", \"msg\": " + transValue(this.getMsg()) + ", \"data\": " + transValue(this.getData()) + "}"; } /** * 转换 value 值: * 如果 value 值属于 String 类型,则在前后补上引号 * 如果 value 值属于其它类型,则原样返回 * * @param value 具体要操作的值 * @return 转换后的值 */ private String transValue(Object value) { if(value == null) { return null; } if(value instanceof String) { return "\"" + value + "\""; } return String.valueOf(value); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaSugar.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; import cn.dev33.satoken.fun.SaFunction; import java.util.function.Supplier; /** * 代码语法糖封装 * * @author click33 * @since 1.43.0 */ public class SaSugar { /** * 执行一个 Lambda 表达式,返回这个 Lambda 表达式的结果值, *
方便组织代码,例如: *
 
	 	int value = Sugar.get(() -> {
			int a = 1;
			int b = 2;
			return a + b;
		});
		
* @param 返回值类型 * @param lambda lambda 表达式 * @return lambda 的执行结果 */ public static R get(Supplier lambda) { return lambda.get(); } /** * 执行一个 Lambda 表达式 *
方便组织代码,例如: *
 
	 	Sugar.exe(() -> {
			int a = 1;
			int b = 2;
			return a + b;
		});
		
* @param lambda lambda 表达式 */ public static void exe(SaFunction lambda) { lambda.run(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; /** * Sa-Token 常量类 * *

* 一般的常量采用就近原则,定义在各自相应的模块中。 * 但有一些常量没有明确的归属模块,会在很多模块中使用到,比如版本号、开源地址等,属于全局性的础性常量,这些常量统一定义在此类中。 *

* * @author click33 * @since 1.8.0 */ public class SaTokenConsts { private SaTokenConsts() { } // ------------------ Sa-Token 版本信息 /** * Sa-Token 当前版本号 */ public static final String VERSION_NO = "v1.45.0"; /** * Sa-Token 开源地址 Gitee */ public static final String GITEE_URL = "https://gitee.com/dromara/sa-token"; /** * Sa-Token 开源地址 GitHub */ public static final String GITHUB_URL = "https://github.com/dromara/sa-token"; /** * Sa-Token 开发文档地址 */ public static final String DEV_DOC_URL = "https://sa-token.cc"; // ------------------ 常量 key 标记 /** * 常量 key 标记: 如果 token 为本次请求新创建的,则以此字符串为 key 存储在当前请求 str 中 */ public static final String JUST_CREATED = "JUST_CREATED_"; /** * 常量 key 标记: 如果 token 为本次请求新创建的,则以此字符串为 key 存储在当前 request 中(不拼接前缀,纯Token) */ public static final String JUST_CREATED_NOT_PREFIX = "JUST_CREATED_NOT_PREFIX_"; /** * 常量 key 标记: 如果本次请求已经验证过 activeTimeout, 则以此 key 在 storage 中做一个标记 */ public static final String TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY = "TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY_"; /** * 常量 key 标记: 在登录时,默认使用的设备类型 */ public static final String DEFAULT_LOGIN_DEVICE_TYPE = "DEF"; /** * 常量 key 标记: 在封禁账号时,默认封禁的服务类型 */ public static final String DEFAULT_DISABLE_SERVICE = "login"; /** * 常量 key 标记: 在封禁账号时,默认封禁的等级 */ public static final int DEFAULT_DISABLE_LEVEL = 1; /** * 常量 key 标记: 在封禁账号时,可使用的最小封禁级别 */ public static final int MIN_DISABLE_LEVEL = 1; /** * 常量 key 标记: 账号封禁级别,表示未被封禁 */ public static final int NOT_DISABLE_LEVEL = -2; /** * 常量 key 标记: 在进行临时身份切换时使用的 key */ public static final String SWITCH_TO_SAVE_KEY = "SWITCH_TO_SAVE_KEY_"; /** * 常量 key 标记: 在进行 Token 二级验证时,使用的 key */ @Deprecated public static final String SAFE_AUTH_SAVE_KEY = "SAFE_AUTH_SAVE_KEY_"; /** * 常量 key 标记: 在进行 Token 二级认证时,写入的 value 值 */ public static final String SAFE_AUTH_SAVE_VALUE = "SAFE_AUTH_SAVE_VALUE"; /** * 常量 key 标记: 在进行 Token 二级验证时,默认的业务类型 */ public static final String DEFAULT_SAFE_AUTH_SERVICE = "important"; /** * 常量 key 标记: 临时 Token 认证模块,默认的业务类型 */ @Deprecated public static final String DEFAULT_TEMP_TOKEN_SERVICE = "record"; // ------------------ token-style 相关 /** * Token风格: uuid */ public static final String TOKEN_STYLE_UUID = "uuid"; /** * Token风格: 简单uuid (不带下划线) */ public static final String TOKEN_STYLE_SIMPLE_UUID = "simple-uuid"; /** * Token风格: 32位随机字符串 */ public static final String TOKEN_STYLE_RANDOM_32 = "random-32"; /** * Token风格: 64位随机字符串 */ public static final String TOKEN_STYLE_RANDOM_64 = "random-64"; /** * Token风格: 128位随机字符串 */ public static final String TOKEN_STYLE_RANDOM_128 = "random-128"; /** * Token风格: tik风格 (2_14_16) */ public static final String TOKEN_STYLE_TIK = "tik"; // ------------------ SaSession 的类型 /** * SaSession 的类型: Account-Session */ public static final String SESSION_TYPE__ACCOUNT = "Account-Session"; /** * SaSession 的类型: Token-Session */ public static final String SESSION_TYPE__TOKEN = "Token-Session"; /** * SaSession 的类型: Anon-Token-Session */ public static final String SESSION_TYPE__ANON = "Anon-Token-Session"; /** * SaSession 的类型: Custom-Session */ public static final String SESSION_TYPE__CUSTOM = "Custom-Session"; // ------------------ 其它 /** * 连接 Token 前缀和 Token 值的字符 */ public static final String TOKEN_CONNECTOR_CHAT = " "; /** * 切面、拦截器、过滤器等各种组件的注册优先级顺序 */ public static final int ASSEMBLY_ORDER = -100; /** * 防火墙校验过滤器的注册顺序 */ public static final int FIREWALL_CHECK_FILTER_ORDER = -102; /** * 跨域处理过滤器的注册顺序 */ public static final int CORS_FILTER_ORDER = -103; /** * 上下文过滤器的注册顺序 */ public static final int SA_TOKEN_CONTEXT_FILTER_ORDER = -104; /** * RPC 框架权限过滤器的注册顺序 */ public static final int RPC_PERMISSION_FILTER_ORDER = -30000; /** * RPC 框架上下文过滤器的注册顺序 */ public static final int RPC_CONTEXT_FILTER_ORDER = -30005; /** * Content-Type key */ public static final String CONTENT_TYPE_KEY = "Content-Type"; /** * Content-Type text/plain; charset=utf-8 */ public static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain; charset=utf-8"; /** * Content-Type application/json;charset=UTF-8 */ public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json;charset=UTF-8"; // =================== 废弃 =================== /** * 请更换为 JUST_CREATED */ @Deprecated public static final String JUST_CREATED_SAVE_KEY = JUST_CREATED; /** * 请更换为 TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY */ @Deprecated public static final String TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY = TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY; } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaTtlMethods.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; import cn.dev33.satoken.dao.SaTokenDao; import java.util.*; /** * TTL 操作工具方法 * * @author click33 * @since 1.43.0 */ public interface SaTtlMethods { /** * 获取一个新的 Token 集合 * @return / */ default List newTokenValueList() { return new ArrayList<>(); } /** * 获取一个新的 TokenIndexMap 集合 * @return / */ default Map newTokenIndexMap() { return new LinkedHashMap<>(); } /** * 获取最大 ttl 值 * @param ttlList / * @return / */ default long getMaxTtl(ArrayList ttlList) { long maxTtl = 0; for (long ttl : ttlList) { if(ttl == SaTokenDao.NEVER_EXPIRE) { maxTtl = SaTokenDao.NEVER_EXPIRE; break; } if(ttl > maxTtl) { maxTtl = ttl; } } return maxTtl; } /** * 获取最大 ttl 值:过期时间 (13位时间戳) 转 ttl (秒) * @param expireTimeList / * @return / */ default long getMaxTtlByExpireTime(Collection expireTimeList) { long maxTtl = 0; for (long expireTime : expireTimeList) { long ttl = expireTimeToTtl(expireTime); if(ttl == SaTokenDao.NEVER_EXPIRE) { maxTtl = SaTokenDao.NEVER_EXPIRE; break; } if(ttl > maxTtl) { maxTtl = ttl; } } return maxTtl; } /** * 过期时间 (13位时间戳) 转 (13位时间戳) ttl (秒) * @param expireTime / * @return / */ default long expireTimeToTtl(long expireTime) { if(expireTime == SaTokenDao.NEVER_EXPIRE) { return SaTokenDao.NEVER_EXPIRE; } if(expireTime == SaTokenDao.NOT_VALUE_EXPIRE) { return SaTokenDao.NOT_VALUE_EXPIRE; } long currentTime = System.currentTimeMillis(); if(expireTime < currentTime) { return SaTokenDao.NOT_VALUE_EXPIRE; } return (expireTime - currentTime) / 1000; } /** * ttl (秒) 转 过期时间 (13位时间戳) * @param ttl / * @return / */ default long ttlToExpireTime(long ttl) { if(ttl == SaTokenDao.NEVER_EXPIRE) { return SaTokenDao.NEVER_EXPIRE; } if(ttl < 0) { return SaTokenDao.NOT_VALUE_EXPIRE; } return ttl * 1000 + System.currentTimeMillis(); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/SaValue2Box.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; /** * 封装两个值的容器,方便取值、写值等操作,value1 和 value2 用逗号隔开,形如:123,abc * * @author click33 * @since 1.35.0 */ public class SaValue2Box { /** * 第一个值 */ private Object value1; /** * 第二个值 */ private Object value2; /** * 直接提供两个值构建 * @param value1 第一个值 * @param value2 第二个值 */ public SaValue2Box(Object value1, Object value2) { this.value1 = value1; this.value2 = value2; } /** * 根据字符串构建,字符串形如:123,abc * @param valueString 形如:123,abc */ public SaValue2Box(String valueString) { if(valueString == null){ return; } String[] split = valueString.split(","); if(split.length == 0){ // do nothing } else if(split.length == 1){ this.value1 = split[0]; } else { this.value1 = split[0]; this.value2 = split[1]; } } /** * 获取第一个值 * @return 第一个值 */ public Object getValue1() { return value1; } /** * 获取第二个值 * @return 第二个值 */ public Object getValue2() { return value2; } /** * 设置第一个值 * @param value1 第一个值 */ public void setValue1(Object value1) { this.value1 = value1; } /** * 设置第二个值 * @param value2 第二个值 */ public void setValue2(Object value2) { this.value2 = value2; } /** * 判断第一个值是否为 null 或者空字符串 * @return / */ public boolean value1IsEmpty() { return SaFoxUtil.isEmpty(value1); } /** * 判断第二个值是否为 null 或者空字符串 * @return / */ public boolean value2IsEmpty() { return SaFoxUtil.isEmpty(value2); } /** * 获取第一个值,并转化为 String 类型 * @return / */ public String getValue1AsString() { return value1 == null ? null : value1.toString(); } /** * 获取第二个值,并转化为 String 类型 * @return / */ public String getValue2AsString() { return value2 == null ? null : value2.toString(); } /** * 获取第一个值,并转化为 long 类型 * @return / */ public long getValue1AsLong() { return Long.parseLong(value1.toString()); } /** * 获取第二个值,并转化为 long 类型 * @return / */ public long getValue2AsLong() { return Long.parseLong(value2.toString()); } /** * 获取第一个值,并转化为 long 类型,值不存在则返回默认值 * @return / */ public Long getValue1AsLong(Long defaultValue) { // 这里如果改成三元表达式,会导致自动拆箱造成空指针异常,所以只能用 if-else if(value1 == null){ return defaultValue; } return Long.parseLong(value1.toString()); } /** * 获取第二个值,并转化为 long 类型,值不存在则返回默认值 * @return / */ public Long getValue2AsLong(Long defaultValue) { // 这里如果改成三元表达式,会导致自动拆箱造成空指针异常,所以只能用 if-else if(value2 == null){ return defaultValue; } return Long.parseLong(value2.toString()); } /** * 该容器是否为无值状态,即:value1 无值、value2 无值 * @return / */ public boolean isNotValueState() { return value1IsEmpty() && value2IsEmpty(); } /** * 该容器是否为单值状态,即:value1 有值、value2 == 无值 * @return / */ public boolean isSingleValueState() { return ! value1IsEmpty() && value2IsEmpty(); } /** * 该容器是否为双值状态,即:value2 有值 (在 value2 有值的情况下,即使 value1 无值,也视为双值状态) * @return / */ public boolean isDoubleValueState() { return ! value2IsEmpty(); } /** * 获取两个值的字符串形式,形如:123,abc * *

*
     *     System.out.println(new SaValue2Box(1, 2));     // 1,2
     *     System.out.println(new SaValue2Box(null, null));   // null
     *     System.out.println(new SaValue2Box(1, null));   // 1
     *     System.out.println(new SaValue2Box(null, 2));  // ,2
     * 
* @return / */ @Override public String toString() { if(value1 == null && value2 == null) { return null; } if(value1 != null && value2 == null) { return value1.toString(); } return (value1 == null ? "" : value1.toString()) + "," + (value2 == null ? "" : value2.toString()); } } ================================================ FILE: sa-token-core/src/main/java/cn/dev33/satoken/util/StrFormatter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; /** * 字符串格式化工具,将字符串中的 {} 按序替换为参数 *

* 本工具类 copy 自 Hutool: * https://github.com/dromara/hutool/blob/v5-master/hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java *

* * @author Looly * @since 1.33.0 */ public class StrFormatter { /** * 占位符(保留原有 public 访问权限,避免破坏外部依赖) * @deprecated 语义不明确,建议内部使用 {@link #DEFAULT_PLACEHOLDER} 替代 */ @Deprecated public static String EMPTY_JSON = "{}"; /** * 反斜杠转义字符(保留原有 public 访问权限,避免破坏外部依赖) * @deprecated 命名不规范,建议内部使用 {@link #BACKSLASH_CHAR} 替代 */ @Deprecated public static char C_BACKSLASH = '\\'; /** * 新增内部规范常量(private,仅内部使用) * 默认占位符 */ private static final String DEFAULT_PLACEHOLDER = "{}"; /** * 反斜杠转义字符 * */ private static final char BACKSLASH_CHAR = '\\'; /** * 字符串构建器初始扩容长度 * */ private static final int BUFFER_INIT_CAPACITY = 50; /** * 格式化字符串
* 此方法只是简单将占位符 {} 按照顺序替换为参数
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
* 例:
* 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
* 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
* 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
* * @param strPattern 字符串模板 * @param argArray 参数列表 * @return 格式化后的结果 */ public static String format(String strPattern, Object... argArray) { return formatWith(strPattern, DEFAULT_PLACEHOLDER, argArray); } /** * 格式化字符串
* 此方法只是简单将指定占位符 按照顺序替换为参数
* 如果想输出占位符使用 \\转义即可,如果想输出占位符之前的 \ 使用双转义符 \\\\ 即可
* 例:
* 通常使用:format("this is {} for {}", "{}", "a", "b") =》 this is a for b
* 转义{}: format("this is \\{} for {}", "{}", "a", "b") =》 this is {} for a
* 转义\: format("this is \\\\{} for {}", "{}", "a", "b") =》 this is \a for b
* * @param strPattern 字符串模板 * @param placeHolder 占位符,例如{} * @param argArray 参数列表 * @return 格式化后的结果 * @since 1.33.0 */ public static String formatWith(String strPattern, String placeHolder, Object... argArray) { if (SaFoxUtil.isEmpty(strPattern) || SaFoxUtil.isEmpty(placeHolder) || SaFoxUtil.isEmpty(argArray)) { return strPattern; } final int strPatternLength = strPattern.length(); final int placeHolderLength = placeHolder.length(); // 初始化定义好的长度以获得更好的性能 final StringBuilder sbu = new StringBuilder(strPatternLength + BUFFER_INIT_CAPACITY); int handledPosition = 0;// 记录已经处理到的位置 int delimIndex;// 占位符所在位置 for (int argIndex = 0; argIndex < argArray.length; argIndex++) { delimIndex = strPattern.indexOf(placeHolder, handledPosition); if (delimIndex == -1) {// 剩余部分无占位符 if (handledPosition == 0) { // 不带占位符的模板直接返回 return strPattern; } // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 sbu.append(strPattern, handledPosition, strPatternLength); return sbu.toString(); } // 转义符 if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == BACKSLASH_CHAR) {// 转义符 if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == BACKSLASH_CHAR) {// 双转义符 // 转义符之前还有一个转义符,占位符依旧有效 sbu.append(strPattern, handledPosition, delimIndex - 1); sbu.append(argArray[argIndex]); handledPosition = delimIndex + placeHolderLength; } else { // 占位符被转义 argIndex--; sbu.append(strPattern, handledPosition, delimIndex - 1); sbu.append(placeHolder.charAt(0)); handledPosition = delimIndex + 1; } } else {// 正常占位符 sbu.append(strPattern, handledPosition, delimIndex); sbu.append(argArray[argIndex]); handledPosition = delimIndex + placeHolderLength; } } // 加入最后一个占位符后所有的字符 sbu.append(strPattern, handledPosition, strPatternLength); return sbu.toString(); } } ================================================ FILE: sa-token-demo/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo 0.0.1-SNAPSHOT pom sa-token-demo-alone-redis sa-token-demo-alone-redis-cluster sa-token-demo-alone-redis-sb4 sa-token-demo-apikey sa-token-demo-async sa-token-demo-beetl sa-token-demo-bom-import sa-token-demo-caffeine sa-token-demo-case sa-token-demo-device-lock sa-token-demo-dubbo/sa-token-demo-dubbo-provider sa-token-demo-dubbo/sa-token-demo-dubbo-consumer sa-token-demo-dubbo/sa-token-demo-dubbo3-provider sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer sa-token-demo-freemarker sa-token-demo-hutool-timed-cache sa-token-demo-jwt sa-token-demo-oauth2/sa-token-demo-oauth2-server sa-token-demo-oauth2/sa-token-demo-oauth2-client sa-token-demo-quick-login sa-token-demo-quick-login-sb3 sa-token-demo-remember-me/sa-token-demo-remember-me-server sa-token-demo-solon sa-token-demo-solon-redisson sa-token-demo-springboot sa-token-demo-springboot3-redis sa-token-demo-springboot4-redis sa-token-demo-springboot-low-version sa-token-demo-springboot-redis sa-token-demo-springboot-redisson sa-token-demo-sse sa-token-demo-ssm sa-token-demo-sso/sa-token-demo-sso-server sa-token-demo-sso/sa-token-demo-sso1-client sa-token-demo-sso/sa-token-demo-sso2-client sa-token-demo-sso/sa-token-demo-sso3-client sa-token-demo-sso/sa-token-demo-sso3-client-nosdk sa-token-demo-sso/sa-token-demo-sso3-client-resdk sa-token-demo-sso/sa-token-demo-sso3-client-anon sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon sa-token-demo-test sa-token-demo-thymeleaf sa-token-demo-webflux sa-token-demo-webflux-springboot3 sa-token-demo-webflux-springboot4 sa-token-demo-websocket sa-token-demo-websocket-spring sa-token-demo-loveqq-boot ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-alone-redis 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/SaTokenAloneRedisApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; /** * Sa-Token整合SpringBoot 示例 * @author click33 * */ @SpringBootApplication public class SaTokenAloneRedisApplication { public static void main(String[] args) { SpringApplication.run(SaTokenAloneRedisApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/AjaxJson.java ================================================ package com.pj.test; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { @Autowired StringRedisTemplate stringRedisTemplate; // 测试Sa-Token缓存, 浏览器访问: http://localhost:8081/test/login @RequestMapping("login") public AjaxJson login(@RequestParam(defaultValue="10001") String id) { System.out.println("--------------- 测试Sa-Token缓存"); StpUtil.login(id); return AjaxJson.getSuccess(); } // 测试业务缓存 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public AjaxJson test() { System.out.println("--------------- 测试业务缓存"); stringRedisTemplate.opsForValue().set("hello", "Hello World"); return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # Sa-Token配置 sa-token: # Token名称 (同时也是cookie名称) token-name: satoken # Token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # Token风格 token-style: uuid # 配置Sa-Token单独使用的Redis连接 alone-redis: # Redis模式(默认单体) # pattern: single # Redis数据库索引(默认为0) database: 2 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间(毫秒) timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 spring: # 配置业务使用的Redis连接 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-alone-redis-cluster 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/SaTokenAloneRedisClusterApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; /** * Sa-Token整合SpringBoot 示例 * @author click33 * */ @SpringBootApplication public class SaTokenAloneRedisClusterApplication { public static void main(String[] args) { SpringApplication.run(SaTokenAloneRedisClusterApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/AjaxJson.java ================================================ package com.pj.test; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { @Autowired StringRedisTemplate stringRedisTemplate; // 测试Sa-Token缓存, 浏览器访问: http://localhost:8081/test/login @RequestMapping("login") public AjaxJson login(@RequestParam(defaultValue="10001") String id) { System.out.println("--------------- 测试Sa-Token缓存"); StpUtil.login(id); return AjaxJson.getSuccess(); } // 测试业务缓存 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public AjaxJson test() { System.out.println("--------------- 测试业务缓存"); stringRedisTemplate.opsForValue().set("hello", "Hello World"); return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # Sa-Token配置 sa-token: # Token名称 (同时也是cookie名称) token-name: satoken # Token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # Token风格 token-style: uuid # 配置Sa-Token单独使用的Redis连接 alone-redis: # 普通集群 pattern: cluster # Redis服务器连接用户名(默认为空) username: # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10s cluster: # Redis集群服务器节点地址 nodes: 127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 # 最大重定向次数 maxRedirects: 2 lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 spring: # 配置业务使用的Redis连接 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application_sentinel.yml ================================================ # 端口 server: port: 8081 # Sa-Token配置 sa-token: # Token名称 (同时也是cookie名称) token-name: satoken # Token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # Token风格 token-style: uuid # 配置Sa-Token单独使用的Redis连接 alone-redis: # 哨兵模式 pattern: sentinel # Redis数据库索引(默认为0) database: 2 # Redis服务器连接用户名(默认为空) username: # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10s sentinel: #哨兵的名字 master: master_name # Redis集群服务器节点地址 nodes: 127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 spring: # 配置业务使用的Redis连接 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-sb4/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-alone-redis-sb4 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 4.0.3 1.45.0 org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-aspectj cn.dev33 sa-token-spring-boot4-starter ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} cn.dev33 sa-token-redis-template-jdk-serializer ${sa-token.version} test org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis-by-spring-boot4 ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/SaTokenAloneRedisSb4Application.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 整合 SpringBoot4 示例,整合 alone-redis 插件(权限缓存与业务缓存分离) * * @author click33 */ @SpringBootApplication public class SaTokenAloneRedisSb4Application { public static void main(String[] args) { SpringApplication.run(SaTokenAloneRedisSb4Application.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 测试专用Controller,演示 alone-redis 权限缓存与业务缓存分离 * * @author click33 */ @RestController @RequestMapping("/test/") public class TestController { @Autowired StringRedisTemplate stringRedisTemplate; // 测试Sa-Token缓存,浏览器访问: http://localhost:8083/test/login @RequestMapping("login") public SaResult login(@RequestParam(defaultValue = "10001") String id) { System.out.println("--------------- 测试Sa-Token缓存"); StpUtil.login(id); return SaResult.ok(); } // 测试业务缓存,浏览器访问: http://localhost:8083/test/test @RequestMapping("test") public SaResult test() { System.out.println("--------------- 测试业务缓存"); stringRedisTemplate.opsForValue().set("hello", "Hello World"); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/resources/application.yml ================================================ # 端口 server: port: 8083 # Sa-Token配置 sa-token: # Token名称 (同时也是cookie名称) token-name: satoken # Token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # Token风格 token-style: uuid # 配置Sa-Token单独使用的Redis连接 alone-redis: # Redis数据库索引(默认为0) database: 2 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间(毫秒) timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 # 配置业务使用的Redis连接 spring: data: redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-apikey/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-apikey 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-apikey ${sa-token.version} org.springframework.boot spring-boot-devtools provided org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/SaTokenApiKeyApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.apikey.SaApiKeyManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaTokenApiKeyApplication { public static void main(String[] args) { SpringApplication.run(SaTokenApiKeyApplication.class, args); System.out.println("启动成功:Sa-Token 配置如下:" + SaManager.getConfig()); System.out.println("启动成功:API Key 配置如下:" + SaApiKeyManager.getConfig()); System.out.println("测试访问:http://localhost:8081/index.html"); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyDataLoaderImpl.java ================================================ package com.pj.mock; import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; import cn.dev33.satoken.apikey.model.ApiKeyModel; import org.springframework.beans.factory.annotation.Autowired; /** * API Key 数据加载器实现类 (从数据库查询) * * @author click33 * @since 2025/4/4 */ //@Component // 打开此注解后,springboot 会自动注入此组件,打开 Sa-Token API Key 模块的数据库模式 public class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader { @Autowired SaApiKeyMockMapper apiKeyMockMapper; // 指定框架不再维护 API Key 索引信息,而是由我们手动从数据库维护 @Override public Boolean getIsRecordIndex() { return false; } // 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑) @Override public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) { return apiKeyMockMapper.getApiKeyModel(apiKey); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyMockMapper.java ================================================ package com.pj.mock; import cn.dev33.satoken.apikey.model.ApiKeyModel; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * 模拟数据库操作类 * * @author click33 * @since 2025/4/4 */ @Component public class SaApiKeyMockMapper { // 添加模拟测试数据 public static final Map map = new HashMap<>(); static { ApiKeyModel ak1 = new ApiKeyModel(); ak1.setLoginId(10001); // 设置绑定的用户 id ak1.setApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); // 设置 API Key 值 ak1.setTitle("test"); // 设置名称 ak1.setExpiresTime(System.currentTimeMillis() + 2592000); // 设置失效时间,13位时间戳,-1=永不失效 map.put(ak1.getApiKey(), ak1); ApiKeyModel ak2 = new ApiKeyModel(); ak2.setLoginId(10001); // 设置绑定的用户 id ak2.setApiKey("AK-NxcO63u57zbOWCmLaiVQuVWXssRwAxFcAxcFF"); // 设置 API Key 值 ak2.setTitle("commit2"); // 设置名称 ak1.addScope("commit", "pull"); // 设置权限范围 ak2.setExpiresTime(System.currentTimeMillis() + 2592000); // 设置失效时间,13位时间戳,-1=永不失效 map.put(ak2.getApiKey(), ak2); } // 返回指定 API Key 对应的 ApiKeyModel public ApiKeyModel getApiKeyModel(String apiKey) { return map.get(apiKey); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/GlobalException.java ================================================ package com.pj.satoken; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import cn.dev33.satoken.util.SaResult; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // 输出 API 请求日志,方便调试代码 // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 .setBeforeAuth(obj -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 允许指定域访问跨域资源 .setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyController.java ================================================ package com.pj.test; import cn.dev33.satoken.apikey.model.ApiKeyModel; import cn.dev33.satoken.apikey.template.SaApiKeyUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * API Key 相关接口 * * @author click33 */ @RestController public class ApiKeyController { // 返回当前登录用户拥有的 ApiKey 列表 @RequestMapping("/myApiKeyList") public SaResult myApiKeyList() { List apiKeyList = SaApiKeyUtil.getApiKeyList(StpUtil.getLoginId()); return SaResult.data(apiKeyList); } // 创建一个新的 ApiKey,并返回 @RequestMapping("/createApiKey") public SaResult createApiKey() { ApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(StpUtil.getLoginId()).setTitle("test"); SaApiKeyUtil.saveApiKey(akModel); return SaResult.data(akModel); } // 修改 ApiKey @RequestMapping("/updateApiKey") public SaResult updateApiKey(ApiKeyModel akModel) { // 先验证一下是否为本人的 ApiKey SaApiKeyUtil.checkApiKeyLoginId(akModel.getApiKey(), StpUtil.getLoginId()); // 修改 ApiKeyModel akModel2 = SaApiKeyUtil.getApiKey(akModel.getApiKey()); akModel2.setTitle(akModel.getTitle()); akModel2.setExpiresTime(akModel.getExpiresTime()); akModel2.setIsValid(akModel.getIsValid()); akModel2.setScopes(akModel.getScopes()); SaApiKeyUtil.saveApiKey(akModel2); return SaResult.ok(); } // 删除 ApiKey @RequestMapping("/deleteApiKey") public SaResult deleteApiKey(String apiKey) { SaApiKeyUtil.checkApiKeyLoginId(apiKey, StpUtil.getLoginId()); SaApiKeyUtil.deleteApiKey(apiKey); return SaResult.ok(); } // 删除当前用户所有 ApiKey @RequestMapping("/deleteMyAllApiKey") public SaResult deleteMyAllApiKey() { SaApiKeyUtil.deleteApiKeyByLoginId(StpUtil.getLoginId()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyResourcesController.java ================================================ package com.pj.test; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.apikey.annotation.SaCheckApiKey; import cn.dev33.satoken.apikey.model.ApiKeyModel; import cn.dev33.satoken.apikey.template.SaApiKeyUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * API Key 资源 相关接口 * * @author click33 */ @RestController public class ApiKeyResourcesController { // 必须携带有效的 ApiKey 才能访问 @SaCheckApiKey @RequestMapping("/akRes1") public SaResult akRes1() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } // 必须携带有效的 ApiKey ,且具有 userinfo 权限 @SaCheckApiKey(scope = "userinfo") @RequestMapping("/akRes2") public SaResult akRes2() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } // 必须携带有效的 ApiKey ,且同时具有 userinfo、chat 权限 @SaCheckApiKey(scope = {"userinfo", "chat"}) @RequestMapping("/akRes3") public SaResult akRes3() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } // 必须携带有效的 ApiKey ,且具有 userinfo、chat 其中之一权限 @SaCheckApiKey(scope = {"userinfo", "chat"}, mode = SaMode.OR) @RequestMapping("/akRes4") public SaResult akRes4() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 登录 Controller * * @author click33 */ @RestController public class LoginController { // 登录 @RequestMapping("login") public SaResult login(@RequestParam(defaultValue="10001") String id) { StpUtil.login(id); return SaResult.ok().set("satoken", StpUtil.getTokenValue()); } // 查询当前登录人 @RequestMapping("getLoginId") public SaResult getLoginId() { return SaResult.data(StpUtil.getLoginId()); } // 注销 @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # 开启日志信息 is-log: true # API Key 相关配置 api-key: # API Key 前缀 prefix: AK- # API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) timeout: 2592000 # 框架是否记录索引信息 is-record-index: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/resources/static/common.js ================================================ // 服务器接口主机地址 var baseUrl = "http://localhost:8081"; // 封装一下Ajax function ajax(path, data, successFn, errorFn) { console.log(baseUrl + path); fetch(baseUrl + path, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'satoken': localStorage.getItem('satoken') }, body: serializeToQueryString(data), }) .then(response => response.json()) .then(res => { console.log('返回数据:', res); if(res.code == 200) { successFn(res); } else { if(errorFn) { errorFn(res); } else { showMsg('错误:' + res.msg); } } }) .catch(error => { console.error('请求失败:', error); return alert("异常:" + JSON.stringify(error)); }); } // ------------ 工具方法 --------------- // 从url中查询到指定名称的参数值 function getParam(name, defaultValue) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == name) { return pair[1]; } } return (defaultValue == undefined ? null : defaultValue); } // 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true function serializeToQueryString(obj) { return Object.entries(obj) .filter(([_, value]) => value != null) // 过滤 null 和 undefined .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } // 随机生成字符串 function randomString(len) { len = len || 32; var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; var maxPos = $chars.length; var str = ''; for (i = 0; i < len; i++) { str += $chars.charAt(Math.floor(Math.random() * maxPos)); } return str; } // 带动画的弹出提示 function showMsg(message) { const alertBox = document.createElement('div'); // 初始样式(包含隐藏状态) Object.assign(alertBox.style, { position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%) scale(0.8) translateY(-30px)', // 初始缩放+位移 opacity: '0', background: 'rgba(0, 0, 0, 0.85)', color: 'white', padding: '16px 32px', borderRadius: '8px', transition: 'all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55)', // 弹性动画曲线 pointerEvents: 'none', whiteSpace: 'nowrap', fontSize: '16px', boxShadow: '0 4px 12px rgba(0,0,0,0.25)' // 添加投影增强立体感 }); alertBox.textContent = message; document.body.appendChild(alertBox); // 强制重绘确保动画触发 void alertBox.offsetHeight; // 应用入场动画 Object.assign(alertBox.style, { opacity: '1', transform: 'translate(-50%, -50%) scale(1) translateY(-20px)' }); // 自动消失逻辑 setTimeout(() => { Object.assign(alertBox.style, { opacity: '0', transform: 'translate(-50%, -50%) scale(0.9) translateY(-20px)' }); alertBox.addEventListener('transitionend', () => { alertBox.remove(); }, { once: true }); }, 3000); } // 将日期格式化 yyyy-MM-dd HH:mm:ss function formatDateTime(date) { date = new Date(date); // 补零函数 const pad = (n, len) => n.toString().padStart(len, '0'); // 分解时间组件 const year = date.getFullYear(); const month = pad(date.getMonth() + 1, 2); // 0-11 → 1-12 const day = pad(date.getDate(), 2); const hours = pad(date.getHours(), 2); // 24小时制 const minutes = pad(date.getMinutes(), 2); const seconds = pad(date.getSeconds(), 2); const milliseconds = pad(date.getMilliseconds(), 3); // 拼接格式 // return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } ================================================ FILE: sa-token-demo/sa-token-demo-apikey/src/main/resources/static/index.html ================================================ Title

Sa-Token - API Key 测试页

登录

当前登录人:
输入账号 id 登录:

API Key 列表

名称 API Key 权限(多个用逗号隔开) 过期时间 是否生效 操作

调用 API

使用的 API Key:
需要正确的 API Key
需要具备 Scope: userinfo
需要具备 Scope: userinfo,chat (需要全部具备)
需要具备 Scope: userinfo,chat (具备其一即可)
================================================ FILE: sa-token-demo/sa-token-demo-async/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-async 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 com.pj.SaTokenAsyncApplication org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-async/src/main/java/com/pj/SaTokenAsyncApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; /** * Sa-Token 异步方案 测试 * @author click33 * */ @EnableAsync // 启用异步 @EnableScheduling // 启动定时任务 @SpringBootApplication public class SaTokenAsyncApplication { public static void main(String[] args) { SpringApplication.run(SaTokenAsyncApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-async/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-async/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // 输出 API 请求日志,方便调试代码 // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.context.mock.SaTokenContextMockUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Date; /** * 测试几种场景的异步场景 Controller * * @author click33 */ @RestController @RequestMapping("/test/") public class TestController { @Autowired public ThreadPoolTaskExecutor taskExecutor; // 【同步】登录 ---- http://localhost:8081/test/login?id=10001 @RequestMapping("login") public SaResult login(@RequestParam(defaultValue = "10001") long id) { StpUtil.login(id); return SaResult.ok("登录成功"); } // 【同步】判断是否登录 --- http://localhost:8081/test/isLogin @RequestMapping("isLogin") public SaResult isLogin() { System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenValue()); } // 【同步】注销 浏览器访问: http://localhost:8081/test/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.data(null); } // 【异步】new Thread --- http://localhost:8081/test/isLogin2 @RequestMapping("isLogin2") public SaResult isLogin2() { System.out.println("是否登录:" + StpUtil.isLogin()); String tokenValue = StpUtil.getTokenValue(); new Thread(() -> { SaTokenContextMockUtil.setMockContext(()->{ StpUtil.setTokenValueToStorage(tokenValue); System.out.println("是否登录:" + StpUtil.isLogin()); }); }).start(); return SaResult.data(StpUtil.getTokenValue()); } // 【异步】线程池 ThreadPoolTaskExecutor --- http://localhost:8081/test/isLogin3 @RequestMapping("isLogin3") public SaResult isLogin3(HttpServletRequest request, HttpServletResponse response) { System.out.println("是否登录:" + StpUtil.isLogin()); String tokenValue = StpUtil.getTokenValue(); taskExecutor.execute(() -> { SaTokenContextMockUtil.setMockContext(()->{ StpUtil.setTokenValueToStorage(tokenValue); System.out.println("是否登录:" + StpUtil.isLogin()); }); }); return SaResult.data(StpUtil.getTokenValue()); } // 【异步】@Async --- http://localhost:8081/test/isLogin4 @Async @RequestMapping("isLogin4") public SaResult isLogin4(@CookieValue("satoken") String satoken) { SaTokenContextMockUtil.setMockContext(()->{ StpUtil.setTokenValueToStorage(satoken); System.out.println("是否登录:" + StpUtil.isLogin()); }); return SaResult.ok(); } // 【异步】定时任务 @Scheduled(cron = "0 * * * * ?") // 一分钟执行一次 // @Scheduled(cron = "0/10 * * * * ?") // 十秒执行一次 public void scheduledMethod(){ // 错误写法:直接调用 Sa-Token API 会报错 // System.out.println("定时任务,Mock 范围外:是否登录:" + StpUtil.isLogin()); System.out.println(SaFoxUtil.formatDate(new Date())); // 需要先设置模拟上下文 SaTokenContextMockUtil.setMockContext(() -> { // StpUtil.setTokenValueToStorage("f452571f-bfdb-413d-aba9-e26992cf07be"); // 模拟 Token System.out.println("定时任务,Mock 范围内:是否登录:" + StpUtil.isLogin()); // 模拟登录 // StpUtil.login(10066); // 模拟 登录 // System.out.println("定时任务,Mock 范围内:登录账号:" + StpUtil.getLoginId()); }); } } /* 使用 InheritableThreadLocal 存储上下文带来的坑: @RequestMapping("isLogin2") public SaResult isLogin2() { System.out.println("是否登录:" + StpUtil.isLogin()); new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("是否登录:" + StpUtil.isLogin()); }).start(); return SaResult.data(null); } 如果不 Thread.sleep(1000),外面 true,里面 true 如果 Thread.sleep(1000),则外面 true,里面false 因为 SpringBoot 会在请求结束后清除 request 里的数据, 此时子线程内部可以读取到 request,但是 request 无值,导致代码既能成功运行,又逻辑错误,是一种难以排查的隐形 bug 应该避免使用 InheritableThreadLocal 来存储上下文数据 */ ================================================ FILE: sa-token-demo/sa-token-demo-async/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-beetl/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-beetl 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web com.ibeetl beetl-framework-starter 1.2.40.Beetl.RELEASE cn.dev33 sa-token-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-devtools provided org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/SaTokenBeetlDemoApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; @SpringBootApplication public class SaTokenBeetlDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenBeetlDemoApplication.class, args); System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); System.out.println("\n测试访问:http://localhost:8081/"); } } ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.stp.StpUtil; import com.ibeetl.starter.BeetlTemplateCustomize; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 为 Beetl 视图引擎注册自定义函数: // 通过 stp.xxx() 调用 StpUtil.stpLogic 对象上所有 public 方法 @Bean public BeetlTemplateCustomize beetlTemplateCustomize(){ return groupTemplate -> groupTemplate.registerFunctionPackage("stp", StpUtil.stpLogic); } } ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; /** * 测试 Controller * @author click33 * */ @RestController public class TestController { // 首页 @RequestMapping("/") public Object index() { return new ModelAndView("index.btl"); } // 登录 @RequestMapping("login") public SaResult login(@RequestParam(defaultValue="10001") String id) { StpUtil.login(id); StpUtil.getSession().set("name", "zhangsan"); return SaResult.ok(); } // 注销 @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ================================================ FILE: sa-token-demo/sa-token-demo-beetl/src/main/resources/templates/index.btl ================================================ Sa-Token 集成 Beetl 示例

Sa-Token 集成 Beetl —— 测试页面

当前是否登录:${stp.isLogin()}

操作: 登录 注销

登录之后才能显示: <% if(stp.isLogin()){ %> value <%}%>

不登录才能显示: <% if(!stp.isLogin()){ %> value <%}%>

具有角色 admin 才能显示:<% if(stp.hasRole("admin")){ %> value <% } %>

同时具备多个角色才能显示:<% if(stp.hasRoleAnd("admin", "ceo", "cto")){ %> value <% } %>

只要具有其中一个角色就能显示:<% if(stp.hasRoleOr("admin", "ceo", "cto")){ %> value <% } %>

不具有角色 admin 才能显示:<% if(!stp.hasRole("admin")){ %> value <% } %>

具有权限 user-add 才能显示:<% if(stp.hasPermission("user-add")){ %> value <% } %>

同时具备多个权限才能显示:<% if(stp.hasPermissionAnd("user-add", "user-delete", "user-get")){ %> value <% } %>

只要具有其中一个权限就能显示:<% if(stp.hasPermissionOr("user-add", "user-delete", "user-get")){ %> value <% } %>

不具有权限 user-add 才能显示:<% if(!stp.hasPermission("user-add")){ %> value <% } %>

<% if(stp.isLogin()){ %>

从SaSession中取值:${stp.getSession()["name"]}
或:${stp.getSession().name}

<%}%>
================================================ FILE: sa-token-demo/sa-token-demo-bom-import/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-bom-import 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter cn.dev33 sa-token-redis-jackson cn.dev33 sa-token-jwt org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true cn.dev33 sa-token-bom 1.45.0 pom import ================================================ FILE: sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/SaTokenDemoApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.jwt.StpLogicJwtForSimple; import cn.dev33.satoken.stp.StpLogic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; /** * Sa-Token 使用 bom 包引入框架 */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } // Sa-Token 整合 jwt (Simple 简单模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForSimple(); } } ================================================ FILE: sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e){ e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); StpUtil.login(10001); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { StpUtil.checkLogin(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-bom-import/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-caffeine/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-caffeine 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-caffeine ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/SaTokenDemoApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 整合 Caffeine 示例 * @author click33 * */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); System.out.println(SaManager.getSaTokenDao()); } } ================================================ FILE: sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-caffeine/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true ================================================ FILE: sa-token-demo/sa-token-demo-case/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-case 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} cn.dev33 sa-token-spring-el ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 示例 * @author click33 * */ @SpringBootApplication public class SaTokenCaseApplication { public static void main(String[] args) { SpringApplication.run(SaTokenCaseApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/more/SaCheckELController.java ================================================ package com.pj.cases.more; import cn.dev33.satoken.annotation.SaCheckEL; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * SaCheckEL EL表达式注解鉴权示例 * * @author click33 * @since 2022-10-13 */ @RestController @RequestMapping("/check-el/") public class SaCheckELController { // 登录校验 ---- http://localhost:8081/check-el/test1 @SaCheckEL("stp.checkLogin()") @RequestMapping("test1") public SaResult test1() { return SaResult.ok(); } // 角色校验 ---- http://localhost:8081/check-el/test2 @SaCheckEL("stp.checkRole('dev-admin')") @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } // 权限校验 ---- http://localhost:8081/check-el/test3 @SaCheckEL("stp.checkPermission('user:edit')") @RequestMapping("test3") public SaResult test3() { return SaResult.ok(); } // 二级认证 ---- http://localhost:8081/check-el/test4 @SaCheckEL("stp.checkSafe()") @RequestMapping("test4") public SaResult test4() { return SaResult.ok(); } // 参数长度校验 ---- http://localhost:8081/check-el/test5?name=zhangsan @SaCheckEL("NEED( #name.length() > 3 )") @RequestMapping("test5") public SaResult test5(@RequestParam(defaultValue = "") String name) { return SaResult.ok().set("name", name); } // 参数长度校验,并自定义异常描述信息 ---- http://localhost:8081/check-el/test6?name=z @SaCheckEL("NEED( #name !=null && #name.length() > 3, 'name长度不够' )") @RequestMapping("test6") public SaResult test6(String name) { return SaResult.ok().set("name", name); } // 已登录, 或者查询数据在公开范围内 ---- http://localhost:8081/check-el/test7?id=10044 @SaCheckEL("NEED( stp.isLogin() or (#id != null and #id > 10010) )") @RequestMapping("test7") public SaResult test7(long id) { return SaResult.ok().set("id", id); } // SaSession 里取值校验 ---- http://localhost:8081/check-el/test8 @SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )") @RequestMapping("test8") public SaResult test8() { return SaResult.ok(); } // 多账号体系鉴权测试 ---- http://localhost:8081/check-el/test9 @SaCheckEL("stpUser.checkLogin()") @RequestMapping("test9") public SaResult test9() { return SaResult.ok(); } // 本模块需要鉴权的权限码 public String permissionCode = "article:add"; // 调用本类的成员变量 ---- http://localhost:8081/check-el/test10 @SaCheckEL("stp.checkPermission( this.permissionCode )") @RequestMapping("test10") public SaResult test10() { return SaResult.ok(); } // 忽略鉴权测试 ---- http://localhost:8081/check-el/test11 @SaIgnore @SaCheckEL("stp.checkPermission( 'abc' )") @RequestMapping("test11") public SaResult test11() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java ================================================ package com.pj.cases.plugin; import cn.dev33.satoken.temp.SaTempUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * 测试专用 Controller * @author click33 * */ @RestController @RequestMapping("/temp-token/") public class TempTokenController { // 创建 浏览器访问: http://localhost:8081/temp-token/create @RequestMapping("create") public SaResult create() { String token = SaTempUtil.createToken(10001, 1200); System.out.println("创建成功:" + token); return SaResult.data(token); } // 创建,获取时裁剪前缀 浏览器访问: http://localhost:8081/temp-token/create2 @RequestMapping("create2") public SaResult create2() { String token = SaTempUtil.createToken("shop_" + 1001, 1200); System.out.println("创建成功:" + token); System.out.println("获取对应值:" + SaTempUtil.parseToken(token)); System.out.println("获取对应值,并裁剪前缀:" + SaTempUtil.parseToken(token, "shop_", Long.class)); System.out.println("指定错误前缀来获取:" + SaTempUtil.parseToken(token, "art_", Long.class)); return SaResult.data(token); } // 创建,回收 浏览器访问: http://localhost:8081/temp-token/create3 @RequestMapping("create3") public SaResult create3() { String token = SaTempUtil.createToken(10003, 1200); System.out.println("创建成功:" + token); System.out.println("获取对应值:" + SaTempUtil.parseToken(token)); SaTempUtil.deleteToken(token); System.out.println("回收后再获取:" + SaTempUtil.parseToken(token)); return SaResult.data(token); } // 创建,记录索引 浏览器访问: http://localhost:8081/temp-token/create4 @RequestMapping("create4") public SaResult create4() { String token1 = SaTempUtil.createToken(10004, 1200, true); String token2 = SaTempUtil.createToken(10004, 1300, true); String token3 = SaTempUtil.createToken(10004, -1, true); System.out.println("token1 剩余有效期:" + SaTempUtil.getTimeout(token1)); System.out.println("token2 剩余有效期:" + SaTempUtil.getTimeout(token2)); System.out.println("token3 剩余有效期:" + SaTempUtil.getTimeout(token3)); SaTempUtil.deleteToken(token3); // 获取已创建的 token 列表 System.out.println("获取已创建的 token 列表 "); List tempTokenList = SaTempUtil.getTempTokenList(10004); System.out.println(tempTokenList); return SaResult.data(token1); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java ================================================ package com.pj.cases.test; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试专用 Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { System.out.println("------------进来了"); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/DisableController.java ================================================ package com.pj.cases.up; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 账号封禁示例 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/disable/") public class DisableController { /* * 测试步骤: 1、访问登录接口,可以正常登录 ---- http://localhost:8081/disable/login?userId=10001 2、注销登录 ---- http://localhost:8081/disable/logout 3、禁用账号 ---- http://localhost:8081/disable/disable?userId=10001 4、再次访问登录接口,登录失败 ---- http://localhost:8081/disable/login?userId=10001 5、解封账号 ---- http://localhost:8081/disable/untieDisable?userId=10001 6、再次访问登录接口,登录成功 ---- http://localhost:8081/disable/login?userId=10001 */ // 会话登录接口 ---- http://localhost:8081/disable/login?userId=10001 @RequestMapping("login") public SaResult login(long userId) { // 1、先检查此账号是否已被封禁 StpUtil.checkDisable(userId); // 2、检查通过后,再登录 StpUtil.login(userId); return SaResult.ok("账号登录成功"); } // 会话注销接口 ---- http://localhost:8081/disable/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok("账号退出成功"); } // 封禁指定账号 ---- http://localhost:8081/disable/disable?userId=10001 @RequestMapping("disable") public SaResult disable(long userId) { /* * 账号封禁: * 参数1:要封禁的账号id * 参数2:要封禁的时间,单位:秒,86400秒=1天 */ StpUtil.disable(userId, 86400); return SaResult.ok("账号 " + userId + " 封禁成功"); } // 解封指定账号 ---- http://localhost:8081/disable/untieDisable?userId=10001 @RequestMapping("untieDisable") public SaResult untieDisable(long userId) { StpUtil.untieDisable(userId); return SaResult.ok("账号 " + userId + " 解封成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/HttpBasicController.java ================================================ package com.pj.cases.up; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Sa-Token Http Basic 认证 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/basic/") public class HttpBasicController { /* * 测试步骤: 1、访问资源接口,被拦截,无法返回数据信息 ---- http://localhost:8081/basic/getInfo 2、浏览器弹出窗口,要求输入账号密码,输入:账号=sa,密码=123456,确认 3、后端返回数据信息 4、后续再次访问接口时,无需重复输入账号密码 */ // 资源接口 ---- http://localhost:8081/basic/getInfo @RequestMapping("getInfo") public SaResult login() { // 1、Http Basic 认证校验,账号=sa,密码=123456 SaHttpBasicUtil.check("sa:123456"); // 2、返回数据 String data = "这是通过 Http Basic 校验后才返回的数据"; return SaResult.data(data); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java ================================================ package com.pj.cases.up; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Sa-Token 同端互斥登录示例 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/mutex/") public class MutexLoginController { /* * 前提1:准备至少四个浏览器:A、B、C、D * 前提2:配置文件需要把 sa-token.is-concurrent 的值改为 false * * 测试步骤: 1、在浏览器A上登录账号10001,设备为PC ---- http://localhost:8081/mutex/login?userId=10001&device=PC 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin 2、在浏览器B上登录账号10001,设备为APP ---- http://localhost:8081/mutex/login?userId=10001&device=APP 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin 3、复查一下览器A上的账号是否登录,发现并没有顶替下去,原因是两个浏览器指定的登录设备不同,允许同时在线 ---- http://localhost:8081/mutex/isLogin 4、在浏览器C上登录账号10001,设备为PC ---- http://localhost:8081/mutex/login?userId=10001&device=PC 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin 5、复查一下浏览器A上的账号是否登录,发现账号已被顶替下线 ---- http://localhost:8081/mutex/isLogin 6、再复查一下浏览器B上的账号是否登录,发现账号未被顶替下线,因为浏览器B上登录的设备是APP,而浏览器C顶替的设备是PC ---- http://localhost:8081/mutex/isLogin 7、此时再从浏览器D上登录账号10001,设备为APP ---- http://localhost:8081/mutex/login?userId=10001&device=APP 检查是否登录成功,返回true: ---- http://localhost:8081/mutex/isLogin 8、此时再复查一下浏览器B上的账号是否登录,发现账号已被顶替下线 ---- http://localhost:8081/mutex/isLogin */ // 会话登录接口 ---- http://localhost:8081/mutex/doLogin?userId=10001&device=PC @RequestMapping("login") public SaResult login(long userId, String device) { /* * 参数1:要登录的账号 * 参数2:此账号在什么设备上登录的 */ StpUtil.login(userId, device); return SaResult.ok("登录成功"); } // 查询当前登录状态 ---- http://localhost:8081/mutex/isLogin @RequestMapping("isLogin") public SaResult isLogin() { // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false boolean isLogin = StpUtil.isLogin(); return SaResult.ok("当前客户端是否登录:" + isLogin + ",登录的设备是:" + StpUtil.getLoginDeviceType()); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/NotCookieController.java ================================================ package com.pj.cases.up; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 前后端分离模式示例 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/NotCookie/") public class NotCookieController { // 前后端一体模式的登录样例 ---- http://localhost:8081/NotCookie/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { if("zhang".equals(name) && "123456".equals(pwd)) { // 会话登录 StpUtil.login(10001); return SaResult.ok(); } return SaResult.error("登录失败"); } // 前后端分离模式的登录样例 ---- http://localhost:8081/NotCookie/doLogin2?name=zhang&pwd=123456 @RequestMapping("doLogin2") public SaResult doLogin2(String name, String pwd) { if("zhang".equals(name) && "123456".equals(pwd)) { // 会话登录 StpUtil.login(10001); // 与常规登录不同点之处:这里需要把 Token 信息从响应体中返回到前端 SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return SaResult.data(tokenInfo); } return SaResult.error("登录失败"); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/RememberMeController.java ================================================ package com.pj.cases.up; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 记住我模式登录 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/RememberMe/") public class RememberMeController { // 记住我登录 ---- http://localhost:8081/RememberMe/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001, true); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 不记住我登录 ---- http://localhost:8081/RememberMe/doLogin2?name=zhang&pwd=123456 @RequestMapping("doLogin2") public SaResult doLogin2(String name, String pwd) { if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001, false); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 七天免登录 ---- http://localhost:8081/RememberMe/doLogin3?name=zhang&pwd=123456 @RequestMapping("doLogin3") public SaResult doLogin3(String name, String pwd) { if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001, 60 * 60 * 24 * 7); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SafeAuthController.java ================================================ package com.pj.cases.up; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 二级认证示例 * * @author click33 * @since 2022-10-16 */ @RestController @RequestMapping("/safe/") public class SafeAuthController { /* * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 * * 测试步骤: 1、前端调用 deleteProject 接口,尝试删除仓库。 ---- http://localhost:8081/safe/deleteProject 2、后端校验会话尚未完成二级认证,返回: 仓库删除失败,请完成二级认证后再次访问接口。 3、前端将信息提示给用户,用户输入密码,调用 openSafe 接口。 ---- http://localhost:8081/safe/openSafe?password=123456 4、后端比对用户输入的密码,完成二级认证,有效期为:120秒。 5、前端在 120 秒内再次调用 deleteProject 接口,尝试删除仓库。 ---- http://localhost:8081/safe/deleteProject 6、后端校验会话已完成二级认证,返回:仓库删除成功。 */ // 删除仓库 ---- http://localhost:8081/safe/deleteProject @RequestMapping("deleteProject") public SaResult deleteProject(String projectId) { // 第1步,先检查当前会话是否已完成二级认证 // 这个地方既可以通过 StpUtil.isSafe() 手动判断, // 也可以通过 StpUtil.checkSafe() 或者 @SaCheckSafe 来校验(校验不通过时将抛出 NotSafeException 异常) if(!StpUtil.isSafe()) { return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口"); } // 第2步,如果已完成二级认证,则开始执行业务逻辑 // ... // 第3步,返回结果 return SaResult.ok("仓库删除成功"); } // 提供密码进行二级认证 ---- http://localhost:8081/safe/openSafe?password=123456 @RequestMapping("openSafe") public SaResult openSafe(String password) { // 比对密码(此处只是举例,真实项目时可拿其它参数进行校验) if("123456".equals(password)) { // 比对成功,为当前会话打开二级认证,有效期为120秒,意为在120秒内再调用 deleteProject 接口都无需提供密码 StpUtil.openSafe(120); return SaResult.ok("二级认证成功"); } // 如果密码校验失败,则二级认证也会失败 return SaResult.error("二级认证失败"); } // 手动关闭二级认证 ---- http://localhost:8081/safe/closeSafe @RequestMapping("closeSafe") public SaResult closeSafe() { StpUtil.closeSafe(); return SaResult.ok(); } // ------------------ 指定业务类型进行二级认证 // 获取应用秘钥 ---- http://localhost:8081/safe/getClientSecret @RequestMapping("getClientSecret") public SaResult getClientSecret() { // 第1步,先检查当前会话是否已完成 client业务 的二级认证 StpUtil.checkSafe("client"); // 第2步,如果已完成二级认证,则返回数据 return SaResult.data("aaaa-bbbb-cccc-dddd-eeee"); } // 提供手势密码进行二级认证 ---- http://localhost:8081/safe/openClientSafe?gesture=35789 @RequestMapping("openClientSafe") public SaResult openClientSafe(String gesture) { // 比对手势密码(此处只是举例,真实项目时可拿其它参数进行校验) if("35789".equals(gesture)) { // 比对成功,为当前会话打开二级认证: // 业务类型为:client // 有效期为600秒==10分钟,意为在10分钟内,调用 getClientSecret 时都无需再提供手势密码 StpUtil.openSafe("client", 600); return SaResult.ok("二级认证成功"); } // 如果密码校验失败,则二级认证也会失败 return SaResult.error("二级认证失败"); } // 查询当前会话是否已完成指定的二级认证 ---- http://localhost:8081/safe/isClientSafe @RequestMapping("isClientSafe") public SaResult isClientSafe() { return SaResult.ok("当前是否已完成 client 二级认证:" + StpUtil.isSafe("client")); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SearchSessionController.java ================================================ package com.pj.cases.up; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.model.SysUser; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 会话查询示例 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/search/") public class SearchSessionController { /* * 测试步骤: 1、先登录5个账号 ---- http://localhost:8081/search/login?userId=10001&张三&age=18 ---- http://localhost:8081/search/login?userId=10002&李四&age=20 ---- http://localhost:8081/search/login?userId=10003&王五&age=22 ---- http://localhost:8081/search/login?userId=10004&赵六&age=24 ---- http://localhost:8081/search/login?userId=10005&冯七&age=26 2、根据分页参数获取会话列表 http://localhost:8081/search/getList?start=0&size=10 */ // 会话登录接口 ---- http://localhost:8081/search/login?userId=10001&张三&age=18 @RequestMapping("login") public SaResult login(long userId, String name, int age) { // 先登录上 StpUtil.login(userId); // 再把 User 对象存储在 SaSession 上 SysUser user = new SysUser(); user.setId(userId); user.setName(name); user.setAge(age); StpUtil.getSession().set("user", user); // 返回 return SaResult.ok("账号登录成功"); } // 会话查询接口 ---- http://localhost:8081/search/getList?start=0&size=10 @RequestMapping("getList") public SaResult getList(int start, int size) { // 创建集合 List sessionList = new ArrayList<>(); // 分页查询数据 List sessionIdList = StpUtil.searchSessionId("", start, size, false); for (String sessionId: sessionIdList) { SaSession session = StpUtil.getSessionBySessionId(sessionId); sessionList.add(session); } // 返回 return SaResult.data(sessionList); } // 会话查询接口 ---- http://localhost:8081/disable/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok("账号退出成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java ================================================ package com.pj.cases.up; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.secure.SaBase64Util; import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 密码加密示例 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/secure/") public class SecureController { // 摘要加密 ---- http://localhost:8081/secure/digest @RequestMapping("digest") public SaResult digest() { // md5加密 System.out.println(SaSecureUtil.md5("123456")); // sha1加密 System.out.println(SaSecureUtil.sha1("123456")); // sha256加密 System.out.println(SaSecureUtil.sha256("123456")); return SaResult.ok(); } // AES加密 ---- http://localhost:8081/secure/aes @RequestMapping("aes") public SaResult aes() { // 定义秘钥和明文 String key = "123456"; String text = "Sa-Token 一个轻量级java权限认证框架"; // 加密 String ciphertext = SaSecureUtil.aesEncrypt(key, text); System.out.println("AES加密后:" + ciphertext); // 解密 String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); System.out.println("AES解密后:" + text2); return SaResult.ok(); } // RSA加密 ---- http://localhost:8081/secure/rsa // @RequestMapping("rsa") // public SaResult rsa() { // // 定义私钥和公钥 // String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg=="; // String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB"; // // // 文本 // String text = "Sa-Token 一个轻量级java权限认证框架"; // // // 使用公钥加密 // String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text); // System.out.println("公钥加密后:" + ciphertext); // // // 使用私钥解密 // String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext); // System.out.println("私钥解密后:" + text2); // // return SaResult.ok(); // } // Base64 编码 ---- http://localhost:8081/secure/base64 @RequestMapping("base64") public SaResult base64() { // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用Base64编码 String base64Text = SaBase64Util.encode(text); System.out.println("Base64编码后:" + base64Text); // 使用Base64解码 String text2 = SaBase64Util.decode(base64Text); System.out.println("Base64解码后:" + text2); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SwitchToController.java ================================================ package com.pj.cases.up; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 身份切换 * * @author click33 * @since 2022-10-17 */ @RestController @RequestMapping("/SwitchTo/") public class SwitchToController { /* * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 */ // 身份切换 ---- http://localhost:8081/SwitchTo/switchTo?userId=10044 @RequestMapping("switchTo") public SaResult switchTo(long userId) { // 将当前会话 [身份临时切换] 为其它账号 // --- 切换的有效期为本次请求 StpUtil.switchTo(userId); // 此时再调用此方法会返回 10044 (我们临时切换到的账号id) System.out.println(StpUtil.getLoginId());; // 结束 [身份临时切换] StpUtil.endSwitch(); // 此时再打印账号,就又回到了原来的值:10001 System.out.println(StpUtil.getLoginId()); return SaResult.ok(); } // 以 lambda 表达式的方式身份切换 ---- http://localhost:8081/SwitchTo/switchTo2?userId=10044 @RequestMapping("switchTo2") public SaResult switchTo2(long userId) { // 输出 10001 System.out.println("------- [身份临时切换] 调用前,当前登录账号id是:" + StpUtil.getLoginId()); // 以 lambda 表达式的方式身份切换,作用范围只在这个 lambda 表达式内有效 StpUtil.switchTo(userId, () -> { System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); // 输出 true System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); // 输出 10044 }); // 结束后,再次获取当前登录账号,输出10001 System.out.println("------- [身份临时切换] 调用结束,当前登录账号id是:" + StpUtil.getLoginId()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/AtCheckController.java ================================================ package com.pj.cases.use; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 注解鉴权示例 * * @author click33 * @since 2022-10-13 */ @RestController @RequestMapping("/at-check/") public class AtCheckController { /* * 前提1:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 * * 前提2:项目在配置类中注册拦截器 SaInterceptor ,代码在 com.pj.satoken.SaTokenConfigure * 此拦截器将打开注解鉴权功能 * * 然后我们就可以使用以下示例中的代码进行注解鉴权了 */ // 登录鉴权 ---- http://localhost:8081/at-check/checkLogin // 登录认证后才可以进入方法 @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { // 通过注解鉴权后才能进入方法 ... return SaResult.ok(); } // 权限校验 ---- http://localhost:8081/at-check/checkPermission // 只有具有 user.add 权限的账号才可以进入方法 @SaCheckPermission("user.add") @RequestMapping("checkPermission") public SaResult checkPermission() { // ... return SaResult.ok(); } // 权限校验2 ---- http://localhost:8081/at-check/checkPermission2 // 一次性校验多个权限,必须全部拥有,才可以进入方法 @SaCheckPermission(value = {"user.add", "user.delete", "user.update"}, mode = SaMode.AND) @RequestMapping("checkPermission2") public SaResult checkPermission2() { // ... return SaResult.ok(); } // 权限校验3 ---- http://localhost:8081/at-check/checkPermission3 // 一次性校验多个权限,只要拥有其中一个,就可以进入方法 @SaCheckPermission(value = {"user.add", "user.delete", "user.update"}, mode = SaMode.OR) @RequestMapping("checkPermission3") public SaResult checkPermission3() { // ... return SaResult.ok(); } // 角色校验 ---- http://localhost:8081/at-check/checkRole // 只有具有 super-admin 角色的账号才可以进入方法 @SaCheckRole("super-admin") @RequestMapping("checkRole") public SaResult checkRole() { // ... return SaResult.ok(); } // 角色权限双重 “or校验” ---- http://localhost:8081/at-check/userAdd // 具备 "user.add"权限 或者 "admin"角色 即可通过校验 @RequestMapping("userAdd") @SaCheckPermission(value = "user.add", orRole = "admin") public SaResult userAdd() { return SaResult.data("用户信息"); } // 忽略校验 ---- http://localhost:8081/at-check/ignore // 使用 @SaIgnore 修饰的方法,无需任何校验即可进入,具体使用示例可参照在线文档 @SaIgnore @SaCheckLogin @RequestMapping("ignore") public SaResult ignore() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/JurAuthController.java ================================================ package com.pj.cases.use; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 权限认证示例 * * @author click33 * @since 2022-10-13 */ @RestController @RequestMapping("/jur/") public class JurAuthController { /* * 前提1:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 * * 前提2:项目实现 StpInterface 接口,代码在 com.pj.satoken.StpInterfaceImpl * Sa-Token 将从此实现类获取 每个账号拥有哪些权限。 * * 然后我们就可以使用以下示例中的代码进行鉴权了 */ // 查询权限 ---- http://localhost:8081/jur/getPermission @RequestMapping("getPermission") public SaResult getPermission() { // 查询权限信息 ,如果当前会话未登录,会返回一个空集合 List permissionList = StpUtil.getPermissionList(); System.out.println("当前登录账号拥有的所有权限:" + permissionList); // 查询角色信息 ,如果当前会话未登录,会返回一个空集合 List roleList = StpUtil.getRoleList(); System.out.println("当前登录账号拥有的所有角色:" + roleList); // 返回给前端 return SaResult.ok() .set("roleList", roleList) .set("permissionList", permissionList); } // 权限校验 ---- http://localhost:8081/jur/checkPermission @RequestMapping("checkPermission") public SaResult checkPermission() { // 判断:当前账号是否拥有一个权限,返回 true 或 false // 如果当前账号未登录,则永远返回 false StpUtil.hasPermission("user.add"); StpUtil.hasPermissionAnd("user.add", "user.delete", "user.get"); // 指定多个,必须全部拥有才会返回 true StpUtil.hasPermissionOr("user.add", "user.delete", "user.get"); // 指定多个,只要拥有一个就会返回 true // 校验:当前账号是否拥有一个权限,校验不通过时会抛出 `NotPermissionException` 异常 // 如果当前账号未登录,则永远校验失败 StpUtil.checkPermission("user.add"); StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); // 指定多个,必须全部拥有才会校验通过 StpUtil.checkPermissionOr("user.add", "user.delete", "user.get"); // 指定多个,只要拥有一个就会校验通过 return SaResult.ok(); } // 角色校验 ---- http://localhost:8081/jur/checkRole @RequestMapping("checkRole") public SaResult checkRole() { // 判断:当前账号是否拥有一个角色,返回 true 或 false // 如果当前账号未登录,则永远返回 false StpUtil.hasRole("admin"); StpUtil.hasRoleAnd("admin", "ceo", "cfo"); // 指定多个,必须全部拥有才会返回 true StpUtil.hasRoleOr("admin", "ceo", "cfo"); // 指定多个,只要拥有一个就会返回 true // 校验:当前账号是否拥有一个角色,校验不通过时会抛出 `NotRoleException` 异常 // 如果当前账号未登录,则永远校验失败 StpUtil.checkRole("admin"); StpUtil.checkRoleAnd("admin", "ceo", "cfo"); // 指定多个,必须全部拥有才会校验通过 StpUtil.checkRoleOr("admin", "ceo", "cfo"); // 指定多个,只要拥有一个就会校验通过 return SaResult.ok(); } // 权限通配符 ---- http://localhost:8081/jur/wildcardPermission @RequestMapping("wildcardPermission") public SaResult wildcardPermission() { // 前提条件:在 StpInterface 实现类中,为账号返回了 "art.*" 泛权限 StpUtil.hasPermission("art.add"); // 返回 true StpUtil.hasPermission("art.delete"); // 返回 true StpUtil.hasPermission("goods.add"); // 返回 false,因为前缀不符合 // * 符合可以出现在任意位置,比如权限码的开头,当账号拥有 "*.delete" 时 StpUtil.hasPermission("goods.add"); // false StpUtil.hasPermission("goods.delete"); // true StpUtil.hasPermission("art.delete"); // true // 也可以出现在权限码的中间,比如当账号拥有 "shop.*.user" 时 StpUtil.hasPermission("shop.add.user"); // true StpUtil.hasPermission("shop.delete.user"); // true StpUtil.hasPermission("shop.delete.goods"); // false,因为后缀不符合 // 注意点: // 1、上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 // 2、角色校验也可以加 * ,指定泛角色,例如: "*.admin",暂不赘述 return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/KickoutController.java ================================================ package com.pj.cases.use; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 权限认证示例 * * @author click33 * @since 2022-10-13 */ @RestController @RequestMapping("/kickout/") public class KickoutController { /* * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 */ // 将指定账号强制注销 ---- http://localhost:8081/kickout/logout?userId=10001 @RequestMapping("logout") public SaResult logout(long userId) { // 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。 StpUtil.logout(userId); // 返回 return SaResult.ok(); } // 将指定账号踢下线 ---- http://localhost:8081/kickout/kickout?userId=10001 @RequestMapping("kickout") public SaResult kickout(long userId) { // 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。 StpUtil.kickout(userId); // 返回 return SaResult.ok(); } /* * 你可以分别在强制注销和踢人下线后,再次访问一下登录校验接口,对比一下两者返回的提示信息有何不同 * ---- http://localhost:8081/acc/checkLogin */ // 根据 Token 值踢人 ---- http://localhost:8081/kickout/kickoutByTokenValue?tokenValue=xxxx-xxxx-xxxx-xxxx已登录账号的token值 @RequestMapping("kickoutByTokenValue") public SaResult kickoutByTokenValue(String tokenValue) { StpUtil.kickoutByTokenValue(tokenValue); // 返回 return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/LoginAuthController.java ================================================ package com.pj.cases.use; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token 登录认证示例 * * @author click33 * @since 2022-10-13 */ @RestController @RequestMapping("/acc/") public class LoginAuthController { // 会话登录接口 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录 // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { // 第二步:根据账号id,进行登录 // 此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象 StpUtil.login(10001); // SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述 return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询当前登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false boolean isLogin = StpUtil.isLogin(); return SaResult.ok("当前客户端是否登录:" + isLogin); } // 校验当前登录状态 ---- http://localhost:8081/acc/checkLogin @RequestMapping("checkLogin") public SaResult checkLogin() { // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException` StpUtil.checkLogin(); // 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息 return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息"); } // 获取当前登录的账号是谁 ---- http://localhost:8081/acc/getLoginId @RequestMapping("getLoginId") public SaResult getLoginId() { // 需要注意的是,StpUtil.getLoginId() 自带登录校验效果 // 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的 Object userId = StpUtil.getLoginId(); System.out.println("当前登录的账号id是:" + userId); // 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值 // 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id Object userId2 = StpUtil.getLoginId(0); System.out.println("当前登录的账号id是:" + userId2); // 或者使其在未登录的时候返回 null Object userId3 = StpUtil.getLoginIdDefaultNull(); System.out.println("当前登录的账号id是:" + userId3); // 类型转换: // StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型 int userId4 = StpUtil.getLoginIdAsInt(); // 将返回值转换为 int 类型 long userId5 = StpUtil.getLoginIdAsLong(); // 将返回值转换为 long 类型 String userId6 = StpUtil.getLoginIdAsString(); // 将返回值转换为 String 类型 // 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换? // 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6); // 返回给前端 return SaResult.ok("当前客户端登录的账号id是:" + userId); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { // TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称 String tokenName = StpUtil.getTokenName(); System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName); // 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值 // 框架默认前端可以从以下三个途径中提交 Token: // Cookie (浏览器自动提交) // Header头 (代码手动提交) // Query 参数 (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx // 读取顺序为: Query 参数 --> Header头 -- > Cookie // 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token String tokenValue = StpUtil.getTokenValue(); System.out.println("前端提交的Token值为:" + tokenValue); // TokenInfo 包含了此 Token 的大多数信息 SaTokenInfo info = StpUtil.getTokenInfo(); System.out.println("Token 名称:" + info.getTokenName()); System.out.println("Token 值:" + info.getTokenValue()); System.out.println("当前是否登录:" + info.getIsLogin()); System.out.println("当前登录的账号id:" + info.getLoginId()); System.out.println("当前登录账号的类型:" + info.getLoginType()); System.out.println("当前登录客户端的设备类型:" + info.getLoginDeviceType()); System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 System.out.println("当前 Token 距离被冻结还剩:" + info.getTokenActiveTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 System.out.println("当前 Account-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在 // 返回给前端 return SaResult.data(StpUtil.getTokenInfo()); } // 会话注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { // 退出登录会清除三个地方的数据: // 1、Redis中保存的 Token 信息 // 2、当前请求上下文中保存的 Token 信息 // 3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除) StpUtil.logout(); // StpUtil.logout() 在未登录时也是可以调用成功的, // 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态 System.out.println("当前是否处于登录状态:" + StpUtil.isLogin()); // 返回给前端 return SaResult.ok("退出登录成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/RouterCheckController.java ================================================ package com.pj.cases.use; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 为路由拦截鉴权准备的路示例 * * @author click33 * @since 2022-10-15 */ @RestController public class RouterCheckController { // 路由拦截鉴权测试 ---- http://localhost:8081/xxx @RequestMapping({ "/user/doLogin", "/user/doLogin2", "/user/info", "/admin/info", "/goods/info", "/orders/info", "/notice/info", "/comment/info", "/router/print", "/router/print2" }) public SaResult checkLogin() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/SaSessionController.java ================================================ package com.pj.cases.use; import java.util.Arrays; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.model.SysUser; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaSessionCustomUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * Sa-Token Session会话示例 * * @author click33 * @since 2022-10-15 */ @RestController @RequestMapping("/session/") public class SaSessionController { /* * 前提:首先调用登录接口进行登录,代码在 com.pj.cases.use.LoginAuthController 中有详细解释,此处不再赘述 * ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 */ // 简单存取值 ---- http://localhost:8081/session/getValue @RequestMapping("getValue") public SaResult getValue() { // 获取当前登录账号的专属 SaSession 对象 // 注意点1:只有登录后才可以调用这个方法 // 注意点2:每个账号获取到的都是不同的 SaSession 对象,存取值时不会互相影响 // 注意点3:SaSession 和 HttpSession 是两个完全不同的对象,不可混淆使用 SaSession session = StpUtil.getSession(); // 存值 session.set("name", "zhangsan"); session.set("age", 18); // 取值 Object name = session.get("name"); String name2 = session.getString("name"); // 取值,并转化为 String 数据类型 int age = session.getInt("age"); // 转 int 类型 long age2 = session.getLong("age"); // 转 long 类型 float age3 = session.getFloat("age"); // 转 float 类型 double age4 = session.getDouble("age"); // 转 double 类型 int age5 = session.get("age5", 22); // 取不到时就返回默认值 int age6 = session.get("age5", () -> { // 取不到时就执行 lambda 获取值 return 26; }); /* * 存取值范围是一次会话有效的,也就是说,在一次登录有效期内,你可以在一个请求里存值,然后在另一个请求里取值 */ List list = Arrays.asList(name, name2, age, age2, age3, age4, age5, age6); System.out.println(list); return SaResult.data(list); } // 复杂存取值 ---- http://localhost:8081/session/getModel @RequestMapping("getModel") public SaResult getModel() { // 实例化 SysUser user = new SysUser(); user.setId(10001); user.setName("张三"); user.setAge(19); // 写入这个对象到 SaSession 中 StpUtil.getSession().set("user", user); // 然后我们就可以在任意代码处获取这个 user 了 SysUser user2 = StpUtil.getSession().getModel("user", SysUser.class); // 返回 return SaResult.data(user2); } // 自定义Session ---- http://localhost:8081/session/customSession @RequestMapping("customSession") public SaResult customSession() { // 自定义 Session 就是指使用一个特定的 key,来获取 Session 对象 SaSession roleSession = SaSessionCustomUtil.getSessionById("role-1001"); // 一样可以自由的存值写值 roleSession.set("nnn", "lalala"); System.out.println(roleSession.get("nnn")); // 返回 return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotHttpBasicAuthException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import cn.dev33.satoken.exception.NotSafeException; import cn.dev33.satoken.util.SaResult; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 拦截:未登录异常 @ExceptionHandler(NotLoginException.class) public SaResult handlerException(NotLoginException e) { // 打印堆栈,以供调试 e.printStackTrace(); // 返回给前端 return SaResult.error(e.getMessage()); } // 拦截:缺少权限异常 @ExceptionHandler(NotPermissionException.class) public SaResult handlerException(NotPermissionException e) { e.printStackTrace(); return SaResult.error("缺少权限:" + e.getPermission()); } // 拦截:缺少角色异常 @ExceptionHandler(NotRoleException.class) public SaResult handlerException(NotRoleException e) { e.printStackTrace(); return SaResult.error("缺少角色:" + e.getRole()); } // 拦截:二级认证校验失败异常 @ExceptionHandler(NotSafeException.class) public SaResult handlerException(NotSafeException e) { e.printStackTrace(); return SaResult.error("二级认证校验失败:" + e.getService()); } // 拦截:服务封禁异常 @ExceptionHandler(DisableServiceException.class) public SaResult handlerException(DisableServiceException e) { e.printStackTrace(); return SaResult.error("当前账号 " + e.getService() + " 服务已被封禁 (level=" + e.getLevel() + "):" + e.getDisableTime() + "秒后解封"); } // 拦截:Http Basic 校验失败异常 @ExceptionHandler(NotHttpBasicAuthException.class) public SaResult handlerException(NotHttpBasicAuthException e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } // 拦截:其它所有异常 @ExceptionHandler(Exception.class) public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/model/SysUser.java ================================================ package com.pj.model; import java.io.Serializable; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser implements Serializable { /** * */ private static final long serialVersionUID = -2853125262828437774L; public SysUser() { } public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } @Override public String toString() { return "SysUser [id=" + id + ", name=" + name + ", age=" + age + "]"; } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTempTemplate.java ================================================ package com.pj.satoken; import cn.dev33.satoken.temp.SaTempTemplate; /** * 自定义临时 token 认证组件子类 * * @author click33 * @since 2025/4/9 */ //@Component public class MySaTempTemplate extends SaTempTemplate { @Override public String createToken(Object value, long timeout, boolean isRecordIndex) { System.out.println("------- 自定义一些逻辑 createToken "); return super.createToken(value, timeout, isRecordIndex); } @Override public Object parseToken(String token) { System.out.println("------- 自定义一些逻辑 parseToken "); return super.parseToken(token); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTokenListener.java ================================================ package com.pj.satoken; import cn.dev33.satoken.listener.SaTokenListener; import cn.dev33.satoken.stp.parameter.SaLoginParameter; /** * Sa-Token 自定义侦听器的实现 * * @author click33 * @since 2022-10-17 */ //@Component // 打开此注解,让 SpringBoot 扫描到组件,即可完成自定义侦听器的注入 public class MySaTokenListener implements SaTokenListener { /** 每次登录时触发 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { System.out.println("---------- 自定义侦听器实现 doLogin"); } /** 每次注销时触发 */ @Override public void doLogout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doLogout"); } /** 每次被踢下线时触发 */ @Override public void doKickout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doKickout"); } /** 每次被顶下线时触发 */ @Override public void doReplaced(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doReplaced"); } /** 每次被封禁时触发 */ @Override public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { System.out.println("---------- 自定义侦听器实现 doDisable"); } /** 每次被解封时触发 */ @Override public void doUntieDisable(String loginType, Object loginId, String service) { System.out.println("---------- 自定义侦听器实现 doUntieDisable"); } /** 每次打开二级认证时触发 */ @Override public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { System.out.println("---------- 自定义侦听器实现 doOpenSafe"); } /** 每次关闭二级认证时触发 */ @Override public void doCloseSafe(String loginType, String tokenValue, String service) { System.out.println("---------- 自定义侦听器实现 doCloseSafe"); } /** 每次创建Session时触发 */ @Override public void doCreateSession(String id) { System.out.println("---------- 自定义侦听器实现 doCreateSession"); } /** 每次注销Session时触发 */ @Override public void doLogoutSession(String id) { System.out.println("---------- 自定义侦听器实现 doLogoutSession"); } /** 每次Token续期时触发 */ @Override public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaLogForSlf4j.java ================================================ package com.pj.satoken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import cn.dev33.satoken.log.SaLog; /** * 将 Sa-Token log 信息转接到 Slf4j * * @author click33 * @since 2022-11-2 */ //@Component public class SaLogForSlf4j implements SaLog { Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); @Override public void trace(String str, Object... args) { log.trace(str, args); } @Override public void debug(String str, Object... args) { log.debug(str, args); } @Override public void info(String str, Object... args) { log.info(str, args); } @Override public void warn(String str, Object... args) { log.trace(str, args); } @Override public void error(String str, Object... args) { log.error(str, args); } @Override public void fatal(String str, Object... args) { log.error(str, args); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.PostConstruct; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor(handle -> { // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); // 指定一条 match 规则 SaRouter .match("/user/**") // 拦截的 path 列表,可以写多个 .notMatch("/user/doLogin", "/user/doLogin2") // 排除掉的 path 列表,可以写多个 .check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式 // 权限校验 -- 不同模块认证不同权限 SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); // 甚至你可以随意的写一个打印语句 SaRouter.match("/router/print", r -> System.out.println("----啦啦啦----")); // 写一个完整的 lambda SaRouter.match("/router/print2", r -> { System.out.println("----啦啦啦2----"); // ... 其它代码 }); /* * 相关路由都定义在 com.pj.cases.use.RouterCheckController 中 */ })).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); // 权限校验 -- 不同模块认证不同权限 // 这里你可以写和拦截器鉴权同样的代码,不同点在于: // 校验失败后不会进入全局异常组件,而是进入下面的 .setError 函数 SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } /** * CORS 跨域处理 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } /** * 重写 Sa-Token 框架内部算法策略 */ @PostConstruct public void rewriteSaStrategy() { // 重写Sa-Token的注解处理器,增加注解合并功能 SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> { return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); }; // 重写 SaCheckELRootMap 扩展函数,增加注解鉴权 EL 表达式可使用的根对象 SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> { System.out.println("--------- 执行 SaCheckELRootMap 增强,目前已包含的的跟对象包括:" + rootMap.keySet()); // 新增 stpUser 根对象,使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权 rootMap.put("stpUser", StpUserUtil.getStpLogic()); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import cn.dev33.satoken.stp.StpInterface; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码 * * @author click33 * @since 2022-10-13 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user.add"); list.add("user.update"); list.add("user.get"); // list.add("user.delete"); list.add("art.*"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java ================================================ package com.pj.satoken; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaTwoParamFunction; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import java.util.List; /** * 【User账号体系】Sa-Token 权限认证工具类 * * @author click33 * @since 1.0.0 */ public class StpUserUtil { private StpUserUtil() {} /** * 多账号体系下的类型标识 */ public static final String TYPE = "user"; /** * 底层使用的 StpLogic 对象 */ public static StpLogic stpLogic = new StpLogic(TYPE); /** * 获取当前 StpLogic 的账号类型 * * @return / */ public static String getLoginType(){ return stpLogic.getLoginType(); } /** * 安全的重置 StpLogic 对象 * *
1、更改此账户的 StpLogic 对象 *
2、put 到全局 StpLogic 集合中 *
3、发送日志 * * @param newStpLogic / */ public static void setStpLogic(StpLogic newStpLogic) { // 1、重置此账户的 StpLogic 对象 stpLogic = newStpLogic; // 2、添加到全局 StpLogic 集合中 // 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic SaManager.putStpLogic(newStpLogic); // 3、$$ 发布事件:更新了 stpLogic 对象 SaTokenEventCenter.doSetStpLogic(stpLogic); } /** * 获取 StpLogic 对象 * * @return / */ public static StpLogic getStpLogic() { return stpLogic; } // ------------------- 获取 token 相关 ------------------- /** * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 * * @return / */ public static String getTokenName() { return stpLogic.getTokenName(); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 */ public static void setTokenValue(String tokenValue){ stpLogic.setTokenValue(tokenValue); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param cookieTimeout Cookie存活时间(秒) */ public static void setTokenValue(String tokenValue, int cookieTimeout){ stpLogic.setTokenValue(tokenValue, cookieTimeout); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param loginParameter 登录参数 */ public static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ stpLogic.setTokenValue(tokenValue, loginParameter); } /** * 获取当前请求的 token 值 * * @return 当前tokenValue */ public static String getTokenValue() { return stpLogic.getTokenValue(); } /** * 获取当前请求的 token 值 (不裁剪前缀) * * @return / */ public static String getTokenValueNotCut(){ return stpLogic.getTokenValueNotCut(); } /** * 获取当前会话的 token 参数信息 * * @return token 参数信息 */ public static SaTokenInfo getTokenInfo() { return stpLogic.getTokenInfo(); } // ------------------- 登录相关操作 ------------------- // --- 登录 /** * 会话登录 * * @param id 账号id,建议的类型:(long | int | String) */ public static void login(Object id) { stpLogic.login(id); } /** * 会话登录,并指定登录设备类型 * * @param id 账号id,建议的类型:(long | int | String) * @param deviceType 设备类型 */ public static void login(Object id, String deviceType) { stpLogic.login(id, deviceType); } /** * 会话登录,并指定是否 [记住我] * * @param id 账号id,建议的类型:(long | int | String) * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public static void login(Object id, boolean isLastingCookie) { stpLogic.login(id, isLastingCookie); } /** * 会话登录,并指定此次登录 token 的有效期, 单位:秒 * * @param id 账号id,建议的类型:(long | int | String) * @param timeout 此次登录 token 的有效期, 单位:秒 */ public static void login(Object id, long timeout) { stpLogic.login(id, timeout); } /** * 会话登录,并指定所有登录参数 Model * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model */ public static void login(Object id, SaLoginParameter loginParameter) { stpLogic.login(id, loginParameter); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public static String createLoginSession(Object id) { return stpLogic.createLoginSession(id); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model * @return 返回会话令牌 */ public static String createLoginSession(Object id, SaLoginParameter loginParameter) { return stpLogic.createLoginSession(id, loginParameter); } /** * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public static String getOrCreateLoginSession(Object id) { return stpLogic.getOrCreateLoginSession(id); } // --- 注销 (根据 token) /** * 在当前客户端会话注销 */ public static void logout() { stpLogic.logout(); } /** * 在当前客户端会话注销,根据注销参数 */ public static void logout(SaLogoutParameter logoutParameter) { stpLogic.logout(logoutParameter); } /** * 注销下线,根据指定 token * * @param tokenValue 指定 token */ public static void logoutByTokenValue(String tokenValue) { stpLogic.logoutByTokenValue(tokenValue); } /** * 注销下线,根据指定 token、注销参数 * * @param tokenValue 指定 token * @param logoutParameter / */ public static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.logoutByTokenValue(tokenValue, logoutParameter); } /** * 踢人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token */ public static void kickoutByTokenValue(String tokenValue) { stpLogic.kickoutByTokenValue(tokenValue); } /** * 踢人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token * @param logoutParameter 注销参数 */ public static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.kickoutByTokenValue(tokenValue, logoutParameter); } /** * 顶人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token */ public static void replacedByTokenValue(String tokenValue) { stpLogic.replacedByTokenValue(tokenValue); } /** * 顶人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token * @param logoutParameter / */ public static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.replacedByTokenValue(tokenValue, logoutParameter); } // --- 注销 (根据 loginId) /** * 会话注销,根据账号id * * @param loginId 账号id */ public static void logout(Object loginId) { stpLogic.logout(loginId); } /** * 会话注销,根据账号id 和 设备类型 * * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) */ public static void logout(Object loginId, String deviceType) { stpLogic.logout(loginId, deviceType); } /** * 会话注销,根据账号id 和 注销参数 * * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void logout(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.logout(loginId, logoutParameter); } /** * 踢人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id */ public static void kickout(Object loginId) { stpLogic.kickout(loginId); } /** * 踢人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) */ public static void kickout(Object loginId, String deviceType) { stpLogic.kickout(loginId, deviceType); } /** * 踢人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void kickout(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.kickout(loginId, logoutParameter); } /** * 顶人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id */ public static void replaced(Object loginId) { stpLogic.replaced(loginId); } /** * 顶人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) */ public static void replaced(Object loginId, String deviceType) { stpLogic.replaced(loginId, deviceType); } /** * 顶人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void replaced(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.replaced(loginId, logoutParameter); } // --- 注销 (会话管理辅助方法) /** * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) * @param session / * @param terminal / */ public static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByLogout(session, terminal); } /** * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) * @param session / * @param terminal / */ public static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByKickout(session, terminal); } /** * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) * @param session / * @param terminal / */ public static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByReplaced(session, terminal); } // 会话查询 /** * 判断当前会话是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public static boolean isLogin() { return stpLogic.isLogin(); } /** * 判断指定账号是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public static boolean isLogin(Object loginId) { return stpLogic.isLogin(loginId); } /** * 检验当前会话是否已经登录,如未登录,则抛出异常 */ public static void checkLogin() { stpLogic.checkLogin(); } /** * 获取当前会话账号id,如果未登录,则抛出异常 * * @return 账号id */ public static Object getLoginId() { return stpLogic.getLoginId(); } /** * 获取当前会话账号id, 如果未登录,则返回默认值 * * @param 返回类型 * @param defaultValue 默认值 * @return 登录id */ public static T getLoginId(T defaultValue) { return stpLogic.getLoginId(defaultValue); } /** * 获取当前会话账号id, 如果未登录,则返回null * * @return 账号id */ public static Object getLoginIdDefaultNull() { return stpLogic.getLoginIdDefaultNull(); } /** * 获取当前会话账号id, 并转换为 String 类型 * * @return 账号id */ public static String getLoginIdAsString() { return stpLogic.getLoginIdAsString(); } /** * 获取当前会话账号id, 并转换为 int 类型 * * @return 账号id */ public static int getLoginIdAsInt() { return stpLogic.getLoginIdAsInt(); } /** * 获取当前会话账号id, 并转换为 long 类型 * * @return 账号id */ public static long getLoginIdAsLong() { return stpLogic.getLoginIdAsLong(); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null * * @param tokenValue token * @return 账号id */ public static Object getLoginIdByToken(String tokenValue) { return stpLogic.getLoginIdByToken(tokenValue); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null * * @param tokenValue token * @return 账号id */ public Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { return stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue); } /** * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) * * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String key) { return stpLogic.getExtra(key); } /** * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) * * @param tokenValue 指定的 Token 值 * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String tokenValue, String key) { return stpLogic.getExtra(tokenValue, key); } // ------------------- Account-Session 相关 ------------------- /** * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param loginId 账号id * @param isCreate 是否新建 * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return stpLogic.getSessionByLoginId(loginId, isCreate); } /** * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null * * @param sessionId SessionId * @return Session对象 */ public static SaSession getSessionBySessionId(String sessionId) { return stpLogic.getSessionBySessionId(sessionId); } /** * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param loginId 账号id * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId) { return stpLogic.getSessionByLoginId(loginId); } /** * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param isCreate 是否新建 * @return Session对象 */ public static SaSession getSession(boolean isCreate) { return stpLogic.getSession(isCreate); } /** * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public static SaSession getSession() { return stpLogic.getSession(); } // ------------------- Token-Session 相关 ------------------- /** * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param tokenValue Token值 * @return Session对象 */ public static SaSession getTokenSessionByToken(String tokenValue) { return stpLogic.getTokenSessionByToken(tokenValue); } /** * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public static SaSession getTokenSession() { return stpLogic.getTokenSession(); } /** * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) * * @return Token-Session 对象 */ public static SaSession getAnonTokenSession() { return stpLogic.getAnonTokenSession(); } // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- /** * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) *

* 请注意: 即使 token 已被冻结 也可续签成功, * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 *

*/ public static void updateLastActiveToNow() { stpLogic.updateLastActiveToNow(); } /** * 检查当前 token 是否已被冻结,如果是则抛出异常 */ public static void checkActiveTimeout() { stpLogic.checkActiveTimeout(); } // ------------------- 过期时间相关 ------------------- /** * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getTokenTimeout() { return stpLogic.getTokenTimeout(); } /** * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param token 指定token * @return token剩余有效时间 */ public static long getTokenTimeout(String token) { return stpLogic.getTokenTimeout(token); } /** * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getSessionTimeout() { return stpLogic.getSessionTimeout(); } /** * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getTokenSessionTimeout() { return stpLogic.getTokenSessionTimeout(); } /** * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) * * @return / */ public static long getTokenActiveTimeout() { return stpLogic.getTokenActiveTimeout(); } /** * 对当前 token 的 timeout 值进行续期 * * @param timeout 要修改成为的有效时间 (单位: 秒) */ public static void renewTimeout(long timeout) { stpLogic.renewTimeout(timeout); } /** * 对指定 token 的 timeout 值进行续期 * * @param tokenValue 指定 token * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) */ public static void renewTimeout(String tokenValue, long timeout) { stpLogic.renewTimeout(tokenValue, timeout); } // ------------------- 角色认证操作 ------------------- /** * 获取:当前账号的角色集合 * * @return / */ public static List getRoleList() { return stpLogic.getRoleList(); } /** * 获取:指定账号的角色集合 * * @param loginId 指定账号id * @return / */ public static List getRoleList(Object loginId) { return stpLogic.getRoleList(loginId); } /** * 判断:当前账号是否拥有指定角色, 返回 true 或 false * * @param role 角色 * @return / */ public static boolean hasRole(String role) { return stpLogic.hasRole(role); } /** * 判断:指定账号是否含有指定角色标识, 返回 true 或 false * * @param loginId 账号id * @param role 角色标识 * @return 是否含有指定角色标识 */ public static boolean hasRole(Object loginId, String role) { return stpLogic.hasRole(loginId, role); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 * @return true或false */ public static boolean hasRoleAnd(String... roleArray){ return stpLogic.hasRoleAnd(roleArray); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 * @return true或false */ public static boolean hasRoleOr(String... roleArray){ return stpLogic.hasRoleOr(roleArray); } /** * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException * * @param role 角色标识 */ public static void checkRole(String role) { stpLogic.checkRole(role); } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 */ public static void checkRoleAnd(String... roleArray){ stpLogic.checkRoleAnd(roleArray); } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 */ public static void checkRoleOr(String... roleArray){ stpLogic.checkRoleOr(roleArray); } // ------------------- 权限认证操作 ------------------- /** * 获取:当前账号的权限码集合 * * @return / */ public static List getPermissionList() { return stpLogic.getPermissionList(); } /** * 获取:指定账号的权限码集合 * * @param loginId 指定账号id * @return / */ public static List getPermissionList(Object loginId) { return stpLogic.getPermissionList(loginId); } /** * 判断:当前账号是否含有指定权限, 返回 true 或 false * * @param permission 权限码 * @return 是否含有指定权限 */ public static boolean hasPermission(String permission) { return stpLogic.hasPermission(permission); } /** * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false * * @param loginId 账号 id * @param permission 权限码 * @return 是否含有指定权限 */ public static boolean hasPermission(Object loginId, String permission) { return stpLogic.hasPermission(loginId, permission); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public static boolean hasPermissionAnd(String... permissionArray){ return stpLogic.hasPermissionAnd(permissionArray); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public static boolean hasPermissionOr(String... permissionArray){ return stpLogic.hasPermissionOr(permissionArray); } /** * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException * * @param permission 权限码 */ public static void checkPermission(String permission) { stpLogic.checkPermission(permission); } /** * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] * * @param permissionArray 权限码数组 */ public static void checkPermissionAnd(String... permissionArray) { stpLogic.checkPermissionAnd(permissionArray); } /** * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 */ public static void checkPermissionOr(String... permissionArray) { stpLogic.checkPermissionOr(permissionArray); } // ------------------- id 反查 token 相关操作 ------------------- /** * 获取指定账号 id 的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @return token值 */ public static String getTokenValueByLoginId(Object loginId) { return stpLogic.getTokenValueByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return token值 */ public static String getTokenValueByLoginId(Object loginId, String deviceType) { return stpLogic.getTokenValueByLoginId(loginId, deviceType); } /** * 获取指定账号 id 的 token 集合 * * @param loginId 账号id * @return 此 loginId 的所有相关 token */ public static List getTokenValueListByLoginId(Object loginId) { return stpLogic.getTokenValueListByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的 token 集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public static List getTokenValueListByLoginId(Object loginId, String deviceType) { return stpLogic.getTokenValueListByLoginId(loginId, deviceType); } /** * 获取指定账号 id 已登录设备信息集合 * * @param loginId 账号id * @return 此 loginId 的所有登录 token */ public static List getTerminalListByLoginId(Object loginId) { return stpLogic.getTerminalListByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的已登录设备信息集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return / */ public static List getTerminalListByLoginId(Object loginId, String deviceType) { return stpLogic.getTerminalListByLoginId(loginId, deviceType); } /** * 获取指定账号 id 已登录设备信息集合,执行特定函数 * * @param loginId 账号id * @param function 需要执行的函数 */ public static void forEachTerminalList(Object loginId, SaTwoParamFunction function) { stpLogic.forEachTerminalList(loginId, function); } /** * 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ public static String getLoginDeviceType() { return stpLogic.getLoginDeviceType(); } /** * 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ public static String getLoginDeviceTypeByToken(String tokenValue) { return stpLogic.getLoginDeviceTypeByToken(tokenValue); } /** * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 * * @return / */ public static long getTokenLastActiveTime() { return stpLogic.getTokenLastActiveTime(); } /** * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 * @param deviceId / * @return / */ public static boolean isTrustDeviceId(Object userId, String deviceId) { return stpLogic.isTrustDeviceId(userId, deviceId); } // ------------------- 会话管理 ------------------- /** * 根据条件查询缓存中所有的 token * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return token集合 */ public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { return stpLogic.searchTokenValue(keyword, start, size, sortType); } /** * 根据条件查询缓存中所有的 SessionId * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public static List searchSessionId(String keyword, int start, int size, boolean sortType) { return stpLogic.searchSessionId(keyword, start, size, sortType); } /** * 根据条件查询缓存中所有的 Token-Session-Id * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { return stpLogic.searchTokenSessionId(keyword, start, size, sortType); } // ------------------- 账号封禁 ------------------- /** * 封禁:指定账号 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, long time) { stpLogic.disable(loginId, time); } /** * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) * * @param loginId 账号id * @return / */ public static boolean isDisable(Object loginId) { return stpLogic.isDisable(loginId); } /** * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id */ public static void checkDisable(Object loginId) { stpLogic.checkDisable(loginId); } /** * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @return / */ public static long getDisableTime(Object loginId) { return stpLogic.getDisableTime(loginId); } /** * 解封:指定账号 * * @param loginId 账号id */ public static void untieDisable(Object loginId) { stpLogic.untieDisable(loginId); } // ------------------- 分类封禁 ------------------- /** * 封禁:指定账号的指定服务 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param service 指定服务 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, String service, long time) { stpLogic.disable(loginId, service, time); } /** * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return / */ public static boolean isDisable(Object loginId, String service) { return stpLogic.isDisable(loginId, service); } /** * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void checkDisable(Object loginId, String... services) { stpLogic.checkDisable(loginId, services); } /** * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return see note */ public static long getDisableTime(Object loginId, String service) { return stpLogic.getDisableTime(loginId, service); } /** * 解封:指定账号、指定服务 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void untieDisable(Object loginId, String... services) { stpLogic.untieDisable(loginId, services); } // ------------------- 阶梯封禁 ------------------- /** * 封禁:指定账号,并指定封禁等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, int level, long time) { stpLogic.disableLevel(loginId, level, time); } /** * 封禁:指定账号的指定服务,并指定封禁等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, String service, int level, long time) { stpLogic.disableLevel(loginId, service, level, time); } /** * 判断:指定账号是否已被封禁到指定等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, int level) { return stpLogic.isDisableLevel(loginId, level); } /** * 判断:指定账号的指定服务,是否已被封禁到指定等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, String service, int level) { return stpLogic.isDisableLevel(loginId, service, level); } /** * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, int level) { stpLogic.checkDisableLevel(loginId, level); } /** * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, String service, int level) { stpLogic.checkDisableLevel(loginId, service, level); } /** * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @return / */ public static int getDisableLevel(Object loginId) { return stpLogic.getDisableLevel(loginId); } /** * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @param service 指定封禁服务 * @return / */ public static int getDisableLevel(Object loginId, String service) { return stpLogic.getDisableLevel(loginId, service); } // ------------------- 临时身份切换 ------------------- /** * 临时切换身份为指定账号id * * @param loginId 指定loginId */ public static void switchTo(Object loginId) { stpLogic.switchTo(loginId); } /** * 结束临时切换身份 */ public static void endSwitch() { stpLogic.endSwitch(); } /** * 判断当前请求是否正处于 [ 身份临时切换 ] 中 * * @return / */ public static boolean isSwitch() { return stpLogic.isSwitch(); } /** * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 * * @param loginId 指定账号id * @param function 要执行的方法 */ public static void switchTo(Object loginId, SaFunction function) { stpLogic.switchTo(loginId, function); } // ------------------- 二级认证 ------------------- /** * 在当前会话 开启二级认证 * * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(long safeTime) { stpLogic.openSafe(safeTime); } /** * 在当前会话 开启二级认证 * * @param service 业务标识 * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(String service, long safeTime) { stpLogic.openSafe(service, safeTime); } /** * 判断:当前会话是否处于二级认证时间内 * * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe() { return stpLogic.isSafe(); } /** * 判断:当前会话 是否处于指定业务的二级认证时间内 * * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String service) { return stpLogic.isSafe(service); } /** * 判断:指定 token 是否处于二级认证时间内 * * @param tokenValue Token 值 * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String tokenValue, String service) { return stpLogic.isSafe(tokenValue, service); } /** * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 */ public static void checkSafe() { stpLogic.checkSafe(); } /** * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 * * @param service 业务标识 */ public static void checkSafe(String service) { stpLogic.checkSafe(service); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @return 剩余有效时间 */ public static long getSafeTime() { return stpLogic.getSafeTime(); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @param service 业务标识 * @return 剩余有效时间 */ public static long getSafeTime(String service) { return stpLogic.getSafeTime(service); } /** * 在当前会话 结束二级认证 */ public static void closeSafe() { stpLogic.closeSafe(); } /** * 在当前会话 结束指定业务标识的二级认证 * * @param service 业务标识 */ public static void closeSafe(String service) { stpLogic.closeSafe(service); } // ------------------- Bean 对象、字段代理 ------------------- /** * 根据当前配置对象创建一个 SaLoginParameter 对象 * * @return / */ public static SaLoginParameter createSaLoginParameter() { return stpLogic.createSaLoginParameter(); } // ------------------- 过期方法 ------------------- /** *

请更换为 getLoginDeviceType

* 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ @Deprecated public static String getLoginDevice() { return stpLogic.getLoginDevice(); } /** *

请更换为 getLoginDeviceTypeByToken

* 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ @Deprecated public static String getLoginDeviceByToken(String tokenValue) { return stpLogic.getLoginDeviceByToken(tokenValue); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java ================================================ package com.pj.satoken.custom_annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 账号校验:在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 * * @author click33 * */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface CheckAccount { /** * 需要校验的账号 * * @return / */ String name(); /** * 需要校验的密码 * * @return / */ String pwd(); } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckLogin.java ================================================ package com.pj.satoken.custom_annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 登录认证(User版):只有登录之后才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) * * @author click33 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckLogin { } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckPermission.java ================================================ package com.pj.satoken.custom_annotation; import cn.dev33.satoken.annotation.SaMode; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限认证(User版):必须具有指定权限才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) * * @author click33 * */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckPermission { /** * 需要校验的权限码 * @return 需要校验的权限码 */ String [] value() default {}; /** * 验证模式:AND | OR,默认AND * @return 验证模式 */ SaMode mode() default SaMode.AND; /** * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 * *

* 例1:@SaCheckPermission(value="user-add", orRole="admin"), * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。 *

* *

* 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。
* 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。 *

* * @return / */ String[] orRole() default {}; } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckRole.java ================================================ package com.pj.satoken.custom_annotation; import cn.dev33.satoken.annotation.SaMode; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 角色认证(User版):必须具有指定角色标识才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) * @author click33 * */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckRole { /** * 需要校验的角色标识 * @return 需要校验的角色标识 */ String [] value() default {}; /** * 验证模式:AND | OR,默认AND * @return 验证模式 */ SaMode mode() default SaMode.AND; } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckSafe.java ================================================ package com.pj.satoken.custom_annotation; import cn.dev33.satoken.util.SaTokenConsts; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 二级认证校验(User版):客户端必须完成二级认证之后,才能进入该方法,否则将被抛出异常。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上)。 * * @author click33 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaUserCheckSafe { /** * 要校验的服务 * * @return / */ String value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE; } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java ================================================ package com.pj.satoken.custom_annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.exception.SaTokenException; import com.pj.satoken.custom_annotation.CheckAccount; import org.springframework.stereotype.Component; import java.lang.reflect.AnnotatedElement; /** * 注解 CheckAccount 的处理器 * * @author click33 * */ @Component public class CheckAccountHandler implements SaAnnotationHandlerInterface { // 指定这个处理器要处理哪个注解 @Override public Class getHandlerAnnotationClass() { return CheckAccount.class; } // 每次请求校验注解时,会执行的方法 @Override public void checkMethod(CheckAccount at, AnnotatedElement element) { // 获取前端请求提交的参数 String name = SaHolder.getRequest().getParamNotNull("name"); String pwd = SaHolder.getRequest().getParamNotNull("pwd"); // 与注解中指定的值相比较 if(name.equals(at.name()) && pwd.equals(at.pwd()) ) { // 校验通过,什么也不做 } else { // 校验不通过,则抛出异常 throw new SaTokenException("账号或密码错误,未通过校验"); } } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckLoginHandler.java ================================================ package com.pj.satoken.custom_annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.annotation.handler.SaCheckLoginHandler; import com.pj.satoken.StpUserUtil; import com.pj.satoken.custom_annotation.SaUserCheckLogin; import org.springframework.stereotype.Component; import java.lang.reflect.AnnotatedElement; /** * 注解 SaUserCheckLogin 的处理器 * * @author click33 */ @Component public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaUserCheckLogin.class; } @Override public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) { SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckPermissionHandler.java ================================================ package com.pj.satoken.custom_annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.annotation.handler.SaCheckPermissionHandler; import com.pj.satoken.StpUserUtil; import com.pj.satoken.custom_annotation.SaUserCheckPermission; import org.springframework.stereotype.Component; import java.lang.reflect.AnnotatedElement; /** * 注解 SaUserCheckPermission 的处理器 * * @author click33 */ @Component public class SaUserCheckPermissionHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaUserCheckPermission.class; } @Override public void checkMethod(SaUserCheckPermission at, AnnotatedElement element) { SaCheckPermissionHandler._checkMethod(StpUserUtil.TYPE, at.value(), at.mode(), at.orRole()); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckRoleHandler.java ================================================ package com.pj.satoken.custom_annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.annotation.handler.SaCheckRoleHandler; import com.pj.satoken.StpUserUtil; import com.pj.satoken.custom_annotation.SaUserCheckRole; import org.springframework.stereotype.Component; import java.lang.reflect.AnnotatedElement; /** * 注解 SaUserCheckRole 的处理器 * * @author click33 */ @Component public class SaUserCheckRoleHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaUserCheckRole.class; } @Override public void checkMethod(SaUserCheckRole at, AnnotatedElement element) { SaCheckRoleHandler._checkMethod(StpUserUtil.TYPE, at.value(), at.mode()); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckSafeHandler.java ================================================ package com.pj.satoken.custom_annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.annotation.handler.SaCheckSafeHandler; import com.pj.satoken.StpUserUtil; import com.pj.satoken.custom_annotation.SaUserCheckSafe; import org.springframework.stereotype.Component; import java.lang.reflect.AnnotatedElement; /** * 注解 SaUserCheckPermission 的处理器 * * @author click33 */ @Component public class SaUserCheckSafeHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaUserCheckSafe.class; } @Override public void checkMethod(SaUserCheckSafe at, AnnotatedElement element) { SaCheckSafeHandler._checkMethod(StpUserUtil.TYPE, at.value()); } } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java ================================================ package com.pj.satoken.merge_annotation; import cn.dev33.satoken.annotation.SaCheckLogin; import com.pj.satoken.StpUserUtil; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 登录认证(User版):只有登录之后才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) * @author click33 * */ @SaCheckLogin(type = StpUserUtil.TYPE) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckLogin { } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java ================================================ package com.pj.satoken.merge_annotation; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaMode; import com.pj.satoken.StpUserUtil; import org.springframework.core.annotation.AliasFor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 权限认证(User版):必须具有指定权限才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) * @author click33 * */ @SaCheckPermission(type = StpUserUtil.TYPE) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckPermission { /** * 需要校验的权限码 * @return 需要校验的权限码 */ @AliasFor(annotation = SaCheckPermission.class) String [] value() default {}; /** * 验证模式:AND | OR,默认AND * @return 验证模式 */ @AliasFor(annotation = SaCheckPermission.class) SaMode mode() default SaMode.AND; /** * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 * *

* 例1:@SaCheckPermission(value="user-add", orRole="admin"), * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。 *

* *

* 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。
* 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。 *

* * @return / */ @AliasFor(annotation = SaCheckPermission.class) String[] orRole() default {}; } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java ================================================ package com.pj.satoken.merge_annotation; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaMode; import com.pj.satoken.StpUserUtil; import org.springframework.core.annotation.AliasFor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 角色认证(User版):必须具有指定角色标识才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) * @author click33 * */ @SaCheckRole(type = StpUserUtil.TYPE) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckRole { /** * 需要校验的角色标识 * @return 需要校验的角色标识 */ @AliasFor(annotation = SaCheckRole.class) String [] value() default {}; /** * 验证模式:AND | OR,默认AND * @return 验证模式 */ @AliasFor(annotation = SaCheckRole.class) SaMode mode() default SaMode.AND; } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckSafe.java ================================================ package com.pj.satoken.merge_annotation; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.util.SaTokenConsts; import com.pj.satoken.StpUserUtil; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 二级认证校验(User版):客户端必须完成二级认证之后,才能进入该方法,否则将被抛出异常。 * *

可标注在方法、类上(效果等同于标注在此类的所有方法上)。 * * @author click33 */ @SaCheckSafe(type = StpUserUtil.TYPE) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaUserCheckSafe { /** * 要校验的服务 * * @return / */ String value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE; } ================================================ FILE: sa-token-demo/sa-token-demo-case/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true # jwt 秘钥 jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-device-lock 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 com.pj.SaTokenDeviceLockApplication org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/SaTokenDeviceLockApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 测试 * @author click33 * */ @SpringBootApplication public class SaTokenDeviceLockApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDeviceLockApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // 输出 API 请求日志,方便调试代码 // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 .setBeforeAuth(obj -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") // ---------- 设置跨域响应头 ---------- // 允许指定域访问跨域资源 .setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.util.DeviceLockCheckUtil; import com.pj.util.PhoneCodeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 登录测试 * * @author click33 */ @RestController @RequestMapping("/acc/") public class LoginController { @Autowired SysUserMockDao userMockDao; // 账号密码登录 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd, String deviceId) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { long userId = userMockDao.getUserIdByName(name); // 登录前,检测设备锁 if ( ! StpUtil.isTrustDeviceId(userId, deviceId)) { DeviceLockCheckUtil.setDeviceIdToUserId(deviceId, 10001); // 与前端约定好,返回421表示此设备需要验证 return SaResult.get(421, "新设备登录,需要验证设备", deviceId); } // 登录 return login(userId, deviceId); } return SaResult.error("登录失败"); } // 查询登录状态 @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.data(StpUtil.isLogin()); } // 注销登录 @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } // 返回设备id绑定的 userId 的手机号,脱敏形式 @RequestMapping("getPhone") public SaResult getPhone(String deviceId) { long userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId); String phone = userMockDao.getPhoneByUserId(userId); return SaResult.data(phone.substring(0, 3) + "****" + phone.substring(7)); } // 发送验证码 @RequestMapping("sendCode") public SaResult sendCode(String deviceId) { long userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId); String phone = userMockDao.getPhoneByUserId(userId); PhoneCodeUtil.sendCode(phone); return SaResult.ok(); } // 验证验证码 @RequestMapping("checkCode") public SaResult checkCode(String deviceId, String code) { long userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId); String phone = userMockDao.getPhoneByUserId(userId); PhoneCodeUtil.checkCode(phone, code); // 校验通过,开始登录 return login(userId, deviceId); } // 指定账号登录 private SaResult login(long userId, String deviceId) { StpUtil.login(userId, new SaLoginParameter().setDeviceId(deviceId)); return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/SysUserMockDao.java ================================================ package com.pj.test; import org.springframework.stereotype.Service; /** * 模拟数据库操作类 * * @author click33 * @since 2025/3/5 */ @Service public class SysUserMockDao { // 返回指定 userId 绑定的手机号 public String getPhoneByUserId(long userId) { return "13112341234"; } // 返回指定用户名对应的 userId public long getUserIdByName(String name) { return 10001; } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/DeviceLockCheckUtil.java ================================================ package com.pj.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.util.SaFoxUtil; /** * 设备锁操作工具类 * @author click33 * @since 2025/3/5 */ public class DeviceLockCheckUtil { /** * 保存设备id与用户id的映射关系 * @param deviceId / * @param userId / */ public static void setDeviceIdToUserId(String deviceId, long userId) { if(SaFoxUtil.isEmpty(deviceId) || SaFoxUtil.isEmpty(userId)) { throw new RuntimeException("设备id或用户id不能为空"); } SaManager.getSaTokenDao().set(saveKeyPrefix() + deviceId, String.valueOf(userId), 1200); } /** * 返回设备id绑定的用户id * @param deviceId / */ public static long getUserIdByDeviceId(String deviceId) { String userIdStr = SaManager.getSaTokenDao().get(saveKeyPrefix() + deviceId); if(userIdStr == null) { throw new RuntimeException("此设备id目前未绑定任何用户"); } return Long.parseLong(userIdStr); } // 返回数据保存时使用的前缀 public static Object saveKeyPrefix() { return SaManager.getConfig().getTokenName() + ":device-to-userid:"; } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/PhoneCodeUtil.java ================================================ package com.pj.util;//package com.pj.oauth2.custom; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.util.SaFoxUtil; /** * 手机验证码工具类 (仅做逻辑模拟,不做真实发送) * * @author click33 * @since 2024/8/23 */ public class PhoneCodeUtil { // 指定手机号发送验证码 public static void sendCode(String phone) { String code = SaFoxUtil.getRandomNumber(100000, 999999) + ""; SaManager.getSaTokenDao().set("phone_code:" + phone, code, 60 * 5); System.out.println("手机号:" + phone + ",验证码:" + code + ",已发送成功"); } // 校验验证码是否正确,不正确则抛出异常 public static void checkCode(String phone, String code) { String oldCode = SaManager.getSaTokenDao().get("phone_code:" + phone); if( ! code.equals(oldCode) ) { throw new RuntimeException("验证码错误"); } // 验证通过后,立即删除验证码 SaManager.getSaTokenDao().delete("phone_code:" + phone); } } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-device-lock-h5/common.js ================================================ // 服务器接口主机地址 var baseUrl = "http://localhost:8081"; // 封装一下Ajax function ajax(path, data, successFn) { console.log(baseUrl + path); fetch(baseUrl + path, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'satoken': localStorage.getItem('satoken') }, body: serializeToQueryString(data), }) .then(response => response.json()) .then(res => { console.log('返回数据:', res); successFn(res); }) .catch(error => { console.error('提交失败:', error); return alert("异常:" + JSON.stringify(error)); }); } // 获取本地的 设备id function getLocalDeviceId() { let localDeviceId = localStorage.getItem('local-device-id'); if(!localDeviceId) { localDeviceId = randomString(60); localStorage.setItem('local-device-id', localDeviceId); } return localDeviceId; } // ------------ 工具方法 --------------- // 从url中查询到指定名称的参数值 function getParam(name, defaultValue){ var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i value != null) // 过滤 null 和 undefined .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } // 随机生成字符串 function randomString(len) {   len = len || 32;   var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';   var maxPos = $chars.length;   var str = '';   for (i = 0; i < len; i++) {     str += $chars.charAt(Math.floor(Math.random() * maxPos));   }   return str; } ================================================ FILE: sa-token-demo/sa-token-demo-device-lock-h5/device-lock-auth.html ================================================ 设备锁测试-认证页

================================================ FILE: sa-token-demo/sa-token-demo-device-lock-h5/index.html ================================================ 设备锁测试-首页

设备锁测试-首页

当前是否登录:

登录   注销

================================================ FILE: sa-token-demo/sa-token-demo-device-lock-h5/login.html ================================================ 设备锁测试-登录页 ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml ================================================ 4.0.0 com.pj sa-token-demo-dubbo-consumer 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.15 1.8 3.1.1 1.45.0 2.7.21 1.4.2 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo-registry-nacos ${dubbo.version} com.alibaba.nacos nacos-client ${nacos.version} cn.dev33 sa-token-dubbo ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/DubboConsumerApplication.java ================================================ package com.pj; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Dubbo 服务消费端 * * @author click33 * */ @EnableDubbo @SpringBootApplication public class DubboConsumerApplication { public static void main(String[] args) { SpringApplication.run(DubboConsumerApplication.class, args); System.out.println("DubboConsumerApplication 启动成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/controller/TestController.java ================================================ package com.pj.controller; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.service.DemoService; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @DubboReference private DemoService demoService; // Consumer端登录,状态传播到Provider端 --- http://localhost:8081/test @RequestMapping("test") public SaResult test() { demoService.isLogin("----------- 登录前 "); StpUtil.login(10001); demoService.isLogin("----------- 登录后 "); return SaResult.ok(); } // Provider端登录,状态回传到Consumer端 --- http://localhost:8081/test2 @RequestMapping("test2") public SaResult test2() { System.out.println("----------- 登录前 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); demoService.doLogin(10002); System.out.println("----------- 登录后 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.ok(); } // Consumer端登录,状态在Consumer端保持 --- http://localhost:8081/test3 @RequestMapping("test3") public SaResult test3() { System.out.println("----------- 登录前 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); StpUtil.login(10003); demoService.isLogin("----------- Provider状态"); System.out.println("----------- 登录后 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.ok(); } // Provider端登录,状态在Provider端保持 --- http://localhost:8081/test4 @RequestMapping("test4") public SaResult test4() { // 登录 demoService.doLogin(10004); // 打印一下 demoService.isLogin("----------- 会话信息 "); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/service/DemoService.java ================================================ package com.pj.service; public interface DemoService { /** * 登录 * @param loginId 账号id */ void doLogin(Object loginId); /** * 判断是否登录,打印状态 */ void isLogin(String str); } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/resources/application.yml ================================================ server: # 端口号 port: 8081 spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 dubbo: application: # 服务名称 name: dubbo-consumer-demo registry: # 注册中心地址 address: nacos://127.0.0.1:8001 ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml ================================================ 4.0.0 com.pj sa-token-demo-dubbo-provider 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.15 1.8 3.1.1 1.45.0 2.7.21 1.4.2 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo-registry-nacos ${dubbo.version} com.alibaba.nacos nacos-client ${nacos.version} cn.dev33 sa-token-dubbo ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/DubboProviderApplication.java ================================================ package com.pj; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Dubbo 服务提供端 * * @author click33 * */ @EnableDubbo @SpringBootApplication public class DubboProviderApplication { public static void main(String[] args) { SpringApplication.run(DubboProviderApplication.class, args); System.out.println("DubboProviderApplication 启动成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/controller/TestController.java ================================================ package com.pj.controller; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.service.DemoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @Autowired private DemoService demoService; // test @RequestMapping("test") public SaResult test() { demoService.isLogin("----------- 登录前 " + StpUtil.isLogin()); StpUtil.login(10001); demoService.isLogin("----------- 登录后 " + StpUtil.isLogin()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoService.java ================================================ package com.pj.service; public interface DemoService { /** * 登录 * @param loginId 账号id */ void doLogin(Object loginId); /** * 判断是否登录,打印状态 */ void isLogin(String str); } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoServiceImpl.java ================================================ package com.pj.service; import org.apache.dubbo.config.annotation.DubboService; import cn.dev33.satoken.stp.StpUtil; @DubboService() public class DemoServiceImpl implements DemoService { @Override public void doLogin(Object loginId) { StpUtil.login(loginId); } @Override public void isLogin(String str) { System.out.println(str); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/resources/application.yml ================================================ server: # 端口号 port: 8080 spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s # Dubbo dubbo: # 服务名 application: name: dubbo-provider-demo # 扫描包 scan: base-packages: com.pj # 注册中心地址 registry: address: nacos://127.0.0.1:8001 # 协议 protocol: name: dubbo port: 12345 ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml ================================================ 4.0.0 com.pj sa-token-demo-dubbo3-consumer 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.4.3 17 3.1.1 1.45.0 3.2.2 2.2.2 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot3-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo-registry-nacos ${dubbo.version} com.alibaba.nacos nacos-client ${nacos.version} cn.dev33 sa-token-dubbo3 ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/Dubbo3ConsumerApplication.java ================================================ package com.pj; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Dubbo3 服务消费端 * * @author click33 * */ @EnableDubbo @SpringBootApplication public class Dubbo3ConsumerApplication { public static void main(String[] args) { SpringApplication.run(Dubbo3ConsumerApplication.class, args); System.out.println("Dubbo3ConsumerApplication 启动成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/controller/TestController.java ================================================ package com.pj.controller; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.service.DemoService; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @DubboReference private DemoService demoService; // Consumer端登录,状态传播到Provider端 @RequestMapping("test") public SaResult test() { demoService.isLogin("----------- 登录前 "); StpUtil.login(10001); demoService.isLogin("----------- 登录后 "); return SaResult.ok(); } // Provider端登录,状态回传到Consumer端 @RequestMapping("test2") public SaResult test2() { System.out.println("----------- 登录前 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); demoService.doLogin(10002); System.out.println("----------- 登录后 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.ok(); } // Consumer端登录,状态在Consumer端保持 @RequestMapping("test3") public SaResult test3() { System.out.println("----------- 登录前 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); StpUtil.login(10003); demoService.isLogin("----------- Provider状态"); System.out.println("----------- 登录后 "); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.ok(); } // Provider端登录,状态在Provider端保持 @RequestMapping("test4") public SaResult test4() { // 登录 demoService.doLogin(10004); // 打印一下 demoService.isLogin("----------- 会话信息 "); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/service/DemoService.java ================================================ package com.pj.service; public interface DemoService { /** * 登录 * @param loginId 账号id */ void doLogin(Object loginId); /** * 判断是否登录,打印状态 */ void isLogin(String str); } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/resources/application.yml ================================================ server: # 端口号 port: 8081 spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 dubbo: application: # 服务名称 name: dubbo-consumer-demo registry: # 注册中心地址 address: nacos://127.0.0.1:8001 ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml ================================================ 4.0.0 com.pj sa-token-demo-dubbo3-provider 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.4.3 17 3.1.1 1.45.0 3.2.2 2.2.2 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot3-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.apache.dubbo dubbo-spring-boot-starter ${dubbo.version} org.apache.dubbo dubbo-registry-nacos ${dubbo.version} com.alibaba.nacos nacos-client ${nacos.version} cn.dev33 sa-token-dubbo3 ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/Dubbo3ProviderApplication.java ================================================ package com.pj; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Dubbo3 服务提供端 * * @author click33 * */ @EnableDubbo @SpringBootApplication public class Dubbo3ProviderApplication { public static void main(String[] args) throws Exception { SpringApplication.run(Dubbo3ProviderApplication.class, args); System.out.println("Dubbo3ProviderApplication 启动成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/controller/TestController.java ================================================ package com.pj.controller; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.service.DemoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { // 如果把 @Autowired 改为 @DubboReference // 则可能在首次调用 dubbo 服务时控制台出现以下异常(只打印异常信息,不影响调用): // java.lang.reflect.InaccessibleObjectException: Unable to make field private byte java.lang.StackTraceElement.format accessible: // module java.base does not "opens java.lang" to unnamed module @3a52dba3 // // 在启动参数上加上如下即可解决: // --add-opens java.base/java.math=ALL-UNNAMED @Autowired public DemoService demoService; // test @RequestMapping("test") public SaResult test() { demoService.isLogin("----------- 登录前 " + StpUtil.isLogin()); StpUtil.login(10001); demoService.isLogin("----------- 登录后 " + StpUtil.isLogin()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoService.java ================================================ package com.pj.service; public interface DemoService { /** * 登录 * @param loginId 账号id */ void doLogin(Object loginId); /** * 判断是否登录,打印状态 */ void isLogin(String str); } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoServiceImpl.java ================================================ package com.pj.service; import org.apache.dubbo.config.annotation.DubboService; import cn.dev33.satoken.stp.StpUtil; @DubboService() public class DemoServiceImpl implements DemoService { @Override public void doLogin(Object loginId) { StpUtil.login(loginId); } @Override public void isLogin(String str) { System.out.println(str); System.out.println("Token值:" + StpUtil.getTokenValue()); System.out.println("是否登录:" + StpUtil.isLogin()); } } ================================================ FILE: sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/resources/application.yml ================================================ server: # 端口号 port: 8080 spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s # Dubbo dubbo: # 服务名 application: name: dubbo-provider-demo # 扫描包 scan: base-packages: com.pj # 注册中心地址 registry: address: nacos://127.0.0.1:8001 # 协议 protocol: name: dubbo port: 12345 ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-freemarker 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-freemarker cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-freemarker ${sa-token.version} org.springframework.boot spring-boot-devtools provided org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/SaTokenFreemarkerDemoApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 参考链接:https://blog.csdn.net/m0_64210833/article/details/135994864 */ @SpringBootApplication public class SaTokenFreemarkerDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenFreemarkerDemoApplication.class, args); System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); System.out.println("\n测试访问:http://localhost:8081/"); } } ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.freemarker.dialect.SaTokenTemplateModel; import cn.dev33.satoken.stp.StpUtil; import freemarker.template.TemplateModelException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import javax.annotation.PostConstruct; /** * [Sa-Token 权限认证] 配置类 * * @author click33 */ @Configuration public class SaTokenConfigure { @Autowired FreeMarkerConfigurer configurer; /** * 注入 Sa-Token Freemarker 标签模板模型 对象 */ @PostConstruct public void setSaTokenTemplateModel() throws TemplateModelException { // 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签, // 例如:<#if sa.login()>... configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel()); // 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法, // 例如:${stp.getSession().get('name')} configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic); } } ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import cn.dev33.satoken.stp.StpInterface; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; /** * 测试 Controller * * @author click33 */ @RestController public class TestController { // 首页 @RequestMapping("/") public Object index() { return new ModelAndView("index"); } // 登录 @RequestMapping("login") public SaResult login(@RequestParam(defaultValue="10001") String id) { StpUtil.login(id); StpUtil.getSession().set("name", "zhangsan"); return SaResult.ok(); } // 注销 @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 spring: # Freemarker 相关配置 freemarker: # 指定模板文件的目录 template-loader-path: classpath:/templates # 指定Freemarker模板文件的后缀名 suffix: .ftl # 关闭模板缓存,方便测试 cache: false # 检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试 settings: template_update_delay: 0 ================================================ FILE: sa-token-demo/sa-token-demo-freemarker/src/main/resources/templates/index.ftl ================================================ Sa-Token 集成 Freemarker 标签方言

Sa-Token 集成 Freemarker 标签方言 —— 测试页面

当前是否登录:<#if stp.isLogin()>是<#else>否

登录 注销

登录之后才能显示:<@sa.login>value

不登录才能显示:<@sa.notLogin>value

具有角色 admin 才能显示:<@sa.hasRole value="admin">value

同时具备多个角色才能显示:<@sa.hasRoleAnd value="admin, ceo, cto">value

只要具有其中一个角色就能显示:<@sa.hasRoleOr value="admin, ceo, cto">value

不具有角色 admin 才能显示:<@sa.notRole value="admin">value

具有权限 user-add 才能显示:<@sa.hasPermission value="user-add">value

同时具备多个权限才能显示:<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value

只要具有其中一个权限就能显示:<@sa.hasPermissionOr value="user-add, user-delete, user-get">value

不具有权限 user-add 才能显示:<@sa.notPermission value="user-add">value

从SaSession中取值: <#if stp.isLogin()> ${stp.getSession().get('name')}

================================================ FILE: sa-token-demo/sa-token-demo-grpc/client/pom.xml ================================================ sa-token-demo-grpc com.lym 0.0.1-SNAPSHOT 4.0.0 client 8 8 UTF-8 ================================================ FILE: sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/Client.java ================================================ package com.lym; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author lym * @description * @date 2022/8/26 10:58 **/ @SpringBootApplication @EnableDiscoveryClient public class Client { public static void main(String[] args) { SpringApplication.run(Client.class); } } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/controller/TestController.java ================================================ package com.lym.controller; import cn.dev33.satoken.stp.StpUtil; import com.lym.grpc.client.GrpcAuthService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author lym * @description * @date 2022/8/26 11:01 **/ @RestController public class TestController { @Autowired private GrpcAuthService grpcAuthService; // 客户端登录,状态带到服务端。 @RequestMapping("test") public void test() { System.out.println("登录前:" + grpcAuthService.isLogin()); System.out.println("登录前:" + StpUtil.isLogin()); StpUtil.login(1); System.out.println("登录后:" + grpcAuthService.isLogin()); System.out.println("登录后:" + StpUtil.getTokenValue()); System.out.println("登录后:" + StpUtil.getLoginId()); } // 服务端登录,登录状态带回客户端 @RequestMapping("test2") public String test2() { System.out.println("登录前:" + grpcAuthService.isLogin()); System.out.println("登录前:" + StpUtil.isLogin()); String token = grpcAuthService.login(3); System.out.println("登录后:" + grpcAuthService.isLogin()); System.out.println("登录后:" + StpUtil.getTokenValue()); System.out.println("登录后:" + StpUtil.getLoginId()); assert StpUtil.getTokenValue().equals(token); return token; } } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/grpc/client/GrpcAuthService.java ================================================ package com.lym.grpc.client; import com.google.protobuf.Empty; import com.lym.grpc.auth.Auth; import com.lym.grpc.auth.AuthServiceGrpc; import net.devh.boot.grpc.client.inject.GrpcClient; import org.springframework.stereotype.Service; /** * @author lym * @description * @date 2022/8/26 11:02 **/ @Service public class GrpcAuthService { @GrpcClient("test-server") private AuthServiceGrpc.AuthServiceBlockingStub grpcAuthService; public boolean isLogin() { Auth.GrpcBool resp = grpcAuthService.isLogin(Empty.getDefaultInstance()); return resp.getVal(); } public String login(Integer id) { Auth.GrpcString resp = grpcAuthService.login(Auth.GrpcInt.newBuilder().setVal(id).build()); return resp.getVal(); } } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/client/src/main/proto/auth.proto ================================================ syntax = "proto3"; package auth; option java_package = "com.lym.grpc.auth"; import "google/protobuf/empty.proto"; message GrpcBool{ bool val = 1; } message GrpcInt{ int32 val = 1; } message GrpcString{ string val = 1; } service AuthService{ rpc isLogin(google.protobuf.Empty) returns (GrpcBool); rpc login(GrpcInt) returns (GrpcString); } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/client/src/main/resources/application.yml ================================================ server: port: 2222 spring: application: name: test-client redis: host: localhost cloud: nacos: server-addr: localhost:8848 sa-token: is-read-cookie: false grpc: server: port: 2223 client: test-server: negotiation-type: PLAINTEXT ================================================ FILE: sa-token-demo/sa-token-demo-grpc/pom.xml ================================================ 4.0.0 pom client server org.springframework.boot spring-boot-starter-parent 2.6.3 com.lym sa-token-demo-grpc 0.0.1-SNAPSHOT sa-token-demo-grpc sa-token-demo-grpc 8 8 1.8 UTF-8 UTF-8 UTF-8 1.18.10 1.45.0 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-data-redis cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} cn.dev33 sa-token-grpc ${sa-token.version} com.alibaba.cloud spring-cloud-alibaba-dependencies 2021.0.1.0 pom import org.springframework.cloud spring-cloud-dependencies 2021.0.1 pom import kr.motd.maven os-maven-plugin org.springframework.boot spring-boot-maven-plugin org.xolstice.maven.plugins protobuf-maven-plugin com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier} grpc-java io.grpc:protoc-gen-grpc-java:1.12.0:exe:${os.detected.classifier} compile compile-custom ================================================ FILE: sa-token-demo/sa-token-demo-grpc/server/pom.xml ================================================ sa-token-demo-grpc com.lym 0.0.1-SNAPSHOT 4.0.0 server 8 8 UTF-8 ================================================ FILE: sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/Server.java ================================================ package com.lym; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author lym * @description * @date 2022/8/26 10:58 **/ @SpringBootApplication @EnableDiscoveryClient public class Server { public static void main(String[] args) { SpringApplication.run(Server.class); } } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/grpc/server/GrpcAuthService.java ================================================ package com.lym.grpc.server; import cn.dev33.satoken.stp.StpUtil; import com.google.protobuf.Empty; import com.lym.grpc.auth.Auth; import com.lym.grpc.auth.AuthServiceGrpc; import com.lym.service.AuthService; import io.grpc.stub.StreamObserver; import net.devh.boot.grpc.server.service.GrpcService; import org.springframework.beans.factory.annotation.Autowired; /** * @author lym * @description * @date 2022/8/26 11:29 **/ @GrpcService public class GrpcAuthService extends AuthServiceGrpc.AuthServiceImplBase { @Autowired private AuthService authService; @Override public void isLogin(Empty request, StreamObserver responseObserver) { boolean isLogin = authService.isLogin(); responseObserver.onNext(Auth.GrpcBool.newBuilder().setVal(isLogin).build()); responseObserver.onCompleted(); } @Override public void login(Auth.GrpcInt request, StreamObserver responseObserver) { StpUtil.login(request.getVal()); Auth.GrpcString resp = Auth.GrpcString.newBuilder().setVal(StpUtil.getTokenValue()).build(); responseObserver.onNext(resp); responseObserver.onCompleted(); } } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/service/AuthService.java ================================================ package com.lym.service; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Service; /** * @author lym * @description * @date 2022/8/26 11:30 **/ @Service public class AuthService { public boolean isLogin() { if (StpUtil.isLogin()) { System.out.println("id:" + StpUtil.getLoginIdAsInt()); System.out.println("token:" + StpUtil.getTokenValue()); } else { System.out.println("未登录"); } return StpUtil.isLogin(); } } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/server/src/main/proto/auth.proto ================================================ syntax = "proto3"; package auth; option java_package = "com.lym.grpc.auth"; import "google/protobuf/empty.proto"; message GrpcBool{ bool val = 1; } message GrpcInt{ int32 val = 1; } message GrpcString{ string val = 1; } service AuthService{ rpc isLogin(google.protobuf.Empty) returns (GrpcBool); rpc login(GrpcInt) returns (GrpcString); } ================================================ FILE: sa-token-demo/sa-token-demo-grpc/server/src/main/resources/application.yml ================================================ server: port: 5555 spring: application: name: test-server redis: host: localhost cloud: nacos: server-addr: localhost:8848 sa-token: is-read-cookie: false grpc: server: port: 5556 ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-hutool-timed-cache 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-hutool-timed-cache ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/SaTokenDemoApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 整合 Hutool-TimedCache 示例 * @author click33 * */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); System.out.println(SaManager.getSaTokenDao()); } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 (BeforeAuth不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.util.Ttime; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public SaResult login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-jwt/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-jwt 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-jwt ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-test test ================================================ FILE: sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/SaTokenJwtDemoApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaTokenJwtDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenJwtDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.jwt.StpLogicJwtForSimple; import cn.dev33.satoken.stp.StpLogic; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * Sa-Token 整合 jwt */ @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForSimple(); // return new StpLogicJwtForMixin(); // return new StpLogicJwtForStateless(); } } ================================================ FILE: sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 全局异常处理 */ @RestControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin") public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/TestJwtController.java ================================================ package com.pj.test; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.pj.util.AjaxJson; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Date; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestJwtController { // 测试登录接口, 浏览器访问: http://localhost:8081/test/login @RequestMapping("login") public AjaxJson login(@RequestParam(defaultValue="10001") String id) { System.out.println("======================= 进入方法,测试登录接口 ========================= "); System.out.println("当前会话的token:" + StpUtil.getTokenValue()); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull()); StpUtil.login(id, new SaLoginParameter().setExtra("name", "张三")); // 在当前会话登录此账号 System.out.println("登录成功"); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginId()); // System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt()); System.out.println("当前登录设备:" + StpUtil.getLoginDeviceType()); // System.out.println("当前token信息:" + StpUtil.getTokenInfo()); return AjaxJson.getSuccess().setData(StpUtil.getTokenValue()); } // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo @RequestMapping("tokenInfo") public AjaxJson tokenInfo() { System.out.println("======================= 进入方法,打印当前token信息 ========================= "); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); System.out.println(tokenInfo); return AjaxJson.getSuccessData(tokenInfo); } // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session @RequestMapping("session") public AjaxJson session() throws JsonProcessingException { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("测试取值name:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", new Date()); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getSession().get("name")); System.out.println( new ObjectMapper().writeValueAsString(StpUtil.getSession())); return AjaxJson.getSuccess(); } // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") @SaCheckLogin public AjaxJson test() { System.out.println(); System.out.println("--------------进入请求--------------"); System.out.println(StpUtil.getExtra("username")); System.out.println(StpUtil.getExtra("nick")); return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-jwt/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/pom.xml ================================================ 4.0.0 com.kfyty loveqq-framework 1.1.2 sa-token-demo-loveqq-boot 0.0.1-SNAPSHOT 17 17 17 17 1.45.0 com.kfyty loveqq-boot ${loveqq.framework.version} com.kfyty loveqq-boot-starter-netty ${loveqq.framework.version} cn.dev33 sa-token-loveqq-boot-starter ${sa-token.version} com.kfyty loveqq-boot-starter-logback ${loveqq.framework.version} org.yaml snakeyaml org.apache.maven.plugins maven-compiler-plugin 3.10.1 17 17 UTF-8 ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/SaTokenLoveqqApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import com.kfyty.loveqq.framework.boot.K; import com.kfyty.loveqq.framework.core.autoconfig.annotation.BootApplication; import com.kfyty.loveqq.framework.web.core.autoconfig.annotation.EnableWebMvc; /** * Sa-Token 整合 loveqq-framework 示例 * * @author kfyty725 */ @EnableWebMvc @BootApplication public class SaTokenLoveqqApplication { public static void main(String[] args) { K.run(SaTokenLoveqqApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/MyFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pj.satoken; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.stp.StpUtil; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.web.core.filter.Filter; import com.kfyty.loveqq.framework.web.core.filter.FilterChain; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; /** * 自定义过滤器 */ @Component public class MyFilter implements Filter { /** * 实现该方法,可以实现 servlet/reactor 的统一 * 但是该方法内部是同步方法,若需要异步,可以实现仅 reactor 支持的 {@link Filter#doFilter(ServerRequest, ServerResponse, FilterChain)} 方法 * * @param request 请求 * @param response 响应 */ @Override public Continue doFilter(ServerRequest request, ServerResponse response) { System.out.println("进入自定义过滤器"); // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); try { System.out.println(StpUtil.isLogin()); } finally { SaTokenContextUtil.clearContext(prev); } return Continue.TRUE; } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.loveqq.boot.filter.SaRequestFilter; import cn.dev33.satoken.util.SaResult; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure { /** * 注册 [sa-token全局过滤器] */ @Bean public SaRequestFilter getSaReactorFilter() { return new SaRequestFilter() // 指定 [拦截路由] .addInclude("/**") // 指定 [放行路由] .addExclude("/favicon.ico") // 指定[认证函数]: 每次请求执行 .setAuth(r -> { System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); }) // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import cn.dev33.satoken.stp.StpInterface; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import java.util.ArrayList; import java.util.List; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import com.kfyty.loveqq.framework.web.core.annotation.ExceptionHandler; import com.kfyty.loveqq.framework.web.core.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.loveqq.boot.context.SaReactorHolder; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; import com.kfyty.loveqq.framework.web.core.annotation.GetMapping; import com.kfyty.loveqq.framework.web.core.annotation.RequestMapping; import com.kfyty.loveqq.framework.web.core.annotation.RestController; import com.kfyty.loveqq.framework.web.core.annotation.bind.CookieValue; import com.kfyty.loveqq.framework.web.core.annotation.bind.RequestParam; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; import reactor.core.publisher.Mono; import java.time.Duration; /** * 测试专用 Controller * 本示例是基于 reactor 编写,如果是 servlet,去除 SaReactorHolder/SaTokenContextUtil 包装,直接调用 sa-token api 即可 * * @author click33 */ @RestController @RequestMapping("/test/") public class TestController { @Autowired UserService userService; // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login @GetMapping("login") public Mono login(@RequestParam(defaultValue = "10001") String id) { return SaReactorHolder.sync(() -> { StpUtil.login(id); return SaResult.ok("登录成功"); }); } // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin @GetMapping("isLogin") public SaResult isLogin(ServerRequest request, ServerResponse response) { try { SaTokenContextUtil.setContext(request, response); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); } finally { SaTokenContextUtil.clearContext(null); } } // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 @GetMapping("isLogin2") public SaResult isLogin2(ServerRequest request, ServerResponse response) { SaResult res = SaTokenContextUtil.setContext(request, response, () -> { System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); return SaResult.data(res); } // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 @GetMapping("isLogin3") public Mono isLogin3() { return SaReactorHolder.sync(() -> { System.out.println("是否登录:" + StpUtil.isLogin()); userService.isLogin(); return SaResult.data(StpUtil.getTokenInfo()); }); } // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 @GetMapping("isLogin4") public Mono isLogin4() { return userService.findUserIdByNamePwd("ZhangSan", "123456") .flatMap(userId -> SaReactorHolder.sync(() -> { StpUtil.login(userId); return SaResult.data(StpUtil.getTokenInfo()); })); } // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 @GetMapping("isLogin5") public Mono isLogin5() { System.out.println("线程id-----" + Thread.currentThread().getId()); // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 return Mono.delay(Duration.ofSeconds(1)) .doOnNext(r -> System.out.println("线程id-----" + Thread.currentThread().getId())) .map(r -> SaReactorHolder.sync(() -> userService.isLogin())) .map(r -> userService.findUserIdByNamePwd("ZhangSan", "123456")) .map(r -> SaReactorHolder.sync(() -> userService.isLogin())) .flatMap(isLogin -> { System.out.println("是否登录 " + isLogin); return SaReactorHolder.sync(() -> { System.out.println("是否登录 " + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 @GetMapping("isLogin6") public SaResult isLogin6(@CookieValue("satoken") String satoken) { System.out.println("token 为:" + satoken); System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); } // 测试 浏览器访问: http://localhost:8081/test/test @GetMapping("test") public SaResult test() { System.out.println("线程id------- " + Thread.currentThread().getId()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/UserService.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Service; import reactor.core.publisher.Mono; /** * 模拟 Service 方法 * @author click33 * @since 2025/4/6 */ @Service public class UserService { public boolean isLogin() { System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); return StpUtil.isLogin(); } public Mono findUserIdByNamePwd(String name, String pwd) { // ... return Mono.just(10001L); } } ================================================ FILE: sa-token-demo/sa-token-demo-loveqq-boot/src/main/resources/application.yml ================================================ # 端口 k: server: port: 8081 ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml ================================================ 4.0.0 com.pj sa-token-demo-oauth2-client 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.8 3.1.1 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-starter-thymeleaf com.ejlchina okhttps 3.1.1 org.springframework.boot spring-boot-devtools provided org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动:Sa-OAuth2 ClientServer端 * @author click33 */ @SpringBootApplication public class SaOAuth2ClientApplication { public static void main(String[] args) { SpringApplication.run(SaOAuth2ClientApplication.class, args); System.out.println("\nSa-Token-OAuth Client端启动成功\n\n" + str); } static String str = "-------------------- Sa-Token-OAuth2 示例 --------------------\n\n" + "首先在host文件 (C:\\windows\\system32\\drivers\\etc\\hosts) 添加以下内容: \r\n" + " 127.0.0.1 sa-oauth-server.com \r\n" + " 127.0.0.1 sa-oauth-client.com \r\n" + "再从浏览器访问:\r\n" + " http://sa-oauth-client.com:8002"; } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java ================================================ package com.pj.oauth2; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.ejlchina.okhttps.OkHttps; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.pj.utils.SoMap; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; /** * Sa-OAuth2 Client端 控制器 * @author click33 */ @RestController public class SaOAuthClientController { // 相关参数配置 private final String clientId = "1001"; // 应用id private final String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥 private final String serverUrl = "http://sa-oauth-server.com:8000"; // 服务端接口 // 进入首页 @RequestMapping("/") public Object index(HttpServletRequest request) { request.setAttribute("uid", StpUtil.getLoginIdDefaultNull()); return new ModelAndView("index.html"); } // 根据Code码进行登录,获取 Access-Token 和 openid @RequestMapping("/codeLogin") public SaResult codeLogin(String code) throws JsonProcessingException { // 调用Server端接口,获取 Access-Token 以及其他信息 String str = OkHttps.sync(serverUrl + "/oauth2/token") .addBodyPara("grant_type", "authorization_code") .addBodyPara("code", code) .addBodyPara("client_id", clientId) .addBodyPara("client_secret", clientSecret) .post() .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); // code不等于200 代表请求失败 if(so.getInt("code") != 200) { return SaResult.error(so.getString("msg")); } // 根据openid获取其对应的userId long uid = getUserIdByOpenid(so.getString("openid")); so.set("uid", uid); // 返回相关参数 StpUtil.login(uid); return SaResult.data(so); } // 根据 Refresh-Token 去刷新 Access-Token @RequestMapping("/refresh") public SaResult refresh(String refreshToken) throws JsonProcessingException { // 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token String str = OkHttps.sync(serverUrl + "/oauth2/refresh") .addBodyPara("grant_type", "refresh_token") .addBodyPara("client_id", clientId) .addBodyPara("client_secret", clientSecret) .addBodyPara("refresh_token", refreshToken) .post() .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); // code不等于200 代表请求失败 if(so.getInt("code") != 200) { return SaResult.error(so.getString("msg")); } // 返回相关参数 return SaResult.data(so); } // 模式三:密码式-授权登录 @RequestMapping("/passwordLogin") public SaResult passwordLogin(String username, String password) throws JsonProcessingException { // 模式三:密码式-授权登录 String str = OkHttps.sync(serverUrl + "/oauth2/token") .addBodyPara("grant_type", "password") .addBodyPara("client_id", clientId) .addBodyPara("client_secret", clientSecret) .addBodyPara("username", username) .addBodyPara("password", password) .post() .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); // code不等于200 代表请求失败 if(so.getInt("code") != 200) { return SaResult.error(so.getString("msg")); } // 根据openid获取其对应的userId long uid = getUserIdByOpenid(so.getString("openid")); so.set("uid", uid); // 返回相关参数 StpUtil.login(uid); return SaResult.data(so); } // 模式四:获取应用的 Client-Token @RequestMapping("/clientToken") public SaResult clientToken() throws JsonProcessingException { // 调用Server端接口 String str = OkHttps.sync(serverUrl + "/oauth2/client_token") .addBodyPara("grant_type", "client_credentials") .addBodyPara("client_id", clientId) .addBodyPara("client_secret", clientSecret) .post() .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); // code不等于200 代表请求失败 if(so.getInt("code") != 200) { return SaResult.error(so.getString("msg")); } // 返回相关参数 return SaResult.data(so); } // 注销登录 @RequestMapping("/logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } // 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息 @RequestMapping("/getUserinfo") public SaResult getUserinfo(String accessToken) throws JsonProcessingException { // 调用Server端接口,查询开放的资源 String str = OkHttps.sync(serverUrl + "/oauth2/userinfo") .addBodyPara("access_token", accessToken) .post() .getBody() .toString(); SoMap so = SoMap.getSoMap().setJsonString(str); System.out.println("返回结果: " + new ObjectMapper().writeValueAsString(so)); // code不等于200 代表请求失败 if(so.getInt("code") != 200) { return SaResult.error(so.getString("msg")); } // 返回相关参数 (data=获取到的资源 ) return SaResult.data(so); } // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } // ------------ 模拟方法 ------------------ // 模拟方法:根据openid获取userId private long getUserIdByOpenid(String openid) { // 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId return 10001; } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java ================================================ package com.pj.utils; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦 *

所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用 *

最新:2020-12-10 新增部分构造方法 * @author click33 */ public class SoMap extends LinkedHashMap { private static final long serialVersionUID = 1L; public SoMap() { } /** 以下元素会在isNull函数中被判定为Null, */ public static final Object[] NULL_ELEMENT_ARRAY = {null, ""}; public static final List NULL_ELEMENT_LIST; static { NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY); } // ============================= 读值 ============================= /** 获取一个值 */ @Override public Object get(Object key) { if("this".equals(key)) { return this; } return super.get(key); } /** 如果为空,则返回默认值 */ public Object get(Object key, Object defaultValue) { Object value = get(key); if(valueIsNull(value)) { return defaultValue; } return value; } /** 转为String并返回 */ public String getString(String key) { Object value = get(key); if(value == null) { return null; } return String.valueOf(value); } /** 如果为空,则返回默认值 */ public String getString(String key, String defaultValue) { Object value = get(key); if(valueIsNull(value)) { return defaultValue; } return String.valueOf(value); } /** 转为int并返回 */ public int getInt(String key) { Object value = get(key); if(valueIsNull(value)) { return 0; } return Integer.valueOf(String.valueOf(value)); } /** 转为int并返回,同时指定默认值 */ public int getInt(String key, int defaultValue) { Object value = get(key); if(valueIsNull(value)) { return defaultValue; } return Integer.valueOf(String.valueOf(value)); } /** 转为long并返回 */ public long getLong(String key) { Object value = get(key); if(valueIsNull(value)) { return 0; } return Long.valueOf(String.valueOf(value)); } /** 转为double并返回 */ public double getDouble(String key) { Object value = get(key); if(valueIsNull(value)) { return 0.0; } return Double.valueOf(String.valueOf(value)); } /** 转为boolean并返回 */ public boolean getBoolean(String key) { Object value = get(key); if(valueIsNull(value)) { return false; } return Boolean.valueOf(String.valueOf(value)); } /** 转为Date并返回,根据自定义格式 */ public Date getDateByFormat(String key, String format) { try { return new SimpleDateFormat(format).parse(getString(key)); } catch (Exception e) { throw new RuntimeException(e); } } /** 转为Date并返回,根据格式: yyyy-MM-dd */ public Date getDate(String key) { return getDateByFormat(key, "yyyy-MM-dd"); } /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */ public Date getDateTime(String key) { return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss"); } /** 转为Map并返回 */ @SuppressWarnings({ "unchecked", "rawtypes" }) public SoMap getMap(String key) { Object value = get(key); if(value == null) { return SoMap.getSoMap(); } if(value instanceof Map) { return SoMap.getSoMap((Map)value); } if(value instanceof String) { return SoMap.getSoMap().setJsonString((String)value); } throw new RuntimeException("值无法转化为SoMap: " + value); } /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */ @SuppressWarnings("unchecked") public List getList(String key) { Object value = get(key); List list = null; if(value == null || value.equals("")) { list = new ArrayList(); } else if(value instanceof List) { list = (List)value; } else { list = new ArrayList(); list.add(value); } return list; } /** 获取集合 (指定泛型类型) */ public List getList(String key, Class cs) { List list = getList(key); List list2 = new ArrayList(); for (Object obj : list) { T objC = getValueByClass(obj, cs); list2.add(objC); } return list2; } /** 获取集合(逗号分隔式),(指定类型) */ public List getListByComma(String key, Class cs) { String listStr = getString(key); if(listStr == null || listStr.equals("")) { return new ArrayList<>(); } // 开始转化 String [] arr = listStr.split(","); List list = new ArrayList(); for (String str : arr) { if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) { str = str.trim(); } T objC = getValueByClass(str, cs); list.add(objC); } return list; } /** 根据指定类型从map中取值,返回实体对象 */ public T getModel(Class cs) { try { return getModelByObject(cs.newInstance()); } catch (Exception e) { throw new RuntimeException(e); } } /** 从map中取值,塞到一个对象中 */ public T getModelByObject(T obj) { // 获取类型 Class cs = obj.getClass(); // 循环复制 for (Field field : cs.getDeclaredFields()) { try { // 获取对象 Object value = this.get(field.getName()); if(value == null) { continue; } field.setAccessible(true); Object valueConvert = getValueByClass(value, field.getType()); field.set(obj, valueConvert); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException("属性取值出错:" + field.getName(), e); } } return obj; } /** * 将指定值转化为指定类型并返回 * @param obj * @param cs * @param * @return */ @SuppressWarnings("unchecked") public static T getValueByClass(Object obj, Class cs) { String obj2 = String.valueOf(obj); Object obj3 = null; if (cs.equals(String.class)) { obj3 = obj2; } else if (cs.equals(int.class) || cs.equals(Integer.class)) { obj3 = Integer.valueOf(obj2); } else if (cs.equals(long.class) || cs.equals(Long.class)) { obj3 = Long.valueOf(obj2); } else if (cs.equals(short.class) || cs.equals(Short.class)) { obj3 = Short.valueOf(obj2); } else if (cs.equals(byte.class) || cs.equals(Byte.class)) { obj3 = Byte.valueOf(obj2); } else if (cs.equals(float.class) || cs.equals(Float.class)) { obj3 = Float.valueOf(obj2); } else if (cs.equals(double.class) || cs.equals(Double.class)) { obj3 = Double.valueOf(obj2); } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { obj3 = Boolean.valueOf(obj2); } else { obj3 = (T)obj; } return (T)obj3; } // ============================= 写值 ============================= /** * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去) */ public void setDefaultValue(String key, Object defaultValue) { if(isNull(key)) { set(key, defaultValue); } } /** set一个值,连缀风格 */ public SoMap set(String key, Object value) { // 防止敏感key if(key.toLowerCase().equals("this")) { return this; } put(key, value); return this; } /** 将一个Map塞进SoMap */ public SoMap setMap(Map map) { if(map != null) { for (String key : map.keySet()) { this.set(key, map.get(key)); } } return this; } /** 将一个对象解析塞进SoMap */ public SoMap setModel(Object model) { if(model == null) { return this; } Field[] fields = model.getClass().getDeclaredFields(); for (Field field : fields) { try{ field.setAccessible(true); boolean isStatic = Modifier.isStatic(field.getModifiers()); if(!isStatic) { this.set(field.getName(), field.get(model)); } }catch (Exception e){ throw new RuntimeException(e); } } return this; } /** 将json字符串解析后塞进SoMap */ public SoMap setJsonString(String jsonString) { try { @SuppressWarnings("unchecked") Map map = new ObjectMapper().readValue(jsonString, Map.class); return this.setMap(map); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } // ============================= 删值 ============================= /** delete一个值,连缀风格 */ public SoMap delete(String key) { remove(key); return this; } /** 清理所有value为null的字段 */ public SoMap clearNull() { Iterator iterator = this.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); if(this.isNull(key)) { iterator.remove(); this.remove(key); } } return this; } /** 清理指定key */ public SoMap clearIn(String ...keys) { List keys2 = Arrays.asList(keys); Iterator iterator = this.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); if(keys2.contains(key) == true) { iterator.remove(); this.remove(key); } } return this; } /** 清理掉不在列表中的key */ public SoMap clearNotIn(String ...keys) { List keys2 = Arrays.asList(keys); Iterator iterator = this.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); if(keys2.contains(key) == false) { iterator.remove(); this.remove(key); } } return this; } /** 清理掉所有key */ public SoMap clearAll() { clear(); return this; } // ============================= 快速构建 ============================= /** 构建一个SoMap并返回 */ public static SoMap getSoMap() { return new SoMap(); } /** 构建一个SoMap并返回 */ public static SoMap getSoMap(String key, Object value) { return new SoMap().set(key, value); } /** 构建一个SoMap并返回 */ public static SoMap getSoMap(Map map) { return new SoMap().setMap(map); } /** 将一个对象集合解析成为SoMap */ public static SoMap getSoMapByModel(Object model) { return SoMap.getSoMap().setModel(model); } /** 将一个对象集合解析成为SoMap集合 */ public static List getSoMapByList(List list) { List listMap = new ArrayList(); for (Object model : list) { listMap.add(getSoMapByModel(model)); } return listMap; } /** 克隆指定key,返回一个新的SoMap */ public SoMap cloneKeys(String... keys) { SoMap so = new SoMap(); for (String key : keys) { so.set(key, this.get(key)); } return so; } /** 克隆所有key,返回一个新的SoMap */ public SoMap cloneSoMap() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(key, this.get(key)); } return so; } /** 将所有key转为大写 */ public SoMap toUpperCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(key.toUpperCase(), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key转为小写 */ public SoMap toLowerCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(key.toLowerCase(), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key中下划线转为中划线模式 (kebab-case风格) */ public SoMap toKebabCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(wordEachKebabCase(key), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key中下划线转为小驼峰模式 */ public SoMap toHumpCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(wordEachBigFs(key), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key中小驼峰转为下划线模式 */ public SoMap humpToLineCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(wordHumpToLine(key), this.get(key)); } this.clearAll().setMap(so); return this; } // ============================= 辅助方法 ============================= /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */ public boolean isNull(String key) { return valueIsNull(get(key)); } /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */ public boolean isContainNull(String ...keys) { for (String key : keys) { if(this.isNull(key)) { return true; } } return false; } /** 与isNull()相反 */ public boolean isNotNull(String key) { return !isNull(key); } /** 指定key的value是否为null,作用同isNotNull() */ public boolean has(String key) { return !isNull(key); } /** 指定value在此SoMap的判断标准中是否为null */ public boolean valueIsNull(Object value) { return NULL_ELEMENT_LIST.contains(value); } /** 验证指定key不为空,为空则抛出异常 */ public SoMap checkNull(String ...keys) { for (String key : keys) { if(this.isNull(key)) { throw new RuntimeException("参数" + key + "不能为空"); } } return this; } static Pattern patternNumber = Pattern.compile("[0-9]*"); /** 指定key是否为数字 */ public boolean isNumber(String key) { String value = getString(key); if(value == null) { return false; } return patternNumber.matcher(value).matches(); } /** * 转为JSON字符串 */ public String toJsonString() { try { // SoMap so = SoMap.getSoMap(this); return new ObjectMapper().writeValueAsString(this); } catch (Exception e) { throw new RuntimeException(e); } } // /** // * 转为JSON字符串, 带格式的 // */ // public String toJsonFormatString() { // try { // return JSON.toJSONString(this, true); // } catch (Exception e) { // throw new RuntimeException(e); // } // } // ============================= web辅助 ============================= /** * 返回当前request请求的的所有参数 * @return */ public static SoMap getRequestSoMap() { // 大善人SpringMVC提供的封装 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { throw new RuntimeException("当前线程非JavaWeb环境"); } // 当前request HttpServletRequest request = servletRequestAttributes.getRequest(); if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) { initRequestSoMap(request); } return (SoMap)request.getAttribute("currentSoMap"); } /** 初始化当前request的 SoMap */ private static void initRequestSoMap(HttpServletRequest request) { SoMap soMap = new SoMap(); Map parameterMap = request.getParameterMap(); // 获取所有参数 for (String key : parameterMap.keySet()) { try { String[] values = parameterMap.get(key); // 获得values if(values.length == 1) { soMap.set(key, values[0]); } else { List list = new ArrayList(); for (String v : values) { list.add(v); } soMap.set(key, list); } } catch (Exception e) { throw new RuntimeException(e); } } request.setAttribute("currentSoMap", soMap); } /** * 验证返回当前线程是否为JavaWeb环境 * @return */ public static boolean isJavaWeb() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 if(servletRequestAttributes == null) { return false; } return true; } // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) ============================= /** get 当前页 */ public int getKeyPageNo() { int pageNo = getInt("pageNo", 1); if(pageNo <= 0) { pageNo = 1; } return pageNo; } /** get 页大小 */ public int getKeyPageSize() { int pageSize = getInt("pageSize", 10); if(pageSize <= 0 || pageSize > 1000) { pageSize = 10; } return pageSize; } /** get 排序方式 */ public int getKeySortType() { return getInt("sortType"); } // ============================= 工具方法 ============================= /** * 将一个一维集合转换为树形集合 * @param list 集合 * @param idKey id标识key * @param parentIdKey 父id标识key * @param childListKey 子节点标识key * @return 转换后的tree集合 */ public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) { // 声明新的集合,存储tree形数据 List newTreeList = new ArrayList(); // 声明hash-Map,方便查找数据 SoMap hash = new SoMap(); // 将数组转为Object的形式,key为数组中的id for (int i = 0; i < list.size(); i++) { SoMap json = (SoMap) list.get(i); hash.put(json.getString(idKey), json); } // 遍历结果集 for (int j = 0; j < list.size(); j++) { // 单条记录 SoMap aVal = (SoMap) list.get(j); // 在hash中取出key为单条记录中pid的值 SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString()); // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中 if (hashVp != null) { // 检查是否有child属性,有则添加,没有则新建 if (hashVp.get(childListKey) != null) { @SuppressWarnings("unchecked") List ch = (List) hashVp.get(childListKey); ch.add(aVal); hashVp.put(childListKey, ch); } else { List ch = new ArrayList(); ch.add(aVal); hashVp.put(childListKey, ch); } } else { newTreeList.add(aVal); } } return newTreeList; } /** 指定字符串的字符串下划线转大写模式 */ private static String wordEachBig(String str){ String newStr = ""; for (String s : str.split("_")) { newStr += wordFirstBig(s); } return newStr; } /** 返回下划线转小驼峰形式 */ private static String wordEachBigFs(String str){ return wordFirstSmall(wordEachBig(str)); } /** 将指定单词首字母大写 */ private static String wordFirstBig(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1, str.length()); } /** 将指定单词首字母小写 */ private static String wordFirstSmall(String str) { return str.substring(0, 1).toLowerCase() + str.substring(1, str.length()); } /** 下划线转中划线 */ private static String wordEachKebabCase(String str) { return str.replaceAll("_", "-"); } /** 驼峰转下划线 */ private static String wordHumpToLine(String str) { return str.replaceAll("[A-Z]", "_$0").toLowerCase(); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml ================================================ server: port: 8002 # sa-token配置 sa-token: # token名称 (同时也是cookie名称) token-name: satoken-client ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html ================================================ Sa-OAuth2-Client端-测试页 ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/index.html ================================================ Sa-Token-OAuth2 Client 端 - 测试页 ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml ================================================ 4.0.0 com.pj sa-token-demo-oauth2-server 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.8 3.1.1 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-oauth2 ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-thymeleaf cn.dev33 sa-token-jwt ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java ================================================ package com.pj; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动:Sa-OAuth2 Server端 * @author click33 */ @SpringBootApplication public class SaOAuth2ServerApplication { public static void main(String[] args) { SpringApplication.run(SaOAuth2ServerApplication.class, args); System.out.println("\nSa-Token-OAuth2 Server端启动成功,配置如下:"); System.out.println(SaOAuth2Manager.getServerConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/mock/SaClientMockDao.java ================================================ package com.pj.mock; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * SaClientModel 模拟查询操作 * * @author click33 * @since 2024/11/15 */ @Component public class SaClientMockDao { public List list; /** * 构造方法,添加三个模拟应用 */ public void init(){ list = new ArrayList<>(); // 模拟应用1 SaClientModel client1 = new SaClientModel() .setClientId("1001") // client id .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 .addAllowRedirectUris("*") // 所有允许授权的 url .addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") // 所有签约的权限 .setSubjectId("1000001") // 主体 id (可选) .addAllowGrantTypes( // 所有允许的授权模式 GrantType.authorization_code, // 授权码式 GrantType.implicit, // 隐藏式 GrantType.refresh_token, // 刷新令牌 GrantType.password, // 密码式 GrantType.client_credentials, // 客户端模式 "phone_code" // 自定义授权模式 手机号验证码登录 ); list.add(client1); // 模拟应用2 SaClientModel client2 = new SaClientModel() .setClientId("1002") .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") .addAllowRedirectUris("*") .addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") .setSubjectId("1000001") // 主体 id (可选) .addAllowGrantTypes( GrantType.authorization_code, GrantType.implicit, GrantType.refresh_token, GrantType.password, GrantType.client_credentials ); list.add(client2); // 模拟应用3 SaClientModel client3 = new SaClientModel() .setClientId("1003") .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") .addAllowRedirectUris("*") .addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") .addAllowGrantTypes( GrantType.authorization_code, GrantType.implicit, GrantType.refresh_token, GrantType.password, GrantType.client_credentials ); list.add(client3); } /** * 根据应用 id 查找对应的应用,找不到则返回 null * @param clientId 应用 id * @return 应用对象 */ public SaClientModel getClientModel(String clientId) { if(list == null) { init(); } return list.stream() .filter(e -> e.getClientId().equals(clientId)) .findFirst() .orElse(null); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java ================================================ package com.pj.oauth2; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import com.pj.mock.SaClientMockDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * Sa-Token OAuth2:自定义数据加载器 * * @author click33 */ @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { @Autowired SaClientMockDao saClientMockDao; // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { // 此为模拟数据,真实环境需要从数据库查询 return saClientMockDao.getClientModel(clientId); } // 根据 clientId 和 loginId 获取 openid @Override public String getOpenid(String clientId, Object loginId) { // 此处使用框架默认算法生成 openid,真实项目建议改为从数据库查询 return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ResourcesController.java ================================================ package com.pj.oauth2; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.template.SaOAuth2Util; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.LinkedHashMap; import java.util.Map; /** * Sa-Token OAuth2 Resources 端 Controller * *

Resources 端:OAuth2 资源端,允许 Client 端根据 Access-Token 置换相关资源

* *

在 OAuth2 中,认证端和资源端: * 1、可以在一个 Controller 中,也可以在不同的 Controller 中 * 2、可以在同一个项目中,也可以在不同的项目中(在不同项目中时需要两端连同一个 Redis ) *

* * @author click33 * @since 2024/12/6 */ @RestController public class SaOAuth2ResourcesController { // 示例:获取 userinfo 信息:昵称、头像、性别等等 @RequestMapping("/oauth2/userinfo") public SaResult userinfo() { // 获取 Access-Token 对应的账号id String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken); System.out.println("-------- 此Access-Token对应的账号id: " + loginId); // 校验 Access-Token 是否具有权限: userinfo SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo"); // 模拟账号信息 (真实环境需要查询数据库获取信息) Map map = new LinkedHashMap<>(); // map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client map.put("nickname", "林小林"); map.put("avatar", "http://xxx.com/1.jpg"); map.put("age", "18"); map.put("sex", "男"); map.put("address", "山东省 青岛市 城阳区"); return SaResult.ok().setMap(map); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java ================================================ package com.pj.oauth2; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import java.util.HashMap; import java.util.Map; /** * Sa-Token-OAuth2 Server 认证端 Controller * * @author click33 */ @RestController public class SaOAuth2ServerController { // OAuth2-Server 端:处理所有 OAuth2 相关请求 @RequestMapping("/oauth2/*") public Object request() { System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); return SaOAuth2ServerProcessor.instance.dister(); } // Sa-Token OAuth2 定制化配置 @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 未登录的视图 SaOAuth2Strategy.instance.notLoginView = ()->{ return new ModelAndView("login.html"); }; // 登录处理函数 SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> { if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok().set("satoken", StpUtil.getTokenValue()); } return SaResult.error("账号名或密码错误"); }; // 授权确认视图 SaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{ Map map = new HashMap<>(); map.put("clientId", clientId); map.put("scope", scopes); return new ModelAndView("confirm.html", map); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/CustomPasswordGrantTypeHandler.java ================================================ //package com.pj.oauth2.custom_grant_type; // //import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; //import cn.dev33.satoken.oauth2.granttype.handler.PasswordGrantTypeHandler; //import cn.dev33.satoken.oauth2.granttype.handler.model.PasswordAuthResult; //import org.springframework.stereotype.Component; // ///** // * 自定义 Password Grant_Type 授权模式处理器认证过程 // * // * @author click33 // * @since 2025/5/11 // */ //@Component //public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler { // // @Override // public PasswordAuthResult loginByUsernamePassword(String username, String password) { // if("sa".equals(username) && "123456".equals(password)) { // long userId = 10001; // return new PasswordAuthResult(userId); // } else { // throw new SaOAuth2Exception("无效账号密码"); // } // } // //} ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneCodeGrantTypeHandler.java ================================================ //package com.pj.oauth2.custom_grant_type; // //import cn.dev33.satoken.SaManager; //import cn.dev33.satoken.context.model.SaRequest; //import cn.dev33.satoken.oauth2.SaOAuth2Manager; //import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; //import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; //import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; //import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; //import org.springframework.stereotype.Component; // //import java.util.List; // ///** // * 自定义 phone_code 授权模式处理器 // * // * @author click33 // * @since 2024/8/23 // */ //@Component //public class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { // // @Override // public String getHandlerGrantType() { // return "phone_code"; // } // // @Override // public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { // // // 获取前端提交的参数 // String phone = req.getParamNotNull("phone"); // String code = req.getParamNotNull("code"); // String realCode = SaManager.getSaTokenDao().get("phone_code:" + phone); // // // 1、校验验证码是否正确 // if(!code.equals(realCode)) { // throw new SaOAuth2Exception("验证码错误"); // } // // // 2、校验通过,删除验证码 // SaManager.getSaTokenDao().delete("phone_code:" + phone); // // // 3、登录 // long userId = 10001; // 模拟 userId,真实项目应该根据手机号从数据库查询 // // // 4、构建 ra 对象 // RequestAuthModel ra = new RequestAuthModel(); // ra.clientId = clientId; // ra.loginId = userId; // ra.scopes = scopes; // // // 5、生成 Access-Token // AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = "phone_code"); // return at; // } //} ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneLoginController.java ================================================ //package com.pj.oauth2.custom_grant_type; // //import cn.dev33.satoken.SaManager; //import cn.dev33.satoken.util.SaFoxUtil; //import cn.dev33.satoken.util.SaResult; //import org.springframework.web.bind.annotation.RequestMapping; //import org.springframework.web.bind.annotation.RestController; // ///** // * 自定义手机登录接口 // * // * @author click33 // * @since 2024/8/23 // */ //@RestController //public class PhoneLoginController { // // @RequestMapping("/oauth2/sendPhoneCode") // public SaResult sendCode(String phone) { // String code = SaFoxUtil.getRandomNumber(100000, 999999) + ""; // SaManager.getSaTokenDao().set("phone_code:" + phone, code, 60 * 5); // System.out.println("手机号:" + phone + ",验证码:" + code + ",已发送成功"); // return SaResult.ok("验证码发送成功"); // } // //} ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/CustomOidcScopeHandler.java ================================================ //package com.pj.oauth2.custom_scope; // //import cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel; //import cn.dev33.satoken.oauth2.scope.handler.OidcScopeHandler; //import org.springframework.stereotype.Component; // ///** // * 扩展 OIDC 权限处理器,返回更多字段 // * // * @author click33 // * @since 2024/8/24 // */ //@Component //public class CustomOidcScopeHandler extends OidcScopeHandler { // // @Override // public IdTokenModel workExtraData(IdTokenModel idToken) { // Object userId = idToken.sub; // System.out.println("----- 为 idToken 追加扩展字段 ----- "); // // idToken.extraData.put("uid", userId); // 用户id // idToken.extraData.put("nickname", "linXiaoLin"); // 昵称 // idToken.extraData.put("picture", "https://sa-token.cc/logo.png"); // 头像 // idToken.extraData.put("email", "456456@xx.com"); // 邮箱 // idToken.extraData.put("phone_number", "13144556677"); // 手机号 // // // 更多字段 ... // // 可参考:https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims // // return idToken; // } // //} ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/UserinfoScopeHandler.java ================================================ //package com.pj.oauth2.custom_scope; // //import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; //import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; //import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; //import org.springframework.stereotype.Component; // //import java.util.LinkedHashMap; //import java.util.Map; // ///** // * 自定义 userinfo scope 处理器 // * @author click33 // * @since 2024/8/20 // */ //@Component //public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface { // // // 指示当前处理器所要处理的 scope // @Override // public String getHandlerScope() { // return "userinfo"; // } // // // 当构建的 AccessToken 具有此权限时,所需要执行的方法 // @Override // public void workAccessToken(AccessTokenModel at) { // System.out.println("--------- userinfo 权限,加工 AccessTokenModel --------- "); // // 模拟账号信息 (真实环境需要查询数据库获取信息) // Map map = new LinkedHashMap(); // map.put("userId", "10008"); // map.put("nickname", "shengzhang_"); // map.put("avatar", "http://xxx.com/1.jpg"); // map.put("age", "18"); // map.put("sex", "男"); // map.put("address", "山东省 青岛市 城阳区"); // at.extraData.put("userinfo", map); // } // // // 当构建的 ClientToken 具有此权限时,所需要执行的方法 // @Override // public void workClientToken(ClientTokenModel ct) { // } // // // 当使用 RefreshToken 刷新 AccessToken 时,是否重新执行 workAccessToken 构建方法 // // 在一些实时性较高的数据中需要指定为 true // @Override // public boolean refreshAccessTokenIsWork() { // return true; // } // //} ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java ================================================ package com.pj.oauth2.h5; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; /** * Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口) */ @RestController public class SaOAuth2ServerH5Controller { /** * 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx * *

情况1:客户端未登录,返回 code=401,提示用户登录

*

情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认

*

情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数)

* * @return / */ @PostMapping("/oauth2/getRedirectUri") public Object getRedirectUri() { // 获取变量 SaRequest req = SaHolder.getRequest(); SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type); // 1、先判断是否开启了指定的授权模式 SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg); // 2、如果尚未登录, 则先去登录 long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L); if(loginId == 0L) { return SaResult.get(401, "need login", null); } // 3、构建请求 Model RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); // 4、开发者自定义的授权前置检查 SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); // 5、校验:重定向域名是否合法 oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); // 6、校验:此次申请的Scope,该Client是否已经签约 oauth2Template.checkContractScope(ra.clientId, ra.scopes); // 7、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); if(isNeedCarefulConfirm) { SaClientModel cm = oauth2Template.checkClientModel(ra.clientId); if( ! cm.getIsAutoConfirm()) { // code=411,需要用户手动确认授权 return SaResult.get(411, "need confirm", null); } } // 8、判断授权类型,重定向到不同地址 // 如果是 授权码式,则:开始重定向授权,下放code if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) { CodeModel codeModel = dataGenerate.generateCode(ra); String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); return SaResult.ok().set("redirect_uri", redirectUri); } // 如果是 隐藏式,则:开始重定向授权,下放 token if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) { AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); return SaResult.ok().set("redirect_uri", redirectUri); } // 默认返回 throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/GlobalExceptionHandler.java ================================================ package com.pj.satoken; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**").addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); // ... }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 .setBeforeAuth(obj -> { SaHolder.getResponse() // ---------- 设置跨域响应头 ---------- // 允许指定域访问跨域资源 .setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "*") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") ; // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/Test2Controller.java ================================================ package com.pj.test; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.template.SaOAuth2Util; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; /** * 测试 OAuth2 相关 token 增删查 * * @author click33 * @since 2024/8/25 */ @RestController @RequestMapping("/test") public class Test2Controller { // 测试:查询全部 Access-Token --- http://localhost:8000/test/getAccessTokenValueList?clientId=1001&loginId=10001 @RequestMapping("/getAccessTokenValueList") public SaResult getAccessTokenValueList(String clientId, long loginId) { List accessTokenValueList = SaOAuth2Util.getAccessTokenValueList(clientId, loginId); return SaResult.data(accessTokenValueList); } // 测试:查询全部 Access-Token, 带过期时间 --- http://localhost:8000/test/getAccessTokenIndexMap?clientId=1001&loginId=10001 @RequestMapping("/getAccessTokenIndexMap") public SaResult getAccessTokenIndexMap(String clientId, long loginId) { Map accessTokenIndexMap = SaOAuth2Manager.getDao().getAccessTokenIndexMap_FromAdjustAfter(clientId, loginId); return SaResult.data(accessTokenIndexMap); } // 测试:回收指定 Access-Token --- http://localhost:8000/test/revokeAccessToken?access_token=xxxxxxxxxx @RequestMapping("/revokeAccessToken") public SaResult revokeAccessToken(String access_token) { SaOAuth2Util.revokeAccessToken(access_token); return SaResult.ok(); } // 测试:回收全部 Access-Token --- http://localhost:8000/test/revokeAccessTokenByIndex?clientId=1001&loginId=10001 @RequestMapping("/revokeAccessTokenByIndex") public SaResult revokeAccessTokenByIndex(String clientId, long loginId) { SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId); return SaResult.ok(); } // 测试:查询全部 Refresh-Token --- http://localhost:8000/test/getRefreshTokenValueList?clientId=1001&loginId=10001 @RequestMapping("/getRefreshTokenValueList") public SaResult getRefreshTokenValueList(String clientId, long loginId) { List refreshTokenValueList = SaOAuth2Util.getRefreshTokenValueList(clientId, loginId); return SaResult.data(refreshTokenValueList); } // 测试:查询全部 Refresh-Token, 带过期时间 --- http://localhost:8000/test/getRefreshTokenIndexMap?clientId=1001&loginId=10001 @RequestMapping("/getRefreshTokenIndexMap") public SaResult getRefreshTokenIndexMap(String clientId, long loginId) { Map refreshTokenIndexMap = SaOAuth2Manager.getDao().getRefreshTokenIndexMap_FromAdjustAfter(clientId, loginId); return SaResult.data(refreshTokenIndexMap); } // 测试:回收指定 Refresh-Token --- http://localhost:8000/test/revokeRefreshToken?refresh_token=xxxxxxxxxx @RequestMapping("/revokeRefreshToken") public SaResult revokeRefreshToken(String refresh_token) { SaOAuth2Util.revokeRefreshToken(refresh_token); return SaResult.ok(); } // 测试:回收全部 Refresh-Token --- http://localhost:8000/test/revokeRefreshTokenByIndex?clientId=1001&loginId=10001 @RequestMapping("/revokeRefreshTokenByIndex") public SaResult revokeRefreshTokenByIndex(String clientId, long loginId) { SaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId); return SaResult.ok(); } // 测试:查询全部 Client-Token --- http://localhost:8000/test/getClientTokenValueList?clientId=1001 @RequestMapping("/getClientTokenValueList") public SaResult getClientTokenValueList(String clientId) { List clientTokenValueList = SaOAuth2Util.getClientTokenValueList(clientId); return SaResult.data(clientTokenValueList); } // 测试:查询全部 Client-Token, 带过期时间 --- http://localhost:8000/test/getClientTokenIndexMap?clientId=1001&loginId=10001 @RequestMapping("/getClientTokenIndexMap") public SaResult getClientTokenIndexMap(String clientId, long loginId) { Map rlientTokenIndexMap = SaOAuth2Manager.getDao().getClientTokenIndexMap_FromAdjustAfter(clientId, loginId); return SaResult.data(rlientTokenIndexMap); } // 测试:回收指定 Client-Token --- http://localhost:8000/test/revokeClientToken?client_token=xxxxxxxxxxx @RequestMapping("/revokeClientToken") public SaResult revokeClientToken(String client_token) { SaOAuth2Util.revokeClientToken(client_token); return SaResult.ok(); } // 测试:回收全部 Client-Token --- http://localhost:8000/test/revokeClientTokenByIndex?clientId=1001 @RequestMapping("/revokeClientTokenByIndex") public SaResult revokeClientTokenByIndex(String clientId) { SaOAuth2Util.revokeClientTokenByIndex(clientId); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.oauth2.annotation.SaCheckAccessToken; import cn.dev33.satoken.oauth2.annotation.SaCheckClientIdSecret; import cn.dev33.satoken.oauth2.annotation.SaCheckClientToken; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * OAuth2 相关注解测试 Controller * * @author click33 * @since 2024/8/25 */ @RestController @RequestMapping("/test") public class TestController { // 测试:携带有效的 access_token 才可以进入请求 // 你可以在请求参数中携带 access_token 参数,或者从请求头以 Authorization: bearer xxx 的形式携带 @SaCheckAccessToken @RequestMapping("/checkAccessToken") public SaResult checkAccessToken() { return SaResult.ok("访问成功"); } // 测试:携带有效的 access_token ,并且具备指定 scope 才可以进入请求 @SaCheckAccessToken(scope = "userinfo") @RequestMapping("/checkAccessTokenScope") public SaResult checkAccessTokenScope() { return SaResult.ok("访问成功"); } // 测试:携带有效的 access_token ,并且具备指定 scope 列表才可以进入请求 @SaCheckAccessToken(scope = {"openid", "userinfo"}) @RequestMapping("/checkAccessTokenScopeList") public SaResult checkAccessTokenScopeList() { return SaResult.ok("访问成功"); } // 测试:携带有效的 client_token 才可以进入请求 // 你可以在请求参数中携带 client_token 参数,或者从请求头以 Authorization: bearer xxx 的形式携带 @SaCheckClientToken @RequestMapping("/checkClientToken") public SaResult checkClientToken() { return SaResult.ok("访问成功"); } // 测试:携带有效的 client_token ,并且具备指定 scope 才可以进入请求 @SaCheckClientToken(scope = "userinfo") @RequestMapping("/checkClientTokenScope") public SaResult checkClientTokenScope() { return SaResult.ok("访问成功"); } // 测试:携带有效的 client_token ,并且具备指定 scope 列表才可以进入请求 @SaCheckClientToken(scope = {"openid", "userinfo"}) @RequestMapping("/checkClientTokenScopeList") public SaResult checkClientTokenScopeList() { return SaResult.ok("访问成功"); } // 测试:携带有效的 client_id 和 client_secret 信息,才可以进入请求 // 你可以在请求参数中携带 client_id 和 client_secret 参数,或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带 @SaCheckClientIdSecret @RequestMapping("/checkClientIdSecret") public SaResult checkClientIdSecret() { return SaResult.ok("访问成功"); } } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml ================================================ server: port: 8000 # sa-token配置 sa-token: # token名称 (同时也是 Cookie 名称) token-name: satoken # 是否打印操作日志 is-log: true # jwt 秘钥 jwt-secret-key: saxsaxsaxsax # OAuth2.0 配置 oauth2-server: # 是否全局开启授权码模式 enable-authorization-code: true # 是否全局开启 Implicit 模式 enable-implicit: true # 是否全局开启密码模式 enable-password: true # 是否全局开启客户端模式 enable-client-credentials: true # 定义哪些 scope 是高级权限,多个用逗号隔开 # higher-scope: openid,userid # 定义哪些 scope 是低级权限,多个用逗号隔开 # lower-scope: userinfo spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间(毫秒) timeout: 1000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html ================================================ Sa-OAuth2-认证中心-确认授权页

================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/login.html ================================================ Sa-OAuth2-认证中心-登录页 ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css ================================================ *{margin: 0; padding: 0;} body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} ::-webkit-input-placeholder{color: #ccc;} /* 视图盒子 */ .view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} /* 背景 EAEFF3 */ .bg-1{height: 100%; background: #E4B17F;} /* 内容盒子 */ .content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} .content-box{display: none;} .region-default{display: block;} .message-box{width: 100%;; text-align: center;} /* 登录盒子 */ /* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ .login-box{width: 450px; margin: auto; max-width: 90%; height: 100%;} .login-box{display: flex; align-items: center; text-align: center;} /* 表单 */ .from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} .from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} .from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} /* 输入框 */ .from-item{border: 0px #000 solid; margin-bottom: 15px;} .s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} .s-input{font-size: 12px;} .s-input:focus{border-color: #409eff} /* 登录按钮 */ .s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} .s-btn:hover{background-color: #50aEFF;} /* 重置按钮 */ .reset-box{text-align: left; font-size: 12px;} .reset-box a{text-decoration: none;} .reset-box a:hover{text-decoration: underline;} /* 确认授权按钮 */ .confirm-btn{text-indent: 0; cursor: pointer; background-color: #409EFF; border: 1px #409EFF solid; color: #FFF; padding: 5px 15px;} .confirm-btn:hover{background-color: #50aEFF;} /* loading框样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js ================================================ // OAuth-Server 后端 接口地址 var baseUrl = "http://sa-oauth-server.com:8000"; // ----------------------------------- 相关事件 ----------------------------------- // 显示默认区域 function showDefaultRegion(){ $('.content-box').hide(); $('.region-default').show(); } // 显示登录框区域 function showLoginRegion(){ $('.content-box').hide(); $('.region-login').show(); } // 显示确认授权框区域 function showConfirmRegion(){ $('.content-box').hide(); $('.region-confirm').show(); $('.show-clientId').text(getParam('client_id')); $('.show-scope').text(getParam('scope')); } // 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 function tryJump(){ var data = location.search.substr(1); sa.ajax("/oauth2/getRedirectUri", data, function(res) { // 情况1:客户端未登录,返回 code=401,提示用户登录 if(res.code === 401) { showLoginRegion(); return; } // 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认 if(res.code === 411) { showConfirmRegion(); return; } // 情况3:已登录且请求的 scope 已确认授权,返回 code=200,data=最终重定向 url 地址(携带code码参数) if(res.code == 200) { console.log('跳转:', res.redirect_uri); location.href = res.redirect_uri; return; } console.log('未知状态码,', res.code, res); layer.alert('错误:' + JSON.stringify(res)) }) } // 登录事件 function doLogin() { // 开始登录 var data = { name: $('[name=name]').val(), pwd: $('[name=pwd]').val() }; sa.ajax("/oauth2/doLogin", data, function(res) { if(res.code == 200) { localStorage.setItem('satoken', res.satoken); layer.msg('登录成功', {anim: 0, icon: 6 }); setTimeout(function() { location.reload(); }, 800); } else { layer.msg(res.msg, {anim: 6, icon: 2 }); } }) } // 确认授权事件 function yes() { var data = location.search.substr(1) + '&build_redirect_uri=true'; sa.ajax("/oauth2/doConfirm", data, function(res) { if(res.code == 200) { layer.msg('确认授权成功,即将跳转...', {anim: 0, icon: 6 }); setTimeout(function() { console.log('跳转:', res.redirect_uri); location.href = res.redirect_uri; }, 800); } else { layer.msg(res.msg, {anim: 6, icon: 2 }); } }) } // 拒绝授权事件 function no() { var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权"); location.href = url; } // 页面加载完毕后触发 window.onload = function() { tryJump(); // 绑定回车事件 $('[name=name],[name=pwd]').bind('keypress', function(event){ if(event.keyCode == "13") { $('.login-btn').click(); } }); // 输入框获取焦点 $("[name=name]").focus(); } // ----------------------------------- 工具函数封装 ----------------------------------- // sa var sa = {}; // 打开loading sa.loading = function(msg) { layer.closeAll(); // 开始前先把所有弹窗关了 return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'}); }; // 隐藏loading sa.hideLoading = function() { layer.closeAll(); }; // 封装一下Ajax sa.ajax = function(url, data, successFn) { sa.loading("加载中..."); $.ajax({ url: baseUrl + url, type: "post", data: data, dataType: 'json', headers: { 'X-Requested-With': 'XMLHttpRequest', 'satoken': localStorage.getItem('satoken') }, success: function(res){ sa.hideLoading(); console.log('返回数据:', res); successFn(res); }, error: function(xhr, type, errorThrown){ sa.hideLoading(); if(xhr.status == 0){ return alert('无法连接到服务器,请检查网络'); } return alert("异常:" + JSON.stringify(xhr)); } }); } // 从url中查询到指定名称的参数值 function getParam(name, defaultValue){ var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i -1 && index < url.length - 1) { // 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 & if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) { return url + '&' + parameStr; } else { return url + parameStr; } } } // 打印信息 var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/"; console.log(str); ================================================ FILE: sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html ================================================ Sa-OAuth2-Server 认证中心(前后端分离版)
This page is provided by Sa-Token-OAuth2
================================================ FILE: sa-token-demo/sa-token-demo-quick-login/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-quick-login 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-quick-login ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-devtools true org.springframework.boot spring-boot-maven-plugin ================================================ FILE: sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaQuicikStartup.java ================================================ package com.pj; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import cn.dev33.satoken.quick.SaQuickManager; /** * springboot启动之后 * @author click33 * */ @Component public class SaQuicikStartup implements CommandLineRunner { @Value("${spring.application.name:sa-quick}") private String applicationName; @Value("${server.port:8080}") private String port; @Value("${server.servlet.context-path:}") private String path; // @Value("${spring.profiles.active:}") // private String active; @Override public void run(String... args) throws Exception { String str = "\n------------- " + applicationName + " 启动成功 (" + getNow() + ") -------------\n" + " - home: " + "http://localhost:" + port + path + "\n" + " - name: " + SaQuickManager.getConfig().getName() + "\n"+ " - pwd : " + SaQuickManager.getConfig().getPwd() + "\n"; System.out.println(str); } /** * 返回系统当前时间的YYYY-MM-dd hh:mm:ss 字符串格式 */ private static String getNow(){ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } } ================================================ FILE: sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaTokenQuickDemoApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaTokenQuickDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenQuickDemoApplication.class, args); } } ================================================ FILE: sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaTokenConsts; /** * 测试专用Controller * @author click33 * */ @RestController public class TestController { // 浏览器访问测试: http://localhost:8081 @RequestMapping({"/"}) public String index() { String str = "
" // + "

Welcome to the system

" + "

资源页 (登录后才可进入本页面)

" + "
" + "

Sa-Token " + SaTokenConsts.VERSION_NO + "

"; return str; } } ================================================ FILE: sa-token-demo/sa-token-demo-quick-login/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # Sa-Token-Quick-Login 配置 sa: # 登录账号 name: sa # 登录密码 pwd: 123456 # 是否自动随机生成账号密码 (此项为true时, name与pwd失效) auto: false # 是否开启全局认证(关闭后将不再强行拦截) auth: true # 登录页标题 title: Sa-Token 登录 # 是否显示底部版权信息 copr: true # 指定拦截路径 # include: /** # 指定排除路径 # exclude: /1.jpg # 将本地磁盘的某个路径作为静态资源开放 # dir: file:E:\static # 静态文件路径映射 spring: resources: static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/, ${sa.dir:} ================================================ FILE: sa-token-demo/sa-token-demo-quick-login-sb3/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-quick-login-sb3 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.4.3 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot3-starter ${sa-token.version} cn.dev33 sa-token-quick-login ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-devtools true org.springframework.boot spring-boot-maven-plugin ================================================ FILE: sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaQuicikStartup.java ================================================ package com.pj; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import cn.dev33.satoken.quick.SaQuickManager; /** * springboot启动之后 * @author click33 * */ @Component public class SaQuicikStartup implements CommandLineRunner { @Value("${spring.application.name:sa-quick}") private String applicationName; @Value("${server.port:8080}") private String port; @Value("${server.servlet.context-path:}") private String path; // @Value("${spring.profiles.active:}") // private String active; @Override public void run(String... args) throws Exception { String str = "\n------------- " + applicationName + " 启动成功 (" + getNow() + ") -------------\n" + " - home: " + "http://localhost:" + port + path + "\n" + " - name: " + SaQuickManager.getConfig().getName() + "\n"+ " - pwd : " + SaQuickManager.getConfig().getPwd() + "\n"; System.out.println(str); } /** * 返回系统当前时间的YYYY-MM-dd hh:mm:ss 字符串格式 */ private static String getNow(){ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } } ================================================ FILE: sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaTokenQuickSb3DemoApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaTokenQuickSb3DemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenQuickSb3DemoApplication.class, args); } } ================================================ FILE: sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaTokenConsts; /** * 测试专用Controller * @author click33 * */ @RestController public class TestController { // 浏览器访问测试: http://localhost:8081 @RequestMapping({"/"}) public String index() { String str = "
" // + "

Welcome to the system

" + "

资源页 (登录后才可进入本页面)

" + "
" + "

Sa-Token " + SaTokenConsts.VERSION_NO + "

"; return str; } } ================================================ FILE: sa-token-demo/sa-token-demo-quick-login-sb3/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # Sa-Token-Quick-Login 配置 sa: # 登录账号 name: sa # 登录密码 pwd: 123456 # 是否自动随机生成账号密码 (此项为true时, name与pwd失效) auto: false # 是否开启全局认证(关闭后将不再强行拦截) auth: true # 登录页标题 title: Sa-Token 登录 # 是否显示底部版权信息 copr: true # 指定拦截路径 # include: /** # 指定排除路径 # exclude: /1.jpg # 将本地磁盘的某个路径作为静态资源开放 # dir: file:E:\static # 静态文件路径映射 spring: resources: static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/, ${sa.dir:} ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/.gitignore ================================================ # vite创建项目时自动生成的git忽略配置文件 logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/README.md ================================================ # Vue 3 + Vite [Node下载地址](https://nodejs.org/zh-cn/) 安装最新版本Node环境, 然后执行如下命令开启开发服务: ``` npm install npm run dev ``` [cookie/sessionstorage/localstorage三者的区别](https://blog.csdn.net/weixin_45541388/article/details/125367823) ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/index.html ================================================ 记住我模式Demo页面
================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/package.json ================================================ { "name": "page_project", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "axios": "^1.3.4", "element-plus": "^2.2.33", "qs": "^6.11.0", "vue": "^3.2.45", "vue-axios": "^3.5.2" }, "devDependencies": { "@vitejs/plugin-vue": "^4.0.0", "vite": "^4.1.0" } } ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/src/App.vue ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/src/main.js ================================================ import { createApp } from 'vue' import App from './App.vue' import axios from 'axios' // 请求发送接收工具 import VueAxios from 'vue-axios' // vue封装axios import qs from 'qs' // axios请求参数类型封装 import ElementPlus from 'element-plus' // elementUI for vue3 import 'element-plus/dist/index.css' // 加载elementUI样式 import zhCn from 'element-plus/es/locale/lang/zh-cn' // 引入中文本地化组件 const app = createApp(App) // vue组件内通过 this.$f() 来调用 app.config.globalProperties.$f = (params) => { return qs.stringify(params) } app.use(VueAxios, axios) .use(ElementPlus, { locale: zhCn }) .mount('#app') ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/page_project/vite.config.js ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // 开启代理服务 // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], server: { port: 5173, host: true, proxy: { '^/back/.*$': { target: 'http://localhost:80' } } } }) ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-remember-me-server 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/RememberMeApplication.java ================================================ package cc.sa_token; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RememberMeApplication { public static void main(String[] args) { SpringApplication.run(RememberMeApplication.class, args); } } ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/controller/UserLoginController.java ================================================ package cc.sa_token.controller; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/back/user") public class UserLoginController { @RequestMapping("/login") public SaResult doLogin(String name, String pwd, Boolean remember) { if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001, remember); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return SaResult.ok() .set("tokenName", tokenInfo.getTokenName()) .set("tokenValue", tokenInfo.getTokenValue()); } else { return SaResult.error("登录失败"); } } @RequestMapping("/state") public SaResult checkNowLoginState() { return SaResult.ok().setData(StpUtil.isLogin()); } @RequestMapping("/logout") public SaResult doLogout() { StpUtil.logout(); return SaResult.ok().setData(StpUtil.isLogin()); } } ================================================ FILE: sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/resources/application.yml ================================================ # 端口 server: port: 80 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-solon/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-solon 0.0.1-SNAPSHOT org.noear solon-parent 3.2.1 17 17 17 1.45.0 UTF-8 UTF-8 org.noear solon-web org.noear solon.logging.simple cn.dev33 sa-token-solon-plugin ${sa-token.version} cn.dev33 sa-token-redisx ${sa-token.version} cn.dev33 sa-token-snack3 ${sa-token.version} org.apache.maven.plugins maven-compiler-plugin 3.8.1 -parameters 1.8 1.8 UTF-8 ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/SaTokenDemoApp.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; /** * sa-token整合 solon 示例 * @author noear * */ @SolonMain public class SaTokenDemoApp { public static void main(String[] args) { Solon.start(SaTokenDemoApp.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaLogForSlf4j.java ================================================ package com.pj.satoken; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.log.SaLogForConsole; import cn.dev33.satoken.util.StrFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 将 Sa-Token log 信息转接到 slf4j 接口 * * @author noear 2022/11/14 created */ //@Component public class SaLogForSlf4j extends SaLogForConsole implements SaLog { static final Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); /** * 打印日志到控制台 * * @param level 日志等级 * @param str 字符串 * @param args 参数列表 */ public void println(int level, String str, Object... args) { SaTokenConfig config = SaManager.getConfig(); if (config.getIsLog() && level >= config.getLogLevelInt()) { switch (level) { case trace: log.trace(LOG_PREFIX + StrFormatter.format(str, args)); break; case debug: log.debug(LOG_PREFIX + StrFormatter.format(str, args)); break; case info: log.info(LOG_PREFIX + StrFormatter.format(str, args)); break; case warn: log.warn(LOG_PREFIX + StrFormatter.format(str, args)); break; case error: case fatal: log.error(LOG_PREFIX + StrFormatter.format(str, args)); break; } } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaLogForSolon.java ================================================ package com.pj.satoken; import org.noear.solon.core.util.LogUtil; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.log.SaLogForConsole; import cn.dev33.satoken.util.StrFormatter; /** * 将 Sa-Token log 信息转接到 Solon * * @author click33 * @since 2022-11-2 */ //@Component public class SaLogForSolon extends SaLogForConsole implements SaLog { /** * 打印日志到控制台 * * @param level 日志等级 * @param str 字符串 * @param args 参数列表 */ public void println(int level, String str, Object... args) { SaTokenConfig config = SaManager.getConfig(); if (config.getIsLog() && level >= config.getLogLevelInt()) { switch (level) { case trace: LogUtil.global().trace(LOG_PREFIX + StrFormatter.format(str, args)); break; case debug: LogUtil.global().debug(LOG_PREFIX + StrFormatter.format(str, args)); break; case info: LogUtil.global().info(LOG_PREFIX + StrFormatter.format(str, args)); break; case warn: LogUtil.global().warn(LOG_PREFIX + StrFormatter.format(str, args)); break; case error: case fatal: LogUtil.global().error(LOG_PREFIX + StrFormatter.format(str, args)); break; } } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisx; import cn.dev33.satoken.solon.integration.SaTokenInterceptor; import com.pj.util.AjaxJson; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * [Sa-Token 权限认证] 配置类 * @author click33 * @author noear */ @Configuration public class SaTokenConfigure { /** * 注册 [sa-token全局过滤器] */ @Bean(index = -100) public SaTokenInterceptor tokenPathFilter() { return new SaTokenInterceptor() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**").addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(r -> { // System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> new Object()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return AjaxJson.getError(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-Frame-Options", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }); } //如果需要 redis dao,加这段代表 @Bean public SaTokenDao saTokenDaoInit(@Inject("${sa-token-dao.redis}") SaTokenDaoForRedisx saTokenDao) { return saTokenDao; } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import cn.dev33.satoken.stp.StpInterface; import org.noear.solon.annotation.Component; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被 solon 扫描,即可完成 sa-token 的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java ================================================ package com.pj.satoken.custom_annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 账号校验:在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 * * @author click33 * */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface CheckAccount { /** * 需要校验的账号 * * @return / */ String name(); /** * 需要校验的密码 * * @return / */ String pwd(); } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java ================================================ package com.pj.satoken.custom_annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.exception.SaTokenException; import com.pj.satoken.custom_annotation.CheckAccount; import org.noear.solon.annotation.Component; import java.lang.reflect.AnnotatedElement; /** * 注解 CheckAccount 的处理器 * * @author click33 * */ @Component public class CheckAccountHandler implements SaAnnotationHandlerInterface { // 指定这个处理器要处理哪个注解 @Override public Class getHandlerAnnotationClass() { return CheckAccount.class; } // 每次请求校验注解时,会执行的方法 @Override public void checkMethod(CheckAccount at, AnnotatedElement method) { // 获取前端请求提交的参数 String name = SaHolder.getRequest().getParamNotNull("name"); String pwd = SaHolder.getRequest().getParamNotNull("pwd"); // 与注解中指定的值相比较 if(name.equals(at.name()) && pwd.equals(at.pwd()) ) { // 校验通过,什么也不做 } else { // 校验不通过,则抛出异常 throw new SaTokenException("账号或密码错误,未通过校验"); } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/GlobalExceptionFilter.java ================================================ package com.pj.test; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.*; import org.noear.solon.annotation.Component; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 全局异常处理 * * @author noear */ @Component public class GlobalExceptionFilter implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { chain.doFilter(ctx); } catch (SaTokenException e) { // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if (e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if (e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if (e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } ctx.render(aj); } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import com.pj.util.AjaxJson; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; /** * 压力测试 * @author click33 * @author noear */ @Controller @Mapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @Mapping("login") public AjaxJson login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.session.SaSessionCustomUtil; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import com.pj.util.AjaxJson; import com.pj.util.Ttime; import org.noear.snack.ONode; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import org.noear.solon.annotation.Param; import java.util.Date; import java.util.List; /** * 测试专用Controller * @author click33 * @author noear */ @Controller @Mapping("/test/") public class TestController { // 测试登录接口, 浏览器访问: http://localhost:8081/test/login @Mapping("login") public AjaxJson login(@Param(defaultValue="10001") String id) { System.out.println("======================= 进入方法,测试登录接口 ========================= "); System.out.println("当前会话的token:" + StpUtil.getTokenValue()); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull()); StpUtil.login(id); // 在当前会话登录此账号 System.out.println("登录成功"); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginId()); // System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt()); System.out.println("当前登录设备:" + StpUtil.getLoginDevice()); // System.out.println("当前token信息:" + StpUtil.getTokenInfo()); return AjaxJson.getSuccess(); } // 测试退出登录 , 浏览器访问: http://localhost:8081/test/logout @Mapping("logout") public AjaxJson logout() { StpUtil.logout(); // StpUtil.logoutByLoginId(10001); return AjaxJson.getSuccess(); } // 测试角色接口, 浏览器访问: http://localhost:8081/test/testRole @Mapping("testRole") public AjaxJson testRole() { System.out.println("======================= 进入方法,测试角色接口 ========================= "); System.out.println("是否具有角色标识 user " + StpUtil.hasRole("user")); System.out.println("是否具有角色标识 admin " + StpUtil.hasRole("admin")); System.out.println("没有admin权限就抛出异常"); StpUtil.checkRole("admin"); System.out.println("在【admin、user】中只要拥有一个就不会抛出异常"); StpUtil.checkRoleOr("admin", "user"); System.out.println("在【admin、user】中必须全部拥有才不会抛出异常"); StpUtil.checkRoleAnd("admin", "user"); System.out.println("角色测试通过"); return AjaxJson.getSuccess(); } // 测试权限接口, 浏览器访问: http://localhost:8081/test/testJur @Mapping("testJur") public AjaxJson testJur() { System.out.println("======================= 进入方法,测试权限接口 ========================= "); System.out.println("是否具有权限101" + StpUtil.hasPermission("101")); System.out.println("是否具有权限user-add" + StpUtil.hasPermission("user-add")); System.out.println("是否具有权限article-get" + StpUtil.hasPermission("article-get")); System.out.println("没有user-add权限就抛出异常"); StpUtil.checkPermission("user-add"); System.out.println("在【101、102】中只要拥有一个就不会抛出异常"); StpUtil.checkPermissionOr("101", "102"); System.out.println("在【101、102】中必须全部拥有才不会抛出异常"); StpUtil.checkPermissionAnd("101", "102"); System.out.println("权限测试通过"); return AjaxJson.getSuccess(); } // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session @Mapping("session") public AjaxJson session() { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("测试取值name:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", new Date()); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getSession().get("name")); System.out.println( ONode.stringify(StpUtil.getSession())); return AjaxJson.getSuccess(); } // 测试自定义session接口, 浏览器访问: http://localhost:8081/test/session2 @Mapping("session2") public AjaxJson session2() { System.out.println("======================= 进入方法,测试自定义session接口 ========================= "); // 自定义session就是无需登录也可以使用 的session :比如拿用户的手机号当做 key, 来获取 session System.out.println("自定义 session的id为:" + SaSessionCustomUtil.getSessionById("1895544896").getId()); System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); SaSessionCustomUtil.getSessionById("1895544896").set("name", "张三"); // 写入值 System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); return AjaxJson.getSuccess(); } // ---------- // 测试token专属session, 浏览器访问: http://localhost:8081/test/getTokenSession @Mapping("getTokenSession") public AjaxJson getTokenSession() { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前token专属session: " + StpUtil.getTokenSession().getId()); System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); StpUtil.getTokenSession().set("name", "张三"); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); return AjaxJson.getSuccess(); } // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo @Mapping("tokenInfo") public AjaxJson tokenInfo() { System.out.println("======================= 进入方法,打印当前token信息 ========================= "); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); System.out.println(tokenInfo); return AjaxJson.getSuccessData(tokenInfo); } // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atCheck @SaCheckLogin // 注解式鉴权:当前会话必须登录才能通过 @SaCheckRole("super-admin") // 注解式鉴权:当前会话必须具有指定角色标识才能通过 @SaCheckPermission("user-add") // 注解式鉴权:当前会话必须具有指定权限才能通过 @Mapping("atCheck") public AjaxJson atCheck() { System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= "); System.out.println("只有通过注解鉴权,才能进入此方法"); // StpUtil.checkActiveTimeout(); // StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess(); } // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atJurOr @Mapping("atJurOr") @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) // 注解式鉴权:只要具有其中一个权限即可通过校验 public AjaxJson atJurOr() { return AjaxJson.getSuccessData("用户信息"); } // [活动时间] 续签: http://localhost:8081/test/rene @Mapping("rene") public AjaxJson rene() { StpUtil.checkActiveTimeout(); StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess("续签成功"); } // 测试踢人下线 浏览器访问: http://localhost:8081/test/kickOut @Mapping("kickOut") public AjaxJson kickOut() { // 先登录上 StpUtil.login(10001); // 踢下线 StpUtil.kickout(10001); // 再尝试获取 StpUtil.getLoginId(); // 返回 return AjaxJson.getSuccess(); } // 测试登录接口, 按照设备类型登录, 浏览器访问: http://localhost:8081/test/login2 @Mapping("login2") public AjaxJson login2(@Param(defaultValue="10001") String id, @Param(defaultValue="PC") String device) { StpUtil.login(id, device); return AjaxJson.getSuccess(); } // 测试身份临时切换: http://localhost:8081/test/switchTo @Mapping("switchTo") public AjaxJson switchTo() { System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull()); System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); StpUtil.switchTo(10044, () -> { System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId()); }); System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); return AjaxJson.getSuccess(); } // 测试会话治理 浏览器访问: http://localhost:8081/test/search @Mapping("search") public AjaxJson search() { System.out.println("--------------"); Ttime t = new Ttime().start(); List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10, true); for (String v : tokenValue) { // SaSession session = StpUtil.getSessionBySessionId(sid); System.out.println(v); } System.out.println("用时:" + t.end().toString()); return AjaxJson.getSuccess(); } // 测试指定设备类型登录 浏览器访问: http://localhost:8081/test/loginByDevice @Mapping("loginByDevice") public AjaxJson loginByDevice() { System.out.println("--------------"); StpUtil.login(10001, "PC"); return AjaxJson.getSuccessData("登录成功"); } // 测试 浏览器访问: http://localhost:8081/test/test @Mapping("test") public AjaxJson test() { System.out.println("进来了"); return AjaxJson.getSuccess("访问成功"); } // 测试 浏览器访问: http://localhost:8081/test/test2 @Mapping("test2") public AjaxJson test2() { return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/UserController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; /** * 登录测试 * @author click33 * @author noear */ @Controller @Mapping("/user/") public class UserController { // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 @Mapping("doLogin") public String doLogin(String username, String password) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登录成功"; } return "登录失败"; } // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin @Mapping("isLogin") public String isLogin(String username, String password) { return "当前会话是否登录:" + StpUtil.isLogin(); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-solon/src/main/resources/app.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true sa-token-dao: #名字可以随意取 redis: server: "localhost:6379" # password: 123456 db: 1 maxTotal: 200 ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-solon-redisson 0.0.1-SNAPSHOT org.noear solon-parent 3.2.1 1.45.0 UTF-8 UTF-8 org.noear solon-web org.noear solon.logging.simple org.noear redisson-solon-plugin cn.dev33 sa-token-solon-plugin ${sa-token.version} cn.dev33 sa-token-redisson ${sa-token.version} org.apache.maven.plugins maven-compiler-plugin 3.8.1 -parameters 1.8 1.8 UTF-8 ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; /** * sa-token整合 solon 示例 * @author noear * */ @SolonMain public class SaTokenDemoApp { public static void main(String[] args) { Solon.start(SaTokenDemoApp.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java ================================================ package com.pj.satoken; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.log.SaLogForConsole; import cn.dev33.satoken.util.StrFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 将 Sa-Token log 信息转接到 slf4j 接口 * * @author noear 2022/11/14 created */ //@Component public class SaLogForSlf4j extends SaLogForConsole implements SaLog { static final Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); /** * 打印日志到控制台 * * @param level 日志等级 * @param str 字符串 * @param args 参数列表 */ public void println(int level, String str, Object... args) { SaTokenConfig config = SaManager.getConfig(); if (config.getIsLog() && level >= config.getLogLevelInt()) { switch (level) { case trace: log.trace(LOG_PREFIX + StrFormatter.format(str, args)); break; case debug: log.debug(LOG_PREFIX + StrFormatter.format(str, args)); break; case info: log.info(LOG_PREFIX + StrFormatter.format(str, args)); break; case warn: log.warn(LOG_PREFIX + StrFormatter.format(str, args)); break; case error: case fatal: log.error(LOG_PREFIX + StrFormatter.format(str, args)); break; } } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java ================================================ package com.pj.satoken; import org.noear.solon.core.util.LogUtil; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.log.SaLogForConsole; import cn.dev33.satoken.util.StrFormatter; /** * 将 Sa-Token log 信息转接到 Solon * * @author click33 * @since 2022-11-2 */ //@Component public class SaLogForSolon extends SaLogForConsole implements SaLog { /** * 打印日志到控制台 * * @param level 日志等级 * @param str 字符串 * @param args 参数列表 */ public void println(int level, String str, Object... args) { SaTokenConfig config = SaManager.getConfig(); if (config.getIsLog() && level >= config.getLogLevelInt()) { switch (level) { case trace: LogUtil.global().trace(LOG_PREFIX + StrFormatter.format(str, args)); break; case debug: LogUtil.global().debug(LOG_PREFIX + StrFormatter.format(str, args)); break; case info: LogUtil.global().info(LOG_PREFIX + StrFormatter.format(str, args)); break; case warn: LogUtil.global().warn(LOG_PREFIX + StrFormatter.format(str, args)); break; case error: case fatal: LogUtil.global().error(LOG_PREFIX + StrFormatter.format(str, args)); break; } } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisson; import cn.dev33.satoken.solon.integration.SaTokenInterceptor; import com.pj.util.AjaxJson; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; import org.redisson.api.RedissonClient; import org.redisson.solon.RedissonSupplier; /** * [Sa-Token 权限认证] 配置类 * @author click33 * @author noear */ @Configuration public class SaTokenConfigure { /** * 注册 [sa-token全局过滤器] */ @Bean(index = -100) public SaTokenInterceptor tokenPathFilter() { return new SaTokenInterceptor() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**").addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(r -> { // System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> new Object()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return AjaxJson.getError(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-Frame-Options", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }); } /** * 构造 RedissonClient * */ @Bean public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) { return supplier.get(); } /** * 构建 SaTokenDao * */ @Bean public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) { return new SaTokenDaoForRedisson(redissonClient); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import cn.dev33.satoken.stp.StpInterface; import org.noear.solon.annotation.Component; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被 solon 扫描,即可完成 sa-token 的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java ================================================ package com.pj.test; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.*; import org.noear.solon.annotation.Component; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 全局异常处理 * * @author noear */ @Component public class GlobalExceptionFilter implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { chain.doFilter(ctx); } catch (SaTokenException e) { // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if (e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if (e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if (e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } ctx.render(aj); } } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java ================================================ package com.pj.test; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import com.pj.util.AjaxJson; import cn.dev33.satoken.stp.StpUtil; import org.noear.solon.annotation.Param; /** * 测试: 同域单点登录 * @author click33 * @author noear */ @Controller @Mapping("/sso/") public class SSOController { // 测试:进行登录 @Mapping("doLogin") public AjaxJson doLogin(@Param(defaultValue = "10001") String id) { System.out.println("---------------- 进行登录 "); StpUtil.login(id); return AjaxJson.getSuccess("登录成功: " + id); } // 测试:是否登录 @Mapping("isLogin") public AjaxJson isLogin() { System.out.println("---------------- 是否登录 "); boolean isLogin = StpUtil.isLogin(); return AjaxJson.getSuccess("是否登录: " + isLogin); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import com.pj.util.AjaxJson; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; /** * 压力测试 * @author click33 * @author noear */ @Controller @Mapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @Mapping("login") public AjaxJson login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import java.util.Date; import java.util.List; import com.pj.util.AjaxJson; import com.pj.util.Ttime; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.session.SaSessionCustomUtil; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import org.noear.snack.ONode; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import org.noear.solon.annotation.Param; /** * 测试专用Controller * @author click33 * @author noear */ @Controller @Mapping("/test/") public class TestController { // 测试登录接口, 浏览器访问: http://localhost:8081/test/login @Mapping("login") public AjaxJson login(@Param(defaultValue="10001") String id) { System.out.println("======================= 进入方法,测试登录接口 ========================= "); System.out.println("当前会话的token:" + StpUtil.getTokenValue()); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull()); StpUtil.login(id); // 在当前会话登录此账号 System.out.println("登录成功"); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginId()); // System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt()); System.out.println("当前登录设备:" + StpUtil.getLoginDevice()); // System.out.println("当前token信息:" + StpUtil.getTokenInfo()); return AjaxJson.getSuccess(); } // 测试退出登录 , 浏览器访问: http://localhost:8081/test/logout @Mapping("logout") public AjaxJson logout() { StpUtil.logout(); // StpUtil.logoutByLoginId(10001); return AjaxJson.getSuccess(); } // 测试角色接口, 浏览器访问: http://localhost:8081/test/testRole @Mapping("testRole") public AjaxJson testRole() { System.out.println("======================= 进入方法,测试角色接口 ========================= "); System.out.println("是否具有角色标识 user " + StpUtil.hasRole("user")); System.out.println("是否具有角色标识 admin " + StpUtil.hasRole("admin")); System.out.println("没有admin权限就抛出异常"); StpUtil.checkRole("admin"); System.out.println("在【admin、user】中只要拥有一个就不会抛出异常"); StpUtil.checkRoleOr("admin", "user"); System.out.println("在【admin、user】中必须全部拥有才不会抛出异常"); StpUtil.checkRoleAnd("admin", "user"); System.out.println("角色测试通过"); return AjaxJson.getSuccess(); } // 测试权限接口, 浏览器访问: http://localhost:8081/test/testJur @Mapping("testJur") public AjaxJson testJur() { System.out.println("======================= 进入方法,测试权限接口 ========================= "); System.out.println("是否具有权限101" + StpUtil.hasPermission("101")); System.out.println("是否具有权限user-add" + StpUtil.hasPermission("user-add")); System.out.println("是否具有权限article-get" + StpUtil.hasPermission("article-get")); System.out.println("没有user-add权限就抛出异常"); StpUtil.checkPermission("user-add"); System.out.println("在【101、102】中只要拥有一个就不会抛出异常"); StpUtil.checkPermissionOr("101", "102"); System.out.println("在【101、102】中必须全部拥有才不会抛出异常"); StpUtil.checkPermissionAnd("101", "102"); System.out.println("权限测试通过"); return AjaxJson.getSuccess(); } // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session @Mapping("session") public AjaxJson session() { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("测试取值name:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", new Date()); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getSession().get("name")); System.out.println( ONode.stringify(StpUtil.getSession())); return AjaxJson.getSuccess(); } // 测试自定义session接口, 浏览器访问: http://localhost:8081/test/session2 @Mapping("session2") public AjaxJson session2() { System.out.println("======================= 进入方法,测试自定义session接口 ========================= "); // 自定义session就是无需登录也可以使用 的session :比如拿用户的手机号当做 key, 来获取 session System.out.println("自定义 session的id为:" + SaSessionCustomUtil.getSessionById("1895544896").getId()); System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); SaSessionCustomUtil.getSessionById("1895544896").set("name", "张三"); // 写入值 System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); return AjaxJson.getSuccess(); } // ---------- // 测试token专属session, 浏览器访问: http://localhost:8081/test/getTokenSession @Mapping("getTokenSession") public AjaxJson getTokenSession() { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前token专属session: " + StpUtil.getTokenSession().getId()); System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); StpUtil.getTokenSession().set("name", "张三"); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); return AjaxJson.getSuccess(); } // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo @Mapping("tokenInfo") public AjaxJson tokenInfo() { System.out.println("======================= 进入方法,打印当前token信息 ========================= "); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); System.out.println(tokenInfo); return AjaxJson.getSuccessData(tokenInfo); } // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atCheck @SaCheckLogin // 注解式鉴权:当前会话必须登录才能通过 @SaCheckRole("super-admin") // 注解式鉴权:当前会话必须具有指定角色标识才能通过 @SaCheckPermission("user-add") // 注解式鉴权:当前会话必须具有指定权限才能通过 @Mapping("atCheck") public AjaxJson atCheck() { System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= "); System.out.println("只有通过注解鉴权,才能进入此方法"); // StpUtil.checkActiveTimeout(); // StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess(); } // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atJurOr @Mapping("atJurOr") @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) // 注解式鉴权:只要具有其中一个权限即可通过校验 public AjaxJson atJurOr() { return AjaxJson.getSuccessData("用户信息"); } // [活动时间] 续签: http://localhost:8081/test/rene @Mapping("rene") public AjaxJson rene() { StpUtil.checkActiveTimeout(); StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess("续签成功"); } // 测试踢人下线 浏览器访问: http://localhost:8081/test/kickOut @Mapping("kickOut") public AjaxJson kickOut() { // 先登录上 StpUtil.login(10001); // 踢下线 StpUtil.kickout(10001); // 再尝试获取 StpUtil.getLoginId(); // 返回 return AjaxJson.getSuccess(); } // 测试登录接口, 按照设备类型登录, 浏览器访问: http://localhost:8081/test/login2 @Mapping("login2") public AjaxJson login2(@Param(defaultValue="10001") String id, @Param(defaultValue="PC") String device) { StpUtil.login(id, device); return AjaxJson.getSuccess(); } // 测试身份临时切换: http://localhost:8081/test/switchTo @Mapping("switchTo") public AjaxJson switchTo() { System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull()); System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); StpUtil.switchTo(10044, () -> { System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId()); }); System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); return AjaxJson.getSuccess(); } // 测试会话治理 浏览器访问: http://localhost:8081/test/search @Mapping("search") public AjaxJson search() { System.out.println("--------------"); Ttime t = new Ttime().start(); List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10, true); for (String v : tokenValue) { // SaSession session = StpUtil.getSessionBySessionId(sid); System.out.println(v); } System.out.println("用时:" + t.end().toString()); return AjaxJson.getSuccess(); } // 测试指定设备类型登录 浏览器访问: http://localhost:8081/test/loginByDevice @Mapping("loginByDevice") public AjaxJson loginByDevice() { System.out.println("--------------"); StpUtil.login(10001, "PC"); return AjaxJson.getSuccessData("登录成功"); } // 测试 浏览器访问: http://localhost:8081/test/test @Mapping("test") public AjaxJson test() { System.out.println("进来了"); return AjaxJson.getSuccess("访问成功"); } // 测试 浏览器访问: http://localhost:8081/test/test2 @Mapping("test2") public AjaxJson test2() { return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; /** * 登录测试 * @author click33 * @author noear */ @Controller @Mapping("/user/") public class UserController { // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 @Mapping("doLogin") public String doLogin(String username, String password) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登录成功"; } return "登录失败"; } // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin @Mapping("isLogin") public String isLogin(String username, String password) { return "当前会话是否登录:" + StpUtil.isLogin(); } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true sa-token-dao: config: | singleServerConfig: password: "123456" address: "redis://localhost:6379" database: 0 ================================================ FILE: sa-token-demo/sa-token-demo-springboot/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-springboot 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/SaTokenDemoApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; /** * Sa-Token整合SpringBoot 示例 * @author click33 * */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 全局异常处理 */ @ControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ResponseBody @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.pj.util.AjaxJson; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return AjaxJson.getError(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/AtController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckHttpBasic; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8081/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.util.AjaxJson; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public AjaxJson login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import java.util.Date; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.pj.util.AjaxJson; import com.pj.util.Ttime; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.session.SaSessionCustomUtil; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试登录接口, 浏览器访问: http://localhost:8081/test/login @RequestMapping("login") public AjaxJson login(@RequestParam(defaultValue="10001") String id) { System.out.println("======================= 进入方法,测试登录接口 ========================= "); System.out.println("当前会话的token:" + StpUtil.getTokenValue()); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull()); StpUtil.login(id); // 在当前会话登录此账号 System.out.println("登录成功"); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号:" + StpUtil.getLoginId()); // System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt()); System.out.println("当前登录设备:" + StpUtil.getLoginDevice()); // System.out.println("当前token信息:" + StpUtil.getTokenInfo()); return AjaxJson.getSuccess(); } // 测试退出登录 , 浏览器访问: http://localhost:8081/test/logout @RequestMapping("logout") public AjaxJson logout() { StpUtil.logout(); // StpUtil.logoutByLoginId(10001); return AjaxJson.getSuccess(); } // 测试角色接口, 浏览器访问: http://localhost:8081/test/testRole @RequestMapping("testRole") public AjaxJson testRole() { System.out.println("======================= 进入方法,测试角色接口 ========================= "); System.out.println("是否具有角色标识 user " + StpUtil.hasRole("user")); System.out.println("是否具有角色标识 admin " + StpUtil.hasRole("admin")); System.out.println("没有admin权限就抛出异常"); StpUtil.checkRole("admin"); System.out.println("在【admin、user】中只要拥有一个就不会抛出异常"); StpUtil.checkRoleOr("admin", "user"); System.out.println("在【admin、user】中必须全部拥有才不会抛出异常"); StpUtil.checkRoleAnd("admin", "user"); System.out.println("角色测试通过"); return AjaxJson.getSuccess(); } // 测试权限接口, 浏览器访问: http://localhost:8081/test/testJur @RequestMapping("testJur") public AjaxJson testJur() { System.out.println("======================= 进入方法,测试权限接口 ========================= "); System.out.println("是否具有权限101" + StpUtil.hasPermission("101")); System.out.println("是否具有权限user-add" + StpUtil.hasPermission("user-add")); System.out.println("是否具有权限article-get" + StpUtil.hasPermission("article-get")); System.out.println("没有user-add权限就抛出异常"); StpUtil.checkPermission("user-add"); System.out.println("在【101、102】中只要拥有一个就不会抛出异常"); StpUtil.checkPermissionOr("101", "102"); System.out.println("在【101、102】中必须全部拥有才不会抛出异常"); StpUtil.checkPermissionAnd("101", "102"); System.out.println("权限测试通过"); return AjaxJson.getSuccess(); } // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session @RequestMapping("session") public AjaxJson session() throws JsonProcessingException { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("当前登录账号session的id" + StpUtil.getSession().getId()); System.out.println("测试取值name:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", new Date()); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getSession().get("name")); System.out.println( new ObjectMapper().writeValueAsString(StpUtil.getSession())); return AjaxJson.getSuccess(); } // 测试自定义session接口, 浏览器访问: http://localhost:8081/test/session2 @RequestMapping("session2") public AjaxJson session2() { System.out.println("======================= 进入方法,测试自定义session接口 ========================= "); // 自定义session就是无需登录也可以使用 的session :比如拿用户的手机号当做 key, 来获取 session System.out.println("自定义 session的id为:" + SaSessionCustomUtil.getSessionById("1895544896").getId()); System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); SaSessionCustomUtil.getSessionById("1895544896").set("name", "张三"); // 写入值 System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name")); return AjaxJson.getSuccess(); } // ---------- // 测试token专属session, 浏览器访问: http://localhost:8081/test/getTokenSession @RequestMapping("getTokenSession") public AjaxJson getTokenSession() { System.out.println("======================= 进入方法,测试会话session接口 ========================= "); System.out.println("当前是否登录:" + StpUtil.isLogin()); System.out.println("当前token专属session: " + StpUtil.getTokenSession().getId()); System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); StpUtil.getTokenSession().set("name", "张三"); // 写入一个值 System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name")); return AjaxJson.getSuccess(); } // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo @RequestMapping("tokenInfo") public AjaxJson tokenInfo() { System.out.println("======================= 进入方法,打印当前token信息 ========================= "); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); System.out.println(tokenInfo); return AjaxJson.getSuccessData(tokenInfo); } // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atCheck @SaCheckLogin // 注解式鉴权:当前会话必须登录才能通过 @SaCheckRole("super-admin") // 注解式鉴权:当前会话必须具有指定角色标识才能通过 @SaCheckPermission("user-add") // 注解式鉴权:当前会话必须具有指定权限才能通过 @RequestMapping("atCheck") public AjaxJson atCheck() { System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= "); System.out.println("只有通过注解鉴权,才能进入此方法"); // StpUtil.checkActiveTimeout(); // StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess(); } // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atJurOr @RequestMapping("atJurOr") @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) // 注解式鉴权:只要具有其中一个权限即可通过校验 public AjaxJson atJurOr() { return AjaxJson.getSuccessData("用户信息"); } // [活动时间] 续签: http://localhost:8081/test/rene @RequestMapping("rene") public AjaxJson rene() { StpUtil.checkActiveTimeout(); StpUtil.updateLastActiveToNow(); return AjaxJson.getSuccess("续签成功"); } // 测试踢人下线 浏览器访问: http://localhost:8081/test/kickOut @RequestMapping("kickOut") public AjaxJson kickOut() { // 先登录上 StpUtil.login(10001); // 踢下线 StpUtil.kickout(10001); // 再尝试获取 StpUtil.getLoginId(); // 返回 return AjaxJson.getSuccess(); } // 测试登录接口, 按照设备登录, 浏览器访问: http://localhost:8081/test/login2 @RequestMapping("login2") public AjaxJson login2(@RequestParam(defaultValue="10001") String id, @RequestParam(defaultValue="PC") String device) { StpUtil.login(id, device); return AjaxJson.getSuccess(); } // 测试身份临时切换: http://localhost:8081/test/switchTo @RequestMapping("switchTo") public AjaxJson switchTo() { System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull()); System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); StpUtil.switchTo(10044, () -> { System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId()); }); System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); return AjaxJson.getSuccess(); } // 测试会话治理 浏览器访问: http://localhost:8081/test/search @RequestMapping("search") public AjaxJson search() { System.out.println("--------------"); Ttime t = new Ttime().start(); List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10, true); for (String v : tokenValue) { // SaSession session = StpUtil.getSessionBySessionId(sid); System.out.println(v); } System.out.println("用时:" + t.end().toString()); return AjaxJson.getSuccess(); } // 测试指定设备登录 浏览器访问: http://localhost:8081/test/loginByDevice @RequestMapping("loginByDevice") public AjaxJson loginByDevice() { System.out.println("--------------"); StpUtil.login(10001, "PC"); return AjaxJson.getSuccessData("登录成功"); } // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public AjaxJson test() { System.out.println("------------进来了"); return AjaxJson.getSuccess(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public AjaxJson test2() { return AjaxJson.getSuccess(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-springboot-low-version/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-springboot-low-version 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.1.18.RELEASE 1.45.0 com.pj.SaTokenApplication 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.hutool hutool-all 5.8.36 cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 com.fasterxml.jackson.core jackson-core 2.17.3 com.fasterxml.jackson.core jackson-annotations 2.17.3 com.fasterxml.jackson.core jackson-databind 2.17.3 src/main/java **/*.xml src/main/resources **/*.* org.apache.maven.plugins maven-jar-plugin true lib/ ${java.run.main.class} org.apache.maven.plugins maven-dependency-plugin copy package copy-dependencies ${project.build.directory}/lib ================================================ FILE: sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/SaTokenApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 测试 * @author click33 * */ @SpringBootApplication public class SaTokenApplication { public static void main(String[] args) { SpringApplication.run(SaTokenApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); // System.out.println(StpUtil.getSessionByLoginId(10001)); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 返回给前端 return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // 输出 API 请求日志,方便调试代码 // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 .setBeforeAuth(obj -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") // ---------- 设置跨域响应头 ---------- // 允许指定域访问跨域资源 .setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); StpUtil.getTokenSession(); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 校验登录 ---- http://localhost:8081/acc/checkLogin @RequestMapping("checkLogin") public SaResult checkLogin() { StpUtil.checkLogin(); return SaResult.ok(); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 查询账号登录设备信息 ---- http://localhost:8081/acc/terminalInfo @RequestMapping("terminalInfo") public SaResult terminalInfo() { System.out.println("账号 10001 登录设备信息:"); List terminalList = StpUtil.getTerminalListByLoginId(10001); for (SaTerminalInfo ter : terminalList) { System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime()); } return SaResult.data(terminalList); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.login(10001, SaLoginParameter.create().setIsConcurrent(false)); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-low-version/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-springboot-redis 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redis-jackson ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/SaTokenDemoApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token整合SpringBoot 示例,整合redis * @author click33 * */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 (BeforeAuth不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/AtController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckHttpBasic; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8081/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public SaResult login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redis/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-springboot-redisson 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-redisson-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-jackson ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/SaTokenDemoApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; /** * Sa-Token整合SpringBoot 示例,整合redis * @author click33 * */ @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonConfig.java ================================================ package com.pj.redisson; import org.redisson.config.SingleServerConfig; import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * redisson 配置 * * @author 疯狂的狮子Li */ @Configuration @EnableConfigurationProperties(RedissonProperties.class) public class RedissonConfig { @Autowired private RedissonProperties redissonProperties; /** * 自定义Redisson配置注入器 被RedissonAutoConfiguration调用执行 * 具体参考 {@link org.redisson.spring.starter.RedissonAutoConfiguration} *

* 使用自定义配置类手动注入配置数据 * 也可根据redisson官网使用properties文件配置 */ @Bean public RedissonAutoConfigurationCustomizer redissonCustomizer() { return config -> { config.setThreads(redissonProperties.getThreads()); config.setNettyThreads(redissonProperties.getNettyThreads()); SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig(); if (singleServerConfig != null) { // 使用单机模式 config.useSingleServer() .setTimeout(singleServerConfig.getTimeout()) .setClientName(singleServerConfig.getClientName()) .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout()) .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize()) .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize()) .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize()); } }; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonProperties.java ================================================ package com.pj.redisson; import org.redisson.config.SingleServerConfig; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.stereotype.Component; /** * Redisson 配置属性 * * @author 疯狂的狮子Li */ @Component @ConfigurationProperties(prefix = "redisson") public class RedissonProperties { /** * 线程池数量,默认值 = 当前处理核数量 * 2 */ private int threads; /** * Netty线程池数量,默认值 = 当前处理核数量 * 2 */ private int nettyThreads; /** * 单机服务配置 */ @NestedConfigurationProperty private SingleServerConfig singleServerConfig; public int getThreads() { return threads; } public void setThreads(int threads) { this.threads = threads; } public int getNettyThreads() { return nettyThreads; } public void setNettyThreads(int nettyThreads) { this.nettyThreads = nettyThreads; } public SingleServerConfig getSingleServerConfig() { return singleServerConfig; } public void setSingleServerConfig(SingleServerConfig singleServerConfig) { this.singleServerConfig = singleServerConfig; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.util.SaResult; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/AtController.java ================================================ package com.pj.test; import cn.dev33.satoken.annotation.*; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8081/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public SaResult login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot-redisson/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: localhost # Redis服务器连接端口 port: 6379 # Redis服务器连接密码 为空需注释掉 # password: # 连接超时时间 timeout: 10s redisson: # 线程池数量 threads: 8 # Netty线程池数量 nettyThreads: 32 # 单节点配置 singleServerConfig: # 客户端名称 clientName: test # 最小空闲连接数 connectionMinimumIdleSize: 8 # 连接池大小 connectionPoolSize: 32 # 连接空闲超时,单位:毫秒 idleConnectionTimeout: 10000 # 命令等待超时,单位:毫秒 timeout: 3000 # 如果尝试在此限制之内发送成功,则开始启用 timeout 计时。 retryAttempts: 3 # 命令重试发送时间间隔,单位:毫秒 retryInterval: 1500 # 发布和订阅连接池大小 subscriptionConnectionPoolSize: 50 ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-springboot3-redis 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.5.11 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot3-starter ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/SaTokenSpringBoot3Application.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 整合 SpringBoot3 示例,整合redis * @author click33 * */ @SpringBootApplication public class SaTokenSpringBoot3Application { public static void main(String[] args) { SpringApplication.run(SaTokenSpringBoot3Application.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.util.SaResult; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/AtController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckHttpBasic; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8081/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public SaResult login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); System.out.println(SpringMVCUtil.getRequest()); System.out.println(SaTokenContextJakartaServletUtil.getRequest()); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/getRequestPath @RequestMapping("getRequestPath") public SaResult getRequestPath() { System.out.println("-------------- 测试请求 path 获取"); System.out.println("request.getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); System.out.println("saRequest.getRequestPath() " + SaHolder.getRequest().getRequestPath()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot3-redis/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-springboot4-redis 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 4.0.3 1.45.0 org.springframework.boot spring-boot-starter-webmvc org.springframework.boot spring-boot-starter-aspectj cn.dev33 sa-token-spring-boot4-starter ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/SaTokenSpringBoot4Application.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 整合 SpringBoot4 示例,整合 redis * @author click33 * */ @SpringBootApplication public class SaTokenSpringBoot4Application { public static void main(String[] args) { SpringApplication.run(SaTokenSpringBoot4Application.class, args); System.out.println("\n🎉 启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/NotFoundHandle.java ================================================ //package com.pj.current; // //import java.io.IOException; // //import org.springframework.boot.web.servlet.error.ErrorController; //import org.springframework.web.bind.annotation.RequestMapping; //import org.springframework.web.bind.annotation.RestController; // //import cn.dev33.satoken.util.SaResult; //import jakarta.servlet.http.HttpServletRequest; //import jakarta.servlet.http.HttpServletResponse; // ///** // * 处理 404 // * @author click33 // */ //@RestController //public class NotFoundHandle implements ErrorController { // // @RequestMapping("/error") // public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { // response.setStatus(200); // return SaResult.get(404, "not found", null); // } // //} ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.util.SaResult; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // System.out.println("---------- sa全局认证 " + SaHolder.getRequest().getRequestPath()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/AtController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckHttpBasic; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8082/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8082/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8082/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8082/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8082/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8082/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8082/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8082/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/FaviconController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class FaviconController { @RequestMapping("/favicon.ico") public String favicon() { return ""; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8082/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8082/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8082/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8082/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8082/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public SaResult login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试 浏览器访问: http://localhost:8082/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了"); System.out.println(SpringMVCUtil.getRequest()); System.out.println(SaTokenContextJakartaServletUtil.getRequest()); return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8082/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8082/test/getRequestPath @RequestMapping("getRequestPath") public SaResult getRequestPath() { System.out.println("-------------- 测试请求 path 获取"); System.out.println("request.getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); System.out.println("saRequest.getRequestPath() " + SaHolder.getRequest().getRequestPath()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-springboot4-redis/src/main/resources/application.yml ================================================ # 端口 server: port: 8082 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-sse/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sse 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.7.18 1.45.0 com.pj.SaTokenSseApplication org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.hutool hutool-all 5.8.36 cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 测试 * @author click33 * */ @SpringBootApplication public class SaTokenSseApplication { // SSE 连接测试在线工具:https://toolshu.com/sse public static void main(String[] args) { SpringApplication.run(SaTokenSseApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * CORS 跨域处理 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.util.SseEmitterHolder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?uid=10001 @RequestMapping("doLogin") public SaResult doLogin(@RequestParam(defaultValue = "10001") long uid) { StpUtil.login(uid); return SaResult.data(StpUtil.getTokenInfo()); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { if(StpUtil.isLogin()) { long uid = StpUtil.getLoginIdAsLong(); SseEmitterHolder.closeByUid(uid); StpUtil.logout(); } return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import com.pj.util.SseEmitterHolder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * SSE 推送 */ @RestController public class SseAdminController { // 推送消息 --- http://localhost:8081/sse/send?uid=10001&message=hello123 @RequestMapping(value = "/sse/send") public SaResult sendMessage(long uid, String message) { SseEmitterHolder.sendMessageByUid(uid, message); return SaResult.ok(); } // 断开 --- http://localhost:8081/sse/close?uid=10001 @RequestMapping(value = "/sse/close") public SaResult close(long uid){ SseEmitterHolder.closeByUid(uid); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java ================================================ package com.pj.test; import com.pj.util.SseEmitterHolder; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; /** * SSE 连接 */ @RestController public class SseController { // 创建连接 --- http://localhost:8081/sse?satoken=d8a8e1c7-62a4-4656-8b54-cc14e6348ceb @RequestMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter createSse(String satoken) { return SseEmitterHolder.createSse(satoken); } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java ================================================ package com.pj.util; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * SSE 连接管理器 * * @author click33 * @since 2025/4/11 */ public class SseEmitterHolder { public static final Map sseEmitterMap = new ConcurrentHashMap<>(); /** * 创建客户端 */ public static SseEmitter createSse(String satoken) { Object loginId = StpUtil.getLoginIdByToken(satoken); if(loginId == null) { throw new NotLoginException("无效 token", StpUtil.TYPE, NotLoginException.INVALID_TOKEN); } long uid = SaFoxUtil.getValueByType(loginId, Long.class); // 默认 30 秒超时,设置为 0L 则永不超时 SseEmitter sseEmitter = new SseEmitter(600 * 1000L); sseEmitterMap.put(satoken, sseEmitter); System.out.println("连接成功:satoken=" + satoken + ",uid=" + uid); // 完成后回调 sseEmitter.onCompletion(() -> { System.out.println("结束连接:satoken=" + satoken + ",uid=" + uid); sseEmitterMap.remove(satoken); }); //超时回调 sseEmitter.onTimeout(() -> { System.out.println("连接超时:satoken=" + satoken + ",uid=" + uid); }); //异常回调 sseEmitter.onError( e -> { // try { System.out.println("连接异常:satoken=" + satoken + ",uid=" + uid); System.err.println(e.getMessage()); // sseEmitter.send(SseEmitter.event() // .id(String.valueOf(uid)) // .name("发生异常!") // .data("发生异常请重试!") // .reconnectTime(3000)); // sseEmitterMap.put(uid, sseEmitter); // } catch (IOException ee) { // ee.printStackTrace(); // } }); try { sseEmitter.send(SseEmitter.event().reconnectTime(5000)); } catch (IOException e) { e.printStackTrace(); } return sseEmitter; } /** * 给指定 token 客户端发送消息 * */ public static void sendMessageByToken(String satoken, String message) { SseEmitter sseEmitter = sseEmitterMap.get(satoken); if (sseEmitter == null) { System.out.println("该 token 暂未建立连接:" + satoken); return; } try { sseEmitter.send(SseEmitter.event().reconnectTime(60 * 1000L).data(message)); System.out.println("消息推送成功,token=" + satoken + ", message=" + message); }catch (Exception e) { e.printStackTrace(); // sseEmitterMap.remove(uid); // log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage()); // sseEmitter.complete(); } } /** * 给指定 用户 所有客户端发送消息 * */ public static void sendMessageByUid(long uid, String message) { List tokenList = StpUtil.getTokenValueListByLoginId(uid); for (String token : tokenList) { sendMessageByToken(token, message); } } /** * 指定 token 断开连接 * */ public static void closeByToken(String satoken) { SseEmitter sseEmitter = sseEmitterMap.get(satoken); if (sseEmitter == null) { System.out.println("该 token 暂未建立连接:" + satoken); return; } try { sendMessageByToken(satoken, "连接已断开!"); sseEmitter.complete(); System.out.println("连接已断开,token=" + satoken); }catch (Exception e) { e.printStackTrace(); // sseEmitterMap.remove(uid); // log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage()); // sseEmitter.complete(); } } /** * 指定 uid 断开连接 * */ public static void closeByUid(long uid) { List tokenList = StpUtil.getTokenValueListByLoginId(uid); for (String token : tokenList) { closeByToken(token); } } } ================================================ FILE: sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-ssm/README.md ================================================ ## SSM 架构集成 Sa-Token 示例 说是SSM,其实没有M,仅仅是给使用 SpringMVC 非 SpringBoot 的项目提供一个简单的 Sa-Token 集成示例。 直接运行项目即可,里面注释挺全的,也不必做过多说明了 (其实就是我懒,光搭建起来这个架子就累瘫了,各种版本兼容问题报起错来大汗淋漓,推荐新项目能上 SpringBoot 就赶紧上吧,千万别在SSM上浪费生命)。 推荐 jdk8 + tomcat8。 ================================================ FILE: sa-token-demo/sa-token-demo-ssm/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-ssm war org.apache.maven.plugins maven-compiler-plugin 8 8 0.0.1-SNAPSHOT UTF-8 UTF-8 5.3.7 2.16.1 1.45.0 javax.servlet javax.servlet-api 3.1.0 org.springframework spring-core ${spring.version} org.springframework spring-web ${spring.version} org.springframework spring-oxm ${spring.version} org.springframework spring-tx ${spring.version} org.springframework spring-webmvc ${spring.version} org.springframework spring-context ${spring.version} org.springframework spring-context-support ${spring.version} org.springframework spring-aop ${spring.version} com.fasterxml.jackson.core jackson-core ${jackson.version} com.fasterxml.jackson.core jackson-databind ${jackson.version} cn.dev33 sa-token-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-redis-jackson ${sa-token.version} redis.clients jedis 3.8.0 org.springframework.data spring-data-redis 2.3.9.RELEASE com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.11.2 ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java ================================================ package com.pj.controller; import cn.dev33.satoken.annotation.*; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8080/sa_token_demo_ssm_war/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/LoginController.java ================================================ package com.pj.controller; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { System.out.println("-------- 12344"); // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 校验登录 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/checkLogin @RequestMapping("checkLogin") public SaResult checkLogin() { StpUtil.checkLogin(); return SaResult.ok(); } // 查询 Token 信息 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8080/sa_token_demo_ssm_war/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/PageController.java ================================================ package com.pj.controller; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * 页面访问测试 * @author click33 * @since 2024/4/14 */ @Controller public class PageController { // http://localhost:8080/sa_token_demo_ssm_war/home @RequestMapping("/home") public String index() { System.out.println("------- home页,所有游客可访问"); return "home"; } // http://localhost:8080/sa_token_demo_ssm_war/user @RequestMapping("/user") public String user() { System.out.println("------- user页,登录后才能访问"); StpUtil.checkLogin(); return "user"; } // http://localhost:8080/sa_token_demo_ssm_war/admin @RequestMapping("/admin") public String admin() { System.out.println("------- admin页,具有admin角色才能访问"); StpUtil.checkRole("admin"); return "admin"; } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/TestController.java ================================================ package com.pj.controller; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.stp.SaLoginConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Date; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试登录 ---- http://localhost:8080/sa_token_demo_ssm_war/test/login @RequestMapping("login") public SaResult login(@RequestParam(defaultValue = "10001") long id) { StpUtil.login(id, SaLoginConfig.setActiveTimeout(-1)); return SaResult.ok("登录成功"); } // 测试 浏览器访问: http://localhost:8080/sa_token_demo_ssm_war/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了 " + SaFoxUtil.formatDate(new Date())); // StpUtil.getLoginId(); // 返回 return SaResult.data(null); } // 测试 浏览器访问: http://localhost:8080/sa_token_demo_ssm_war/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8080/sa_token_demo_ssm_war/getRequestPath @RequestMapping("getRequestPath") public SaResult getRequestPath() { System.out.println("------------ 测试访问路径获取 "); // System.out.println("SpringMVCUtil.getRequest().getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); System.out.println("SaHolder.getRequest().getRequestPath() " + SaHolder.getRequest().getRequestPath()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { // 打印堆栈,以供调试 System.err.println("全局异常---------------"); e.printStackTrace(); // 返回给前端 return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import cn.dev33.satoken.util.SaResult; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/model/SysUser.java ================================================ package com.pj.model; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser { public SysUser() {} public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } @Override public String toString() { return "SysUser [id=" + id + ", name=" + name + ", age=" + age + "]"; } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaInterceptorImpl.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.interceptor.SaInterceptor; /** * 路由拦截鉴权测试 * @author click33 * @since 2024/4/15 */ public class SaInterceptorImpl extends SaInterceptor { public SaInterceptorImpl() { super(hadnle->{ System.out.println("-------------- SA 路由拦截鉴权,你访问的是:" + SaHolder.getRequest().getRequestPath()); // System.out.println("你访问的是:" + SaHolder.getRequest().getRequestPath()); // SaRouter.match("/test/test", r -> StpUtil.checkLogin()); // 根据路由划分模块,不同模块不同鉴权 // SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); // SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); // SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); // SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); // SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); // SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); // 更多写法参考:https://sa-token.cc/doc.html#/use/route-check }); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaTokenBeanInjection.java ================================================ package com.pj.satoken; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate; import cn.dev33.satoken.json.SaJsonTemplateForJackson; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.plugin.SaTokenPluginHolder; import cn.dev33.satoken.spring.SaBeanInject; import cn.dev33.satoken.spring.SaTokenContextForSpring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnectionFactory; /** * 手动注入 Sa-Token 所需要的组件 * @author click33 * @since 2024/4/15 */ public class SaTokenBeanInjection { public SaTokenBeanInjection( SaLog log, SaTokenConfig config, @Autowired(required = false) SaTokenPluginHolder pluginHolder, RedisConnectionFactory connectionFactory, String routePrefix ) { System.out.println("---------------- 手动注入 Sa-Token 所需要的组件 start ----------------"); // 日志组件、配置信息 SaBeanInject inject = new SaBeanInject(log, config, pluginHolder); // 基于 Spring 的上下文处理器 inject.setSaTokenContext(new SaTokenContextForSpring()); // 基于 Jackson 的 json解析器 inject.setSaJsonTemplate(new SaJsonTemplateForJackson()); // 基于 Jackson 序列化的 Redis 持久化组件 SaTokenDaoForRedisTemplate saTokenDaoForRedisTemplate = new SaTokenDaoForRedisTemplate(); saTokenDaoForRedisTemplate.init(connectionFactory); inject.setSaTokenDao(saTokenDaoForRedisTemplate); // 权限和角色数据 inject.setStpInterface(new StpInterfaceImpl()); // 项目路由前缀,方便路由拦截鉴权的 ApplicationInfo.routePrefix = routePrefix; // System.out.println(routePrefix); // 注入更多组件 .... // inject.setXxx System.out.println("---------------- 手动注入 Sa-Token 所需要的组件 end ----------------"); } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import cn.dev33.satoken.stp.StpInterface; import java.util.ArrayList; import java.util.List; /** * 自定义权限验证接口扩展 */ public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/resources/applicationContext.xml ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-mvc.xml ================================================ text/html;charset=UTF-8 text/json;charset=UTF-8 application/json;charset=UTF-8 ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-redis.xml ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-sa-token.xml ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/admin.jsp ================================================ <%@ page contentType="text/html;charset=UTF-8" language="java" %> Admin.jsp

Admin.jsp

具有 admin 角色才可以访问

================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/home.jsp ================================================ <%@ page contentType="text/html;charset=UTF-8" language="java" %> Home.jsp

Home.jsp

所有游客可访问

================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/user.jsp ================================================ <%@ page contentType="text/html;charset=UTF-8" language="java" %> User.jsp

User.jsp

登录后才可以访问

================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/web.xml ================================================ yixiao2 contextConfigLocation classpath:applicationContext.xml org.springframework.web.context.ContextLoaderListener springmvc org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring-mvc.xml 1 springmvc / default /static/* 404 /error index.jsp ================================================ FILE: sa-token-demo/sa-token-demo-ssm/src/main/webapp/index.jsp ================================================ <%@ page contentType="text/html;charset=UTF-8" language="java" %> 欢迎页 index.jsp

欢迎页 index.jsp

这是个外置位的 jsp 页面,可以不经过 Controller 直接访问到

================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html ================================================ Sa-Token-SSO-Client端-测试页(前后端分离版-原生h5)

Sa-Token SSO-Client 应用端(前后端分离版-原生h5)

当前是否登录:

登录 - 单应用注销 - 单浏览器注销 - 全端注销 - 账号资料

================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-common.js ================================================ // 服务器接口主机地址 // var baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 var baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 // 封装一下Ajax function ajax(path, data, successFn, errorFn) { console.log('发起请求:', baseUrl + path, JSON.stringify(data)); fetch(baseUrl + path, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'satoken': localStorage.getItem('satoken') }, body: serializeToQueryString(data), }) .then(response => response.json()) .then(res => { console.log('返回数据:', res); if(res.code === 500) { return alert(res.msg); } successFn(res); }) .catch(error => { console.error('请求失败:', error); return alert("异常:" + JSON.stringify(error)); }); } // ------------ 工具方法 --------------- // 从url中查询到指定名称的参数值 function getParam(name, defaultValue) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == name) { return pair[1]; } } return (defaultValue == undefined ? null : defaultValue); } // 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true function serializeToQueryString(obj) { return Object.entries(obj) .filter(([_, value]) => value != null) // 过滤 null 和 undefined .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } // 向指定标签里 set 内容 function setHtml(select, html) { const dom = document.querySelector('.is-login'); if(dom) { dom.innerHTML = html; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-login.html ================================================ Sa-Token-SSO-Client端-登录中转页页 ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/README.md ================================================ # sa-token-demo-sso-client-vue2 Sa-Token SSO-Client 应用端(前后端分离版-Vue2) 在线文档:[https://sa-token.cc/](https://sa-token.cc/) ## 运行 先安装依赖 ``` bat npm install --registry=https://registry.npm.taobao.org ``` 运行 ``` bat npm run serve ``` 打包 ``` bat npm run build ``` ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/jsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "esnext", "baseUrl": "./", "moduleResolution": "node", "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package.json ================================================ { "name": "hello-world", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "axios": "^1.1.3", "core-js": "^3.6.5", "vue": "^2.6.11", "vue-router": "^3.6.5" }, "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-service": "~5.0.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/vue3-essential", "eslint:recommended" ], "parserOptions": { "parser": "@babel/eslint-parser" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead", "not ie 11" ] } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/index.html ================================================ Sa-Token SSO-Client 应用端(前后端分离版-Vue2)
================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/App.vue ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/main.js ================================================ import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false import router from './router' new Vue({ router, render: h => h(App) }).$mount('#app') ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/router/index.js ================================================ import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) /** * 路由表 */ export const routes = [ // 首页 { name: 'index', path: "/index", component: () => import('../views/sso-index.vue') }, // SSO-登录页 { name: 'sso-login', path: '/sso-login', component: () => import('../views/sso-login.vue') }, // 访问 / 时自动重定向到 /index { path: '/', redirect: '/index' } ] const router = new Router({ routes: routes }) export default router ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-common.js ================================================ import axios from 'axios' // sso-client 的后端服务地址 // export const baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 export const baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 // 封装一下 Ajax 方法 export const ajax = function(path, data, successFn) { console.log('发起请求:', baseUrl + path, JSON.stringify(data)); axios({ url: baseUrl + path, method: 'post', data: data, headers: { "Content-Type": "application/x-www-form-urlencoded", "satoken": localStorage.getItem("satoken") } }). then(function (response) { // 成功时执行 const res = response.data; console.log('返回数据:', res); if(res.code === 500) { return alert(res.msg); } successFn(res); }). catch(function (error) { console.error('请求失败:', error); return alert("异常:" + JSON.stringify(error)); }) } // 从url中查询到指定名称的参数值 export const getParam = function(name, defaultValue){ var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-login.vue ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/vue.config.js ================================================ const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true }) ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/README.md ================================================ # sa-token-demo-sso-client-vue3 Sa-Token SSO-Client 应用端(前后端分离版-Vue3) 在线文档:[https://sa-token.cc/](https://sa-token.cc/) ## 运行 先安装依赖 ``` bat npm install --registry=https://registry.npm.taobao.org ``` 运行 ``` bat npm run dev ``` 打包 ``` bat npm run build ``` ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/index.html ================================================ Sa-Token SSO-Client 应用端(前后端分离版-Vue3)
================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package.json ================================================ { "name": "sa-token-demo-sso-client-vue3", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "axios": "^1.1.3", "vue": "^3.2.41", "vue-router": "^4.1.6" }, "devDependencies": { "@vitejs/plugin-vue": "^3.2.0", "vite": "^3.2.7" } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/App.vue ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/main.js ================================================ import { createApp } from 'vue' import App from './App.vue' // createApp const app = createApp(App); // 安装 vue-router import router from './router'; app.use(router); // 绑定dom app.mount('#app'); ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/router/index.js ================================================ import { createRouter, createWebHistory } from 'vue-router'; /** * 创建 vue-router 实例 */ const router = createRouter({ history: createWebHistory(), routes: [ // 首页 { name: 'index', path: "/index", component: () => import('../views/sso-index.vue'), }, // SSO-登录页 { name: 'sso-login', path: '/sso-login', component: () => import('../views/sso-login.vue'), }, // 访问 / 时自动重定向到 /index { path: "/", redirect: '/index' } ], }); // 导出 export default router; ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-common.js ================================================ import axios from 'axios' // sso-client 的后端服务地址 // export const baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 export const baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 // 封装一下 Ajax 方法 export const ajax = function(path, data, successFn) { console.log('发起请求:', baseUrl + path, JSON.stringify(data)); axios({ url: baseUrl + path, method: 'post', data: data, headers: { "Content-Type": "application/x-www-form-urlencoded", "satoken": localStorage.getItem("satoken") } }). then(function (response) { // 成功时执行 const res = response.data; console.log('返回数据:', res); if(res.code === 500) { return alert(res.msg); } successFn(res); }). catch(function (error) { console.error('请求失败:', error); return alert("异常:" + JSON.stringify(error)); }) } // 从url中查询到指定名称的参数值 export const getParam = function(name, defaultValue){ var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-login.vue ================================================ ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/vite.config.js ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()] }) ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso-server 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-thymeleaf cn.dev33 sa-token-forest ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaSsoServerApplication { public static void main(String[] args) { SpringApplication.run(SaSsoServerApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getServerConfig()); System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/H5Controller.java ================================================ package com.pj.h5; import cn.dev33.satoken.sso.template.SaSsoServerUtil; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 前后台分离架构下集成SSO所需的代码 (SSO-Server端) *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

* @author click33 * */ @RestController public class H5Controller { /** * 返回当前是否已经登录 */ @RequestMapping("/sso/isLogin") public SaResult isLogin() { return SaResult.data(StpUtil.isLogin()); } /** * 获取 redirectUrl */ @RequestMapping("/sso/getRedirectUrl") public SaResult getRedirectUrl(String client, String redirect, String mode) { // 未登录情况下,返回 code=401 if(StpUtil.isLogin() == false) { return SaResult.code(401); } // 已登录情况下,构建 redirectUrl redirect = SaFoxUtil.decoderUrl(redirect); if(SaSsoConsts.MODE_SIMPLE.equals(mode)) { // 模式一 SaSsoServerUtil.checkRedirectUrl(client, redirect); return SaResult.data(redirect); } else { // 模式二或模式三 String redirectUrl = SaSsoServerUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getTokenValue()); return SaResult.data(redirectUrl); } } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/SaTokenConfigure.java ================================================ package com.pj.h5; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 (解决跨域问题) * * @author click33 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java ================================================ package com.pj.sso; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import cn.dev33.satoken.util.SaResult; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/HomeController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.stp.StpUtil; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * SSO 平台中心模式示例,跳连接进入子系统 */ @RestController public class HomeController { // 平台化首页 @RequestMapping({"/", "/home"}) public Object index() { // 如果未登录,则先去登录 if(!StpUtil.isLogin()) { return SaHolder.getResponse().redirect("/sso/auth"); } // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; // 组织网页结构返回到前端 String title = "

SSO 平台首页 (平台中心模式)

"; String client1 = "

进入Client1系统

"; String client2 = "

进入Client2系统

"; String client3 = "

进入Client3系统

"; return title + client1 + client2 + client3; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; /** * Sa-Token-SSO Server端 Controller * @author click33 * */ @RestController public class SsoServerController { /** * SSO-Server端:处理所有SSO相关请求 * http://{host}:{port}/sso/auth -- 单点登录授权地址 * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoServerProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:未登录时返回的View ssoServerTemplate.strategy.notLoginView = () -> { return new ModelAndView("sa-login.html"); }; // 配置:登录处理函数 ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { // 此处仅做模拟登录,真实环境应该查询数据库进行登录 if("sa".equals(name) && "123456".equals(pwd)) { String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }; // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { System.out.println("收到消息:" + message); // 自定义返回结果(模拟) return SaResult.ok() .set("id", message.get("loginId")) .set("name", "LinXiaoYu") .set("sex", "女") .set("age", 18); }); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml ================================================ # 端口 server: port: 9000 # Sa-Token 配置 sa-token: # 打印操作日志 is-log: true # SSO 模式一配置 (非模式一不需要配置) # cookie: # # 配置 Cookie 作用域 # domain: stp.com # SSO-Server 配置 sso-server: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 home-route: /home # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) allow-anon-client: true # 所有允许的授权回调地址 (匿名 client 使用) allow-url: "*" # API 接口调用秘钥 (全局默认 + 匿名 client 使用) secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用列表:配置接入的应用信息 clients: # 应用 sso-client1:采用模式一对接 (同域、同Redis) sso-client1: client: sso-client1 allow-url: "*" # 应用 sso-client2:采用模式二对接 (跨域、同Redis) sso-client2: client: sso-client2 allow-url: "*" secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) sso-client3: # 应用名称 client: sso-client3 # 允许授权地址 allow-url: "*" # 是否接收消息推送 is-push: true # 消息推送地址 push-url: http://sa-sso-client1.com:9003/sso/pushC # 接口调用秘钥 (如果不配置则使用全局默认秘钥) secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client3-resdk:采用 ReSdk 模式对接 sso-client3-resdk: # 应用名称 client: sso-client3-resdk # 允许授权地址 allow-url: "*" # 是否接收消息推送 is-push: true # 消息推送地址 push-url: http://sa-sso-client1.com:9005/sso/pushC # 接口调用秘钥 (如果不配置则使用全局默认秘钥) secret-key: SSO-C3-ReSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor spring: # Redis配置 (SSO模式一和模式二使用 Redis 来同步会话) redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 forest: # 关闭 forest 请求日志打印 log-enabled: false ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/layer.js ================================================ /*! layer-v3.1.1 Web弹层组件 MIT License http://layer.layui.com/ By 贤心 */ ;!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
'+(f?r.title[0]:r.title)+"
":"";return r.zIndex=s,t([r.shade?'
':"",'
'+(e&&2!=r.type?"":u)+'
'+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
'+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
'+e+"
"}():"")+(r.resize?'':"")+"
"],u,i('
')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
    '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
  • '+(t[0].content||"no content")+"
  • ";i'+(t[i].content||"no content")+"";return a}()+"
",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
'+(u.length>1?'':"")+'
'+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js ================================================ /*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

'+(e?n.title[0]:n.title)+"

":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
'+e+"
"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

'+(n.content||"")+"

"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
':"")+'
"+l+'
'+n.content+"
"+c+"
",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css ================================================ .layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1E9FFF;background-color:#1E9FFF;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#E9E7E7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:230px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;overflow:hidden;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:43px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.css ================================================ *{margin: 0; padding: 0;} body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} ::-webkit-input-placeholder{color: #ccc;} /* 视图盒子 */ .view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} /* 背景 EAEFF3 */ .bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);} .bg-2{height: 50%; background-color: #EAEFF3;} /* 渐变背景 */ /*.bg-1{ background-size: 500%; background-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5); animation: bganimation 30s infinite; } @keyframes bganimation{ 0%{background-position: 0% 50%;} 50%{background-position: 100% 50%;} 100%{background-position: 0% 50%;} } */ /* 背景 */ .bg-1{background: #101C34;} .bg-2{background: #101C34;} /* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */ /* 内容盒子 */ .content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} /* 登录盒子 */ /* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ .login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;} .login-box{display: flex; align-items: center; text-align: center;} /* 表单 */ .from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} .from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} .from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} /* 输入框 */ .from-item{border: 0px #000 solid; margin-bottom: 15px;} .s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} .s-input{font-size: 12px;} .s-input:focus{border-color: #409eff} /* 登录按钮 */ .s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} .s-btn:hover{background-color: #50aEFF;} /* 重置按钮 */ .reset-box{text-align: left; font-size: 12px;} .reset-box a{text-decoration: none;} .reset-box a:hover{text-decoration: underline;} /* loading框样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js ================================================ // sa var sa = {}; // 打开loading sa.loading = function(msg) { layer.closeAll(); // 开始前先把所有弹窗关了 return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); }; // 隐藏loading sa.hideLoading = function() { layer.closeAll(); }; // ----------------------------------- 登录事件 ----------------------------------- $('.login-btn').click(function(){ sa.loading("正在登录..."); // 开始登录 setTimeout(function() { $.ajax({ url: "sso/doLogin", type: "post", data: { name: $('[name=name]').val(), pwd: $('[name=pwd]').val() }, dataType: 'json', success: function(res){ console.log('返回数据:', res); sa.hideLoading(); if(res.code == 200) { layer.msg('登录成功', {anim: 0, icon: 6 }); setTimeout(function() { location.reload(); }, 800) } else { layer.msg(res.msg, {anim: 6, icon: 2 }); } }, error: function(xhr, type, errorThrown){ sa.hideLoading(); if(xhr.status == 0){ return layer.alert('无法连接到服务器,请检查网络'); } return layer.alert("异常:" + JSON.stringify(xhr)); } }); }, 400); }); // 绑定回车事件 $('[name=name],[name=pwd]').bind('keypress', function(event){ if(event.keyCode == "13") { $('.login-btn').click(); } }); // 输入框获取焦点 $("[name=name]").focus(); // 打印信息 var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/"; console.log(str); ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/templates/sa-login.html ================================================ Sa-SSO-Server 认证中心-登录
This page is provided by Sa-Token-SSO
================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/common.js ================================================ // 服务端地址 var baseUrl = "http://sa-sso-server.com:9000"; // sa var sa = {}; // 打开loading sa.loading = function(msg) { layer.closeAll(); // 开始前先把所有弹窗关了 return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'}); }; // 隐藏loading sa.hideLoading = function() { layer.closeAll(); }; // 封装一下Ajax sa.ajax = function(url, data, successFn) { $.ajax({ url: baseUrl + url, type: "post", data: data, dataType: 'json', headers: { 'X-Requested-With': 'XMLHttpRequest', 'satoken': localStorage.getItem('satoken') }, success: function(res){ console.log('返回数据:', res); successFn(res); }, error: function(xhr, type, errorThrown){ if(xhr.status == 0){ return alert('无法连接到服务器,请检查网络'); } return alert("异常:" + JSON.stringify(xhr)); } }); } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/home.html ================================================ SSO-Server 平台首页

SSO-Server 平台首页 (前后端分离模式) (平台中心模式)

进入Client1系统

进入Client2系统

进入Client3系统

================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.css ================================================ *{margin: 0; padding: 0;} body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} ::-webkit-input-placeholder{color: #ccc;} /* 视图盒子 */ .view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} /* 背景 EAEFF3 */ .bg-1{height: 100%; background: #c0cCf4;} /* 内容盒子 */ .content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} /* 登录盒子 */ /* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ .login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;} .login-box{display: flex; align-items: center; text-align: center;} /* 表单 */ .from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} .from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} .from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} /* 输入框 */ .from-item{border: 0px #000 solid; margin-bottom: 15px;} .s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} .s-input{font-size: 12px;} .s-input:focus{border-color: #409eff} /* 登录按钮 */ .s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} .s-btn:hover{background-color: #50aEFF;} /* 重置按钮 */ .reset-box{text-align: left; font-size: 12px;} .reset-box a{text-decoration: none;} .reset-box a:hover{text-decoration: underline;} /* loading框样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.html ================================================ Sa-SSO-Server 认证中心-登录
This page is provided by Sa-Token-SSO
================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.js ================================================ // ----------------------------------- 相关事件 ----------------------------------- // 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码 var pData = { client: getParam('client', ''), redirect: getParam('redirect', ''), mode: getParam('mode', '') }; // 提供 redirect 参数时,登录后往 redirect 跳转 if(pData.redirect) { sa.ajax("/sso/getRedirectUrl", pData, function(res) { if(res.code == 200) { // 已登录,并且redirect地址有效,开始跳转 location.href = res.data; } else if(res.code == 401) { console.log('未登录'); } else { layer.alert(res.msg); } }) } else { // 未提供 redirect 参数时,登录后往 home 跳转 sa.ajax("/sso/isLogin", {}, function(res) { if(res.data) { location.href = './home.html'; } else { console.log('未登录,请先登录...'); } }) } // 登录 $('.login-btn').click(function(){ sa.loading("正在登录..."); // 开始登录 var data = { name: $('[name=name]').val(), pwd: $('[name=pwd]').val() }; sa.ajax("/sso/doLogin", data, function(res) { sa.hideLoading(); if(res.code == 200) { localStorage.setItem('satoken', res.data); layer.msg('登录成功', {anim: 0, icon: 6 }); setTimeout(function() { location.reload(); }, 800); } else { layer.msg(res.msg, {anim: 6, icon: 2 }); } }) }); // 绑定回车事件 $('[name=name],[name=pwd]').bind('keypress', function(event){ if(event.keyCode == "13") { $('.login-btn').click(); } }); // 输入框获取焦点 $("[name=name]").focus(); // 从url中查询到指定名称的参数值 function getParam(name, defaultValue){ var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i 4.0.0 cn.dev33 sa-token-demo-sso1-client 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/SaSso1ClientApplication.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * SSO模式一,Client端 Demo * @author click33 * */ @SpringBootApplication public class SaSso1ClientApplication { public static void main(String[] args) { SpringApplication.run(SaSso1ClientApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://s1.stp.com:9001"); System.out.println("测试访问应用端二: http://s2.stp.com:9001"); System.out.println("测试访问应用端三: http://s3.stp.com:9001"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") public String index(HttpServletRequest request) { String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) ); SaSsoClientConfig cfg = SaSsoManager.getClientConfig(); String str = "

Sa-Token SSO-Client 应用端 (模式一)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单浏览器注销 - " + "全端注销 " + "

"; return str; } // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/resources/application.yml ================================================ # 端口 server: port: 9001 # Sa-Token 配置 sa-token: # 打印操作日志 is-log: true # SSO-相关配置 sso-client: # client 标识 client: sso-client1 # SSO-Server端主机地址 server-url: http://sso.stp.com:9000 # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 alone-redis: # Redis数据库索引 database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso2-client 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis ${sa-token.version} cn.dev33 sa-token-forest ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/SaSso2ClientApplication.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaSso2ClientApplication { public static void main(String[] args) { SpringApplication.run(SaSso2ClientApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式二 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9002"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9002"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9002"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/H5Controller.java ================================================ package com.pj.h5; import cn.dev33.satoken.sso.model.SaCheckTicketResult; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

* @author click33 * */ @RestController public class H5Controller { // 判断当前是否登录 @RequestMapping("/sso/isLogin") public Object isLogin() { return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); } // 返回SSO认证中心登录地址 @RequestMapping("/sso/getSsoAuthUrl") public SaResult getSsoAuthUrl(String clientLoginUrl) { String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); return SaResult.data(serverAuthUrl); } // 根据 ticket 进行登录 @RequestMapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); StpUtil.login(ctr.loginId, new SaLoginParameter() .setTimeout(ctr.remainTokenTimeout) .setDeviceId(ctr.deviceId) ); return SaResult.data(StpUtil.getTokenValue()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/SaTokenConfigure.java ================================================ package com.pj.h5; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * * @author click33 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // 首页 @RequestMapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式二)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "单浏览器注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { } // 当前应用独自注销 (不退出其它应用) @RequestMapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @RequestMapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 获取本地 loginId Object loginId = StpUtil.getLoginId(); // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", loginId); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/resources/application.yml ================================================ # 端口 server: port: 9002 # sa-token配置 sa-token: # 打印操作日志 is-log: true # SSO-相关配置 sso-client: # 应用标识 client: sso-client2 # SSO-Server 端主机地址 server-url: http://sa-sso-server.com:9000 # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html # API 接口调用秘钥 (单点注销时会用到) secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 alone-redis: # Redis数据库索引 database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 forest: # 关闭 forest 请求日志打印 log-enabled: false ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso3-client 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-forest ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/SaSso3ClientApplication.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaSso3ClientApplication { public static void main(String[] args) { SpringApplication.run(SaSso3ClientApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式三 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9003"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9003"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9003"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/H5Controller.java ================================================ package com.pj.h5; import cn.dev33.satoken.sso.model.SaCheckTicketResult; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

* * @author click33 */ @RestController public class H5Controller { // 当前是否登录 @RequestMapping("/sso/isLogin") public Object isLogin() { return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); } // 返回SSO认证中心登录地址 @RequestMapping("/sso/getSsoAuthUrl") public SaResult getSsoAuthUrl(String clientLoginUrl) { String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); return SaResult.data(serverAuthUrl); } // 根据ticket进行登录 @RequestMapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket, "/sso/doLoginByTicket"); StpUtil.login(ctr.loginId, new SaLoginParameter() .setTimeout(ctr.remainTokenTimeout) .setDeviceId(ctr.deviceId) ); return SaResult.data(StpUtil.getTokenValue()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/SaTokenConfigure.java ================================================ package com.pj.h5; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * * @author click33 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "单浏览器注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { } // 当前应用独自注销 (不退出其它应用) @RequestMapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @RequestMapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 获取本地 loginId Object loginId = StpUtil.getLoginId(); // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", loginId); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/resources/application.yml ================================================ # 端口 server: port: 9003 # sa-token配置 sa-token: # 打印操作日志 is-log: true # sso-client 相关配置 sso-client: # 应用标识 client: sso-client3 # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html # 使用 Http 请求校验 ticket (模式三) is-http: true # API 接口调用秘钥 secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor spring: # 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) redis: # Redis数据库索引 database: 3 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 forest: # 关闭 forest 请求日志打印 log-enabled: false ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso3-client-anon 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-forest ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/SaSso3ClientAnonApplication.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaSso3ClientAnonApplication { public static void main(String[] args) { SpringApplication.run(SaSso3ClientAnonApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式三 (匿名应用) Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9006"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9006"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9006"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/GlobalExceptionHandler.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // 首页 @RequestMapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式三-匿名应用)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "单浏览器注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射 // // 将 centerId 转换为 loginId 的函数 // ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { // return "Stu" + centerId; // }; // // 将 loginId 转换为 centerId 的函数 // ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { // return loginId.toString().substring(3); // }; } // 当前应用独自注销 (不退出其它应用) @RequestMapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @RequestMapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 原写法:直接调用 StpUtil.getLoginId() 当做 centerId 来提交 // Object centerId = StpUtil.getLoginId(); // 新写法:获取本地 loginId 对应的认证中心 centerId Object centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId()); // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", centerId); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/resources/application.yml ================================================ # 端口 server: port: 9006 # sa-token配置 sa-token: # 配置一个不同的 token-name,以避免在和模式三 demo 一起测试时发生数据覆盖 token-name: satoken-client-anon # sso-client 相关配置 sso-client: # client 标识 匿名应用就是指不配置 client 标识的应用 # client: sso-client3 # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 使用 Http 请求校验ticket (模式三) is-http: true # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个) reg-logout-call: true # API 接口调用秘钥 secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor spring: # 配置 Redis 连接 (此处与SSO-Server端连接不同的Redis) redis: # Redis数据库索引 database: 6 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 forest: # 关闭 forest 请求日志打印 log-enabled: false ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso3-client-nosdk 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 org.springframework.boot spring-boot-starter-web com.dtflys.forest forest-spring-boot-starter 1.5.26 ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaSsoClientNoSdkApplication { public static void main(String[] args) { SpringApplication.run(SaSsoClientNoSdkApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式三 (NoSdk版) demo 启动成功 ----------------------"); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9004"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9004"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9004"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); System.err.println("自 v1.43.0 版本起,Sa-Token SSO 不再维护 NoSdk 示例,此项目仅做留档"); System.err.println("如您需要非 Sa-Token 技术栈项目接入 SSO-Server 认证中心,请参考 ReSdk 版本示例"); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import java.io.IOException; import java.util.Objects; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.pj.sso.util.AjaxJson; import com.pj.sso.util.MyHttpSessionHolder; /** * SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") public String index(HttpSession session) { String str = "

Sa-Token SSO-Client 应用端

" + "

当前会话登录账号:" + session.getAttribute("userId") + "

" + "

登录" + " 注销" + " 获取资料

"; return str; } // SSO-Client端:单点登录地址 @RequestMapping("/sso/login") public Object ssoLogin(String ticket, @RequestParam(defaultValue = "/") String back, HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException { // 如果已经登录,则直接返回 if(session.getAttribute("userId") != null) { response.sendRedirect(back); return null; } /* * 此时有两种情况: * 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心 * 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录 */ if(ticket == null) { String currUrl = request.getRequestURL().toString(); String clientLoginUrl = currUrl + "?back=" + SsoRequestUtil.encodeUrl(back); String serverAuthUrl = SsoRequestUtil.authUrl + "?redirect=" + clientLoginUrl; response.sendRedirect(serverAuthUrl); return null; } else { // 获取当前 client 端的单点注销回调地址 String ssoLogoutCall = ""; if(SsoRequestUtil.isSlo) { ssoLogoutCall = request.getRequestURL().toString().replace("/sso/login", "/sso/logoutCall"); } // 校验 ticket String timestamp = String.valueOf(System.currentTimeMillis()); // 时间戳 String nonce = SsoRequestUtil.getRandomString(20); // 随机字符串 String sign = SsoRequestUtil.getSignByTicket(ticket, ssoLogoutCall, timestamp, nonce); // 参数签名 String checkUrl = SsoRequestUtil.checkTicketUrl + "?timestamp=" + timestamp + "&nonce=" + nonce + "&sign=" + sign + "&ticket=" + ticket + "&ssoLogoutCall=" + ssoLogoutCall; AjaxJson result = SsoRequestUtil.request(checkUrl); // 200 代表校验成功 if(result.getCode() == 200 && SsoRequestUtil.isEmpty(result.getData()) == false) { // 登录上 Object loginId = result.getData(); session.setAttribute("userId", loginId); // 返回 back 地址 response.sendRedirect(back); return null; } else { // 将 sso-server 回应的消息作为异常抛出 throw new RuntimeException(result.getMsg()); } } } // SSO-Client端:单点注销地址 @RequestMapping("/sso/logout") public Object ssoLogout(@RequestParam(defaultValue = "/") String back, HttpServletResponse response, HttpSession session) throws IOException { // 如果未登录,则无需注销 if(session.getAttribute("userId") == null) { response.sendRedirect(back); return null; } // 调用 sso-server 认证中心单点注销API Object loginId = session.getAttribute("userId"); // 账号id String timestamp = String.valueOf(System.currentTimeMillis()); // 时间戳 String nonce = SsoRequestUtil.getRandomString(20); // 随机字符串 String sign = SsoRequestUtil.getSign(loginId, timestamp, nonce); // 参数签名 String url = SsoRequestUtil.sloUrl + "?loginId=" + loginId + "×tamp=" + timestamp + "&nonce=" + nonce + "&sign=" + sign; AjaxJson result = SsoRequestUtil.request(url); // 校验响应状态码,200 代表成功 if(result.getCode() == 200) { // 极端场景下,sso-server 中心的单点注销可能并不会通知到此 client 端,所以这里需要再补一刀 session.removeAttribute("userId"); // 返回 back 地址 response.sendRedirect(back); return null; } else { // 将 sso-server 回应的消息作为异常抛出 throw new RuntimeException(result.getMsg()); } } // SSO-Client端:单点注销回调地址 @RequestMapping("/sso/logoutCall") public Object ssoLogoutCall(String loginId, String autoLogout, String timestamp, String nonce, String sign) { // 校验签名 String calcSign = SsoRequestUtil.getSignByLogoutCall(loginId, autoLogout, timestamp, nonce); if(calcSign.equals(sign) == false) { System.out.println("无效签名,拒绝应答:" + sign); return AjaxJson.getError("无效签名,拒绝应答" + sign); } // 注销这个账号id for (HttpSession session: MyHttpSessionHolder.sessionList) { Object userId = session.getAttribute("userId"); if(Objects.equals(String.valueOf(userId), loginId)) { session.removeAttribute("userId"); } } return AjaxJson.getSuccess("账号id=" + loginId + " 注销成功"); } // 查询我的账号信息 (调用此接口的前提是 sso-server 端开放了 /sso/userinfo 路由) @RequestMapping("/sso/myInfo") public Object myInfo(HttpSession session) { // 如果尚未登录 if(session.getAttribute("userId") == null) { return "尚未登录,无法获取"; } // 组织 url 参数 Object loginId = session.getAttribute("userId"); // 账号id String timestamp = String.valueOf(System.currentTimeMillis()); // 时间戳 String nonce = SsoRequestUtil.getRandomString(20); // 随机字符串 String sign = SsoRequestUtil.getSign(loginId, timestamp, nonce); // 参数签名 String url = SsoRequestUtil.getDataUrl + "?loginId=" + loginId + "×tamp=" + timestamp + "&nonce=" + nonce + "&sign=" + sign; AjaxJson result = SsoRequestUtil.request(url); // 返回给前端 return result; } // 全局异常拦截 @ExceptionHandler public AjaxJson handlerException(Exception e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java ================================================ package com.pj.sso; import com.dtflys.forest.Forest; import com.pj.sso.util.AjaxJson; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.MessageDigest; import java.util.Map; import java.util.Random; /** * 封装一些 sso 共用方法 * * @author click33 * @since 2022-4-30 */ public class SsoRequestUtil { /** * SSO-Server端主机地址 */ public static String serverUrl = "http://sa-sso-server.com:9000"; /** * SSO-Server端 统一认证地址 */ public static String authUrl = serverUrl + "/sso/auth"; /** * SSO-Server端 ticket校验地址 */ public static String checkTicketUrl = serverUrl + "/sso/checkTicket"; /** * 单点注销地址 */ public static String sloUrl = serverUrl + "/sso/signout"; /** * SSO-Server端 查询userinfo地址 */ public static String getDataUrl = serverUrl + "/sso/getData"; /** * 打开单点注销功能 */ public static boolean isSlo = true; /** * 接口调用秘钥 */ public static String secretKey = "kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"; // -------------------------- 工具方法 /** * 发出请求,并返回 SaResult 结果 * @param url 请求地址 * @return 返回的结果 */ public static AjaxJson request(String url) { Map map = Forest.post(url).executeAsMap(); return new AjaxJson(map); } /** * 根据参数计算签名 * @param loginId 账号id * @param timestamp 当前时间戳,13位 * @param nonce 随机字符串 * @return 签名 */ public static String getSign(Object loginId, String timestamp, String nonce) { return md5("loginId=" + loginId + "&nonce=" + nonce + "×tamp=" + timestamp + "&key=" + secretKey); } // 单点注销回调时构建签名 public static String getSignByLogoutCall(Object loginId, String autoLogout, String timestamp, String nonce) { return md5("autoLogout=" + autoLogout + "&loginId=" + loginId + "&nonce=" + nonce + "×tamp=" + timestamp + "&key=" + secretKey); } // 校验ticket 时构建签名 public static String getSignByTicket(String ticket, String ssoLogoutCall, String timestamp, String nonce) { return md5("nonce=" + nonce + "&ssoLogoutCall=" + ssoLogoutCall + "&ticket=" + ticket + "×tamp=" + timestamp + "&key=" + secretKey); } /** * 指定元素是否为null或者空字符串 * @param str 指定元素 * @return 是否为null或者空字符串 */ public static boolean isEmpty(Object str) { return str == null || "".equals(str); } /** * md5加密 * @param str 指定字符串 * @return 加密后的字符串 */ public static String md5(String str) { str = (str == null ? "" : str); char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; try { byte[] btInput = str.getBytes(); MessageDigest mdInst = MessageDigest.getInstance("MD5"); mdInst.update(btInput); byte[] md = mdInst.digest(); int j = md.length; char[] strA = new char[j * 2]; int k = 0; for (byte byte0 : md) { strA[k++] = hexDigits[byte0 >>> 4 & 0xf]; strA[k++] = hexDigits[byte0 & 0xf]; } return new String(strA); } catch (Exception e) { throw new RuntimeException(e); } } /** * 生成指定长度的随机字符串 * * @param length 字符串的长度 * @return 一个随机字符串 */ public static String getRandomString(int length) { String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { int number = random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } /** * URL编码 * @param url see note * @return see note */ public static String encodeUrl(String url) { try { return URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/AjaxJson.java ================================================ package com.pj.sso.util; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; /** * ajax请求返回Json格式数据的封装
* 所有预留字段:
* code=状态码
* msg=描述信息
* data=携带对象
* pageNo=当前页
* pageSize=页大小
* startIndex=起始索引
* dataCount=数据总数
* pageCount=分页总数
*

返回范例:

*
	{
		"code": 200,    // 成功时=200, 失败时=500  msg=失败原因
		"msg": "ok",
		"data": {}
	} 
	
*/ public class AjaxJson extends LinkedHashMap implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 // ============================ 写值取值 ================================== /** 给code赋值,连缀风格 */ public AjaxJson setCode(int code) { this.put("code", code); return this; } /** 返回code */ public Integer getCode() { return (Integer)this.get("code"); } /** 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.put("msg", msg); return this; } /** 获取msg */ public String getMsg() { return (String)this.get("msg"); } /** 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.put("data", data); return this; } /** 获取data */ public Object getData() { return this.get("data"); } /** 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) this.getData(); } /** 给dataCount(数据总数)赋值,连缀风格 */ public AjaxJson setDataCount(Long dataCount) { this.put("dataCount", dataCount); // 如果提供了数据总数,则尝试计算page信息 if(dataCount != null && dataCount >= 0) { // 如果:已有page信息 if(get("pageNo") != null) { this.initPageInfo(); } // // 或者:是JavaWeb环境 // else if(SoMap.isJavaWeb()) { // SoMap so = SoMap.getRequestSoMap(); // this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize()); // this.initPageInfo(); // } } return this; } /** 获取dataCount(数据总数) */ public Long getDataCount() { return (Long)this.get("dataCount"); } /** 设置pageNo 和 pageSize,并计算出startIndex于pageCount */ public AjaxJson setPageNoAndSize(long pageNo, long pageSize) { this.put("pageNo", pageNo); this.put("pageSize", pageSize); return this; } /** 根据 pageSize dataCount,计算startIndex 与 pageCount */ public AjaxJson initPageInfo() { long pageNo = (long)this.get("pageNo"); long pageSize = (long)this.get("pageSize"); long dataCount = (long)this.get("dataCount"); this.set("startIndex", (pageNo - 1) * pageSize); long pc = dataCount / pageSize; this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1)); return this; } /** 写入一个值 自定义key, 连缀风格 */ public AjaxJson set(String key, Object data) { this.put(key, data); return this; } /** 写入一个Map, 连缀风格 */ public AjaxJson setMap(Map map) { for (String key : map.keySet()) { this.put(key, map.get(key)); } return this; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.setCode(code); this.setMsg(msg); this.setData(data); if(dataCount != null) { this.setDataCount(dataCount); } } public AjaxJson(Map map) { for (String key: map.keySet()) { this.set(key, map.get(key)); } } /** 返回成功 */ public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } /** 返回失败 */ public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } /** 返回警告 */ public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } /** 返回未登录 */ public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } /** 返回没有权限的 */ public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } /** 返回一个自定义状态码的 */ public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } /** 返回分页和数据的 */ public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } /** 返回, 根据受影响行数的(大于0=ok,小于0=error) */ public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } /** 返回,根据布尔值来确定最终结果的 (true=ok,false=error) */ public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } // // 历史版本遗留代码 // public int code; // 状态码 // public String msg; // 描述信息 // public Object data; // 携带对象 // public Long dataCount; // 数据总数,用于分页 } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/MyHttpSessionHolder.java ================================================ package com.pj.sso.util; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import org.springframework.stereotype.Component; /** * 记录所有已创建的 HttpSession 对象 * * 此种方式有性能问题,仅做demo示例,真实项目中请更换为其它方案记录用户会话数据 * * @author click33 * @since 2022-4-30 */ @Component public class MyHttpSessionHolder implements HttpSessionListener { public static List sessionList = new ArrayList<>(); public void sessionCreated(HttpSessionEvent httpSessionEvent) { sessionList.add(httpSessionEvent.getSession()); } public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { HttpSession session = httpSessionEvent.getSession(); sessionList.remove(session); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/resources/application.yml ================================================ # 端口 server: port: 9004 forest: # 打开/关闭Forest请求日志(默认为 true) log-request: true spring: # Redis连接 redis: # Redis数据库索引 database: 2 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso3-client-resdk 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-forest ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/SaSsoClientReSdkApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SaSsoClientReSdkApplication { public static void main(String[] args) { SpringApplication.run(SaSsoClientReSdkApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式三 (ReSdk版) demo 启动成功 ----------------------"); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9005"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9005"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9005"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/MyHttpSessionHolder.java ================================================ package com.pj.resdk; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import org.springframework.stereotype.Component; /** * 记录所有已创建的 HttpSession 对象 * * 此种方式有性能问题,仅做demo示例,真实项目中请更换为其它方案记录用户会话数据 * * @author click33 * @since 2022-4-30 */ @Component public class MyHttpSessionHolder implements HttpSessionListener { public static List sessionList = new ArrayList<>(); public void sessionCreated(HttpSessionEvent httpSessionEvent) { sessionList.add(httpSessionEvent.getSession()); } public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { HttpSession session = httpSessionEvent.getSession(); sessionList.remove(session); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/StpLogicForHttpSession.java ================================================ package com.pj.resdk; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import javax.servlet.http.HttpSession; import java.util.Objects; /** * 会话对象 - httpSession 版 * * @author click33 * @since 2025/5/6 */ public class StpLogicForHttpSession extends StpLogic { /** * 初始化 StpLogic, 并指定账号类型 * * @param type / * */ public StpLogicForHttpSession(String type) { super(type); } // 判断当前会话是否已登录 @Override public boolean isLogin() { return SpringMVCUtil.getRequest().getSession().getAttribute("userId") != null; } // 获取当前会话的登录ID @Override public Object getLoginId() { Object userId = SpringMVCUtil.getRequest().getSession().getAttribute("userId"); if(userId == null) { throw new RuntimeException("当前会话未登录"); } return userId; } // 获取当前登录设备 id @Override public String getLoginDeviceId() { return null; } // 当前会话注销 @Override public void logout(SaLogoutParameter logoutParameter) { SpringMVCUtil.getRequest().getSession().removeAttribute("userId"); } // 当前账号id注销 @Override public void _logout(Object loginId, SaLogoutParameter logoutParameter) { System.out.println("--- 注销账号id:" + loginId); for (HttpSession session: MyHttpSessionHolder.sessionList) { Object userId = session.getAttribute("userId"); if(Objects.equals(String.valueOf(userId), String.valueOf(loginId))) { session.removeAttribute("userId"); } } } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/GlobalExceptionHandler.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import com.pj.resdk.StpLogicForHttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; /** * SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") public String index(HttpSession session) { boolean isLogin = session.getAttribute("userId") != null; Object loginId = session.getAttribute("userId"); String str = "

Sa-Token SSO-Client 应用端 (模式三-ReSdk)

" + "

当前会话是否登录:" + isLogin + " (" + loginId + ")

" + "

" + "登录 - " + "单应用注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有 SSO 相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @RequestMapping("/sso/*") public Object ssoLogin() { return SaSsoClientProcessor.instance.dister(); } // 当前应用独自注销 (不退出其它应用) @RequestMapping("/sso/logoutByAlone") public Object logoutByAlone(HttpSession session) { session.removeAttribute("userId"); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 自定义底层使用的会话操作对象 ssoClientTemplate.setStpLogic(new StpLogicForHttpSession(StpUtil.TYPE)); // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { HttpSession session = SpringMVCUtil.getRequest().getSession(); session.setAttribute("userId", ctr.loginId); return SaHolder.getResponse().redirect(back); }; } // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @RequestMapping("/sso/myInfo") public Object myInfo(HttpSession session) { // 如果尚未登录 if(session.getAttribute("userId") == null) { return "尚未登录,无法获取"; } // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", session.getAttribute("userId")); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/resources/application.yml ================================================ # 端口 server: port: 9005 # sa-token 配置 sa-token: # 是否打印操作日志 is-log: true # sso-client 相关配置 sso-client: # client 标识 client: sso-client3-resdk # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 使用 Http 请求校验ticket (模式三) is-http: true # API 接口调用秘钥 secret-key: SSO-C3-ReSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso-server-solon 0.0.1-SNAPSHOT org.noear solon-parent 3.2.1 1.45.0 org.noear solon-web ${solon.version} cn.dev33 sa-token-solon-plugin ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redisx ${sa-token.version} cn.dev33 sa-token-snack3 ${sa-token.version} org.noear snack3 3.2.133 org.noear solon.view.thymeleaf ${solon.version} cn.dev33 sa-token-forest ${sa-token.version} ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaConfig.java ================================================ package com.pj; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisx; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * @author noear 2023/3/13 created */ @Configuration public class SaConfig { /** * 构建建 SaToken redis dao(如果不需要 redis;可以注释掉) * */ @Bean public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) { return saTokenDao; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @SolonMain public class SaSsoServerApp { public static void main(String[] args) { Solon.start(SaSsoServerApp.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getServerConfig()); System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } /* * 类型序列化测试代码 * System.out.println(SaManager.getSaJsonTemplate()); SaSsoClientInfo sci = new SaSsoClientInfo(); sci.setClient("client1"); List list = new ArrayList<>(); list.add(sci); StpUtil.getSessionByLoginId(10001).set("list", list); List list2 = (List)StpUtil.getSessionByLoginId(10001).get("list"); for (SaSsoClientInfo info : list2) { System.out.println(info); } */ } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/H5Controller.java ================================================ package com.pj.h5; import cn.dev33.satoken.sso.template.SaSsoUtil; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; /** * 前后台分离架构下集成SSO所需的代码 (SSO-Server端) *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

* @author click33 * */ @Controller public class H5Controller { /** * 获取 redirectUrl */ @Mapping("/sso/getRedirectUrl") public SaResult getRedirectUrl(String client, String redirect, String mode) { // 未登录情况下,返回 code=401 if(StpUtil.isLogin() == false) { return SaResult.code(401); } // 已登录情况下,构建 redirectUrl redirect = SaFoxUtil.decoderUrl(redirect); if(SaSsoConsts.MODE_SIMPLE.equals(mode)) { // 模式一 SaSsoUtil.checkRedirectUrl(client, redirect); return SaResult.data(redirect); } else { // 模式二或模式三 String redirectUrl = SaSsoUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getLoginDeviceId()); return SaResult.data(redirectUrl); } } // /** // * 控制当前类的异常 // */ // @Override // public void render(Object data, Context ctx) throws Throwable { // if (data instanceof Throwable) { // Throwable e = (Throwable) data; // e.printStackTrace(); // ctx.render(SaResult.error(e.getMessage())); // } else { // ctx.render(data); // } // } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/SaTokenConfigure.java ================================================ package com.pj.h5; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 (解决跨域问题) * * @author click33 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Component; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 全局异常处理 * @author click33 * */ @Component public class GlobalExceptionFilter implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { chain.doFilter(ctx); } catch (Exception e) { e.printStackTrace(); ctx.render(SaResult.error(e.getMessage())); } } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/HomeController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.stp.StpUtil; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; /** * SSO 平台中心模式示例,跳连接进入子系统 */ @Controller public class HomeController { // 平台化首页 @Mapping("/home") public Object index() { // 如果未登录,则先去登录 if(!StpUtil.isLogin()) { return SaHolder.getResponse().redirect("/sso/auth"); } // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; // 组织网页结构返回到前端 String title = "

SSO 平台首页 (平台中心模式)

"; String client1 = "

进入Client1系统

"; String client2 = "

进入Client2系统

"; String client3 = "

进入Client3系统

"; return title + client1 + client2 + client3; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoServerController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import org.noear.solon.core.handle.ModelAndView; /** * Sa-Token-SSO Server端 Controller * @author click33 * */ @Controller @Configuration public class SsoServerController { /** * SSO-Server端:处理所有SSO相关请求 * http://{host}:{port}/sso/auth -- 单点登录授权地址 * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) */ @Mapping("/sso/*") public Object ssoRequest() { return SaSsoServerProcessor.instance.dister(); } // 配置SSO相关参数 @Bean private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:未登录时返回的View ssoServerTemplate.strategy.notLoginView = () -> { return new ModelAndView("sa-login.html"); }; // 配置:登录处理函数 ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { // 此处仅做模拟登录,真实环境应该查询数据库进行登录 if("sa".equals(name) && "123456".equals(pwd)) { String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }; // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { System.out.println("收到消息:" + message); // 自定义返回结果(模拟) return SaResult.ok() .set("id", message.get("loginId")) .set("name", "LinXiaoYu") .set("sex", "女") .set("age", 18); }); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/layer.js ================================================ /*! layer-v3.1.1 Web弹层组件 MIT License http://layer.layui.com/ By 贤心 */ ;!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
'+(f?r.title[0]:r.title)+"
":"";return r.zIndex=s,t([r.shade?'
':"",'
'+(e&&2!=r.type?"":u)+'
'+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
'+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
'+e+"
"}():"")+(r.resize?'':"")+"
"],u,i('
')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
    '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
  • '+(t[0].content||"no content")+"
  • ";i'+(t[i].content||"no content")+"";return a}()+"
",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
'+(u.length>1?'':"")+'
'+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/mobile/layer.js ================================================ /*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

'+(e?n.title[0]:n.title)+"

":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
'+e+"
"):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

'+(n.content||"")+"

"),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
':"")+'
"+l+'
'+n.content+"
"+c+"
",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/layer/theme/default/layer.css ================================================ .layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1E9FFF;background-color:#1E9FFF;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#E9E7E7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:230px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;overflow:hidden;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:43px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.css ================================================ *{margin: 0; padding: 0;} body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} ::-webkit-input-placeholder{color: #ccc;} /* 视图盒子 */ .view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} /* 背景 EAEFF3 */ .bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);} .bg-2{height: 50%; background-color: #EAEFF3;} /* 渐变背景 */ /*.bg-1{ background-size: 500%; background-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5); animation: bganimation 30s infinite; } @keyframes bganimation{ 0%{background-position: 0% 50%;} 50%{background-position: 100% 50%;} 100%{background-position: 0% 50%;} } */ /* 背景 */ .bg-1{background: #101C34;} .bg-2{background: #101C34;} /* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */ /* 内容盒子 */ .content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} /* 登录盒子 */ /* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ .login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;} .login-box{display: flex; align-items: center; text-align: center;} /* 表单 */ .from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} .from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} .from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} /* 输入框 */ .from-item{border: 0px #000 solid; margin-bottom: 15px;} .s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} .s-input{font-size: 12px;} .s-input:focus{border-color: #409eff} /* 登录按钮 */ .s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} .s-btn:hover{background-color: #50aEFF;} /* 重置按钮 */ .reset-box{text-align: left; font-size: 12px;} .reset-box a{text-decoration: none;} .reset-box a:hover{text-decoration: underline;} /* loading框样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/static/sa-res/login.js ================================================ // sa var sa = {}; // 打开loading sa.loading = function(msg) { layer.closeAll(); // 开始前先把所有弹窗关了 return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); }; // 隐藏loading sa.hideLoading = function() { layer.closeAll(); }; // ----------------------------------- 登录事件 ----------------------------------- $('.login-btn').click(function(){ sa.loading("正在登录..."); // 开始登录 setTimeout(function() { $.ajax({ url: "sso/doLogin", type: "post", data: { name: $('[name=name]').val(), pwd: $('[name=pwd]').val() }, dataType: 'json', success: function(res){ console.log('返回数据:', res); sa.hideLoading(); if(res.code == 200) { layer.msg('登录成功', {anim: 0, icon: 6 }); setTimeout(function() { location.reload(); }, 800) } else { layer.msg(res.msg, {anim: 6, icon: 2 }); } }, error: function(xhr, type, errorThrown){ sa.hideLoading(); if(xhr.status == 0){ return layer.alert('无法连接到服务器,请检查网络'); } return layer.alert("异常:" + JSON.stringify(xhr)); } }); }, 400); }); // 绑定回车事件 $('[name=name],[name=pwd]').bind('keypress', function(event){ if(event.keyCode == "13") { $('.login-btn').click(); } }); // 输入框获取焦点 $("[name=name]").focus(); // 打印信息 var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/"; console.log(str); ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/view/sa-login.html ================================================ Sa-SSO-Server 认证中心-登录
This page is provided by Sa-Token-SSO
================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml ================================================ # 端口 server: port: 9000 # Sa-Token 配置 sa-token: # 打印操作日志 is-log: true # SSO 模式一配置 (非模式一不需要配置) # cookie: # # 配置 Cookie 作用域 # domain: stp.com # SSO-Server 配置 sso-server: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 home-route: /home # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) allow-anon-client: true # 所有允许的授权回调地址 (匿名 client 使用) allow-url: "*" # API 接口调用秘钥 (全局默认 + 匿名 client 使用) secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用列表:配置接入的应用信息 clients: # 应用 sso-client1:采用模式一对接 (同域、同Redis) sso-client1: client: sso-client1 allowUrl: "*" # 应用 sso-client2:采用模式二对接 (跨域、同Redis) sso-client2: client: sso-client2 allowUrl: "*" secretKey: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) sso-client3: # 应用名称 client: sso-client3 # 允许授权地址 allowUrl: "*" # 是否接收消息推送 isPush: true # 消息推送地址 pushUrl: http://sa-sso-client1.com:9003/sso/pushC # 接口调用秘钥 (如果不配置则使用全局默认秘钥) secretKey: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" # password: 123456 db: 1 maxTotal: 200 ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso1-client-solon 0.0.1-SNAPSHOT org.noear solon-parent 3.2.1 1.45.0 org.noear solon-web ${solon.version} cn.dev33 sa-token-solon-plugin ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redisx ${sa-token.version} cn.dev33 sa-token-snack3 ${sa-token.version} org.noear snack3 3.2.133 ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java ================================================ package com.pj; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisx; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * @author noear 2023/3/13 created */ @Configuration public class SaConfig { /** * 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) * */ @Bean public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) { return saTokenDao; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; /** * SSO模式一,Client端 Demo * @author click33 * */ @SolonMain public class SaSso1ClientApp { public static void main(String[] args) { Solon.start(SaSso1ClientApp.class, args); System.out.println("\nSa-Token SSO模式一 Client端启动成功"); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://s1.stp.com:9001"); System.out.println("测试访问应用端二: http://s2.stp.com:9001"); System.out.println("测试访问应用端三: http://s3.stp.com:9001"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; import org.noear.solon.annotation.Produces; import org.noear.solon.boot.web.MimeType; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Render; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @Controller public class SsoClientController implements Render { // SSO-Client端:首页 @Produces(MimeType.TEXT_HTML_VALUE) @Mapping("/") public String index() { String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), Context.current().queryString()) ); SaSsoClientConfig cfg = SaSsoManager.getClientConfig(); String str = "

Sa-Token SSO-Client 应用端 (模式一)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单浏览器注销 - " + "全端注销 " + "

"; return str; } // 全局异常拦截并转换 @Override public void render(Object data, Context ctx) throws Throwable { if(data instanceof Exception){ data = SaResult.error(((Exception)data).getMessage()); } ctx.render(data); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml ================================================ # 端口 server: port: 9001 # Sa-Token 配置 sa-token: # 打印操作日志 is-log: true # SSO-相关配置 sso-client: # client 标识 client: sso-client1 # SSO-Server端 - 主机地址 server-url: http://sso.stp.com:9000 # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" # password: 123456 db: 1 maxTotal: 200 ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso2-client-solon 0.0.1-SNAPSHOT org.noear solon-parent 3.2.1 1.45.0 org.noear solon-web ${solon.version} cn.dev33 sa-token-solon-plugin ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redisx ${sa-token.version} cn.dev33 sa-token-forest ${sa-token.version} cn.dev33 sa-token-snack3 ${sa-token.version} org.noear snack3 3.2.133 ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java ================================================ package com.pj; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisx; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * @author noear 2023/3/13 created */ @Configuration public class SaConfig { /** * 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) * */ @Bean public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) { return saTokenDao; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @SolonMain public class SaSso2ClientApp { public static void main(String[] args) { Solon.start(SaSso2ClientApp.class, args); System.out.println(); System.out.println("---------------------- Solon Sa-Token SSO 模式二 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9002"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9002"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9002"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java ================================================ package com.pj.h5; import cn.dev33.satoken.sso.model.SaCheckTicketResult; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; /** * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

* @author click33 * */ @Controller public class H5Controller { // 判断当前是否登录 @Mapping("/sso/isLogin") public Object isLogin() { return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); } // 返回SSO认证中心登录地址 @Mapping("/sso/getSsoAuthUrl") public SaResult getSsoAuthUrl(String clientLoginUrl) { String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); return SaResult.data(serverAuthUrl); } // 根据 ticket 进行登录 @Mapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); StpUtil.login(ctr.loginId, new SaLoginParameter() .setTimeout(ctr.remainTokenTimeout) .setDeviceId(ctr.deviceId) ); return SaResult.data(StpUtil.getTokenValue()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java ================================================ package com.pj.h5; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 (解决跨域问题) * * @author click33 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Component; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 全局异常处理 * @author click33 * */ @Component public class GlobalExceptionFilter implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { chain.doFilter(ctx); } catch (Exception e) { e.printStackTrace(); ctx.render(SaResult.error(e.getMessage())); } } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.*; import org.noear.solon.boot.web.MimeType; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @Controller @Configuration public class SsoClientController { // 首页 @Produces(MimeType.TEXT_HTML_VALUE) @Mapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式二)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "单浏览器注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @Mapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Bean private void configSso(SaSsoClientTemplate ssoClientTemplate) { } // 当前应用独自注销 (不退出其它应用) @Mapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @Mapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 获取本地 loginId Object loginId = StpUtil.getLoginId(); // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", loginId); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml ================================================ # 端口 server: port: 9002 # sa-token配置 sa-token: # 打印操作日志 is-log: true # SSO-相关配置 sso-client: # 应用标识 client: sso-client2 # SSO-Server 端主机地址 server-url: http://sa-sso-server.com:9000 # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html # API 接口调用秘钥 (单点注销时会用到) secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token 单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis) sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" # password: 123456 db: 1 maxTotal: 200 ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-sso3-client-solon 0.0.1-SNAPSHOT org.noear solon-parent 3.2.1 1.45.0 org.noear solon-web ${solon.version} cn.dev33 sa-token-solon-plugin ${sa-token.version} cn.dev33 sa-token-sso ${sa-token.version} cn.dev33 sa-token-redisx ${sa-token.version} cn.dev33 sa-token-forest ${sa-token.version} cn.dev33 sa-token-snack3 ${sa-token.version} org.noear snack3 3.2.133 ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java ================================================ package com.pj; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisx; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * @author noear 2023/3/13 created */ @Configuration public class SaConfig { /** * 构建建 SaToken redis dao(如果不需要 redis;可以注释掉) * */ @Bean public SaTokenDao saTokenDaoInit(@Inject("${sa-token.dao.redis}") SaTokenDaoForRedisx saTokenDao) { return saTokenDao; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java ================================================ package com.pj; import cn.dev33.satoken.sso.SaSsoManager; import org.noear.solon.Solon; import org.noear.solon.annotation.SolonMain; @SolonMain public class SaSso3ClientApp { public static void main(String[] args) { Solon.start(SaSso3ClientApp.class, args); System.out.println(); System.out.println("---------------------- Solon Sa-Token SSO 模式三 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9003"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9003"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9003"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/H5Controller.java ================================================ package com.pj.h5; import cn.dev33.satoken.sso.model.SaCheckTicketResult; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Mapping; /** * 前后台分离架构下集成SSO所需的代码 (SSO-Client端) *

(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

* @author click33 * */ @Controller public class H5Controller { // 判断当前是否登录 @Mapping("/sso/isLogin") public Object isLogin() { return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); } // 返回SSO认证中心登录地址 @Mapping("/sso/getSsoAuthUrl") public SaResult getSsoAuthUrl(String clientLoginUrl) { String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); return SaResult.data(serverAuthUrl); } // 根据 ticket 进行登录 @Mapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); StpUtil.login(ctr.loginId, new SaLoginParameter() .setTimeout(ctr.remainTokenTimeout) .setDeviceId(ctr.deviceId) ); return SaResult.data(StpUtil.getTokenValue()); } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java ================================================ package com.pj.h5; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 (解决跨域问题) * * @author click33 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java ================================================ package com.pj.sso; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.Component; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 全局异常处理 * @author click33 * */ @Component public class GlobalExceptionFilter implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { chain.doFilter(ctx); } catch (Exception e) { e.printStackTrace(); ctx.render(SaResult.error(e.getMessage())); } } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoClientController.java ================================================ package com.pj.sso; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoClientUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.noear.solon.annotation.*; import org.noear.solon.boot.web.MimeType; /** * Sa-Token-SSO Client端 Controller * @author click33 */ @Controller @Configuration public class SsoClientController { // SSO-Client端:首页 @Produces(MimeType.TEXT_HTML_VALUE) @Mapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "单浏览器注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @Mapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Bean private void configSso(SaSsoClientTemplate ssoClientTemplate) { } // 当前应用独自注销 (不退出其它应用) @Mapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @Mapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 获取本地 loginId Object loginId = StpUtil.getLoginId(); // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", loginId); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } } ================================================ FILE: sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml ================================================ # 端口 server: port: 9003 # sa-token配置 sa-token: # 打印操作日志 is-log: true # sso-client 相关配置 sso-client: # 应用标识 client: sso-client3 # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html # 使用 Http 请求校验 ticket (模式三) is-http: true # API 接口调用秘钥 secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token Dao(此处与SSO-Server端连接不同的Redis) sa-token.dao: #名字可以随意取 redis: server: "localhost:6379" # password: 123456 db: 4 maxTotal: 200 ================================================ FILE: sa-token-demo/sa-token-demo-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-test 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 com.pj.SaTokenApplication org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.hutool hutool-all 5.8.36 cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 cn.dev33 sa-token-sign ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-actuator src/main/java **/*.xml src/main/resources **/*.* org.apache.maven.plugins maven-jar-plugin true lib/ ${java.run.main.class} org.apache.maven.plugins maven-dependency-plugin copy package copy-dependencies ${project.build.directory}/lib ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/SaTokenApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token 测试 * @author click33 * */ @SpringBootApplication public class SaTokenApplication { public static void main(String[] args) { SpringApplication.run(SaTokenApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/GlobalException.java ================================================ package com.pj.current; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.pj.util.AjaxJson; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 不同异常返回不同状态码 AjaxJson aj = null; if (e instanceof NotLoginException) { // 如果是未登录异常 NotLoginException ee = (NotLoginException) e; aj = AjaxJson.getNotLogin().setMsg(ee.getMessage()); } else if(e instanceof NotRoleException) { // 如果是角色异常 NotRoleException ee = (NotRoleException) e; aj = AjaxJson.getNotJur("无此角色:" + ee.getRole()); } else if(e instanceof NotPermissionException) { // 如果是权限异常 NotPermissionException ee = (NotPermissionException) e; aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission()); } else if(e instanceof DisableServiceException) { // 如果是被封禁异常 DisableServiceException ee = (DisableServiceException) e; aj = AjaxJson.getNotJur("当前账号 " + ee.getService() + " 服务已被封禁 (level=" + ee.getLevel() + "):" + ee.getDisableTime() + "秒后解封"); } else { // 普通异常, 输出:500 + 异常信息 aj = AjaxJson.getError(e.getMessage()); } // 返回给前端 return aj; } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/NotFoundHandle.java ================================================ package com.pj.current; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.util.SaResult; /** * 处理 404 * @author click33 */ @RestController public class NotFoundHandle implements ErrorController { @RequestMapping("/error") public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); return SaResult.get(404, "not found", null); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysRole.java ================================================ package com.pj.model; /** * Role 实体类 * * @author click33 * @since 2022-10-15 */ public class SysRole { // // public SysRole() { // } // // public SysRole(long id, String name) { // super(); // this.id = id; // this.name = name; // } // // // /** // * 角色id // */ // private long id; // // /** // * 角色名称 // */ // private String name; // // /** // * @return id // */ // public long getId() { // return id; // } // // /** // * @param id 要设置的 id // */ // public void setId(long id) { // this.id = id; // } // // /** // * @return name // */ // public String getName() { // return name; // } // // /** // * @param name 要设置的 name // */ // public void setName(String name) { // this.name = name; // } // // @Override // public String toString() { // return "SysRole [id=" + id + ", name=" + name + "]"; // } // } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysUser.java ================================================ package com.pj.model; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser { public SysUser() { } public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * 用户角色 */ private SysRole role; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } public SysRole getRole() { return role; } public SysUser setRole(SysRole role) { this.role = role; return this; } @Override public String toString() { return "SysUser{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", role=" + role + '}'; } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaLogForSlf4j.java ================================================ package com.pj.satoken; import cn.dev33.satoken.log.SaLog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 将 Sa-Token log 信息转接到 Slf4j * * @author click33 * @since 2022-11-2 */ //@Component public class SaLogForSlf4j implements SaLog { Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); @Override public void trace(String str, Object... args) { log.trace(str, args); } @Override public void debug(String str, Object... args) { log.debug(str, args); } @Override public void info(String str, Object... args) { log.info(str, args); } @Override public void warn(String str, Object... args) { log.warn(str, args); } @Override public void error(String str, Object... args) { log.error(str, args); } @Override public void fatal(String str, Object... args) { log.error(str, args); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.plugin.SaTokenPluginHolder; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册 Sa-Token 拦截器打开注解鉴权功能 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } /** * 注册 [Sa-Token 全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 [拦截路由] 与 [放行路由] .addInclude("/**")// .addExclude("/favicon.ico") // 认证函数: 每次请求执行 .setAuth(obj -> { // 输出 API 请求日志,方便调试代码 // SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行 .setBeforeAuth(obj -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } /** * CORS 跨域处理 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } /** * 注册插件 */ @Bean public SaTokenPluginHolder getSaTokenPluginHolder() { System.out.println("自定义插件安装钩子函数..."); return SaTokenPluginHolder.instance // .onBeforeInstall(SaTokenPluginForJackson.class, plugin -> { // System.out.println("SaTokenPluginForJackson 插件安装前置钩子..."); // }) // // .onAfterInstall(SaTokenPluginForJackson.class, plugin -> { // System.out.println("SaTokenPluginForJackson 插件安装后置钩子..."); // }) // // .onAfterInstall(SaTokenPluginForJackson.class, plugin -> { // System.out.println("SaTokenPluginForJackson 插件安装后置钩子2..."); // }) // .onInstall(SaTokenPluginForJackson.class, plugin -> { // System.out.println("注册 install 钩子函数后,插件的默认安装行为将不再执行 ..."); // }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import cn.dev33.satoken.stp.StpInterface; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpUserUtil.java ================================================ package com.pj.satoken; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaTwoParamFunction; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import java.util.List; /** * 【User账号体系】Sa-Token 权限认证工具类 * * @author click33 * @since 1.0.0 */ public class StpUserUtil { private StpUserUtil() {} /** * 多账号体系下的类型标识 */ public static final String TYPE = "user"; /** * 底层使用的 StpLogic 对象 */ public static StpLogic stpLogic = new StpLogic(TYPE); /** * 获取当前 StpLogic 的账号类型 * * @return / */ public static String getLoginType(){ return stpLogic.getLoginType(); } /** * 安全的重置 StpLogic 对象 * *
1、更改此账户的 StpLogic 对象 *
2、put 到全局 StpLogic 集合中 *
3、发送日志 * * @param newStpLogic / */ public static void setStpLogic(StpLogic newStpLogic) { // 1、重置此账户的 StpLogic 对象 stpLogic = newStpLogic; // 2、添加到全局 StpLogic 集合中 // 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic SaManager.putStpLogic(newStpLogic); // 3、$$ 发布事件:更新了 stpLogic 对象 SaTokenEventCenter.doSetStpLogic(stpLogic); } /** * 获取 StpLogic 对象 * * @return / */ public static StpLogic getStpLogic() { return stpLogic; } // ------------------- 获取 token 相关 ------------------- /** * 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀 * * @return / */ public static String getTokenName() { return stpLogic.getTokenName(); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 */ public static void setTokenValue(String tokenValue){ stpLogic.setTokenValue(tokenValue); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param cookieTimeout Cookie存活时间(秒) */ public static void setTokenValue(String tokenValue, int cookieTimeout){ stpLogic.setTokenValue(tokenValue, cookieTimeout); } /** * 在当前会话写入指定 token 值 * * @param tokenValue token 值 * @param loginParameter 登录参数 */ public static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){ stpLogic.setTokenValue(tokenValue, loginParameter); } /** * 获取当前请求的 token 值 * * @return 当前tokenValue */ public static String getTokenValue() { return stpLogic.getTokenValue(); } /** * 获取当前请求的 token 值 (不裁剪前缀) * * @return / */ public static String getTokenValueNotCut(){ return stpLogic.getTokenValueNotCut(); } /** * 获取当前会话的 token 参数信息 * * @return token 参数信息 */ public static SaTokenInfo getTokenInfo() { return stpLogic.getTokenInfo(); } // ------------------- 登录相关操作 ------------------- // --- 登录 /** * 会话登录 * * @param id 账号id,建议的类型:(long | int | String) */ public static void login(Object id) { stpLogic.login(id); } /** * 会话登录,并指定登录设备类型 * * @param id 账号id,建议的类型:(long | int | String) * @param deviceType 设备类型 */ public static void login(Object id, String deviceType) { stpLogic.login(id, deviceType); } /** * 会话登录,并指定是否 [记住我] * * @param id 账号id,建议的类型:(long | int | String) * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public static void login(Object id, boolean isLastingCookie) { stpLogic.login(id, isLastingCookie); } /** * 会话登录,并指定此次登录 token 的有效期, 单位:秒 * * @param id 账号id,建议的类型:(long | int | String) * @param timeout 此次登录 token 的有效期, 单位:秒 */ public static void login(Object id, long timeout) { stpLogic.login(id, timeout); } /** * 会话登录,并指定所有登录参数 Model * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model */ public static void login(Object id, SaLoginParameter loginParameter) { stpLogic.login(id, loginParameter); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public static String createLoginSession(Object id) { return stpLogic.createLoginSession(id); } /** * 创建指定账号 id 的登录会话数据 * * @param id 账号id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model * @return 返回会话令牌 */ public static String createLoginSession(Object id, SaLoginParameter loginParameter) { return stpLogic.createLoginSession(id, loginParameter); } /** * 获取指定账号 id 的登录会话数据,如果获取不到则创建并返回 * * @param id 账号id,建议的类型:(long | int | String) * @return 返回会话令牌 */ public static String getOrCreateLoginSession(Object id) { return stpLogic.getOrCreateLoginSession(id); } // --- 注销 (根据 token) /** * 在当前客户端会话注销 */ public static void logout() { stpLogic.logout(); } /** * 在当前客户端会话注销,根据注销参数 */ public static void logout(SaLogoutParameter logoutParameter) { stpLogic.logout(logoutParameter); } /** * 注销下线,根据指定 token * * @param tokenValue 指定 token */ public static void logoutByTokenValue(String tokenValue) { stpLogic.logoutByTokenValue(tokenValue); } /** * 注销下线,根据指定 token、注销参数 * * @param tokenValue 指定 token * @param logoutParameter / */ public static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.logoutByTokenValue(tokenValue, logoutParameter); } /** * 踢人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token */ public static void kickoutByTokenValue(String tokenValue) { stpLogic.kickoutByTokenValue(tokenValue); } /** * 踢人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param tokenValue 指定 token * @param logoutParameter 注销参数 */ public static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.kickoutByTokenValue(tokenValue, logoutParameter); } /** * 顶人下线,根据指定 token *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token */ public static void replacedByTokenValue(String tokenValue) { stpLogic.replacedByTokenValue(tokenValue); } /** * 顶人下线,根据指定 token、注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param tokenValue 指定 token * @param logoutParameter / */ public static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { stpLogic.replacedByTokenValue(tokenValue, logoutParameter); } // --- 注销 (根据 loginId) /** * 会话注销,根据账号id * * @param loginId 账号id */ public static void logout(Object loginId) { stpLogic.logout(loginId); } /** * 会话注销,根据账号id 和 设备类型 * * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型) */ public static void logout(Object loginId, String deviceType) { stpLogic.logout(loginId, deviceType); } /** * 会话注销,根据账号id 和 注销参数 * * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void logout(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.logout(loginId, logoutParameter); } /** * 踢人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id */ public static void kickout(Object loginId) { stpLogic.kickout(loginId); } /** * 踢人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型) */ public static void kickout(Object loginId, String deviceType) { stpLogic.kickout(loginId, deviceType); } /** * 踢人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void kickout(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.kickout(loginId, logoutParameter); } /** * 顶人下线,根据账号id *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id */ public static void replaced(Object loginId) { stpLogic.replaced(loginId); } /** * 顶人下线,根据账号id 和 设备类型 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param deviceType 设备类型 (填 null 代表顶替该账号的所有设备类型) */ public static void replaced(Object loginId, String deviceType) { stpLogic.replaced(loginId, deviceType); } /** * 顶人下线,根据账号id 和 注销参数 *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id * @param logoutParameter 注销参数 */ public static void replaced(Object loginId, SaLogoutParameter logoutParameter) { stpLogic.replaced(loginId, logoutParameter); } // --- 注销 (会话管理辅助方法) /** * 在 Account-Session 上移除 Terminal 信息 (注销下线方式) * @param session / * @param terminal / */ public static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByLogout(session, terminal); } /** * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式) * @param session / * @param terminal / */ public static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByKickout(session, terminal); } /** * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式) * @param session / * @param terminal / */ public static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) { stpLogic.removeTerminalByReplaced(session, terminal); } // 会话查询 /** * 判断当前会话是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public static boolean isLogin() { return stpLogic.isLogin(); } /** * 判断指定账号是否已经登录 * * @return 已登录返回 true,未登录返回 false */ public static boolean isLogin(Object loginId) { return stpLogic.isLogin(loginId); } /** * 检验当前会话是否已经登录,如未登录,则抛出异常 */ public static void checkLogin() { stpLogic.checkLogin(); } /** * 获取当前会话账号id,如果未登录,则抛出异常 * * @return 账号id */ public static Object getLoginId() { return stpLogic.getLoginId(); } /** * 获取当前会话账号id, 如果未登录,则返回默认值 * * @param 返回类型 * @param defaultValue 默认值 * @return 登录id */ public static T getLoginId(T defaultValue) { return stpLogic.getLoginId(defaultValue); } /** * 获取当前会话账号id, 如果未登录,则返回null * * @return 账号id */ public static Object getLoginIdDefaultNull() { return stpLogic.getLoginIdDefaultNull(); } /** * 获取当前会话账号id, 并转换为 String 类型 * * @return 账号id */ public static String getLoginIdAsString() { return stpLogic.getLoginIdAsString(); } /** * 获取当前会话账号id, 并转换为 int 类型 * * @return 账号id */ public static int getLoginIdAsInt() { return stpLogic.getLoginIdAsInt(); } /** * 获取当前会话账号id, 并转换为 long 类型 * * @return 账号id */ public static long getLoginIdAsLong() { return stpLogic.getLoginIdAsLong(); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶、被冻结等状态,则返回 null * * @param tokenValue token * @return 账号id */ public static Object getLoginIdByToken(String tokenValue) { return stpLogic.getLoginIdByToken(tokenValue); } /** * 获取指定 token 对应的账号id,如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结),则返回 null * * @param tokenValue token * @return 账号id */ public Object getLoginIdByTokenNotThinkFreeze(String tokenValue) { return stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue); } /** * 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) * * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String key) { return stpLogic.getExtra(key); } /** * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) * * @param tokenValue 指定的 Token 值 * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String tokenValue, String key) { return stpLogic.getExtra(tokenValue, key); } // ------------------- Account-Session 相关 ------------------- /** * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param loginId 账号id * @param isCreate 是否新建 * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return stpLogic.getSessionByLoginId(loginId, isCreate); } /** * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,则返回 null * * @param sessionId SessionId * @return Session对象 */ public static SaSession getSessionBySessionId(String sessionId) { return stpLogic.getSessionBySessionId(sessionId); } /** * 获取指定账号 id 的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param loginId 账号id * @return SaSession 对象 */ public static SaSession getSessionByLoginId(Object loginId) { return stpLogic.getSessionByLoginId(loginId); } /** * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * * @param isCreate 是否新建 * @return Session对象 */ public static SaSession getSession(boolean isCreate) { return stpLogic.getSession(isCreate); } /** * 获取当前已登录账号的 Account-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public static SaSession getSession() { return stpLogic.getSession(); } // ------------------- Token-Session 相关 ------------------- /** * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @param tokenValue Token值 * @return Session对象 */ public static SaSession getTokenSessionByToken(String tokenValue) { return stpLogic.getTokenSessionByToken(tokenValue); } /** * 获取当前 token 的 Token-Session,如果该 SaSession 尚未创建,则新建并返回 * * @return Session对象 */ public static SaSession getTokenSession() { return stpLogic.getTokenSession(); } /** * 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) * * @return Token-Session 对象 */ public static SaSession getAnonTokenSession() { return stpLogic.getAnonTokenSession(); } // ------------------- Active-Timeout token 最低活跃度 验证相关 ------------------- /** * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) *

* 请注意: 即使 token 已被冻结 也可续签成功, * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 *

*/ public static void updateLastActiveToNow() { stpLogic.updateLastActiveToNow(); } /** * 检查当前 token 是否已被冻结,如果是则抛出异常 */ public static void checkActiveTimeout() { stpLogic.checkActiveTimeout(); } // ------------------- 过期时间相关 ------------------- /** * 获取当前会话 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getTokenTimeout() { return stpLogic.getTokenTimeout(); } /** * 获取指定 token 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @param token 指定token * @return token剩余有效时间 */ public static long getTokenTimeout(String token) { return stpLogic.getTokenTimeout(token); } /** * 获取当前登录账号的 Account-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getSessionTimeout() { return stpLogic.getSessionTimeout(); } /** * 获取当前 token 的 Token-Session 剩余有效时间(单位: 秒,返回 -1 代表永久有效,-2 代表没有这个值) * * @return token剩余有效时间 */ public static long getTokenSessionTimeout() { return stpLogic.getTokenSessionTimeout(); } /** * 获取当前 token 剩余活跃有效期:当前 token 距离被冻结还剩多少时间(单位: 秒,返回 -1 代表永不冻结,-2 代表没有这个值或 token 已被冻结了) * * @return / */ public static long getTokenActiveTimeout() { return stpLogic.getTokenActiveTimeout(); } /** * 对当前 token 的 timeout 值进行续期 * * @param timeout 要修改成为的有效时间 (单位: 秒) */ public static void renewTimeout(long timeout) { stpLogic.renewTimeout(timeout); } /** * 对指定 token 的 timeout 值进行续期 * * @param tokenValue 指定 token * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) */ public static void renewTimeout(String tokenValue, long timeout) { stpLogic.renewTimeout(tokenValue, timeout); } // ------------------- 角色认证操作 ------------------- /** * 获取:当前账号的角色集合 * * @return / */ public static List getRoleList() { return stpLogic.getRoleList(); } /** * 获取:指定账号的角色集合 * * @param loginId 指定账号id * @return / */ public static List getRoleList(Object loginId) { return stpLogic.getRoleList(loginId); } /** * 判断:当前账号是否拥有指定角色, 返回 true 或 false * * @param role 角色 * @return / */ public static boolean hasRole(String role) { return stpLogic.hasRole(role); } /** * 判断:指定账号是否含有指定角色标识, 返回 true 或 false * * @param loginId 账号id * @param role 角色标识 * @return 是否含有指定角色标识 */ public static boolean hasRole(Object loginId, String role) { return stpLogic.hasRole(loginId, role); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 * @return true或false */ public static boolean hasRoleAnd(String... roleArray){ return stpLogic.hasRoleAnd(roleArray); } /** * 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 * @return true或false */ public static boolean hasRoleOr(String... roleArray){ return stpLogic.hasRoleOr(roleArray); } /** * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException * * @param role 角色标识 */ public static void checkRole(String role) { stpLogic.checkRole(role); } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ] * * @param roleArray 角色标识数组 */ public static void checkRoleAnd(String... roleArray){ stpLogic.checkRoleAnd(roleArray); } /** * 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ] * * @param roleArray 角色标识数组 */ public static void checkRoleOr(String... roleArray){ stpLogic.checkRoleOr(roleArray); } // ------------------- 权限认证操作 ------------------- /** * 获取:当前账号的权限码集合 * * @return / */ public static List getPermissionList() { return stpLogic.getPermissionList(); } /** * 获取:指定账号的权限码集合 * * @param loginId 指定账号id * @return / */ public static List getPermissionList(Object loginId) { return stpLogic.getPermissionList(loginId); } /** * 判断:当前账号是否含有指定权限, 返回 true 或 false * * @param permission 权限码 * @return 是否含有指定权限 */ public static boolean hasPermission(String permission) { return stpLogic.hasPermission(permission); } /** * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false * * @param loginId 账号 id * @param permission 权限码 * @return 是否含有指定权限 */ public static boolean hasPermission(Object loginId, String permission) { return stpLogic.hasPermission(loginId, permission); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public static boolean hasPermissionAnd(String... permissionArray){ return stpLogic.hasPermissionAnd(permissionArray); } /** * 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 * @return true 或 false */ public static boolean hasPermissionOr(String... permissionArray){ return stpLogic.hasPermissionOr(permissionArray); } /** * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException * * @param permission 权限码 */ public static void checkPermission(String permission) { stpLogic.checkPermission(permission); } /** * 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ] * * @param permissionArray 权限码数组 */ public static void checkPermissionAnd(String... permissionArray) { stpLogic.checkPermissionAnd(permissionArray); } /** * 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ] * * @param permissionArray 权限码数组 */ public static void checkPermissionOr(String... permissionArray) { stpLogic.checkPermissionOr(permissionArray); } // ------------------- id 反查 token 相关操作 ------------------- /** * 获取指定账号 id 的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @return token值 */ public static String getTokenValueByLoginId(Object loginId) { return stpLogic.getTokenValueByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的 token *

* 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return token值 */ public static String getTokenValueByLoginId(Object loginId, String deviceType) { return stpLogic.getTokenValueByLoginId(loginId, deviceType); } /** * 获取指定账号 id 的 token 集合 * * @param loginId 账号id * @return 此 loginId 的所有相关 token */ public static List getTokenValueListByLoginId(Object loginId) { return stpLogic.getTokenValueListByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的 token 集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public static List getTokenValueListByLoginId(Object loginId, String deviceType) { return stpLogic.getTokenValueListByLoginId(loginId, deviceType); } /** * 获取指定账号 id 已登录设备信息集合 * * @param loginId 账号id * @return 此 loginId 的所有登录 token */ public static List getTerminalListByLoginId(Object loginId) { return stpLogic.getTerminalListByLoginId(loginId); } /** * 获取指定账号 id 指定设备类型端的已登录设备信息集合 * * @param loginId 账号id * @param deviceType 设备类型,填 null 代表不限设备类型 * @return / */ public static List getTerminalListByLoginId(Object loginId, String deviceType) { return stpLogic.getTerminalListByLoginId(loginId, deviceType); } /** * 获取指定账号 id 已登录设备信息集合,执行特定函数 * * @param loginId 账号id * @param function 需要执行的函数 */ public static void forEachTerminalList(Object loginId, SaTwoParamFunction function) { stpLogic.forEachTerminalList(loginId, function); } /** * 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ public static String getLoginDeviceType() { return stpLogic.getLoginDeviceType(); } /** * 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ public static String getLoginDeviceTypeByToken(String tokenValue) { return stpLogic.getLoginDeviceTypeByToken(tokenValue); } /** * 获取当前 token 的最后活跃时间(13位时间戳),如果不存在则返回 -2 * * @return / */ public static long getTokenLastActiveTime() { return stpLogic.getTokenLastActiveTime(); } /** * 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 * @param deviceId / * @return / */ public static boolean isTrustDeviceId(Object userId, String deviceId) { return stpLogic.isTrustDeviceId(userId, deviceId); } // ------------------- 会话管理 ------------------- /** * 根据条件查询缓存中所有的 token * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return token集合 */ public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { return stpLogic.searchTokenValue(keyword, start, size, sortType); } /** * 根据条件查询缓存中所有的 SessionId * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public static List searchSessionId(String keyword, int start, int size, boolean sortType) { return stpLogic.searchSessionId(keyword, start, size, sortType); } /** * 根据条件查询缓存中所有的 Token-Session-Id * * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) * * @return sessionId集合 */ public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { return stpLogic.searchTokenSessionId(keyword, start, size, sortType); } // ------------------- 账号封禁 ------------------- /** * 封禁:指定账号 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, long time) { stpLogic.disable(loginId, time); } /** * 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) * * @param loginId 账号id * @return / */ public static boolean isDisable(Object loginId) { return stpLogic.isDisable(loginId); } /** * 校验:指定账号是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id */ public static void checkDisable(Object loginId) { stpLogic.checkDisable(loginId); } /** * 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @return / */ public static long getDisableTime(Object loginId) { return stpLogic.getDisableTime(loginId); } /** * 解封:指定账号 * * @param loginId 账号id */ public static void untieDisable(Object loginId) { stpLogic.untieDisable(loginId); } // ------------------- 分类封禁 ------------------- /** * 封禁:指定账号的指定服务 *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id * @param service 指定服务 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, String service, long time) { stpLogic.disable(loginId, service, time); } /** * 判断:指定账号的指定服务 是否已被封禁(true=已被封禁, false=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return / */ public static boolean isDisable(Object loginId, String service) { return stpLogic.isDisable(loginId, service); } /** * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void checkDisable(Object loginId, String... services) { stpLogic.checkDisable(loginId, services); } /** * 获取:指定账号 指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) * * @param loginId 账号id * @param service 指定服务 * @return see note */ public static long getDisableTime(Object loginId, String service) { return stpLogic.getDisableTime(loginId, service); } /** * 解封:指定账号、指定服务 * * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void untieDisable(Object loginId, String... services) { stpLogic.untieDisable(loginId, services); } // ------------------- 阶梯封禁 ------------------- /** * 封禁:指定账号,并指定封禁等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, int level, long time) { stpLogic.disableLevel(loginId, level, time); } /** * 封禁:指定账号的指定服务,并指定封禁等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, String service, int level, long time) { stpLogic.disableLevel(loginId, service, level, time); } /** * 判断:指定账号是否已被封禁到指定等级 * * @param loginId 指定账号id * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, int level) { return stpLogic.isDisableLevel(loginId, level); } /** * 判断:指定账号的指定服务,是否已被封禁到指定等级 * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, String service, int level) { return stpLogic.isDisableLevel(loginId, service, level); } /** * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, int level) { stpLogic.checkDisableLevel(loginId, level); } /** * 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id * @param service 指定封禁服务 * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, String service, int level) { stpLogic.checkDisableLevel(loginId, service, level); } /** * 获取:指定账号被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @return / */ public static int getDisableLevel(Object loginId) { return stpLogic.getDisableLevel(loginId); } /** * 获取:指定账号的 指定服务 被封禁的等级,如果未被封禁则返回-2 * * @param loginId 指定账号id * @param service 指定封禁服务 * @return / */ public static int getDisableLevel(Object loginId, String service) { return stpLogic.getDisableLevel(loginId, service); } // ------------------- 临时身份切换 ------------------- /** * 临时切换身份为指定账号id * * @param loginId 指定loginId */ public static void switchTo(Object loginId) { stpLogic.switchTo(loginId); } /** * 结束临时切换身份 */ public static void endSwitch() { stpLogic.endSwitch(); } /** * 判断当前请求是否正处于 [ 身份临时切换 ] 中 * * @return / */ public static boolean isSwitch() { return stpLogic.isSwitch(); } /** * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 * * @param loginId 指定账号id * @param function 要执行的方法 */ public static void switchTo(Object loginId, SaFunction function) { stpLogic.switchTo(loginId, function); } // ------------------- 二级认证 ------------------- /** * 在当前会话 开启二级认证 * * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(long safeTime) { stpLogic.openSafe(safeTime); } /** * 在当前会话 开启二级认证 * * @param service 业务标识 * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(String service, long safeTime) { stpLogic.openSafe(service, safeTime); } /** * 判断:当前会话是否处于二级认证时间内 * * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe() { return stpLogic.isSafe(); } /** * 判断:当前会话 是否处于指定业务的二级认证时间内 * * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String service) { return stpLogic.isSafe(service); } /** * 判断:指定 token 是否处于二级认证时间内 * * @param tokenValue Token 值 * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String tokenValue, String service) { return stpLogic.isSafe(tokenValue, service); } /** * 校验:当前会话是否已通过二级认证,如未通过则抛出异常 */ public static void checkSafe() { stpLogic.checkSafe(); } /** * 校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常 * * @param service 业务标识 */ public static void checkSafe(String service) { stpLogic.checkSafe(service); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @return 剩余有效时间 */ public static long getSafeTime() { return stpLogic.getSafeTime(); } /** * 获取:当前会话的二级认证剩余有效时间(单位: 秒, 返回-2代表尚未通过二级认证) * * @param service 业务标识 * @return 剩余有效时间 */ public static long getSafeTime(String service) { return stpLogic.getSafeTime(service); } /** * 在当前会话 结束二级认证 */ public static void closeSafe() { stpLogic.closeSafe(); } /** * 在当前会话 结束指定业务标识的二级认证 * * @param service 业务标识 */ public static void closeSafe(String service) { stpLogic.closeSafe(service); } // ------------------- Bean 对象、字段代理 ------------------- /** * 根据当前配置对象创建一个 SaLoginParameter 对象 * * @return / */ public static SaLoginParameter createSaLoginParameter() { return stpLogic.createSaLoginParameter(); } // ------------------- 过期方法 ------------------- /** *

请更换为 getLoginDeviceType

* 返回当前会话的登录设备类型 * * @return 当前令牌的登录设备类型 */ @Deprecated public static String getLoginDevice() { return stpLogic.getLoginDevice(); } /** *

请更换为 getLoginDeviceTypeByToken

* 返回指定 token 会话的登录设备类型 * * @param tokenValue 指定token * @return 当前令牌的登录设备类型 */ @Deprecated public static String getLoginDeviceByToken(String tokenValue) { return stpLogic.getLoginDeviceByToken(tokenValue); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/AtController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckHttpBasic; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.annotation.SaCheckSafe; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * @author click33 * */ @RestController @RequestMapping("/at/") public class AtController { // 登录认证,登录之后才可以进入方法 ---- http://localhost:8081/at/checkLogin @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 权限认证,具备user-add权限才可以进入方法 ---- http://localhost:8081/at/checkPermission @SaCheckPermission("user-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限认证,同时具备所有权限才可以进入 ---- http://localhost:8081/at/checkPermissionAnd @SaCheckPermission({"user-add", "user-delete", "user-update"}) @RequestMapping("checkPermissionAnd") public SaResult checkPermissionAnd() { return SaResult.ok(); } // 权限认证,只要具备其中一个就可以进入 ---- http://localhost:8081/at/checkPermissionOr @SaCheckPermission(value = {"user-add", "user-delete", "user-update"}, mode = SaMode.OR) @RequestMapping("checkPermissionOr") public SaResult checkPermissionOr() { return SaResult.ok(); } // 角色认证,只有具备admin角色才可以进入 ---- http://localhost:8081/at/checkRole @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 完成二级认证 ---- http://localhost:8081/at/openSafe @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(200); // 打开二级认证,有效期为200秒 return SaResult.ok(); } // 通过二级认证后才可以进入 ---- http://localhost:8081/at/checkSafe @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 通过Basic认证后才可以进入 ---- http://localhost:8081/at/checkBasic @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("checkBasic") public SaResult checkBasic() { return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); StpUtil.getTokenSession(); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 校验登录 ---- http://localhost:8081/acc/checkLogin @RequestMapping("checkLogin") public SaResult checkLogin() { StpUtil.checkLogin(); return SaResult.ok(); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 查询账号登录设备信息 ---- http://localhost:8081/acc/terminalInfo @RequestMapping("terminalInfo") public SaResult terminalInfo() { System.out.println("账号 10001 登录设备信息:"); List terminalList = StpUtil.getTerminalListByLoginId(10001); for (SaTerminalInfo ter : terminalList) { System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime()); } return SaResult.data(terminalList); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.login(10001, SaLoginParameter.create().setIsConcurrent(false)); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/StressTestController.java ================================================ package com.pj.test; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pj.util.Ttime; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 压力测试 * @author click33 * */ @RestController @RequestMapping("/s-test/") public class StressTestController { // 测试 浏览器访问: http://localhost:8081/s-test/login // 测试前,请先将 is-read-cookie 配置为 false @RequestMapping("login") public SaResult login() { // StpUtil.getTokenSession().logout(); // StpUtil.logoutByLoginId(10001); int count = 10; // 循环多少轮 int loginCount = 10000; // 每轮循环多少次 // 循环10次 取平均时间 List list = new ArrayList<>(); for (int i = 1; i <= count; i++) { System.out.println("\n---------------------第" + i + "轮---------------------"); Ttime t = new Ttime().start(); // 每次登录的次数 for (int j = 1; j <= loginCount; j++) { StpUtil.login("1000" + j, "PC-" + j); if(j % 1000 == 0) { System.out.println("已登录:" + j); } } t.end(); list.add((t.returnMs() + 0.0) / 1000); System.out.println("第" + i + "轮" + "用时:" + t.toString()); } // System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size()); System.out.println("\n---------------------测试结果---------------------"); System.out.println(list.size() + "次测试: " + list); double ss = 0; for (int i = 0; i < list.size(); i++) { ss += list.get(i); } System.out.println("平均用时: " + ss / list.size()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/Test2Controller.java ================================================ package com.pj.test; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试专用Controller * @author click33 * */ @RestController public class Test2Controller { // 测试登录 ---- http://localhost:8081/test @RequestMapping("/test") public SaResult test2() { System.out.println(SpringMVCUtil.getRequest()); System.out.println(SaTokenContextServletUtil.getRequest()); // StpUtil.login(30003); // System.out.println(StpUtil.getSession().timeout()); // System.out.println(StpUtil.getStpLogic().getTokenSession(false)); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.annotation.SaCheckHttpDigest; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Date; import java.util.List; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { // 测试登录 ---- http://localhost:8081/test/login @RequestMapping("login") public SaResult login(@RequestParam(defaultValue = "10001") long id, String dt) { StpUtil.login(id, new SaLoginParameter() .setIsConcurrent(true) .setIsShare(false) // .setDeviceType(dt) .setMaxLoginCount(4) .setMaxTryTimes(12) .setTerminalExtra("deviceSimpleTitle", "XiaoMi 15 Ultra") .setTerminalExtra("loginAddress", "浙江省杭州市西湖区") .setTerminalExtra("loginIp", "127.0.0.1") .setTerminalExtra("loginTime", SaFoxUtil.formatDate(new Date())) ); StpUtil.getTokenSession(); return SaResult.ok("登录成功"); } // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("------------进来了 " + SaFoxUtil.formatDate(new Date())); // 获取所有已登录的会话id List sessionIdList = StpUtil.searchSessionId(null, 0, -1, false); for (String sessionId : sessionIdList) { // 根据会话id,查询对应的 SaSession 对象,此处一个 SaSession 对象即代表一个登录的账号 SaSession session = StpUtil.getSessionBySessionId(sessionId); // 查询这个账号都在哪些设备登录了,依据上面的示例,账号A 的 SaTerminalInfo 数量是 3,账号B 的 SaTerminalInfo 数量是 2 List terminalList = session.terminalListCopy(); System.out.println("会话id:" + sessionId + ",共在 " + terminalList.size() + " 设备登录"); } // 返回 return SaResult.data(null); } // 测试 浏览器访问: http://localhost:8081/test/test2 @RequestMapping("test2") public SaResult test2() { return SaResult.ok(); } // 测试 浏览器访问: http://localhost:8081/test/getRequestPath @RequestMapping("getRequestPath") public SaResult getRequestPath() { System.out.println("------------ 测试访问路径获取 "); System.out.println("SpringMVCUtil.getRequest().getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); System.out.println("SaHolder.getRequest().getRequestPath() " + SaHolder.getRequest().getRequestPath()); return SaResult.ok(); } // 测试 Http Digest 认证 浏览器访问: http://localhost:8081/test/testDigest @SaCheckHttpDigest("sa:123456") @RequestMapping("testDigest") public SaResult testDigest() { // SaHttpDigestUtil.check("sa", "123456"); // 返回 return SaResult.data(null); } // 测试注销 浏览器访问: http://localhost:8081/test/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.data(null); } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/AjaxJson.java ================================================ package com.pj.util; import java.io.Serializable; import java.util.List; /** * ajax请求返回Json格式数据的封装 */ public class AjaxJson implements Serializable{ private static final long serialVersionUID = 1L; // 序列化版本号 public static final int CODE_SUCCESS = 200; // 成功状态码 public static final int CODE_ERROR = 500; // 错误状态码 public static final int CODE_WARNING = 501; // 警告状态码 public static final int CODE_NOT_JUR = 403; // 无权限状态码 public static final int CODE_NOT_LOGIN = 401; // 未登录状态码 public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码 public int code; // 状态码 public String msg; // 描述信息 public Object data; // 携带对象 public Long dataCount; // 数据总数,用于分页 /** * 返回code * @return */ public int getCode() { return this.code; } /** * 给msg赋值,连缀风格 */ public AjaxJson setMsg(String msg) { this.msg = msg; return this; } public String getMsg() { return this.msg; } /** * 给data赋值,连缀风格 */ public AjaxJson setData(Object data) { this.data = data; return this; } /** * 将data还原为指定类型并返回 */ @SuppressWarnings("unchecked") public T getData(Class cs) { return (T) data; } // ============================ 构建 ================================== public AjaxJson(int code, String msg, Object data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson(CODE_SUCCESS, "ok", null, null); } public static AjaxJson getSuccess(String msg) { return new AjaxJson(CODE_SUCCESS, msg, null, null); } public static AjaxJson getSuccess(String msg, Object data) { return new AjaxJson(CODE_SUCCESS, msg, data, null); } public static AjaxJson getSuccessData(Object data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } public static AjaxJson getSuccessArray(Object... data) { return new AjaxJson(CODE_SUCCESS, "ok", data, null); } // 返回失败 public static AjaxJson getError() { return new AjaxJson(CODE_ERROR, "error", null, null); } public static AjaxJson getError(String msg) { return new AjaxJson(CODE_ERROR, msg, null, null); } // 返回警告 public static AjaxJson getWarning() { return new AjaxJson(CODE_ERROR, "warning", null, null); } public static AjaxJson getWarning(String msg) { return new AjaxJson(CODE_WARNING, msg, null, null); } // 返回未登录 public static AjaxJson getNotLogin() { return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null); } // 返回没有权限的 public static AjaxJson getNotJur(String msg) { return new AjaxJson(CODE_NOT_JUR, msg, null, null); } // 返回一个自定义状态码的 public static AjaxJson get(int code, String msg){ return new AjaxJson(code, msg, null, null); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, Object data){ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount); } // 返回,根据受影响行数的(大于0=ok,小于0=error) public static AjaxJson getByLine(int line){ if(line > 0){ return getSuccess("ok", line); } return getError("error").setData(line); } // 返回,根据布尔值来确定最终结果的 (true=ok,false=error) public static AjaxJson getByBoolean(boolean b){ return b ? getSuccess("ok") : getError("error"); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @SuppressWarnings("rawtypes") @Override public String toString() { String data_string = null; if(data == null){ } else if(data instanceof List){ data_string = "List(length=" + ((List)data).size() + ")"; } else { data_string = data.toString(); } return "{" + "\"code\": " + this.getCode() + ", \"msg\": \"" + this.getMsg() + "\"" + ", \"data\": " + data_string + ", \"dataCount\": " + dataCount + "}"; } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/Ttime.java ================================================ package com.pj.util; /** * 用于测试用时 * @author click33 * */ public class Ttime { private long start=0; //开始时间 private long end=0; //结束时间 public static Ttime t = new Ttime(); //static快捷使用 /** * 开始计时 * @return */ public Ttime start() { start=System.currentTimeMillis(); return this; } /** * 结束计时 */ public Ttime end() { end=System.currentTimeMillis(); return this; } /** * 返回所用毫秒数 */ public long returnMs() { return end-start; } /** * 格式化输出结果 */ public void outTime() { System.out.println(this.toString()); } /** * 结束并格式化输出结果 */ public void endOutTime() { this.end().outTime(); } @Override public String toString() { return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s } } ================================================ FILE: sa-token-demo/sa-token-demo-test/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-thymeleaf 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-thymeleaf cn.dev33 sa-token-spring-boot-starter ${sa-token.version} cn.dev33 sa-token-thymeleaf ${sa-token.version} org.springframework.boot spring-boot-devtools provided org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/SaTokenThymeleafDemoApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; @SpringBootApplication public class SaTokenThymeleafDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenThymeleafDemoApplication.class, args); System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); System.out.println("\n测试访问:http://localhost:8081/"); } } ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.thymeleaf.spring5.view.ThymeleafViewResolver; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.thymeleaf.dialect.SaTokenDialect; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // Sa-Token 标签方言 (Thymeleaf版) @Bean public SaTokenDialect getSaTokenDialect() { return new SaTokenDialect(); } // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 @Autowired private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { viewResolver.addStaticVariable("stp", StpUtil.stpLogic); } } ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import cn.dev33.satoken.util.SaResult; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 测试 Controller * * @author click33 */ @RestController public class TestController { // 首页 @RequestMapping("/") public Object index() { return new ModelAndView("index.html"); } // 登录 @RequestMapping("login") public SaResult login(@RequestParam(defaultValue="10001") String id) { StpUtil.login(id); StpUtil.getSession().set("name", "zhangsan"); return SaResult.ok(); } // 注销 @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 ================================================ FILE: sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/templates/index.html ================================================ Sa-Token 集成 Thymeleaf 标签方言

Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面

当前是否登录:

登录 注销

登录之后才能显示:value

不登录才能显示:value

具有角色 admin 才能显示:value

同时具备多个角色才能显示:value

只要具有其中一个角色就能显示:value

不具有角色 admin 才能显示:value

具有权限 user-add 才能显示:value

同时具备多个权限才能显示:value

只要具有其中一个权限就能显示:value

不具有权限 user-add 才能显示:value

从SaSession中取值:

================================================ FILE: sa-token-demo/sa-token-demo-webflux/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-webflux 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.7.18 1.45.0 org.springframework.boot spring-boot-starter-webflux cn.dev33 sa-token-reactor-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/SaTokenWebfluxApplication.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token整合webflux 示例 * @author click33 * */ @SpringBootApplication public class SaTokenWebfluxApplication { public static void main(String[] args) { SpringApplication.run(SaTokenWebfluxApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/satoken/MyFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pj.satoken; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * 自定义过滤器 */ @Component public class MyFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { System.out.println("进入自定义过滤器"); try { // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 SaReactorSyncHolder.setContext(exchange); System.out.println(StpUtil.isLogin()); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure { /** * 注册 [sa-token全局过滤器] */ @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 指定 [拦截路由] .addInclude("/**") // 指定 [放行路由] .addExclude("/favicon.ico") // 指定[认证函数]: 每次请求执行 .setAuth(r -> { System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); }) // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); e.printStackTrace(); return SaResult.error(e.getMessage()); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/DefineRoutes.java ================================================ package com.pj.test; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; @Configuration public class DefineRoutes { /** * 函数式编程,初始化路由表 * @return 路由表 */ @SuppressWarnings("deprecation") @Bean public RouterFunction getRoutes() { return RouterFunctions.route(RequestPredicates.GET("/fun"), req -> { return SaReactorSyncHolder.setContext(req.exchange(), () -> { System.out.println("是否登录:" + StpUtil.isLogin()); SaResult res = SaResult.data(StpUtil.getTokenInfo()); return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).syncBody(res); }); }); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.time.Duration; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { @Autowired UserService userService; // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login @RequestMapping("login") public Mono login(@RequestParam(defaultValue="10001") String id) { return SaReactorHolder.sync(() -> { StpUtil.login(id); return SaResult.ok("登录成功"); }); } // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin @RequestMapping("isLogin") public SaResult isLogin(ServerWebExchange exchange) { try { SaReactorSyncHolder.setContext(exchange); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); } finally { SaReactorSyncHolder.clearContext(); } } // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 @RequestMapping("isLogin2") public SaResult isLogin2(ServerWebExchange exchange) { SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); return SaResult.data(res); } // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 @RequestMapping("isLogin3") public Mono isLogin3() { return SaReactorHolder.sync(() -> { System.out.println("是否登录:" + StpUtil.isLogin()); userService.isLogin(); return SaResult.data(StpUtil.getTokenInfo()); }); } // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 @RequestMapping("isLogin4") public Mono isLogin4() { return userService.findUserIdByNamePwd("ZhangSan", "123456").flatMap(userId -> { return SaReactorHolder.sync(() -> { StpUtil.login(userId); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 @RequestMapping("isLogin5") public Mono isLogin5() { System.out.println("线程id-----" + Thread.currentThread().getId()); // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 return Mono.delay(Duration.ofSeconds(1)) .doOnNext(r-> System.out.println("线程id-----" + Thread.currentThread().getId())) .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) .map(r-> userService.findUserIdByNamePwd("ZhangSan", "123456")) .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) .flatMap(isLogin -> { System.out.println("是否登录 " + isLogin); return SaReactorHolder.sync(() -> { System.out.println("是否登录 " + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 @RequestMapping("isLogin6") public SaResult isLogin6(@CookieValue("satoken") String satoken) { System.out.println("token 为:" + satoken); System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); } // API测试:SaSession 写值 --- http://localhost:8081/test/sessionSet @RequestMapping("sessionSet") public Mono sessionSet() { return SaReactorHolder.sync(() -> { System.out.println("session name 值为:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", "zhangsan"); System.out.println("session name 值为:" + StpUtil.getSession().get("name")); return SaResult.data(StpUtil.getSession().get("name")); }); } // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("线程id------- " + Thread.currentThread().getId()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/UserService.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; /** * 模拟 Service 方法 * @author click33 * @since 2025/4/6 */ @Service public class UserService { public boolean isLogin() { System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); return StpUtil.isLogin(); } public Mono findUserIdByNamePwd(String name, String pwd) { // ... return Mono.just(10001L); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-webflux-springboot3 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.0.1 1.45.0 org.springframework.boot spring-boot-starter-webflux cn.dev33 sa-token-reactor-spring-boot3-starter ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/SaTokenWebfluxSpringboot3Application.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token整合webflux 示例 (springboot3) * * @author click33 * @since 2023年1月3日 * */ @SpringBootApplication public class SaTokenWebfluxSpringboot3Application { public static void main(String[] args) { SpringApplication.run(SaTokenWebfluxSpringboot3Application.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pj.satoken; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * 自定义过滤器 */ @Component public class MyFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { System.out.println("进入自定义过滤器"); try { // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 SaReactorSyncHolder.setContext(exchange); System.out.println(StpUtil.isLogin()); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure { /** * 注册 [sa-token全局过滤器] */ @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 指定 [拦截路由] .addInclude("/**") // 指定 [放行路由] .addExclude("/favicon.ico") // 指定[认证函数]: 每次请求执行 .setAuth(r -> { System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); }) // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/DefineRoutes.java ================================================ package com.pj.test; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; @Configuration public class DefineRoutes { /** * 函数式编程,初始化路由表 * @return 路由表 */ @SuppressWarnings("deprecation") @Bean public RouterFunction getRoutes() { return RouterFunctions.route(RequestPredicates.GET("/fun"), req -> { return SaReactorSyncHolder.setContext(req.exchange(), () -> { System.out.println("是否登录:" + StpUtil.isLogin()); SaResult res = SaResult.data(StpUtil.getTokenInfo()); return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).syncBody(res); }); }); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.time.Duration; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { @Autowired UserService userService; // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login @RequestMapping("login") public Mono login(@RequestParam(defaultValue="10001") String id) { return SaReactorHolder.sync(() -> { StpUtil.login(id); return SaResult.ok("登录成功"); }); } // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin @RequestMapping("isLogin") public SaResult isLogin(ServerWebExchange exchange) { try { SaReactorSyncHolder.setContext(exchange); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); } finally { SaReactorSyncHolder.clearContext(); } } // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 @RequestMapping("isLogin2") public SaResult isLogin2(ServerWebExchange exchange) { SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); return SaResult.data(res); } // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 @RequestMapping("isLogin3") public Mono isLogin3() { return SaReactorHolder.sync(() -> { System.out.println("是否登录:" + StpUtil.isLogin()); userService.isLogin(); return SaResult.data(StpUtil.getTokenInfo()); }); } // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 @RequestMapping("isLogin4") public Mono isLogin4() { return userService.findUserIdByNamePwd("ZhangSan", "123456").flatMap(userId -> { return SaReactorHolder.sync(() -> { StpUtil.login(userId); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 @RequestMapping("isLogin5") public Mono isLogin5() { System.out.println("线程id-----" + Thread.currentThread().getId()); // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 return Mono.delay(Duration.ofSeconds(1)) .doOnNext(r-> System.out.println("线程id-----" + Thread.currentThread().getId())) .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) .map(r-> userService.findUserIdByNamePwd("ZhangSan", "123456")) .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) .flatMap(isLogin -> { System.out.println("是否登录 " + isLogin); return SaReactorHolder.sync(() -> { System.out.println("是否登录 " + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 @RequestMapping("isLogin6") public SaResult isLogin6(@CookieValue("satoken") String satoken) { System.out.println("token 为:" + satoken); System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); } // API测试:SaSession 写值 --- http://localhost:8081/test/sessionSet @RequestMapping("sessionSet") public Mono sessionSet() { return SaReactorHolder.sync(() -> { System.out.println("session name 值为:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", "zhangsan"); System.out.println("session name 值为:" + StpUtil.getSession().get("name")); return SaResult.data(StpUtil.getSession().get("name")); }); } // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("线程id------- " + Thread.currentThread().getId()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/UserService.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; /** * 模拟 Service 方法 * @author click33 * @since 2025/4/6 */ @Service public class UserService { public boolean isLogin() { System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); return StpUtil.isLogin(); } public Mono findUserIdByNamePwd(String name, String pwd) { // ... return Mono.just(10001L); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot3/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-webflux-springboot4 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 4.0.3 1.45.0 org.springframework.boot spring-boot-starter-webflux cn.dev33 sa-token-reactor-spring-boot4-starter ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java ================================================ package com.pj; import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Sa-Token整合webflux 示例 (springboot4) * * @author click33 * @since 2026年2月27日 * */ @SpringBootApplication public class SaTokenWebfluxSpringboot4Application { public static void main(String[] args) { SpringApplication.run(SaTokenWebfluxSpringboot4Application.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.pj.satoken; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * 自定义过滤器 */ @Component public class MyFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { System.out.println("进入自定义过滤器"); try { // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 SaReactorSyncHolder.setContext(exchange); System.out.println(StpUtil.isLogin()); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java ================================================ package com.pj.satoken; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * @author click33 * */ @Configuration public class SaTokenConfigure { /** * 注册 [sa-token全局过滤器] */ @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 指定 [拦截路由] .addInclude("/**") // 指定 [放行路由] .addExclude("/favicon.ico") // 指定[认证函数]: 每次请求执行 .setAuth(r -> { System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); }) // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) ; } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java ================================================ package com.pj.satoken; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 */ @Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user-add"); list.add("user-delete"); list.add("user-update"); list.add("user-get"); list.add("article-get"); return list; } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java ================================================ package com.pj.test; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; @Configuration public class DefineRoutes { /** * 函数式编程,初始化路由表 * @return 路由表 */ @Bean public RouterFunction getRoutes() { return RouterFunctions.route(RequestPredicates.GET("/fun"), req -> { return SaReactorSyncHolder.setContext(req.exchange(), () -> { System.out.println("是否登录:" + StpUtil.isLogin()); SaResult res = SaResult.data(StpUtil.getTokenInfo()); return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(res); }); }); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java ================================================ package com.pj.test; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 */ @RestControllerAdvice public class GlobalException { @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java ================================================ package com.pj.test; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.time.Duration; /** * 测试专用Controller * @author click33 * */ @RestController @RequestMapping("/test/") public class TestController { @Autowired UserService userService; // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login @RequestMapping("login") public Mono login(@RequestParam(defaultValue="10001") String id) { return SaReactorHolder.sync(() -> { StpUtil.login(id); return SaResult.ok("登录成功"); }); } // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin @RequestMapping("isLogin") public SaResult isLogin(ServerWebExchange exchange) { try { SaReactorSyncHolder.setContext(exchange); System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); } finally { SaReactorSyncHolder.clearContext(); } } // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2 @RequestMapping("isLogin2") public SaResult isLogin2(ServerWebExchange exchange) { SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); return SaResult.data(res); } // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3 @RequestMapping("isLogin3") public Mono isLogin3() { return SaReactorHolder.sync(() -> { System.out.println("是否登录:" + StpUtil.isLogin()); userService.isLogin(); return SaResult.data(StpUtil.getTokenInfo()); }); } // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4 @RequestMapping("isLogin4") public Mono isLogin4() { return userService.findUserIdByNamePwd("ZhangSan", "123456").flatMap(userId -> { return SaReactorHolder.sync(() -> { StpUtil.login(userId); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5 @RequestMapping("isLogin5") public Mono isLogin5() { System.out.println("线程id-----" + Thread.currentThread().getId()); // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹 return Mono.delay(Duration.ofSeconds(1)) .doOnNext(r-> System.out.println("线程id-----" + Thread.currentThread().getId())) .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) .map(r-> userService.findUserIdByNamePwd("ZhangSan", "123456")) .map(r-> SaReactorHolder.sync( () -> userService.isLogin() )) .flatMap(isLogin -> { System.out.println("是否登录 " + isLogin); return SaReactorHolder.sync(() -> { System.out.println("是否登录 " + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); }); } // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6 @RequestMapping("isLogin6") public SaResult isLogin6(@CookieValue("satoken") String satoken) { System.out.println("token 为:" + satoken); System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken)); return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken)); } // API测试:SaSession 写值 --- http://localhost:8081/test/sessionSet @RequestMapping("sessionSet") public Mono sessionSet() { return SaReactorHolder.sync(() -> { System.out.println("session name 值为:" + StpUtil.getSession().get("name")); StpUtil.getSession().set("name", "zhangsan"); System.out.println("session name 值为:" + StpUtil.getSession().get("name")); return SaResult.data(StpUtil.getSession().get("name")); }); } // 测试 浏览器访问: http://localhost:8081/test/test @RequestMapping("test") public SaResult test() { System.out.println("线程id------- " + Thread.currentThread().getId()); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java ================================================ package com.pj.test; import cn.dev33.satoken.stp.StpUtil; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; /** * 模拟 Service 方法 * @author click33 * @since 2025/4/6 */ @Service public class UserService { public boolean isLogin() { System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin()); return StpUtil.isLogin(); } public Mono findUserIdByNamePwd(String name, String pwd) { // ... return Mono.just(10001L); } } ================================================ FILE: sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-websocket/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-websocket 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-websocket cn.dev33 sa-token-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; /** * Sa-Token 整合 WebSocket 鉴权示例 * @author click33 * */ @SpringBootApplication public class SaTokenWebSocketApplication { /* * 1、访问登录接口,拿到会话Token: * http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 * * 2、找一个WebSocket在线测试页面进行连接, * 例如: * https://www.bejson.com/httputil/websocket/ * 然后连接地址: * ws://localhost:8081/ws-connect/2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5 */ public static void main(String[] args) { SpringApplication.run(SaTokenWebSocketApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java ================================================ package com.pj.ws; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 开启WebSocket支持 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java ================================================ package com.pj.ws; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import org.springframework.stereotype.Component; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; /** * WebSocket 连接测试 */ @Component @ServerEndpoint("/ws-connect/{satoken}") public class WebSocketConnect { /** * 固定前缀 */ private static final String USER_ID = "user_id_"; /** * 存放Session集合,方便推送消息 (javax.websocket.Session) */ private static ConcurrentHashMap sessionMap = new ConcurrentHashMap<>(); // 监听:连接成功 @OnOpen public void onOpen(Session session, @PathParam("satoken") String satoken) throws IOException { // 根据 token 获取对应的 userId Object loginId = StpUtil.getLoginIdByToken(satoken); if(loginId == null) { session.close(); throw new SaTokenException("连接失败,无效Token:" + satoken); } // put到集合,方便后续操作 long userId = SaFoxUtil.getValueByType(loginId, long.class); sessionMap.put(USER_ID + userId, session); // 给个提示 String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; System.out.println(tips); sendMessage(session, tips); } // 监听: 连接关闭 @OnClose public void onClose(Session session) { System.out.println("连接关闭,sid=" + session.getId()); for (String key : sessionMap.keySet()) { if(sessionMap.get(key).getId().equals(session.getId())) { sessionMap.remove(key); } } } // 监听:收到客户端发送的消息 @OnMessage public void onMessage(Session session, String message) { System.out.println("sid为:" + session.getId() + ",发来:" + message); } // 监听:发生异常 @OnError public void onError(Session session, Throwable error) { System.out.println("sid为:" + session.getId() + ",发生错误"); error.printStackTrace(); } // --------- // 向指定客户端推送消息 public static void sendMessage(Session session, String message) { try { System.out.println("向sid为:" + session.getId() + ",发送:" + message); session.getBasicRemote().sendText(message); } catch (IOException e) { throw new RuntimeException(e); } } // 向指定用户推送消息 public static void sendMessage(long userId, String message) { Session session = sessionMap.get(USER_ID + userId); if(session != null) { sendMessage(session, message); } } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-demo-websocket-spring 0.0.1-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.14 1.45.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-websocket cn.dev33 sa-token-spring-boot-starter ${sa-token.version} org.springframework.boot spring-boot-configuration-processor true ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java ================================================ package com.pj; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import cn.dev33.satoken.SaManager; /** * Sa-Token 整合 WebSocket 鉴权示例 * @author click33 * */ @SpringBootApplication public class SaTokenWebSocketSpringApplication { /* * 1、访问登录接口,拿到会话Token: * http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 * * 2、找一个WebSocket在线测试页面进行连接, * 例如: * https://www.bejson.com/httputil/websocket/ * 然后连接地址: * ws://localhost:8081/ws-connect?satoken=2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5 */ public static void main(String[] args) { SpringApplication.run(SaTokenWebSocketSpringApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java ================================================ package com.pj.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java ================================================ package com.pj.ws; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; /** * 处理 WebSocket 连接 * * @author click33 * @since 2022-2-11 */ public class MyWebSocketHandler extends TextWebSocketHandler { /** * 固定前缀 */ private static final String USER_ID = "user_id_"; /** * 存放Session集合,方便推送消息 */ private static ConcurrentHashMap webSocketSessionMaps = new ConcurrentHashMap<>(); // 监听:连接开启 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // put到集合,方便后续操作 String userId = session.getAttributes().get("userId").toString(); webSocketSessionMaps.put(USER_ID + userId, session); // 给个提示 String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; System.out.println(tips); sendMessage(session, tips); } // 监听:连接关闭 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // 从集合移除 String userId = session.getAttributes().get("userId").toString(); webSocketSessionMaps.remove(USER_ID + userId); // 给个提示 String tips = "Web-Socket 连接关闭,sid=" + session.getId() + ",userId=" + userId; System.out.println(tips); } // 收到消息 @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { System.out.println("sid为:" + session.getId() + ",发来:" + message); } // ----------- // 向指定客户端推送消息 public static void sendMessage(WebSocketSession session, String message) { try { System.out.println("向sid为:" + session.getId() + ",发送:" + message); session.sendMessage(new TextMessage(message)); } catch (IOException e) { throw new RuntimeException(e); } } // 向指定用户推送消息 public static void sendMessage(long userId, String message) { WebSocketSession session = webSocketSessionMaps.get(USER_ID + userId); if(session != null) { sendMessage(session, message); } } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java ================================================ package com.pj.ws; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; /** * WebSocket 相关配置 * * @author click33 * @since 2022-2-11 */ @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { // 注册 WebSocket 处理器 @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry // WebSocket 连接处理器 .addHandler(new MyWebSocketHandler(), "/ws-connect") // WebSocket 拦截器 .addInterceptors(new WebSocketInterceptor()) // 允许跨域 .setAllowedOrigins("*"); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java ================================================ package com.pj.ws; import java.util.Map; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import cn.dev33.satoken.stp.StpUtil; /** * WebSocket 握手的前置拦截器 * * @author click33 * @since 2022-2-11 */ public class WebSocketInterceptor implements HandshakeInterceptor { // 握手之前触发 (return true 才会握手成功 ) @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map attr) { System.out.println("---- 握手之前触发 " + StpUtil.getTokenValue()); // 未登录情况下拒绝握手 if(StpUtil.isLogin() == false) { System.out.println("---- 未授权客户端,连接失败"); return false; } // 标记 userId,握手成功 attr.put("userId", StpUtil.getLoginIdAsLong()); return true; } // 握手之后触发 @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("---- 握手之后触发 "); } } ================================================ FILE: sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-dependencies/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-bom ${revision} ../sa-token-bom/pom.xml pom sa-token-dependencies sa-token-dependencies Sa-Token Dependencies 2.13.4.1 2.11.2 3.1.0 3.1.0 6.0.0 3.0.9.RELEASE 2.3.34 3.2.1 1.8.2 3.2.139 4.0.20 4.9.17 3.14.4 1.1.5-java8 2.5.0 2.7.21 2.10.1.RELEASE 5.8.36 0.12.6 1.2.83 2.0.15 3.45.0 5.8.36 3.2.0 1.6.4 4.1.0 3.2.7 0.7.0 javax.servlet javax.servlet-api ${servlet-api.version} jakarta.servlet jakarta.servlet-api ${jakarta-servlet-api.version} com.fasterxml.jackson.core jackson-databind ${jackson-databind.version} tools.jackson.core jackson-databind ${jackson3-databind.version} org.noear solon ${solon.version} org.noear snack3 ${noear-snack3.version} org.noear snack4 ${noear-snack4.version} io.jboot jboot ${jboot.version} com.jfinal jfinal ${jfinal.version} com.kfyty loveqq-core ${loveqq.version} com.kfyty loveqq-mvc-core ${loveqq.version} com.kfyty loveqq-boot-starter-redisson ${loveqq.version} org.redisson redisson ${redisson.version} org.redisson redisson-spring-boot-starter ${redisson.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 ${jackson-datatype-jsr310.version} com.alibaba fastjson ${fastjson.version} com.alibaba.fastjson2 fastjson2 ${fastjson2.version} org.noear redisx ${noear-redisx.version} org.noear solon-test ${solon.version} org.apache.commons commons-pool2 ${commons-pool2.version} org.thymeleaf thymeleaf ${thymeleaf.version} org.freemarker freemarker ${freemarker.version} cn.hutool hutool-jwt ${hutool-jwt.version} org.apache.dubbo dubbo ${dubbo.version} net.devh grpc-spring-boot-starter ${grpc-spring-boot-starter.version} io.jsonwebtoken jjwt ${jjwt.version} cn.hutool hutool-cache ${hutool-cache.version} com.github.ben-manes.caffeine caffeine ${caffeine.version} com.dtflys.forest forest-core ${forest.version} cn.zhxu okhttps ${okhttps.version} ================================================ FILE: sa-token-doc/README.md ================================================

logo

Sa-Token v1.45.0

✨ 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!

--- ## 📝 前言:️️ 为了保证新同学不迷路,请允许我唠叨一下:无论您从何处看到本篇文章,最新开发文档永远在:[https://sa-token.cc](https://sa-token.cc), 建议收藏在浏览器书签,如果您已经身处本网站下,则请忽略此条说明。 回望 2020 年初,我为 Sa-Token 提交第一行代码之际,彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架,更是一整套清晰、自洽的权限架构设计思想。 因此,这几年间我将大量时间倾注在 Sa-Token 的文档编写,几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨,以求做到最清晰、干练、易懂的表述。用心阅读文档,你学习到的将不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。 ## 🛠️ Sa-Token 介绍 **Sa-Token** 是一个轻量级 Java 权限认证框架,主要解决:**登录认证**、**权限认证**、**单点登录**、**OAuth2.0**、**分布式Session会话**、**微服务网关鉴权** 等一系列权限相关问题。 Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要: ``` java // 会话登录,参数填登录人的账号id StpUtil.login(10001); ``` 无需实现任何接口,无需创建任何配置文件,只需要这一句静态代码的调用,便可以完成会话登录认证。 如果一个接口需要登录后才能访问,我们只需调用以下代码: ``` java // 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常 StpUtil.checkLogin(); ``` 在 Sa-Token 中,大多数功能都可以一行代码解决: 踢人下线: ``` java // 将账号id为 10077 的会话踢下线 StpUtil.kickout(10077); ``` 权限认证: ``` java // 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法 @SaCheckPermission("user:add") public String insert(SysUser user) { // ... return "用户增加"; } ``` 路由拦截鉴权: ``` java // 根据路由划分模块,不同模块不同鉴权 registry.addInterceptor(new SaInterceptor(handler -> { SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); // 更多模块... })).addPathPatterns("/**"); ``` 当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅! ## 🎉 Sa-Token 功能一览 Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。 - **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。 - **权限认证** —— 权限认证、角色认证、会话二级认证。 - **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。 - **注解式鉴权** —— 优雅的将鉴权与业务代码分离。 - **路由拦截式鉴权** —— 根据路由拦截鉴权,可适配 restful 模式。 - **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。 - **持久层扩展** —— 可集成 Redis,重启数据不丢失。 - **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。 - **Token风格定制** —— 内置六种 Token 风格,还可:自定义 Token 生成策略。 - **记住我模式** —— 适配 [记住我] 模式,重启浏览器免验证。 - **二级认证** —— 在已登录的基础上再次认证,保证安全性。 - **模拟他人账号** —— 实时操作任意用户状态数据。 - **临时身份切换** —— 将会话身份临时切换为其它账号。 - **同端互斥登录** —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。 - **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。 - **密码加密** —— 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。 - **会话查询** —— 提供方便灵活的会话查询接口。 - **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。 - **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。 - **全局过滤器** —— 方便的处理跨域,全局设置安全响应头等操作。 - **多账号体系认证** —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表) - **单点登录** —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。 - **单点注销** —— 任意子系统内发起注销,即可全端下线。 - **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务,支持openid模式 。 - **分布式会话** —— 提供共享数据中心分布式会话方案。 - **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。 - **RPC调用鉴权** —— 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔 - **临时Token认证** —— 解决短时间的 Token 授权问题。 - **独立Redis** —— 将权限缓存与业务缓存分离。 - **Quick快速登录认证** —— 为项目零代码注入一个登录页面。 - **标签方言** —— 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。 - **jwt集成** —— 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。 - **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。 - **参数签名** —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。 - **自动续签** —— 提供两种Token过期策略,灵活搭配使用,还可自动续签。 - **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。 - **最新技术栈** —— 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。 功能结构图: ## 📖❓ 疑问解答 **1、Sa-Token 功能全不全?** 七年磨一剑:五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证,我们提供的是一站式解决方案。 **2、Sa-Token 好不好学?** 中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。 **3、Sa-Token 用的人多不多?** 截止统计日 (2026-1-25) 起,Sa-Token 在: - Gitee 关注量达到 48627 Star,位列平台所有推荐项目排行榜第一名。 - GitHub 关注量达到 18523 Star,是主要竞争框架 Spring Security 的 1.97 倍,Apache Shiro 的 4.19 倍。 - 25+ 微信粉丝群 (500人),8+ QQ粉丝群 (1000人 or 2000人) ,在线文档访问量月PV 20万+。 这是众多开发者用脚投票的数据,相信这些数据比任何言语都能证明 Sa-Token 的热度。 **4、Sa-Token 有哪些权威认证?** 曾获荣誉包括但不限于:Gitee GVP 最有价值开源项目、GitCode G-Star 优质开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目、OSCHINA 2022 年度最火热中国开源项目社区之一、开放原子基金会2023快速成长开源项目、 Dromara 组织顶尖项目(之一)、可信开源社区共同体预备成员、所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。 Gitee High Star 计划项目(5000+star)。Gitee 2025年度开源项目 Web应用开发 Top 2。 **5、Sa-Token 收费吗?** Sa-Token 采用 Apache-2.0 开源协议,承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token,我们也不回避:[赞助链接](https://sa-token.cc/doc.html#/more/sa-token-donate)。 我们将定期同步赞助者名单到在线文档展示。(您需要注意的一点是:该赞助仅为友情赞助,不提供任何商业交换) **6、Sa-Token 是封装的 SpringSecurity 吗?是套壳 ApacheShiro 吗?** 不是。Sa-Token 不是一个后台模板,也不是针对 xx 框架的二次封装套壳,而是从 0 开始的纯血自研框架,核心包零依赖,完全自主可控的架构内核 + 众多主流框架的集成适配。 ## 📈 开源仓库 Star 趋势

[![github-chart](https://starchart.cc/dromara/sa-token.svg 'GitHub')](https://starchart.cc/dromara/sa-token) 如果 Sa-Token 帮助到了您,希望您可以为其点上一个 `star`: [码云](https://gitee.com/dromara/sa-token)、 [AtomGit](https://atomgit.com/dromara/sa-token)、 [GitHub](https://github.com/dromara/sa-token) ## 🚀 使用 Sa-Token 的开源项目 参考:[Sa-Token 生态](/more/link) ## 💬 交流群 加入 Sa-Token 框架 QQ、微信讨论群:[点击加入](/more/join-group.md) ================================================ FILE: sa-token-doc/_sidebar.md ================================================ - **开始** - [框架介绍](/) - [在 SpringBoot 环境集成](/start/example) - [在 WebFlux 环境集成](/start/webflux-example) - [在 Solon 环境集成](/start/solon-example) - [其它环境集成示例](/start/download) - **基础** - [登录认证](/use/login-auth) - [权限认证](/use/jur-auth) - [踢人下线](/use/kick) - [注解鉴权](/use/at-check) - [路由拦截鉴权](/use/route-check) - [Session会话](/use/session) - [框架配置](/use/config) - **深入** - [集成 Redis](/up/integ-redis) - [前后端分离](/up/not-cookie) - [自定义 Token 风格](/up/token-style) - [Token 提交前缀](/up/token-prefix) - [同端互斥登录](/up/mutex-login) - [记住我模式](/up/remember-me) - [登录参数 & 注销参数](/up/login-parameter) - [二级认证](/up/safe-auth) - [模拟他人 & 身份切换](/up/mock-person) - [账号封禁](/up/disable) - [密码加密](/up/password-secure) - [会话查询](/up/search-session) - [Http Basic/Digest 认证](/up/basic-auth) - [全局侦听器](/up/global-listener) - [全局过滤器](/up/global-filter) - [多账号认证](/up/many-account) - **单点登录** - [单点登录简述](/sso/readme) - [搭建统一认证中心:SSO-Server](/sso/sso-server) - [SSO-Server 认证中心开放 API 接口](/sso/sso-apidoc) - [SSO模式一 共享Cookie同步会话](/sso/sso-type1) - [SSO模式二 URL重定向传播会话](/sso/sso-type2) - [SSO模式三 Http请求获取会话](/sso/sso-type3) - [配置域名校验](/sso/sso-check-domain) - [定制化登录页面](/sso/sso-custom-login) - [自定义API路由](/sso/sso-custom-api) - [平台中心跳转模式](/sso/sso-home-jump) - [匿名 client 接入](/sso/anon-client) - [单点注销](/sso/signout) - [前后端分离下的整合方案](/sso/sso-h5) - [消息推送机制](/sso/message-push) - [用户数据同步 / 迁移](/sso/user-data-sync) - [NoSdk、ReSdk 模式与非 java 项目](/sso/sso-nosdk) - [SSO 代码 API 参考](/sso/sso-dev) - [常见问题总结](/sso/sso-questions) - [Sa-Pro:单点登录商业版](/pro/st_sso) - **OAuth2.0** - [OAuth2.0简述](/oauth2/readme) - [OAuth2-Server搭建](/oauth2/oauth2-server) - [OAuth2-Server端开放 API 接口](/oauth2/oauth2-apidoc) - [自定义数据加载器](/oauth2/oauth2-data-loader) - [配置 client 域名校验 ](/oauth2/oauth2-check-domain) - [自定义 Scope 权限及处理器](/oauth2/oauth2-custom-scope) - [为 Scope 划分等级](/oauth2/oauth2-scope-level) - [自定义 grant_type](/oauth2/oauth2-custom-grant_type) - [定制化登录页面与授权页面](/oauth2/oauth2-custom-login) - [自定义 API 路由 ](/oauth2/oauth2-custom-api) - [OAuth2-Server端前后台分离](/oauth2/oauth2-h5) - [OpenId 与 UnionId](/oauth2/oauth2-openid) - [开启 OIDC 协议](/oauth2/oauth2-oidc) - [使用注解校验 Access-Token](/oauth2/oauth2-at-check) - [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking) - [OAuth2 代码 API 参考](/oauth2/oauth2-dev) - [常见问题总结](/oauth2/oauth2-questions) - [Sa-Max:统一认证商业版](/pro/st_oauth2) - **微服务** - [分布式Session会话](/micro/dcs-session) - [网关统一鉴权](/micro/gateway-auth) - [内部服务外网隔离](/micro/same-token) - [依赖引入说明](/micro/import-intro) - **插件** - [AOP注解鉴权](/plugin/aop-at) - [临时 Token 认证](/plugin/temp-token) - [Quick-Login快速登录插件](/plugin/quick-login) - [Alone独立Redis插件](/plugin/alone-redis) - [缓存层扩展](/plugin/dao-extend) - [JSON 序列化扩展](/plugin/json-extend) - [序列化插件扩展包](/plugin/custom-serializer) - [和 Thymeleaf 集成](/plugin/thymeleaf-extend) - [和 Freemarker 集成](/plugin/freemarker-extend) - [注解鉴权 SpEL 表达式](/plugin/spel-at) - [和 jwt 集成](/plugin/jwt-extend) - [和 Dubbo 集成](/plugin/dubbo-extend) - [和 gRPC 集成](/plugin/grpc-extend) - [API 接口参数签名](/plugin/api-sign) - [API Key 接口调用秘钥](/plugin/api-key) - [Sa-Token 插件开发指南](/fun/plugin-dev) - [自定义 SaTokenContext 指南](/fun/sa-token-context) - **API手册** - [StpUtil-鉴权工具类](/api/stp-util) - [SaSession-会话对象](/api/sa-session) - [SaTokenDao-数据持久接口](/api/sa-token-dao) - [SaStrategy-全局策略](/api/sa-strategy) - [全局类、方法](/more/common-action) - **框架设计** - [仓库目录](/arch/dir-intro) - [数据结构](/arch/data-structure) - **其它** - [更新日志](/more/update-log) - [框架生态](/more/link) - [框架博客](/more/blog) - [推荐公众号](/more/tj-gzh) - [加入讨论群](/more/join-group) - [Sa-Token 内容合作群](/more/content-cooperation) - [赞助 Sa-Token](/more/sa-token-donate) - [需求提交](/more/demand-commit) - [问卷调查](/more/wenjuan) - **附录** - [常见问题排查](/more/common-questions) - [框架名词解释](/more/noun-intro) - [Sa-Token功能结构图](/fun/auth-flow) - [全局 Log 输出](/fun/log) - [异步 & Mock 上下文](/fun/async--mock) - [未登录场景值详解](/fun/not-login-scene) - [Token有效期详解](/fun/token-timeout) - [Session模型详解](/fun/session-model) - [数据读写三大作用域](/fun/three-scope) - [TokenInfo参数详解](/fun/token-info) - [异常细分状态码](/fun/exception-code) - [自定义注解](/fun/custom-annotations) - [防火墙](/fun/firewall) - [参考:把权限放在缓存里](/fun/jur-cache) - [参考:把路由拦截鉴权动态化](/fun/dynamic-router-check) - [解决反向代理 uri 丢失的问题](/fun/curr-domain) - [解决跨域问题](/fun/cors-filter) - [技术选型:SSO 与 OAuth2 对比](/fun/sso-vs-oauth2) - [集成 MongoDB 参考一](/up/integ-spring-mongod-1) - [集成 MongoDB 参考二](/up/integ-spring-mongod-2) - [从 Shiro、SpringSecurity、JWT 迁移](/fun/auth-framework-function-test) - [issue 提问模板](/fun/issue-template) - [为Sa-Token贡献代码](/fun/git-pr) - [Sa-Token开源大事记](/fun/timeline) - [团队成员](/fun/team) - [Sa-Token框架掌握度--在线考试](/fun/sa-token-test)






----- 到底线了 -----

================================================ FILE: sa-token-doc/api/sa-session.md ================================================ # SaSession-会话对象 SaSession-会话对象,专业数据缓存组件。 --- ### 1、常量 ``` java SaSession.USER= "USER"; // 在 Session 上存储用户对象时建议使用的key SaSession.ROLE_LIST = "ROLE_LIST"; // 在 Session 上存储角色时建议使用的key SaSession.PERMISSION_LIST = "PERMISSION_LIST"; // 在 Session 上存储权限时建议使用的key ``` ### 2、构建相关 ``` java session.getId(); // 获取此 Session 的 id session.setId(id); // 写入此 Session 的 id session.getCreateTime(); // 返回当前会话创建时间(时间戳) session.setCreateTime(createTime); // 写入此 Session 的创建时间(时间戳) ``` ### 3、SaTerminalInfo 相关 ``` java session.setTerminalList(terminalList); // 写入登录终端信息列表 session.getTerminalList(); // 获取登录终端信息列表 session.terminalListCopy(); // 获取 登录终端信息列表 (拷贝副本) session.getTerminalListByDeviceType(deviceType); // 获取 登录终端信息列表 (拷贝副本),根据 deviceType 筛选 session.getTerminal(tokenValue); // 查找一个终端信息,根据 tokenValue session.addTerminal(terminal); // 添加一个终端信息 session.removeTerminal(tokenValue); // 移除一个终端信息 session.maxTerminalIndex(); // 获取最大的终端索引值,如无返0 session.isTrustDeviceId("xxxxxxxxxxxxxxxxxxxxxxxx"); // 判断指定设备 id 是否为可信任设备 ``` ### 4、一些操作 ``` java session.update(); // 更新Session(从持久库更新刷新一下) session.logout(); // 注销Session (从持久库删除) session.logoutByTerminalCountToZero(); // 当 Session 上的 SaTerminalInfo 数量为零时,注销会话 session.getTimeout(); // 获取此Session的剩余存活时间 (单位: 秒) session.updateTimeout(timeout); // 修改此Session的剩余存活时间 session.updateMinTimeout(minTimeout); // 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改) session.updateMaxTimeout(maxTimeout); // 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改) session.trans(value); // value为 -1 时返回 Long.MAX_VALUE,否则原样返回 ``` ### 5、存取值 ``` java session.get(key); // 取值 session.get(key, defaultValue); // 取值 (指定默认值) session.get(key, () -> {}); // 取值 (如果值为 null,则执行 fun 函数获取值,并把函数返回值写入缓存) session.getString(key); // 取值 (转String类型) session.getInt(key); // 取值 (转int类型) session.getLong(key); // 取值 (转long类型) session.getDouble(key); // 取值 (转double类型) session.getFloat(key); // 取值 (转float类型) session.getModel(key, clazz); // 取值 (指定转换类型) session.getModel(key, clazz, defaultValue); // 取值 (指定转换类型, 并指定值为Null时返回的默认值) session.has(key); // 是否含有某个key session.set(key, value); // 写值 session.setByNull(key, value); // 写值 (只有在此 key 原本无值的情况下才会写入) session.delete(key); // 删值 session.keys(); // 返回当前Session的所有key session.clear(); // 清空所有值 session.getDataMap(); // 获取数据挂载集合(如果更新map里的值,请调用session.update()方法避免产生脏数据 ) session.refreshDataMap(dataMap); // 写入数据集合 (不改变底层对象,只将此dataMap所有数据进行替换) ``` ================================================ FILE: sa-token-doc/api/sa-strategy.md ================================================ # SaStrategy-全局策略 SaStrategy-全局策略,核心逻辑的代理封装。 --- ### 核心策略 ``` java /** * 创建 Token 的策略 *

参数 [账号id, 账号类型] */ public BiFunction createToken = (loginId, loginType) -> { // 默认,还是uuid return "xxxxx-xxxxx-xxxxx-xxxxx"; }; /** * 创建 Session 的策略 *

参数 [SessionId] */ public Function createSession = (sessionId) -> { return new SaSession(sessionId); }; /** * 反序列化 SaSession 时默认指定的类型 */ public Class sessionClassType = SaSession.class; /** * 判断:集合中是否包含指定元素(模糊匹配) *

参数 [集合, 元素] */ public BiFunction, String, Boolean> hasElement = (list, element) -> { return false; }; /** * 生成唯一式 token 的算法 *

参数:元素名称, 最大尝试次数, 创建 token 函数, 检查 token 函数

*/ public SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> { // ... return "xxxxxx"; }; /** * 是否自动续期,每次续期前都会执行,可以加入动态判断逻辑 *

参数 当前 stpLogic 实例对象 *

返回 true 自动续期 false 不自动续期 */ public Function autoRenew = (stpLogic) -> { return stpLogic.getConfigOrGlobal().getAutoRenew(); }; /** * 创建 StpLogic 的算法 *

参数:账号体系标识

*

返回:创建好的 StpLogic 对象

*/ public SaCreateStpLogicFunction createStpLogic = (loginType) -> { return new StpLogic(loginType); }; /** * 路由匹配策略 *

参数:pattern, path

*

返回:是否匹配

*/ public SaRouteMatchFunction routeMatcher = (pattern, path) -> { return true; }; /** * CORS 策略处理函数 *

参数:请求包装对象, 响应包装对象, 数据读写对象

*/ public SaCorsHandleFunction corsHandle = (req, res, sto) -> { }; ``` ### 注解操作相关策略 ``` java /** * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) *

参数:Method句柄

*

返回:无

*/ public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { // ... }; /** * 对一个 [Element] 对象进行注解校验 (注解鉴权内部实现) *

参数:element元素

*

返回:无

*/ @SuppressWarnings("unchecked") public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> { // ... }; /** * 从元素上获取注解 *

参数:element元素,要获取的注解类型

*

返回:注解对象

*/ public SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{ return element.getAnnotation(annotationClass); }; /** * 判断一个 Method 或其所属 Class 是否包含指定注解 *

参数:Method、注解

*

返回:是否包含

*/ public SaIsAnnotationPresentFunction isAnnotationPresent = (method, annotationClass) -> { // ... return false; }; /** * SaCheckELRootMap 扩展函数 *

参数:SaCheckELRootMap 对象

*/ public SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> { // 默认不做任何处理 }; ``` ### 防火墙相关策略 ``` java /** * 防火墙校验函数 *

参数:请求对象、响应对象、预留扩展参数

*/ public SaFirewallCheckFunction check = (req, res, extArg) -> { // ... }; /** * 自定义当请求 path 校验不通过时地处理方案 *

参数:防火墙校验异常、请求对象、响应对象、预留扩展参数

*/ SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> { // 自定义处理逻辑 ... }; ``` 参考:[防火墙](/fun/firewall) ================================================ FILE: sa-token-doc/api/sa-token-dao.md ================================================ # SaTokenDao-数据持久接口 SaTokenDao 是数据持久层接口,负责所有会话数据的底层写入和读取。 --- ### 1、常量 ``` java SaTokenDao.NEVER_EXPIRE = -1; // 常量,表示一个key永不过期 (在一个key被标注为永远不过期时返回此值) SaTokenDao.NOT_VALUE_EXPIRE = -2; // 常量,表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值) ``` ### 2、字符串读写 ``` java dao.get(key); // 获取Value,如无返空 dao.set(key, value, timeout); // 写入Value,并设定存活时间 (单位: 秒) dao.update(key, value); // 更新Value (过期时间不变) dao.delete(key); // 删除Value dao.getTimeout(key); // 获取Value的剩余存活时间 (单位: 秒) dao.updateTimeout(key, timeout); // 修改Value的剩余存活时间 (单位: 秒) ``` ### 3、对象读写 ``` java dao.getObject(key); // 获取Object,如无返空 dao.setObject(key, value, timeout); // 写入Object,并设定存活时间 (单位: 秒) dao.updateObject(key, value); // 更新Object (过期时间不变) dao.deleteObject(key); // 删除Object dao.getObjectTimeout(key); // 获取Object的剩余存活时间 (单位: 秒) dao.updateObjectTimeout(key, timeout); // 修改Object的剩余存活时间 (单位: 秒) ``` ### 4、Session读写 ``` java dao.getSession(sessionId); // 获取Session,如无返空 dao.setSession(session, timeout); // 写入Session,并设定存活时间 (单位: 秒) dao.setSession(session); // 更新Session (过期时间不变) dao.deleteSession(sessionId); // 删除Session dao.getSessionTimeout(sessionId); // 获取Session的剩余存活时间 (单位: 秒) dao.updateSessionTimeout(sessionId, timeout); // 修改Session的剩余存活时间 (单位: 秒) ``` ### 5、会话管理 ``` java dao.searchData(prefix, keyword, start, size, sortType); // 搜索数据 ``` ================================================ FILE: sa-token-doc/api/stp-util.md ================================================ # StpUtil - 鉴权工具类 StpUtil 是 Sa-Token 整体功能的核心,大多数功能均由此工具类提供。 --- ### 1、常规操作 ``` java StpUtil.getStpLogic(); // 获取底层 StpLogic 对象。 StpUtil.setStpLogic(newStpLogic); // 安全的重置底层 StpLogic 引用。 StpUtil.getLoginType(); // 获取账号类型 (例如:login、user、admin、teacher、student等等)。 StpUtil.getTokenName(); // 获取 Token 的名称 StpUtil.getTokenValue(); // 获取本次请求前端提交的 Token。 StpUtil.getTokenValueNotCut(); // 获取本次请求前端提交的 Token (不裁剪前缀) 。 StpUtil.setTokenValue(tokenValue); // 在当前会话中写入 Token 值。 StpUtil.setTokenValue(tokenValue, timeout); // 在当前会话中写入 Token 值,并指定 Cookie 有效期。 StpUtil.setTokenValue(tokenValue, loginParameter); // 在当前会话中写入 Token 值,并指定登录参数。 StpUtil.setTokenValueToStorage(tokenValue); // 将 Token 写入当前请求的 Storage 存储器。 StpUtil.getTokenInfo(); // 获取当前 Token 的详细参数。 ``` ### 2、登录相关 ``` java StpUtil.login(10001); // 会话登录 StpUtil.login(10001, "APP"); // 会话登录,并指定设备类型 StpUtil.login(10001, true); // 会话登录,并指定是否 [记住我] StpUtil.login(10001, 86400); // 会话登录,并指定此次 token 有效期(单位:秒) StpUtil.login(10001, loginParameter); // 会话登录,并指定所有登录参数Model StpUtil.createLoginSession(10001); // 创建指定账号id的登录会话,此方法不会将 Token 注入到上下文 StpUtil.createLoginSession(10001, loginParameter); // 创建指定账号id的登录会话,此方法不会将 Token 注入到上下文 StpUtil.getOrCreateLoginSession(10001); // 获取指定账号的登录会话,若不存在则创建并返回 ``` 更多 SaLoginParameter 登录参数的用法与示例,请参考 [登录参数示例章节](/up/login-parameter)。 ### 3、注销相关 ``` java StpUtil.logout(); // 会话注销 StpUtil.logout(logoutParameter); // 会话注销,根据注销参数 StpUtil.logout(10001); // 会话注销,根据账号id StpUtil.logout(10001, "PC"); // 会话注销,根据账号id 和 设备类型 StpUtil.logout(10001, logoutParameter); // 会话注销,根据账号id 和 注销参数 StpUtil.logoutByTokenValue(token); // 指定 Token 强制注销 StpUtil.logoutByTokenValue(token, logoutParameter); // 指定 Token 强制注销,带注销参数 StpUtil.kickout(10001); // 踢人下线,根据账号id StpUtil.kickout(10001, "PC"); // 踢人下线,根据账号id 和 设备类型 StpUtil.kickout(10001, logoutParameter); // 踢人下线,根据账号id 和 注销参数 StpUtil.kickoutByTokenValue(token); // 踢人下线,根据token StpUtil.kickoutByTokenValue(token, logoutParameter); // 踢人下线,根据token,带注销参数 StpUtil.replacedByTokenValue(token); // 顶人下线,根据token StpUtil.replacedByTokenValue(token, logoutParameter); // 顶人下线,根据token,带注销参数 StpUtil.replaced(10001); // 顶人下线,根据账号id StpUtil.replaced(10001, "PC"); // 顶人下线,根据账号id 和 设备类型 StpUtil.replaced(10001, logoutParameter); // 顶人下线,根据账号id 和 注销参数 ``` 更多 SaLogoutParameter 注销参数的用法与示例,请参考 [注销参数示例章节](/up/login-parameter?id=_2、注销参数)。 ### 4、会话查询 ``` java StpUtil.isLogin(); // 当前会话是否已经登录 StpUtil.isLogin(10001); // 判断指定账号是否已登录 StpUtil.checkLogin(); // 检验当前会话是否已经登录,如未登录,则抛出异常 StpUtil.getLoginId(); // 获取当前会话账号id, 如果未登录,则抛出异常 StpUtil.getLoginId(defaultValue); // 获取当前会话账号id, 如果未登录,则返回默认值 StpUtil.getLoginIdDefaultNull(); // 获取当前会话账号id, 如果未登录,则返回null StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转换为String类型 StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转换为int类型 StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转换为long类型 StpUtil.getLoginIdByToken(token); // 获取指定Token对应的账号id,如果未登录,则返回 null StpUtil.getLoginIdByTokenNotThinkFreeze(token); // 获取指定Token对应的账号id(不考虑冻结状态),如果未登录,则返回 null StpUtil.getExtra(key); // 获取当前 Token 的扩展信息(此函数只在jwt模式下生效) StpUtil.getExtra(token, key); // 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) ``` ### 5、Session 相关 ``` java // Account-Session 相关 StpUtil.getSession(); // 获取当前会话的Session,如果Session尚未创建,则新建并返回 StpUtil.getSession(true); // 获取当前会话的Session, 如果Session尚未创建,isCreate=是否新建并返回 StpUtil.getSessionByLoginId(10001); // 获取指定账号id的Session,如果Session尚未创建,则新建并返回 StpUtil.getSessionByLoginId(10001, true); // 获取指定账号id的Session, 如果Session尚未创建,isCreate=是否新建并返回 // Token-Session 相关 StpUtil.getTokenSession(); // 获取当前会话的Session,如果Session尚未创建,则新建并返回 StpUtil.getTokenSessionByToken(token); // 获取指定Token-Session,如果Session尚未创建,则新建并返回 StpUtil.getAnonTokenSession(); // 获取当前匿名 Token-Session (可在未登录情况下使用的Token-Session) // 其它 StpUtil.getSessionBySessionId("xxxx-xxxx-xxxx"); // 获取指定 sessionId 的 Session,若不存在则返回 null StpUtil.isTrustDeviceId(123456, "xxxxxxxxxxxxxxxxxxxxxxxx"); // 判断对于指定 loginId 来讲,指定设备 id 是否为可信任设备 ``` ### 6、Token有效期相关 ``` java // Token 最低活跃频率 StpUtil.getTokenActiveTimeout(); // 获取当前 token 距离被冻结还剩多少时间 (单位: 秒) StpUtil.getTokenLastActiveTime(); // 获取当前 token 最后活跃时间 StpUtil.checkActiveTimeout(); // 检查当前token 是否已经被冻结,如果是则抛出异常 StpUtil.updateLastActiveToNow(); // 续签当前token:(将 [最后操作时间] 更新为当前时间戳) // Token 有效期 StpUtil.getTokenTimeout(); // 获取当前登录者的 token 剩余有效时间 (单位: 秒) StpUtil.getTokenTimeout(token); // 获取指定 token 的剩余有效时间 (单位: 秒) StpUtil.getSessionTimeout(); // 获取当前登录者的 Account-Session 剩余有效时间 (单位: 秒) StpUtil.getTokenSessionTimeout(); // 获取当前 Token-Session 剩余有效时间 (单位: 秒) StpUtil.renewTimeout(timeout); // 对当前 Token 的 timeout 值进行续期 StpUtil.renewTimeout(token, timeout); // 对指定 Token 的 timeout 值进行续期 ``` ### 7、角色认证 ``` java StpUtil.getRoleList(); // 获取:当前账号的角色集合 StpUtil.getRoleList(10001); // 获取:指定账号的角色集合 StpUtil.hasRole(role); // 判断:当前账号是否拥有指定角色, 返回true或false StpUtil.hasRole(loginId, role); // 判断:指定账号是否含有指定角色标识, 返回true或false StpUtil.hasRoleAnd(...roleArray); // 判断:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] StpUtil.hasRoleOr(...roleArray); // 判断:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] StpUtil.checkRole(role); // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException StpUtil.checkRoleAnd(...roleArray); // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] StpUtil.checkRoleOr(...roleArray); // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] ``` ### 8、权限认证 ``` java StpUtil.getPermissionList(); // 获取:当前账号的权限集合 StpUtil.getPermissionList(10001); // 获取:指定账号的权限集合 StpUtil.hasPermission(permission); // 判断:当前账号是否拥有指定权限, 返回true或false StpUtil.hasPermission(loginId, permission); // 判断:指定账号是否含有指定权限标识, 返回true或false StpUtil.hasPermissionAnd(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过] StpUtil.hasPermissionOr(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可] StpUtil.checkPermission(permission); // 校验:当前账号是否含有指定权限标识, 如果验证未通过,则抛出异常: NotPermissionException StpUtil.checkPermissionAnd(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过] StpUtil.checkPermissionOr(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可] ``` ### 9、id 反查 Token ``` java StpUtil.getTokenValueByLoginId(10001); // 获取指定账号id的tokenValue StpUtil.getTokenValueByLoginId(10001, "PC"); // 获取指定账号id指定设备类型端的tokenValue StpUtil.getTokenValueListByLoginId(10001); // 获取指定账号id的tokenValue集合 StpUtil.getTokenValueListByLoginId(10001, "APP"); // 获取指定账号id指定设备类型端的tokenValue 集合 StpUtil.getTerminalListByLoginId(10001); // 获取指定账号 id 已登录设备信息集合 StpUtil.getTerminalListByLoginId(10001, "PC"); // 获取指定账号 id 指定设备类型端的已登录设备信息集合 StpUtil.forEachTerminalList(10001, (terminal) -> {}); // 遍历指定账号的已登录设备列表 StpUtil.getTerminalInfo(); // 获取当前会话的终端信息 StpUtil.getTerminalInfoByToken(token); // 获取指定 token 的终端信息 StpUtil.getLoginDeviceId(); // 返回当前会话的登录设备 id StpUtil.getLoginDeviceIdByToken(token); // 返回指定 token 的登录设备 id StpUtil.getLoginDeviceType(); // 返回当前会话的登录设备类型 StpUtil.getLoginDeviceTypeByToken(token); // 返回指定 token 的登录设备类型 ``` ### 10、会话管理 ``` java StpUtil.searchTokenValue(keyword, start, size, sortType); // 根据条件查询Token StpUtil.searchSessionId(keyword, start, size, sortType); // 根据条件查询SessionId StpUtil.searchTokenSessionId(keyword, start, size, sortType); // 根据条件查询Token专属Session的Id ``` 详细可参考:[会话治理](/up/search-session) ### 11、账号封禁 ``` java StpUtil.disable(10001, 1200); // 封禁:指定账号 指定时间(单位s) StpUtil.isDisable(10001); // 判断:指定账号是否已被封禁 (true=已被封禁, false=未被封禁) StpUtil.checkDisable(10001); // 校验:指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` StpUtil.getDisableTime(10001); // 获取:指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) StpUtil.untieDisable(loginId); // 解封:指定账号 ``` ### 12、分类封禁 (version >= 1.31.0) ``` java StpUtil.disable(10001, "<业务标识>", 86400); // 封禁:指定账号的指定服务 指定时间(单位s) StpUtil.isDisable(10001, "<业务标识>"); // 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) StpUtil.checkDisable(10001, "<业务标识>"); // 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`(支持传入多个业务标识) StpUtil.getDisableTime(10001, "<业务标识>"); // 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) StpUtil.untieDisable(loginId, "<业务标识>"); // 解封:指定账号的指定服务(支持传入多个业务标识) ``` ### 13、阶梯封禁 (version >= 1.31.0) ``` java StpUtil.disableLevel(10001, "comment", 3, 10000); // 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间 StpUtil.disableLevel(10001, 3, 10000); // 阶梯封禁(无业务标识,使用默认服务),参数:封禁账号、封禁级别、封禁时间 StpUtil.getDisableLevel(10001, "comment"); // 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2) StpUtil.getDisableLevel(10001); // 获取:指定账号的封禁等级(无业务标识,未封禁返回 -2) StpUtil.isDisableLevel(10001, "comment", 3); // 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false StpUtil.isDisableLevel(10001, 3); // 判断:指定账号是否被封禁到指定级别(无业务标识) StpUtil.checkDisableLevel(10001, "comment", 2); // 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常 StpUtil.checkDisableLevel(10001, 2); // 校验:指定账号是否被封禁到指定级别(无业务标识) ``` ### 14、身份切换 ``` java StpUtil.switchTo(10044); // 临时切换身份为指定账号id StpUtil.endSwitch(); // 结束临时切换身份 StpUtil.isSwitch(); // 当前是否正处于[身份临时切换]中 StpUtil.switchTo(10044, () -> {}); // 在一个代码段里方法内,临时切换身份为指定账号id ``` ### 15、二级认证 ``` java StpUtil.openSafe(safeTime); // 在当前会话 开启二级认证 StpUtil.isSafe(); // 当前会话 是否处于二级认证时间内 StpUtil.checkSafe(); // 检查当前会话是否已通过二级认证,如未通过则抛出异常 StpUtil.getSafeTime(); // 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) StpUtil.closeSafe(); // 在当前会话 结束二级认证 ``` ### 16、带有业务标识的二级认证 ``` java StpUtil.openSafe("<业务标识>", safeTime); // 在当前会话 指定业务标识开启二级认证 StpUtil.isSafe("<业务标识>"); // 当前会话 指定业务标识是否处于二级认证时间内 StpUtil.isSafe(tokenValue, "<业务标识>"); // 判断指定 token 的指定业务是否处于二级认证时间内 StpUtil.checkSafe("<业务标识>"); // 检查当前会话,指定业务标识是否已通过二级认证,如未通过则抛出异常 StpUtil.getSafeTime("<业务标识>"); // 获取当前会话的指定业务标识二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) StpUtil.closeSafe("<业务标识>"); // 在当前会话 结束指定业务标识二级认证 ``` ================================================ FILE: sa-token-doc/arch/data-structure.md ================================================ # 数据结构 ## 1、登录会话 ### 1.1、token -> loginId 映射 ``` js // ttl = 此 token 的 timeout 有效期 {tokenName}:{loginType}:token:{tokenValue} --> {loginId} ```
详细 示例: ``` js satoken:login:token:47ab0105-2be1-400c-b517-82f81a0cfcf8 --> 10001 ``` 异常 value 格式 ``` js -1 未能从请求中读取到有效 token -2 已读取到 token,但是 token 无效 -3 已读取到 token,但是 token 已经过期 (详) -4 已读取到 token,但是 token 已被顶下线 -5 已读取到 token,但是 token 已被踢下线 -6 已读取到 token,但是 token 已被冻结 -7 未按照指定前缀提交 token ```
### 1.2、active-timeout ``` js // ttl = 对应 token 的 timeout 有效期值 {tokenName}:{loginType}:last-active:{tokenValue} --> {13位时间戳} ```
详细 示例: ``` js satoken:login:last-active:06d1f12b-614e-4c00-8d8e-c07fef5f4aa9 --> 1722334954193 ``` value 格式分两种: ``` 1722334954193 // 单值时:此 token 最后访问日期 1722334954193, 1200 // 双值时:此 token 最后访问日期,此 token 指定的动态 active-timeout 值 ``` 注意:判断一个 token 是否 active-timeout 过期,与 ttl 无关,而是利 value 值计算: ``` js 当前时间 - token 最后访问时间 > active-timeout (true=token 已冻结,false=token 未冻结 ) ```
### 1.3、SaSession ``` js {tokenName}:{loginType}:session:{loginId} --> {SaSession 对象} // Account-Session {tokenName}:{loginType}:token-session:{loginId} --> {SaSession 对象} // Token-Session {tokenName}:custom:session:{sessionId} --> {SaSession 对象} // Custom-Session ```
详细 key 示例 ``` js // Account-Session satoken:login:session:1000001 // Token-Session satoken:login:session:47ab0105-2be1-400c-b517-82f81a0cfcf8 // Custom-Session satoken:custom:session:role-1001 ``` value 格式 ``` js { "@class": "cn.dev33.satoken.dao.SaSessionForJacksonCustomized", // java calss 信息 "id": "satoken:login:session:10001", // sessionId "type": "Account-Session", // session类型:Account-Session / Token-Session / Custom-Session "loginType": "login", // 账号类型 "loginId": [ // 对应登录id 值(Account-Session才会有值) "java.lang.Long", 10001 ], "token": null, // 对应 token 值 (Token-Session才会有值) "createTime": 1722334954145, // 此 session 创建时间,13位时间戳 "dataMap": { // 此 session 挂载数据 "@class": "java.util.concurrent.ConcurrentHashMap", "name": "张三" // 此 session 挂载数据 详情 // 更多值 ... }, "terminalList": [ // 已登录终端信息列表(Account-Session才会有值) "java.util.Vector", [ { "@class": "cn.dev33.satoken.session.SaTerminalInfo", "index": 1, "tokenValue": "2551663f-bb98-47d7-9af3-e2e6a28dadce", // 客户端 token 值 "deviceType": "DEF", // 登录设备类型 "deviceId": "xxxxxxxxx", // 登录设备id "extraData": { // 扩展信息列表 (手动自定义值) "@class": "java.util.LinkedHashMap", "deviceSimpleTitle": "XiaoMi 15 Ultra", "loginAddress": "浙江省杭州市西湖区", "loginIp": "127.0.0.1", "loginTime": "2025-03-08 15:00:02" }, "createTime": 1741406340845 // 登录时间 } ] ] } ```
### 1.4、二级认证 ``` js {tokenName}:{loginType}:safe:{service}:{tokenValue} --> SAFE_AUTH_SAVE_VALUE ``` value 为常亮值:`SAFE_AUTH_SAVE_VALUE` ### 1.5、账号服务封禁 ``` js {tokenName}:{loginType}:disable:{service}:{loginId} --> {level} ``` value 为封禁等级,int类型 ### 1.6、其它 SaApplication 全局变量 ``` js {tokenName}:var:{变量名} ``` 本次请求新创建 token,在 SaStorage 存储 key ``` js JUST_CREATED_ --> {token} ``` 本次请求新创建 token,在 SaStorage 存储 key (无前缀方式) ``` js JUST_CREATED_NOT_PREFIX_ --> {token} ``` 临时身份切换,使用的key ``` js SWITCH_TO_SAVE_KEY_{loginType} --> {loginId} ``` ## 2、SSO 单点登录 ### 2.1、ticket -> loginId 映射 ``` js // ttl = 此 ticket 有效期,下同理 {tokenName}:ticket:{ticket} --> {loginId} ``` ### 2.2、ticket -> client 映射 ``` js {tokenName}:ticket-client:{ticket} --> {client} ``` ### 2.3、loginId -> ticket 映射(client + loginId 反查 ticket) ``` js {tokenName}:ticket-index:{client}:{loginId} --> {ticket} ``` ## 3、OAuth2 统一认证 ### 3.1、Code 授权码 ``` js {tokenName}:oauth2:code:{code} --> {CodeModel 对象} ```
详细 value 示例: ``` js { "@class": "cn.dev33.satoken.oauth2.model.CodeModel", // java class 信息 "code": "AbRVp2HrgyklE0BXYWszskGJWAGY7xhGu6Zaco4zJECzGYagCCFWj0jOlHza", // code值 "scope": "", // 所申请权限列表,多个用逗号隔开 "loginId": "10001", // 对应的loginId "redirectUri": "", // 重定向地址 } ```
clientId + loginId 反查 code ``` js {tokenName}:oauth2:code-index:{clientId}:{loginId} --> {code 值} ``` ### 3.2、Access-Token 资源令牌 ``` js {tokenName}:oauth2:access-token:{accessToken} --> {AccessTokenModel 对象} ```
详细 value 示例: ``` js { "@class": "cn.dev33.satoken.oauth2.data.model.AccessTokenModel", // java class 信息 "accessToken": "Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P", // 资源令牌值 "refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值 "expiresTime": 1738280553695, // 资源令牌到期时间 "refreshExpiresTime": 1740865353760, // 刷新令牌到期时间 "clientId": "1001", // 对应的应用id "loginId": "10001", // 对应的loginId "scopes": [ // 所具有的权限列表 "java.util.ArrayList", [ "userinfo", "userid", "openid", "unionid", "oidc" ] ], "tokenType": "bearer", // tokenType "grantType": "authorization_code", // 授权方式 "extraData": { // 扩展数据 "@class": "java.util.LinkedHashMap", "userid": "10001", "openid": "ded91dc189a437dd1bac2274be167d50", "unionid": "11d48faa74c4e5f19355ccc53c1c5c7a", "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g" }, "createTime": 1738273353694, // 创建时间 "expiresIn": 7199 // 资源令牌剩余有效时间,单位秒 "refreshExpiresIn": 2592000, // 刷新令牌剩余有效时间,单位秒 } ```
clientId + loginId 反查 Access-Token ``` js {tokenName}:oauth2:access-token-index:{clientId}:{loginId} --> {access_token 值} ``` ### 3.3、Refresh-Token 资源令牌 ``` js {tokenName}:oauth2:refresh-token:{refreshToken} --> {RefreshTokenModel 对象} ```
详细 value 示例: ``` js { "@class": "cn.dev33.satoken.oauth2.data.model.RefreshTokenModel", // java class 信息 "refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值 "expiresTime": 1740865353760, // 刷新令牌到期时间 "clientId": "1001", // 对应的应用id "loginId": "10001", // 对应的loginId "scopes": [ // 所具有的权限列表 "java.util.ArrayList", [ "userinfo", "userid", "openid", "unionid", "oidc" ] ], "extraData": { // 扩展数据 "@class": "java.util.LinkedHashMap", "userid": "10001", "openid": "ded91dc189a437dd1bac2274be167d50", "unionid": "11d48faa74c4e5f19355ccc53c1c5c7a", "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g" }, "createTime": 1738273353760, // 创建时间 "expiresIn": 2591999 // 刷新令牌剩余有效时间,单位秒 } ```
clientId + loginId 反查 Refresh-Token ``` js {tokenName}:oauth2:refresh-token-index:{clientId}:{loginId} --> {refresh_token 值} ``` ### 3.4、Client-Token 应用令牌 ``` js {tokenName}:oauth2:client-token:{clientToken} --> {ClientTokenModel 对象} ```
详细 value 示例: ``` js { "@class": "cn.dev33.satoken.oauth2.data.model.ClientTokenModel", // java class 信息 "clientToken": "lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv", // 应用令牌值 "expiresTime": 1738280930646, // 应用令牌到期时间 "clientId": "1001", // 对应的应用id "scopes": [ // 所具有的权限列表 "java.util.ArrayList", [ "userinfo", "userid", "openid", "unionid", "oidc" ] ], "tokenType": "bearer", // tokenType "grantType": "client_credentials", // 授权类型 "extraData": { // 扩展数据 "@class": "java.util.LinkedHashMap" }, "createTime": 1738273730646, // 创建时间 "expiresIn": 7199 // 应用令牌剩余有效时间,单位秒 } ```
clientId 反查 Client-Token ``` js {tokenName}:oauth2:client-token-index:{clientId} --> {client_token 值} ``` Lower-Client-Token 次级应用令牌索引 ``` js {tokenName}:oauth2:lower-client-token-index:{clientId} --> {client_token 值} ``` ### 3.5、用户授权记录 ``` js {tokenName}:oauth2:grant-scope:{clientId}:{loginId} --> {scope列表} ``` 值为 scope 列表,多个用逗号隔开,例如:`userinfo,openid,userid`。 ## 4、插件 ### 4.1、临时 token 会话 temp-token -> value ``` js // namespace 默认值为 "temp-token" {tokenName}:{namespace}:{temp-token} --> {value} ``` value 反查 temp-token ``` js {tokenName}:raw-session:{namespace}:{value} --> {Raw#SaSession 对象} ``` - 在 SaSession 以 `__HD_TEMP_TOKEN_MAP` 为 key 存储 temp-token 索引列表。值类型为 Map。 - 其中:Map 的 key = temp-token 值,Map 的 value = 此 temp-token 到期时间戳。 ### 4.2、 Same-Token Same-Token ``` js {tokenName}:var:same-token --> {same-token 值} ``` Past-Same-Token ``` js {tokenName}:var:past-same-token --> {same-token 值} ``` ### 4.3、Sign 签名 随机字符串 ``` js // nonce 值 默认为 32位随机字符 {tokenName}:sign:nonce:{nonce} --> {nonce 值} ``` ### 4.4、API Key ``` js // namespace 默认值为 "apikey" (全小写), ttl = 此 API Key 剩余有效期 {tokenName}:{namespace}:{apikey} --> {ApiKeyModel 对象} ```
详细 key 示例: ``` js satoken:apikey:AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R ``` value 示例: ``` js { "@class": "cn.dev33.satoken.apikey.model.ApiKeyModel", // java class 信息 "title": "test", // API Key 名称 "intro": null, // 用途介绍 "apiKey": "AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R", // API Key 值 "loginId": "10001", // 所属用户 id "createTime": 1766509019137, // 创建时间戳 "expiresTime": 1769101019136, // 到期时间戳 "isValid": true, // 是否有效 "scopes": [ // 含有权限 "java.util.ArrayList", [ "userinfo", "user-update" ] ], "extraData": null // 扩展数据:Map 类型 } ```
value 反查 API Key ``` js {tokenName}:raw-session:{namespace}:{value} --> {Raw#SaSession 对象} ``` - 在 SaSession 以 `__HD_API_KEY_LIST` 为 key 存储 API Key 索引列表。值类型为 List (API Key 列表)。 ================================================ FILE: sa-token-doc/arch/dir-intro.md ================================================ # 仓库目录介绍 --- ### 1、仓库根目录介绍: ``` js ── sa-token ├── sa-token-core // [核心] Sa-Token 核心模块 ├── sa-token-dependencies // [依赖] Sa-Token 依赖版本信息 ├── sa-token-special-dependencies // [依赖] Sa-Token 特殊依赖(SpringBoot2/3/4 版本隔离) ├── sa-token-bom // [核心] Sa-Token bom 包 ├── sa-token-starter // [整合] Sa-Token 与其它框架整合 ├── sa-token-plugin // [插件] Sa-Token 插件合集 ├── sa-token-demo // [示例] Sa-Token 示例合集 ├── sa-token-test // [测试] Sa-Token 单元测试合集 ├── sa-token-doc // [文档] Sa-Token 开发文档 ├── MEMO // [备忘] 内部备忘录、开发记录 ├── pom.xml // [依赖] 顶级pom文件 ├── LICENSE // 开源协议 ├── mvn clean.bat // 一键 mvn clean 核心包+所有示例包 ├── mvn test.bat // 一键单元测试 ├── preview-doc.bat // 一键预览开发文档 ├── README.md // 仓库自述文件 ``` ### 2、所有目录详细介绍: ``` js ── sa-token ├── sa-token-core // [核心] Sa-Token 核心模块 ├── sa-token-dependencies // [依赖] Sa-Token 依赖版本信息 ├── sa-token-special-dependencies // [依赖] Sa-Token 特殊依赖(SpringBoot2/3/4 版本隔离) ├── sa-token-bom // [核心] Sa-Token bom 包 ├── sa-token-starter // [整合] Sa-Token 与其它框架整合 ├── sa-token-servlet // [整合] Sa-Token 整合 Servlet 容器实现类包 ├── sa-token-jakarta-servlet // [整合] Sa-Token 整合 Jakarta-Servlet 容器实现类包 ├── sa-token-spring-boot-webmvc-reactor-v2v3v4-common // [整合] Sa-Token SpringBoot WebMvc+Reactor 公共包 (2/3/4) ├── sa-token-spring-boot-reactor-v2v3v4-common // [整合] Sa-Token SpringBoot Reactor 公共包 (2/3/4) ├── sa-token-spring-boot-starter // [整合] Sa-Token 整合 SpringBoot2 快速集成 ├── sa-token-spring-boot-webmvc-v3v4-common // [整合] Sa-Token SpringBoot WebMvc 公共包 (3/4) ├── sa-token-spring-boot3-starter // [整合] Sa-Token 整合 SpringBoot3 快速集成 ├── sa-token-spring-boot4-starter // [整合] Sa-Token 整合 SpringBoot4 快速集成 ├── sa-token-reactor-spring-boot-starter // [整合] Sa-Token 整合 SpringBoot2 Reactor 响应式编程 快速集成 ├── sa-token-reactor-spring-boot3-starter // [整合] Sa-Token 整合 SpringBoot3 Reactor 响应式编程 快速集成 ├── sa-token-reactor-spring-boot4-starter // [整合] Sa-Token 整合 SpringBoot4 Reactor 响应式编程 快速集成 ├── sa-token-solon-plugin // [整合] Sa-Token 整合 Solon 快速集成 ├── sa-token-jfinal-plugin // [整合] Sa-Token 整合 JFinal 快速集成 ├── sa-token-jboot-plugin // [整合] Sa-Token 整合 jboot 快速集成 ├── sa-token-loveqq-boot-starter // [整合] Sa-Token 整合 LoveQQ-Boot 快速集成 ├── sa-token-plugin // [插件] Sa-Token 插件合集 ├── sa-token-jackson // [插件] Sa-Token 整合 Jackson (json序列化插件) ├── sa-token-jackson3 // [插件] Sa-Token 整合 Jackson3 (json序列化插件) ├── sa-token-fastjson // [插件] Sa-Token 整合 Fastjson (json序列化插件) ├── sa-token-fastjson2 // [插件] Sa-Token 整合 Fastjson2 (json序列化插件) ├── sa-token-snack3 // [插件] Sa-Token 整合 Snack3 (json序列化插件) ├── sa-token-snack4 // [插件] Sa-Token 整合 Snack4 (json序列化插件) ├── sa-token-hutool-timed-cache // [插件] Sa-Token 整合 Hutool 缓存组件 Timed-Cache(基于内存) (数据缓存插件) ├── sa-token-caffeine // [插件] Sa-Token 整合 Caffeine 缓存组件(基于内存) (数据缓存插件) ├── sa-token-thymeleaf // [插件] Sa-Token 整合 Thymeleaf (自定义标签方言) ├── sa-token-freemarker // [插件] Sa-Token 整合 Freemarker (自定义标签方言) ├── sa-token-dubbo // [插件] Sa-Token 整合 Dubbo (RPC 调用鉴权、状态传递) ├── sa-token-dubbo3 // [插件] Sa-Token 整合 Dubbo3 (RPC 调用鉴权、状态传递) ├── sa-token-temp-jwt // [插件] Sa-Token 整合 jjwt (临时 Token) ├── sa-token-jwt // [插件] Sa-Token 整合 jjwt (JWT 登录认证) ├── sa-token-sso // [插件] Sa-Token 实现 SSO 单点登录 ├── sa-token-oauth2 // [插件] Sa-Token 实现 OAuth2.0 认证 ├── sa-token-apikey // [插件] Sa-Token 实现 API Key 认证 ├── sa-token-sign // [插件] Sa-Token 实现 API 参数签名 ├── sa-token-redisson // [插件] Sa-Token 整合 Redisson (数据缓存插件) ├── sa-token-redisx // [插件] Sa-Token 整合 Redisx (数据缓存插件) ├── sa-token-serializer-features // [插件] Sa-Token 序列化实现扩展 ├── sa-token-redis-template // [插件] Sa-Token 整合 RedisTemplate (数据缓存插件) ├── sa-token-redis-template-jdk-serializer // [插件] Sa-Token 整合 RedisTemplate - 使用 jdk 序列化算法 (数据缓存插件) ├── sa-token-redis-jackson // [插件] Sa-Token 整合 RedisTemplate - 使用 Jackson 序列化算法 (数据缓存插件) ├── sa-token-alone-redis // [插件] Sa-Token 独立 Redis 插件,实现 [ 权限缓存与业务缓存分离 ] ├── sa-token-spring-aop // [插件] Sa-Token 整合 SpringAOP 注解鉴权 ├── sa-token-spring-el // [插件] Sa-Token 实现 SpringEL 表达式注解鉴权 ├── sa-token-grpc // [插件] Sa-Token 整合 gRPC (RPC 调用鉴权、状态传递) ├── sa-token-quick-login // [插件] Sa-Token 快速注入登录页插件 ├── sa-token-redisson-spring-boot-starter // [插件] Sa-Token 整合 Redisson - SpringBoot 自动配置包 (数据缓存插件) ├── sa-token-forest // [插件] Sa-Token 整合 Forest,http 请求处理器 ├── sa-token-okhttps // [插件] Sa-Token 整合 OkHttps,http 请求处理器 ├── sa-token-demo // [示例] Sa-Token 示例合集 ├── sa-token-demo-alone-redis // [示例] Sa-Token 集成 alone-redis 模块 ├── sa-token-demo-alone-redis-cluster // [示例] Sa-Token 集成 alone-redis 模块、集群模式 ├── sa-token-demo-apikey // [示例] Sa-Token API Key 模块示例 ├── sa-token-demo-async // [示例] Sa-Token 异步场景示例 ├── sa-token-demo-beetl // [示例] Sa-Token 集成 beetl 示例 ├── sa-token-demo-bom-import // [示例] Sa-Token bom 包导入示例 ├── sa-token-demo-case // [示例] Sa-Token 各模块示例 ├── sa-token-demo-device-lock // [示例] Sa-Token 设备锁登录示例 - 后端 ├── sa-token-demo-device-lock-h5 // [示例] Sa-Token 设备锁登录示例 - 前端 ├── sa-token-demo-dubbo // [示例] Sa-Token 集成 dubbo ├── sa-token-demo-dubbo-consumer // [示例] Sa-Token 集成 dubbo 鉴权,消费端(调用端) ├── sa-token-demo-dubbo-provider // [示例] Sa-Token 集成 dubbo 鉴权,生产端(被调用端) ├── sa-token-demo-dubbo3-consumer // [示例] Sa-Token 集成 dubbo3 鉴权,消费端(调用端) ├── sa-token-demo-dubbo3-provider // [示例] Sa-Token 集成 dubbo3 鉴权,生产端(被调用端) ├── sa-token-demo-freemarker // [示例] Sa-Token 集成 Freemarker 标签方言 ├── sa-token-demo-grpc // [示例] Sa-Token 集成 grpc 鉴权 ├── client // [示例] Sa-Token 集成 grpc 鉴权,client 端 ├── server // [示例] Sa-Token 集成 grpc 鉴权,server 端 ├── sa-token-demo-hutool-timed-cache // [示例] Sa-Token 集成 hutool timed-cache ├── sa-token-demo-caffeine // [示例] Sa-Token 集成 Caffeine ├── sa-token-demo-jwt // [示例] Sa-Token 集成 jwt 登录认证 ├── sa-token-demo-oauth2 // [示例] Sa-Token 集成 OAuth2.0 ├── sa-token-demo-oauth2-client // [示例] Sa-Token 集成 OAuth2.0 (客户端) ├── sa-token-demo-oauth2-client-h5 // [示例] Sa-Token OAuth2 前端测试页 ├── sa-token-demo-oauth2-server // [示例] Sa-Token 集成 OAuth2.0 (服务端) ├── sa-token-demo-oauth2-server-h5 // [示例] Sa-Token 集成 OAuth2.0 (服务端 - 前后台分离示例) ├── sa-token-demo-quick-login // [示例] Sa-Token 集成 quick-login 模块 ├── sa-token-demo-quick-login-sb3 // [示例] Sa-Token 集成 quick-login 模块 (SpringBoot3) ├── sa-token-demo-remember-me // [示例] Sa-Token 实现 [ 记住我 ] 模式 ├── page_project // [示例] Sa-Token 实现 [ 记住我 ] 模式、前端页面 ├── sa-token-demo-remember-me-server // [示例] Sa-Token 实现 [ 记住我 ] 模式、后端接口 ├── sa-token-demo-solon // [示例] Sa-Token 集成 Solon ├── sa-token-demo-solon-reisson // [示例] Sa-Token 集成 Solon、Reisson ├── sa-token-demo-springboot // [示例] Sa-Token 整合 SpringBoot ├── sa-token-demo-springboot3-redis // [示例] Sa-Token 整合 SpringBoot3 整合 Redis ├── sa-token-demo-springboot4-redis // [示例] Sa-Token 整合 SpringBoot4 整合 Redis ├── sa-token-demo-springboot-low-version // [示例] Sa-Token 整合 SpringBoot2 低版本 ├── sa-token-demo-springboot-redis // [示例] Sa-Token 整合 SpringBoot 整合 Redis ├── sa-token-demo-springboot-redisson // [示例] Sa-Token 整合 SpringBoot 整合 redisson ├── sa-token-demo-sse // [示例] 在 SSE 中使用 Sa-Token ├── sa-token-demo-ssm // [示例] 在 SSM 中使用 Sa-Token ├── sa-token-demo-sso // [示例] Sa-Token 集成 SSO 单点登录 ├── sa-token-demo-sso-server // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 ├── sa-token-demo-sso1-client // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 (同域、同Redis) ├── sa-token-demo-sso2-client // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 (跨域、同Redis) ├── sa-token-demo-sso3-client // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (跨域、跨Redis) ├── sa-token-demo-sso3-client-nosdk // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (不使用sdk,纯手动对接) ├── sa-token-demo-sso3-client-resdk // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (ReSdk 模式,重写部分方法对接任意技术栈) ├── sa-token-demo-sso3-client-anon // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (匿名应用接入示例) ├── sa-token-demo-sso-server-h5 // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 (前后端分离) ├── sa-token-demo-sso-client-h5 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-原生h5 版本) ├── sa-token-demo-sso-server-vue2 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-Vue2 版本) ├── sa-token-demo-sso-client-vue3 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-Vue3 版本) ├── sa-token-demo-sso-for-solon // [示例] Sa-Token 集成 SSO 单点登录(Solon 版) ├── sa-token-demo-sso-server-solon // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 ├── sa-token-demo-sso1-client-solon // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 (同域、同Redis) ├── sa-token-demo-sso2-client-solon // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 (跨域、同Redis) ├── sa-token-demo-sso3-client-solon // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (跨域、跨Redis) ├── sa-token-demo-test // [示例] Sa-Token 整合测试项目 ├── sa-token-demo-thymeleaf // [示例] Sa-Token 集成 Thymeleaf 标签方言 ├── sa-token-demo-webflux // [示例] Sa-Token 整合 WebFlux ├── sa-token-demo-webflux-springboot3 // [示例] Sa-Token 整合 WebFlux (SpringBoot3) ├── sa-token-demo-webflux-springboot4 // [示例] Sa-Token 整合 WebFlux (SpringBoot4) ├── sa-token-demo-websocket // [示例] Sa-Token 集成 Web-Socket 鉴权示例 ├── sa-token-demo-websocket-spring // [示例] Sa-Token 集成 Web-Socket(Spring封装版) 鉴权示例 ├── sa-token-demo-loveqq-boot // [示例] Sa-Token 集成 LoveQQ-Boot ├── pom.xml // 示例 pom 文件,用于帮助在 idea 中一键导入所有 demo ├── sa-token-test // [测试] Sa-Token 单元测试合集 ├── sa-token-easy-test // [测试] Sa-Token 简易测试 ├── sa-token-springboot-test // [测试] Sa-Token SpringBoot 整合测试 ├── sa-token-jwt-test // [测试] Sa-Token jwt 整合测试 ├── sa-token-temp-jwt-test // [测试] Sa-Token temp-jwt 整合测试 ├── sa-token-json-test // [测试] Sa-Token json 序列化测试 ├── sa-token-jackson3-test // [测试] Sa-Token Jackson3 整合测试 ├── sa-token-serializer-test // [测试] Sa-Token 序列化测试 ├── sa-token-doc // [文档] Sa-Token 开发文档 ├── MEMO // [备忘] 内部备忘录、开发记录 ├── pom.xml // [依赖] 顶级pom文件 ├── LICENSE // 开源协议 ├── mvn clean.bat // 一键 mvn clean 核心包+所有示例包 ├── mvn test.bat // 一键单元测试 ├── preview-doc.bat // 一键预览开发文档 ├── README.md // 仓库自述文件 ``` 其它([sa-tokens](https://gitee.com/sa-tokens) 组织下相关仓库): - [Awesome-Sa-Token](https://gitee.com/sa-tokens/awesome-sa-token):集成 Sa-Token 的优秀开源案例收集。 - [sa-token-rust](https://gitee.com/sa-tokens/sa-token-rust):Sa-Token 的 Rust 版本,轻量级 Rust 权限认证框架。 - [sa-token-go](https://gitee.com/sa-tokens/sa-token-go):Sa-Token 的 Go 版本,轻量级 Go 权限认证框架。 - [Sa-Token-Study](https://gitee.com/sa-tokens/Sa-Token-Study):Sa-Token 涉及技术点学习笔记与实战。 - [Sa-Token-Login-Demos](https://gitee.com/sa-tokens/Sa-Token-Login-Demos):各种登录方式示例集合,一站式学习 Sa-Token 登录认证。 - [sa-token-doc-big-file](https://gitee.com/sa-tokens/sa-token-doc-big-file):sa-token-doc 文档中的图片资源文件。 - [sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin):Sa-Token 第三方插件合集。 - [sa-token-demo-cross](https://gitee.com/sa-tokens/sa-token-demo-cross):Sa-Token 处理跨域场景示例。 - [auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test):Java 权限认证框架功能 测试 / 对比 / 迁移。 ================================================ FILE: sa-token-doc/doc/index-backup.html ================================================ Sa-Token

Sa-Token 文档已迁移,请打开:最新地址

================================================ FILE: sa-token-doc/doc/index.html ================================================ Sa-Token

Sa-Token 文档已迁移,请打开:最新地址

================================================ FILE: sa-token-doc/doc.html ================================================ Sa-Token
加载中...
目录

如果 Sa-Token 帮助到了你,希望你可以向同事、朋友推荐了解本框架,这对我们非常重要,感谢支持!

加油,工程师!

================================================ FILE: sa-token-doc/fun/async--mock.md ================================================ # 异步 & Mock 上下文 ### 异步上下文 有一些方法(例如`StpUtil.isLogin()`)只可以在同步的 Web 上下文中才可以调用,如果在异步上下文中调用则会抛出异常: ``` text cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化 ``` 这是因为这些方法需要从前端的 `HttpServletRequest` 中读取 Token 参数,而异步上下文通常不是一次“请求”,不具有 `HttpServletRequest` 的概念,所以无法成功调用。 一般哪些场景属于异步上下文? - 通过 `new Thread(() -> { ... }).start()` 启动子线程。 - 通过 `taskExecutor.execute(() -> { ... })` 线程池启动异步任务。 - 通过 `@Async` 注解标注的方法。 - 通过 `@Scheduled(cron = "")` 启动的定时任务。 - 消息队列中消费消息的函数。 - ... 凡是不通过 web 请求调用触发的线程,在 Sa-Token 中均属于异步上下文,也可以称作 “非 Web 上下文”。 此时调用 `StpUtil.isLogin()`、`StpUtil.getLoginId()` 等需要 Web 上下文的 API,就会抛出上述异常。 如果你需要在 非 Web 上下文 中调用上述 API,则需要手动 mock 一个上下文,才可以调用成功: 例如: ``` java // 【异步】new Thread @RequestMapping("isLogin2") public SaResult isLogin2() { System.out.println("是否登录:" + StpUtil.isLogin()); String tokenValue = StpUtil.getTokenValue(); new Thread(() -> { SaTokenContextMockUtil.setMockContext(()->{ StpUtil.setTokenValueToStorage(tokenValue); System.out.println("是否登录:" + StpUtil.isLogin()); }); }).start(); return SaResult.data(StpUtil.getTokenValue()); } ``` 参考上述方法,你需要先调用 `SaTokenContextMockUtil.setMockContext(() -> { ... })` Mock 出一个 Web 上下文填充到上下文管理器中, 然后在 Mock 上下文范围内调用 `StpUtil.setTokenValueToStorage(tokenValue)` 指定当前上下文的 token 值,其效果等同于在 web 上下文中前端提交了此 token 值。 更多使用姿势请参考仓库示例:[Async-TestController.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java) ### 响应式上下文 在 WebFlux / Spring Cloud 等响应式环境下调用 Sa-Token 的同步 API 也有可能发生上下文异常: ``` cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化 at cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff.getModelBox(SaTokenContextForThreadLocalStaff.java:73) ~[classes/:na] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaReactorFilter [DefaultWebFilterChain] *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor [DefaultWebFilterChain] *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor [DefaultWebFilterChain] *__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor [DefaultWebFilterChain] *__checkpoint ⇢ HTTP GET "/test/isLogin" [ExceptionHandlingWebHandler] ``` 如果是在自定义 Filter 中报的这个错,需要你在调用 Sa-Token 的同步 API 之前手动 set 一下上下文: ``` java // 自定义过滤器 @Component public class MyFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { try { // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文 SaReactorSyncHolder.setContext(exchange); System.out.println(StpUtil.isLogin()); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } } ``` 参考:[MyFilter.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java) 在 Controller 里同理: ``` java @RequestMapping("isLogin2") public SaResult isLogin2(ServerWebExchange exchange) { SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{ System.out.println("是否登录:" + StpUtil.isLogin()); return SaResult.data(StpUtil.getTokenInfo()); }); return SaResult.data(res); } ``` 更多示例请参考: [TestController.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java) ================================================ FILE: sa-token-doc/fun/auth-flow.md ================================================ # Sa-Token 功能结构图 --- ### Sa-Token 功能结构图: sa-token-rz ### Sa-Token 认证流程图: sa-token-rz PS:鼠标右键选择 **`[在新窗口打开图片]`** 即可高清模式查看图片。 ================================================ FILE: sa-token-doc/fun/auth-framework-function-test.md ================================================ # Java 权限认证框架功能 测试 / 对比 / 迁移。 对比以下框架的常见功能,为项目技术栈迁移提供代码示例 - Sa-Token - Apache Shiro - Spring Security - JWT > [!TIP| label:注意事项] > - 因个人精力&能力有限,本篇只展示部分常见功能的对比,也欢迎大家一起贡献案例,提交pr。 > - 代码案例仓库:[https://gitee.com/sa-tokens/auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test) > - 注:本篇主要展示一些常见功能不同框架的实现差异,而非每个框架的所含功能点对比。 --- ### 依赖引入 ``` xml cn.dev33 sa-token-spring-boot3-starter 1.39.0 ``` ``` xml org.apache.shiro shiro-spring-boot-web-starter 1.13.0 ``` ``` xml org.springframework.boot spring-boot-starter-security 3.3.2 ``` SpringBoot 项目下一般不用特别指定 SpringSecurity 版本号 ``` xml cn.hutool hutool-all 5.8.29 ``` ### 会话登录 & 会话状态查询 测试 Controller ``` java @RestController @RequestMapping("/acc/") public class LoginController { @Autowired SysUserDao sysUserDao; // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); if(user == null) { return AjaxJson.getError("用户不存在"); } if(!user.getPassword().equals(password)) { return AjaxJson.getError("密码错误"); } // 登录 StpUtil.login(user.getId()); StpUtil.getSession().set("user", user); return AjaxJson.getSuccess("登录成功"); } // 查询登录状态 @RequestMapping("isLogin") public AjaxJson isLogin() { if(StpUtil.isLogin()) { return AjaxJson.getSuccess("已登录,账号id:" + StpUtil.getLoginId()); } return AjaxJson.getError("未登录"); } } ``` 自定义 Realm ``` java public class MyRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; // 加载用户信息 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username = (String)token.getPrincipal(); SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ return null; } return new SimpleAuthenticationInfo( sysUser, sysUser.getPassword(), getName() ); } // 加载权限信息 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } } ``` Shiro 配置类 ``` java @Configuration public class ShiroConfigure { @Bean public MyRealm myRealm() { return new MyRealm(); } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); return bean; } } ``` 测试 Controller ``` java @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8082/acc/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); return AjaxJson.getSuccess("登录成功!"); } catch (AuthenticationException e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } // 查询登录状态 ---- http://localhost:8082/acc/isLogin @RequestMapping("isLogin") public AjaxJson isLogin() { Subject subject = SecurityUtils.getSubject(); if(subject.isAuthenticated()) { SysUser sysUser = (SysUser)subject.getPrincipal(); return AjaxJson.getSuccess("已登录,账号id:" + sysUser.getId()); } return AjaxJson.getError("未登录"); } } ``` 定义 SpringSecurity 配置类 ``` java @Configuration public class SpringSecurityConfigure { /** * Spring Security的核心过滤器链配置 */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { // 定义安全请求拦截规则 httpSecurity.authorizeHttpRequests(router -> { router // 放行接口 .requestMatchers("/acc/doLogin", "/acc/isLogin").permitAll() // 所有请求都需要认证 .anyRequest().authenticated(); ; }); // 默认的表单登录 httpSecurity.formLogin(withDefaults()); // 是否启用 csrf 防御 httpSecurity.csrf( csrf -> csrf.disable() ); // 一些安全相关的全局响应头 httpSecurity.headers(httpSecurityHeadersConfigurer -> { httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable); httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable); }); return httpSecurity.build(); } /** * Spring Security 认证管理器 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } } ``` 定义 SpringSecurity UserDetails 管理器 ``` java /** * 自定义 SpringSecurity UserDetails 管理器 * * @author click33 * @since 2024/8/8 */ @Component public class CustomUserDetailsManager implements UserDetailsManager { @Autowired SysUserDao sysUserDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ throw new UsernameNotFoundException("用户不存在"); } return User.withUsername(sysUser.getUsername()) .password("{noop}" + sysUser.getPassword()) .build(); } @Override public void createUser(UserDetails user) { } @Override public void updateUser(UserDetails user) { } @Override public void deleteUser(String username) { } @Override public void changePassword(String oldPassword, String newPassword) { } @Override public boolean userExists(String username) { return false; } } ``` 测试 Controller ``` java @RestController @RequestMapping("/acc/") public class LoginController { @Autowired AuthenticationManager authenticationManager; @Autowired SysUserDao sysUserDao; // 测试登录 ---- http://localhost:8083/acc/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password, HttpServletRequest request) { try { // 验证账号密码 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username)); Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); // 存入上下文 SecurityContextHolder.getContext().setAuthentication(authentication); request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); // 返回 return AjaxJson.getSuccess("登录成功!"); } catch (Exception e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } // 查询登录状态 ---- http://localhost:8083/acc/isLogin @RequestMapping("isLogin") public AjaxJson isLogin() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return AjaxJson.getSuccess("是否登录:" + !(authentication instanceof AnonymousAuthenticationToken)) .set("principal", authentication.getPrincipal()) .set("details", authentication.getDetails()); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/acc/") public class LoginController { @Autowired SysUserDao sysUserDao; // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); if(user == null) { return AjaxJson.getError("用户不存在"); } if(!user.getPassword().equals(password)) { return AjaxJson.getError("密码错误"); } // 登录 String token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2); return AjaxJson.getSuccess("登录成功").set("token", token); } // 查询登录状态 @RequestMapping("isLogin") public AjaxJson isLogin(HttpServletRequest request) { try{ String token = request.getHeader("token"); JWT jwt = JwtUtil.parseToken(token); return AjaxJson.getSuccess("已登录") .set("id", jwt.getPayload("userId")) .set("user", jwt.getPayload("user")); } catch (Exception e) { e.printStackTrace(); return AjaxJson.getError("未登录"); } } } ``` ### 会话注销 ``` java @RequestMapping("logout") public AjaxJson logout() { StpUtil.logout(); return AjaxJson.getSuccess("注销成功"); } ``` ``` java @RequestMapping("logout") public AjaxJson logout() { SecurityUtils.getSubject().logout(); return AjaxJson.getSuccess("注销成功"); } ``` ``` java @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { // 其它配置 ... // 注销相关配置 httpSecurity.logout(logout -> { logout.logoutUrl("/acc/logout"); logout.logoutSuccessHandler((request, response, authentication) -> { response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); String jsonStr = new ObjectMapper().writeValueAsString(AjaxJson.getSuccess("注销成功!")); response.getWriter().write(jsonStr); }); }); return httpSecurity.build(); } ``` JWT 无法注销已经颁发的 token 。 ### 账号密码登录(MD5 加 salt) 测试 Controller ``` java @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); if(user == null) { return AjaxJson.getError("用户不存在"); } String salt = "abc"; if(!user.getPassword().equals(SaSecureUtil.md5(salt + password))) { return AjaxJson.getError("密码错误"); } // 登录 StpUtil.login(user.getId()); StpUtil.getSession().set("user", user); return AjaxJson.getSuccess("登录成功"); } ``` 自定义 Realm Bean 设定密码凭证器 ``` java @Bean public MyRealm myRealm() { MyRealm realm = new MyRealm(); // 设定凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); realm.setCredentialsMatcher(credentialsMatcher); // 返回 return realm; } ``` 自定义 Realm 实现类 doGetAuthenticationInfo 方法返回 slat 信息 ``` java // 加载用户信息 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username = (String)token.getPrincipal(); SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ return null; } return new SimpleAuthenticationInfo( sysUser, sysUser.getPassword(), ByteSource.Util.bytes("abc"), // 指定 slat 信息 getName() ); } ``` 登录代码照旧 CustomUserDetailsManager 的 loadUserByUsername 指定 MD5 算法 ``` java @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ throw new UsernameNotFoundException("用户不存在"); } return User.withUsername(sysUser.getUsername()) .password("{MD5}" + sysUser.getPassword()) .build(); } ``` 登录时指定 salt ``` java @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password, HttpServletRequest request) { try { // 验证账号密码 String salt = "abc"; UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, salt + password); // 其它代码照旧 ... // 返回 return AjaxJson.getSuccess("登录成功!"); } catch (Exception e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } ``` 测试 Controller ``` java @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); if(user == null) { return AjaxJson.getError("用户不存在"); } String salt = "abc"; if(!user.getPassword().equals(SecureUtil.md5(salt + password))) { return AjaxJson.getError("密码错误"); } // 登录 String token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2); return AjaxJson.getSuccess("登录成功").set("token", token); } ``` ### 从上下文获取当前登录 User 信息 ``` java // 从上下文获取当前登录 User 信息 @RequestMapping("getCurrUser") public AjaxJson getCurrUser() { return AjaxJson.getSuccess() .set("id", StpUtil.getLoginId()) .set("user", StpUtil.getSession().get("user")); } ``` ``` java // 从上下文获取当前登录 User 信息 @RequestMapping("getCurrUser") public AjaxJson getCurrUser() { Subject subject = SecurityUtils.getSubject(); SysUser sysUser = (SysUser)subject.getPrincipal(); return AjaxJson.getSuccess() .set("id", sysUser.getId()) .set("user", sysUser); } ``` ``` java // 从上下文获取当前登录 User 信息 @RequestMapping("getCurrUser") public AjaxJson getCurrUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(!(authentication instanceof AnonymousAuthenticationToken)) { SysUser sysUser = (SysUser)authentication.getDetails(); return AjaxJson.getSuccess() .set("id", sysUser.getId()) .set("user", sysUser); } return AjaxJson.getError("未登录"); } ``` ``` java // 从上下文获取当前登录 User 信息 @RequestMapping("getCurrUser") public AjaxJson getCurrUser(HttpServletRequest request) { try{ String token = request.getHeader("token"); JWT jwt = JwtUtil.parseToken(token); SysUser sysUser = jwt.getPayloads().get("user", SysUser.class); return AjaxJson.getSuccessData(sysUser); } catch (Exception e) { e.printStackTrace(); return AjaxJson.getError("未登录"); } } ``` ### 从会话上下文上存取值 ``` java // 测试从从会话上下文存取值 @RequestMapping("testSession") public AjaxJson test() { SaSession session = StpUtil.getSession(); System.out.println("从 session 上取值:" + session.get("name")); session.set("name", "zhang"); System.out.println("从 session 上取值:" + session.get("name")); return AjaxJson.getSuccess(); } ``` ``` java // 测试从从会话上下文存取值 @RequestMapping("testSession") public AjaxJson test() { Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); System.out.println("从 session 上取值:" + session.getAttribute("name")); session.setAttribute("name", "zhang"); System.out.println("从 session 上取值:" + session.getAttribute("name")); return AjaxJson.getSuccess(); } ``` ``` java // 测试从从会话上下文存取值 @RequestMapping("testSession") public AjaxJson testSession(HttpServletRequest request) { HttpSession session = request.getSession(); System.out.println("从 session 上取值:" + session.getAttribute("name")); session.setAttribute("name", "zhang"); System.out.println("从 session 上取值:" + session.getAttribute("name")); return AjaxJson.getSuccess(); } ``` ### 角色认证 & 权限认证 自定义 StpInterface 实现类 ``` java @Component public class StpInterfaceImpl implements StpInterface { // 加载角色信息 @Override public List getRoleList(Object loginId, String loginType) { return Arrays.asList("admin", "super-admin", "ceo"); } // 加载权限信息 @Override public List getPermissionList(Object loginId, String loginType) { return Arrays.asList("user:add", "user:delete", "user:update"); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/jur/") public class JurController { // 角色判断 ---- http://localhost:8082/jur/assertRole @RequestMapping("assertRole") public AjaxJson assertRole() { // is 模式,返回 true 或 false System.out.println("单个角色判断:" + StpUtil.hasRole("admin")); System.out.println("多个角色判断(and):" + StpUtil.hasRoleAnd("admin", "dev-admin")); System.out.println("多个角色判断(or):" + StpUtil.hasRoleOr("admin", "dev-admin")); // check 模式,无角色时抛出异常 StpUtil.checkRole("admin"); // 单个 check StpUtil.checkRoleAnd("admin", "dev-admin"); // 多个 check (and) StpUtil.checkRoleOr("admin", "dev-admin"); // 多个 check (or) return AjaxJson.getSuccess(); } // 权限判断 ---- http://localhost:8082/jur/assertPermission @RequestMapping("assertPermission") public AjaxJson assertPermission() { // is 模式,返回 true 或 false System.out.println("单个权限判断:" + StpUtil.hasPermission("user:add")); System.out.println("多个权限判断(and):" + StpUtil.hasPermissionAnd("user:add", "user:delete22")); System.out.println("多个权限判断(or):" + StpUtil.hasPermissionOr("user:add", "user:delete22")); // check 模式,无权限时抛出异常 StpUtil.checkPermission("user:add"); // 单个 check StpUtil.checkPermissionAnd("user:add", "user:delete22"); // 多个 check (and) StpUtil.checkPermissionOr("user:add", "user:delete22"); // 多个 check (or) return AjaxJson.getSuccess(); } } ``` 自定义 Realm 里重写方法 doGetAuthorizationInfo ``` java @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 加载角色信息 authorizationInfo.addRoles(Arrays.asList("admin", "super-admin", "ceo")); // 加载权限信息 authorizationInfo.addStringPermissions(Arrays.asList("user:add", "user:delete", "user:update")); return authorizationInfo; } ``` 测试 Controller ``` java @RestController @RequestMapping("/jur/") public class JurController { // 角色判断 @RequestMapping("assertRole") public AjaxJson assertRole() { Subject subject = SecurityUtils.getSubject(); // is 模式,返回 true 或 false System.out.println("单个角色判断:" + subject.hasRole("admin")); System.out.println("多个角色判断(and):" + subject.hasAllRoles(Arrays.asList("admin", "dev-admin"))); System.out.println("多个角色判断(or):" + (subject.hasRole("admin") || subject.hasRole("dev-admin"))); // check 模式,无角色时抛出异常 subject.checkRole("admin"); // 单个 check subject.checkRoles("admin", "dev-admin"); // 多个 check (and) return AjaxJson.getSuccess(); } // 权限判断 @RequestMapping("assertPermission") public AjaxJson assertPermission() { Subject subject = SecurityUtils.getSubject(); // is 模式,返回 true 或 false System.out.println("单个权限判断:" + subject.isPermitted("user:add")); System.out.println("多个权限判断(and):" + subject.isPermittedAll("user:add", "user:delete22")); System.out.println("多个权限判断(or):" + (subject.isPermitted("user:add") || subject.isPermitted("user:delete22"))); // check 模式,无权限时抛出异常 subject.checkPermission("user:add"); // 单个 check subject.checkPermissions("user:add", "user:delete22"); // 多个 check (and) return AjaxJson.getSuccess(); } } ``` CustomUserDetailsManager 的 loadUserByUsername 里返回用户的 角色 或 权限 信息 ``` java @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserDao.findByUsername(username); if(sysUser == null){ throw new UsernameNotFoundException("用户不存在"); } // 不可以同时返回 roles 和 authorities,因为会相互覆盖,SpringSecurity 源码有bug return User.withUsername(sysUser.getUsername()) .password("{noop}" + sysUser.getPassword()) // .roles("admin", "super-admin", "ceo") .authorities("user:add", "user:delete", "user:update") .build(); } ``` 测试 Controller ``` java @RestController @RequestMapping("/jur/") public class JurController { // 角色判断 @RequestMapping("assertRole") public AjaxJson assertRole() { SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; System.out.println("单个角色判断:" + securityExpressionRoot.hasRole("admin")); System.out.println("多个角色判断(and):" + (securityExpressionRoot.hasRole("admin") && securityExpressionRoot.hasRole("dev-admin"))); System.out.println("多个角色判断(or):" + securityExpressionRoot.hasAnyRole("admin", "dev-admin")); return AjaxJson.getSuccess(); } // 权限判断 @RequestMapping("assertPermission") public AjaxJson assertPermission() { SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; System.out.println("单个权限判断:" + securityExpressionRoot.hasAuthority("user:add")); System.out.println("多个权限判断(and):" + (securityExpressionRoot.hasAuthority("user:add") && securityExpressionRoot.hasAuthority("user:delete2"))); System.out.println("多个权限判断(or):" + securityExpressionRoot.hasAnyAuthority("user:add", "user:delete2")); return AjaxJson.getSuccess(); } } ``` ### 注解鉴权 SaTokenConfigure 配置注解拦截器 ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/at-check/") public class AtCheckController { // 登录校验 @SaCheckLogin @RequestMapping("checkLogin") public AjaxJson checkLogin() { return AjaxJson.getSuccess(); } // 角色校验 @SaCheckRole("admin") @RequestMapping("checkRole") public AjaxJson checkRole() { return AjaxJson.getSuccess(); } // 权限校验 @SaCheckPermission("user:add") @RequestMapping("checkPermission") public AjaxJson checkPermission() { return AjaxJson.getSuccess(); } // 忽略认证校验 @SaIgnore @SaCheckLogin @RequestMapping("ignoreCheck") public AjaxJson ignoreCheck() { return AjaxJson.getSuccess(); } } ``` 测试 Controller ``` java @RestController @RequestMapping("/at-check/") public class AtCheckController { // 登录校验 @RequiresAuthentication @RequestMapping("checkLogin") public AjaxJson checkLogin() { return AjaxJson.getSuccess(); } // 角色校验 @RequiresRoles("admin") @RequestMapping("checkRole") public AjaxJson checkRole() { return AjaxJson.getSuccess(); } // 权限校验 @RequiresPermissions("user:add") @RequestMapping("checkPermission") public AjaxJson checkPermission() { return AjaxJson.getSuccess(); } } ``` `SpringSecurityConfigure` 配置类加上 `@EnableMethodSecurity` 注解 ``` java @Configuration @EnableMethodSecurity public class SpringSecurityConfigure { // ... } ``` 测试 Controller ``` java @RestController @RequestMapping("/at-check/") public class AtCheckController { // 登录校验 @PreAuthorize("isAuthenticated()") @RequestMapping("checkLogin") public AjaxJson checkLogin() { return AjaxJson.getSuccess(); } // 角色校验 @PreAuthorize("hasRole('admin')") @RequestMapping("checkRole") public AjaxJson checkRole() { return AjaxJson.getSuccess(); } // 权限校验 @PreAuthorize("hasAuthority('user:add')") @RequestMapping("checkPermission") public AjaxJson checkPermission() { return AjaxJson.getSuccess(); } } ``` ### 路由拦截鉴权 SaTokenConfigure 配置 ``` java @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解鉴权功能 registry.addInterceptor(new SaInterceptor(handle -> { SaRouter.match("/route-check/getInfo1").stop(); // 不拦截 SaRouter.match("/route-check/getInfo2").check(r -> StpUtil.checkLogin()); // 需要登录 SaRouter.match("/route-check/getInfo3").check(r -> StpUtil.checkRole("admin2")); // 需要角色 SaRouter.match("/route-check/getInfo4").check(r -> StpUtil.checkPermission("user:add3")); // 需要权限 })).addPathPatterns("/**"); } ``` 过滤器配置 ``` java @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); // 路由拦截鉴权 Map filterMap = new LinkedHashMap<>(); filterMap.put("/route-check/getInfo", "anon"); // 不拦截 filterMap.put("/route-check/getInfo2", "authc"); // 需要登录 filterMap.put("/route-check/getInfo3", "perms[admin2]"); // 需要角色 filterMap.put("/route-check/getInfo4", "perms[user:add3]"); // 需要权限 bean.setFilterChainDefinitionMap(filterMap); bean.setLoginUrl("/401"); // 未登录时跳转的 url bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url return bean; } ``` SpringSecurityConfigure 配置 ``` java @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { // 定义安全请求拦截规则 httpSecurity.authorizeHttpRequests(router -> { router .requestMatchers("/route-check/getInfo1").permitAll() // 不拦截 .requestMatchers("/route-check/getInfo2").authenticated() // 需要登录 .requestMatchers("/route-check/getInfo3").hasRole("admin") // 需要 admin 角色 .requestMatchers("/route-check/getInfo4").hasAuthority("user:add") // 需要 user:add 权限 .anyRequest().permitAll(); // 所有请求都放行 }); return httpSecurity.build(); } ``` ### 鉴权未通过的处理方案 定义全局异常处理类 ``` java @RestControllerAdvice public class GlobalException { @ExceptionHandler(NotLoginException.class) public AjaxJson handlerException(NotLoginException e) { return AjaxJson.get(401, "未登录"); } @ExceptionHandler(NotRoleException.class) public AjaxJson handlerException(NotRoleException e) { return AjaxJson.get(403, "缺少角色:" + e.getRole()); } @ExceptionHandler(NotPermissionException.class) public AjaxJson handlerException(NotPermissionException e) { return AjaxJson.get(403, "缺少权限:" + e.getPermission()); } } ``` 过滤器配置 ``` java @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // ... bean.setLoginUrl("/401"); // 未登录时跳转的 url bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url return bean; } ``` 定义路由 ``` java @RestController public class ShiroErrorController { @RequestMapping("/401") public Object error401(HttpServletRequest request, HttpServletResponse response) { response.setStatus(200); return AjaxJson.get(401, "not login"); } @RequestMapping("/403") public Object error403(HttpServletRequest request, HttpServletResponse response) { response.setStatus(200); return AjaxJson.get(403, "鉴权未通过"); } } ``` 实现 `AccessDeniedHandler`, `AuthenticationEntryPoint` 接口 ``` java @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable { // 未登录异常 @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { //验证为未登陆状态会进入此方法,认证错误 response.setStatus(401); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter printWriter = response.getWriter(); String body = "请先进行登录"; printWriter.write(body); printWriter.flush(); } // 权限不足 @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { // 登陆状态下,权限不足执行该方法 response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter printWriter = response.getWriter(); String body = "权限不足"; printWriter.write(body); printWriter.flush(); } } ``` 注入 `SecurityFilterChain` ``` java // 未登录处理逻辑、权限不足处理逻辑 @Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { // 异常处理 httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { // 权限不足处理方案 httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler); // 未登录 处理逻辑 httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler); }); return httpSecurity.build(); } ``` 使用 `try-catch` 捕获,或定义全局异常处理 ``` java @RestControllerAdvice public class GlobalException { // 全局异常拦截(拦截项目中的所有异常) @ExceptionHandler public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) { // 打印堆栈,以供调试 System.out.println("全局异常---------------"); e.printStackTrace(); // 返回给前端 return AjaxJson.getError(e.getMessage()); } } ``` ### 和 Thymeleaf 集成 `pom.xml` 依赖 ``` xml cn.dev33 sa-token-dialect-thymeleaf ${sa-token.version} ``` `SaTokenConfigure` 增加配置 `Sa-Token` 标签方言对象 ``` java // Sa-Token 标签方言 (Thymeleaf版) @Bean public SaTokenDialect getSaTokenDialect() { return new SaTokenDialect(); } ``` 新建 `ThymeleafConfigure` 注入全局变量 ``` java @Configuration public class ThymeleafConfigure { // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 @Autowired public void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { viewResolver.addStaticVariable("stp", StpUtil.stpLogic); } } ``` 新建 `Controller` ``` java @Controller public class HomeController { @RequestMapping("/") public Object index(HttpServletRequest request) { request.setAttribute("isLogin", StpUtil.isLogin()); return new ModelAndView("index.html"); } } ``` 新建 `templates/index.html` ``` html Sa-Token 集成 Thymeleaf 标签方言

Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面

当前是否登录:

登录 注销

登录之后才能显示:value

不登录才能显示:value

具有角色 admin 才能显示:value

同时具备多个角色才能显示:value

只要具有其中一个角色就能显示:value

不具有角色 admin 才能显示:value

具有权限 user-add 才能显示:value

同时具备多个权限才能显示:value

只要具有其中一个权限就能显示:value

不具有权限 user-add 才能显示:value

从SaSession中取值:

``` `pom.xml` 依赖 ``` xml com.github.theborakompanioni thymeleaf-extras-shiro 2.1.0 ``` `ShiroConfigure` 增加配置 `Shiro` 方言对象 ``` java @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } ``` 新建 `Controller` ``` java @Controller public class HomeController { @RequestMapping("/") public Object index(HttpServletRequest request) { Subject subject = SecurityUtils.getSubject(); request.setAttribute("isLogin", subject.isAuthenticated()); return new ModelAndView("index.html"); } } ``` 新建 `templates/index.html` ``` html Shiro 集成 Thymeleaf 标签方言

Shiro 集成 Thymeleaf 标签方言 —— 测试页面

当前是否登录:

登录 注销

登录之后才能显示:value

不登录才能显示:value

具有角色 admin 才能显示:value

同时具备多个角色才能显示:value

只要具有其中一个角色就能显示:value

不具有角色 admin 才能显示:value

具有权限 user-add 才能显示:value

同时具备多个权限才能显示:value

只要具有其中一个权限就能显示:value

不具有权限 user-add 才能显示:value

当前登录账号:

``` `pom.xml` 引入依赖 ``` xml org.thymeleaf.extras thymeleaf-extras-springsecurity6 3.1.2.RELEASE ``` 新建 `Controller` ``` java @RestController public class HomeController { // 首页 @RequestMapping("/") public Object index(HttpServletRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); request.setAttribute("isLogin", !(authentication instanceof AnonymousAuthenticationToken)); return new ModelAndView("index.html"); } } ``` 新建 `templates/index.html` ``` html Shiro 集成 Thymeleaf 标签方言

Shiro 集成 Thymeleaf 标签方言 —— 测试页面

当前是否登录:

登录 注销

登录之后才能显示:value

不登录才能显示:value

具有角色 admin 才能显示:value

同时具备多个角色才能显示:value

只要具有其中一个角色就能显示:value

不具有角色 admin 才能显示:value

具有权限 user-add 才能显示:value

同时具备多个权限才能显示:value

只要具有其中一个权限就能显示:value

不具有权限 user-add 才能显示:value

当前登录账号:

``` ### 前后端分离 1、在登录时,将 token 信息返回到前端 ``` java // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 SysUser user = sysUserDao.findByUsername(username); // user 信息校验代码不再赘述 ... // 登录 StpUtil.login(user.getId()); StpUtil.getSession().set("user", user); return AjaxJson.getSuccess("登录成功").set("satoken", StpUtil.getTokenValue()); // ⚠️ 关键代码 } ``` 2、前端改造 - 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('satoken', res.satoken)`。 - 2、在后续每次请求中,读取本地保存的 satoken 塞到请求 header 中 ``` js const header = {}; if(localStorage.satoken) { header.satoken = localStorage.satoken; } // 后续提交请求... ``` 1、自定义 SessionManager,从请求 header 里读取前端提交的 token ``` java public class MySessionManager extends DefaultWebSessionManager { private static final String TOKEN = "token"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public MySessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String id = WebUtils.toHttp(request).getHeader(TOKEN); // 如果请求头中有 token 则其值为sessionId if (!StringUtils.isEmpty(id)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } else { //否则按默认规则从cookie取sessionId return super.getSessionId(request, response); } } } ``` 2、注入到 SecurityManager 中 ``` java @Configuration public class ShiroConfigure { // 省略其它次要代码 ... @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); manager.setSessionManager(sessionManager()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); return bean; } // 自定义sessionManager @Bean public SessionManager sessionManager() { MySessionManager mySessionManager = new MySessionManager(); return mySessionManager; } } ``` 3、测试 Controller,登录时将 token 信息返回到前端 ``` java // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); String token = subject.getSession().getId().toString(); // ⚠️ 关键代码 return AjaxJson.getSuccess("登录成功!").set("token", token); // ⚠️ 关键代码 } catch (AuthenticationException e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } ``` 4、前端改造 - 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。 - 2、在后续每次请求中,读取本地保存的 token 塞到请求 header 中 ``` js const header = {}; if(localStorage.token) { header.token = localStorage.token; } // 后续提交请求... ``` 见下方 “集成 Redis” 部分,同时做到:集成 Redis + 前后端分离。 `JWT` 不依赖 `Cookie` 保存/传输 token,因此无需特殊定制即可原生支持前后端分离模式。 ### 集成 Redis pom.xml 引入依赖 ``` xml cn.dev33 sa-token-redis-template ${sa-token.version} org.apache.commons commons-pool2 ``` application.yml 新增连接配置 ``` yaml spring: data: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ``` 其它代码照旧 pom.xml 引入依赖 ``` xml org.crazycake shiro-redis 3.3.1 ``` application.yml 新增连接配置 ``` yaml spring: redis: shiro: # Redis服务器地址 host: 127.0.0.1:6379 # Redis服务器连接密码(默认为空) password: # Redis数据库索引(默认为0) database: 2 # 连接超时时间 timeout: 1800 ``` ShiroConfigure 注入相关 Bean ``` java @Configuration public class ShiroConfigure { // 自定义 securityManager @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); // manager.setRealm(myRealm()); // 自定义session管理 使用redis manager.setSessionManager(sessionManager()); // 自定义缓存实现 使用redis manager.setCacheManager(cacheManager()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); return bean; } // -------- 以下为 shiro redis 相关 -------- // Shiro redis 连接信息 @Value("${spring.redis.shiro.host}") private String host; @Value("${spring.redis.shiro.database}") private int database; @Value("${spring.redis.shiro.timeout}") private int timeout; @Value("${spring.redis.shiro.password}") private String password; /** * 配置shiro redisManager */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); if(StringUtils.hasText(password)){ redisManager.setPassword(password); } redisManager.setDatabase(database); redisManager.setTimeout(timeout); return redisManager; } /** * cacheManager 缓存 redis 实现 */ @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * RedisSessionDAO redis 实现 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } // 自定义sessionManager @Bean public SessionManager sessionManager() { MySessionManager mySessionManager = new MySessionManager(); mySessionManager.setSessionDAO(redisSessionDAO()); return mySessionManager; } } ``` SysUser 实体类要实现 Serializable 接口 ``` java @Data @NoArgsConstructor @AllArgsConstructor public class SysUser implements Serializable { // ... } ``` 其它代码照旧 (结合上部分,同时做到集成 Redis + 前后端分离) 1、`pom.xml` 引入依赖 ``` xml org.springframework.session spring-session-data-redis org.springframework.boot spring-boot-starter-data-redis ``` 2、`yml` 增加配置 ``` yml spring: session: store-type: redis timeout: 8H redis: namespace: spring:session data: # redis配置 redis: # Redis数据库索引(默认为0) database: 3 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ``` 3、在 `CustomAccessDeniedHandler` 自定义认证异常处理类中,返回 `json` 格式数据 ``` java @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable { // 未登录异常 @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { //验证为未登陆状态会进入此方法,认证错误 response.setStatus(401); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter printWriter = response.getWriter(); String body = new ObjectMapper().writeValueAsString(AjaxJson.get(401, "请先进行登录")); printWriter.write(body); printWriter.flush(); } // 权限不足 @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { // 登陆状态下,权限不足执行该方法 response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter printWriter = response.getWriter(); String body = new ObjectMapper().writeValueAsString(AjaxJson.get(403, "权限不足")); printWriter.write(body); printWriter.flush(); } } ``` 4、别忘了注入到 `SecurityFilterChain` 过滤器链 ``` java // 异常处理 httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { // 权限不足处理方案 httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler); // 未登录 处理逻辑 httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler); }); ``` 5、在登录时,返回对应 token 信息 ``` java // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password, HttpServletRequest request) { try { // 验证账号密码 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username)); Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); // 存入上下文 SecurityContextHolder.getContext().setAuthentication(authentication); request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); // 返回 String token = request.getSession().getId(); return AjaxJson.getSuccess("登录成功!").set("token", token); } catch (Exception e) { e.printStackTrace(); return AjaxJson.getError(e.getMessage()); } } ``` 6、前端改造 - 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。 - 2、在后续每次请求中,读取本地保存的 token 塞到请求 header 中 ``` js const header = {}; if(localStorage.token) { header.token = localStorage.token; } // 后续提交请求... ``` 7、新建 `HttpSessionConfigure` 配置重写 `HttpSessionId` 读取策略,改为从 `header` 头读取 `token` 参数作为 `SessionId` ``` java @Configuration public class HttpSessionConfigure { // HttpSession 读取策略,从 header 头读取 token 参数作为 session id @Bean public HeaderHttpSessionIdResolver httpSessionStrategy() { System.out.println("----------------- 自定义 HttpSession Id 读取方式"); return new HeaderHttpSessionIdResolver("token"); } } ``` ================================================ FILE: sa-token-doc/fun/cors-filter.md ================================================ # 解决跨域问题 参考1: [https://juejin.cn/post/7491603065944129590](https://juejin.cn/post/7491603065944129590) 参考2: [https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g) 参考3: [https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg](https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg) ================================================ FILE: sa-token-doc/fun/curr-domain.md ================================================ # 解决反向代理 uri 丢失的问题 --- 使用 `request.getRequestURL()` 可获取当前程序所在外网的访问地址,在 Sa-Token 中,其 `SaHolder.getRequest().getUrl()` 也正是借助此API完成, 有很多模块都用到了这个能力,比如SSO单点登录。 我们可以使用如下代码测试此API ``` java // 显示当前程序所在外网的都访问地址 @RequestMapping("test") public String test() { return "您访问的是:" + SaHolder.getRequest().getUrl(); } ``` 从浏览器访问此接口,我们可以看到: test-curr-domain.png 此 API 在本地开发时一般可以正常工作,然而如果我们在部署时使用 Nginx 做了一层反向代理后,其最终结果可能会和我们预想的有一点偏差: test-curr-domain-fxdl.png 不仅是 Nginx,所有包含路由转发的地方都有可能导致上述丢失 uri 的现象,解决方案也很简单,既然程序无法自动识别,我们改成手动获取即可,Sa-Token 提供两个方案: ### 方案一:Nginx转发时追加 header 参数 ##### 1、首先在 Nginx 代理转发的地方增加参数 nginx-add-header.png 重点是这一句:`proxy_set_header Public-Network-URL http://$http_host$request_uri;` ##### 2、在程序中新增类 `CustomSaTokenContextForSpring.java`,重写获取uri的逻辑 ``` java @Primary @Component public class CustomSaTokenContextForSpring extends SaTokenContextForSpring { @Override public SaRequest getRequest() { return new SaRequestForServlet(SpringMVCUtil.getRequest()) { @Override public String getUrl() { if(request.getHeader("Public-Network-URL") != null) { return request.getHeader("Public-Network-URL"); } return request.getRequestURL().toString(); } }; } } ``` 其它逻辑保持不变,框架即可正确获取 uri 地址 > [!ATTENTION| label:风险警告] > 注意:步骤一与步骤二需要同步存在,否则可能有前端假传 header 参数造成安全问题 ### 方案二:直接在yml中配置当前项目的网络访问地址 在 `application.yml` 中增加配置: ``` yaml sa-token: # 配置当前项目的网络访问地址 curr-domain: http://local.dev33.cn:8902/api ``` ``` properties # 配置当前项目的网络访问地址 sa-token.curr-domain=http://local.dev33.cn:8902/api ``` 即可避免路由转发过程中丢失 uri 的问题 ================================================ FILE: sa-token-doc/fun/custom-annotations.md ================================================ # 自定义注解 如果框架内置的注解无法满足你的业务需求,你还可以自定义注解注入到框架中。 --- ### 1、自定义注解 假设有以下业务需求 > [!INFO| label:需求场景] > 自定义一个注解 `@CheckAccount`,具有 `name`、`pwd` 两个字段,在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 #### 1.1、第一步,创建一个注解 ``` java /** * 账号校验:在标注一个方法上时,要求前端必须提交相应的账号密码参数才能访问方法。 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface CheckAccount { /** * 需要校验的账号 */ String name(); /** * 需要校验的密码 */ String pwd(); } ``` #### 1.2、第二步,创建注解处理器 实现 `SaAnnotationHandlerInterface` 接口,指定泛型为刚才自定义的注解 ``` java /** * 注解 CheckAccount 的处理器 */ @Component public class CheckAccountHandler implements SaAnnotationHandlerInterface { // 指定这个处理器要处理哪个注解 @Override public Class getHandlerAnnotationClass() { return CheckAccount.class; } // 每次请求校验注解时,会执行的方法 @Override public void checkMethod(CheckAccount at, AnnotatedElement element) { // 获取前端请求提交的参数 String name = SaHolder.getRequest().getParamNotNull("name"); String pwd = SaHolder.getRequest().getParamNotNull("pwd"); // 与注解中指定的值相比较 if(name.equals(at.name()) && pwd.equals(at.pwd()) ) { // 校验通过,什么也不做 } else { // 校验不通过,则抛出异常 throw new SaTokenException("账号或密码错误,未通过校验"); } } } ``` 参考上述代码,实现类上指定了 `@Component` 注解,使其可以在 ioc 环境下(如 Spring)被自动扫描注册 Sa-Token 中, 如果你的项目属于非 ioc 环境,则需要手动将其注册到 Sa-Token 框架中: ``` java SaAnnotationStrategy.instance.registerAnnotationHandler(new CheckAccountHandler()); ``` #### 1.3、测试自定义的注解 我们在一个请求接口上指定这个注解,来测试一下效果 ``` java @RestController @RequestMapping("/test/") public class TestController { @RequestMapping("test") @CheckAccount(name = "sa", pwd = "123456") public SaResult test() { System.out.println("------------进来了"); return SaResult.ok(); } } ``` 启动项目,使用浏览器访问此接口。 先来个错误的账号密码访问测试一下:[http://localhost:8081/test/test?name=sa&pwd=123](http://localhost:8081/test/test?name=sa&pwd=123) 返回结果: ``` js { "code": 500, "msg": "账号或密码错误,未通过校验", "data": null } ``` 使用正确账号密码测试访问:[http://localhost:8081/test/test?name=sa&pwd=123456](http://localhost:8081/test/test?name=sa&pwd=123456) 返回结果: ``` js { "code": 200, "msg": "ok", "data": null } ``` ### 2、使用自定义注解优化多账号鉴权 在之前的 [ 多账号鉴权 ] 章节,我们介绍了利用 “spring 注解处理器” 达到注解合并的目的,从而简化多账号体系下的注解鉴权写法。 此种方案比较简单,但是也有一些缺点。 - 1、强依赖 Spring,无法在非 Spring 环境中使用。 - 2、注解递归检查可能会造成一些性能下降。 - 3、扩展性较低,只能略微简化框架内置好的注解写法,无法灵活扩展功能。 此处我们再演示一种方案,使用自定义注解的方式达到相同的目的。 #### 2.1、首先定义注解 ``` java /** * 登录认证(User版):只有登录之后才能进入该方法 *

可标注在函数、类上(效果等同于标注在此类的所有方法上) */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckLogin { } ``` #### 2.2、定义注解处理器 ``` java /** * 注解 SaUserCheckLogin 的处理器 */ @Component public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaUserCheckLogin.class; } @Override public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) { SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE); } } ``` #### 2.3、使用新注解 接下来就可以使用我们的自定义注解了: ``` java // 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user") @SaUserCheckLogin @RequestMapping("info") public String info() { return "查询用户信息"; } ``` 注:其它注解 `@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")` 同理, 完整示例参考 Gitee 代码: [自定义注解](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation)。 ================================================ FILE: sa-token-doc/fun/dynamic-router-check.md ================================================ # 参考:把路由拦截鉴权动态化 框架提供的示例是硬代码写死的,不过稍微做一下更改,你就可以让他动态化 --- 参考如下: ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { SaRouter .match("/**") .notMatch(excludePaths()) .check(r -> StpUtil.checkLogin()); })).addPathPatterns("/**"); } // 动态获取哪些 path 可以忽略鉴权 public List excludePaths() { // 此处仅为示例,实际项目你可以写任意代码来查询这些path return Arrays.asList("/path1", "/path2", "/path3"); } ``` 如果不仅仅是登录校验,还需要鉴权,那也很简单: ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { // 遍历校验规则,依次鉴权 Map rules = getAuthRules(); for (String path : rules.keySet()) { SaRouter.match(path, () -> StpUtil.checkPermission(rules.get(path))); } })).addPathPatterns("/**"); } // 动态获取鉴权规则 public Map getAuthRules() { // key 代表要拦截的 path,value 代表需要校验的权限 Map authMap = new LinkedHashMap<>(); authMap.put("/user/**", "user"); authMap.put("/admin/**", "admin"); authMap.put("/article/**", "article"); // 更多规则 ... return authMap; } ``` --- 错误的写法: ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { StpUtil.checkLogin(); })) .addPathPatterns("/**") .excludePathPatterns(excludePaths()); } // 动态获取哪些 path 可以忽略鉴权 public List excludePaths() { // 此处仅为示例,实际项目你可以写任意代码来查询这些path return Arrays.asList("/path1", "/path2", "/path3"); } ``` 错误点:因为 lambda 表达式之外的代码只会在启动时执行一次,所以 `excludePaths()` 方法是无法做到动态化读取的, 若要在项目运行时动态读写,必须把调用 `excludePaths()` 的时机放在 lambda 里。 ================================================ FILE: sa-token-doc/fun/exception-code.md ================================================ # 异常细分状态码 --- ### 获取异常细分状态码 Sa-Token 中的基础异常类是 `SaTokenException`,在此基础上,又针对一些特定场景划分出诸如 `NotLoginException`、`NotPermissionException` 等。 但是框架中异常抛出点远远多于异常种类的划分,比如在 SSO 插件中,[ redirect 重定向地址无效 ] 和 [ ticket 参数值无效 ] 都会导致 SSO 授权的失败, 但是它们抛出的异常都是 `SaSsoException`,如果你需要对这两种异常情形做出不同的处理,仅仅判断异常的 ClassType 显然不够。 为了解决上述需求,Sa-Token 对每个异常抛出点都会指定一个特定的 code 值,就像这样: ``` java if(SaFoxUtil.isUrl(url) == false) { throw new SaSsoException("无效redirect:" + url).setCode(SaSsoErrorCode.CODE_30001); } ``` 就像是打上一个特定的标记,不同异常情形标记的 code 码值也会不同,这就为你精细化异常处理提供了前提。 要在捕获异常时获取这个 code 码也非常简单: ``` java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(SaTokenException.class) public SaResult handlerSaTokenException(SaTokenException e) { // 根据不同异常细分状态码返回不同的提示 if(e.getCode() == 30001) { return SaResult.error("redirect 重定向 url 是一个无效地址"); } if(e.getCode() == 30002) { return SaResult.error("redirect 重定向 url 不在 allowUrl 允许的范围内"); } if(e.getCode() == 30004) { return SaResult.error("提供的 ticket 是无效的"); } // 更多 code 码判断 ... // 默认的提示 return SaResult.error("服务器繁忙,请稍后重试..."); } } ``` SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说,所有异常你都可以通过 `e.getCode()` 的方式获取对应的异常细分状态码。 ### 异常细分状态码-参照表 #### sa-token-code 核心包 | code码值 | 含义 | | :-------- | :-------- | | -1 | 代表这个异常在抛出时未指定异常细分状态码 | | 10001 | 未能获取有效的上下文处理器 | | 10002 | 未能获取有效的上下文 | | 10003 | JSON 转换器未实现 | | 10011 | 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic | | 10021 | 指定的配置文件加载失败 | | 10022 | 配置文件属性无法正常读取 | | 10031 | 重置的侦听器集合不可以为空 | | 10032 | 注册的侦听器不可以为空 | | 10301 | 提供的 Same-Token 是无效的 | | 10311 | 表示未能通过 Http Basic 认证校验 | | 10321 | 提供的 HttpMethod 是无效的 | | 11001 | 未能读取到有效Token | | 11002 | 登录时的账号id值为空 | | 11003 | 更改 Token 指向的 账号Id 时,账号Id值为空 | | 11011 | 未能读取到有效Token | | 11012 | Token无效 | | 11013 | Token已过期 | | 11014 | Token已被顶下线 | | 11015 | Token已被踢下线 | | 11016 | Token已被冻结 | | 11031 | 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常 | | 11041 | 缺少指定的角色 | | 11051 | 缺少指定的权限 | | 11061 | 当前账号未通过服务封禁校验 | | 11062 | 提供要解禁的账号无效 | | 11063 | 提供要解禁的服务无效 | | 11064 | 提供要解禁的等级无效 | | 11071 | 二级认证校验未通过 | | 12001 | 请求中缺少指定的参数 | | 12002 | 构建 Cookie 时缺少 name 参数 | | 12003 | 构建 Cookie 时缺少 value 参数 | | 12101 | Base64 编码异常 | | 12102 | Base64 解码异常 | | 12103 | URL 编码异常 | | 12104 | URL 解码异常 | | 12111 | md5 加密异常 | | 12112 | sha1 加密异常 | | 12113 | sha256 加密异常 | | 12114 | AES 加密异常 | | 12115 | AES 解密异常 | | 12116 | RSA 公钥加密异常 | | 12117 | RSA 私钥加密异常 | | 12118 | RSA 公钥解密异常 | | 12119 | RSA 私钥解密异常 | | 12201 | 参与参数签名的秘钥不可为空 | | 12202 | 给定的签名无效 | | 12203 | timestamp 超出允许的范围 | #### sa-token-servlet | code码值 | 含义 | | :-------- | :-------- | | 20001 | 转发失败 | | 20002 | 重定向失败 | #### sa-token-spring-boot-starter | code码值 | 含义 | | :-------- | :-------- | | 20101 | 企图在非 Web 上下文获取 Request、Response 等对象 | | 20103 | 对象转 JSON 字符串失败 | | 20104 | JSON 字符串转 Map 失败 | | 20105 | 默认的 Filter 异常处理函数 | #### sa-token-reactor-spring-boot-starter | code码值 | 含义 | | :-------- | :-------- | | 20203 | 对象转 JSON 字符串失败 | | 20204 | JSON 字符串转 Map 失败 | | 20205 | 默认的 Filter 异常处理函数 | #### sa-token-solon-plugin | code码值 | 含义 | | :-------- | :-------- | | 20301 | 默认的拦截器异常处理函数 | | 20302 | 默认的 Filter 异常处理函数 | #### sa-token-sso 单点登录相关: | code码值 | 含义 | | :-------- | :-------- | | 30001 | `redirect` 重定向 url 是一个无效地址 | | 30002 | `redirect` 重定向 url 不在 allowUrl 允许的范围内 | | 30003 | 接口调用方提供的 `secretkey` 秘钥无效 | | 30004 | 提供的 `ticket` 是无效的 | | 30005 | 在模式三下,sso-client 调用 sso-server 端 校验ticket接口 时,得到的响应是校验失败 | | 30006 | 在模式三下,sso-client 调用 sso-server 端 单点注销接口 时,得到的响应是注销失败 | | 30007 | http 请求调用 提供的 `timestamp` 与当前时间的差距超出允许的范围 | | 30008 | http 请求调用 提供的 `sign` 无效 | | 30009 | 本地系统没有配置 `secretkey` 字段 | | 30010 | 本地系统没有配置 http 请求处理器 | | 30011 | 该 ticket 不属于当前 client | #### sa-token-oauth2 相关: | code码值 | 含义 | | :-------- | :-------- | | 30101 | client_id 不可为空 | | 30102 | scope 不可为空 | | 30103 | redirect_uri 不可为空 | | 30104 | LoginId 不可为空 | | 30105 | 无效 client_id | | 30106 | 无效 access_token | | 30107 | 无效 client_token | | 30108 | Access-Token 不具备指定的 Scope | | 30109 | Client-Token 不具备指定的 Scope | | 30110 | 无效 code 码 | | 30111 | 无效 Refresh-Token | | 30112 | 请求的 Scope 暂未签约 | | 30113 | 无效 redirect_url | | 30114 | 非法 redirect_url | | 30115 | 无效 client_secret | | 30120 | redirect_uri 不一致 | | 30122 | client_id 不一致 | | 30125 | 无效 response_type | | 30126 | 无效 grant_type | | 30127 | 无效 state | | 30141 | 系统暂未开放的授权模式 | | 30142 | 应用暂未开放的授权模式 | | 30151 | 无效的请求 Method | | 30191 | 其它异常 | #### sa-token-jwt 插件相关: | code码值 | 含义 | | :-------- | :-------- | | 30201 | 对 jwt 字符串解析失败 | | 30202 | 此 jwt 的签名无效 | | 30203 | 此 jwt 的 `loginType` 字段不符合预期 | | 30204 | 此 jwt 已超时 | | 30205 | 没有配置jwt秘钥 | | 30206 | 登录时提供的账号id为空 | #### sa-token-temp-jwt 插件相关: | code码值 | 含义 | | :-------- | :-------- | | 30301 | jwt 模式没有提供秘钥 | | 30302 | jwt 模式不可以删除 Token | | 30303 | Token已超时 | > [!WARNING| label:注意] > 部分插件因异常抛出点较少,暂未做状态码细分处理 ================================================ FILE: sa-token-doc/fun/firewall.md ================================================ # 防火墙 Sa-Token 内置防火墙组件 `SaFirewallStrategy`,用于拦截一些可能造成攻击的危险请求。 例如当前端提交的 path 为 `/test//login` 时,框架将会强制截断请求,响应以下内容: ``` txt 非法请求:/test//login ``` 因为包含双斜杠的 path 请求通常被用于鉴权绕行攻击。类似的拦截规则还有很多, `SaFirewallStrategy` 采用 hooks 机制,允许开发者自由扩展拦截规则, 框架默认具有以下 hook 拦截规则: - `SaFirewallCheckHookForWhitePath`:请求 path 白名单放行。 - `SaFirewallCheckHookForBlackPath`:请求 path 黑名单校验。 - `SaFirewallCheckHookForPathDangerCharacter`:请求 path 危险字符校。 - `SaFirewallCheckHookForPathBannedCharacter`:请求 path 禁止字符校验。 - `SaFirewallCheckHookForDirectoryTraversal`:请求 path 目录遍历符检测。 - `SaFirewallCheckHookForHost`:Host 检测。 - `SaFirewallCheckHookForHttpMethod`:请求 Method 检测。 - `SaFirewallCheckHookForHeader`:请求头检测。 - `SaFirewallCheckHookForParameter`:请求参数检测。 ### 1、默认 hook 配置: 假设我们想要增加请求 path 黑名单,可以使用如下代码: ``` java @Configuration public class SaTokenConfigure { @PostConstruct public void saTokenPostConstruct() { SaFirewallCheckHookForBlackPath.instance.resetConfig("/abc"); } } ``` 现在从浏览器访问 `/abc`,将会被防火墙组件直接拦截: ``` txt 非法请求:/abc ``` 除了 `SaFirewallCheckHookForBlackPath` 以外,其它所有 hook 均可通过此方式重载配置,在此暂不冗余演示。 ### 2、注册新的 hook 规则: 你可以使用如下代码注册新的 hook 规则: ``` java @PostConstruct public void saTokenPostConstruct() { // 注册新 hook 演示,拦截所有带有 pwd 参数的请求,拒绝响应 SaFirewallStrategy.instance.registerHook((req, res, extArg)->{ if(req.getParam("pwd") != null) { throw new FirewallCheckException("请求中不可包含 pwd 参数"); } }); } ``` 除了注册新 hook 规则,你还可以移除默认 hook ,来删减你认为不必要存在的校验规则: ``` java // 移除指定类型的 hook 验证 SaFirewallStrategy.instance.removeHook(SaFirewallCheckHookForHost.class); ``` ### 3、利用自动注入特性注册 hook 如果你的项目属于 IOC 环境(例如 SpringBoot 项目),还可以这样注册 hook: ``` java // 自定义防火墙校验 hook @Component public class SaFirewallCheckHookForXxx implements SaFirewallCheckHook { @Override public void execute(SaRequest req, SaResponse res, Object extArg) { System.out.println("----------- 自定义防火墙校验 hook "); } } ``` ### 4、指定异常处理: 被防火墙拦截的请求不会做出格式化响应,因为通常这些请求为非正常业务请求,只需阻断即可,无需前端依照响应做出页面提示。 如果你的业务切实需要对防火墙拦截做出格式化响应,可以通过以下代码完成: ``` java @PostConstruct public void saTokenPostConstruct() { // 指定防火墙校验不通过时的处理方案 SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> { System.out.println("防火墙校验不通过:" + e.getMessage()); try { HttpServletResponse response = (HttpServletResponse)res.getSource(); response.setContentType("application/json;charset=UTF-8"); String resJson = SaResult.error(e.getMessage()).toString(); response.getWriter().print(resJson); response.getWriter().flush(); } catch (IOException ex) { throw new RuntimeException(ex); } }; } ``` 浏览器将得到以下 json 格式响应: ``` js { "code": 500, "msg": "非法请求:/abc", "data": null } ================================================ FILE: sa-token-doc/fun/git-pr.md ================================================ # 如何更新在线文档 1. 打开要修改的文档页面 2. 滑动右侧页面滑块, 查看页面内容最下方, 评论区上方 3. 找到这一行文字 在线编辑提示 4. 点击Gitee或GitHub按钮中的任意一个, 国内用户推荐使用 [Gitee](https://gitee.com) (请先注册登录后再往下浏览) 5. 此时会进入当前页面源码预览页面,找到下方按钮组 按钮组 6. 点击编辑按钮 7. 此时进入待修改页面的源码页面, 按照markdown格式编辑为需要的结果(Ctrl+P可查看最终效果,再次按下可恢复源码界面) 8. 滑动到最下方点击提交审核即可 # 如何提交代码 ## 环境安装过程 1. 在本地[下载Git软件](https://pc.qq.com/detail/13/detail_22693.html)并安装 2. 配置用户名和邮件地址(Gitee或GitHub上关联的邮箱) ``` git config --global user.name "这里替换为你在项目中希望展示的昵称" git config --global user.email 这里替换为你的关联邮箱 // 查看是否配置正确 git config --list ``` 3. 为了让Gitee服务器认可你的身份,需要配置一次SSH Key, 在本地生成密匙对, 公钥上传到Gitee服务器后台 4. 具体方法见[Gitee如何配置SSH](https://gitee.com/help/articles/4181#article-header0), [Github如何配置SSH](https://docs.github.com/cn/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account) 5. 最小开发环境安装包括[Java JDK 8+](https://pc.qq.com/detail/0/detail_18360.html),[Maven 最新版](http://maven.apache.org/download.cgi) 和[idea IDE 社区版](https://www.jetbrains.com/zh-cn/idea/download/#section=windows) 6. 在idea 中[配置Java环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEjava%E7%8E%AF%E5%A2%83)和[配置maven环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEmaven%E7%8E%AF%E5%A2%83), 基础部分不再赘述 ## 项目下载过程 1. 点击[Gitee](https://gitee.com/dromara/sa-token)或[Github](https://github.com/dromara/sa-token)进入Sa-Token项目主页, 以下以Gitee为例,Github类似(请先注册登录后再往下浏览) 2. 找到页面右上角的按钮组, 点击Forked按钮 按钮组 3. 选择个人仓库并点击确认 4. 此时在你的个人仓库中会多了一个Sa-Token项目 5. 在新的Sa-Token项目中, 点击 克隆/下载 按钮, 点击弹出框里面的复制按钮 6. 在本地某空文件夹下右键选择: git bash here git bash git bash 打开后的图 14. 在里面输入如下命令, 按换行后自动下载整个项目 ``` git clone 这里替换为复制后的链接 ``` ## 项目载入过程 1. 下载结束后, 开启 idea, 选择 File->Open... 选中项目下载后的Sa-Token文件夹(Trust Project 相信此项目, 否则不可编辑) 2. 这时项目就是可编辑状态, 修改完代码并测试完成后即可提交 ## 项目暂存并提交远程 ### 方式一 1. 在idea中打开项目进入Commit选项 本地暂存 2. 勾选需要本地暂存的文件 3. 在同一页面的下方输入提示信息 提示信息 4. 点击Commit按钮暂存到本地, 点击Commit and Push按钮暂存之后提交到远程 ### 方式二 1. 除了点击Commit and Push按钮外,还有一个地方可以提交git git按钮 2. 位置在idea右上方的工具栏里面 3. 指向左下箭头为拉取项目,可以随时更新 4. 打对号为本地暂存 5. 指向右上箭头提交远程 ## 私人项目推送到主项目 1. 提交后进入Gitee个人仓库中克隆的Sa-Token项目 2. 找到下图的Pull Request按钮 工具栏 3. 点击提交, 进入如下页面 提交信息填写页面 4. 在这里,你可以选择要提交的分支,一般都是dev开发分支.可以填写合并信息,其他测试审查之类的可以不填写, 最后点击创建即可完成一次提交. ## 远程项目更新 1. 有时候主项目更新了,之前克隆的项目代码陈旧,如何处理? 2. 在个人仓库的Sa-Token项目主页面中, 找到下图的圆圈 更新按钮 3. 点击右侧圆圈按钮后Gitee会自动同步主项目, 这样就不用像我之前一样,删除项目又重新fork了. ## 为什么在国内推荐Gitee 1. 近期Github下载网速较慢 2. Gitee上中文界面方便操作 ================================================ FILE: sa-token-doc/fun/issue-template.md ================================================ # issue 提问模板 在线提问链接:[Gitee issue](https://gitee.com/dromara/sa-token/issues)、[GitHub issue](https://github.com/dromara/sa-token/issues) > [!TIP| label:请在新建 issue 时,尽量复制模板格式进行提交] > 1. 提交之前率先参考 Sa-Token 常见问题解答 以及善用 Gitee issues 搜索功能,查阅问题是否已有答案,已存在的 issue 就不要再重复提交了。 > 2. 问题已得到处理的 issue 请大家及时手动关闭,如果超过24小时没有追问,我们将默认提交者已找到解决方案,关闭issue。 > 3. 有时候 issue 提交之后,没有得到及时回复,大家可以加入QQ群@管理员寻求帮助。 > 4. 请大家新建 issue 时删除不必要的模板信息、精简语句、**做好代码排版**,对于不方便描述的业务场景,可参阅 Sa-Token 名词解释 方便组织语句,这样有助于减低大家的沟通成本。 > 5. **代码截图要带上行号!报错信息要把异常堆栈截全!页面截图要把地址栏带上!Ajax请求要把请求地址、请求头、请求参数都截全!** --- ### 预期不符: ``` js ### 使用版本: ### 涉及的功能模块: ### 测试步骤: + 我经过以下步骤测试: + 得出以下结果: + 其中第 xx 行的代码输出表现 和文档上描述的不一致: + 我的理解是: 请问,是我的理解不对,还是文档出了问题? ``` ### bug反馈: ``` js ### 使用版本: ### 报错信息: ### 希望结果: ### 复现步骤: < 备注:如果复现步骤比较复杂,请将 demo 上传到 gitee 并留下地址 > ``` ### 功能提问: ``` js ### 对以下问题有疑问: < 备注:请尽量详细描述问题所在 > ``` ### 建议增加新功能: ``` js ### 建议增加的新功能: ### 应用场景阐述: < 备注:请尽量详细描述功能应用场景 > ``` ### 踩坑记录: ``` js ### 遇到的问题: ### 解决方案: < 备注:请尽量描述详细一点,为后人提供清晰的排查思路,人人为我,我为人人 > ``` ================================================ FILE: sa-token-doc/fun/jur-cache.md ================================================ # 参考:将权限数据放在缓存里 前面我们讲解了如何通过`StpInterface`接口注入权限数据,框架默认是不提供缓存能力的,如果你想减小数据库的访问压力,则需要将权限数据放到缓存中 --- 参考如下: ``` java /** * 自定义权限验证接口扩展 */ @Component public class StpInterfaceImpl implements StpInterface { // 返回一个账号所拥有的权限码集合 @Override @SuppressWarnings("unchecked") public List getPermissionList(Object loginId, String loginType) { // 1. 声明权限码集合 List list = new ArrayList<>(); // 2. 遍历角色列表,查询拥有的权限码 for (String roleId : getRoleList(loginId, loginType)) { List permissionList = (List)SaManager.getSaTokenDao().getObject("satoken:role-find-permission:" + roleId); if(permissionList == null) { // 从数据库查询这个角色 id 所拥有的权限列表 permissionList = ... // 查好后,set 到缓存中 SaManager.getSaTokenDao().setObject("satoken:role-find-permission:" + roleId, permissionList, 60 * 60 * 24 * 30); } list.addAll(permissionList); } // 3. 返回权限码集合 return list; } // 返回一个账号所拥有的角色标识集合 @Override @SuppressWarnings("unchecked") public List getRoleList(Object loginId, String loginType) { List roleList = (List)SaManager.getSaTokenDao().getObject("satoken:loginId-find-role:" + loginId); if(roleList == null) { // 从数据库查询这个账号id拥有的角色列表, roleList = ... // 查好后,set 到缓存中 SaManager.getSaTokenDao().setObject("satoken:loginId-find-role:" + loginId, roleList, 60 * 60 * 24 * 30); } return roleList; } } ``` ##### 疑问:为什么不直接缓存 `[账号id->权限列表]`的关系,而是 `[账号id -> 角色id -> 权限列表]`? 答:`[账号id->权限列表]`的缓存方式虽然更加直接粗暴,却有一个严重的问题: - 通常我们系统的权限架构是RBAC模型:权限与用户没有直接的关系,而是:用户拥有指定的角色,角色再拥有指定的权限 - 而这种'拥有关系'是动态的,是可以随时修改的,一旦我们修改了它们的对应关系,便要同步修改或清除对应的缓存数据 现在假设如下业务场景:我们系统中有十万个账号属于同一个角色,当我们变动这个角色的权限时,难道我们要同时清除这十万个账号的缓存信息吗? 这显然是一个不合理的操作,同一时间缓存大量清除容易引起Redis的缓存雪崩 而当我们采用 `[账号id -> 角色id -> 权限列表]` 的缓存模型时,则只需要清除或修改 `[角色id -> 权限列表]` 一条缓存即可 一言以蔽之:权限的缓存模型需要跟着权限模型走,角色缓存亦然 ================================================ FILE: sa-token-doc/fun/log.md ================================================ # 参考:全局 Log 输出 --- ### 打开全局日志输出 以下配置可以打开全局日志输出: ``` yaml sa-token: # 是否输出操作日志 is-log: true ``` ``` properties # 是否输出操作日志 sa-token.is-log=true ``` 此配置项打开之后,框架将会在账号登录、注销、二级认证 等关键性步骤打印日志,以方便项目开发调试。 框架默认将日志信息打印到控制台,如果需要将日志输出到其它地方,你可以重写 SaLog 对象,例如以下代码将会把日志转接到 Slf4j 下: ``` java /** * 将 Sa-Token log 信息转接到 Slf4j */ @Component public class SaLogForSlf4j implements SaLog { Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class); @Override public void trace(String str, Object... args) { log.trace(str, args); } @Override public void debug(String str, Object... args) { log.debug(str, args); } @Override public void info(String str, Object... args) { log.info(str, args); } @Override public void warn(String str, Object... args) { log.warn(str, args); } @Override public void error(String str, Object... args) { log.error(str, args); } @Override public void fatal(String str, Object... args) { log.error(str, args); } } ``` 重新启动项目,观察日志打印变化。 ### 增加API访问日志 手动增加 API 请求日志信息,这将非常有助于你调试代码,例如: ``` java @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/**") .addExclude("/favicon.ico") .setAuth(obj -> { // 输出 API 请求日志,方便调试代码 SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue()); // 其它校验代码... }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) ; } ``` ================================================ FILE: sa-token-doc/fun/not-login-scene.md ================================================ # NotLoginException 场景值 本篇介绍如何根据`NotLoginException`异常的场景值,来定制化处理未登录的逻辑
应用场景举例:未登录、被顶下线、被踢下线等场景需要不同方式来处理 ## 何为场景值 在前面的章节中,我们了解到,在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常,而同为未登录异常却有五种抛出场景的区分 | 场景值 | 对应常量 | 含义说明 | |--- |--- |--- | | -1 | NotLoginException.NOT_TOKEN | 未能从请求中读取到有效 token | | -2 | NotLoginException.INVALID_TOKEN | 已读取到 token,但是 token 无效 | | -3 | NotLoginException.TOKEN_TIMEOUT | 已读取到 token,但是 token 已经过期 ([详](/fun/token-timeout)) | | -4 | NotLoginException.BE_REPLACED | 已读取到 token,但是 token 已被顶下线 | | -5 | NotLoginException.KICK_OUT | 已读取到 token,但是 token 已被踢下线 | | -6 | NotLoginException.TOKEN_FREEZE | 已读取到 token,但是 token 已被冻结 | | -7 | NotLoginException.NO_PREFIX | 未按照指定前缀提交 token | 那么,如何获取场景值呢?废话少说直接上代码: ``` java // 全局异常拦截(拦截项目中的NotLoginException异常) @ExceptionHandler(NotLoginException.class) public SaResult handlerNotLoginException(NotLoginException nle) throws Exception { // 打印堆栈,以供调试 nle.printStackTrace(); // 判断场景值,定制化异常信息 String message = ""; if(nle.getType().equals(NotLoginException.NOT_TOKEN)) { message = "未能读取到有效 token"; } else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) { message = "token 无效"; } else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { message = "token 已过期"; } else if(nle.getType().equals(NotLoginException.BE_REPLACED)) { message = "token 已被顶下线"; } else if(nle.getType().equals(NotLoginException.KICK_OUT)) { message = "token 已被踢下线"; } else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) { message = "token 已被冻结"; } else if(nle.getType().equals(NotLoginException.NO_PREFIX)) { message = "未按照指定前缀提交 token"; } else { message = "当前会话未登录"; } // 返回给前端 return SaResult.error(message); } ```
注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理 ================================================ FILE: sa-token-doc/fun/plugin-dev.md ================================================ # Sa-Token 插件开发指南 插件,从字面意思理解就是可拔插的组件,作用是在不改变 Sa-Token 现有架构的情况下,替换或扩展一部分底层代码逻辑。 --- ## 1、插件开发 为 Sa-Token 开发插件非常简单,以下是几种可行的方式: - 1、自定义全局策略。 - 2、更改全局组件实现。 - 3、实现自定义SaTokenContext。 - 4、其它自由扩展。 下面依次介绍这几种方式。 ### 方式1:自定义全局策略 Sa-Token 将框架的一些关键逻辑抽象出一个统一的概念 —— 策略,并统一定义在 `SaStrategy` 中,源码参考:[SaStrategy](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java) 。 SaStrategy 的每一个函数都可以单独重写,以 “自定义Token生成策略” 这一需求为例: ``` java // 重写 Token 生成策略 SaStrategy.instance.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); // 随机60位长度字符串 }; ``` 就像变量的重新赋值一样,你只需重新指定一个新的策略函数,即可自定义 Token 生成的逻辑。 ### 方式2:更改全局组件实现 你可以找到不符合你需求的组件,重新定义一个子类,以 临时令牌认证 模块为例,你需要自定义 `SaTempTemplate` 的子类: ``` java /** * 临时认证模块 自定义子类实现 */ @Component public class MySaTempTemplate extends SaTempTemplate { @Override public String createToken(Object value, long timeout, boolean isRecordIndex) { System.out.println("------- 自定义一些逻辑 createToken "); return super.createToken(value, timeout, isRecordIndex); } @Override public Object parseToken(String token) { System.out.println("------- 自定义一些逻辑 parseToken "); return super.parseToken(token); } } ``` ### 方式3:实现自定义SaTokenContext SaTokenContext 是对接不同框架的上下文接口,篇幅限制,可参考:[自定义 SaTokenContext 指南](/fun/sa-token-context) ### 方式4:其它自由扩展 这种方式就无需注入什么全局组件替换内部实现了,你可以在 Sa-Token 的基础之上封装任何代码,进行功能扩展。 ## 2、插件注册 在你完成插件开发之后,你还需要考虑一个问题,如何让插件代码注入到项目中。 首先这里需要分两种情况: - 情况1:只打算自己的项目使用这个插件。 - 情况2:准备提交 pr 到 Sa-Token 仓库,让更多人使用。 ### 情况1:只打算自己的项目使用这个插件 这种情况比较简单,如果是 SpringBoot 项目,你可以在自定义插件类上添加注解 `@Component`: ``` java @Component public class MySaTempTemplate extends SaTempTemplate { // ... } ``` 这样在项目启动时, sa-token-spring-boot-starter 集成包将会扫描到这个自定义组件,注入到框架中。 如果是重写全局策略的代码,也可以通过 `@PostConstruct` 注解做到项目启动时自动执行: ``` java @PostConstruct public void rewriteSaStrategy() { // 重写 token 生成策略 SaStrategy.instance.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); }; } ``` 如果是非 SpringBoot 项目,项目环境无法做到自动注入,保底的方案是在 main 方法中,手动注册组件: ``` java public static void main(String[] args) { // 示例:手动替换 Sa-Token 内部组件 // Sa-Token 大部分全局组件都定义在 SaManager 之上,参考:https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java SaManager.setSaTempTemplate(new MySaTempTemplate()); // 示例:手动重写 Sa-Token 全局策略 SaStrategy.instance.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); }; } ``` ### 情况2:准备提交 pr 到 Sa-Token 仓库,让更多人使用。 这种情况稍微复杂一些,因为你基本上很难:通过在插件内部写一些代码,帮助“插件使用者”注册插件到项目中。 一种解决方案是:难办,那就别办了。 对,就是你只负责开发相对应的自定义组件,而将自定义组件的注册过程完全交给使用者,这并不是妥协的选择,反而会给插件使用者更大的自由度, sa-token-jwt、sa-token-thymeleaf 等官方插件都是这样做的。 如果你觉得还是完成插件的自动注入比较好,也是有办法的,那就是利用 SPI 机制来注册组件。 (关于 java SPI 机制,网上教程众多,此处暂不详细介绍,不熟悉的同学可以直接向 deepseek 等 AI 工具提问,给你讲的明明白白的) 你需要考虑一点:这个插件是专门给 SpringBoot 项目使用的,还是面向 Solon、JFinal 等任意项目使用: #### 如果是:SpringBoot 专用插件 如果这个插件只打算给 SpringBoot 项目使用,可以利用 SpringBoot 的 SPI 机制注册插件 SpringBoot2 格式:创建 `resources\META-INF\spring.factories` 文件: ``` txt org.springframework.boot.autoconfigure.EnableAutoConfiguration=插件完全限定名 ``` SpringBoot3 格式:创建 `resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件: ``` txt 插件完全限定名 ``` 这样在别人引入此插件时,便会根据 SPI 文件指定的地址去加载插件类,做到插件引入即注册的效果。 #### 如果是:通用型插件 通用型插件则不能使用 SpringBoot 的 SPI 机制去注册组件,因为其它项目是无法识别 SpringBoot SPI 文件的, 好在 Sa-Token 提供了自己的 SPI 机制,所有环境均可使用: 1、新建 `SaTokenPluginForXxx` 类,此类需要 `implements SaTokenPlugin` 接口,并且推荐定义在 `cn.dev33.satoken.plugin` 下: ``` java /** * SaToken 插件安装:插件作用描述 */ public class SaTokenPluginForXxx implements SaTokenPlugin { @Override public void install() { // 书写需要在项目启动时执行的代码,例如: // SaManager.setXxx(new SaXxxForXxx()); } } ``` 2、新建 `resources\META-INF\satoken\cn.dev33.satoken.plugin.SaTokenPlugin` 文件,填写上插件类的完全限定名地址 ``` txt cn.dev33.satoken.plugin.SaTokenPluginForXxx ``` 这样便可以在项目启动时,被 Sa-Token 插件管理器加载到此插件,执行自定义 `SaTokenPluginForXxx` 实现类的 `install` 方法,完成插件安装。 ## 3、练练手 学废了吗?给你出个题练练手: 开发一个 `sa-token-hutool-json` 插件,要求引入该插件后,自动替换掉 Sa-Token 的 json 序列化方案为 hutool-json 模块。 如果没有思路,可以参考一下 `sa-token-fastjson` 的插件源码实现哦。 ================================================ FILE: sa-token-doc/fun/refer-info.md ================================================ # 参考资料 记录 Sa-Token 框架开发中参考过的一些资料。(由 2025-3-1 开始整理) - Sa-Token对url过滤不全存在的风险点 - https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA - SpringBoot 热拔插 AOP 组件: - https://www.jb51.net/program/297714rev.htm - https://www.bilibili.com/video/BV1WZ421W7Qx - https://blog.csdn.net/Tomwildboar/article/details/139199801 - 单元测试 - https://www.cnblogs.com/flypig666/p/11505277.html ================================================ FILE: sa-token-doc/fun/sa-token-context--backup.md ================================================ # 自定义 SaTokenContext 指南 目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包, 如果我们使用的 Web 框架不在上述列表之中,则需要自定义 SaTokenContext 接口的实现完成整合工作。 --- ### 1、SaTokenContext是什么,为什么要实现 SaTokenContext 接口? 在鉴权中,必不可少的步骤就是从 `HttpServletRequest` 中读取 Token,然而并不是所有框架都具有 HttpServletRequest 对象,例如在 WebFlux 中,只有 `ServerHttpRequest`, 在一些其它Web框架中,可能连 `Request` 的概念都没有。 那么,Sa-Token 如何只用一套代码就对接到所有 Web 框架呢? 解决这个问题的关键就在于 `SaTokenContext` 接口,此接口的作用是屏蔽掉不同 Web 框架之间的差异,提供统一的调用API: sa-token-context SaTokenContext只是一个接口,没有工作能力,这也就意味着 SaTokenContext 接口的实现是必须的。 那么疑问来了,我们之前在 SpringBoot 中引用 Sa-Token 时为什么可以直接使用呢? 其实原理很简单,`sa-token-spring-boot-starter`集成包中已经内置了`SaTokenContext`的实现:[SaTokenContextForSpring](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java), 并且根据 Spring 的自动注入特性,在项目启动时注入到 Sa-Token 中,做到“开箱即用”。 那么如果我们使用不是 Spring 框架,是不是就必须得手动实现 `SaTokenContext` 接口?答案是肯定的,脱离Spring 环境后,我们就不能再使用`sa-token-spring-boot-starter`集成包了, 此时我们只能引入 `sa-token-core` 核心包,然后手动实现 `SaTokenContext` 接口。 不过不用怕,这个工作很简单,只要跟着下面的文档一步步来,你就可以将 Sa-Token 对接到任意Web框架中。 ### 2、实现 Model 接口 我们先来观察一下 `SaTokenContext` 接口的签名: ``` java /** * Sa-Token 上下文处理器 */ public interface SaTokenContext { /** * 获取当前请求的 [Request] 对象 */ public SaRequest getRequest(); /** * 获取当前请求的 [Response] 对象 */ public SaResponse getResponse(); /** * 获取当前请求的 [存储器] 对象 */ public SaStorage getStorage(); /** * 校验指定路由匹配符是否可以匹配成功指定路径 */ public boolean matchPath(String pattern, String path); } ``` 你可能对 `SaRequest` 比较疑惑,这个对象是干什么用的?正如每个 Web 框架都有 Request 概念的抽象,Sa-Token 也封装了 `Request`、`Response`、`Storage`三者的抽象: - `Request`:请求对象,携带着一次请求的所有参数数据。参考:[SaRequest.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java)。 - `Response`:响应对象,携带着对客户端一次响应的所有数据。参考:[SaResponse.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java)。 - `Storage`:请求上下文对象,提供 [一次请求范围内] 的上下文数据读写。参考:[SaStorage.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java)。 因此,在实现 `SaTokenContext` 之前,你必须先实现这三个 Model 接口。 先别着急动手,如果你的 Web 框架是基于 Servlet 规范开发的,那么 Sa-Token 已经为你封装好了三个 Model 接口的实现,你要做的就是引入 `sa-token-servlet`包即可: ``` xml cn.dev33 sa-token-servlet ${sa.top.version} ``` ``` gradle // Sa-Token 权限认证(ServletAPI 集成包) implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' ``` 如果你的 Web 框架不是基于 Servlet 规范,那么你就需要手动实现这三个 Model 接口,我们可以参考 `sa-token-servlet` 是怎样实现的: [SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、 [SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、 [SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。 ### 3、实现 SaTokenContext 接口 接下来我们奔入主题,提供 `SaTokenContext` 接口的实现,同样我们可以参考 Spring 集成包是怎样实现的: ``` java /** * Sa-Token 上下文处理器 [ SpringMVC版本实现 ] */ public class SaTokenContextForSpring implements SaTokenContext { /** * 获取当前请求的Request对象 */ @Override public SaRequest getRequest() { return new SaRequestForServlet(SpringMVCUtil.getRequest()); } /** * 获取当前请求的Response对象 */ @Override public SaResponse getResponse() { return new SaResponseForServlet(SpringMVCUtil.getResponse()); } /** * 获取当前请求的 [存储器] 对象 */ @Override public SaStorage getStorage() { return new SaStorageForServlet(SpringMVCUtil.getRequest()); } /** * 校验指定路由匹配符是否可以匹配成功指定路径 */ @Override public boolean matchPath(String pattern, String path) { return SaPathMatcherHolder.getPathMatcher().match(pattern, path); } } ``` 详细参考: [SaTokenContextForSpring.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java) ### 4、将自定义实现注入到 Sa-Token 框架中 有了 `SaTokenContext` 接口的实现,我们还需要将这个实现类注入到 Sa-Token 之中,伪代码参考如下: ``` java /** * 程序启动类 */ public class Application { public static void main(String[] args) { // 框架启动 XxxApplication.run(xxx); // 将自定义的 SaTokenContext 实现类注入到框架中 SaTokenContext saTokenContext = new SaTokenContextForXxx(); SaManager.setSaTokenContext(saTokenContext); } } ``` 如果你使用的框架带有自动注入特性,那就更简单了,参考 Spring 集成包的 Bean 注入流程: [注册Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java)、 [注入Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java) ### 5、启动项目 启动项目,尝试打印一下 `SaManager.getSaTokenContext()` 对象,如果输出的是你的自定义实现类,那就证明你已经自定义 `SaTokenContext` 成功了, 快来体验一下 Sa-Token 的各种功能吧。 ================================================ FILE: sa-token-doc/fun/sa-token-context.md ================================================ # 自定义 SaTokenContext 指南 目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包, 如果我们使用的 Web 框架不在上述列表之中,则需要自定义 SaTokenContext 相关接口完成整合工作。 我们需要关注的主要就是四个接口: - SaTokenContext:上下文管理器。 - SaRequest:请求对象,携带着一次请求的所有参数数据。 - SaResponse:响应对象,携带着对客户端一次响应的所有数据。 - SaStorage:请求上下文对象,提供 [一次请求范围内] 的上下文数据读写。 --- ### 上下文包装类 在鉴权中,必不可少的步骤就是从 `HttpServletRequest` 中读取 Token,然而当我们调用 `StpUtil.isLogin()` 获取当前会话是否登录时, 我们并没有传递 `HttpServletRequest` 参数,框架是怎么读取出来 Token 的呢? 以 SpringBoot 项目为例,Sa-Token 框架会自动注册一个全局过滤器,在每次接收到请求时,将 `HttpServletRequest` 对象保存在 `ThreadLocal` 之中。 在后续的方法中,如果你调用了 `StpUtil.isLogin()` 等方法,框架便会从 `ThreadLocal` 中获取 `HttpServletRequest` 对象,从而进一步读取 token 等信息。 让我们来看一下具体的代码细节,全局上下文初始化过滤器: ``` java /** * SaTokenContext 上下文初始化过滤器 (基于 Servlet) */ @Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) public class SaTokenContextFilterForServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { SaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response); chain.doFilter(request, response); } finally { SaTokenContextServletUtil.clearContext(); } } } ``` 进一步追踪 `SaTokenContextServletUtil.setContext` 方法: ``` java public static void setContext(HttpServletRequest request, HttpServletResponse response) { SaRequest req = new SaRequestForServlet(request); SaResponse res = new SaResponseForServlet(response); SaStorage stg = new SaStorageForServlet(request); SaManager.getSaTokenContext().setContext(req, res, stg); } ``` 此处有一个细节,为什么保存的不是原生 `HttpServletRequest` 与 `HttpServletResponse`,而是 `SaRequest`、`SaResponse`、`SaStorage` 三个包装对象? 因为并不是所有的 web 框架都具有 `HttpServletRequest` 对象,例如在 WebFlux 中,只有 `ServerHttpRequest`, 在一些其它Web框架中,可能连 `Request` 的概念都没有。 Sa-Token 为了一套代码对接所有的 Web 框架,就在原生请求对象的基础上又封装了一层 `SaTokenContext` 相关接口,用于屏蔽掉不同 Web 框架之间的差异,提供统一的调用API: sa-token-context 因此,要对接不同的 Web 框架,就要针对不同的 Web 框架封装不同版本的 `SaRequest`、`SaResponse`、`SaStorage` 包装类对象。 如果你的 Web 框架是基于 Servlet 规范开发的,那么你可以直接引入 `sa-token-servlet`,这个包封装了针对 Servlet 规范的上下文包装类对象: ``` xml cn.dev33 sa-token-servlet ${sa.top.version} ``` ``` gradle // Sa-Token 权限认证(ServletAPI 集成包) implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' ``` 如果你的 web 框架不是基于 Servlet 规范开发的,也问题不大,手动实现一下即可,参考一下 Servlet 包是怎么做的: [SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、 [SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、 [SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。 封装好包装类对象之后,接下来要做的就是在这个 Web 框架中注册一个全局过滤器,将包装类对象保存到“全局上下文管理器”之中,以备调用: ``` java SaRequest req = new SaRequestForXxx(request); SaResponse res = new SaResponseForXxx(response); SaStorage stg = new SaStorageForXxx(request); SaManager.getSaTokenContext().setContext(req, res, stg); ``` 这样我们即可在具体的 Controller 请求中,成功调用 `StpUtil.isLogin()` 的 API。 总结:整体的步骤并不复杂,就是先定义 `SaRequest`、`SaResponse`、`SaStorage` 的包装类,然后在全局过滤器保存在上下文管理器中。 可以参考具体实现 `sa-token-spring-boot-starter`(SpringBoot2 项目 starter 包): [SaTokenContextFilterForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java) ### 旧版本方案 在旧版本中(< v1.42.0)我们推荐的方案是自定义整个 `SaTokenCentext` 接口,目前此方案在新版本已不推荐,此处仅做留档备份:[自定义 SaTokenContext 指南](/fun/sa-token-context--backup.md) ================================================ FILE: sa-token-doc/fun/sa-token-test.md ================================================ # Sa-Token框架掌握度--在线考试 --- 此份考卷将测评您对Sa-Token框架的掌握程度(满分100),链接:[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx) ================================================ FILE: sa-token-doc/fun/session-model.md ================================================ # Sa-Token 中的 Session会话 模型详解 --- ### 1、Account-Session 提起Session,你脑海中最先浮现的可能就是 JSP 中的 HttpSession,它的工作原理可以大致总结为: 客户端每次与服务器第一次握手时,会被强制分配一个 `[唯一id]` 作为身份标识,注入到 Cookie 之中, 之后每次发起请求时,客户端都要将它提交到后台,服务器根据 `[唯一id]` 找到每个请求专属的Session对象,维持会话 这种机制简单粗暴,却有N多明显的缺点: 1. 同一账号分别在PC、APP登录,会被识别为两个不相干的会话 2. 一个设备难以同时登录两个账号 3. 每次一个新的客户端访问服务器时,都会产生一个新的Session对象,即使这个客户端只访问了一次页面 4. 在不支持Cookie的客户端下,这种机制会失效 Sa-Token Session可以理解为 HttpSession 的升级版: 1. Sa-Token只在调用`StpUtil.login(id)`登录会话时才会产生Session,不会为每个陌生会话都产生Session,节省性能 2. 在登录时产生的Session,是分配给账号id的,而不是分配给指定客户端的,也就是说在PC、APP上登录的同一账号所得到的Session也是同一个,所以两端可以非常轻松的同步数据 3. Sa-Token支持Cookie、Header、body三个途径提交Token,而不是仅限于Cookie 4. 由于不强依赖Cookie,所以只要将Token存储到不同的地方,便可以做到一个客户端同时登录多个账号 这种为账号id分配的Session,我们给它起一个合适的名字:`Account-Session`,你可以通过如下方式操作它: ``` java // 获取当前会话的 Account-Session SaSession session = StpUtil.getSession(); // 从 Account-Session 中读取、写入数据 session.get("name"); session.set("name", "张三"); ``` 使用`Account-Session`在不同端同步数据是非常方便的,因为只要 PC 和 APP 登录的账号id一致,它们对应的都是同一个Session, 举个应用场景:在PC端点赞的帖子列表,在APP端的点赞记录里也要同步显示出来 ### 2、Token-Session 随着业务推进,我们还可能会遇到一些需要数据隔离的场景: > [!NOTE| label:业务场景] > 指定客户端超过两小时无操作就自动下线,如果两小时内有操作,就再续期两小时,直到新的两小时无操作 那么这种请求访问记录应该存储在哪里呢?放在 Account-Session 里吗? 可别忘了,PC端和APP端可是共享的同一个 Account-Session ,如果把数据放在这里, 那就意味着,即使用户在PC端一直无操作,只要手机上用户还在不间断的操作,那PC端也不会过期! 解决这个问题的关键在于,虽然两个设备登录的是同一账号,但是两个它们得到的token是不一样的, Sa-Token针对会话登录,不仅为账号id分配了`Account-Session`,同时还为每个token分配了不同的`Token-Session` 不同的设备端,哪怕登录了同一账号,只要它们得到的token不一致,它们对应的 `Token-Session` 就不一致,这就为我们不同端的独立数据读写提供了支持: ``` java // 获取当前会话的 Token-Session SaSession session = StpUtil.getTokenSession(); // 从 Token-Session 中读取、写入数据 session.get("name"); session.set("name", "张三"); ``` ### 3、Custom-Session 除了以上两种Session,Sa-Token还提供了第三种Session,那就是:`Custom-Session`,你可以将其理解为:自定义Session Custom-Session不依赖特定的 账号id 或者 token,而是依赖于你提供的SessionId: ``` java // 获取指定key的 Custom-Session SaSession session = SaSessionCustomUtil.getSessionById("goods-10001"); // 从 Custom-Session 中读取、写入数据 session.get("name"); session.set("name", "张三"); ``` 只要两个自定义Session的Id一致,它们就是同一个Session Custom-Session的会话有效期默认使用`SaManager.getConfig().getTimeout()`, 如果需要修改会话有效期, 可以在创建之后, 使用对象方法修改 ``` java session.updateTimeout(1000); // 参数说明和全局有效期保持一致 ``` ### 4、Session模型结构图 三种Session创建时机: - `Account-Session`: 指的是框架为每个 账号id 分配的 Session - `Token-Session`: 指的是框架为每个 token 分配的 Session - `Custom-Session`: 指的是以一个 特定的值 作为SessionId,来分配的 Session **假设三个客户端登录同一账号,且配置了不共享token,那么此时的Session模型是:** session-model 简而言之: - `Account-Session` 以账号 id 为主,只要 token 指向的账号 id 一致,那么对应的Session对象就一致 - `Token-Session` 以token为主,只要token不同,那么对应的Session对象就不同 - `Custom-Session` 以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对象 ================================================ FILE: sa-token-doc/fun/sso-vs-oauth2.md ================================================ # 技术选型:[ 单点登录 ] VS [ OAuth2.0 ] --- QQ群里经常有小伙伴提问:项目需要搭建统一认证中心,是用 SSO 方便还是 OAuth2.0 方便呢?针对这个问题,我们列出两者的主要区别以供大家参考: | 功能点 | SSO单点登录 | OAuth2.0 | | :-------- | :-------- | :-------- | | 统一认证 | 支持度高 | 支持度高 | | 统一注销 | 支持度高 | 支持度低 | | 多个系统会话一致性 | 强一致 | 弱一致 | | 第三方应用授权管理 | 不支持 | 支持度高 | | 自有系统授权管理 | 支持度高 | 支持度低 | | Client级的权限校验 | 不支持 | 支持度高 | | 集成简易度 | 比较简单 | 难度中等 | | 适合项目 | 企业内部项目整合 | 企业搭建统一认证授权平台,对外开放服务 | 注:以上仅为在 Sa-Token 中两种技术的差异度比较,不同框架的实现可能略有差异,但整体思想是一致的。 ================================================ FILE: sa-token-doc/fun/team.md ================================================ # 团队成员 ### 开发 负责:代码开发、社区维护、issue 处理、pr 审核等。
头像 昵称 个人主页
小风筝(作者) https://gitee.com/click33
AppleOfGray https://gitee.com/appleOfGray
ly-chn https://gitee.com/ly-chn
### 提案讨论组成员 负责:提案新增、讨论、投票。
头像 昵称 个人主页
刘潇 https://gitee.com/click33
AppleOfGray https://gitee.com/appleOfGray
ly-chn https://gitee.com/ly-chn
茉莉 https://gitee.com/kidoldman
药水 https://gitee.com/java_pioneer
呆某人 https://gitee.com/zhubj0510
春困夏倦秋乏 https://gitee.com/uncarbon97
淡墨 https://gitee.com/jinan-jimeng-network_0
================================================ FILE: sa-token-doc/fun/tech-stack.md ================================================ # Sa-Token 源码用到的所有技术栈 包括但不限于以下: - Maven多模块项目 - Servlet API、临时Cookie与永久Cookie、Request参数获取 - SpringBoot2.0、Redis、Jackson、Hutool、jwt - SpringBoot自定义starter、Spring包扫描 + 依赖注入、AOP注解切面、yml配置映射、拦截器 - Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程 - package-info注释、Serializable序列化接口、synchronized锁 - java加密算法:MD5、SHA1、SHA256、AES、RSA - OAuth2.0、同域单点登录、集群与分布式、路由Ant匹配 ================================================ FILE: sa-token-doc/fun/three-scope.md ================================================ # 三大作用域 --- Sa-Token 数据存储有三大作用域,分别是: - `SaStorage` - 请求作用域:存储的数据只在一次请求内有效。 - `SaSession` - 会话作用域:存储的数据在一次会话范围内有效。 - `SaApplication` - 全局作用域:存储的数据在全局范围内有效。 ### SaStorage - 请求作用域 在 SaStorage 中存储的数据只在一次请求范围内有效,请求结束后数据自动清除。使用 SaStorage 时无需处于登录状态。 ``` java SaStorage storage = SaHolder.getStorage(); storage.get("key"); // 取值 storage.set("key", "value"); // 写值 storage.delete("key"); // 删值 ``` ### SaSession - 会话作用域 在 SaSession 存储的数据在一次会话范围内有效,会话结束后数据自动清除。必须登录后才能使用 SaSession 对象。 ``` java SaSession session = StpUtil.getSession(); session.get("key"); // 取值 session.set("key", "value"); // 写值 session.delete("key"); // 删值 ``` ### SaApplication - 全局作用域 在 SaApplication 存储的数据在全局范围内有效,应用关闭后数据自动清除(如果集成了 Redis 那则是 Redis 关闭后数据自动清除)。使用 SaApplication 时无需处于登录状态。 ``` java SaApplication application = SaHolder.getApplication(); application.get("key"); // 取值 application.set("key", "value"); // 写值 application.delete("key"); // 删值 ``` ================================================ FILE: sa-token-doc/fun/timeline.md ================================================ # Sa-Token 开源大事记 --- - **2020-02-04:** 在 GitHub 提交第一个版本,正式开源。 - **2020-09-14:** GitHub star 数量破 100。 - **2020-10-26:** Gitee star 数量破 100。 - **2021-03-01:** 被 [HelloGitHub] 第 59 期收录推荐。 - **2021-03-26:** GitHub star 数量破 1k。 - **2021-03-30:** 受 TLog 作者邀请,Sa-Token 加入 dromara 社区。 - **2021-03-30:** 被 Gitee 列为推荐项目。 - **2021-03-31:** Gitee star 数量破1K。 - **2021-06-17:** Sa-Token star 数量 (3529) 超过 Shiro (3506)。 - **2021-07-26:** GitHub star 数量破5K。 - **2021-09-24:** Sa-Token star 数量 (6280) 超过 SpringSecurity (6247)。 - **2021-11-08:** 荣获开源中国 “码云 GVP 认证”。 - **2021-11-28:** Gitee star 数量破5K。 - **2021-12-27:** 荣获 OSC-2021 最佳软件 Top 30。 - **2022-05-20:** 成为 [可信开源社区共同体] 预备成员。 - **2022-08-01:** 加入 [中国开源社区 landscape]。 - **2022-08-18:** GitHub 第 10000 个 star 里程碑! - **2023-01-09:** 荣获 OSC 2022 年度最热开源项目社区。 - **2023-11-21:** 被评为“开放原子基金会2023快速成长开源项目”。 - **2024-04-25:** 42.9k star 登顶 Gitee 开源项目推荐榜 Top 1。 - **2024-08-19:** 成为 GitCode G-Star 开源摘星计划毕业项目。 - **2024-11-22:** 所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。 ================================================ FILE: sa-token-doc/fun/token-info.md ================================================ # SaTokenInfo 参数详解 token信息Model: 用来描述一个token的常用参数 ``` js { "code": 200, "msg": "ok", "data": { "tokenName": "satoken", // token名称 "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值 "isLogin": true, // 此token是否已经登录 "loginId": "10001", // 此token对应的LoginId,未登录时为null "loginType": "login", // 账号类型标识 "tokenTimeout": 2591977, // token剩余有效期 (单位: 秒) "sessionTimeout": 2591977, // Account-Session剩余有效时间 (单位: 秒) "tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存) "tokenActiveTimeout": -1, // token 距离被冻结还剩的时间 (单位: 秒) "loginDevice": "DEF" // 登录设备类型 }, } ``` ================================================ FILE: sa-token-doc/fun/token-timeout.md ================================================ # Token有效期详解 Sa-Token 提供两种 Token 自动过期策略,分别是 `timeout` 与 `active-timeout`,配置方法如下: ``` yaml sa-token: # token 有效期(单位:秒),默认30天,-1代表永不过期 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 ``` ``` properties # token 有效期(单位:秒),默认30天,-1代表永不过期 sa-token.timeout=2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 sa-token.active-timeout=-1 ``` 两者的区别,可以通过下面的例子体现: > [!TIP| label:场景示例] > 1. 假设你到银行要存钱,首先就要办理一张卡 (要访问系统接口先登录)。 > 2. 银行为你颁发一张储蓄卡(系统为你颁发一个Token),以后每次存取钱都要带上这张卡(后续每次访问系统都要提交 Token)。 > 3. 银行为这张卡设定两个过期时间: > - 第一个是 `timeout`,代表这张卡的长久有效期,就是指这张卡最长能用多久,假设 `timeout=3年`,那么3年后此卡将被银行删除,想要继续来银行办理业务必须重新办卡(Token 过期后想要访问系统必须重新登录)。 > - 第二个就是 `active-timeout`,代表这张卡的最低活跃频率限制,就是指这张卡必须每隔多久来银行一次,假设 `active-timeout=1月` ,你如果超过1月不来办一次业务,银行就将你的卡冻结,列为长期不动户(Token 长期不访问系统,被冻结,但不会被删除)。 > 4. 两个过期策略可以单独配置,也可以同时配置,只要有其中一个有效期超出了范围,这张卡就会变得不可用(两个有效期只要有一个过期了,Token就无法成功访问系统了)。 下面是对两个过期策略的详细解释: ### timeout 1. `timeout`代表 Token 的长久有效期,单位/秒,例如将其配置为 2592000 (30天),代表在30天后,Token必定过期,无法继续使用。 2. `timeout`~~无法续签,想要继续使用必须重新登录~~。v1.29.0+ 版本新增续期方法:`StpUtil.renewTimeout(100)`。 3. `timeout`的值配置为-1后,代表永久有效,不会过期。 ### active-timeout 1. `active-timeout`代表最低活跃频率,单位/秒,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期(被冻结,但不会删除掉)。 2. 如果在30分钟内用户有操作,则会再次续签30分钟,用户如果一直操作则会一直续签,直到连续30分钟无操作,Token才会过期。 3. `active-timeout`的值配置为-1后,代表永久有效,不会过期,此时也无需频繁续签。 ### 关于active-timeout的续签 如果`active-timeout`配置了大于零的值,Sa-Token 会在登录时开始计时,在每次直接或间接调用`getLoginId()`、`getTokenSession()`时进行一次冻结检查与续签操作。 此时会有两种情况: 1. 一种是会话无操作时间太长,Token已经被冻结,此时框架会抛出`NotLoginException`异常(场景值=-3), 2. 另一种则是会话在`active-timeout`有效期内通过检查,此时Token可以成功续签 ### 我可以手动续签 active-timeout 吗? **可以!** 如果框架的自动续签算法无法满足您的业务需求,你可以进行手动续签,Sa-Token 提供两个API供你操作: 1. `StpUtil.checkActiveTimeout()`: 检查当前Token 是否已经被冻结,如果是则抛出异常 2. `StpUtil.updateLastActiveToNow()`: 续签当前Token:(将 [最后操作时间] 更新为当前时间戳) 注意:在手动续签时,即使 Token 已经被冻结也可续签成功(解冻),如果此场景下需要提示续签失败,可采用先检查再续签的形式保证Token有效性 例如以下代码: ``` java // 先检查是否已被冻结 StpUtil.checkActiveTimeout(); // 检查通过后继续续签 StpUtil.updateLastActiveToNow(); ``` 同时,你还可以关闭框架的自动续签(在配置文件中配置 `autoRenew=false` ),此时续签操作完全由开发者控制,框架不再自动进行任何续签操作 如果你需要给其它 Token 续签: ``` java // 为指定 Token 续签 StpUtil.stpLogic.updateLastActiveToNow(tokenValue); ``` ### timeout 与 active-timeout 可以同时使用吗? **可以同时使用!** 两者的认证逻辑彼此独立,互不干扰,可以同时使用。 ### StpUtil 类中哪些方法支持自动续签 active-timeout? > 直接或间接调用过 `getLoginId()`、`getTokenSession()` 的方法 | 包括但不限于这些: | |---| | StpUtil.checkLogin() | | StpUtil.getLoginId() | | StpUtil.getLoginIdAsInt() | | StpUtil.getLoginIdAsString() | | StpUtil.getLoginIdAsLong() | |---| | StpUtil.getSession() | | StpUtil.getTokenSession() | |---| | StpUtil.getRoleList() | | StpUtil.hasRole() | | StpUtil.hasRoleAnd() | | StpUtil.hasRoleOr() | | StpUtil.checkRole() | | StpUtil.checkRoleAnd() | | StpUtil.checkRoleOr() | |---| | StpUtil.getPermissionList() | | StpUtil.hasPermission() | | StpUtil.hasPermissionAnd() | | StpUtil.hasPermissionOr() | | StpUtil.checkPermission() | | StpUtil.checkPermissionAnd() | | StpUtil.checkPermissionOr() | |---| | StpUtil.openSafe() | | StpUtil.isSafe() | | StpUtil.checkSafe() | | StpUtil.getSafeTime() | | StpUtil.closeSafe() | > 以下注解都间接调用过 getLoginId() 方法 | 支持自动续签的注解 | |---| | @SaCheckLogin | | @SaCheckRole | | @SaCheckPermission | | @SaCheckSafe | ================================================ FILE: sa-token-doc/include/include-qa.md ================================================ > [!WARNING| label:更改了 hosts 但无法访问?] > - 可能 1:你没保存。 > - 可能 2:你后端项目没启动。 > - 可能 3:你访问时端口写错了。 > - 可能 4:你开了 VPN,关掉试试。 ================================================ FILE: sa-token-doc/index.html ================================================ Sa-Token

Sa-Tokenv1.45.0

开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!
 

B站、抖音、视频号 ...

关注我们 → 分享“权限认证架构设计”干货视频

Sa-Token 支持特性

⚡️ 登录认证

多端登录、单端登录、同端互斥登录、七天免登录…… 多种登录策略只需改个配置即可完成

🔑️️ 权限认证

权限认证、角色认证、会话二级认证、注解鉴权、路由鉴权……多种姿势灵活鉴权

⛏️ 踢人下线

强制注销、踢人下线、账号封禁、身份切换、自动续签 …… 提供完善的会话管理方案

🔎 Redis集成

提供 Redis 集成方案、项目重启数据不丢失、多系统数据互通,可自定义数据持久化策略

🚀️️ 前后端分离

内置多种 Token 读取策略,适配APP、小程序、SPA单页应用等前后端分离场景

️🍃 单点登录

同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离……提供各种架构下的SSO接入方案

🍂 OAuth2.0

轻松搭建 OAuth2.0 认证中心,支持四种授权模式,支持 openid 授权机制,支持二次扩展开发

💦️ 微服务支持

分布式 Session 会话、网关统一鉴权、RPC调用鉴权……提供开箱即用的微服务认证方案

🗳️ 开箱即用

提供SpringMVC、WebFlux、Solon、jwt 等常见框架集成包,真正的开箱即用……

七年磨一剑 🗡️ 一站式解决方案

Sa-Token 可以帮你轻松解决大多数权限认证问题! 点击查看功能结构图

曾获荣誉


GVP - Gitee 最有价值开源项目


GitCode G-Star 优质开源项目


OSCHINA 2021 人气指数 TOP 30 开源项目


OSCHINA 2022 年度最火热中国开源项目社区


开放原子基金会2023快速成长开源项目


Gitee 5000 star 专属奖杯


Gitee 2025年度开源项目 Web应用开发 Top 2


Dromara 组织顶尖项目(之一)


可信开源社区共同体预备成员


所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖


Gitee 项目推荐榜 top 1


GitHub stars 超 18k+

Java 鉴权框架 Stars 对比

赞助者名单(感谢!感谢!感谢!)

日期排序 | 赞助额排序
赞助人 赞助金额 留言 时间
第 1/1 页

(如果您也有赞助 Sa-Token 的想法,可以参考: 赞助名单

优秀开源集成案例

Snowy

小诺开源技术

国内首个国密前后分离快速开发平台,基于Vue3、Antdv、SaToken

mall4j

Mall4j商城系统

基于Spring Boot 3 JDK17的一个商城手脚架。

RuoYi-Vue-Plus

疯狂的狮子Li

重写 RuoYi-Vue 所有功能,集成 Sa-Token、Mybatis-Plus、Hutool 定期同步

Smart-Admin

1024创新实验室

坚持以「高质量代码」为核心,「简洁、高效、安全」的中后台解决方案!

SpringBoot_v2

开源oschina

努力打造 springboot 框架的极致细腻的脚手架,原生纯净。

灯灯

最后

专注于多租户解决方案的微服务中后台快速开发平台。

RuoYi-Cloud-Plus

疯狂的狮子Li

重写 RuoYi-Cloud 所有功能 整合 SpringCloudAlibaba、Dubbo3.0、Sa-Token

橙单

orange-form

橙单中台化低代码生成器。多应用、多租户、多渠道、工作流、在线表单等。

拾壹博客

bule

一款 Vue + SpringBoot 前后端分离的博客系统

如果您的开源项目也使用了 Sa-Token,您可以 在此 提交

Sa-Token 官方公众号,及时接收框架更新通知、技术文章


正在使用 Sa-Token 的企业 / 机构

(如果您的企业也使用了 Sa-Token,您可以 在此 提交)

================================================ FILE: sa-token-doc/micro/dcs-session.md ================================================ # 微服务 - 分布式Session会话 --- ### 需求场景 微服务架构下的第一个难题便是数据同步,单机版的`Session`在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。 首先我们要明白,分布式环境下为什么`Session`会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, 这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。 ### 解决方案 要怎么解决这个问题呢?目前的主流方案有四种: 1. **Session同步**:只要一个节点的数据发生了改变,就强制同步到其它所有节点 2. **Session粘滞**:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务 3. **建立会话中心**:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如:`Redis` 4. **颁发无状态token**:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释,例如:`jwt` ### 方案选择 该如何选择一个合适的方案? - 方案一:性能消耗太大,不太考虑 - 方案二:需要从网关处动手,与框架无关 - 方案三:Sa-Token 整合`Redis`非常简单,详见章节:[集成 Redis](/up/integ-redis) - 方案四:详见官方仓库中 Sa-Token 整合`jwt`的示例 由于`jwt`模式不在服务端存储数据,对于比较复杂的业务可能会功能受限,因此更加推荐使用方案三 集成依赖示例: ``` xml cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` ``` gradle // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' ``` 详细参考:[集成 Redis](/up/integ-redis) ================================================ FILE: sa-token-doc/micro/gateway-auth.md ================================================ # 微服务 - 网关统一鉴权 微服务架构下的鉴权一般分为两种: 1. 每个服务各自鉴权 2. 网关统一鉴权 方案一和传统单体鉴权差别不大,不再过多赘述,本篇介绍方案二的整合步骤: --- ### 1、引入依赖 首先,根据 [依赖引入说明](/micro/import-intro) 引入正确的依赖,以`[SpringCloud Gateway]`为例: ``` xml cn.dev33 sa-token-reactor-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 注:Redis包是必须的,因为我们需要和各个服务通过Redis来同步数据 ### 2、实现鉴权接口 ``` java /** * 自定义权限验证接口扩展 */ @Component public class StpInterfaceImpl implements StpInterface { @Override public List getPermissionList(Object loginId, String loginType) { // 返回此 loginId 拥有的权限列表 return ...; } @Override public List getRoleList(Object loginId, String loginType) { // 返回此 loginId 拥有的角色列表 return ...; } } ``` 关于数据的获取,建议以下方案三选一: 1. 在网关处集成ORM框架,直接从数据库查询数据 2. 先从Redis中获取数据,获取不到时走ORM框架查询数据库 3. 先从Redis中获取缓存数据,获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取 ### 3、注册全局过滤器 然后在网关处注册全局过滤器进行鉴权操作 ``` java /** * [Sa-Token 权限认证] 配置类 * @author click33 */ @Configuration public class SaTokenConfigure { // 注册 Sa-Token全局过滤器 @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 拦截地址 .addInclude("/**") /* 拦截全部path */ // 开放地址 .addExclude("/favicon.ico") // 鉴权方法:每次访问进入 .setAuth(obj -> { // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin()); // 权限认证 -- 不同模块, 校验不同权限 SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); // 更多匹配 ... */ }) // 异常处理方法:每次setAuth函数出现异常时进入 .setError(e -> { return SaResult.error(e.getMessage()); }) ; } } ``` 详细操作参考:[路由拦截鉴权](/use/route-check) ================================================ FILE: sa-token-doc/micro/import-intro.md ================================================ # 微服务中使用Sa-Token 依赖引入说明 --- 虽然在 [开始] 章节已经说明了依赖引入规则,但是交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的,此处再次重点强调一下: > [!TIP| style:callout] > **在微服务架构中使用Sa-Token时,网关和内部服务要分开引入Sa-Token依赖(不要直接在顶级父pom中引入Sa-Token)** 总体来讲,我们需要关注的依赖就是两个:`sa-token-spring-boot-starter` 和 `sa-token-reactor-spring-boot-starter`: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 ``` xml cn.dev33 sa-token-reactor-spring-boot-starter ${sa.top.version} ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 至于怎么分辨我们需要引入哪个呢?这个要看你使用的基础框架: 对于内部基础服务来讲,我们一般都是使用SpringBoot默认的web模块:SpringMVC, 因为这个SpringMVC是基于Servlet模型的,在这里我们需要引入的是`sa-token-spring-boot-starter` 对于网关服务,大体来讲分为两种: - 一种是基于Servlet模型的,如:Zuul,我们需要引入的是:`sa-token-spring-boot-starter`,详细戳:[在SpringBoot环境集成](/start/example);理论上`Zuul`并不支持`Spring Boot3` - 一种是基于Reactor模型的,如:SpringCloud Gateway、ShenYu 等等,我们需要引入的是:`sa-token-reactor-spring-boot-starter`,**并且注册全局过滤器!**,详细戳:[在WebFlux环境集成](/start/webflux-example) 注:切不可直接在一个项目里同时引入这两个依赖,否则会造成项目无法启动 另外,我们需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据 ``` xml cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` ``` gradle // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' ``` 详细参考:[集成 Redis](/up/integ-redis) ================================================ FILE: sa-token-doc/micro/same-token.md ================================================ # 微服务 - 内部服务外网隔离 --- ### 一、需求场景 我们的子服务一般不能通过外网直接访问,必须通过网关转发才是一个合法的请求,这种子服务与外网的隔离一般分为两种: 1. 物理隔离:子服务部署在指定的内网环境中,只有网关对外网开放 2. 逻辑隔离:子服务与网关同时暴露在外网,但是子服务会有一个权限拦截层保证只接受网关发送来的请求,绕过网关直接访问子服务会被提示:无效请求 这种鉴权需求牵扯到两个环节: **`网关转发鉴权`** 、 **`服务间内部调用鉴权`** Sa-Token提供两种解决方案: 1. 使用 OAuth2.0 模式的凭证式,将 Client-Token 用作各个服务的身份凭证进行权限校验 2. 使用 Same-Token 模块提供的身份校验能力,完成服务间的权限认证 本篇主要讲解方案二 `Same-Token` 模块的整合步骤,其鉴权流程与 OAuth2.0 类似,不过使用方式上更加简洁(希望使用方案一的同学可参考Sa-OAuth2模块,此处不再赘述) Same-Token_同源系统认证.svg ### 二、网关转发鉴权 ##### 1、引入依赖 在网关处引入的依赖为(此处以 SpringCloud Gateway 为例): ``` xml cn.dev33 sa-token-reactor-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 在下游子服务引入的依赖为: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 ##### 2、网关处添加Same-Token 为网关添加全局过滤器: ``` java /** * 全局过滤器,为请求添加 Same-Token */ @Component public class ForwardAuthFilter implements GlobalFilter { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest newRequest = exchange .getRequest() .mutate() // 为请求追加 Same-Token 参数 .header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()) .build(); ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); return chain.filter(newExchange); } } ``` 此过滤器会为 Request 请求头追加 `Same-Token` 参数,这个参数会被转发到子服务 ##### 3、在子服务里校验参数 在子服务添加过滤器校验参数 ``` java /** * Sa-Token 权限认证 配置类 */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 全局过滤器 @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/**") .addExclude("/favicon.ico") .setAuth(obj -> { // 校验 Same-Token 身份凭证 —— 以下两句代码可简化为:SaSameUtil.checkCurrentRequestToken(); String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN); SaSameUtil.checkToken(token); }) .setError(e -> { return SaResult.error(e.getMessage()); }) ; } } ``` 启动网关与子服务,访问测试: > [!WARNING| label:越过网关访问] > 如果通过网关转发,可以正常访问。如果直接访问子服务会提示:`无效Same-Token:xxx` ### 三、服务间内部调用鉴权 有时候我们需要在一个服务调用另一个服务的接口,这也是需要添加`Same-Token`作为身份凭证的 在服务里添加 Same-Token 流程与网关类似,我们以RPC框架 `Feign` 为例: ##### 1、首先在调用方添加 FeignInterceptor ``` java /** * feign拦截器, 在feign请求发出之前,加入一些操作 */ @Component public class FeignInterceptor implements RequestInterceptor { // 为 Feign 的 RPC 调用 添加请求头Same-Token @Override public void apply(RequestTemplate requestTemplate) { requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); // 如果希望被调用方有会话状态,此处就还需要将 satoken 添加到请求头中 // requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue()); } } ``` ##### 2、在调用接口里使用此 Interceptor ``` java /** * 服务调用 */ @FeignClient( name = "sp-home", // 服务名称 configuration = FeignInterceptor.class, // 请求拦截器 (⚠️ 关键代码) fallbackFactory = SpCfgInterfaceFallback.class // 服务降级处理 ) public interface SpCfgInterface { // 获取server端指定配置信息 @RequestMapping("/SpConfig/getConfig") public String getConfig(@RequestParam("key")String key); } ``` 被调用方的代码无需更改(按照网关转发鉴权处的代码注册全局过滤器),保持启动测试即可 ### 四、Same-Token 模块详解 Same-Token —— 专门解决同源系统互相调用时的身份认证校验,它的作用不仅局限于微服务调用场景 基本使用流程为:服务调用方获取 Same-Token,提交到请求中,被调用方取出 Same-Token 进行校验:如果一致则校验通过,否则拒绝服务。 Same-Token_同源系统认证.svg 首先我们预览一下此模块的相关API: ``` java // 获取当前Same-Token SaSameUtil.getToken(); // 判断一个Same-Token是否有效 SaSameUtil.isValid(token); // 校验一个Same-Token是否有效 (如果无效则抛出异常) SaSameUtil.checkToken(token); // 校验当前Request提供的Same-Token是否有效 (如果无效则抛出异常) SaSameUtil.checkCurrentRequestToken(); // 刷新一次Same-Token (注意集群环境中不要多个服务重复调用) SaSameUtil.refreshToken(); // 在 Request 上储存 Same-Token 时建议使用的key SaSameUtil.SAME_TOKEN; ``` ##### 1、疑问:这个Token保存在什么地方?有没有泄露的风险?Token为永久有效还是临时有效? Same-Token 默认随 Sa-Token 数据一起保存在Redis中,理论上不会存在泄露的风险,每个Token默认有效期只有一天 ##### 2、如何主动刷新Same-Token,例如:五分钟、两小时刷新一次? Same-Token 刷新间隔越短,其安全性越高,每个Token的默认有效期为一天,在一天后再次获取会自动产生一个新的Token > [!WARNING| label:注意点] > 需要注意的一点是:Same-Token默认的自刷新机制,并不能做到高并发可用,多个服务一起触发Token刷新可能会造成毫秒级的短暂服务失效,其只能适用于 项目开发阶段 或 低并发业务场景 因此在微服务架构下,我们需要有专门的机制主动刷新Same-Token,保证其高可用 例如,我们可以专门起一个服务,使用定时任务来刷新Same-Token ``` java /** * Same-Token,定时刷新 */ @Configuration public class SaSameTokenRefreshTask { // 从 0 分钟开始 每隔 5 分钟执行一次 Same-Token @Scheduled(cron = "0 0/5 * * * ? ") public void refreshToken(){ SaSameUtil.refreshToken(); } } ``` 以上的cron表达式刷新间隔可以配置为`五分钟`、`十分钟` 或 `两小时`,只要低于Same-Token的有效期(默认为一天)即可。 ##### 3、如果网关携带token转发的请求在落到子服务的节点上时,恰好刷新了token,导致鉴权未通过怎么办? Same-Token 模块在每次刷新 Token 时,旧 Token 会被作为次级 Token 存储起来, 只要网关携带的 Token 符合新旧 Token 其一即可通过认证,直至下一次刷新,新 Token 再次作为次级 Token 将此替换掉。 ================================================ FILE: sa-token-doc/more/blog.md ================================================ # 框架博客 > 此页面收集 Sa-Token 相关技术文章(不限平台,按照发表日期倒序), > 如果你也想投稿,请考虑加入:[Sa-Token 内容合作群 ](/more/content-cooperation) --- - [[ 公众号 ] Kaleido-AI教程(九)基于Sa-Token实现多账户认证体系 ](https://mp.weixin.qq.com/s/ihW1sM8DvQJS-1ZKhFr9KQ) (2026-3-8) - [[ 公众号 ] Sa-Token 的 token-prefix 和 token-style,到底谁管谁? ](https://mp.weixin.qq.com/s/1_PaPxvEui-16Is6Urw3CA) (2026-3-7) - [[ 公众号 ] JAVA:Spring Boot3 集成 Sa-Token 轻量级权限认证 ](https://mp.weixin.qq.com/s/cjk9ad9tj397Bd0hAyCsyA) (2026-3-6) - [[ 公众号 ] Sa-Token(二)之从入门到实战——一篇文章助你真正了解掌握Sa-Token ](https://mp.weixin.qq.com/s/9-CLoSJZBrfTF2tul-M8Ww) (2026-3-6) - [[ 公众号 ] 不用 Cookie,鉴权照样稳 ](https://mp.weixin.qq.com/s/k8DC-GiYYPbofGDD_4jlPQ) (2026-3-6) - [[ CSDN ] Sa-Token登录策略全解析:从单地登录到同端互斥,这些配置项你都知道吗? ](https://blog.csdn.net/weixin_29284885/article/details/158675177) (2026-3-5) - [[ 公众号 ] 18.6k vs 9.5k Star!若依认证该选谁? ](https://mp.weixin.qq.com/s/BVFWWPiYloa1nuZ4MZ8XZg) (2026-3-4) - [[ 公众号 ] SaToken 支持使用 JSON body验签 ](https://mp.weixin.qq.com/s/0Rr9PuDBUJwaEolhtJxBwA) (2026-3-3) - [[ 公众号 ] 明明接了 Redis,重启后会话还是丢了? ](https://mp.weixin.qq.com/s/-O1qwR0I30wngurGo-qOuw) (2026-3-3) - [[ CSDN ] JAVA:Spring Boot3 集成 Sa-Token 轻量级权限认证](https://shdxhl.blog.csdn.net/article/details/157695326) (2026-2-27) - [[ 公众号 ] 苦 Spring Security 久矣?这款霸榜 Gitee 的权限框架,把优雅做到了极致](https://mp.weixin.qq.com/s/CzBPkeV6jWZ7mpA_6JH09g) (2026-2-27) - [[ 公众号 ] 架构师推荐开源项目:轻量级Java权限认证框架!](https://mp.weixin.qq.com/s/OZtTqYIZNU2l_yyKFvbd9A) (2026-2-24) - [[ 公众号 ] Sa-Token(一)之简介及入门:告别鉴权内耗,让每一位Java开发者都能轻松上手](https://mp.weixin.qq.com/s/HLG1PHnbfOTpC3e-tGutew) (2026-2-24) - [[ CSDN ] Sa-Token SSO 前后端分离实战:SpringBoot + Vue2 单点登录全流程解析](https://blog.csdn.net/weixin_29291863/article/details/158324193) (2026-2-24) - [[ CSDN ] RefreshToken反查踩坑记:Sa-Token 1.42.0临时令牌管理新姿势](https://blog.csdn.net/weixin_28327051/article/details/158301601) (2026-2-23) - [[ 公众号 ] SpringBoot3 + Sa-Token 单点登录|30分钟上手,代码可复制,新手零踩坑](https://mp.weixin.qq.com/s/2jN9HotfYttLFMzHE54XiA) (2026-2-21) - [[ 公众号 ] Sa-Token Session会话:三种模型彻底搞懂,不再傻傻分不清](https://mp.weixin.qq.com/s/gpqogF0QyahuqJIqhs6DUg) (2026-2-21) - [[ 公众号 ] SpringBoot3 + Sa-Token 双Token登录认证实战(避坑版)](https://mp.weixin.qq.com/s/LDSCSZYuUIkQA91MYXhnGQ) (2026-2-20) - [[ CSDN ] SaToken实战:5分钟搞定微信小程序登录功能(附完整代码)](https://blog.csdn.net/weixin_29218509/article/details/158226233) (2026-2-20) - [[ 公众号 ] 告别SpringSecurity!Sa-Token+Gateway+Nacos极简鉴权实战](https://mp.weixin.qq.com/s/lFcH7XyLRtaNH6q4-TzHbQ) (2026-2-19) - [[ 公众号 ] SpringSecurity、Shiro和Sa-Token,哪个更好?](https://mp.weixin.qq.com/s/BEvk1ohFntL7iorDEvqIpg) (2026-2-18) - [[ CSDN ] Sa-Token 1.42.0实战:5分钟搞定API Key权限隔离与TOTP双因子认证](https://blog.csdn.net/weixin_29271053/article/details/158175327) (2026-2-18) - [[ CSDN ] SaToken权限注解全解析:@SaCheckPermission和@SaCheckRole的20种实战用法](https://blog.csdn.net/weixin_28454475/article/details/158163447) (2026-2-18) - [[ 公众号 ] 5 分钟上手 Sa-Token:Spring Boot 权限认证从未如此简单](https://mp.weixin.qq.com/s/2iRMPQdfBEgqSgeld3crXw) (2026-2-17) - [[ 公众号 ] SpringSecurity、Shiro和Sa-Token,哪个更好?](https://mp.weixin.qq.com/s/QdR1tyIXN8GvWbhN48XTjA) (2026-2-13) - [[ 公众号 ] sa-token前后端分离集成redis与jwt基础案例](https://mp.weixin.qq.com/s/c1UYdxuRjWudGnpQa_A72w) (2026-2-11) - [[ 公众号 ] Sa-Token(一)之简介及入门:告别鉴权内耗,让每一位Java开发者都能轻松上手](https://mp.weixin.qq.com/s/JLQSMAgqK1U0vtrdtRsKlA) (2026-2-11) - [[ 公众号 ] Sa-Token 的极简设计哲学](https://mp.weixin.qq.com/s/Yr48InNXxaVkNfVRbllO7w) (2026-2-10) - [[ 公众号 ] 还在用 @PreAuthorize?聊聊我切换到 Sa-Token 路由拦截后的真实体感](https://mp.weixin.qq.com/s/VMtSZDC1AFquCKRakTxytw) (2026-2-10) - [[ 公众号 ] Sa-Token 实战进阶:从“能用”到“好用”的企业级鉴权方案](https://mp.weixin.qq.com/s/hOY37lIxw01aPvjmdTf6VQ) (2026-2-9) - [[ 公众号 ] Sa-Token:把“登录/鉴权/踢人/SSO/OAuth2”做成一套顺手的 Java 权限方案(附 Spring Boot 快速上手)](https://mp.weixin.qq.com/s/zT8iRNuFfOEqDZfhVFy15w) (2026-2-9) - [[ 公众号 ] 开源、免费、一站式 java 权限认证框架,让鉴权变得简单、优雅!](https://mp.weixin.qq.com/s/FLDwIXHQoa6V2nKPs1r4cw) (2026-2-9) - [[ 公众号 ] Sa-Token 注解鉴权](https://mp.weixin.qq.com/s/72oLlgj-x8oetUpIJhR02A) (2026-2-9) - [[ 公众号 ] 用户投诉账号异常登录,CTO 让我 5 分钟内解决](https://mp.weixin.qq.com/s/pfMSZLxmDKIVYoq6UGUj1Q) (2026-2-4) - [[ 公众号 ] Sa-Token实战:SpringBoot与微服务权限认证极简方案](https://mp.weixin.qq.com/s/yNma6FhHvPLNHUqYFkdPCg) (2026-1-25) - [[ 公众号 ] Sa-Token过期机制](https://mp.weixin.qq.com/s/gFQ8YJT1yg5pTm8Z3uDZHw) (2026-1-22) - [[ 公众号 ] 别再写死权限了!SpringBoot + Sa-Token 实现 RBAC 的最佳姿势](https://mp.weixin.qq.com/s/ZwzAInOoqiQ2h0ogWaOoQg) (2026-1-14) - [[ 公众号 ] 集成sa-token跨域正确姿势](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g) (2026-1-9) - [[ 公众号 ] 后端开发必看:最简单的 Java 登录认证框架 Sa-Token 上手指南](https://mp.weixin.qq.com/s/Kk9HEVAG43-FPikiMZKWJw) (2026-1-8) - [[ 公众号 ] Sa-Token:一站式权限认证解决方案的实战指南](https://mp.weixin.qq.com/s/FVkn-3CqWT8dNM6a5kD2oA) (2025-12-30) - [[ 公众号 ] SpringSecurity、Shiro和Sa-Token,哪个更好?](https://mp.weixin.qq.com/s/gtQ7_n9cPJd2-i_Qm-jk1A) (2025-12-28) - [[ 公众号 ] SpringBoot + JWT + Sa-Token:认证鉴权双框架对比,安全登录与权限控制最佳实践](https://mp.weixin.qq.com/s/SDPKdmxtwb4MbOHfg-bF8Q) (2025-12-27) - [[ 公众号 ] 一行代码搞定认证](https://mp.weixin.qq.com/s/UaAw1WdVtumA44SxjFW3yg) (2025-12-24) - [[ 掘金 ] Netty + Sa-Token 实现 WebSocket 握手认证](https://juejin.cn/post/7585490245006950406) (2025-12-21) - [[ 公众号 ] 《第27节》SpringBoot+SaToken实现鉴权功能](https://mp.weixin.qq.com/s/mY_jrQL1dG2yis51rX2i9w) (2025-12-16) - [[ 公众号 ] 告别 Spring Security!Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/s36bdkhi5ACGN2j7hLiD7w) (2025-12-15) - [[ 公众号 ] 《第26节》SpringBoot3+SaToken实现用户注册登录功能](https://mp.weixin.qq.com/s/u7nVa0PJcFWx-9xKq1RNhA) (2025-12-13) - [[ 公众号 ] 《第25节》SpringBoot3之集成sa-token权限认证框架](https://mp.weixin.qq.com/s/sxgzLqiKCf4_fqWAxN8Ozg) (2025-12-12) - [[ 掘金 ] sa-token前后端分离集成redis与jwt基础案例](https://juejin.cn/post/7576843726011645978) (2025-11-26) - [[ 公众号 ] Sa-Token 1.44.0:Java权限认证的“轻量级王者”,让鉴权优雅如诗](https://mp.weixin.qq.com/s/UprusTkp9LZOH9TJDTKJRw) (2025-11-20) - [[ 公众号 ] sa-token-rust 项目:高性能的 Rust 认证授权框架](https://mp.weixin.qq.com/s/jlQAX1K1M64DtUgrHrDX1A) (2025-11-17) - [[ 公众号 ] 不会吧,居然还有人没有用过?全网爆火的权限校验框架 Sa-Token 超详细教程它来了](https://mp.weixin.qq.com/s/kNYq0MmlYB_0tRWI-HvU1g) (2025-11-6) - [[ 公众号 ] 若依框架集成 Sa-Token 实现权限认证与会话管理](https://mp.weixin.qq.com/s/JAgL0hxcPeP0E4OW4oy8Yg) (2025-10-21) - [[ 公众号 ] 太强了!Sa-Token 的 Go 版本!](https://mp.weixin.qq.com/s/idfrMeAMY2CeGAZGY9csmw) (2025-10-20) - [[ 公众号 ] 太强了!Sa-Token 的 rust 版本!](https://mp.weixin.qq.com/s/CveVq368Dz5Xw-a2nT3YDw) (2025-10-12) - [[ 公众号 ] 功能最全的Java权限认证框架](https://mp.weixin.qq.com/s/fO5Mm1UIN8oDOwvbq-sQTw) (2025-10-10) - [[ 公众号 ] 从 0 到 1!Sa-Token 与 SpringBoot 整合教程,让鉴权优雅到飞起](https://mp.weixin.qq.com/s/PudodYBsIQODdfeweo39fQ) (2025-10-16) - [[ 公众号 ] 一篇搞定!SpringBoot 搭建超安全 Sa-Token 登录鉴权系统](https://mp.weixin.qq.com/s/5fbrNS6jMpuViPPO8z_kMw) (2025-10-3) - [[ 公众号 ] 《Spring Cloud Gateway 从入门到实战》第4篇:安全与认证 —— 基于 Sa-Token 的网关统一鉴权方案](https://mp.weixin.qq.com/s/qeWQufIDNtGPyJ0b7yF4vQ) (2025-9-29) - [[ 公众号 ] SpringBoot整合Sa-Token实现认证与鉴权](https://mp.weixin.qq.com/s/l4OjqdeNpXjMyFCSr3PuWw) (2025-9-25) - [[ 公众号 ] Spring Gateway、Sa-Token、Nacos 认证/鉴权方案](https://mp.weixin.qq.com/s/JpXtI75eANwkRAppQZJG9Q) (2025-9-17) - [[ 公众号 ] 告别 Spring Security!Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/hlBH1H6vX-KIlQGmeY8bIg) (2025-9-15) - [[ 公众号 ] Spring Gateway、Sa-Token、Nacos 认证/鉴权方案,yyds!](https://mp.weixin.qq.com/s/OYsxjEmLfkxH0NqZidb4fA) (2025-9-10) - [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.4 Sa-Token高级特性实现](https://mp.weixin.qq.com/s/0Fex83DyngC-mxIu346X1Q) (2025-8-30) - [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.3 权限控制与注解使用](https://mp.weixin.qq.com/s/f4iVDeIAZ-nixqR6BNtUfg) (2025-8-30) - [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.2 登录认证机制详解](https://mp.weixin.qq.com/s/gu5kT93WjEauut7xoXmuag) (2025-8-29) - [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.1 Sa-Token框架基础](https://mp.weixin.qq.com/s/w8c5fvaap7ipMu2OdXNZng) (2025-8-29) - [[ 公众号 ] 告别 Spring Security!Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/5nmEDAsFgEWk-Ymn_pE74w) (2025-8-22) - [[ 公众号 ] 搭建基于sa-token 的网关权限管理系统](https://mp.weixin.qq.com/s/LM6g3QaklSHpaVJXUz5Gvg) (2025-7-19) - [[ 公众号 ] 别再被 Spring Security 和 Shiro 劝退了!这款国产 Java 权限框架真香!](https://mp.weixin.qq.com/s/2C0WSlM8zpjqQDtgv59Kaw) (2025-7-1) - [[ 公众号 ] 一文精通Java集成Sa-Token实现SSO单点登录](https://mp.weixin.qq.com/s/-fex5XFm4wmTmuzZCtUiRw) (2025-5-22) - [[ 公众号 ] 47.8k star,一款接私活神器,10分钟搞定企业级鉴权!](https://mp.weixin.qq.com/s/KRz3-h6etPaKOwHN4Xz7qg) (2025-5-15) - [[ 公众号 ] Sa-Token:17.5k Star!轻量级Java权限认证框架,登录鉴权超简单](https://mp.weixin.qq.com/s/akPTgU8sQkwWmXm4GNSyyQ) (2025-5-11) - [[ 公众号 ] SaToken-微服务认证与授权](https://mp.weixin.qq.com/s/9WYg6iLDST7YDSSsCt1NxQ) (2025-4-3) - [[ 公众号 ] SpringBoot 整合 Sa-Token 快速实现 API 接口签名安全校验](https://mp.weixin.qq.com/s/2NS3axN1CbRYULHrv5Du2w) (2025-3-20) - [[ 公众号 ] SaToken 简化开发的身份认证与权限管理框架](https://mp.weixin.qq.com/s/Yxsswl4Zn8244j4XeV8_VA) (2025-1-24) - [[ 公众号 ] 使用 Sa-Token 平替 Spring Security,告别繁琐的认证与鉴权!](https://mp.weixin.qq.com/s/whKQPm09ApzkVBjlSwO2Tg) (2025-1-15) - [[ 公众号 ] sa-token之@SaIgnore注解失效的真正原因及正确姿势](https://mp.weixin.qq.com/s/c6eckHp2M4oz2x3Hea6pGg) (2025-1-14) - [[ 公众号 ] SpringBoot3.x+Vue3+Sa-Token实现登录认证](https://mp.weixin.qq.com/s/0GkDoOYW8KKxTfzV83J6UA) (2024-12-27) - [[ 公众号 ] 万字雄文:一次说清基于Sa-Token和MaxKey的统一认证中心实现](https://mp.weixin.qq.com/s/Gl2K47F9I6-Il-AieWLplw) (2024-11-15) - [[ 公众号 ] 集成sa-token前后端分离部署配置corsFliter解决跨域失效的真正原因](https://mp.weixin.qq.com/s/bSS4vmKlKM7ov_CUkjxkBg) (2024-07-08) - [[ 公众号 ] sa-token前后端分离解决跨域的正确姿势](https://mp.weixin.qq.com/s/96WbWL28T5_-xzyCfJ7Stg) (2024-07-06) - [[ 公众号 ] 集成sa-token实现登录和RBAC权限控制](https://mp.weixin.qq.com/s/SREjXoyL9s1JfddQnU38yA) (2024-04-16) - [[ CSDN ] springboot整合Sa-Token实现登录认证和权限校验(万字长文)](https://blog.csdn.net/2301_78646673/article/details/136008153) (2024-03-31) - [[ CSDN ]【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) (2023-11-01) - [[ CSDN ]【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) (2023-11-01) - [[ CSDN ] 【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) (2023-08-09) - [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 31 - Sa-Token(五)登录验证拦截器之 Token 有效期及其续签(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/126071871) (2022-07-30) - [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 29 - Sa-Token(四)V1.30.0 登录流程分析(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/125659797) (2022-07-07) - [[ 掘金 ] sa-token过期后WebSocket提示过期](https://juejin.cn/post/7103446095987998733) (2022-5-30) - [[ 掘金 ] Sa-Token 单点登录 SSO模式二 URL重定向传播会话示例](https://juejin.cn/post/7102733249088077854) (2022-5-28) - [[ 掘金 ] SaToken技术分享](https://juejin.cn/post/7097967875670933535) (2022-5-15) - [[今日头条] SpringCloud Gateway配置Nacos服务发现,Sa-Token实现接口授权](https://www.toutiao.com/article/7089584645368578567/) (2022-04-24) - [[ CSDN ] 使用sa-token 进行权限控制](https://blog.csdn.net/u012389318/article/details/124098705) (2022-4-13) - [[ 掘金 ] SpringMVC配置sa-Token](https://juejin.cn/post/7081471627766005790) (2022-4-1) - [[ CSDN ] 【SpringBoot】59、SpringBoot使用Sa-Token-Quick-Login插件快速登录认证](https://lizhou.blog.csdn.net/article/details/123571910) (2022-03-30) - [[ CSDN ] 【Sa-Token】1、Sa-Token实现登录功能](https://lizhou.blog.csdn.net/article/details/119301185) (2022-03-30) - [[ 掘金 ] 【SpringCloud-Alibaba系列教程】13.gateway网关结合Sa-token进行登录鉴权](https://juejin.cn/post/7070805258296885285) (2022-3-3) - [[ CSDN ] Sa-Token的Token有效期和临时有效期的区别](https://blog.csdn.net/ControlDemo/article/details/123177825) (2022-02-28) - [[ 掘金 ] Spring Cloud Gateway 集成Sa-Token](https://juejin.cn/post/7069748160087719967) (2022-2-28) - [[ 掘金 ] Java轻量级权限认证框架 Sa-Token 初体验](https://juejin.cn/post/7068105371839102983) (2022-2-24) - [[ CSDN ] Sa-Token获取当前所有可用Token](https://blog.csdn.net/ControlDemo/article/details/122940634) (2022-02-15) - [[ 掘金 ] 使用 Sa-Token 解决 WebSocket 握手身份认证](https://juejin.cn/post/7064232762664255525) (2022-2-14) - [[ CSDN ] sa-token配置路由拦截放行Swagger路径](https://blog.csdn.net/ControlDemo/article/details/122885782) (2022-02-11) - [[ CSDN ] sa-token 多端登录思路和遇到的坑](https://blog.csdn.net/ControlDemo/article/details/122428512) (2022-1-28) - [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 13 - Sa-Token(三)退出登录流程(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/122691698) (2022-01-25) - [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 12 - Sa-Token(二)通过注解校验用户权限(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/122526722) (2022-01-16) - [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 11 - 集成 Sa-Token 实现登录认证流程(Sa-Token 源码)](https://blog.csdn.net/Michelle_Zhong/article/details/122480703) (2022-01-13) - [[ 掘金 ] Springboot插件集成(三)-权限认证插件sa-token](https://juejin.cn/post/7051872914458542093) (2022-1-11) - [[ CSDN ] Sa-token简单介绍和基本使用](https://blog.csdn.net/weixin_43967582/article/details/122075950) (2021-12-21) - [[ CSDN ] sa-token使用(源码解析 + 万字)](https://blog.csdn.net/weixin_39570751/article/details/121291274) (2021-11-12) - [[ 公众号 ] 还在用Spring Security?推荐你一款使用简单、功能强大的权限认证框架](https://mp.weixin.qq.com/s/L2KOgwJcXCxrSAV8bPJsJQ) (2021-10-8) - [[ 公众号 ] Spring Security太复杂?试试这个轻量、强大、优雅的权限认证框架!](https://mp.weixin.qq.com/s/BWziNxRZH29F2v4Tmb5meA) (2021-09-22) - [[ 博客园 ] Sa-Token之注解鉴权:优雅的将鉴权与业务代码分离!](https://www.cnblogs.com/shengzhang/p/15260818.html) (2021-9-13) - [[ 掘金 ] 开箱即用!看看人家的微服务权限解决方案,那叫一个优雅!](https://juejin.cn/post/7003141949259513887) (2021-9-2) - [[ 掘金 ] 再见Spring Security!推荐一款功能强大的Java权限认证框架,用起来够优雅!](https://juejin.cn/post/7000174417846222878) (2021-8-25) - [[ 掘金 ] 史上功能最全的 Java 权限认证框架!](https://juejin.cn/post/6986174013647093773) (2021-7-18) - [[ 知乎 ] 一个项目搞定Java权限认证框架,二十多个特性开箱即用](https://zhuanlan.zhihu.com/p/390030149) (2021-7-15) - [[ 掘金 ] 从零搭建开发脚手架 集成认证授权 sa-token(尝鲜)](https://juejin.cn/post/6950163768533843999) (2021-4-12) - [[ 掘金 ] 权限认证就它了Sa-Token](https://juejin.cn/post/6938747514837434376) (2021-3-12) - [[ 掘金 ] sa-token之前后端分离模式下如何完成权限认证](https://juejin.cn/post/6937219472507797535) (2021-03-8) - [[ 掘金 ] 一个登录功能也能玩出这么多花样?sa-token带你轻松搞定多地登录、单地登录、同端互斥登录](https://juejin.cn/post/6917884159491276808) (2021-1-15) - [[ 掘金 ] sa-token v1.9.0 版本已发布,带来激动人心新特性:同端互斥登录](https://juejin.cn/post/6914612737020526599) (2021-1-6) - [[ 掘金 ] Spring Boot 系列教程 | 第一百一篇:SpringBoot整合sa-token权限框架](https://juejin.cn/post/6875525673897869319) (2020-9-23) ================================================ FILE: sa-token-doc/more/common-action.md ================================================ # 全局类、方法 本篇介绍 Sa-Token 中一些常用的全局对象、类 --- ### SaManager SaManager 负责管理 Sa-Token 所有全局组件。 ``` java SaManager.getConfig(); // 获取全局配置对象 SaManager.getSaTokenDao(); // 获取数据持久化对象 SaManager.getStpInterface(); // 获取权限认证对象 SaManager.getSaTokenContext(); // 获取SaTokenContext上下文处理对象 SaManager.getSaTokenListener(); // 获取侦听器对象 SaManager.getSaTemp(); // 获取临时令牌认证模块对象 SaManager.getSaJsonTemplate(); // 获取 JSON 转换器 Bean SaManager.getSaSignTemplate(); // 获取参数签名 Bean SaManager.getStpLogic("type"); // 获取指定账号类型的StpLogic对象,获取不到时自动创建并返回 SaManager.getStpLogic("type", false); // 获取指定账号类型的StpLogic对象,获取不到时抛出异常 SaManager.putStpLogic(stpLogic); // 向全局集合中 put 一个 StpLogic ``` ### SaHolder Sa-Token上下文持有类,通过此类快速获取当前环境的相关对象 ``` java SaHolder.getContext(); // 获取当前请求的 SaTokenContext SaHolder.getRequest(); // 获取当前请求的 [Request] 对象 SaHolder.getResponse(); // 获取当前请求的 [Response] 对象 SaHolder.getStorage(); // 获取当前请求的 [Storage] 对象 SaHolder.getApplication(); // 获取全局 SaApplication 对象 ``` ### SaRouter 路由匹配工具类,详细戳:[路由拦截式鉴权](/use/route-check) ### SaFoxUtil Sa-Token内部工具类,包含一些工具方法 ``` java SaFoxUtil.printSaToken(); // 打印 Sa-Token 版本字符画 SaFoxUtil.getRandomString(8); // 生成指定长度的随机字符串 SaFoxUtil.isEmpty(str); // 指定字符串是否为null或者空字符串 SaFoxUtil.isNotEmpty(str); // 指定字符串是否不是null或者空字符串 SaFoxUtil.equals(a, b); // 比较两个对象是否相等 SaFoxUtil.getMarking28(); // 以当前时间戳和随机int数字拼接一个随机字符串 SaFoxUtil.formatDate(date); // 将日期格式化为yyyy-MM-dd HH:mm:ss字符串 SaFoxUtil.searchList(dataList, prefix, keyword, start, size, sortType); // 从集合里查询数据 SaFoxUtil.searchList(dataList, start, size, sortType); // 从集合里查询数据 SaFoxUtil.vagueMatch(patt, str); // 字符串模糊匹配 SaFoxUtil.getValueByType(obj, cs); // 将指定值转化为指定类型 SaFoxUtil.joinParam(url, parameStr); // 在url上拼接上kv参数并返回 SaFoxUtil.joinParam(url, key, value); // 在url上拼接上kv参数并返回 SaFoxUtil.joinSharpParam(url, parameStr); // 在url上拼接锚参数 SaFoxUtil.joinSharpParam(url, key, value); // 在url上拼接锚参数 SaFoxUtil.arrayJoin(arr); // 将数组的所有元素使用逗号拼接在一起 SaFoxUtil.isUrl(str); // 使用正则表达式判断一个字符串是否为URL SaFoxUtil.encodeUrl(str); // URL编码 SaFoxUtil.decoderUrl(str); // URL解码 SaFoxUtil.convertStringToList(str); // 将指定字符串按照逗号分隔符转化为字符串集合 SaFoxUtil.convertListToString(list); // 将指定集合按照逗号连接成一个字符串 SaFoxUtil.convertStringToArray(str); // String 转 Array,按照逗号切割 SaFoxUtil.convertArrayToString(arr); // Array 转 String,按照逗号切割 SaFoxUtil.emptyList(); // 返回一个空集合 SaFoxUtil.toList(... strs); // String 数组转集合 ``` ### SaTokenConfigFactory 配置对象工厂类,通过此类你可以方便的根据 properties 配置文件创建一个配置对象 1、首先在项目根目录,创建一个配置文件:`sa-token.properties` ``` properties # token 名称 (同时也是 cookie 名称) tokenName=satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout=2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 activeTimeout=-1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) isConcurrent=true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) isShare=false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) tokenStyle=uuid # 是否输出操作日志 isLog=false ``` 2、然后使用以下代码获取配置对象 ``` java // 设置配置文件地址 SaTokenConfigFactory.configPath = "sa-token.properties"; // 获取配置信息到 config 对象 SaTokenConfig config = SaTokenConfigFactory.createConfig(); // 注入到 SaManager 中 SaManager.setConfig(config); ``` ### SpringMVCUtil SpringMVC操作的工具类,位于包:`sa-token-spring-boot-starter` ``` java SpringMVCUtil.getRequest(); // 获取本次请求的 request 对象 SpringMVCUtil.getResponse(); // 获取本次请求的 response 对象 SpringMVCUtil.isWeb(); // 判断当前是否处于 Web 上下文中 ``` ### SaReactorHolder & SaReactorSyncHolder Sa-Token集成Reactor时的 ServerWebExchange 工具类,位于包:`sa-token-reactor-spring-boot-starter` ``` java // 异步方式获取 ServerWebExchange 对象 SaReactorHolder.getMonoExchange().map(e -> { System.out.println(e); return e; }); ``` ================================================ FILE: sa-token-doc/more/common-questions.md ================================================ # 常见问题排查 本篇整理大家在群聊里经常提问的一些问题,如有补充,欢迎提交pr [[toc]] --- ## 一、常见报错 ### Q:报错:SaTokenContext 上下文尚未初始化 可能1::你在 异步上下文 / 响应式上下文 里调用了 Sa-Token 的同步 API,解决方案参考:[异步 & Mock 上下文](/fun/async--mock) 可能2:访问了一个不存在的路由,而且 SaInterceptor 拦截器里有鉴权代码。 SpringBoot 默认会把 404 请求转发到 `/error`,如果恰好 SaInterceptor 里有鉴权代码,就会造成: 写入上下文 → 进入拦截器(有上下文,可调用鉴权代码) → 发现是404 → 清除上下文 → 将请求转发至 /error -> 再次进入拦截器(无上下文,不可调用鉴权代码) → 报错:SaTokenContext 上下文尚未初始化。 解决方案:将 "/error" 地址排除在拦截器之外: ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { // 鉴权代码 ... })) .addPathPatterns("/**") .excludePathPatterns("/error"); } } ``` ### Q:报错:NotLoginException:xxx 这个错是说明调用接口的人没有通过登录校验,请注意:**通常情况下,异常提示语已经描述清楚了没有通过校验的具体原因:** **如果是:未能读取到有效Token** - 可能1:前端没有提交 Token(最好从前端f12控制台看看请求参数里有 token 吗)。 - 可能2:前端提交了 Token,但是参数名不对。默认参数名是 `satoken`,可通过配置文件 `sa-token.token-name: satoken` 来更改。 - 可能3:前端提交了 Token,但是你配置了框架不读取,比如说你配置了 `is-read-header=false`(关闭header读取),此时你再从 header 里提交token,框架就无法读取到。 - 可能4:前端提交了 Token,但是 Token前缀 不对,可参考:[自定义 Token 前缀](/up/token-prefix) - 可能5:你的项目属于前后端分离架构,此时浏览器默认不自动提交 Cookie,参考:[前后端分离](/up/not-cookie) - 可能6:你使用了 Nginx 反向代理,而且配置了 自定义Token名称,而且自定义的名称还带有下划线(比如 shop_token),而且还是你的项目还是从 Header头提交Token的,此时 Nginx 默认会吞掉你的下划线参数,可参考:[nginx做转发时,带下划线的header参数丢失](https://blog.csdn.net/zfw_666666/article/details/124420828) - 可能7:可能是跨域了,导致前端提交不上 token,看看前端浏览器有没有跨域的报错。 **如果是:Token无效:6ad93254-b286-4ec9-9997-4430b0341ca0** - 可能1:前端提交的 token 是乱填的,或者从别的项目拷过来的,或者多个项目一起开发时彼此的 Token 串项目了。 - 可能2:前端提交的 token 已过期(timeout超时了)。 - 可能3:在不集成 Redis 的情况下:颁发 token 后,项目重启了,导致 token 无效。 - 可能4:在集成 Redis 的情况下:颁发 token 后,Redis重启了,导致 token 无效。 - 可能5:你提交的 token 和框架读取到的 token 不一致: - 可能5.1:比如说你配置了`is-read-header=false`(关闭header读取),然后你从header提交`token-A`,而框架从Cookie里读取`token-B`,导致鉴权不通过(框架读取顺序为`body->header->cookie`) - 可能5.2:比如说你配置了`token-name=x-token`(自定义token名称),此时你从header提交:`satoken:token-A`(参数名没对上),然后框架从header里读取不到你提交的token,转而继续从Cookie读取到了`token-B`。 - 可能6:在集成 jwt 插件的情况下: - 如果使用的是 Simple 模式:情况和不集成jwt一样。 - 如果使用的是 Mixin 和 Stateless 模式:查看这个 token 颁发后是否更改了 `jwtSecretKey` 配置项。 - 可能7:同一账号登录数量超过12个,导致最先登录的被强制注销掉,这个值可以通过 `maxLoginCount` 来配置,默认值12,-1代表不做限制。 - 可能8:在配置了 `is-concurrent=true, is-share=true`的情况下,你和别人共同登录了同一账号,此时对方注销了登录,由于你们使用的是同一个token,导致你这边的会话也失效了。 - 可能9:可能是多账号鉴权的关系,在多账号模式下,如果是 `StpUserUtil.login()` 颁发的token,你从 `StpUtil.checkLogin()` 进行校验,永远都是无效token,因为账号体系没对上。 **如果是:Token已过期:6ad93254-b286-4ec9-9997-4430b0341ca0** - 可能1:前端提交的 token 已被冻结(active-timeout超时了,比如配置了 active-timeout=120,但是超过了120秒没有访问接口)。 - 可能2:集成jwt,而且使用的是 Mixin 或 Stateless 模式,而且token过期了(timeout超时了)。 **如果是:Token已被顶下线:6ad93254-b286-4ec9-9997-4430b0341ca0** - 可能1:在项目配置了 `is-concurrent=false` 的前提下,这个账号又被别人登录了,导致旧登录被挤了下去。 - 可能2:这个账号被 `StpUtil.replaced(loginId, device)` 方法强制顶下线了。 **如果是:Token已被踢下线:6ad93254-b286-4ec9-9997-4430b0341ca0** - 可能1:这个账号被 `StpUtil.kickout(loginId)` 方法强制踢下线了。 ### Q:加了注解进行鉴权认证,不生效? 1. 注解鉴权功能默认关闭,两种方式任选其一进行打开:注册注解拦截器、集成AOP模块,参考:[注解式鉴权](/use/at-check) 2. 在Spring环境中, 如果同时配置了`WebMvcConfigurer`和`WebMvcConfigurationSupport`时, 也会导致拦截器失效. - **常见场景**: 很多项目中会在`WebMvcConfigurationSupport`中配置`addResourceHandlers`方法开放Swagger等相关静态资源映射, 同时基于Sa-Token添加了`WebMvcConfigurer`配置`addInterceptors`方法注册注解拦截器, 这样会导致注解拦截器失效. - **解决方案**: `WebMvcConfigurer`和`WebMvcConfigurationSupport`只选一个配置, 建议统一通过实现`WebMvcConfigurer`接口进行配置. 3. 如果以上步骤处理后仍然没有效果,加群说明一下复现步骤 ### Q:我加了拦截器鉴权,但是好像没有什么效果,请求没有被拦截住? - 可能1:这个拦截器可能没有注册成功。 - 可能2:你访问的请求没有进入这个拦截器。 尝试按照下面的代码测试一下看看: ``` java // 注册拦截器 @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { System.out.println("--------- flag 1"); registry.addInterceptor(new SaInterceptor(handle -> { System.out.println("--------- flag 2,请求进入了拦截器,访问的 path 是:" + SaHolder.getRequest().getRequestPath()); StpUtil.checkLogin(); // 登录校验,只有会话登录后才能通过这句代码 })) .addPathPatterns("/user/**") .excludePathPatterns("/user/doLogin"); } } ``` 在启动时 `flag 1` 被打印出来,才证明拦截器注册成功了,在访问请求时 `flag 2` 被打印出来,才证明请求进入了拦截器。 如果拦截器没有注册成功,则: - 可能1:`SaTokenConfigure` 配置类不在启动类的同包或者子包下,导致没有被 SpringBoot 扫描到。 - 可能2:你的项目启动类上加了 `@ComponentScan("com.xxx")` 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。 - 可能3:项目属于 Maven 多模块项目,`SaTokenConfigure` 和启动类没有在一个模块,且启动类模块没有引入配置类的模块,导致加载不到。 如果拦截器已经注册成功,但请求没有进入拦截器: - 可能1:你访问的 path,没有被 `.addPathPatterns("/user/**")` 拦截住,或者被 `.excludePathPatterns("/xxx/xx")` 排除掉了。 - 可能2:你访问的是另一个项目,请把当前项目停掉,看看你的请求还能不能访问成功。 如果请求进入拦截器也成功了,那可能是: - 可能1:前端访问时提交了会话 Token,且这个 Token 是有效的,通过了拦截器的代码校验。 - 可能2:你访问的 path,和你预期不符,仔细观察一下打印出来的 path 信息,和你的预期相符吗。 注:以上的排查步骤,对过滤器不生效的情形一样适用。 ### Q:我使用拦截器鉴权时,明明排除了某个路径却仍然被拦截了? - 可能1:你的项目可能是跨域了,先把跨域问题解决掉,参考:[解决跨域问题](/fun/cors-filter) - 可能2:你访问的接口可能是404了,SpringBoot环境下如果访问接口404后,会被转发到`/error`,然后被再次拦截。请确保你访问的 path 有对应的 Controller 承接! - 可能3:可能拦截器这里并没有拦截,但是又被其他地方拦截了。请先把这个拦截器给注释掉,看看还会不会拦截,如果依然拦截,那说明不是这个拦截器的锅,请仔细查看一下控制台抛出的堆栈信息,定位一下到底是哪行代码拦截住这个请求的。 - 可能4:后端拦截的 path 未必是你前端访问的这个path(特别是经过网关转发后的path可能会有变化),建议先打印一下 path 信息,看看和你预想的是否一致,再做分析。 ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { try { System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath()); StpUtil.checkLogin(); System.out.println("-------- 此 path 校验成功:" + SaHolder.getRequest().getRequestPath()); } catch (Exception e) { System.out.println("-------- 此 path 校验失败:" + SaHolder.getRequest().getRequestPath()); throw e; } })).addPathPatterns("/**"); } ``` - 可能5:可能你只提交了一个请求,但是浏览器自动帮你提交了其它请求,举个例子:首次访问网站时,浏览器一般会自动提交 `/favicon.ico`,所以**你需要找出是哪个path被拦截了**,怎么找呢?用【可能4】的代码来测试找。 - 可能6:你的项目配置了 `context-path` 上下文地址,比如 `server.servlet.context-path=/shop`,注意这个地址是不需要加在拦截器上的: ``` java // 这是错误示例,不需要把 context-path 上下文参数写在下面的 excludePathPatterns 地址上。 registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin())) .addPathPatterns("/**").excludePathPatterns("/shop/user/login"); // 这是正确示例,无论你的 context-path 上下文配置了什么样的值,下面的 excludePathPatterns 地址都不需要写上它 registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin())) .addPathPatterns("/**").excludePathPatterns("/user/login"); ``` - 可能7:你写了多个匹配规则,请求越过了第一个规则,但又被其它规则拦下来了,例如以下代码: ``` java // 以下代码,当你未登录访问 `/user/doLogin` 时,会被第1条规则越过,然后被第2条拦下,校验登录,然后抛出异常:`NotLoginException:xxx` registry.addInterceptor(new SaInterceptor(handler -> { SaRouter.match("/**").notMatch("/user/doLogin").check(r -> StpUtil.checkLogin()); // 第1个规则 SaRouter.match("/**").notMatch("/article/getList").check(r -> StpUtil.checkLogin()); // 第2个规则 SaRouter.match("/**").notMatch("/goods/getList").check(r -> StpUtil.checkLogin()); // 第3个规则 })).addPathPatterns("/**"); ``` - 可能8:你自定义的封装方法,并没有按照你的预想情况执行: ``` java public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { // 调用自定义的 excludePaths() 方法获取数据排除鉴权 SaRouter.match("/**").notMatch(excludePaths()).check(r -> StpUtil.checkLogin()); })).addPathPatterns("/**"); } // 自定义查询排查鉴权的地址方法 public static List excludePaths() { List list = ... // 从数据源查询...; return list; } ``` 如上方法, `excludePaths()` 可能并不会像你预想的一样正确执行返回相应的值,请在 `.notMatch()` 处 `一律先硬编码写固定死值来测试`,这时就有两种情况: - 情况1:写固定死值时,代码能正常执行了,那说明你自定义的 `excludePaths()` 方法有问题,执行结果不正确。 - 情况2:写固定也不行,那说明不是 `excludePaths()` 的问题,那再从其它地方开始排查。 ### Q:我在配置文件中加了一些关于 Sa-Token 的配置,但是没有生效。 首先,有没有生效的最佳判断方式是,在main方法中加一个打印,看看打印出来的和你配置文件的一致吗: ``` java @SpringBootApplication public class SaTokenApplication { public static void main(String[] args) { SpringApplication.run(SaTokenApplication.class, args); System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); } } ``` 如果不一致,请排查: - 可能1:项目中还存在代码配置,而代码配置会覆盖 `application.yml` 中配置,详细参考:[框架配置](/use/config)。 - 可能2:你的配置文件名字错误,SpringBoot 项目正常情况下配置文件名称应该是:`application.yml` 或 `application.properties`。 - 可能3:可能是你的配置前缀不对,或者配置缩进不对: ``` yaml # 错误示例,多加了 spring 前缀 spring: sa-token: token-name: xxx-token # 错误示例,缩进不对 sa-token: token-name: xxx-token # 正确的应该是以 sa-token 开头 sa-token: token-name: xxx-token ``` ### Q:我自定义了组件,但是好像没有生效? 1、可能组件没有注入成功,排查方法为在 main 里打印这个组件,是否为自定义的class限定名: ``` java @SpringBootApplication public class SaTokenApplication { public static void main(String[] args) { SpringApplication.run(SaTokenApplication.class, args); System.out.println(SaManager.getStpInterface()); // 打印全局的 StpInterface 实现类 } } ``` 如果打印出的是你的自定义实现类,则证明注入成功,如果不是,则证明没有注入成功,请排查: - 自定义的组件实现类上是否加上了 `@Component` 注解,只有加上这个注解,组件才会被 Spring 自动实例化并注入。 - 自定义的组件实现类是否在启动类的同目录或者子目录上,如果不在则无法被 springboot 启动时扫描,扫描不到也就无法注入。 - 启动类上是否加了 `@ComponentScan` 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。 2、这个组件注入成功了,但是还没到执行时机,比如 `StpInterface` 组件,只有在鉴权时才会触发,如果你的代码仅仅是登录校验,就不会执行到这个组件。 ### Q:集成 Redis 后,明明 Redis 中有值,却还是提示无效Token? 根据以往的处理经验,发生这种情况 90% 的概率是因为你找错了Redis,即:代码连接的Redis和你用管理工具看到的Redis并不是同一个。 你可能会问:我看配置文件明明是同一个啊? 我的回答是:别光看配置文件,不一定准确,在启动时直接执行 `SaManager.getSaTokenDao().set("name", "value", 100000);`, 随便写入一个值,看看能不能根据你的预期写进这个Redis,如果能的话才能证明`代码连接的Reids` 和`你用管理工具看到的Redis` 是同一个,再进行下一步排查。 ### Q:报错:无效Same-Token:xxxxxxxxxxx 与之类似的的报错还有: - SSO模式二时,报错:无效ticket:xxxxxxxxxx - OAuth2模块跨多个项目搭建Server时:报错无效 Access-Token:xxxxxx - 微服务做分布式 Session 认证时,报错:无效 Token:xxxxxxxxx - 等等等等.... 这些功能有个统一的特点,就是需要多个项目连接同一个 Redis 才能搭建成功,如果连接的不是同一个 Redis,就会导致 Token / ticket 无法互相认证。 你可能会问:我看配置文件明明就是连接的同一个 Redis 啊? 别急,和上一个问题一样,**不要凭借肉眼检查下定论**,在你的两个服务之间,分别使用以下代码测试一下: ``` java @SpringBootApplication public class SaTokenApplication { public static void main(String[] args) { SpringApplication.run(SaTokenApplication.class, args); // 写值测试:注意一定要用下列方法测试,不要用自己封装的 RedisUtil 之类的测试 SaManager.getSaTokenDao().set("name", "value", 100000); } } ``` 如果都能根据你的预期写进同一个 Redis,那才能证明两个服务确实连接的是同一个 Redis。 实际上,在交流群中提问这些问题的同学,90%的经过以上测试以后,都会发现两者连接的不是同一个 Reids,原因大多是:Redis配置没有生效、使用了 Alone-Redis 之类的…… 如果你是剩下的 10%,那么继续排查:两边的 sa-token 配置是否完全一致,比如 token-name 配置不一致,也会导致数据无法相互认证。最好是把所有 sa-token 相关的配置都复制过去,试验一下看看。 ### Q:我把 token 有效期设置为 30 天,但是总感觉不到 30 天的时候 token 就无效了,怎么回事? - 可能1:你没有为 sa-token 集成 Redis,框架默认将会话数据保存在内存中,项目重启后数据会消失。 - 可能2:你为 sa-token 集成了 Redis,但是 Redis 重启了,导致会话消失。 - 可能3:你配置了 `is-concurrent=false`,不允许同一账号多端登录,有别人登录了这个账号把你顶下去了。 - 可能4:你配置了 `is-concurrent=true`,但是`is-share=false`,同一账号每次登录产生不同的 token,默认最高可以同时登录12个客户端,超过将自动注销最原先的会话。 - 可能5:你的这个账号,别人也登录了,别人调用了注销方法,把你这边的也注销了。`StpUtil.logout()` 为单 token 注销,`StpUtil.logout(10001)` 为账号所有 token 注销。 - 可能6:你虽然 `sa-token.timeout` 配置了 30 天,但是 `sa-token.active-timeout` 配置了较短的值,超过这个时间无操作,token 就过期了。 - 可能7:你换了浏览器,或者换了电脑,或者清空了浏览器最近缓存记录,自然而然需要重新登录。 - 可能8:你中途改了项目配置,比如改了 `sa-token.token-name` 配置项的值,会导致会话保存的 key 发生改变,效果等同于手动清空了 Redis 数据,需要重新登录。 ### Q:有时候我不加 Token 也可以通过鉴权,请问是怎么回事? - 可能1:你访问的这个接口,根本就没有鉴权的代码,所以可以安全的访问通过。 - 可能2:可能是 Cookie 帮你自动提交了 Token,在浏览器或 Postman 中会自动维护Cookie模式,如不需要可以在配置文件:`is-read-cookie: false`,然后重启项目再测试一下。 ### Q:一个 User 对象存进 Session 后,再取出来时报错:无法从 User 类型转换成 User 类型? - 可能1:你的 User 类中途换了包名,导致存进去时和取出来时对不上,无法成功创建实例。 - 可能2:你打开了代码热刷新模式,先存进去的对象,热刷新后再取出,会报错,关闭热刷新即可解决。 ### Q:在 SaServletFilter 中调用 SpringMVCUtil.getRequest() 报错:非Web上下文无法获取Request? - 可能1:项目中有配置类继承了: `extends WebMvcConfigurationSupport`。 - 可能2:项目中有配置类添加了注解: `@EnableWebMvc`。 解决方案:不要加 `@EnableWebMvc`,不要 `extends WebMvcConfigurationSupport`,要 `implements WebMvcConfigurer` 如果一定要 `extends WebMvcConfigurationSupport` ,可以通过手动注册 Spring 上下文初始化过滤器试试: ``` java @Configuration public class SaTokenConfigure extends WebMvcConfigurationSupport { // Spring 上下文初始化过滤器 可能由于各种原因没有被注册到,这里手动帮忙注册一下 @Bean @ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class }) @ConditionalOnMissingFilterBean(RequestContextFilter.class) public static RequestContextFilter requestContextFilter() { System.out.println("--------------------------- 注册了"); // 加个打印语句或者断点确保这里注册到了 return new OrderedRequestContextFilter(); } } ``` 如果不是以上原因,可以加群提供复现demo。 ### Q:我配置了 active-timeout 值,但是当我每次续签时 Redis 中的 ttl 并没有更新,是不是 bug 了? 不更新是正常现象,`active-timeout`不是根据 ttl 计算的,是根据value值计算的,value 记录的是该 Token 最后访问系统的时间戳, 每次验签时用:当前时间 - 时间戳 > active-timeout,来判断这个 Token 是否已经超时。 ### Q:整合 Redis 时先选择了默认jdk序列化,后又改成 jackson 序列化,程序开始报错,SerializationException? 两者的序列化算法不一致导致的反序列化失败,如果要更改序列化方式,则需要先将 Redis 中历史数据清除,再做更新。 ### Q:调用 `StpUtil.getExtra("name")` 报错:`this api is disabled`。 `StpUtil.getExtra(key)` 是给 sa-token-jwt 插件提供的,不集成这个插件就不能调用这个API,如果是普通模式需要存储自定义参数,请在 SaSession 上存储 ``` java // 在登录时缓存参数 StpUtil.getSession().set("name", "zhangsan"); // 然后我们就可以在任意处获取这个参数 String name = StpUtil.getSession().getString("name"); ``` ### Q:我加了 Sa-Token 的全局过滤器,浏览器报错跨域了怎么办? 参考:[https://juejin.cn/post/7247376558367981627](https://juejin.cn/post/7247376558367981627) ### Q:前后端分离项目中,前端使用 vue,如果不打开 porxy 代理的话,调用 Sa-Token 登录不会将 token 自动注入到 Cookie 中,是因为跨域么? 是。 参考:[前后端分离](/up/not-cookie) ### Q:集成redis后对象模型序列化异常 假设执行如下代码: ``` java @Data public class User implements Serializable { private Long userId; private String username; private String password; } ``` ``` java User user = new User(); user.setUserId(10000L); user.setUsername("oneName"); user.setPassword("onePass"); StpUtil.getSession().set("userObjKey", user); // 这里报错 ``` 报错信息如下: ``` SerializationException: Could not read JSON: Cannot deserialize value of type `java.lang.Long` from Array value (token `JsonToken.START_ARRAY`) ``` Springboot 集成 Sa-Token Redis 后, 一旦 Springboot 切换版本就有可能出现此问题 原因是 Redis 里面有之前的 Sa-Token 会话数据, 清空 Redis 即可。 ### Q:我实现了 StpInterface 接口,但是在登录时没有进入我的实现类代码? 不进入是正常现象, StpInterface 是鉴权接口,在执行鉴权代码时才会进入 StpInterface 实现类,登录认证时不会进入。 ### Q:启动时报错,找不到 xx 类 xx 方法: ``` java Caused by: java.lang.ClassNotFoundException: cn.dev33.satoken.same.SaSameTemplate ``` 一般找不到类,或者找不到方法,都是版本冲突了,使用 Sa-Token 时一定要注意**版本对齐**,意思是所有和 Sa-Token 相关的依赖都需要版本一致。 比如说你如果一个依赖是 1.32.0,一个是 1.31.0,就会造成无法启动: ``` xml cn.dev33 sa-token-spring-boot-starter 1.32.0 cn.dev33 sa-token-core 1.31.0 ``` 请仔细排查你的 pom.xml 文件,是否有 Sa-Token 依赖没对齐,**请不要肉眼检查,用全局搜索 "sa-token" 关键词来找**,如果是多模块或者微服务项目,就整个项目搜索。 ### Q:在多账号模式的注解鉴权时,报错:未能获取对应StpLogic,type=xxx 报这个错说明对应 type 的 StpLogic 尚未初始化到全局 StpLogicMap 中,一般会有两种原因造成这种情况: 1. 注解里的 loginType 拼写错误,请改正 (建议使用常量)。 2. 自定义 StpUtil 尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种: - (1) 从main方法里调用一次 - (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 ### Q:使用拦截器鉴权,访问一个不存在的 path 时,springboot 会自动在控制台打印一下异常。 可尝试添加以下配置解决: ``` properties spring.web.resources.add-mappings=false spring.mvc.throw-exception-if-no-handler-found=true ``` ### Q:开启了全局懒加载后,能启动项目,但是访问接口报“未能获取有效的上下文处理器” 开启了全局懒加载后,能启动项目,但是访问接口报异常 `InvalidContextException`: 未能获取有效的上下文处理器, 配置如下: ``` yaml spring: main: lazy-initialization: true ``` 原因是 Sa-Token 自动配置入口类 SaBeanInject 被延迟加载了,只需要手动指定懒加载排除掉 SaBeanInject 就可以了,实现代码如下: ``` java @Configuration class MyConfiguration { @Bean LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() { return LazyInitializationExcludeFilter.forBeanTypes(SaBeanInject.class); } } ``` [经验来源](https://gitee.com/dromara/sa-token/issues/I7EXIU) ### Q:SpringBoot 3.x 路由拦截鉴权报错:No more pattern data allowed after {*...} or ** pattern element 报错原因:SpringBoot3.x 版本默认将路由匹配机制由 `ant_path_matcher` 改为了 `path_pattern_parser` 模式, 而此模式有一个规则,就是写路由匹配符的时候,不允许 `**` 之后再出现内容。例如:`/admin/**/info` 就是不允许的。 如果你的项目报了这个错,说明你写的路由匹配符出现了上述问题,有三种解决方案: 1. 等待 SpringMVC 官方增强 `path_pattern_parser` 模式能力,使之可以支持 `**` 之后再出现内容。 2. 在写路由匹配规则时,避免使 `**` 之后再出现内容。 3. 将项目的路由匹配机制改为 `ant_path_matcher`。 步骤1:先改项目的: ``` yml spring: mvc: pathmatch: matching-strategy: ant_path_matcher ``` 步骤2:再改 Sa-Token 的: ``` java /** * 重写路由匹配算法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容 */ @PostConstruct public void customRouteMatcher() { SaStrategy.instance.routeMatcher = (pattern, path) -> { return SaPatternsRequestConditionHolder.match(pattern, path); }; } ``` **注意点:** SpringBoot2.x 的 `WebFlux`或 `SC Gateway` 项目,按照上述步骤改造,可能会报错 ``` html java.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/condition/PatternsRequestCondition ``` 只需要将“步骤2”中的代码 `return SaPatternsRequestConditionHolder.match(pattern, path);` 更换为 `return SaPathMatcherHolder.getPathMatcher().match(pattern, path);` 即可,例如: ``` java /** * 重写路由匹配算法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容 */ @PostConstruct public void customRouteMatcher() { SaStrategy.instance.routeMatcher = (pattern, path) -> { return SaPathMatcherHolder.getPathMatcher().match(pattern, path); }; } ``` ### Q:Webflux 环境集成,或者 SpringCloud Gateway 环境集成后,过滤器里路由拦截鉴权报错:`java.lang.NoSuchFieldError: defaultInstance` ``` java java.lang.NoSuchFieldError: defaultInstance at cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil.match(SaPathPatternParserUtil.java:40) at cn.dev33.satoken.reactor.spring.SaTokenContextForSpringReactor.matchPath(SaTokenContextForSpringReactor.java:34) at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:58) at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:72) ... ``` 原因:SpringBoot 版本用的太低了,导致一些类不存在。 - 方案一:升级项目的 SpringBoot 版本至 `2.3.x` 以上 - 方案二:像上面的问题解决方案一样,重写一下相关类: ``` java /** * 重写路由匹配算法,将 PathPatternParser.defaultInstance 改为 SaPathMatcherHolder.getPathMatcher() */ @PostConstruct public void customRouteMatcher() { SaStrategy.instance.routeMatcher = (pattern, path) -> { return SaPathMatcherHolder.getPathMatcher().match(pattern, path); }; } ``` ### Q:过低的 SpringBoot 版本引入 Sa-Token 后报错 在低于 2.2.0 时 (不包含2.2.0本身) 的 SpringBoot 项目中引入 Sa-Token 后,项目启动时会报错: ``` txt org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.dev33.satoken.spring.SaBeanInject': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [cn.dev33.satoken.spring.SaBeanInject]: Constructor threw exception; nested exception is java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator ``` 这是由于缺少 jackson 相关依赖导致的,可以手动添加以下依赖来解决: ``` xml com.fasterxml.jackson.core jackson-core 2.17.3 com.fasterxml.jackson.core jackson-annotations 2.17.3 com.fasterxml.jackson.core jackson-databind 2.17.3 ``` ### Q:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。 在项目根目录进入 cmd,执行 `mvn package`,然后重新运行试试。 如果不行,先执行 `maven clean` ,然后删除 .idea 文件夹里除 `icon.png` 外的所有文件,然后执行 `mvn package`,然后重新运行试试。 如果还不行,删除整个项目,重新从 git 地址拉取一遍,再运行。 ### Q:报错:非 web 上下文无法获取 HttpServletRequest。 报错原因解析: Sa-Token 的部分 API 只能在 Web 上下文中才能调用,例如:`StpUtil.getLoginId()` 获取当前用户Id,这个方法第一步需要先从前端提交的参数里获取 token 值, 当你在 main 方法里调用这个 API 时,由于 main 方法本质上不是一个 Controller 请求,所以框架无法完成 *“从前端提交的参数里获取 token 值”* 这一步骤,框架就只能抛出异常。 按照此标准,Sa-Token 的 API 可粗浅的分为两大类: - 必须在 Web 上下文中才能调用的 API,例如:`StpUtil.getLoginId()`、`StpUtil.getTokenValue()` 等等。 - 无需 Web 上下文也能调用的 API,例如:`StpUtil.getLoginType()`、`SaManager.getConfig()` 等等。 此处无法逐一列出到底哪些 API 属于 *“必须依赖 Web 上下文的 API”*,因为太多了,你只需要记住关键的一点: **当一个 API 执行的代码需要先从前端请求中获取一些数据时,这个 API 就属于 *“必须依赖 Web 上下文的 API”*。** 如果你的代码报这个错,说明你在不是 Web 上下文中的地方,调用了 *“必须依赖 Web 上下文的 API”*,请排查: 1. 是否在 main 方法中调用了 *“必须依赖 Web 上下文的 API”*。 2. 是否在带有 `@Async` 注解的方法中调用了 *“必须依赖 Web 上下文的 API”*。 3. 是否在一些丢失 web 上下文的子线程中调用了 *“必须依赖 Web 上下文的 API”*,例如 `MyBatis-Plus` 的 `insertFill` 自动填充。 4. 是否在一些非 Http 协议的 RPC 框架中(例如 Dubbo)调用了 *“必须依赖 Web 上下文的 API”*。 5. 是否在 SpringBoot 启动初始化的方法中调用了 *“必须依赖 Web 上下文的 API”*,例如 `@PostConstruct` 修饰的方法。 6. 是否在定时任务中调用了 *“必须依赖 Web 上下文的 API”*。 ### Q:报错:未能获取有效的上下文处理器。 报错原因解析: 在 sa-token-core 核心包中,Sa-Token 底层不能确认最终运行的 web 容器,所以抽象了 `SaTokenContext` 接口,对接不同容器时需要注入不同的实现, 通常这个注入工作都是框架自动完成的,你只需要按照文档开始部分集成相应的依赖即可。例如: - 如果你使用的 `SpringBoot 2.x`,请引入 `sa-token-spring-boot-starter`。 - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 - 如果你在基于 WebFlux 架构的网关中使用 Sa-Token,请引入 `sa-token-reactor-spring-boot-starter`(3.x 用 `sa-token-reactor-spring-boot3-starter`,4.x 用 `sa-token-reactor-spring-boot4-starter`)。 - 你要在 Solon 中使用 Sa-Token,就引入:`sa-token-solon-plugin`。 - 等等等等…… 如果你的代码报 *“未能获取有效的上下文处理器”* 这个错,大概率是因为你没有正确引入所需的包,导致框架没有注入正确的 `SaTokenContext` 上下文实现,请排查: 1. 如果你的项目是微服务项目,请直接参考:[微服务-依赖引入说明](/micro/import-intro),如果是单体项目,请往下看: 2. 请判断你的项目是 SpringMVC 环境还是 WebFlux 环境: - 如果是 SpringMVC 环境就引入 `sa-token-spring-boot-starter` 依赖,参考:[在SpringBoot环境集成](/start/example) - 如果是 WebFlux 环境就引入 `sa-token-reactor-spring-boot-starter` 依赖,参考:[在WebFlux环境集成](/start/webflux-example) 3. 如果你还无法分辨你是哪个环境,就看你的 pom.xml 依赖: - 如果引入了`spring-boot-starter-web`就是 SpringMVC 环境。 - 如果引入了 `spring-boot-starter-webflux` 就是WebFlux环境。 - 什么?你说你两个都引入了?那你的项目能启动成功吗? 4. 如果是 WebFlux 环境而且正确引入了依赖,依然报错,**请检查是否注册了 SaReactorFilter 全局过滤器,在 WebFlux 下这一步是必须的**,具体还是请参考上面的 [ 在WebFlux环境集成 ] 章节。 5. 需要仔细注意,如果你使用的是 `SpringBoot 3.x` 或 `SpringBoot 4.x`,请分别引入 `sa-token-spring-boot3-starter` 或 `sa-token-spring-boot4-starter`,不要错误引入 `sa-token-spring-boot-starter`,不然会导致框架报错。 6. 如果你的项目开启了全局懒加载(spring.main.lazy-initialization=true)后,能启动项目,但是访问接口报异常,请直接参考:[Q:开启了全局懒加载后,能启动项目,但是访问接口报未能获取有效的上下文处理器](/more/common-questions?id=q:开启了全局懒加载后,能启动项目,但是访问接口报未能获取有效的上下文处理器) 7. 如果以上步骤排除无误后依然报错,请直接提 issue 或者加入QQ群求助。 ## 二、常见疑问 ### Q:登录方法需要我自己实现吗? 是的,不同于`shiro`等框架,`Sa-Token`不会在登录流程中强插一脚,开发者比对完用户的账号和密码之后,只需要调用`StpUtil.login(id)`通知一下框架即可 ``` java // 会话登录接口 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 第一步:比对前端提交的账号名称、密码 if("zhang".equals(name) && "123456".equals(pwd)) { // 第二步:比对成功后,调用通知框架,xxx账号登录成功 StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } ``` ### Q:框架抛出的权限不足异常,我想根据自定义提示信息,可以吗? 可以,在全局异常拦截器里捕获`NotPermissionException`,可以通过`getPermission()`获取没有通过认证的权限码,可以据此自定义返回信息 ``` java @RestControllerAdvice public class GlobalExceptionHandler { // 全局 NotPermissionException 异常捕获 @ExceptionHandler(NotPermissionException.class) public SaResult handlerException(NotPermissionException e) { e.printStackTrace(); return SaResult.error("缺少权限:" + e.getPermission()); } } ``` ### Q:在 SaInterceptor 中,注解鉴权总是先于路由拦截鉴权执行,能调整一下顺序吗? 框架没有提供直接的 API,但你有以下两种方式可以做到这一点: - 方式1:将 SaInterceptor 里的代码复制出来一份,按照你的需求改一下,然后使用你这个自定义的拦截器,不再使用官方的。 - 方式2:注册两次 SaInterceptor 拦截器,例如: ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 路由拦截鉴权 registry.addInterceptor(new SaInterceptor(r -> { // 路由拦截鉴权的代码 ... }).isAnnotation(false)).addPathPatterns("/**"); // 打开注解鉴权 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } } ``` 如上,第一个完成路由拦截鉴权功能,第二个完成注解鉴权功能。 ### Q:我的项目权限模型不是RBAC模型,很复杂,可以集成吗? 无论什么模型,只要能把一个用户具有的所有权限塞到一个List里返回给框架,就能集成 ### Q:StpInterface 接口的 方法,在什么时候执行? 每次鉴权时执行,例如你调用了 `StpUtil.checkgetPermission("xxx")` 方法,框架就会调用底层的 `StpInterface#getPermissionList` 方法来获取权限数据。 如果你的 `getPermissionList` 里有读数据库的代码,那么你每鉴一次权,系统将访问一次数据库。如果要减小性能消耗,可以把权限数据放在缓存中,参考:[把权限放在缓存里](/fun/jur-cache)。 ### Q:当我配置不并发登录时,每次登陆都会产生一个新的 Token,旧 Token 依然被保存在 Redis 中,框架为什么不删除呢? 首先,不删除旧 Token 的原因是为了在旧 Token 再次访问系统时提示他:已被顶下线。 而且这个 Token 不会永远留在 `Redis` 里,在其 TTL 到期后就会自动清除,如果你想让它立即消失,可以: - 方法一:配置文件把 `is-concurrent` 和 `is-share` 都打开,这样每次登陆都会复用以前的旧 Token,就不会有废弃 Token 产生了。 - 方法二:每次登录前把先调用注销方法 `StpUtil.logout(10001)` ,把这个账号的旧登录都给清除了。 - 方法三:写一个定时任务查询 Redis 值进行删除。 ### Q:我使用过滤器鉴权 or 全局拦截器鉴权,结果 Swagger 不能访问了,我应该排除哪些地址? 尝试加上排除 `"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ,"/doc.html/**","/error","/favicon.ico"` 不同版本可能会有所不同,其实在前端摁一下 `F12` 看看哪个 url 报错排除哪个就行了(另附:注解鉴权是不需要排除的,因为 `Swagger` 本身也没有使用 Sa-Token 的注解) ### Q:SaRouter.match 有多个路径需要排除怎么办? 可以点进去源码看一下,`SaRouter.match`方法有多个重载,可以放一个集合, 例如: ``` java SaRouter.match("/**").notMatch("/login", "/reg").check(r -> StpUtil.checkLogin()); ``` ### Q:为什么StpUtil.login() 不能直接写入一个User对象? `StpUtil.login()`只是为了给当前会话做个唯一标记,通常写入`UserId`即可,如果要存储User对象,可以使用`StpUtil.getSession()`获取Session对象进行存储。 ### Q:前后端分离模式下和普通模式有何不同? 主要是失去了`Cookie`无法自动化保存和提交`token秘钥`,可以参考章节:[前后端分离](/up/not-cookie) ### Q:前后端分离时,前端提交的 header 参数是叫 token 还是 satoken 还是 tokenName? 默认是satoken,如果想换一个名字,更改一下配置文件的`tokenName`即可。 ### Q:一个账号拥有哪些权限,可以做成动态的吗? 权限本来就是动态的,框架预留的 `StpInterface` 接口,就是为了让你可以写任意代码来获取数据 ### Q:路由拦截鉴权,可以做成动态的吗? 参考:[把路由拦截鉴权动态化](/fun/dynamic-router-check) ### Q:我不想让框架自动操作Cookie,怎么办? 在配置文件将`isReadCookie`值配置为`false` ### Q:怎么关掉每次启动时的字符画打印? 在配置文件将`isPrint`值配置为`false` ### Q:StpUtil.getSession()必须登录后才能调用吗?如果我想在用户未登录之前存储一些数据应该怎么办? `StpUtil.getSession()`获取的是`Account-Session`,必须登录后才能使用,如果需要在未登录状态下也使用Session功能,请使用`Token-Session`
步骤:先在配置文件里将`tokenSessionCheckLogin`配置为`false`,然后通过`StpUtil.getTokenSession()`获取Session 。或者直接调用 `StpUtil.getAnonTokenSession()` 获取匿名 Token-Session。 ### Q:我只使用header来传输token,还需要打开Cookie模式吗? 不需要,如果只使用header来传输token,可以在配置文件关闭Cookie模式,例:`isReadCookie=false` ### Q:我想让用户修改密码后立即掉线重新登录,应该怎么做? 框架内置 [强制指定账号下线] 的APi,在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()` ### Q:代码鉴权、注解鉴权、路由拦截鉴权,我该如何选择? 这个问题没有标准答案,这里只能给你提供一些建议,从鉴权粒度的角度来看: 1. 路由拦截鉴权:粒度最粗,只能粗略的拦截一个模块进行权限认证 2. 注解鉴权:粒度较细,可以详细到方法级,比较灵活 3. 代码鉴权:粒度最细,不光可以控制到方法级,甚至可以if语句决定是否鉴权 So:从鉴权粒度的角度来看,需要针对一个模块鉴权的时候,就用路由拦截鉴权,需要控制到方法级的时候,就用注解鉴权,需要根据条件判断是否鉴权的时候,就用代码鉴权 ### Q:Sa-Token的全局过滤器我应该怎么指定它的优先级呢? 为了保证相关组件能够及时初始化,框架默认给过滤器注册的优先级为-100,如果你想更改优先级,直接在注册过滤器的方法上加上 `@Order(xxx)` 即可覆盖框架的默认配置 ### Q:timeout 过期了,获取到的 NotLoginException 场景值是-2,按照文档说的应该是-3吧。是我理解的不对还是操作有误? 你的理解是对的,但是框架现在只能做到返回-2,因为 token 过期后,就从 Redis 中消失了,框架没法分辨这个 token 是曾经有过然后过期的,还是从来就没有在Redis中存在过, 所以只能统一抛出-2,这个行为也和具体使用的 SaTokenDao 有关联,例如集成 sa-token-jwt 插件后,框架就能分辨出来是 token 过期了,抛出-3。 ### Q:Sa-Token 是否提供类似 RefreshToken 的概念,与 AccessToken 相互配合刷新令牌鉴权。 关于长短 token,Sa-Token 没有提供直接的 API 支持,但是你可以利用 “临时 token 认证模块” 轻易的达到这一点: 1. 把 `sa-token.timeout` 的值配置小一点,然后把 `StpUtil.login(10001)` 生成的 token 作为短 token ,用来鉴权。 2. 用 “临时 token 认证模块” 生成长 token, `String refreshToken = SaTempUtil.createToken(10001, 2592000);`。 3. 把这两个 token 一起返回到前端。 4. 你再开个接口,可以让前端通过长 token,刷新短 token,参考代码: ``` java @RequestMapping("/refreshToken") public SaResult refreshToken(String refreshToken) { // 1、验证 Object userId = SaTempUtil.parseToken(refreshToken); if(userId == null) { return SaResult.error("无效 refreshToken"); } // 2、为其生成新的短 token String accessToken = StpUtil.createLoginSession(userId); // 3、返回 return SaResult.data(accessToken); } ``` ### Q:前后端一体项目下,在拦截未登录进入登录页面时,如何登录完成后原路返回? 可以在拦截跳转登录页面时,把原 url 作为 back 参数挂载到登录页后方,登录完成后读取 back 参数并跳转 ``` java @RestControllerAdvice public class GlobalException { // 未登录异常拦截 @ExceptionHandler(NotLoginException.class) public Object handlerException(NotLoginException e) { e.printStackTrace(); return SaHolder.getResponse().redirect("/login?back=" + SaHolder.getRequest().getUrl()); } } ``` ### Q:怎么改变请求返回的 http 状态码? ``` java SaHolder.getResponse().setStatus(401) ``` ### Q:Sa-Token 集成 Redis 如何集群? 以 `sa-token-redis-template` 为例:Sa-Token 底层使用的是 RedisTemplate 对象来操作数据的,也就是说,你只要给 RedisTemplate 配置上集群模式,Sa-Token 自动就是集群模式了。 ### Q:多个项目共用同一个 redis,怎么防止冲突? 首先,如无特殊需求,建议多个项目不要共用同一个 redis,如果非要共用,有以下方式避免数据冲突: - 方式 1:使用不同的 db 索引,Redis 默认提供 16 个 database 容器,每个项目配置不同的 db 索引即可。 - 方式 2:给项目配置不同的 `sa-token.token-name` 值,此配置项默认为 `satoken`,是框架在 Redis 存储数据时使用的统一前缀。 - 方式 3:使用 `sa-token-three-redis-jackson-add-prefix` 插件,参考:[sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin)。 ### Q:如何防止 CSRF 攻击? CSRF 攻击的核心在于利用浏览器自动提交 Cookie 的特性,代替用户发送自己不想发送的请求。 **方案一:关闭 Cookie模式。** 在配置文件里配置 `sa-token.is-read-cookie=false` 关闭 Cookie 读取模式,采用 localStorage 存储 token + header 头提交,即可避免 CSRF 攻击。 **方案二:增加 csrf-token 验证** 如果项目必须采用 Cookie 模式验证,可以在请求中增加 csrf-token 验证的环节: 1、在登录时,生成一个 `csrf_token` 返回到前端: ``` java // 测试登录 @RequestMapping("/login") public SaResult login() { StpUtil.login(10001); String csrfToken = StpUtil.getSession().get("csrf_token", () -> SaFoxUtil.getRandomString(60)); return SaResult.ok().set("csrf_token", csrfToken); } ``` 2、前端将 csrf_token 存储在 localStorage 中(注意一定要存储在 localStorage 而非 Cookie 中,存储在 Cookie 中还是可能会被浏览器自动提交) ``` java localStorage.setItem('csrf_token', csrf_token); ``` 每次请求将 csrf_token 塞到 Header 中。 3、在需要防止 CSRF 攻击的接口验证 csrf_token: ``` java @RequestMapping("/test") public SaResult test() { // 先验证 csrfToken String csrfToken = SaHolder.getRequest().getHeader("csrf_token"); if (csrfToken == null || ! csrfToken.equals(StpUtil.getSession().get("csrf_token")) ) { throw new SaTokenException("csrf_token 不匹配"); } // 通过后再处理具体业务 // ... return SaResult.ok(); } ``` 也可以将验证代码写到全局拦截器中,为所有接口提供校验。 ### Q:如何自定义框架读取 token 的方式? **方式一:通过 StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx") 自定义 token 值** 如果你可以在框架读取 token 之前写一些代码,那么你可以通过如下代码自定义当前请求的 token 值: ``` java @RequestMapping("/test") public SaResult test() { System.out.println(StpUtil.getTokenValue()); // 此时读取到的是前端提交的: cebcc930-c0f5-4009-8eb0-1b6aee63b4aa StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx"); System.out.println(StpUtil.getTokenValue()); // 此时读取到的是我们自定义的: abcdefgxxxxxxxx return SaResult.ok(); } ``` **方式二:重写 StpLogic 读取 token 的方法** ``` java @Component public class MyStpLogic extends StpLogic { public MyStpLogic() { super("login"); } // 自定义 token 读取方式,例如此处改为读取请求头为 my-token 的值 @Override public String getTokenValue() { String token = SaHolder.getRequest().getHeader("my-token"); return token; } } ``` ### Q:文档是否能下载?是否有离线版? 文档已完整开源,请访问 Sa-Token 官方仓库,根目录下的 sa-token-doc 文件夹就是文档。 ### Q:还是有不明白到的地方? 请在`gitee` 、 `github` 提交 `issues`,或者加入qq群交流,[群链接](/more/join-group) ================================================ FILE: sa-token-doc/more/content-cooperation.md ================================================ # Sa-Token 内容合作群 **好内容值得被看见!** 为感谢 Sa-Token 的内容创作者们,我们特别创建了「Sa-Token 内容合作群」,帮助大家的内容触达更多 Sa-Token 的使用者 (加群方式在最下方)。 --- ### 📖 1、一些碎碎念,想和写公众号/录视频的朋友们聊聊 前几天,我在公众号上搜索 “Sa-Token”,想看看有没有人写过相关的教程或者踩坑心得。 说实话,当时没抱太大期望。毕竟我们开发团队这几年来几乎将所有精力都放在了代码开发,而一直疏于内容运营建设。 但结果让我挺意外的 —— **我发现了不少公众号都在写 Sa-Token 的文章,而且其中不少都写得很用心**。 有从零开始的入门教程,有深入源码的解析文章,有对框架各个功能的介绍,还有一些结合真实业务场景的实战案例 …… 解决的都是实实在在的问题。 但这些文章的阅读量… 很多都只有几百,有些甚至只有几十。 我知道这很正常。**技术公众号起步非常不容易,粉丝少的时候,再好的内容也很难被看到**。我自己也经历过这个阶段,知道那种 [ 写了一整天,发出来没人看 ] 的感觉。 为了不让这些有价值的内容埋没,我连夜将这些文章整理到了 Sa-Token 官网:[框架博客](/more/blog)。 在整理这些博客的过程中,我突然有了一个想法。 ### ✨ 2、一个可能 [三赢] 的想法 我在想,我为什么不拉一个群聊,把这些为 Sa-Token 写文章的博主们,给聚集起来呢? 只要有朋友写了 Sa-Token 相关文章,都可以转发到群里,我们团队会把这个文章转发到 Sa-Token 所有粉丝群里: 这可能是一个三赢的合作: - **对于 Sa-Token 来说**:能获得更多的优质内容,帮助新用户更快上手,生态也能更丰富。 - **对写文章的朋友来说**:你的好文章能被更多人看到,公众号能涨涨粉,付出的时间更有价值。 - **对 Sa-Token 的用户来说**:能看到更多的技术干货,学到更多知识,找到各种场景的解决方案,不用重复踩坑。 听起来好像…还不错? 所以我打算建个群,名字就叫 「Sa-Token 内容合作群」。不是什么正式的组织,就是一群对技术内容感兴趣的朋友,凑在一起互相帮帮忙。 ### 🤝 3、这个群主要用来做什么? 1、如果你写了 Sa-Token 相关的文章(或录制了视频课程),可以分享到群里,我们团队会把文章: - **转发到 Sa-Token 所有粉丝群里**:Sa-Token 目前拥有 30+ 微信粉丝群 (500人),10+ QQ粉丝群 (1000人 or 2000人)。 - **挂载到 Sa-Token 在线文档博客栏目**:Sa-Token 目前在线文档访问量月PV 20万+。 相信这一定可以大大提高文章的曝光量。 2、我们团队偶尔也会为 Sa-Token 撰写技术文章,发到群里: - 如果你觉得内容不错,想转载到自己的公众号,**直接转就行**。 - 不用专门申请授权,Sa-Token 官方订阅号所有内容均开放版权,任何人都可以自由转载。 ### ❤️ 4、几个你可能关心的小问题 #### Q:我没写过 Sa-Token 的文章,可以加入吗? 可以。完全没问题。哪怕你之前从来没写过 Sa-Token 的文章,但只要你有公众号,想试试写相关的内容,都欢迎。 如果你没有公众号,但是在别的平台,比如掘金、CSDN有写过文章,也可以加入。如果你在 B站/抖音录制过视频,也可以加入。 总之:只要你有意向在任意平台创作 Sa-Token 相关内容,就可以加入。 #### Q:有 KPI 吗?是不是进了群就要为 Sa-Token 写文章? 没有。 想写就写,不想写就不写。哪怕加群后一篇都不写,也没关系。**这就是个「互助群」,不是「任务群」**。 #### Q:文章有什么要求吗? 就一点:认真写。 可以是入门教程、源码解析、实战心得、bug排查、常见踩坑、对比测评…什么形式都行。不需要多长的篇幅,能把一个知识点讲清楚就好。唯一的要求是:不要是纯 AI 生成的粗制滥造文,不要写明显错误的技术观点。 #### Q:对粉丝量有要求吗? 没有要求。 我自己也是从小号做起来的,完全理解起步的难处。群里不分大号小号,只看内容用不用心。 #### Q:除了转发,还有别的吗? 有。 我偶尔会分享一些 Sa-Token 的更新动态、设计思路,或者我发现的其他好的技术文章。大家也可以互相**聊聊技术写作、视频制作的心得,分享好用的工具**。就是个普通的交流群,只不过主题稍微聚焦一点。 说到底,我希望这个群不只是一个内容分发渠道,更是一个 「Sa-Token内容共创伙伴」的聚集地。我们一起,让好的技术方案被更多人看见和使用。 ### 👋 5、怎么加群? 你可以在 [加入讨论群](/more/join-group) 处,添加我们的微信账号, **请在添加时备注或者加好友成功后发送以下信息:[申请加入 Sa-Token 内容合作群]** 。 一定要备注以上信息,否则我们团队人员只会把你拉入到普通粉丝交流群。 Sa-Token 生态合作群 ================================================ FILE: sa-token-doc/more/demand-commit.md ================================================ # 需求提交 文档不清晰?功能不完善?脑袋里有好 idea?提!都可以提! 比起浮夸的赞美,Sa-Token 更希望收到您的批评与建议。 我们深知一个优秀的项目不能闭门造车,它需要海纳百川:[点我在线提交需求](https://wj.qq.com/s2/10852322/0d8b/) 我们将慎重对待每一位粉丝的珍贵意见 ❤️ ❤️ ❤️: - 对框架新增特性功能且比较简单,会在第一时间进行开发。 - 对框架新增特性功能但比较复杂,会延后几个版本制定相应的计划后进行开发。 - 与框架设计理念不太相符,或超出权限认证范畴,将会视需求人数决定是否开发。 ### 其它反馈途径 除了问卷提交,你还可以从以下渠道向我们提交反馈: - Gitee:[issue 提交](https://gitee.com/dromara/sa-token/issues) - GitHub:[issue 提交](https://github.com/dromara/sa-token/issues) - AtomGit:[issue 提交](https://atomgit.com/dromara/sa-token/issues) - 交流群:[加群链接](/more/join-group) 请大胆提交、大胆咨询,请在交流群中大胆艾特我们,请不要有任何害羞 🤭。就算我们不实现,你也不会损失什么,对吧! ================================================ FILE: sa-token-doc/more/join-group.md ================================================ # 加入讨论群 加入 Sa-Token 专属讨论群,与众多大佬一起努力 (huá shuǐ) 成长 (mō yú)。 --- ### 1、加入QQ交流群 QQ交流群:1081649142 [点击加入](https://qm.qq.com/q/SCAaZ6Ros2) ### 2、加入微信交流群: 微信群 PS:扫码添加微信 (备注:sa),邀您加入群聊。
微信群 加入群聊的好处: - 第一时间收到框架更新通知。 - 第一时间收到框架 bug 通知。 - 第一时间收到新增开源案例通知。 - 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。 ### 3、群规(碎碎念): 有同学质问我们,我加了群,为什么被踢了?我们很少踢人,一般只有严重违反群规了我们才会选择踢人。 - 不要在群里发擦边图、视频。轻度我们会警告,重度我们会选择移出群聊。 - 不要在群里聊代理、魔法上网等话题,如有需求请各自互相私聊,不要在群里聊。 - 不要发和程序员无关的广告,和程序员有关的比如开源项目、IT网站等我们一般不管。 请体谅我们,我们拉一个群也不容易,辛辛苦苦几个月才能拉满一个500人群,结果因为一些违规消息就导致封群,很难受的! 被踢了还能再次加群吗?可以,只要你想加,并保证不再发布违规消息,就可以在被踢7天之后再次申请加群。 ### 4、内部群: 为感谢对 Sa-Token 生态做出贡献的同学,我们特创建了内部群:【Sa-Token 生态共享与合作】 加入群聊条件,以下满足其一即可: - 写过5篇以上有关 Sa-Token 的原创博客。 - 为 Sa-Token 开发过插件。 - 有开源项目集成了 Sa-Token,并在 [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) 完成提交。 - 有为 Sa-Token 录制过教程视频,发表在公共平台(总时长>30分钟,且播放量>2000)。 - 其它一些您认为有对 Sa-Token 生态做出贡献的行为,可以直接联系我们,经内部投票评审通过即可加入(不要害羞,大胆联系我们哦 😊 ) 加入群聊的好处: - 更及时的获知 Sa-Token 下一步更新计划。 - 在 Sa-Token 遇到的任何疑问都可以当面与作者沟通,可协助解决问题。 - 可提出未来版本更新需求,将具有更高的优先级进行评审与开发。 QQ群聊号码:939849926 注:此为专属内部群聊,不满足上述条件的同学请勿过分申请打扰,谢谢合作。满足条件者可以在申请加入时备注上您的项目名称 (例如:xx开源项目作者集成了 sa-token,申请加入群聊),如果字数太多无法写完,也可在开源交流群里@管理员协助交流。 ### 5、Sa-Token 内容合作群 专门为 Sa-Token 内容创作者们准备的交流群:[Sa-Token 内容合作群](/more/content-cooperation) ================================================ FILE: sa-token-doc/more/link.md ================================================ # 使用 Sa-Token 的开源项目 > 集成 Sa-Token 的开源案例收集,取自 Awesome-Sa-Token,定期同步: > [Gitee](https://gitee.com/sa-tokens/awesome-sa-token)、 > [GitHub](https://github.com/sa-tokens/awesome-sa-token)、 > [AtomGit](https://atomgit.com/sa-tokens/awesome-sa-token) --- ### 📊 后台管理 - [[ art-design-pro-java ]](https://github.com/anganing/art-design-pro-java):SpringBoot17+Sa-token+Art-Design-Pro+Unibest 技术栈的企业级后台开发管理系统。 - [[ wemirr-platform ]](https://gitee.com/battcn/wemirr-platform):JDK17、SCA2023、SC2024、Sa-Token、VBen5.x 全网最炫酷,功能最多,最优雅地真开源 多租户、SAAS 微服务项目。 - [[ Lucky-Admin-Vue ]](https://gitee.com/xiaodu6/lucky-admin-vue):一个基于vue-admin-template的后台管理框架,集成了动态角色权限,动态路由,角色权限动态配置,日志框架,代码生成,Sa-Token权限校验,快速构建一个后台的开发框架。 - [[ 灯灯]](https://github.com/dromara/lamp-cloud):基于java + SpringCloudAlibaba +SpringBoot 开发的微服务中后台快速开发平台,专注于多租户 (SaaS架构) 解决方案,亦可作为普通项目(非SaaS架构)的基础开发框架使用,目前已实现 数据源隔离、字段隔离、无租户隔离 等几种模式。 - [[ 橙单 ]](https://gitee.com/orangeform/orange-admin):技术栈Boot3 + Flowable7 + Sa-Token + Mybatis-Flex/Mybatis-Plus + Vue3,支持开箱即用且功能完成的工作流和在线表单功能,提供高颜值的流程和表单编辑器全部前后端源码。 - [[ Sz-Admin ]](https://github.com/feiyuchuixue):一个开源RBAC中后台框架,专为现代应用设计。它结合了最新的技术栈,包括后端的Spring Boot 3、JDK 21、Mybatis Flex、Sa-Token、Knife4j和Flyway,以及前端的Vue 3、Vite5、TypeScript和Element Plus,致力于为您提供一个直观、流畅且功能强大的开发体验。 - [[ newbie-boot3 ]](https://github.com/zhangyuge7/newbie-boot3):企业级中大型项目快速开发平台,后端使用JDK21+SpringBoot3+SaToken+MybatisPlus等,前端基于FiveAdminV2后台管理系统模板开发,使用js+vue3+vite5+ElementPlus等最新技术栈。 - [[ EuBackend ]](https://gitee.com/zhaoeryu/eu-backend):EuBackend 是一套全部开源的前后端分离 Java EE 企业级快速开发平台,基于最新技术栈SpringBoot、Sa-Token、MyBatisPlus等作为后端框架,使用RBAC作为权限控制模型,并且毫无保留给个人及企业免费使用。 - [[ srppms ]](https://gitee.com/cai-bin00/srppms):基于SpringBoot+Vue+sa-token前后端分离的科研项目管理平台。 - [[ twelvet-fast ]](https://gitee.com/twelvet/twelvet-fast):基于Spring Boot 3 JDK17的单体服务极速开发管理平台脚手架,先行体验最新技术栈。 - [[ Sa-Plus ]](https://gitee.com/click33/sa-plus):一个基于 SpringBoot 架构的快速开发框架,内置代码生成器。 - [[ dcy-fast ]](https://gitee.com/dcy421/dcy-fast):一个基于 SpringBoot + Sa-Token + Mybatis-Plus 的后台管理系统,前端vue-element-admin,并且内置代码生成器。 - [[ Helio-Boot ]](https://gitee.com/uncarbon97/helio-boot):基于 SpringBoot + Sa-Token + Mybatis-Plus 的单体开发脚手架,带有配套后台管理前端模板及代码生成器;拥有对应微服务版脚手架`Helio-Cloud` - [[ EasyAdmin ]](https://gitee.com/lakernote/easy-admin):一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统,灵活多变可前后端分离,也可单体,内置代码生成器、权限管理、工作流引擎等 - [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token+Mybatis-Plus+Jackson+Xxl-Job+knife4j+Hutool+OSS 定期同步 - [[ SpringBoot_v2 ]](https://gitee.com/bdj/SpringBoot_v2):SpringBoot_v2项目是努力打造springboot框架的极致细腻的脚手架。 - [[ Ruoyi-Satoken ]](https://gitee.com/wangming123456/ruoyi-satoken):为 ruoyi 进行配置 sa-token - [[ vue-satoken-admin ]](https://gitee.com/niluni/vue-satoken-admin):基于Vue2和Sa-Token1.18.0的后台权限系统。 - [[ bootx-platform ]](https://gitee.com/bootx/bootx-platform):包含支付收单(支付宝、微信、聚合、组合支付)、工作流(Flowable)、三方对接(微信、钉钉、企微、短信)等模块,前端基于Vue2和Vue3分别打造,可应用在不同业务场景中,目标是致力实现媲美商业版应用脚手架。 - [[ spba-admin ]](https://gitee.com/qkdja/spring-boot-admin):基于SpringBoot、Vue开发的通用后台管理系统,做到开箱即用,为新项目开发省去了基础功能开发的步骤。主要使用Sa-Token权限认证、MyBatis-Plus、MySQL、Redis、validation、七牛云等技术。 - [[ QForum-Core ]](https://github.com/Project-QForum/QForum-Core/):QForum 论坛系统官方核心,可拓展性强、轻量级、高性能、前后端分离,基于 SpringBoot2 + Sa-Token + Mybatis-Plus - [[ ExciteCMS-Layui ]](https://gitee.com/ExciteTeam/ExciteCMS-SpringBoot-Layui):ExciteCMS 快速开发脚手架:一款后端基于 SpringBoot2 + Sa-Token + Mybatis-Plus,前端基于 Layuimini 的内容管理系统,具备RBAC、日志管理、代码生成等功能,并集成常用的支付、OSS等第三方服务,拥有详细的开发文档 - [[ sra-admin ]](https://github.com/CoCoTeaNet/sra-admin):快速开发脚手架,核心依赖:springboot3+sqltoy+satoken+hutool | 轻量级 | 只实现了用户、字典、角色、权限等常见功能,能够快速搭建一个web项目。 - [[ QuickBuild ]](https://gitee.com/CodeLiQing/custom-quick-build-platform): 快速构建 | 基于springboot+sa-token+neety+代码生产器(生成vue页面和增删改查代码)| 以及前端vue3和字节arco.design框架整合 - [[ magic-boot ]](https://gitee.com/ssssssss-team/magic-boot):基于 magic-api + Sa-Token 搭建的快速开发平台,可以实现在浏览器编写Vue代码,既改即生效 - [[ chaos ]](https://gitee.com/qishanor/chaos):一个基于 SpringBoot + Sa-Token + Mybatis-Plus的快速开发框架,前端vue-element-avue,内置代码生成器,代码最简洁,最佳学习实践方案。 - [[ xzadmin ]](https://gitee.com/xiaozhizxj/xzadmin):一个基于 Spring Boot+mybatis-plus+sotaken+Redis+Thymeleaf+hutool+easy-captcha+log4j的后台管理系统 - [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + AntDesignVue3 + Vite + SpringBoot + Mp + HuTool + SaToken - [[ XyyAdmin ]](https://gitee.com/xyy12611/springboot-xyy-admin-v3):开箱即用的前后端分离后台权限系统,关键技术SpringBoot、Sa-Token、MySql、Vue3、AntDesignVue。 - [[ Frsimple ]](https://gitee.com/frsimple/springboot):一个基于 SpringBoot + Sa-token + Tdesign-next + vite + vue3 + typescript 的开箱即中后台服务解决方案。 - [[ sa-admin-server ]](https://gitee.com/wlf213/sa-admin-server):sa-admin-server是一个后台管理框架的服务端,核心技术:SpringBoot+SaToken+Quartz+Cache+Redis+Netty+MyBatisPlus; 亮点:RABC动态权限+零SQL+定时任务+缓存+在线IM; 前后端可分离也可一体部署,可选七牛云对象存储和本地存储两种方式。 - [[ RuoYi-Vue-CMS ]](https://gitee.com/liweiyi/RuoYi-Vue-CMS):RuoYi-Vue-CMS是前后端分离的内容管理系统,支持站群管理、多平台静态化、元数据模型扩展、多语言、全文检索,能轻松组织各种复杂内容形态。技术栈:SpringBoot3 + VUE2 + MybatisPlus + Sa-Token + xxl-job + Freemarker + ES + Redis + MySQL。 - [[ springboot-multi-tenant-sa-token ]](https://gitee.com/willf/springboot-multi-tenant-sa-token):轻量的多租户后台管理系统脚手架(SpringBoot,Sa-Token,mybatis-plus,Vue & Element)。 - [[ solon_angis_beetlsql ]](https://gitee.com/smartcity/solon_angis_beetlsql):并元国产开发平台 solon、sa-token、beetlsql、smart-http - [[ zeta-kotlin ]](https://gitee.com/xia5800/zeta-kotlin):zeta-kotlin是使用kotlin语言基于spring boot、mybatis-plus、sa-token等框架开发的项目脚手架。 - [[ nebula-swagger-demo ]](https://gitee.com/flgitee/nebula-swagger-demo):springboot+nebula 集成knife4j案例 - [[ warm-sun]](https://gitee.com/min290/warm-sun):基于solon+vue3开发,jdk17+satoken+redisx/redisson+mybaits-flex+hutool+jackson+mapstruct+poi - [[ContiNew Admin]](https://gitee.com/Charles7c/continew-admin):ContiNew Admin 中后台管理框架/脚手架,Continue New Admin,持续以最新流行技术栈构建,拥抱变化,迭代优化。当前采用的技术栈:Vue3、TypeScript、Arco Design Vue、Spring Boot3(JDK17)、Undertow、Sa-Token、JWT、MariaDB、MyBatis Plus、Redis、Redisson、Easy Excel、Hutool 等。 - [[laymini-admin]](https://gitee.com/wlf213/laymini-admin):基于layuimini前端框架开发的一个简单的后台管理前后端不分离框架,主体技术mybatisplus+sa_token+springboot+freemarker,主要功能:RABC认证授权,后台管理功能,集成Quartz动态定时任务。 - [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin):SmartAdmin国内首个以「高质量代码」为核心,「简洁、高效、安全」中后台快速开发平台;基于SpringBoot + Sa-Token + Mybatis-Plus 和 Vue3 + Vite5 + Ant Design Vue 4.x (同时支持JavaScript和TypeScript双版本);满足国家三级等保要求、支持登录限制、接口数据国产加解密、高防SQL注入等一系列安全体系。 - [[ Halcyon-Admin ]](https://github.com/hhfb8848/halcyon-springboot):基于 Spring Boot 3 和 Vue 3 的通用后台管理系统,专注于提供基本的管理功能,而非特定的部门管理或业务功能。 - [ breeze-boot-satoken-xxx系统 ]:breeze-boot-satoken-xxx 是一个开源免费(前后端分离)中后台管理系统基础解决方案,前端技术栈:( Vue3、 TypeScript、Element Plus、Pinia 、Vite)后端技术栈:(jdk17、 springboot3、SaToken、MybatisPlus等) - SSO 版本,后端:https://gitee.com/breeze-boot/breeze-boot-satoken-sso - SSO 版本,前端:https://gitee.com/breeze-boot/breeze-vite-ui-satoken-sso - OAUTH 版本,后端:https://gitee.com/breeze-boot/breeze-boot-satoken-oauth - OAUTH 版本,前端: https://gitee.com/breeze-boot/breeze-vite-ui-satoken-oauth - [[ Summer-Flowers · 夏花 ]](https://gitee.com/Luv404/summer-flowers):基于 **Spring Boot 3 + JPA + QueryDSL + Sa-Token** 的企业级后台开发框架,前端采用 **SoybeanAdmin**。不同于常见 MyBatis 体系,Summer-Flowers 以 **Entity 作为业务第一表达**,通过 QueryDSL 实现类型安全的复杂查询,配合代码生成器与模块化架构,显著降低中长期项目的维护成本。 ### 🚀 微服务相关 - [[ XHan Admin ]](https://gitee.com/sun-xiaohan/xh-admin-frontend):XHan Admin 是一个开源免费(前后端分离)中后台管理系统基础解决方案, 无专业版收费,所有功能毫无保留的贡献给开源社区,使用最新技术栈全新开发,无任何历史代码包袱。 - [[ RuoYi-Cloud-Plus ]](https://gitee.com/dromara/RuoYi-Cloud-Plus):重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba + Sa-Token + Dubbo + Mybatis-Plus + Xxl-Job 全方位升级 定期同步 - [[ Sp-Cloud ]](https://gitee.com/click33/sp-cloud):Sa-Plus的微服务版本, 基于Spring-Cloud-Alibaba,微服务下使用Sa-Token的样例 - [[ YC-Framework ]](http://framework.youcongtech.com/):致力于打造一款优秀的分布式微服务解决方案 - [[ falser-cloud ]](https://gitee.com/falser/falser-cloud): 基于 SpringCloud Alibaba + SpringCloud gateway + SpringBoot + Sa-Token + vue-admin-template + Nacos + Rabbit MQ + Redis 的一个后台管理系统,前后端分离,权限管理,菜单管理,数据字典,停车场系统管理等功能 - [[ dcy-fast-cloud ]](https://gitee.com/dcy421/dcy-fast-cloud):一个基于 SpringCloudAlibaba + Sa-Token + dubbo2.7.8 + Seata + knife4j + Mybatis-Plus + MapStruct + 的后台管理系统,前端vue-element-admin,并且内置代码生成器+动态路由权限等功能 - [[ fhs-framework ]](https://gitee.com/fhs-opensource/fhs-framework):基于Springboot+Springcloud + Mybatis Plus + Sa-Token + Vue + ElementUI 的快速开发平台(低代码开发平台),本框架永远免费,永久全开源 - [[ Pig-Satoken ]](https://gitee.com/wchenyang/cloud-satoken):重写 Pig 授权方式为 Sa-Token,其他代码不变。 - [[ Helio-Cloud ]](https://gitee.com/uncarbon97/helio-cloud):基于 SpringBoot + SpringCloud Alibaba + Sa-Token + Mybatis-Plus 的微服务开发脚手架,带有配套后台管理前端模板及代码生成器 - [[ BudWk-V7 ]](https://gitee.com/budwk/budwk):基于 NutzBoot + Sa-Token + Dubbo + Nacos注册&配置中心 的微服务开发脚手架(同时提供单应用版本),带有配套后台管理前端模板及代码生成器 - [[ xr-satoken-cloud ]](https://gitee.com/fzhxfw/xr-satoken-cloud):一款基于SaToken轻量级Java权限认证框架构建的微服务后台开发脚手架,基于SpringCloud + SpringCloudAlibaba + Nacos + SaToken + Mybatis等技术搭建,内置RBAC权限管理,代码生成器,文件分片速传等,本项目完全开源免费,定期提交代码到dev开发分支,由个人开发者业余时间维护升级。 - [[ CloudEon ]](https://gitee.com/dromara/CloudEon):一款基于kubernetes的开源大数据平台,旨在为用户提供一种简单、高效、可扩展的大数据解决方案。 - [[ quick-boot ]](https://github.com/csx-bill/quick-boot):一款基于 Spring Cloud 2022 、Spring Boot 3、AMIS 和 APIJSON 的低代码系统。 - [[ linkin-platform ]](https://gitee.com/paohaizi/linkin-platform):Springboot + Springcloud + nacos + Mybatis Plus + Sa-Token + Vue3 + ElementPlus 微服务下使用Sa-Token的样例,是一套比较简洁的后台系统。 - [[ LangChat ]](https://github.com/TyCoding/langchat):( OpenAI / Gemini / Ollama / Azure / 智谱 / 阿里通义大模型 / 百度千帆大模型), Java生态下AI大模型产品解决方案,快速构建企业级AI知识库、AI机器人应用 ### 🛒 商城 - [[ litemall-plus ]](https://gitee.com/ysling-org/litemall-plus):微信小程序SaaS商城系统,可支持多小程序同时运行。 - [[ mall4j ]](https://gitee.com/gz-yami/mall4j):基于Spring Boot 3 JDK17的一个商城手脚架。 - [[ Huanxing-mall ]](https://gitee.com/lijiaxing_boy/huanxing-mall):HuanXing 商城基于SpringCloud 2021 & Alibaba + Sa-token,前端基于 Vue3 +Element plus 的微服务商城 ### 📝 博客 - [[ jthink ]](https://gitee.com/wtsoftware/jthink): 一个基于 SpringBoot + Sa-Token + Thymeleaf 的博客系统 - [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog):一款vue+springboot前后端分离的博客系统,博客后台管理系统使用了vue+elmentui开发,后端使用Sa-Token进行权限管理,支持动态菜单权限,动态定时任务,文件支持本地和七牛云上传,使用ElasticSearch作为全文检索服务,支持QQ、微博、码云登录。 - [[ June 12 ]](https://gitee.com/hanshaung/ants):June 12 是一个纯开源免费的资讯/博客类网站,基于Spring Boot + Sa-Token + Vue开发。 - [[ YuanBlog ]](https://gitee.com/wlf213/yuan-blog):一款代码简单,功能丰富的多人社交博客平台。前后端分离,Vue+SpringBoot3,博客前端使用Quasar,后台管理前端使用NaiveUI,博客后端,后台管理后端分为两个系统,均使用Sa-Token进行认证授权。支持邮箱验证码登录。 - [[ 鸢尾博客 ]](https://gitee.com/lxwise/iris-blog_parent):鸢尾博客是一个基于Spring Boot+Vue3 + TypeScript + Vite+JavaFx的客户端和服务器端的博客系统。项目采用前端与后端分离,支持移动端自适应,配有完备的前台和后台管理功能。后端使用Sa-Token进行权限管理,支持动态菜单权限,服务健康监控,数据流量统计,支持QQ、微博、码云、GitHub等三方登录。 ### 🔌 插件 - [[ Sa-Token-Plugin ]](https://gitee.com/bootx/sa-token-plugin):Sa-Token第三方插件实现,基于Sa-Token-Core,提供一些与官方不同实现机制的的插件集合,作为Sa-Token开源生态的补充 - [[ quarkus-sa-token ]](https://github.com/quarkiverse/quarkus-sa-token): quarkus 整合 Sa-Token。 ### 🌐 多语言 - Rust:[[ sa-token-rust ]](https://github.com/sa-tokens/sa-token-rust): 一个轻量级、高性能的 Rust 认证授权框架。 - Go:[[ sa-token-go ]](https://github.com/sa-tokens/sa-token-go): 一个轻量级、高性能的 Go 权限认证框架。 - PHP:[[ real-token ]](https://gitee.com/jinan-jimeng-network_0/real-token): 一个轻量级 thinkphp6 权限认证框架,让鉴权变得简单、优雅! ### 📦 其它 - [[ Glowxq-OJ ]](https://github.com/glowxq/glowxq-oj):Glowxq-OJ 专业开源在线编程测评系统 | 基于Spring Boot 3.x + Java 21 + Vue 3构建 | 支持ACM/ICPC竞赛、信奥赛训练、编程教育 | 多语言判题、实时竞赛、在线IDE | Docker一键部署 | Modern Online Judge Platform for Competitive Programming & Coding Education。 - [[ FlyFlow ]](https://gitee.com/junyue/flyflow):基于SaToken开发的开源工作流系统:FlyFlow借鉴了钉钉与飞书的界面设计理念,致力于打造一款用户友好、快速上手的工作流程工具。 - [[ Sa-Token-Study ]](https://gitee.com/sa-tokens/sa-token-study):以demo示例的方式讲解 Sa-Token 源码涉及到的技术点,连载中…… - [[ SpringMvc+Sa-Token ]](https://gitee.com/SRD_01/spring-mvc-sa-token): Jsp+SpringMVC+SSO+Sa-Token+Redis | Spring MVC 集成 SaToken Demo 项目 - [[ iot-kit ]](https://gitee.com/iotkit-open-source/iotkit-parent):一个轻量级低门槛的物联网平台,包含了多协议设备接入、规则引擎、第三方平台接入、智能家居小程序等模块的项目,基于SpringBoot架构并集成了Sa-Token的OAuth2认证。 - [[ cubic ]](https://gitee.com/dromara/cubic):一站式问题定位平台,实时线程栈监控、线程池监控、动态arthas命令集、依赖分析等等等,助你快速定位问题。 - [[ ChatGPT-WEB ]](https://github.com/dulaiduwang003/ChatGPT-WEB):基于JDK17+SpringBoot3+UniApp 绘图 聊天 充值应用。(Web版本) - [[ SuperBot-ChatGPTApp ]](https://github.com/dulaiduwang003/SuperBot-ChatGPTApp):基于JDK17+SpringBoot3+UniApp 绘图 聊天 充值应用。(小程序版本) - [[ ScribbleHub ]](https://github.com/dulaiduwang003/ScribbleHub):基于SpringBoot+satoken+wxss开发的博客小程序 - [[ TIME-SEA-chatgpt ]](https://github.com/dulaiduwang003/TIME-SEA-chatgpt):基于SpringBoot+satoken+vue3+uniapp开发的多端Ai平台应用 - [[ SUPERBOT-GPT]](https://github.com/dulaiduwang003/SUPERBOT-GPT):基于SpringBoot3+satoken+uniapp开发的流量主小程序 - [[ DaxPay ]](https://gitee.com/dromara/dax-pay):一款免费开源的支付网关系统,支持支付宝、微信、云闪付等通道,提供收单、退款、聚合支付、对账、分账等功能。 - [[ Dinky ]](https://github.com/DataLinkDC/dinky):基于Apache Flink的实时数据开发平台,实现敏捷的数据开发、部署和运维 - [[ mldong ]](https://gitee.com/mldong/mldong):SpringBoot + Vue3 快速开发平台、自研工作流引擎 ================================================ FILE: sa-token-doc/more/noun-intro.md ================================================ # Sa-Token 名词解释 Sa-Token 无意发明任何晦涩概念提升逼格,但在处理 issue 、Q群解答时还是发现不少同学因为一些基本概念理解偏差导致代码出错, 所以整理本篇针对一些比较容易混淆的地方加以解释说明。 也希望各位同学在提交 issue、Q群提问之前充分阅读本篇文章,保证不要因为基本概念理解偏差,增加不必要的沟通成本。 --- #### 几种 Token - token:指通过 `StpUtil.login()` 登录产生的身份令牌,用来维护用户登录状态,也称:satoken、会话Token。 - temp-token:指通过 `SaTempUtil.createToken()` 临时验证模块产生的Token,也称:临时Token。 - Access-Token:在 OAuth2 模块产生的身份令牌,也称:访问令牌、资源令牌。 - Refresh-Token:在 OAuth2 模块产生的刷新令牌,也称:刷新令牌。 - Same-Token:在 SaSameUtil 模块生成的Token令牌,用于提供子服务外网隔离功能。 #### 两种过期时间: - timeout:会话 Token 的长久有效期。 - active-timeout:会话 Token 的最低活跃频率。 两者的差别详见:[Token有效期详解](/fun/token-timeout) #### 三种Session: - Account-Session:框架为每个账号分配的 Session 对象,也称:账号Session。 - Token-Session:框架为每个 Token 分配的 Session 对象,也称:令牌Session。 - Custom-Session:以一个特定的值作为SessionId,来分配的 Session 对象,也称:自定义Session。 三者差别详见:[Session模型详解](/fun/session-model) #### 账号标识: - loginId:账号id,用来区分不同账号,通过 `StpUtil.login(id)` 来指定。 - device:登录设备类型,例如:`PC`、`APP`,通过 `StpUtil.login(id, device)` 来指定。 - loginType:账号类型,用来区分不同体系的账号,如同一系统的 `User账号` 和 `Admin账号`,详见:[多账号认证](/up/many-account) #### 几种登录策略: - 单地登录:指同一时间只能在一个地方登录,新登录会挤掉旧登录,也可以叫:单端登录。 - 多地登录:指同一时间可以在不同地方登录,新登录会和旧登录共存,也可以叫:多端登录。 - 同端互斥登录:在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线,参考腾讯QQ的登录模式:手机和电脑可以同时在线,但不能两个手机同时在线。 - 限量登录:限定账号登录设备总数量,低于此数量时可以正常登录,高于此数量后每次登录自动清退一个之前的登录。 - 记住我模式:指在一个设备终端登录成功,该设备重启之后依然保持登录状态。 - 单点登录:在进入多个系统时,只需要登录一次即可。解决用户在不同系统间频繁登录的问题。 - 同端多登录:指在一个终端可以同时登录多个账号。 #### 几种注销策略: - 单端注销:只在调用登录的一端注销。 - 全端注销:一端注销,全端下线。 - 同端注销:发起注销后,同类型设备端一起下线,不同设备类型不受影响。 - 单点注销:与单点登录对应,一个系统注销,所有系统一起下线。

视频讲解:如何设计出最优的登录会话策略?单端登录、强制下线、多端互踢、记住我登录

#### 几种鉴权方式: - 代码鉴权:在代码里直接调用 `StpUtil.checkXxx` 相关 API 进行鉴权。 - 注解鉴权:在方法或类上添加 `@SaCheckXxx` 注解进行鉴权。 - 路由拦截鉴权:在全局过滤器或拦截里通过:`SaRouter.match()` 拦截路由进行鉴权。 ================================================ FILE: sa-token-doc/more/sa-token-donate-old.md ================================================ # 赞助 Sa-Token --- Sa-Token 采用 Apache-2.0 开源协议,**承诺框架本身与官网文档永久免费开放**, 但是框架的日常更新与社区运营需要付出大量的精力,靠爱发电难以长久,如果 Sa-Token 帮助到了您,您可以友情支持一下 Sa-Token。 ### 友情赞助 您可以在项目 [Gitee](https://gitee.com/dromara/sa-token) 主页进行捐赠 **已捐赠列表:**

| 赞助人 | 赞助金额 | 留言 | 时间 | | :-------- | :-------- | :-------- | :-------- | | [时间很快](https://gitee.com/frsimple) | ¥ 220.0 | 感谢您的开源项目! | 2023-10-27 | | [立秋](https://gitee.com/code_wh) | ¥ 2.5 | 感谢您的开源项目! | 2023-10-27 | | [PotatoLoofah](https://gitee.com/PotatoLoofah) | ¥ 10.0 | 感谢您的开源项目! | 2023-10-27 | | [ly-chn](https://gitee.com/ly-chn) | ¥ 99.0 | 一定的资金支持有助于开源项目走的更加长远 | 2023-10-17 | | [yangs2w](https://gitee.com/yangs2w) | ¥ 10.0 | 感谢您的开源项目! | 2023-10-10 | | [lee](https://gitee.com/cngeeklee) | ¥ 10.0 | 真正的轻量级权限安全框架,希望继续更新 | 2023-10-06 | | [yang](https://gitee.com/hansdm) | ¥ 10.0 | 感谢您的开源项目! | 2023-09-27 | | [明道云](https://gitee.com/lunan-yn) | ¥ 200.0 | 明道云2023年伙伴大会,[报名链接](https://www.mingdao.com/event/mpc/2023) | 2023-09-25 | | [shenlicao](https://gitee.com/shenlicao) | ¥ 10.0 | 感谢您的开源项目! | 2023-09-15 | | [lostyue](https://gitee.com/lostyue) | ¥ 20.0 | 感谢您的开源项目! | 2023-09-14 | | [huni](https://gitee.com/simin_sizi) | ¥ 10.0 | 感谢您的开源项目! | 2023-09-11 | | [T_T](https://gitee.com/wm26hua) | ¥ 20.0 | 感谢您的开源项目! | 2023-09-07 | | [Meteor](https://gitee.com/meteoroc) | ¥ 2.5 | 感谢您的开源项目! | 2023-08-23 | | [刘斌](https://gitee.com/xuanfather) | ¥ 20.0 | 感谢您的开源项目! | 2023-08-17 | | [快快乐乐小码农](https://gitee.com/happy-little-farmer) | ¥ 1.0 | 感谢您的开源项目! | 2023-08-17 | | [失败女神](https://gitee.com/failedgoddess) | ¥ 50.0 | 感谢您的开源项目! | 2023-08-03 | | 结弦奏(微信打赏) | ¥ 50.0 | 感谢您的开源项目! | 2023-08-07 | | [好心肠的老哥](https://gitee.com/ntdm) | ¥ 10.0 | 非常好的开源项目,希望越来越好! | 2023-08-02 | | [XiaoYi](https://gitee.com/getianit) | ¥ 100.0 | [亚洲云深圳BGP云服务器](https://www.asiayun.com/cart?action=configureproduct&pid=300) | 2023-07-24 | | [张兆伟](https://gitee.com/zhang865700) | ¥ 50.0 | 感谢您的开源项目! | 2023-07-24 | | [mikeinshanghai](https://gitee.com/mikeinshanghai)| ¥ 50.0 | Sa-Token, MeterSphere共成长,共辉煌! | 2023-07-14 | | 吴其敏(微信打赏) | ¥ 200.0 | [CAT 是基于 Java 开发的实时应用监控平台,为美团点评提供了全面的实时监控告警服务。](https://github.com/dianping/cat) | 2023-07-11 | | [Dear胜哥](https://gitee.com/DearShengGe) | ¥ 10.0 | 有幸在摸鱼时间认真看完了全文档,感觉很是不错。开源不易,望作者继续扩展该框架功能! | 2023-06-30 | | [SP](https://gitee.com/LSP1999) | ¥ 10.0 | 就是需要这种简单上手的项目 | 2023-06-15 | | [javahuang](https://gitee.com/javahrp) | ¥ 200.0 | [SurveyKing:功能最强大的调查问卷系统和考试系统,开源](https://gitee.com/surveyking/surveyking) | 2023-06-08 | | [dyjgitdyjgit](https://gitee.com/qtinfogit) | ¥ 20.0 | 感谢您的开源项目! | 2023-05-22 | | 砰嚓嚓(QQ打赏) | ¥ 20.0 | 一点打赏不成敬意 | 2023-05-15 | | [xc_Moving](https://gitee.com/fireZhang) | ¥ 20.0 | 感谢您的开源项目!感谢SA-token帮我度过项目的难关 | 2023-05-11 | | [BeckJin](https://gitee.com/beckjin666) | ¥ 100.0 | [明道云-零代码开发平台,快速响应业务需求。从“IT背锅侠”,变成“IT英雄”。](https://mingdao.com?s=st) | 2023-05-08 | | [SummerHy](https://gitee.com/hurumo) | ¥ 10.0 | 国产,就是棒,:) | 2023-05-07 | | [gdl](https://gitee.com/gdl97) | ¥ 20.0 | 感谢您的开源项目!作者牛逼! | 2023-04-29 | | [bootx](https://gitee.com/bootx) | ¥ 100.0 | [Bootx-Platform:支付收单、三方对接、后端基于 Spring Boot、Spring Cloud 应用脚手架](https://gitee.com/bootx/bootx-platform) | 2023-04-18 | | c(微信打赏) | ¥ 100.0 | 感谢您的开源项目! | 2023-04-17 | | [hurumo](https://gitee.com/hurumo) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-17 | | [李广龙](https://gitee.com/ak47-b) | ¥ 20.0 | 跟大哥学习一辈子学不完 | 2023-04-14 | | [Admin](https://gitee.com/jinan-jimeng-network_0) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-12 | | [王宁波](https://gitee.com/wang-ningbo) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-10 | | F(微信打赏) | ¥ 20.0 | 感谢您的开源项目! | 2023-04-09 | | [zhou](https://gitee.com/mrzhou1) | ¥ 50.0 | 感谢答疑 | 2023-03-29 | | [Java_小生](https://gitee.com/zhang_hanzhe) | ¥ 10.0 | 感谢Sa-Token让我不用去B站肯几十个小时的教程,框架很优秀文档更优秀 | 2023-03-09 | | 空空(微信打赏) | ¥ 10.0 | 感谢您的开源项目! | 2023-03-08 | | [李一博](https://gitee.com/haust_lyb) | ¥ 8.88 | 感谢您的开源项目! | 2023-03-07 | | [陈乾](https://gitee.com/qianpou) | ¥ 50.0 | 感谢您的开源项目! | 2023-03-07 | | [陈乾](https://gitee.com/qianpou) | ¥ 20.0 | 感谢您的开源项目! | 2023-03-05 | | [熊孩子](https://gitee.com/xhz1230) | ¥ 20.0 | 感谢您的开源项目! | 2023-02-17 | | [不问烟雨](https://gitee.com/xiaominfagui) | ¥ 10.0 | 牛 | 2023-01-12 | | [tsing](https://gitee.com/tsing666) | ¥ 10.0 | 感谢您的开源项目! | 2023-01-08 | | [SWmachine](https://gitee.com/SWmachine) | ¥ 10.0 | 您的开源很好用! | 2023-01-07 | | [Peter Z](https://gitee.com/zj1995) | ¥ 10.0 | 感谢您的开源项目! | 2022-12-26 | | [ken](https://gitee.com/affction) | ¥ 10.0 | 感谢您的开源项目! | 2022-12-19 | | [刘涛](https://gitee.com/doILike) | ¥ 10.0 | 感谢您的开源项目! | 2022-12-13 | | [时间很快](https://gitee.com/frsimple) | ¥ 50.0 | 感谢您的开源项目! | 2022-11-29 | | [ThatYear](https://gitee.com/wangmuqing) | ¥ 20.0 | 感谢您的开源项目! | 2022-11-24 | | [IlovePea](https://gitee.com/IlovePea) | ¥ 10.0 | 感谢您的开源项目! | 2022-11-22 | | [feel](https://gitee.com/xujiahuim) | ¥ 10.0 | 感谢您的开源项目! | 2022-11-17 | | [laruui](https://gitee.com/laruui) | ¥ 10.0 | 感谢您的开源项目! | 2022-10-28 | | [就眠儀式](https://gitee.com/Jmysy) | ¥ 50.0 | 感谢您的开源项目! | 2022-10-26 | | [王文博](https://gitee.com/rl520) | ¥ 20.0 | 感谢您的开源项目! | 2022-10-24 | | [feyong](https://gitee.com/feyong) | ¥ 10.0 | 感谢您的开源项目! | 2022-10-18 | | [xueshize](https://gitee.com/xueshize) | ¥ 20.0 | 感谢您的开源项目! | 2022-10-12 | | [西东](https://gitee.com/noear_admin) | ¥ 99.0 | 感谢您的开源项目! | 2022-10-05 | | [BlueRose](https://gitee.com/Bluerose_2) | ¥ 20.0 | 感谢您的付出,项目非常棒! | 2022-09-22 | | [邱道长](https://gitee.com/qiudaozhang) | ¥ 20.0 | 优秀的项目,赞 | 2022-09-09 | | [jerrydo](https://gitee.com/jerrydo) | ¥ 10.0 | 感谢您的开源项目!很强大! | 2022-08-10 | | [小北宸呀](https://gitee.com/a_aas) | ¥ 10.0 | 感谢您的开源项目!我就喜欢你这种把我当白痴的官方文档 | 2022-07-08 | | [jwc_gitee](https://gitee.com/jwc-gitee) | ¥ 10.0 | 感谢您的开源项目! | 2022-07-07 | | [zhihong](https://gitee.com/zzh13520704819) | ¥ 20.0 | 感谢您的开源项目! | 2022-06-20 | | [风如歌](https://gitee.com/the-wind-is-like-a-song) | ¥ 10.0 | 这个框架简直满足了我所有对于安全框架的需求,赞一个,加油sa-token加油中国开源! | 2022-06-17 | | [qiuyue](https://gitee.com/bmlt) | ¥ 10.0 | satoken牛逼 | 2022-06-16 | | [刘时立](https://gitee.com/liu-shili) | ¥ 10.0 | 非常棒的开源项目! | 2022-06-13 | | [yuncai929](https://gitee.com/null_448_5562) | ¥ 10.0 | 感谢您的开源项目! | 2022-06-10 | | [sun_2020](https://gitee.com/sun-two-thousand-and-twenty) | ¥ 50.0 | 感谢您的开源项目! | 2022-06-08 | | [LZ](https://gitee.com/FUNKBOY) | ¥ 6.66 | 感谢您的开源项目!顺便踩一脚Spring Security,sa加油! | 2022-05-18 | | [cray](https://gitee.com/hyy6300) | ¥ 10.0 | 感谢您的开源项目! | 2022-05-10 | | [别处理](https://gitee.com/zshnb) | ¥ 10.0 | 非常好的项目,希望能一直做下去 | 2022-05-01 | | [李洪星](https://gitee.com/li_hong_xing) | ¥ 10.0 | 解决了很多之前项目中遇到的问题。感谢您的开源项目! | 2022-04-29 | | [乡村阿土哥](https://gitee.com/895995040) | ¥ 10.0 | 感谢您的开源项目! | 2022-04-29 | | [Horatio201](https://gitee.com/horatio201) | ¥ 20.0 | 太牛了! | 2022-04-25 | | [阿文](https://gitee.com/qq921124136) | ¥ 20.0 | 很好的框架,在开发文档里学到了很多知识点 | 2022-04-21 | | 行长 (微信打赏) | ¥ 20.0 | 微信打赏 | 2022-04-15 | | [xq584](https://gitee.com/xq584) | ¥ 10.0 | 感谢您的开源项目! | 2022-04-08 | | [yukihane](https://gitee.com/yukihane) | ¥ 10.0 | 感谢您的开源项目! | 2022-04-07 | | [alkinn](https://gitee.com/alkinn) | ¥ 10.0 | 感谢您的开源项目! | 2022-03-29 | | [lele](https://gitee.com/lelez) | ¥ 10.0 | 感谢您的开源项目! | 2022-03-29 | | Robin Tin (微信打赏) | ¥ 28.88 | 微信打赏 | 2022-03-24 | | [刘嘉威](https://gitee.com/liu_jiawei) | ¥ 6.66 | 真滴好用~ | 2022-03-23 | | 秦政 (微信打赏) | ¥ 20.00 | 微信打赏 | 2022-03-22 | | 秦政 (微信打赏) | ¥ 6.66 | 微信打赏 | 2022-03-22 | | 黎子豪 (微信打赏) | ¥ 18.88 | 请你喝杯咖啡 | 2022-03-21 | | [Charles7c](https://gitee.com/Charles7c) | ¥ 20.0 | 感谢您的开源项目!希望 SSO 模块越来越好! | 2022-03-17 | | [晓辉](https://gitee.com/zxhShow) | ¥ 10 | 感谢您的开源项目! | 2022-03-07 | | [老杨](https://gitee.com/yangs914) | ¥ 6.66 | 感谢您的开源项目! | 2022-03-01 | | 赵津 (微信打赏) | ¥ 16.00 | 微信打赏 | 2022-02-20 | | [前世男友](https://gitee.com/lanbaba666) | ¥ 10 | 感谢您的开源项目! | 2022-02-17 | | 两岁 (微信打赏) | ¥ 188 | 微信打赏 | 2021-12-27 | | 刚子 (微信打赏) | ¥ 50 | 微信打赏 | 2021-12-27 | | [网络小渣渣](https://gitee.com/a9777) | ¥ 10 | 感谢您的开源项目! | 2021-12-24 | | [周周周杨](https://gitee.com/ChaoGeWanJiu) | ¥ 10 | 感谢您的开源项目! | 2021-12-18 | | [MrXionGe](https://gitee.com/MrXionGe) | ¥ 10 | SA加油~~ | 2021-12-17 | | [duyiliu](https://gitee.com/duyiliu) | ¥ 10 | 化繁为简,是门艺术。 | 2021-12-16 | | [liu](https://gitee.com/liuliuliu123456) | ¥ 50 | 感谢您的开源项目! | 2021-12-15 | | [fuhouyin](https://gitee.com/fuhouyin) | ¥ 10 | 感谢您的开源项目! | 2021-12-01 | | [图灵谷](https://gitee.com/stephenson37) | ¥ 20 | 感谢您的开源项目! | 2021-11-30 | | [luyuan](https://gitee.com/meitesi) | ¥ 20 | 感谢您的开源项目! | 2021-11-29 | | [xiaoyan](https://gitee.com/l-yun) | ¥ 200 | 感谢您的开源项目! | 2021-11-26 | | [yijunzhao](https://gitee.com/yijunzhao) | ¥ 20 | 感谢您的开源项目! | 2021-11-21 | | [万声鹉](https://gitee.com/wanshengwu) | ¥ 10 | 感谢您的开源项目! | 2021-11-15 | | [Taller](https://gitee.com/evilatom) | ¥ 10 | 感谢您的开源项目! | 2021-11-13 | | [公子骏](https://gitee.com/dt_flys) | ¥ 20 | 感谢您的开源项目! | 2021-11-08 | | [铂赛东](https://gitee.com/bryan31) | ¥ 20 | 开源加油! | 2021-11-08 | | [孔孔的空空](https://gitee.com/kongmr) | ¥ 100 | 感谢您的开源项目! | 2021-11-02 | | [songfazhun](https://gitee.com/fzsong) | ¥ 10 | 感谢您的开源项目! | 2021-10-28 | | [ithorns](https://gitee.com/ithorns) | ¥ 10 | 感谢您的开源项目! | 2021-10-25 | | [xiaoyan](https://gitee.com/l-yun) | ¥ 200 | 节日快乐 | 2021-10-24 | | [apifox001](https://gitee.com/apifox001) | ¥ 200 | [Apifox:API 文档、API 调试、API Mock、API 自动化测试](https://apifox.com/) | 2021-10-15 | | [永夜](https://gitee.com/cn-src) | ¥ 20 | 感谢您的开源项目! | 2021-09-18 | | [苏永晓](https://gitee.com/suyongxiao) | ¥ 10 | 感谢您的开源项目! | 2021-09-01 | | [xiaoyan](https://gitee.com/l-yun) | ¥ 200 | 好的作者理应被认可 | 2021-08-24 | | [xiaoyan](https://gitee.com/l-yun) | ¥ 50 | be better | 2021-07-31 | | [孔孔的空空](https://gitee.com/kongmr) | ¥ 500 | 感谢您的开源项目! | 2021-07-30 | | [Wizzer](https://gitee.com/wizzer) | ¥ 20 | 感谢您的开源项目! | 2021-05-22 | | [二范先生](https://gitee.com/mr-er-fan) | ¥ 20 | 省长加油啊 喝杯茶 | 2021-03-16 | | [萧瑟](https://gitee.com/fengduidui) | ¥ 20 | 感谢您的开源项目! | 2021-03-16 | | [xue1992wz](https://gitee.com/xue1992wz) | ¥ 20 | 感谢您的开源项目! | 2021-03-16 | | [whcrow](https://gitee.com/whcrow) | ¥ 20 | 军师加油! | 2021-03-16 | | [RockMan](https://gitee.com/njx33) | ¥ 10 | 感谢您的开源项目! | 2020-12-17 | | [zhangjiaxiaozhuo](https://gitee.com/zhangjiaxiaozhuo)| ¥ 10 | 感谢您的开源项目! | 2020-12-15 | | [知知](https://gitee.com/double_zhi) | ¥ 10 | 感谢您的开源项目! | 2020-12-15 | | [省长](https://gitee.com/click33) | ¥ 10 | java中最好用的权限认证框架! | 2020-12-15 |
0 位用户赞助, 点击展开 ↓ 点击收起 ↑
感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ! ### 商业赞助 一次性赞助 200 元或以上,可帮助您的产品在 Sa-Token 交流群艾特全体成员推广一次。 并同时在赞助列表处高亮产品链接。 Sa-Token 目前总计14+微信交流群(每个群人数大约400以上,总计人数5000+),4个QQ交流群(2000人群和1000人群,总计人数6000+),大部分为 java 开发工程师,可有效推广您的产品。 - 优先推广和程序员相关的互联网产品,比如:低代码开发平台、网课、开发软件、云服务器、个人博客等等,实体产品如键盘、显示器、耳机等等,如果是和程序员无关的产品,可酌情考虑是否推广。 - 拒绝接受违反法律法规、以及灰色相关的产品推广,为避免不必要的麻烦,目前也拒绝推广IP代理、上网工具等等。 - 为避免过多打扰群友,目前一天内至多在群里推广两次,超过次数的可顺延到第二天。 有意见合作者可直接在 gitee 发起赞赏后,将您的产品信息发至 [ sa-pro 小助手 ] ,加好友链接点 [这里](https://sa-pro.yun94.cn/),点击后往下拉有二维码,添加时请备注:Sa-Token 商业赞助。 注:在群发消息时,我们会明确对群友声明,此条消息为赞助商的产品,属于硬广信息。如您介意这一点,我们将暂时无法与您合作。 群发消息类似于以下: ``` txt 感谢 xx 老板对 Sa-Token 的商业赞助,以下是老板的产品,大家感兴趣的可以关注一下: xxx 商品名称 链接:https://xxxxxn.com/xxx ``` > 目前不接受 Sa-Token 官网文档插入广告推广,只接受一次性商业赞助推广。 ================================================ FILE: sa-token-doc/more/sa-token-donate.md ================================================ # 赞助 Sa-Token --- Sa-Token 采用 Apache-2.0 开源协议,**承诺框架本身与在线文档永久免费开放**, 但是框架的日常更新与社区运营需要付出大量的精力,靠爱发电难以长久,如果 Sa-Token 帮助到了您,您可以友情支持一下 Sa-Token。 ### 友情赞助 微信赞赏码 **已捐赠列表:**
日期排序 | 赞助额排序
赞助人 赞助金额 留言 时间
第 1/1 页
感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ! ================================================ FILE: sa-token-doc/more/tj-gzh-hz.md ================================================ # 公众号合作 --- ### 推荐须知: Sa-Token作为一个新兴项目,迫切需要一定的途径进行项目推广
如果您也是java公众号运营者,欢迎您将Sa-Token框架推荐给您的粉丝: 1. 您无需为Sa-Token专门撰写文案,只需要复制项目仓库的 Readme 内容即可,可参考:[链接](https://mp.weixin.qq.com/s/xMCedNj6Nti2BwGzS9A0mg) 2. 在文章底部或内容中留下项目官网或者GitHub仓库链接 3. 文章需至少 1000+ 的阅读量 作为回报,Sa-Token将: 1. 在框架官方文档 [[推荐公众号]](/more/tj-gzh) 处留下您的公众号二维码(按照推荐日期倒叙排列) 2. 在框架官方交流群里@全体成员推广您的公众号一次 3. 您的公众号所有新推文章都可以将链接发送到Sa-Token交流群中,增加阅读量(为避免频繁推送连接,请不要超过一周三次)
如果您还有除公众号以外的其它途径可以与Sa-Token相互推荐,欢迎加群交流……(群链接在首页) ================================================ FILE: sa-token-doc/more/tj-gzh.md ================================================ # 推荐公众号 --- ### Java技术公众号:
Java大飞哥 专注软件开发、技术架构设计、源码分享、JAVA技术。 小简聊开发 偶尔更新前后端技术文章,偶尔发布生活散文,更新随缘。浏览器搜索JanYork了解我更多。 Java笔记虾 专注于Java技术栈,推送 Spring全家桶,Dubbo等相关技术知识 程序猿DD 一线技术工作者的学习、生活与见闻 TJ君 一个励志推荐10000款开源项目与免费工具的程序猿
JavaGuide 专注Java后端学习和大厂面试的公众号! Hollis 《Java工程师成神之路》系列作者 macrozheng 专注Java技术分享,作者Github开源项目mall(40K+Star) 终码一生 分享Java开发技术(JVM,多线程,高并发,性能调优) CodeSheep 一只爱技术的程序羊,想把分享变成一种习惯!
Java开发宝典 分享Java基础、Java框架、数据库、微服务、中间件、分布式、架构等技术干货 MarkerHub 专注于梳理java知识,解析开源项目。 架构师必备 分享干货文章,做一个有逼格的架构师社区! Github导航站 分享好玩有趣、新奇、实用的开源项目 Java爱好者 分享Java开发编程资源和Java技术文章
IT大咖说 大咖干货,不再错过。 Java编程 专注Java技术分享,Java基础知识/数据结构/算法 大侠学JAVA 道阻且长,行则将至,专注分享JAVA领域的干货 Java大厂面试官 一名一直奋斗在一线的程序员, 记录工作用遇到的问题和解决方案 程序员大神 有一个在程序员圈混了10年的老程序员, 分享程序员相关的精选内容教程
码农学习联盟 码农学习联盟,程序员码农学习第一站!Java、Python、大数据、机器学习、人工智能 程序猿 传播编程经验,挖掘程序员优秀的学习资源。 架构师必备 分享干货文章,做一个有逼格的架构师社区! GitHuboy 专注于分享 Python、Java、Web、AI、数据分析等多个领域的优质学习资源 开源最前线 推荐热门开源软件,介绍新开源项目,报导开源资讯!
Dromara开源组织 Dromara开源组织官方公众号
Gitee GitHub 官网
方志朋 主要分享Java、Python等技术,用大厂程序员的视角来探讨技术进阶、面试指南、职业规划等。 Java仓库 专注Java全栈开发,分享实用技术干货。 Java技术江湖 一位阿里 Java 工程师的技术小站, 分享技术干货和学习经验 java那些事 分享java中各种新技术的应用方法,做一个潮流的java技术人!
Java研发军团 Java系列文章个人博客,MySQL、SSM、Redis、Spring Java团长 分享些技术干货,致力于Java全栈开发! 武哥聊编程 你若对得起时间,时间便对得起你~ 我是武哥!谢谢你的关注~每天进步一点点! Java笔记虾 专注于Java技术栈,推送 Spring全家桶,Dubbo,Zookeeper,Redis,Linux,多线程 Java项目学习 关注我,我来带你从零开始做Java项目!
程序员追风 专注于分享Java各类学习笔记、面试题以及IT类资讯。 程序员编程 每天下午13:30分发文,主要发布开源项目、面试题、最新技术资讯、干货学习资源~ 写代码的渣渣鹏 关注我,学好Java。Spring Boot、 微服务、高并发、多线程、JVM、Spring Cloud GitHub精选 专注于分享优质的开源项目、学习资源,Java、Python、Go、Web 前端、AI、数据分析 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。

感谢以上公众号对 Sa-Token 项目的推荐 ================================================ FILE: sa-token-doc/more/update-log.md ================================================ # 更新日志 ### v1.45.0 @2026-3-8 - core: - 新增:新增重复登录处理策略,当同一账号不允许多客户端同时登录时支持选择踢人下线或拦截本次登录。 **[重要]** merge: [pr 349](https://gitee.com/dromara/sa-token/pulls/349) - 修复:修复 `StpUtil.getLoginIdByTokenNotThinkFreeze` 方法缺少 `static` 修饰符的问题。 - 优化:优化路由匹配 pattern 缓存算法,消除魔法值。merge: [pr 907](https://github.com/dromara/Sa-Token/pulls/907) - 优化:移除冗余导包。 - 插件: - 新增:新增 `sa-token-jackson3` 插件,用于 Jackson 3 的 JSON 操作。 **[重要]** - 新增:新增 `sa-token-jackson3-test` 单元测试。 - 新增:新增 `sa-token-snack4` 插件。 **[重要]** merge: [pr 356](https://gitee.com/dromara/sa-token/pulls/356) - 修复:修复 Dubbo 上下文清理问题。 merge: [pr 889](https://github.com/dromara/Sa-Token/pulls/889) - 新增:loveqq-framework 版本更新。merge: [pr 351](https://gitee.com/dromara/sa-token/pulls/351) - starter: - 新增:新增 `sa-token-spring-boot4-starter` 集成包,支持 Spring Boot 4 环境集成。 **[重要]** - 新增:新增 `sa-token-reactor-spring-boot4-starter` 集成包,支持 Reactor + Spring Boot 4 环境集成。 **[重要]** - 新增:新增 `sa-token-demo-springboot4`、`sa-token-demo-webflux-springboot4` 示例。 - 新增:新增 Spring Boot 4 整合 demo 示例。 - 重构: - 重构:重构 `sa-token-dependencies` 相关模块,优化依赖关系。 **[重要]** - 重构:重构 Spring Boot WebMVC/Reactor 相关集成包,优化依赖关系。 **[重要]** - 优化:优化整体模块依赖关系。 - Solon: - 优化:`sa-token-solon-plugin` 优化 Gateway 接口的处理,避免使用路由接口。merge: [pr 348](https://gitee.com/dromara/sa-token/pulls/348) - SSO: - 新增:sso-server 前后端分离模式下 平台中心模式 demo 示例。 - 修复:SSO 模块 msgType 参数说明、API 说明修正。 - 新增:SSO 模块视频讲解链接:B站 王清江唷 SSO篇(29集)。 **[重要]** - 补全:SSO 模块内置消息处理器相关文档。 - 新增:文档为 `sa-token-sso` 模块定义 STS 协议。 **[重要]** - OAuth2: - 修复:修复 `sa-token-oauth2` 组件使用 `sa-token-fastjson2` 序列化导致的类型转换问题。merge: [pr 355](https://gitee.com/dromara/sa-token/pulls/355) - 优化:修改 `ClientIdSecretModel` 的读取构建逻辑。merge: [pr 346](https://gitee.com/dromara/sa-token/pulls/346) - 文档: - 同步:同步公众号文章列表、博客列表、赞助者名单、企业登记案例。 - 新增:新增 Sa-Token 内容合作者群。 **[重要]** - 新增:新增《Gitee 2025 年度开源项目 Web 应用开发 Top 2》证书展示。 - 新增:新增赞赏码展示、文档首页 stars 对比图。 - 新增:新增解决跨域专题文章。 - 新增:增加微信群聊信息展示。 - 优化:优化框架 Slogan。 - 优化:优化 README、案例库展示。 - 优化:文档主题切换增加水滴特效,调整主题色块顺序。 - 优化:文档优化 [登录认证]、[权限认证]、[路由拦截鉴权] 篇。 - 优化:补全全局策略说明、数据结构说明。 - 新增:目录树增加专门栏目记录项目架构设计。 - 优化:功能结构图增加点击事件跳转到对应功能文档。 - 优化:子服务外网隔离章节增加示意图。 - 优化:Same-Token 同源系统认证图示说明。 - 修复:更换 GitCode logo 为 AtomGit。 - 修复:更换 QQ 群链接、微信群聊展示图。 - 修复:文档图片地址更换为本地文件。 - 修复:错别字修复。 - 修复:maven-pull.md 文档,解决父子项目依赖下载问题。 - 新增:Maven 父子项目无法下载依赖的问题解决方案。merge: [pr 358](https://gitee.com/dromara/sa-token/pulls/358) - 修复:订正文档错别字。merge: [pr 354](https://gitee.com/dromara/sa-token/pulls/354) - 修复:文档内代码示例修正。merge: [pr 347](https://gitee.com/dromara/sa-token/pulls/347) - AI: - 新增:新增 organize-update-log SKILL,用于格式化整理版本更新日志信息。 - 新增:新增 commit-message SKILL,用于整理 git commit 日志信息。 - 新增:新增 upgrade-version SKILL,用于统一升级修改版本号。 - 新增:新增 remove-redundancy-import SKILL,用于检查 Java 类中无效冗余导包并移除。 - 其它: - 新增:readme 增加快问快答区域。 - 新增:增加忽略 .vscode 目录。 - 优化:注释优化。 - 重构:备忘录重构为专门的文件夹。 - 重构:调整项目发布配置至 Maven Central Portal。merge: [pr 792](https://github.com/dromara/Sa-Token/pull/792) - 优化:部分构建配置升级到最新版。 ### v1.44.0 @2025-6-7 - 修复:修复 sso-server 前后端分离示例无法正常登录的问题。 - 修复:修复 SSO 模式三全端注销失效的问题。 - 修复:修复 SSO `SaSsoClientModel` 部分场景下无法序列化的问题。 - 新增:OAuth2 模块新增支持从 `SaOAuth2DataLoader` 接口获取高级权限与低级权限的方法。merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/339) - 修复:修复 `sa-token-dubbo` 与 `sa-token-dubbo3` 每次调用都强制需要上下文的问题。 - 文档:新增 `sa-token-dubbo3` 的说明。 - 文档:更新赞助者名单。 - 文档:新增 `loveqq-framework` 框架集成包。 **[重要]** merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/340) ### v1.43.0 @2025-5-17 - core: - 新增:`SaLogoutParameter` 新增 `deviceId` 参数,用于控制指定设备 id 的注销。 **[重要]** - 新增:新增 `SaHttpTemplate` 请求处理器模块。 - 新增:TOTP 增加 `issuer` 字段。 merge: [pr 329](https://gitee.com/dromara/sa-token/pulls/329) - 修复:修复 `Http Digest` 认证时 url 上带有查询参数时认证无法通过的问题。merge: [pr 334](https://gitee.com/dromara/sa-token/pulls/334) - 新增:@SaCheckOr 注解添加 `append` 字段,用于抓取未预先定义的注解类型进行批量注解鉴权。 - 新增:侦听器 `doRenewTimeout` 方法添加 loginType 参数。 - 新增:`SaInterceptor` 新增 `beforeAuth` 认证前置函数。 - SSO: - 新增:单点注销支持单设备注销。 **[重要]** fix: [#IA6ZK0](https://gitee.com/dromara/sa-token/issues/IA6ZK0) 、[#747](https://github.com/dromara/Sa-Token/issues/747) - 新增:新增消息推送机制。 **[重要]** fix: [#IBGXA7](https://gitee.com/dromara/sa-token/issues/IBGXA7) - 新增:配置项 clients 用于单独配置每个 client 的授权信息。 **[重要]** - 新增:配置项 `allowAnonClient` 决定是否启用匿名 client。 - 新增:SSO 模块新增配置文件方式启用“不同 client 不同秘钥”能力。 - 重构:sso-client 封装化获取 client 标识值。 - 新增:新增 SSO Strategy 策略类。 - 新增:sso-client 新增 `convertCenterIdToLoginId`、`convertLoginIdToCenterId` 策略函数,用于描述本地 LoginId 与认证中心 loginId 的转换规则。 - 新增:sso-server 新增 `jumpToRedirectUrlNotice` 策略,用于授权重定向跳转之前的通知。 - 优化:调整整体 SSO 示例代码。 - 新增:新增 ReSdk 模式对接示例:`sa-token-demo-sso3-client-resdk`。 **[重要]** - 新增:新增匿名应用模式对接示例:`sa-token-demo-sso3-client-anon`。 **[重要]** - OAuth2: - 新增:`SaClientModel` 新增 `isAutoConfirm` 配置项,用于决定是否允许应用可以自动确认授权。 **[重要]** - 新增:多 `Access-Token` 并存、多 `Refresh-Token` 并存、多 `Client-Token` 并存能力。 **[重要]** fix: [#IBHFD1](https://gitee.com/dromara/sa-token/issues/IBHFD1) 、 [#IBLL4Q](https://gitee.com/dromara/sa-token/issues/IBLL4Q) 、[#724](https://github.com/dromara/Sa-Token/issues/724) - 新增:Scope 分割符支持加号。merge: [pr 333](https://gitee.com/dromara/sa-token/pulls/333) - 修复:修复 oidc 协议下,当用户数据变动后,id_token 仍是旧信息的问题。 - 优化:对 `OAuth2 Password` 认证模式需要重写处理器添加强提醒。 - 优化:将认证流程回调从 `SaOAuth2ServerConfig` 转移到 `SaOAuth2Strategy`。 - 新增:新增 `SaOAuth2Strategy.instance.userAuthorizeClientCheck` 策略,用于检查指定用户是否可以授权指定应用。fix: [#553](https://github.com/dromara/Sa-Token/issues/553) - 优化:优化调整 `sa-token-oauth2` 模块代码结构及注释。 - 新增:`currentAccessToken()`、`currentClientToken()`,简化读取 `access_token`、`client_token` 步骤 - 插件: - 新增:新增 `sa-token-forest` 插件,用于在 Http 请求处理器模块整合 Forest。 - 新增:新增 `sa-token-okhttps` 插件,用于在 Http 请求处理器模块整合 OkHttps。 - 拆分:API Key 模块拆分独立插件包:`sa-token-apikey`。 - 拆分:API Sign 模块拆分独立插件包:`sa-token-sign`。 - 修复:修复 `sa-token-dubbo` 插件部分场景上下文控制出错的问题。 - 修复:修复 `sa-token-sanck3` `SaSessionForSnack3Customized:getModel` 接收 map 值时会出错的问题。 merge: [pr 330](https://gitee.com/dromara/sa-token/pulls/330) - 修复:修复使用 `sa-token-redis-template-jdk-serializer` 时反序列化错误。merge: [pr 331](https://gitee.com/dromara/sa-token/pulls/331) - 修复:`sa-token-snack3` 优化 `objectToJson` 序列化处理(增加类名,但不增加根类名)。 - 重构:重构 `sa-token-redis-template`、`sa-token-redis-template-jdk-serializer` 插件中 update 方法 ttl 获取方式改为毫秒,以减少 update 时的 ttl 计算误差。 **[重要]** - 示例: - 新增:新增 SSE 鉴权示例。 - 文档: - 新增:新增文档离线版下载。 - 新增:新增框架功能列表插图。 - 新增:新增示例:如何在响应式环境下的 Filter 里调用 Sa-Token 同步 API。 - 新增:新增 QA:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。 - 新增:新增 QA:新增QA:报错:SaTokenContext 上下文尚未初始化。 - 新增:新增 QA:在 idea 导入源码,运行报错:java: 程序包cn.dev33.satoken.oauth2不存在。 - 新增:重写路由匹配算法修正为最新写法。 - 新增:修复 OAuth2 UnionId 章节相关不正确描述。 - 优化:完善 QA:访问了一个不存在的路由,报错:SaTokenContext 上下文尚未初始化。 fix: [#771](https://github.com/dromara/Sa-Token/issues/771) - 优化:补充 sso 模块遗漏的配置字段介绍。 - 优化:OAuth2-Server 示例添加真正表单。 - 新增:文档新增重写 `PasswordGrantTypeHandler` 处理器示例。 - 新增:sso 章节和 oauth2 章节文档增加可重写策略说明。 - 其它: - 新增:readme 新增框架功能介绍图。 - 新增:SSO 模块新增思维导图说明。 - 新增:readme 新增 Forest 的友情链接。 ### v1.42.0 @2025-4-11 更新导读:[视频](https://www.bilibili.com/video/BV1h85izzEe8/)、[文字版](https://juejin.cn/post/7491971657201451062) - core: - 新增: 新增 `API Key` 模块。 **[重要]** - 新增: 新增 `TOTP` 实现。 **[重要]** - 重构:重构 `TempToken` 模块,新增 value 反查 token 机制。 **[重要]** - 升级: 重构升级 `SaTokenContext` 上下文读写策略。 **[重要]** - 新增: 新增 Mock 上下文模块。 **[重要]** - 删除: 删除二级上下文模块。 - 新增: 新增异步场景使用 demo。 **[重要]** - 新增: 新增 `Base32` 编码工具类。 - 新增:新增 `CORS` 跨域策略处理函数,提供不同架构下统一的跨域处理方案。 - 新增:`renewTimeout` 续期方法增加 token 终端信息有效性校验。 - 新增: 全局配置项 `cookieAutoFillPrefix`:cookie 模式是否自动填充 token 前缀。 - 新增: 全局配置项 `rightNowCreateTokenSession`:在登录时,是否立即创建对应的 `Token-Session`。 - 优化:优化 `Token-Session` 获取算法,减少缓存读取次数。 - 新增:`SaLoginParameter` 支持配置 `SaCookieConfig`,以配置 Cookie 相关参数。 - 修改:防火墙校验过滤器的注册顺序 修改为 -102。 - 新增:防火墙 `hook` 注册新增 `registerHookToFirst`、`registerHookToSecond` 方法,以便更灵活的控制 hook 顺序。 - 插件: - 新增: `sa-token-quick-login` 插件支持 `Http Basic` 方式通过认证。 - 单元测试: - 补全:补全 `Temp Token` 模块单元测试。 - 文档: - 补全:补全赞助者名单。 - 修复:修复 `Thymeleaf` 集成文档不正确的依赖示例说明。 - 修复:修复 `unionid` 章节错误描述。 - 优化:采用更细致的描述优化SSO模式三单点注销步骤。 - 新增:登录认证文档添加 Cookie 查看步骤演示图。 - 新增:多账号模式新增注意点:运行时不可更改 `LoginType`。 - 新增: 多账号模式QA:在一个接口里获取是哪个体系的账号正在登录。 - 新增:新增QA:解决低版本 `SpringBoot (<2.2.0)` 引入 Sa-Token 报错的问题。 - 新增:新增QA:前后端一体项目下,在拦截未登录进入登录页面时,如何登录完成后原路返回? - 新增:新增QA:Sa-Token 集成 Redis 如何集群? - 新增:新增QA:如何自定义框架读取 token 的方式? - 新增:新增QA:修改 `hosts` 文件无效可能原因排查。 - 新增:新增QA:如何防止 CSRF 攻击。 - 新增: “异步 & Mock 上下文” 章节。 - 升级:升级“自定义 SaTokenContext 指南”章节文档。 ### v1.41.0 @2025-3-21 更新导读:[视频](https://www.bilibili.com/video/BV1aNo4YCEM1/)、[文字版](https://juejin.cn/post/7484191942358499368) - core: - 修复:修复 `StpUtil.setTokenValue("xxx")`、`loginParameter.getIsWriteHeader()` 空指针的问题。 fix: [#IBKSM0](https://gitee.com/dromara/sa-token/issues/IBKSM0) - 修复:将 `SaDisableWrapperInfo.createNotDisabled()` 默认返回值封禁等级改为 -2,以保证向之前版本兼容。 - 新增:新增基于 SPI 的插件体系。 **[重要]** - 重构:JSON 转换器模块。 **[重要]** - 新增:新增 serializer 序列化模块,控制 `Object` 与 `String` 的序列化方式。 **[重要]** - 重构:重构防火墙模块,增加 hooks 机制。 **[重要]** - 新增:防火墙新增:请求 path 禁止字符校验、Host 检测、请求 Method 检测、请求头检测、请求参数检测。重构目录遍历符检测算法。 - 重构:重构 `SaTokenDao` 模块,将序列化与存储操作分离。 **[重要]** - 重构:重构 `SaTokenDao` 默认实现类,优化底层设计。 - 新增:`isLastingCookie` 配置项支持在全局配置中定义了。 - 重构:`SaLoginModel` -> `SaLoginParameter`。 **[不向下兼容]** - 重构:`TokenSign` -> `SaTerminalInfo`。 **[不向下兼容]** - 新增:`SaTerminalInfo` 新增 `extraData` 自定义扩展数据设置。 - 新增:`SaLoginParameter` 支持配置 `isConcurrent`、`isShare`、`maxLoginCount`、`maxTryTimes`。 - 新增:新增 `SaLogoutParameter`,用于控制注销会话时的各种细节。 **[重要]** - 新增:新增 `StpLogic#isTrustDeviceId` 方法,用于判断指定设备是否为可信任设备。 - 新增:新增 `StpUtil.getTerminalListByLoginId(loginId)`、`StpUtil.forEachTerminalList(loginId)` 方法,以更方便的实现单账号会话管理。 - 升级:API 参数签名配置支持自定义摘要算法。 - 新增:新增 `@SaCheckSign` 注解鉴权,用于 API 签名参数校验。 - 新增:API 参数签名模块新增多应用模式。 fix: [#IAK2BI](https://gitee.com/dromara/sa-token/issues/IAK2BI), [#I9SPI1](https://gitee.com/dromara/sa-token/issues/I9SPI1), [#IAC0P9](https://gitee.com/dromara/sa-token/issues/IAC0P9) **[重要]** - 重构:全局配置 `is-share` 默认值改为 false。 **[不向下兼容]** - 重构:踢人下线、顶人下线默认将删除对应的 token-session 对象。 - 优化:优化注销会话相关 API。 - 重构:登录默认设备类型值改为 DEF。 **[不向下兼容]** - 重构:`BCrypt` 标注为 `@Deprecated`。 - 新增:`sa-token-quick-login` 支持 `SpringBoot3` 项目。 fix: [#IAFQNE](https://gitee.com/dromara/sa-token/issues/IAFQNE)、[#673](https://github.com/dromara/Sa-Token/issues/673) - 新增:`SaTokenConfig` 新增 `replacedRange`、`overflowLogoutMode`、`logoutRange`、`isLogoutKeepFreezeOps`、``isLogoutKeepTokenSession`` 配置项。 - OAuth2: - 重构:重构 sa-token-oauth2 插件,使注解鉴权处理器的注册过程改为 SPI 插件加载。 - 插件: - 新增:`sa-token-serializer-features` 插件,用于实现各种形式的自定义字符集序列化方案。 - 新增:`sa-token-fastjson` 插件。 - 新增:`sa-token-fastjson2` 插件。 - 新增:`sa-token-snack3` 插件。 - 新增:`sa-token-caffeine` 插件。 - 单元测试: - 新增:`sa-token-json-test` json 模块单元测试。 - 新增:`sa-token-serializer-test` 序列化模块单元测试。 - 文档: - 新增:QA “多个项目共用同一个 redis,怎么防止冲突?” - 优化:补全 OAuth2 模块遗漏的相关配置项。 - 优化:优化 OAuth2 简述章节描述文档。 - 优化:完善 “SSO 用户数据同步 / 迁移” 章节文档。 - 修正:补全项目目录结构介绍文档。 - 新增:文档新增 “登录参数 & 注销参数” 章节。 - 优化:优化“技术求助”按钮的提示文字。 - 新增:新增 `preview-doc.bat` 文件,一键启动文档预览。 - 完善:完善 Redis 集成文档。 - 新增:新增单账号会话查询的操作示例。 - 新增:新增顶人下线 API 介绍。 - 新增:新增 自定义序列化插件 章节。 - 其它: - 新增:新增 `sa-token-demo/pom.xml` 以便在 idea 中一键导入所有 demo 项目。 - 删除:删除不必要的 `.gitignore` 文件 - 重构:重构 `sa-token-solon-plugin` 插件。 - 新增:新增设备锁登录示例。 ### v1.40.0 @2025-2-1 更新导读:[视频](https://www.bilibili.com/video/BV1uNATeeEvg/)、[文字版](https://juejin.cn/post/7467969744307306505) - core: - 新增:新增 `Cookie` 自定义属性支持。 fix: [#693](https://github.com/dromara/Sa-Token/issues/693) **[重要]** - 新增:`SaFirewallStrategy` 防火墙策略:请求 path 黑名单校验、非法字符校验、白名单放行。 **[重要]** - 修复:新增对分号字符的 path 路径校验。 参考:[Sa-Token对url过滤不全存在的风险点](https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA) **[漏洞修复]** - 修复: 修复部分场景下登录后已存在的 `token-session` 没有被续期的问题。 fix: [#IA8U1O](https://gitee.com/dromara/sa-token/issues/IA8U1O) - 优化:优化 `active-timeout` 的检查与续期操作,同一请求内只会检查与续期一次。 - 修复:`SaFoxUtil.joinSharpParam` 方法中不正确的注释。 - 新增:封禁模块新增支持实时从数据库查询数据。 - SSO: - 优化:SSO 示例代码的跨域处理由原生方式改为 Sa-Token 过滤器模式。 - 新增:文档新增 “SSO整合 - NoSdk 模式与非 java 项目” 章节。 - 新增:“不同 SSO Client 配置不同秘钥” 章节增加部分异常的处理方案提示,fix: [#IAFZXL](https://gitee.com/dromara/sa-token/issues/IAFZXL) - 删除:sso demo 示例中部分不必要的代码内容。 - OAuth2: - 新增:OAuth2 Client 前端测试页。 **[重要]** - 新增:`UnionId` 联合id 实现。 **[重要]** - 新增:`oauth2-server` 端前后台分离示例与文档。 fix: [#I9DQGA](https://gitee.com/dromara/sa-token/issues/I9DQGA)、[#I9W2RU](https://gitee.com/dromara/sa-token/issues/I9W2RU) **[重要]** - 新增:`OIDC` 模式 `nonce` 随机数响应校验。 merge: [pr311](https://gitee.com/dromara/sa-token/pulls/311) - 修复:错误方法名 `deleteGrantScope(String state)` -> `deleteState(String state)`。 - 修复:全局配置项 `sa-token.oauth2-server.oidc.iss` 无效的问题。 - 新增:回收 Refresh-Token 方法: `revokeRefreshToken`、`revokeRefreshTokenByIndex`。 - 新增:为 `CodeModel`、`AccessTokenModel`、`RefreshTokenModel`、`ClientTokenModel` 添加 `createTime` 字段,以记录该数据的创建时间。 - 新增:为 Access-Token、Client-Token 添加 `grantType` 字段,以记录该数据的授权类型。 - 新增:`SaOAuth2Util.getCode` 等方法,以更方便的获取、校验授权码。 - 插件: - 新增:新增 `sa-token-freemarker` 插件,整合 `Freemarker` 视图引擎。 fix: [#651](https://github.com/dromara/sa-token/issues/651) **[重要]** - 新增:新增 `sa-token-spring-el` 插件,用于支持 SpEL 表达式注解鉴权。 fix: [#IB3GBB](https://gitee.com/dromara/sa-token/issues/IB3GBB)、fix: [#IAIXSL](https://gitee.com/dromara/sa-token/issues/IAIXSL)、fix: [#I9P24F](https://gitee.com/dromara/sa-token/issues/I9P24F) **[重要]** - 文档: - 新增:新增 `MongoDB` 集成示例。 感谢 `@lilihao` 提供的示例。 merge: [pr322](https://gitee.com/dromara/sa-token/pulls/322)、[pr667](https://github.com/dromara/Sa-Token/pull/667) **[重要]** - 新增:“fox说技术” 视频教程链接。 - 新增:“API接口参数签名”章节 视频讲解链接(B站抓蛙师)。 - 优化:文档首页首屏增加需求提交按钮。 - 其它:补全赞助者名单、`Dromara` 项目链接等信息。 - 新增:`SpringBoot3.x` 版本配置 Redis 注意事项。fix: [#688](https://github.com/dromara/Sa-Token/issues/688) - 新增:`gitcode` g-star badge 展示。 - 修复:`OAuth2` 滞后的配置信息示例。 - 新增:新增视频账号链接。 - 新增:新增团队成员展示。 ### v1.39.0 @2024-8-28 - 核心: - 升级:重构注解鉴权底层,支持自定义鉴权注解了。 **[重要]** - 修复:修复前端提交同名 `Cookie` 时的框架错读现象。 - 更名:`NotBasicAuthException` -> `NotHttpBasicAuthException`。 - 插件: - 修复:修复 `sa-token-quick-login` 插件无法正常拦截的问题。 - SSO: - 优化:优化 sso-server 前后端分离 demo 代码。 - 优化:优化 sso-server 前后端分离时的跳转流程。 - OAuth2: - 重构:`sa-token-oauth2` 模块整体重构。 **[重要]** **[不向下兼容]** - 新增:新增支持自定义 `scope` 处理器。 **[重要]** - 新增:新增支持自定义 `grant_type`。 **[重要]** - 新增:新增 `scope` 划分等级。 **[重要]** - 新增:新增 `oidc` 协议支持。 **[重要]** - 新增:新增支持默认 `openid` 生成算法。 **[重要]** - 新增:新增 `OAuth2` 注解鉴权支持。 **[重要]** - 修复:`redirect_url` 参数校验增加规则:不允许出现@字符、*通配符只能出现在最后一位。关联 [issue](https://github.com/dromara/Sa-Token/issues/529) **[重要]** - 优化:优化代码注释、异常提示信息。 - 升级:兼容 `Http Basic` 提交 `client` 信息的场景。感谢 github `@CuiGeekYoung` 提交的pr。 - 升级:兼容 `Bearer Token` 方式提交 `access_token` 和 `client_token`。 - 升级:适配拆分式路由。 - 新增:将 `scope` 字段改为 List 类型。 - 重构:抽离 `SaOAuth2Strategy` 全局策略接口,定义一些创建 token 的算法策略。 - 新增:新增 `addAllowUrls` `addContractScopes` 方法,简化 `SaClientModel` 构建代码。 - 重构:抽离 `SaOAuth2Dao` 接口,负责数据持久。 - 重构:抽离 `SaOAuth2DataLoader` 数据加载器接口。 - 重构:抽离 `SaOAuth2DataGenerate` 数据构造器接口。 - 重构:抽离 `SaOAuth2DataConverter` 数据转换器接口。 - 重构:抽离 `SaOAuth2DataResolver` 数据解析器接口。 - 重构:重构 `SaOAuth2Handle` -> `SaOAuth2ServerProcessor` 更方便的逻辑重写。 - 重构:重构 `PastToken` -> `LowerClientToken`。 - 新增:新增 `state` 值校验,同一 `state` 参数不可重复使用。 - 优化:完善 `SaOAuth2Util` 相关方法,更方便的二次开发。 - 新增:新增部分异常类,细分异常 `ClassType`。 - 优化:优化 `sa-token-oauth2` 异常细分状态码。 - 文档: - 新增:新增数据结构说明。 - 新增:新增不同 `client` 不同登录页说明。 - 优化:优化文档 [将权限数据放在缓存里] 示例。 - 新增:新增 从 Shiro、SpringSecurity、JWT 迁移 示例。 **[重要]** ### v1.38.0 @2024-5-12 - sa-token-core: - 修复:修复 `StpUtil.getSessionByLoginId(xx)` 参数为 null 时创建无效 `SaSession` 的 bug。 - 优化:在 `SpringBoot 3.x` 版本下错误的引入依赖时将得到启动失败的提示。 (感谢`Uncarbon`提交的pr) - 优化:进一步优化权限校验算法,hasXxx API 只会返回 true 或 false,不再抛出异常。 - 重构:`InvalidContextException` 更名为 `SaTokenContextException`。 **[已做向下兼容处理]** - 重构:彻底删除 `NotPermissionException` 异常中的 `getCode()` 方法。 **[过期API清理]** - 重构:重构 `SaTokenException` 类方法 `throwBy-`>`notTrue`、`throwByNull-`>`notEmpty`。**[已做向下兼容处理]** - 重构:`StpUtil.getSessionBySessionId` 提供的 `SessionId` 为空时将直接抛出异常,而不是再返回null。**[不向下兼容]** - 新增:新增 `Http Digest` 认证模块简单实现。 **[重要]** - 重构:更换 `HttpBasic` 认证模块包名。 **[已做向下兼容处理]** - 新增:新增 `StpUtil.getLoginDeviceByToken(xxx)` 方法,用于获取任意 token 的登录设备类型。 - 新增:新增 `StpUtil.getTokenLastActiveTime()` 方法,获取当前 token 最后活跃时间。 - 修复:修复“当登录时指定 timeout 小于全局 timeout 时,`Account-Session` 有效期为全局 timeout”的问题。 - 优化:首次获取 `Token-Session` 时,其有效期将保持和 token 有效期相同,而不是再是全局 timeout 值。 - 移除:移除 `SaSignConfig` 的 `isCheckNonce` 配置项。 **[不向下兼容]** - 新增:`SaSignTemplate#checkRequest` 增加“可指定参与签名参数”的功能。 - 重构:将部分加密算法设置为过期。 - 重构:优化 token 读取策略,空字符串将视为没有提交token。 - 修复:`sa-token-bom` 补全缺失依赖。 - 优化:二级认证校验之前必须先通过登录认证校验。 - 修复:修复 `StpUtil.getLoginId(T defaultValue)` 传入 null 时无法正确返回值的bug。 - sa-token-sso: - 优化:SSO 模式三,API 调用签名校验时,限定参与签名的参数列表,更安全。 - 新增:新增 `autoRenewTimeout` 配置项:是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) - 新增:`SaSsoConfig` 新增配置 `isCheckSign`(是否校验参数签名),方便本地开发时的调试。 - 新增:`SaSsoConfig` 新增配置 `currSsoLogin`,用于强制指定当前系统的 sso 登录地址。 - 重构:整体重构 `sa-token-sso` 模块,将 `server` 端和 `client` 端代码拆分。 **[重要]** **[不向下兼容]** - 新增:`SaSsoConfig` 配置项 `ssoLogoutCall` 重命名为 `currSsoLogoutCall`。**[已做向下兼容处理]** - 重构:模式三在校验 Ticket 时,也将强制校验签名才能调通请求。**[不向下兼容]** - 新增:新增 `maxRegClient` 配置项,用于控制模式三下 client 注册数量。 - 新增:新增不同 SSO Client 配置不同 `secret-key` 的方案。 **[重要]** - 重构:匿名 client 将不再能解析出所有应用的 ticket。**[不向下兼容]** - 新增:新增 `homeRoute` 配置项:在 ``/sso/auth`` 登录后不指定 redirect 参数的情况下默认跳转的路由。 - 优化:优化登录有效期策略,SSO Client 端登录时将延续 SSO Server 端的会话剩余有效期。 - 新增:新增 `checkTicketAppendData` 策略函数,用于在校验 ticket 后,给 sso-client 端追加返回信息。 - 新增:SSO章节文档新增用户数据同步/迁移方案的建议。 - 修复:修复利用@字符可以绕过域名允许列表校验的漏洞。 **[漏洞修复]** - 修复:禁止 `allow-url` 配置项 * 符号出现在中间位置,因为这有可能导致校验被绕过。 **[漏洞修复]** - 新增插件/示例: - 新增:新增插件 `sa-token-hutool-timed-cache`,用于整合 Hutool 缓存插件 TimedCache。 **[重要]** - 新增:新增 SSM 架构整合 Sa-Token 简单示例。 **[重要]** - 新增:新增 beetl 整合 Sa-Token 简单示例。 **[重要]** - 文档: - 部分章节将 `@Autowired` 更换为更合适的 `@PostConstruct` - 新增过滤器执行顺序更改示例。 - 其它: - 优化:将跨域处理demo拆分为独立仓库。 - 优化:解决 springboot 集成 sa-token 后排除 jackson 依赖无法成功启动的问题。 - 优化:解决 `sa-token-jwt` 模块重复设置 keyt 秘钥问题。(感谢`KonBAI`提交的pr) - 优化:jwt模式 token 过期后,抛出的异常描述是 token 已过期,而不再是 token 无效。 - 修复:补齐 `sa-token-spring-aop` 模块中遗漏监听的注解。 ### v1.37.0 @2023-10-18 - 修复:修复路由拦截鉴权可被绕过的问题。 **[漏洞修复]** - 重构:未登录时调用鉴权 API 抛出未登录异常而不再是无权限异常。 - 优化:优化 SaTokenDao 组件更换时的逻辑。 - 文档:提供 SpringBoot3.x 路由匹配出错的解决方案。 ### v1.36.0 @2023-9-22 - sa-token-core: - 修复:API接口签名校验参数接口NPE问题,增加必须参数的非空校验处理。 - 新增:加密工具类新增 sha384、sha512 实现。 感谢 `@若初995` 提交的pr。 **[重要]** - 修复:`SaFoxUtil.vagueMatch()` 正则匹配的一些问题。 **[漏洞修复]** - 修复:`SaRouter.match()` 路由匹配的一些问题。 **[漏洞修复]** - 其它: - 优化:`sa-token-alone-redis` 去掉不必要的配置项判断。 - 新增:`sa-token-solon-plugin` 增加对 solon 网关的支持。 - 新增:新增第三方插件专用仓库:`sa-token-three-plugin` 。 - 升级:`sa-token-solon-plugin` 增加对 solon 网关的支持。 - 文档: - 新增:新增开启全局懒加载时不能注入上下文处理器的处理方案 。 - 新增:新增 RefreshToken 示例。 **[重要]** - 新增:文档新增 sa-token 小助手,可在线实时技术提问。 **[重要]** - 优化:其它一些优化。 - 新增插件: - `sa-token-redisson-jackson2`:通用 redisson 集成方案 (spring, solon, jfinal 等都可用) ### v1.35.0 @2023-6-23 - sa-token-core: - 优化:前端未提供 token 时,`getTokenSession()` 将抛出未登录异常,而不是返回 null。 **[不向下兼容]** - 新增:SaSession 新增字段:`type`、`loginType`、`loginId`、`token`。 - 重构:全局过滤器抽离 SaFilter 统一接口。 - 重构:全局过滤器 `includeList`、`excludeList` 改为 public,同时移除对应的 getter 方法。 **[不向下兼容]** - 重构:将全局过滤器的 BeforeAuth 认证设为不受 `includeList` 与 `excludeList` 的限制,所有请求都会进入。 **[不向下兼容]** - 新增:新增循环生成 token 的算法,用于确保 Token 的唯一性。 **[重要]** - 重构:API 接口签名所有方法均迁移至 core 核心模块。 **[重要]** - 新增:新增彩色日志打印,更方便的分辨不同日志等级。 **[重要]** - 重构:重构概念:临时有效期 -> token 最低活跃频率,过期后 token 冻结。 - 重构:重构概念:`User-Session` -> `Account-Session`。 - 新增:新增 `getTokenTimeout(String token)` 方法,获取任意 token 剩余有效期。 - 优化:在登录时增加判断当前 StpLogic 是否支持 extra 扩展参数模式,如果不支持则打印警告信息。 - 新增:NotLoginException 增加新场景值 -6、-7,提供更精确的未登录异常描述信息。 - 新增:TokenSign 新增 tag 挂载参数,可在登录时方便的存储一些客户端特有数据。 **[重要]** - 新增:新增 `SaStrategy#createStpLogic`,用于指定动态创建 StpLogic 时的算法策略。 - 新增:新增 `@SaCheckOr` 批量注解鉴权:只要满足其中一个注解即可通过验证。 **[重要]** - 重构:重命名:`SaStrategy.me` -> `SaStrategy.instance`。 - 重构:在登录时强制性检查账号 id 是否为异常值,如果是则登录失败。 - 重构:重构概念:`activity-timeout` -> `active-timeout`。 **[重要]** - 新增:新增动态 `active-timeout` 能力,可在每次登录时指定 `active-timeout` 值。 **[重要]** - 优化:将 `SaStrategy` 所有策略声明抽离为单独的函数式接口。 - 新增:增加为 StpLogic 单独配置 `SaTokenConfig` 参数的能力。 - sa-token-sso: - 修复:在 SSO 模式三中 `ticket` 校验地址配错时,会出现 NPE 的问题 - 新增:新增 `getData` 接口配置,在模式三拉取数据时可以传递任意参数。 **[重要]** - 重构:模式三秘钥配置更改:`sa-token.sso.secretkey=xxx` -> `sa-token.sign.secret-key=xxx`。 **[不向下兼容]** - 重构:模式三校验签名方法更改:`SaSsoUtil.checkSign(req)` -> `SaSignUtil.checkRequest(req)`。 **[不向下兼容]** - 新增:新增 `sa-token.sso.mode` 配置项,用于约定此系统使用的 SSO 模式。 - 优化:优化校验 ticket 的逻辑。 - sa-token-jwt: - 修复:jwt 令牌的签名类型可以被篡改的问题。 **[重要]** - 其它: - 优化:所有模块优化注释,更方便开发者阅读源码。 - 优化:在所有 .java 文件中添加 license 头说明。 - 优化:修复大部分代码警告。 - 新增:新增 thymeleaf 标签方言命名空间,增强 ide 代码提示。 **[重要]** - 新增:定义 `sa-token-bom` 包,方便引入 sa-token 时对齐版本。 - 新增:sa-token-dubbo3 插件新增代码示例。 - 新增:新增跨域文章和示例:Header 参数版和第三方 Cookie 版。 **[重要]** - 修复:修复 `sa-token-alone-redis` 在低版本 springboot 下无法启动成功(缺少 `username` 属性)的问题。 - 新增插件: - 新增:新增 `sa-token-context-dubbo3` 插件。 感谢 `@qiudaozhang` 提交的 pr。 **[重要]** - 文档: - 新增:部分常见报错排查。 - 新增:首页图片增加懒加载效果,节省流量。 - 新增:增加 Cookie 配置示例。 - 修复:整理 demo 结构目录结构,修复不正确的路径说明。 - 新增:新增 api-sign 模块文档。 **[重要]** - 简化包名 **[重要]** **[不向下兼容]** - `sa-token-dao-redis` -> `sa-token-redis` - `sa-token-dao-redis-jackson` -> `sa-token-redis-jackson`。 - `sa-token-dao-redis-fastjson` -> `sa-token-redis-fastjson`。 - `sa-token-dao-redis-fastjson2` -> `sa-token-redis-fastjson2`。 - `sa-token-dao-redisson-jackson` -> `sa-token-redisson-jackson`。 - `sa-token-dao-redisx` -> `sa-token-redisx`。 - `sa-token-context-dubbo` -> `sa-token-dubbo`。 - `sa-token-context-dubbo3` -> `sa-token-dubbo3`。 - `sa-token-context-grpc` -> `sa-token-grpc`。 ### v1.34.0 @2023-1-11 新增插件: - 新增:新增 `SpringBoot3.x` 集成插件,感谢 `@jry` 提供的参考思路。 **[重要]** - 新增:新增 `sa-token-dao-redisson-jackson` 插件,感谢 `@疯狂的狮子Li` 提交的pr。 **[重要]** sa-token-core 核心包: - 升级:升级 Sign 签名模块,增加部分重载方法。 - 重构:`SaSignTemplate#joinParams` 更名为 `joinParamsDictSort`。 **[不向下兼容]** - 升级:升级临时 Token 认证模块,可指定 service 参数。 - 删除:彻底删除过期类 `SaAnnotationInterceptor` 和 `SaRouteInterceptor`。 - 修复:修复源码注释和文档的部分不合适之处。 sa-token-sso 单点登录: - 删除:SSO 模块移除过期类 `SaSsoHandle` 类。 - 新增:SSO 模块增加 ticket 的 client 锁定功能,解决部分场景下的 ticket 劫持问题。 **[重要]** - 修复:修复 SSO 模式2,在 client 端配置 `is-share=false` 时无法单点注销的问题。 - 修复:修复 SSO 模式3 部分场景下注销时无法正常回退页面的问题。 其它模块: - sa-token-oauth2:修复 OAuth2 模块示例 getClientModel 方法 clientId 写错的问题。 - sa-token-alone-redis:新增:Alone-Redis 新增集群配置能力,感谢 `@appleOfGray` 提交的pr。 **[重要]** - sa-token-jwt:重构:使用 jwt-simple 模式后 is-share 恒等于 false,无论是否有设定 `setExtra` 数据。 ### v1.33.0 @2022-11-16 - 重构:重构异常状态码机制。 **[重要]** - 重构:重构 sa-token-sso 模块异常码改为 300 开头,sa-token-jwt 异常码改为 302 开头。 **[不向下兼容]** - 新增:新增全局 Log 模块。 **[重要]** - 重构:`SaTokenListenerForConsolePrint` 改名 `SaTokenListenerForLog`。 **[不向下兼容]** - 修复:修复多线程下 `SaFoxUtil.getRandomString()` 随机数重复问题。 - 修复:修复 sa-token-demo-sso3-client-nosdk 项目中单点注销 url 配置错误的问题 - 文档:文档优化。 ### v1.32.0 @2022-10-28 - 修复:修复 sa-token-dao-redis-fastjson 插件多余序列化 `timeout` 字段的问题。 - 修复:修复 sa-token-dao-redis-fastjson 插件 `session.getModel` 无法反序列化实体类的问题。 - 修复:修复 `sa-token-quick-login` 插件指定拦截排除路由不生效的问题。 - 修复:修复 `sa-token-alone-redis` + `sa-token-dao-redis-fastson` 时 Redis 无法分离的问题。 - 修复:修复在配置了 cookie.path 后,注销时无法彻底清除 Cookie 的问题。 - 升级:`SaFoxUtil.getValueByType()` 新增对 char 类型的转换。 - 新增:新增 `sa-token-dao-redis-fastjson2` 插件。 **[重要]** - 新增:新增全局配置 `is-write-header`,控制登录后是否将 Token 写入响应头。 **[重要]** - 新增:二级认证模块新增指定业务标识能力。 **[重要]** - 重构:Id-Token 模块更名为 Same-Token。 **[重要]** **[不向下兼容]** - 重构:重构会话查询参数作用:由`start=-1`时查询全部会话,改为 `start=0,size=-1` 时查询全部。 **[不向下兼容]** - 重构:`SaManager.getStpLogic("type")` 默认当对应type不存在时不再抛出异常,而是自动创建并返回。 - 重构:重构SSO模块,静态式API改为实例式:SaSsoHandle -> SaSsoProcessor。 **[重要]** **[不向下兼容]** - 重构:SSO-Server 端单点注销地址修改 `/sso/logout` -> `/sso/signout`,避免与 SSO-Client 端同 path 的冲突。 **[不向下兼容]** - 新增:文档新增 SSO 平台中心模式示例,跳连接进入子系统。 **[重要]** - 新增:新增SSO前后端分离集成示例 vue2 & vue3 版本。 **[重要]** - 重构:SSO 示例项目 http 请求工具改为 Forest。 - 新增:SSO模块文档新增单个项目同时搭建 `sso-server` 和 `sso-client` 的示例。 **[重要]** - 新增:SSO模块文档新增一个项目同时搭建两个 `sso-server` 服务 的示例。 **[重要]** - 文档:在线文档新增代码示例。 - 文档:在线文档增加全局调色功能。 - 文档:[自定义 SaTokenContext 指南] 章节新增对三种模型的解释。 - 文档:新增多账号体系混合鉴权代码示例。 - 文档:文档增加 `Gradle` 依赖方式和 `properties` 风格配置。 - 新增:新增 `sa-token-dependencies`,统一定义依赖版本。 **[重要]** ##### 已知问题: > 部分场景下 Token 重复问题,受影响版本 `=v1.32.0` > - 受影响模块: > - sa-token-core 切换了 Token 风格:tik、random-32、random-64、random-128,如果使用 默认uuid、simple-uuid 风格则不受影响。 > - sa-token-core 使用了临时 Token 认证模块,如果集成了 sa-token-temp-jwt 则不受影响。 > - sa-token-core 使用了 Same-Token 模块。 > - sa-token-jwt 全模块 > - sa-token-oauth2 全模块 > - sa-token-sso 模式二和模式三 ### v1.31.0 @2022-9-8 - 文档:新增优秀开源案例展示。 - 文档:新增博客展示,欢迎大家投稿。 - 新增:新增 `SaInterceptor` 综合拦截器。 **[重要]** **[不向下兼容]** - 新增:新增 新增 `@SaIgnore` 忽略鉴权注解。 **[重要]** - 新增:新增插件 `sa-token-dao-redis-fastjson`,感谢 `@sikadai` 提交的pr。 **[重要]** - 新增:新增插件 `sa-token-context-grpc`,感谢 `@LiYiMing666` 提交的pr。 **[重要]** - 重构:SaSession 取消 `tokenSignList` 的 final 修饰符。 - 新增:SaSession 添加 `setTokenSignList` 方法。 - 重构:TokenSign 新增 `setValue` 和 `setDevice` 方法。 - 修复:修复多账号模式下不能正确重置 `StpLogic` 的问题。 - 修复:修复 SaSession 对象中 TokenSign 判断有可能空指针的问题。 - 修复:解决当权限码为 null 时可能带来的空指针问题。 - 新增:新增 `StpUtil.getExtra(tokenValue, key)` 方法,用于获取任意 token 的扩展参数。 - 优化:优化 `StpLogic#logoutByTokenValue` 方法逻辑,精简代码。 - 重构:`SaTokenConfig` 配置类字段 `isReadHead` 改为 `isReadHeader`。 **[不向下兼容]** - 修复:修复部分场景下踢人下线会抛出异常 `非Web上下文无法获取Request` 的问题。 - 新增:新增方法 `StpLogic#getAnonTokenSession`,可在未登录情况下安全的获取 Token-Session。 **[重要]** - 新增:新增 `SaApplication` 对象,用于全局作用域存取值。 **[重要]** - 重构:将 `SaTokenListener` 改为事件发布订阅模式,允许同时注册多个侦听器。 **[重要]** **[不向下兼容]** - 重构:**StpUtil.login(id) 不再强制校验账号是否禁用,需要手动校验。** **[不向下兼容]** - 重构:`DisableLoginException` 更换名称为 `DisableServiceException`。 **[不向下兼容]** - 新增:新增对账号限制、分类封禁、阶梯封禁功能。 **[重要]** - 新增:会话查询API增加反序获取会话方式。 - 新增:SSO模块增加 server-url 属性,用于简化各种 url 配置。 **[重要]** - 修复:修复单点登录模块 `ssoLogoutCall` 配置项无效的问题。 - 优化:优化 `SaSsoHandle.checkTicket(ticket, currUri);` 方法,使其不提供 currUri 参数时将不再注册单点注销回调。 - 修复:修复 `SaOAuth2Handle` 类中 `doLogin` 方法没有使用 `Param.pwd` 常量的问题。 - 新增:新增 `SaOAuth2Util.checkClientTokenScope(clientToken, scopes)` 方法,校验 Client-Token 是否含有指定 Scope。 - 删除:删除 `sa-token-jwt` 模块过期 class。 - 重构:`sa-token-jwt` 模块依赖改为 `hutool-jwt`,并升级版本为 5.8.5。 - 重构:`sa-token-jwt` 模块改为 `Util + Template` 形式,方便针对部分代码重写。 **[重要]** - 新增:在线文档添加API手册。 - 重构:`sa-token-oauth2` 模块密码模式新增 `client_secret` 参数校验。**[不向下兼容]** - 新增:集成 `jacoco` 插件,核心包单元测试覆盖率提高至 90% 以上。 - 优化:开源案例分离专属仓库:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) ### v1.30.0 @2022-05-9 - 新增:新增集成 Web-Socket 鉴权示例。 **[重要]** - 新增:新增集成 Web-Socket(Spring封装版) 鉴权示例。 - 新增:新增 jfinal 集成包 `sa-token-jfinal-plugin` **[重要]** - 新增:新增 jboot 集成包 `sa-token-jboot-plugin` (感谢 @nxstv 提交的pr) - 修复:修复整合 sa-token-jwt Style 模式时,`StpUtil.getExtra("key")` 无效的bug - 升级:升级 `sa-token-context-dubbo` dubbo版本:`2.7.11` -> `2.7.15` - 升级:借助 `flatten-maven-plugin` 统一版本号定义 (感谢 @ruansheng8 提交的pr) **[重要]** - 修复:修复在 `springboot 2.6.x` 下 `quick-login` 插件循环依赖无法启动的问题 - 优化:`sa-token-spring-aop` 依赖改为 `sa-token-core`,避免在webflux环境下启动报错的问题 - 优化:源码注释 设备标识 改为 设备类型 更符合语义 - 修复:解决部分协议下 dubbo 参数变为小写导致 `Id-Token` 鉴权无效的问题 - 升级:单元测试升级为 JUnit5 - 新增:新增 `maxLoginCount` 配置,指定同一账号可同时在线的最大数量 **[重要]** - 升级:彻底删除 SaTokenAction 接口,完全由 SaStrategy 代替 - 新增:新增 `sa-token-dao-redisx` 插件,感谢 @noear 提交的pr **[重要]** - 优化:增加 parseToken 未配置 jwt 密钥时的异常提示,感谢 @BATTLEHAWK00 提交的pr - 优化:sso,oauth2 插件中调用配置类使用 getter 方法,感谢 @Naah 提交的pr - 新增:新增 json 转换器模块 - 重构:SaTokenListener#doLogin 方法新增 tokenValue 参数 **[不向下兼容]** - 升级:SpringBoot 相关组件依赖版本升级至 `2.5.12` - 文档:在线文档所有 `AjaxJson` 改为 `SaResult` - 文档:“多账号认证” -> 改为 “多账户认证” - 文档:部分章节新增动态演示图 **[重要]** - 升级:顶级异常类 `SaTokenException` 增加 code 异常细分状态码。[详见](/fun/exception-code) **[重要]** - **注意升级:受异常细分状态码影响,`NotPermissionException` 类中 `getCode()` 方法改为 `getPermission()`。** **[不向下兼容]** - SSO 模块升级: - 重构:SSO 模块从核心包拆分为独立插件 `sa-token-sso` **[重要]** - 优化:SSO模式三单点注销回调方法中,注销语句改为:`stpLogic.logout(loginId)` 更符合情景 - 修复:解决 sso 构建认证地址时,部分 Servlet 版本内部实现不一致带来的双 back 参数问题。 - 升级:SSO 模块提供精细化异常处理 - 重构:SSO 模式三接口 `/sso/checkTicket`、`/sso/logout`,更改响应体格式 **[不向下兼容]** - 优化:SSO 模式三单点注销搭建示例增加 `try-catch`,提高容错性 - 优化:`SsoUtil.singleLogout` 改为 `SsoUtil.ssoLogout`,且无需再提供 secretkey 参数 **[不向下兼容]** - 升级:将 SSO 模式三的接口调用改为签名式校验。 **[重要]** **[不向下兼容]** - 新增:新增 SSO 模式三下无 sdk 的对接示例, 感谢 @Sa-药水 的建议反馈 **[重要]** - sa-token-jwt 模块升级: - 重构:`sa-token-jwt` 的创建,强制校验loginType **[不向下兼容]** - 重构:`StpLogicJwtForStateless` 由重写 login 方法改为重写 createLoginSession - 重构:`SaJwtUtil` 工具类不再吞并异常消息,且提供精细化异常 code 码。 - 重构:改名:StpLogicJwtForStyle -> StpLogicJwtForSimple - 重构:改名:StpLogicJwtForMix -> StpLogicJwtForMixin - 修复:修复 `StpLogicJwtForSimple` 模式下 Extra 数据可能受到旧 token 影响的bug ### v1.29.0 @2022-02-10 - 升级:sa-token-jwt插件可在登录时添加额外数据。 - 重构:优化Dubbo调用时向下传递Token的规则,可避免在项目启动时由于Context无效引发的bug。 - 重构:OAuth2 授权模式开放由全局配置和Client单独配置共同设定。 - 重构:OAuth2 模块部分属性支持每个 Client 单独配置。 - 重构:OAuth2 模块部分方法名修复单词拼写错误:converXxx -> convertXxx。 - 重构:修复 OAuth2 模块 `deleteAccessTokenIndex` 回收 token 不彻底的bug。 - 新增:OAuth2 模块新增 `pastClientTokenTimeout`,用于指定 PastClientToken 默认有效期。 - 文档:常见报错章节增加目录树,方便查阅。 - 文档:优化文档样式。 - 新增:新增 BCrypt 加密。 - 修复:修复StpUtil.getLoginIdByToken(token) 在部分场景下返回出错的bug。 - 重构:优化OAuth2模块密码式校验步骤。 - 新增:新增Jackson定制版Session,避免timeout属性的序列化。 - 新增:SaLoginModel新增setToken方法,用于预定本次登录产生的Token。 - 新增:新增 StpUtil.createLoginSession() 方法,用于无Token注入的方式创建登录会话。 - 新增:OAuth2 与 StpUtil 登录会话数据互通。 - 新增:新增 `StpUtil.renewTimeout(100);` 方法,用于 Token 的 Timeout 值续期。 - 修复:修复默认dao实现类中 `updateObject` 无效的bug - 完善:完善单元测试。 ### v1.28.0 @2021-11-5 - 新增:新增 `sa-token-jwt` 插件,用于与jwt的整合 **[重要]** - 新增:新增 `sa-token-context-dubbo` 插件,用于与 Dubbo 的整合 **[重要]** - 文档:文档新增章节:Sa-Token 插件开发指南 **[重要]** - 文档:文档新增章节:名称解释 - 优化:抽离 `getSaTokenDao()` 方法,方便重写 - 新增:单元测试新增多账号模式数据不互通测试 - 优化:优化在线文档,修复部分错误之处 - 优化:优化未登录异常抛出提示,标注无效的Token值 - 修复:修复单词拼写错误 `getDeviceOrDefault` - 优化:优化 jwt 集成示例 - 文档:新增常见问题总结 ### v1.27.0 @2021-10-11 - 升级:增强 SaRouter 链式匹配能力 **[重要]** - 新增:新增插件 Thymeleaf 标签方言 **[重要]** - 新增:@SaCheckPermission 增加 orRole 字段,用于权限角色“双重or”匹配 **[重要]** - 升级:Cookie模式增加 `secure`、`httpOnly`、`sameSite`等属性的配置 **[重要]** - 重构:重构SSO三种模式,抽离出统一的认证中心 **[重要]** - 新增:新增 SaStrategy 策略类,方便内部逻辑按需重写 **[重要]** - 新增:临时认证模块新增 deleteToken 方法用于回收 Token - 新增:新增 kickout、replaced 等注销会话的方法,更灵活的控制会话周期 **[重要]** - 新增:权限认证增加API:`StpUtil.hasPermissionAnd`、`StpUtil.hasPermissionOr` - 新增:角色认证增加API:`StpUtil.hasRoleAnd`、`StpUtil.hasRoleOr` - 新增:新增 `StpUtil.getRoleList()` 和 `StpUtil.getPermissionList()` 方法 - 新增:新增 StpLogic 自动注入特性,可快速方便的扩展 StpLogic 对象 - 优化:优化同端互斥登录逻辑,如果登录时没有指定设备类型标识,则默认顶替所有设备类型下线 - 优化:在未登录时调用 hasRole 和 hasPermission 不再抛出异常,而是返回false - 升级:升级注解鉴权算法,并提供更简单的重写方式 - 文档:新增常见报错排查,方便快速排查异常报错 - 文档:文档新增SSO单点登录与OAuth2技术选型对比 - 破坏式更新: - [向下兼容] 废弃 SaTokenAction 接口,替代方案: SaStrategy - [向下兼容] 移除 `StpUtil.logoutByLoginId()` 更换为 `StpUtil.kickout()`; - [不向下兼容] 侦听器 doLogoutByLoginId 与 doReplaced 方法移除 device 参数 - [不向下兼容] 侦听器 doLogoutByLoginId 方法重命名为 doKickout ### v1.26.0 @2021-9-2 - 优化:优化单点登录文档 - 新增:新增 `Http Basic` 认证 **[重要]** - 新增:文档新增跨域解决方案 - 文档:新增 Nginx 转发请求丢失uri的解决方案 - 文档:新增 SSO 自定义 API 路由示例 **[重要]** - 示例:新增 `SSO-Server` 端前后端分离示例 **[重要]** ### v1.25.0 @2021-8-16 - 新增:`SaRequest`新增`getHeader(name, defaultValue)`方法,用于获取header默认值 - 新增:`SaRequest` 添加 `forward` 转发方法 - 新增:Readme新增源码模块介绍、友情链接、正在使用Sa-Token的项目 - 重构:重构SSO单点登录模块源码,增加可读性 - 新增:SSO配置表新增所属端说明 - 新增:SSO模式三新增账号资料同步示例 **[重要]** - 新增:前后端分离模式下接入SSO的示例 **[重要]** - 优化:优化SSO单点注销重定向逻辑 - 重构:重构SSO单点登录模块部分API - 优化:优化SaQuickBean中过滤器处理逻辑 - 文档:优化文档样式,增加示例 - 文档:代码鉴权、注解鉴权、路由拦截鉴权,选择指南 - 文档:文档新增 SSO旧有系统改造指南 - 文档:SSO集成文档里添加API列表 - 文档:新增 `Sa-Token-Study` 链接,讲解 Sa-Token 源码涉及到的技术点 - 不兼容更新重构: - 重构:修复 `SaReactorHolder.getContent()` 拼写错误:`content` -> `context` ### v1.24.0 @2021-7-24 - 修复:修复部分场景下Alone-Redis插件导致项目无法启动的问题 - 优化:增加对SpringBoot1.x版本的兼容性 - 新增:SaOAuth2Util新增checkScope函数,用于校验令牌是否具备指定权限 - 新增:OAuth2.0模块新增revoke接口,用于提前回收 Access-Token 令牌 - 新增:新增`Sa-Id-Token` 模块,解决微服务内部调用鉴权 **[重要]** - 文档:新增OAuth2.0模块常用方法说明 - 优化:大幅度优化文档示例 ### v1.23.0 @2021-7-19 - 新增:Sa-Token-OAuth2 模块正式发布 **[重要]** - 修复:修复jwt集成demo无法正确注册StpLogic的bug - 修复:修复登录时某些场景下Session续期可能不正常的bug - 优化:代码注释优化,文档优化 ### v1.22.0 @2021-7-10 - 新增:SaSsoConfig 部分属性增加set连缀风格 - 优化:SaSsoUtil 可定制化底层的 `StpLogic` - 新增:新增 `SaSsoHandle` 大幅度简化单点登录整合步骤 **[重要]** - 新增:新增Sa-Token在线测评,链接:[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx) **[重要]** - 新增:Sa-Token-Quick-Login 插件新增拦截与放行路径配置 - 优化:大幅度优化文档示例 ### v1.21.0 @2021-7-2 - 新增:新增Token二级认证 **[重要]** - 新增:新增`Sa-Token-Alone-Redis`独立Redis插件 **[重要]** - 新增:新增SSO三种模式,彻底解决所有场景下的单点登录问题 **[重要]** - 新增:新增多账号模式下,注解合并示例 **[重要]** - 新增:新增`SaRouter.back()`函数,用于停止匹配返回结果 - 不兼容更新重构: - 更改yml配置前缀:原`[spring.sa-token.]` 改为 `[sa-token.]`,目前版本暂时向下兼容,请尽快更新 ### v1.20.0 @2021-6-17 - 新增:新增Solon适配插件,感谢大佬 `@刘西东` 提供的pr **[重要]** - 新增:新增`SaRouter.stop()`函数,用于一次性跳出匹配链功能 **[重要]** - 新增:新增单元测试 **[重要]** - 新增:新增临时令牌验证模块 **[重要]** - 新增:新增`sa-token-temp-jwt`模块整合jwt临时令牌鉴权 **[重要]** - 新增:会话 `SaSession.get()` 增加缓存API,简化代码 - 新增:新增框架调查问卷 - 修复:修复同时引入 `Spring Cloud Bus` 与 `Sa-Token` 冲突的问题 **[重要]** - 修复:修复`SaServletFilter`异常函数中无法自定义`Content-Type`的问题 - 文档:新增微服务依赖引入说明 - 文档:新增认证流程图 - 不兼容更新重构: - 方法:`StpUtil.setLoginId(id)` -> `StpUtil.login(id)` - 方法:`StpUtil.getLoginKey()` -> `StpUtil.getLoginType()` (注意其它所有地方的`LoginKey`均已更改为`loginType`) - 工具类:`SaRouterUtil` -> `SaRouter` - 配置类:`allowConcurrentLogin` -> `isConcurrent` - 配置类:`isV` -> `isPrint` - 为保证平滑更新,旧API仍旧保留,但已增加`@Deprecated`注解,请尽快更新至新API ### v1.19.0 @2021-5-10 - 新增:注解鉴权新增定制loginType功能 **[重要]** - 重构:重构目录结构,抽离`plugin`模块 **[重要]** - 新增:新增 `sa-token-quick-login` 插件,零代码集成登录功能 **[重要]** - 优化:所有函数式接口增加`@FunctionalInterface`注解,感谢群友`@MrXionGe`提供的建议 - 优化:文档优化... ### v1.18.0 @2021-4-24 - 新增:新增权限通配符功能,灵活设置权限 **[重要]** - 修复:修复自动续签处的逻辑错误 - 新增:新增Web开发常见漏洞防护建议 - 修复:修复`SaRequest`中缺少`getMethod()`的bug - 修复:修复自动续签时的逻辑错误,感谢群成员`@N`的建议 - 新增:全局过滤器新增 `beforAuth` 前置函数 - 修复:修复在带有上下文的项目中无法正确获取请求路径的bug,感谢群成员`@dlwlrma`提供的建议 - 新增:新增`SaHolder`上下文持有类,可方便的在上下文中读写数据 - 重构:`SaTokenManager` -> `SaManager` - 重构:`SaTokenInsideUtil` -> `SaFoxUtil` ### v1.17.0 @2021-4-17 - 修复:在WebFlux环境中引入Redis集成包无法启动的问题 - 修复:修复JWT集成示例中版本升级API的变更 - 优化:优化启动时字符画打印 - 文档:新增集成环境说明 - 文档:新增功能介绍图 - 新增:全局过滤器增加限定[拦截路径]与[排除路径]功能 - 重构:全局过滤器执行函数放到成员变量里,连缀风格配置 - 新增:新增全局侦听器,可在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 **[重要]** ### v1.16.0 @2021-4-12 - 新增:新增账号封禁功能,指定时间内账号无法登陆 **[重要]** - 新增:核心包脱离`ServletAPI`,彻底零依赖! **[重要]** - 新增:新增基于`ThreadLocal`的上下文容器 **[重要]** - 新增:新增`Reactor`响应式编程支持,`WebFlux`集成! **[重要]** - 新增:新增全局过滤器,解决拦截器无法拦截静态资源的问题 **[重要]** - 新增:新增微服务网关鉴权方案!可接入`ShenYu`、`Gateway`等网关组件! **[重要]** - 新增:AOP切面定义`Order`顺序为`-100`,可保证在多个自定义切面前执行 - 文档:新增推荐公众号列表 ### v1.15.0 @2021-3-23 - 新增:文档添加源码涉及技术栈说明 - 优化:优化路由拦截器模块文档,更简洁的示例 - 修复:修复非web环境下的错误提示,Request->Response - 修复:修复Cookie注入时path判断错误,感谢@zhangzi0291提供的PR - 新增:文档集成Redis章节新增redis配置示例说明,感谢群友 `@-)` 提供的建议 - 新增:增加token前缀模式,可在配置token读取前缀,适配`Bearer token`规范 **[重要]** - 优化:`SaTokenManager`初始化Bean去除`initXxx`方法,优化代码逻辑 - 新增:`SaTokenManager`新增`stpLogicMap`集合,记录所有`StpLogic`的初始化,方便查找 - 新增:`Session`新增timeout操作API,可灵活修改Session的剩余有效时间 - 新增:token前缀改为强制校验模式,如果配置了前缀,则前端提交token时必须带有 - 优化:精简`SaRouteInterceptor`,只保留自定义验证和默认的登陆验证,去除冗余功能 - 优化:`SaRouterUtil`迁移到core核心包,优化依赖架构 - 优化:默认Dao实现类里`Timer定时器`改为子线程 + sleep 模拟 - 新增:`Session`新增各种类型转换API,可快速方便存取值 **[重要]** - 升级注意: - `SaRouterUtil`类迁移到核心包,注意更换import地址 - `SaRouteInterceptor`去出冗余API,详情参考路由鉴权部分 ### v1.14.0 @2021-3-12 - 新增:新增`SaLoginModel`登录参数Model,适配 [记住我] 模式 **[重要]** - 新增:新增 `StpUtil.login()` 时指定token有效期,可灵活控制用户的一次登录免验证时长 - 新增:新增Cookie时间判断,在`timeout`设置为-1时,`Cookie`有效期将为`Integer.MAX_VALUE` **[重要]** - 新增:新增密码加密工具类,可快速MD5、SHA1、SHA256、AES、RSA加密 **[重要]** - 新增:新增 OAuth2.0 模块 **[重要]** - 新增:`SaTokenConfig`配置类所有set方法支持链式调用 - 新增:`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用 - 优化:`StpLogic`类所有`getKey`方法重名为`splicingKey`,更语义化的函数名称 - 新增:`IsRunFunction`新增`noExe`函数,用于指定当`isRun`值为`false`时执行的函数 - 新增:`SaSession`新增数据存取值操作API - 优化:优化`SaTokenDao`接口,增加Object操作API - 优化:jwt示例`createToken`方法去除默认秘钥判断,只在启动项目时打印警告 - 文档:常见问题新增示例(修改密码后如何立即掉线) - 文档:权限认证文档新增[如何把权限精确搭到按钮级]示例说明 - 文档:优化文档,部分模块添加图片说明 ### v1.13.0 @2021-2-9 - 优化:优化源码注释与文档 - 新增:文档集成Gitalk评论系统 - 优化:源码包`Maven`版本号更改为变量形式 - 修复:文档处方法名`getPermissionList`错误的bug - 修复:修复`StpUtil.getTokenInfo()`会触发自动续签的bug - 修复:修复接口 `SaTokenDao` 的 `searchData` 函数注释错误 - 新增:`SaSession`的创建抽象到`SaTokenAction`接口,方便按需重写 - 新建:框架内异常统一继承 `SaTokenException` 方便在异常处理时分辨处理 - 新增:`SaSession`新增`setId()`与`setCreateTime()`方法,方便部分框架的序列化 - 新增:新增`autoRenew`配置,用于控制是否打开自动续签模式 - 新增:同域模式下的单点登录 **[重要]** - 新增:完善分布式会话的文档说明 ### v1.12.0 @2021-1-12 - 新增:提供JWT集成示例 **[重要]** - 新增:新增路由式鉴权,可方便的根据路由匹配鉴权 **[重要]** - 新增:新增身份临时切换功能,可在一个代码段内将会话临时切换为其它账号 **[重要]** - 优化:将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`,更语义化的名称 - 优化:优化文档 - 升级:v1.12.1,新增`SaRouterUtil`工具类,更方便的路由鉴权 **[重要]** ### v1.11.0 @2021-1-10 - 新增:提供AOP注解鉴权方案 **[重要]** - 优化自动生成token的算法 ### v1.10.0 @2021-1-9 - 新增:提供查询所有会话方案 **[重要]** - 修复:修复token设置为永不过期时无法正常被顶下线的bug,感谢github用户 @zjh599245299 提出的bug ### v1.9.0 @2021-1-6 - 优化:`spring-boot-starter-data-redis` 由 `2.3.7.RELEASE` 改为 `2.3.3.RELEASE` - 修复:补上注解拦截器里漏掉验证`@SaCheckRole`的bug - 新增:新增同端互斥登录,像QQ一样手机电脑同时在线,但是两个手机上互斥登录 **[重要]** ### v1.8.0 @2021-1-2 - 优化:优化源码注释 - 修复:修复部分文档错别字 - 修复:修复项目文件夹名称错误 - 优化:优化文档配色,更舒服的代码展示 - 新增:提供`sa-token`集成 `redis` 的 `spring-boot-starter` 方案 **[重要]** - 新增:新增集成 `redis` 时,以`jackson`作为序列化方案 **[重要]** - 新增:dao层默认实现增加定时清理过期数据功能 **[重要]** - 新增:新增`token专属session`, 更灵活的会话管理 **[重要]** - 新增:增加配置,指定在获取`token专属session`时是否必须登录 - 新增:在无token时自动创建会话,完美兼容`token-session`会话模型! **[重要]** - 修改:权限码限定必须为String类型 - 优化:注解验证模式由boolean属性改为枚举方式 - 删除:`StpUtil`删除部分冗长API,保持API清爽性 - 新增:新增角色验证 (角色验证与权限验证已完全分离) **[重要]** - 优化:移除`StpUtil.kickoutByLoginId()`API,由`logoutByLoginId`代替 - 升级:开源协议修改为`Apache-2.0` ### v1.7.0 @2020-12-24 - 优化:项目架构改为maven多模块形式,方便增加新模块 **[重要]** - 优化:与`springboot`的集成改为`springboot-starter`模式,无需`@SaTokenSetup`注解即可完成自动装配 **[重要]** - 新增:新增`activity-timeout`配置,可控制token临时过期与续签功能 **[重要]** - 新增:`timeout`过期时间新增-1值,代表永不过期 - 新增:`StpUtil.getTokenInfo()`改为对象形式,新增部分常用字段 - 优化:解决在无cookie模式下,不集成redis时会话无法主动过期的问题 - 修复:修复文档首页样式问题 ### v1.6.0 @2020-12-17 - 新增:花式token生成方案 **[重要]** - 优化:优化`readme.md` - 修复:修复`SaCookieOper`与`SaTokenAction`无法自动注入的问题 ### v1.5.1 @2020-12-16 - 新增:细化未登录异常类型,提供五种场景值:未提供token、token无效、token已过期 、token已被顶下线、token已被踢下线 **[重要]** - 修复:修复`StpUtil.getSessionByLoginId(String loginId)`方法转换key出错的bug,感谢群友 @(#°Д°)、@一米阳光 发现的bug - 优化:修改方法`StpUtil.getSessionByLoginId(Object loginId)`的isCreate值默认为true - 修改:`方法delSaSession`修改为`deleteSaSession`,更加语义化的函数名称 - 新增:新增`StpUtil.getTokenName()`方法,更语义化的获取tokenName - 新增:新增`SaTokenAction`框架行为Bean,方便重写逻辑 - 优化:`Cookie操作`改为接口代理模式,使其可以被重写 - 优化:文档里集成redis部分增加redis的pom依赖示例 - 修复:登录验证-> `StpUtil.getLoginId_defaultNull()` 修复方法名错误的问题 - 优化:优化`readme.md` - 升级:开源协议修改为`MIT` ### v1.4.0 @2020-9-7 - 优化:修改一些函数、变量名称,使其更符合阿里java代码规范 - 优化:`tokenValue`的读取优先级改为:`request` > `body` > `header` > `cookie` **[重要]** - 新增:新增`isReadCookie`配置,决定是否从`cookie`里读取`token`信息 - 优化:如果`isReadCookie`配置为`false`,那么在登录时也不会把`cookie`写入`cookie` - 新增:新增`getSessionByLoginId(Object loginId, boolean isCreate)`方法 - 修复:修复文档部分错误,修正群号码 ### v1.3.0 @2020-5-2 - 新增:新增 `StpUtil.checkLogin()` 方法,更符合语义化的鉴权方法 - 新增:注册拦截器时可设置 `StpLogic` ,方便不同模块不同鉴权方式 - 新增:抛出异常时增加 `loginType` 区分,方便多账号体系鉴权处理 - 修复:修复启动时的版本字符画版本号打印不对的bug - 修复:修复文档部分不正确之处 - 新增:新增文档的友情链接 ### v1.2.0 @2020-3-7 - 新增:新增注解式验证,可在路由方法中使用注解进行权限验证 **[重要]** - 参考:[注解式验证](use/at-check) ### v1.1.0 @2020-2-12 - 修复:修复`StpUtil.getLoginId(T defaultValue)`取值转换错误的bug ### v1.0.0 @2020-2-4 - 第一个版本出炉 ================================================ FILE: sa-token-doc/more/wenjuan.md ================================================ # 问卷调查 我们想以运营一款产品的心态来运营一个开源框架,所以我们迫切希望您能够填写这份问卷,这有6道选择题,应该只会略微占用您1-3分钟的时间,Sa-Token将会非常重视每一位粉丝的宝贵意见! [https://wj.qq.com/s2/14587150/b5b4/](https://wj.qq.com/s2/14587150/b5b4/) ================================================ FILE: sa-token-doc/oauth2/oauth2-apidoc.md ================================================ # Sa-Token-OAuth2 Server端 API列表 基于官方仓库的搭建示例,`OAuth2-Server`端会暴露出以下API,`OAuth2-Client`端可据此文档进行对接 --- ## 1、模式一:授权码(Authorization Code) ### 1.1、获取授权码 根据以下格式构建URL,引导用户访问 (复制时请注意删减掉相应空格和换行符) ``` url http://{host}:{port}/oauth2/authorize ?response_type=code &client_id={client_id} &redirect_uri={redirect_uri} &scope={scope} &state={state} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | response_type | 是 | 返回类型,这里请填写:`code` | | client_id | 是 | 应用 id | | redirect_uri | 是 | 用户确认授权后,重定向的 url 地址 | | scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | | state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加,如果填写则每次填写的值不可以重复 | 注意点: 1. 如果用户在 `OAuth-Server` 端尚未登录:会被转发到登录视图,你可以参照文档或官方示例自定义登录页面。 2. 如果 `scope` 参数为空,或者请求的 `scope` 用户近期已确认授权过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放 `code` 授权码。 用户确认授权之后,会被重定向至`redirect_uri`,并追加 `code` 参数与 `state` 参数,形如: ``` url redirect_uri?code={code}&state={state} ``` `Code` 授权码具有以下特点: 1. 每次授权产生的 `Code` 码都不一样。 2. `Code` 码用完即废,不能二次使用。 3. 一个 `Code` 的有效期默认为五分钟,超时自动作废。 4. 每次授权产生新 `Code` 码,会导致旧 `Code` 码立即作废,即使旧 `Code` 码尚未使用。
RestAPI 登录接口:/oauth2/doLogin 如果用户在 OAuth-Server 端尚未登录,则会被阻塞在登录界面,开始登录,需要在页面上调用`/oauth2/doLogin`完成登录(此接口非 OAuth2 标准协议接口) ``` url http://{host}:{port}/oauth2/doLogin ?name={name} &pwd={pwd} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | name | 否 | 账号 | | pwd | 否 | 密码 | 访问此接口将进入自定义的 `cfg.doLoginHandle` 函数开始登录,你只要在此函数内调用 `StpUtil.login(xxx)` 即代表登录成功。 另外需要注意:此接口并非只能携带 `name`、`pwd` 参数,因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。
RestAPI 确认授权接口:/oauth2/doConfirm 如果 oauth-client 端申请的 scope 在 OAuth-Server 端需要用户手动确认授权,则会被阻塞在授权界面, 需要在页面上调用`/oauth2/doConfirm`完成授权(此接口非 OAuth2 标准协议接口) ``` url http://{host}:{port}/oauth2/doConfirm ?client_id={value} &scope={value} &build_redirect_uri={true|false} &response_type={value} &redirect_uri={value} &state={value} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | client_id | 是 | 应用 id | | scope | 是 | 具体确认的权限,多个用逗号(或空格)隔开 | | build_redirect_uri | 否 | 是否立即构建 `redirect_uri` 授权地址,取值:true | false | | response_type | 否 | 取 url 上的 `response_type` 参数来提交 | | redirect_uri | 否 | 取 url 上的 `redirect_uri` 参数来提交 | | state | 否 | 取 url 上的 `state` 参数来提交 | 此接口有两种调用方式,一种只提供 `client_id`、`scope` 两个参数,此时返回结果代表是否确认授权成功: ``` js { code: 200, msg: 'ok', data: null, } ``` 一种是指定 `build_redirect_uri: true`,并同时提供 `client_id`、`scope`、`response_type`、`redirect_uri`、`state` 全部参数, 此时返回结果包括最终的 code 授权地址: ``` js { code: 200, msg: 'ok', data: null, redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC' } ``` 前端在 ajax 回调函数中直接使用 `location.href=res.redirect_uri` 跳转即可,无需再重复访问 `/oauth2/authorize` 接口。
### 1.2、根据授权码获取 Access-Token 获得 `Code` 码后,我们可以通过以下接口,获取到用户的 `Access-Token`、`Refresh-Token` 等信息。 ``` url http://{host}:{port}/oauth2/token ?grant_type=authorization_code &client_id={client_id} &client_secret={client_secret} &code={code} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | grant_type | 是 | 授权类型,这里请填写:`authorization_code` | | client_id | 是 | 应用 id | | client_secret | 是 | 应用秘钥 | | code | 是 | 步骤 1.1 中获取到的授权码 | 也可以通过 `Basic Authorization` 方式提交 `client` 信息,格式为在请求 `header` 头添加 `Authorization` 参数: ``` js header['Authorization'] = base64(`${client_id}:${client_secret}`); ``` 接口返回示例: ``` js { "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 "msg": "ok", "data": null, "token_type": "bearer", "access_token": "Gly7mnnXSdCxkOqmOwcA5SbG6ZtPmJVX7ZgSn1pidhRmnenBEgxbWJS8VWxA", // Access-Token值 "refresh_token": "EuYNwpxdc18MpaZLPyhFeyAyzr2IOWEr4q3QUGgPWqdJujQqvohjQEDJpwOm", // Refresh-Token值 "expires_in": 7199, // Access-Token剩余有效期,单位秒 "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 "client_id": "1001", // 应用 id "scope": "userinfo" // 此令牌包含的权限 } ``` ### 1.3、根据 Refresh-Token 刷新 Access-Token (如果需要的话) Access-Token的有效期较短,如果每次过期都需要重新授权的话,会比较影响用户体验,因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token` ``` url http://{host}:{port}/oauth2/refresh ?grant_type=refresh_token &client_id={client_id} &client_secret={client_secret} &refresh_token={refresh_token} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | grant_type | 是 | 授权类型,这里请填写:`refresh_token` | | client_id | 是 | 应用 id | | client_secret | 是 | 应用秘钥 | | refresh_token | 是 | 步骤1.2中获取到的 `Refresh-Token` 值 | 接口返回值同章节1.2,此处不再赘述 ### 1.4、回收 Access-Token (如果需要的话) 在A ccess-Token 过期之前主动将其回收 ``` url http://{host}:{port}/oauth2/revoke ?client_id={client_id} &client_secret={client_secret} &access_token={access_token} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | client_id | 是 | 应用 id | | client_secret | 是 | 应用秘钥 | | access_token | 是 | 步骤1.2中获取到的`Access-Token`值 | 返回值样例: ``` js { "code": 200, "msg": "ok", "data": null } ``` ### 1.5、根据 Access-Token 获取相应用户的账号信息 注:此接口非 OAuth2 标准协议接口,为官方仓库 demo 模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数 ``` url http://{host}:{port}/oauth2/userinfo?access_token={access_token} ``` 返回值样例: ``` js { "code": 200, "msg": "ok", "nickname": "shengzhang_", // 账号昵称 "avatar": "http://xxx.com/1.jpg", // 头像地址 "age": "18", // 年龄 "sex": "男", // 性别 "address": "山东省 青岛市 城阳区" // 所在城市 } ``` 除了直接在 url 中以 query 参数方式提交 `access_token`,你也可以在 `Authorization` 请求头以 `Bearer Token` 方式提交: ``` js header['Authorization'] = 'Bearer access_token'; ``` ## 2、模式二:隐藏式(Implicit) 根据以下格式构建URL,引导用户访问: ``` url http://{host}:{port}/oauth2/authorize ?response_type=token &client_id={client_id} &redirect_uri={redirect_uri} &scope={scope} &state={state} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | response_type | 是 | 返回类型,这里请填写:`token` | | client_id | 是 | 应用 id | | redirect_uri | 是 | 用户确认授权后,重定向的url地址 | | scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | | state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加,如果填写则每次填写的值不可以重复 | 此模式会越过授权码的步骤,直接返回 `Access-Token` 到前端页面,形如: ``` url redirect_uri#token=xxxx-xxxx-xxxx-xxxx ``` 注意 token 是以 `#` 锚参数的形式拼接到 url 上的。 ## 3、模式三:密码式(Password) 首先在Client端构建表单,让用户输入 Server 端的账号和密码,然后在 Client 端访问接口 ``` url http://{host}:{port}/oauth2/token ?grant_type=password &client_id={client_id} &client_secret={client_secret} &username={username} &password={password} &scope={scope} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | grant_type | 是 | 返回类型,这里请填写:`password`| | client_id | 是 | 应用 id | | client_secret | 是 | 应用秘钥 | | username | 是 | 用户的 `OAuth2-Server` 端账号 | | password | 是 | 用户的 `OAuth2-Server` 端密码 | | scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | 接口返回示例: ``` js { "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 "msg": "ok", "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token 值 "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token 值 "expires_in": 7199, // Access-Token 剩余有效期,单位秒 "refresh_expires_in": 2591999, // Refresh-Token 剩余有效期,单位秒 "client_id": "1001", // 应用 id "scope": "", // 此令牌包含的权限 } ``` > [!WARNING| label:重写认证处理器] > 在正式项目中,password 认证模式需要重写 `PasswordGrantTypeHandler` 处理器,在后面的 [自定义 grant_type](/oauth2/oauth2-custom-grant_type) 章节我们会详细介绍 ## 4、模式四:凭证式(Client Credentials) 以上三种模式获取的都是用户的 `Access-Token`,代表用户对第三方应用的授权, 在OAuth2.0中还有一种针对 Client级别的授权, 即:`Client-Token`,代表应用自身的资源授权 在 Client 端的后台访问以下接口: ``` url http://{host}:{port}/oauth2/client_token ?grant_type=client_credentials &client_id={client_id} &client_secret={client_secret} &scope={scope} ``` 参数详解: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | grant_type | 是 | 返回类型,这里请填写:`client_credentials`| | client_id | 是 | 应用 id | | client_secret | 是 | 应用秘钥 | | scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | 接口返回值样例: ``` js { "code": 200, "msg": "ok", "client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO", // Client-Token 值 "expires_in": 7199, // Token剩余有效时间,单位秒 "client_id": "1001", // 应用 id "scope": null // 包含权限 } ``` 注:`Client-Token`具有延迟作废特性,即:在每次获取最新`Client-Token`的时候,旧`Client-Token`不会立即过期,而是作为`Lower-Client-Token`再次储存起来, 资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用。 ================================================ FILE: sa-token-doc/oauth2/oauth2-at-check.md ================================================ # Sa-Token OAuth2 模块相关注解 sa-token-oauth2 模块扩展了三个注解用于相关数据校验: - `@SaCheckAccessToken`:指定请求中必须包含有效的 `access_token` ,并且包含指定的 `scope`。 - `@SaCheckClientToken`:指定请求中必须包含有效的 `client_token` ,并且包含指定的 `scope`。 - `@SaCheckClientIdSecret`:指定请求中必须包含有效的 `client_id` 和 `client_secret` 信息。 和 Sa-Token-Code 模块的注解一样,你必须先注册框架的内置拦截器,才可以使用这些注解,详细参考:[注解鉴权](/use/at-check) 。 --- ### 1、@SaCheckAccessToken 示例 ``` java @RestController @RequestMapping("/test") public class TestController { // 测试:携带有效的 access_token 才可以进入请求 // 你可以在请求参数中携带 access_token 参数,或者从请求头以 Authorization: bearer xxx 的形式携带 @SaCheckAccessToken @RequestMapping("/checkAccessToken") public SaResult checkAccessToken() { return SaResult.ok("访问成功"); } // 测试:携带有效的 access_token ,并且具备指定 scope 才可以进入请求 @SaCheckAccessToken(scope = "userinfo") @RequestMapping("/checkAccessTokenScope") public SaResult checkAccessTokenScope() { return SaResult.ok("访问成功"); } // 测试:携带有效的 access_token ,并且具备指定 scope 列表才可以进入请求 @SaCheckAccessToken(scope = {"openid", "userinfo"}) @RequestMapping("/checkAccessTokenScopeList") public SaResult checkAccessTokenScopeList() { return SaResult.ok("访问成功"); } } ``` ### 2、@SaCheckClientToken 示例 ``` java @RestController @RequestMapping("/test") public class TestController { // 测试:携带有效的 client_token 才可以进入请求 // 你可以在请求参数中携带 client_token 参数,或者从请求头以 Authorization: bearer xxx 的形式携带 @SaCheckClientToken @RequestMapping("/checkClientToken") public SaResult checkClientToken() { return SaResult.ok("访问成功"); } // 测试:携带有效的 client_token ,并且具备指定 scope 才可以进入请求 @SaCheckClientToken(scope = "userinfo") @RequestMapping("/checkClientTokenScope") public SaResult checkClientTokenScope() { return SaResult.ok("访问成功"); } // 测试:携带有效的 client_token ,并且具备指定 scope 列表才可以进入请求 @SaCheckClientToken(scope = {"openid", "userinfo"}) @RequestMapping("/checkClientTokenScopeList") public SaResult checkClientTokenScopeList() { return SaResult.ok("访问成功"); } } ``` ### 3、@SaCheckClientIdSecret 示例 ``` java @RestController @RequestMapping("/test") public class TestController { // 测试:携带有效的 client_id 和 client_secret 信息,才可以进入请求 // 你可以在请求参数中携带 client_id 和 client_secret 参数,或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带 @SaCheckClientIdSecret @RequestMapping("/checkClientIdSecret") public SaResult checkClientIdSecret() { return SaResult.ok("访问成功"); } } ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-check-domain.md ================================================ # OAuth2 整合-配置域名校验 --- ### 1、code 劫持攻击 在前面章节的 OAuth-Server 搭建示例中: ``` java @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { if("1001".equals(clientId)) { return new SaClientModel() // ... .addAllowRedirectUris("*") // 所有允许授权的 url // ... } return null; } // 其它代码 ... } ``` 配置项 `AllowRedirectUris` 意为配置此 `Client` 端所有允许的授权地址,不在此配置项中的 URL 将无法下发 `code` 授权码。 为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 `code` 劫持的风险。 假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL: > [http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com](http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com) 当不知情的小红被诱导访问了这个 URL 时,它将被重定向至百度首页。 oauth2-ticket-jc 可以看到,代表着用户身份的 code 授权码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个 URL 将小红的 code 授权码自动提交到攻击者自己的服务器,伪造小红身份登录网站。 ### 2、防范方法 造成此漏洞的直接原因就是我们对此 client 配置了过于宽泛的 `AllowRedirectUris` 允许授权地址,防范的方法也很简单,就是缩小 `AllowRedirectUris` 授权范围。 我们将其配置为一个具体的URL: ``` java @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { if("1001".equals(clientId)) { return new SaClientModel() // ... .addAllowRedirectUris("http://sa-oauth-client.com:8002/") // 所有允许授权的 url // ... } return null; } // 其它代码 ... } ``` 再次访问上述链接: oauth2-feifa-rf URL 没有通过校验,拒绝授权! ### 3、配置安全性参考表 | 配置方式 | 举例 | 安全性 | 建议 | | :-------- | :-------- | :-------- | :-------- | | 配置为* | `*` | | **禁止在生产环境下使用** | | 配置到域名 | `http://sa-oauth-client.com:8002/*` | | 不建议在生产环境下使用 | | 配置到详细地址 | `http://sa-oauth-client.com:8002/xxx/xxx` | | 可以在生产环境下使用 | ### 4、其它规则 1、AllowRedirectUris 配置的地址不允许出现 `@` 字符。 - 反例:`http://user@sa-token.cc` - 反例:`http://sa-oauth-client.com@sa-token.cc` *详见源码:[SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) `checkRedirectUri` 方法。* 2、AllowRedirectUris 配置的地址 `*` 通配符只允许出现在字符串末尾,不允许出现在字符串中间位置。 - 反例:`http*://sa-oauth-client.com/` - 反例:`http://*.sa-oauth-client.com/` *详见源码: [SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) `checkRedirectUriListNormalStaticMethod` 方法。* 参考:[github/issue/529](https://github.com/dromara/Sa-Token/issues/529) 感谢这位 `@m4ra7h0n` 用户反馈的漏洞。 ================================================ FILE: sa-token-doc/oauth2/oauth2-custom-api.md ================================================ # OAuth2-自定义 API 路由 --- ### 方式一:修改全局变量 在之前的章节中,我们演示了如何搭建一个 OAuth2 认证中心: ``` java /** * Sa-Token-OAuth2 Server端 Controller */ @RestController public class SaOAuth2ServerController { // OAuth2-Server 端:处理所有 OAuth2 相关请求 @RequestMapping("/oauth2/*") public Object request() { return SaOAuth2ServerProcessor.instance.dister(); } // ... 其它代码 } ``` 这种写法集成简单但却不够灵活。例如获取 code 授权码地址只能是:`http://{host}:{port}/oauth2/authorize`,如果我们想要自定义其API地址,应该怎么做呢? 打开 OAuth2 模块相关源码,有关 API 的设计都定义在: [SaOAuth2Consts.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java) 中,我们可以对其进行二次修改。 例如,我们可以在 Main 方法启动类或者 OAuth2 配置方法中修改变量值: ``` java // 配置 OAuth2 相关参数 @Autowired private void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 自定义API地址 SaOAuth2Consts.Api.authorize = "/oauth2/authorize2"; // ... } ``` 启动项目,统一认证地址就被我们修改成了:`http://{host}:{port}/oauth2/authorize2` ### 方式二:拆分路由入口 根据上述路由入口:`@RequestMapping("/oauth2/*")`,我们给它起一个合适的名字 —— 聚合式路由。 与之对应的,我们可以将其修改为拆分式路由: ``` java /** * Sa-Token-OAuth2 Server端 Controller */ @RestController public class SaOAuth2ServerController { // 模式一:Code授权码 || 模式二:隐藏式 @RequestMapping("/oauth2/authorize") public Object authorize() { return SaOAuth2ServerProcessor.instance.authorize(); } // 用户登录 @RequestMapping("/oauth2/doLogin") public Object doLogin() { return SaOAuth2ServerProcessor.instance.doLogin(); } // 用户确认授权 @RequestMapping("/oauth2/doConfirm") public Object doConfirm() { return SaOAuth2ServerProcessor.instance.doConfirm(); } // Code 换 Access-Token || 模式三:密码式 @RequestMapping("/oauth2/token") public Object token() { return SaOAuth2ServerProcessor.instance.token(); } // Refresh-Token 刷新 Access-Token @RequestMapping("/oauth2/refresh") public Object refresh() { return SaOAuth2ServerProcessor.instance.refresh(); } // 回收 Access-Token @RequestMapping("/oauth2/revoke") public Object revoke() { return SaOAuth2ServerProcessor.instance.revoke(); } // 模式四:凭证式 @RequestMapping("/oauth2/client_token") public Object clientToken() { return SaOAuth2ServerProcessor.instance.clientToken(); } } ``` 拆分式路由 与 聚合式路由 在功能上完全等价,且提供了更为细致的路由管控。 ================================================ FILE: sa-token-doc/oauth2/oauth2-custom-grant_type.md ================================================ # OAuth2-自定义权限处理器 ### 1、需求场景 OAuth2 协议的 `/oauth2/token` 接口定义了两种获取 `access_token` 的 `grant_type`,分别是: - `authorization_code`:使用用户授权的授权码获取 access_token。 - `password`:使用用户提交的账号、密码来获取 access_token。 你可以重写内置 `grant_type` 处理器,或添加自定义 `grant_type` 处理器,来支持更多的场景。 --- ### 2、重写 password 认证模式处理器 当我们按照文档搭建的代码直接测试 password 认证模式时,控制台会得到警告: ``` txt 警告信息:当前 password 认证模式,使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle),仅供开发测试 正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法 ``` 这是因为为方便测试,框架内部直接将 password 认证请求转发到了 `SaOAuth2Strategy.instance.doLoginHandle` 来处理, 在真正的项目中需要大家重写 password 认证模式处理器: ``` java /** * 自定义 Password Grant_Type 授权模式处理器认证过程 */ @Component public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler { @Override public PasswordAuthResult loginByUsernamePassword(String username, String password) { if("sa".equals(username) && "123456".equals(password)) { long userId = 10001; return new PasswordAuthResult(userId); } else { throw new SaOAuth2Exception("无效账号密码"); } } } ``` ### 3、添加自定义 grant_type 处理器 假设有以下需求:通过 手机号+验证码 登录,返回 `access_token`。 #### 3.1、新增验证码发送接口 首先在 oauth2-server 端开放一个接口,为指定手机号发送验证码。 ``` java /** * 自定义手机登录接口 */ @RestController public class PhoneLoginController { @RequestMapping("/oauth2/sendPhoneCode") public SaResult sendCode(String phone) { String code = SaFoxUtil.getRandomNumber(100000, 999999) + ""; SaManager.getSaTokenDao().set("phone_code:" + phone, code, 60 * 5); System.out.println("手机号:" + phone + ",验证码:" + code + ",已发送成功"); return SaResult.ok("验证码发送成功"); } } ``` 真实项目肯定是要对接短信服务商的,此处我们仅做模拟代码,将发送的验证码打印在控制台上。 #### 3.2、自定义 grant_type 处理器 在 oauth2-server 新建 `PhoneCodeGrantTypeHandler` 实现 `SaOAuth2GrantTypeHandlerInterface` 接口: ``` java /** * 自定义 phone_code 授权模式处理器 */ @Component public class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { @Override public String getHandlerGrantType() { return "phone_code"; } @Override public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { // 获取前端提交的参数 String phone = req.getParamNotNull("phone"); String code = req.getParamNotNull("code"); String realCode = SaManager.getSaTokenDao().get("phone_code:" + phone); // 1、校验验证码是否正确 if(!code.equals(realCode)) { throw new SaOAuth2Exception("验证码错误"); } // 2、校验通过,删除验证码 SaManager.getSaTokenDao().delete("phone_code:" + phone); // 3、登录 long userId = 10001; // 模拟 userId,真实项目应该根据手机号从数据库查询 // 4、构建 ra 对象 RequestAuthModel ra = new RequestAuthModel(); ra.clientId = clientId; ra.loginId = userId; ra.scopes = scopes; // 5、生成 Access-Token AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = "phone_code"); return at; } } ``` #### 3.3、为应用添加允许的授权类型 在 `SaOAuth2DataLoader` 实现类中,为 client 的允许授权类型增加自定义的 `phone_code` ``` java // Sa-Token OAuth2:自定义数据加载器 @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { @Override public SaClientModel getClientModel(String clientId) { if("1001".equals(clientId)) { return new SaClientModel() .setClientId("1001") .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") .addAllowRedirectUris("*") // 所有允许授权的 url .addContractScopes("openid", "userid", "userinfo") .addAllowGrantTypes( GrantType.authorization_code, GrantType.implicit, GrantType.refresh_token, GrantType.password, GrantType.client_credentials, "phone_code" // 重要代码:自定义授权模式 手机号验证码登录 ) ; } return null; } // 其它代码 ... } ``` 完工,开始测试。 ### 4、测试步骤 #### 1、先发送验证码 ``` url http://sa-oauth-server.com:8000/oauth2/sendPhoneCode?phone=13144556677 ``` #### 2、请求 token 注意 `grant_type` 要填写我们自定义的 `phone_code`,code 的具体值可以在后端的控制台上看到 ``` url http://sa-oauth-server.com:8000/oauth2/token ?grant_type=phone_code &client_id=1001 &client_secret=aaaa-bbbb-cccc-dddd-eeee &scope=openid &phone=13144556677 &code={value} ``` 返回结果参考如下: ``` js { "code": 200, "msg": "ok", "data": null, "token_type": "bearer", "access_token": "pfxRz6KVacwvKNu4IHmDsCJs33kvvARs2z1lTch7stog8nRt6rfVLowtAZ0E", "refresh_token": "qcFD6Wo2qZidofXQtWF5oK5ML6ljHKufQ5SbouBxzGnHhnMjUG4VV0iXZhdE", "expires_in": 7199, "refresh_expires_in": 2591999, "client_id": "1001", "scope": "openid", "openid": "ded91dc189a437dd1bac2274be167d50" } ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-custom-login.md ================================================ # OAuth2 定制化登录页面 --- ### 1、如何自定义 OAuth-Server 端的登录视图? 重写 `SaOAuth2Strategy.instance.notLoginView` 策略: ``` java @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 配置:未登录时返回的View SaOAuth2Strategy.instance.notLoginView = ()->{ return new ModelAndView("xxx.html"); }; } ``` 在以上返回的视图中 ajax 方式调用 `/oauth2/doLogin` 接口,该接口接受以下参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | name | 否 | 账号 | | pwd | 否 | 密码 | 接口返回值根据你重写的 `cfg.doLoginHandle` 策略进行自由决定。 ### 2、如何自定义登录API的接口地址? 根据需求点选择解决方案: #### 2.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数? ``` java // 在任意代码处获取前端提交的参数 String xxx = SaHolder.getRequest().getParam("xxx"); ``` #### 2.2、想完全自定义一个接口来接受前端登录请求? ``` java // 直接定义一个拦截路由为 `/oauth2/doLogin` 的接口即可 @RequestMapping("/oauth2/doLogin") public SaResult ss(String name, String pwd) { System.out.println("------ 请求进入了自定义的API接口 ---------- "); if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功!"); } return SaResult.error("登录失败!"); } ``` #### 2.3、不想使用`/oauth2/doLogin`这个接口,想自定义一个API地址? 答:直接在前端更改点击按钮时 Ajax 的请求地址即可 ### 3、如何自定义 OAuth-Server 端的确认授权视图? 重写 `SaOAuth2Strategy.instance.confirmView` 策略: ``` java @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 配置:授权确认视图 SaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{ Map map = new HashMap<>(); map.put("clientId", clientId); map.put("scope", scopes); return new ModelAndView("confirm.html", map); }; } ``` 在以上返回的视图中 ajax 方式调用 `/oauth2/doConfirm` 接口,即可完成授权,该接口接受以下参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | client_id | 是 | 应用 id | | scope | 是 | 具体授予的权限,多个用逗号(或空格)隔开 | 接口返回值样例: ``` js { "code": 200, "msg": "ok", "data": null, } ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-custom-scope.md ================================================ # OAuth2-自定义 Scope 权限及处理器 --- ### 1、需求场景 一般情况下,对于第三方 oauth2-client 来讲,仅仅拿到用户的 access_token 是不够的,还需要拿到更多的信息,比如用户昵称、头像等资料。 sa-token-oauth2 提供两种模式,让 access_token 可以得到更多信息。 - 自定义接口模式:在 oauth2-server 端开放一个资料查询接口,在 oauth2-client 得到 `access_token` 后,再次调用这个接口来获取 `userinfo` 信息。 - 自定义权限处理器模式:自定义一个 `ScopeHandler`,直接在返回 `access_token` 时追加字段,将 `userinfo` 信息和 `access_token` 一并返回到 oauth2-client。 ### 2、自定义接口模式 #### 1、新建查询接口 在 oauth2-server 新建接口,查询指定 `access_token` 代表的 `userId` 其 `userinfo`: ``` java // 获取 userinfo 信息:昵称、头像、性别等等 @RequestMapping("/oauth2/userinfo") public SaResult userinfo() { // 获取 Access-Token 对应的账号id String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken); System.out.println("-------- 此Access-Token对应的账号id: " + loginId); // 校验 Access-Token 是否具有权限: userinfo SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo"); // 模拟账号信息 (真实环境需要查询数据库获取信息) Map map = new LinkedHashMap<>(); // map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client map.put("nickname", "林小林"); map.put("avatar", "http://xxx.com/1.jpg"); map.put("age", "18"); map.put("sex", "男"); map.put("address", "山东省 青岛市 城阳区"); return SaResult.ok().setMap(map); } ``` #### 2、申请 code 时指定权限 oauth2-client 申请 `code` 时,一定需要加上 `userinfo` 权限 ``` url http://sa-oauth-server.com:8000/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=userinfo ``` #### 3、code 换 access_token 访问上述链接后,得到 `code` 授权码,然后我们拿着 `code` 换 `access_token` ``` url http://sa-oauth-server.com:8000/oauth2/token ?grant_type=authorization_code &client_id=1001 &client_secret=aaaa-bbbb-cccc-dddd-eeee &code=${code} ``` #### 4、access_token 取 userinfo 使用返回的 `access_token` 再次访问接口 `/oauth2/userinfo` ``` url http://sa-oauth-server.com:8000/oauth2/userinfo?access_token=${access_token} ``` 返回以下结果: ``` js { "code": 200, "msg": "ok", "data": null, "nickname": "林小林", "avatar": "http://xxx.com/1.jpg", "age": "18", "sex": "男", "address": "山东省 青岛市 城阳区" } ``` 拿到 userinfo。 ### 3、自定义权限处理器模式 #### 1、新建权限处理器 在 oauth2-server 新建 `UserinfoScopeHandler.java` 实现 `SaOAuth2ScopeHandlerInterface` 接口: ``` java /** * 自定义 userinfo scope 处理器 */ @Component public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface { // 指示当前处理器所要处理的 scope @Override public String getHandlerScope() { return "userinfo"; } // 当构建的 AccessToken 具有此权限时,所需要执行的方法 @Override public void workAccessToken(AccessTokenModel at) { System.out.println("--------- userinfo 权限,加工 AccessTokenModel --------- "); // 模拟账号信息 (真实环境需要查询数据库获取信息) Map map = new LinkedHashMap(); map.put("userId", "10008"); map.put("nickname", "shengzhang_"); map.put("avatar", "http://xxx.com/1.jpg"); map.put("age", "18"); map.put("sex", "男"); map.put("address", "山东省 青岛市 城阳区"); at.extraData.putAll(map); } // 当构建的 ClientToken 具有此权限时,所需要执行的方法 @Override public void workClientToken(ClientTokenModel ct) { } // 当使用 RefreshToken 刷新 AccessToken 时,是否重新执行 workAccessToken 构建方法 // 在一些实时性较高的数据中需要指定为 true @Override public boolean refreshAccessTokenIsWork() { return true; } } ``` 如上所述,所有写入到 `extraData` 中的数据,都将追加返回到 oauth2-client 端。 #### 2、申请 code 时指定权限 oauth2-client 申请 `code` 时,一定需要加上 `userinfo` 权限 ``` url http://sa-oauth-server.com:8000/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=userinfo ``` #### 3、code 换 access_token 访问上述链接后,得到 `code` 授权码,然后我们拿着 `code` 换 `access_token` ``` url http://sa-oauth-server.com:8000/oauth2/token ?grant_type=authorization_code &client_id=1001 &client_secret=aaaa-bbbb-cccc-dddd-eeee &code=${code} ``` 返回结果如下 ``` js { "code": 200, "msg": "ok", "data": null, "token_type": "bearer", "access_token": "LQ24xI0hX25vIzvciHPA0PNsnGCweSFM1Bzl8783li07VAXpw8sEfn9xsta2", "refresh_token": "rKB8mby1Mw8yZXHbWzliHx6lmatcLcULLw5C5cUMBhMMRx72DFg5u0owZgrA", "expires_in": 7199, "refresh_expires_in": 2591999, "client_id": "1001", "scope": "openid,userid,userinfo", "userinfo": { "userId": "10008", "nickname": "shengzhang_", "avatar": "http://xxx.com/1.jpg", "age": "18", "sex": "男", "address": "山东省 青岛市 城阳区" } } ``` 拿到 userinfo。 #### 总结 相比于自定义接口模式,自定义权限处理器模式可以少一次网络请求,让 oauth2-client 端提前拿到 `userinfo` 信息。 ### 4、最终权限处理器 当一个自定义权限处理器,监听的 scope 字符串为 `_FINALLY_WORK_SCOPE` 时,则代表这个权限处理器为“最终权限处理器”。 最终权限处理器会永远在所有权限处理器工作完成之后执行一次,即使 oauth2-client 端没有申请任何 scope,最终权限处理器也会固定执行。 示例: ``` java /** * 最终权限处理器:在所有权限处理器工作完成之后,执行此权限处理器 */ @Component public class FinallyWorkScopeHandler implements SaOAuth2ScopeHandlerInterface { @Override public String getHandlerScope() { return SaOAuth2Consts._FINALLY_WORK_SCOPE; } @Override public void workAccessToken(AccessTokenModel at) { // 在所有权限处理器工作完成之后,执行此处方法加工 AccessToken // System.out.println(123); } @Override public void workClientToken(ClientTokenModel ct) { // System.out.println(456); } } ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-data-loader.md ================================================ # OAuth2-自定义数据加载器 ### 1、基于内存的数据加载 在之前搭建 OAuth2-Server 示例中,我们演示了 client 信息配置方案: ``` java // Sa-Token OAuth2 定制化配置 @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 添加 client oauth2Server.addClient( new SaClientModel() .setClientId("1001") // client id .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 .addAllowRedirectUris("*") // 所有允许授权的 url .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 .addAllowGrantTypes( // 所有允许的授权模式 GrantType.authorization_code, // 授权码式 GrantType.implicit, // 隐式式 GrantType.refresh_token, // 刷新令牌 GrantType.password, // 密码式 GrantType.client_credentials, // 客户端模式 ) ) // 可以添加更多 client 信息,只要保持 clientId 唯一就行了 // oauth2Server.addClient(...) } ``` 你也可以在 `application.yml` 配置中 `client` 信息: ``` yaml # sa-token配置 sa-token: # OAuth2.0 配置 oauth2-server: # client 列表 clients: # 客户端1 1001: # 客户端id client-id: 1001 # 客户端秘钥 client-secret: aaaa-bbbb-cccc-dddd-eeee # 所有允许授权的 url allow-redirect-uris: - http://sa-oauth-client.com:8002 - http://sa-oauth-client.com:8002/* # 所有签约的权限 contract-scopes: - openid - userid - userinfo # 所有允许的授权模式 allow-grant-types: - authorization_code - implicit - refresh_token - password - client_credentials # 客户端2 1002: # 客户端id client-id: 1002 # 更多配置 ... ``` ``` properties ########### 客户端1 # 客户端id sa-token.oauth2-server.clients.1001.client-id=1001 # 客户端秘钥 sa-token.oauth2-server.clients.1001.client-secret=aaaa-bbbb-cccc-dddd-eeee # 所有允许授权的 url sa-token.oauth2-server.clients.1001.allow-redirect-uris[0]=http://sa-oauth-client.com:8002 sa-token.oauth2-server.clients.1001.allow-redirect-uris[1]=http://sa-oauth-client.com:8002/* # 所有签约的权限 sa-token.oauth2-server.clients.1001.contract-scopes[0]=openid sa-token.oauth2-server.clients.1001.contract-scopes[1]=userid sa-token.oauth2-server.clients.1001.contract-scopes[2]=userinfo # 所有允许的授权模式 sa-token.oauth2-server.clients.1001.allow-grant-types[0]=authorization_code sa-token.oauth2-server.clients.1001.allow-grant-types[1]=implicit sa-token.oauth2-server.clients.1001.allow-grant-types[2]=refresh_token sa-token.oauth2-server.clients.1001.allow-grant-types[3]=password sa-token.oauth2-server.clients.1001.allow-grant-types[4]=client_credentials ########### 客户端2 sa-token.oauth2-server.clients.1002.client-id=1002 sa-token.oauth2-server.clients.1002.client-secret=... ``` 这两种方案都是基于内存形式的 client 信息配置,只适合简单的测试,一般真实项目的 client 信息都是保存在数据库中的,下面演示一下如何在数据库中动态获取 client 信息 ### 2、基于数据库的数据加载 你只需要自定义数据加载器:新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。 ``` java /** * Sa-Token OAuth2:自定义数据加载器 */ @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { // 此为模拟数据,真实环境需要从数据库查询 if("1001".equals(clientId)) { return new SaClientModel() .setClientId("1001") // client id .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 .addAllowRedirectUris("*") // 所有允许授权的 url .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 .addAllowGrantTypes( // 所有允许的授权模式 GrantType.authorization_code, // 授权码式 GrantType.implicit, // 隐式式 GrantType.refresh_token, // 刷新令牌 GrantType.password, // 密码式 GrantType.client_credentials // 客户端模式 ) ; } return null; } // 根据 clientId 和 loginId 获取 openid @Override public String getOpenid(String clientId, Object loginId) { // 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询 return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); } } ``` 此种形式更加灵活,后续文档将默认按照此种形式来展示示例。 ================================================ FILE: sa-token-doc/oauth2/oauth2-dev.md ================================================ # Sa-Token-OAuth2 Server端 二次开发用到的所有函数说明 官方示例只提供了基本的授权流程,以及 userinfo 资源的开放,如果您需要开放更多的接口,则二次开发时可能用到以下相关 API 方法 --- ### Client 信息相关 ``` java // 获取 ClientModel,根据 clientId SaOAuth2Util.getClientModel(clientId); // 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 SaOAuth2Util.checkClientModel(clientId); // 校验:clientId 与 clientSecret 是否正确 SaOAuth2Util.checkClientSecret(clientId, clientSecret); // 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes SaOAuth2Util.checkClientSecretAndScope(clientId, clientSecret, scopes); // 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false SaOAuth2Util.isContractScope(clientId, scopes); // 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 SaOAuth2Util.checkContractScope(clientId, scopes); // 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 SaOAuth2Util.checkContractScope(clientModel, scopes); // 校验:该 Client 使用指定 url 作为回调地址,是否合法 SaOAuth2Util.checkRedirectUri(clientId, url); // 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope SaOAuth2Util.isGrantScope(loginId, clientId, scopes); // 删除:指定 loginId 针对指定 Client 的授权信息 SaOAuth2Util.deleteGrantScope(loginId, clientId); ``` ### Code 相关 ``` java // 获取 CodeModel,无效的 code 会返回 null SaOAuth2Util.getCode(code); // 校验 Code,成功返回 CodeModel,失败则抛出异常 SaOAuth2Util.checkCode(code); // 获取 Code,根据索引: clientId、loginId SaOAuth2Util.getCodeValue(clientId, loginId); ``` ### Access-Token 相关 ``` java // 获取 AccessTokenModel,无效的 AccessToken 会返回 null SaOAuth2Util.getAccessToken(accessToken); // 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 SaOAuth2Util.checkAccessToken(accessToken); // 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token SaOAuth2Util.getAccessTokenValueList(clientId, loginId); // 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false SaOAuth2Util.hasAccessTokenScope(accessToken, ...scopes); // 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 SaOAuth2Util.checkAccessTokenScope(accessToken, ...scopes); // 获取 Access-Token 所代表的LoginId SaOAuth2Util.getLoginIdByAccessToken(accessToken); // 获取 Access-Token 所代表的 clientId SaOAuth2Util.getClientIdByAccessToken(accessToken); // 回收一个 Access-Token SaOAuth2Util.revokeAccessToken(accessToken); // 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId); ``` ### Refresh-Token 相关 ``` java // 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null SaOAuth2Util.getRefreshToken(refreshToken); // 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 SaOAuth2Util.checkRefreshToken(refreshToken); // 获取 Refresh-Token 列表:此应用下 对 某个用户 签发的所有 Refresh-Token SaOAuth2Util.getRefreshTokenValueList(clientId, loginId); // 回收一个 Refresh-Token SaOAuth2Util.revokeRefreshToken(refreshToken); // 回收全部 Refresh-Token:指定应用下 指定用户 的全部 Refresh-Token SaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId); // 根据 RefreshToken 刷新出一个 AccessToken SaOAuth2Util.refreshAccessToken(refreshToken); ``` ### Client-Token 相关 ``` java // 获取 ClientTokenModel,无效的 ClientToken 会返回 null SaOAuth2Util.getClientToken(clientToken); // 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 SaOAuth2Util.checkClientToken(clientToken); // 获取 Client-Token 列表:此应用下 对 某个用户 签发的所有 Client-token SaOAuth2Util.getClientTokenValueList(clientId); // 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false SaOAuth2Util.hasClientTokenScope(clientToken, ...scopes); // 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 SaOAuth2Util.checkClientTokenScope(clientToken, ...scopes); // 回收一个 ClientToken SaOAuth2Util.revokeClientToken(clientToken); // 回收全部 Client-Token:指定应用下的全部 Client-Token SaOAuth2Util.revokeClientTokenByIndex(clientId); ``` ### 请求查询 ``` java // 数据读取:从当前请求对象中读取 access_token,并查询到 AccessTokenModel 信息,无效 access_token 抛出异常 // 1、请求参数 access_token,2、请求头 Authorization Bearer access_token SaOAuth2Util.currentAccessToken(); // 数据读取:从当前请求对象中读取 client_token,并查询到 ClientTokenModel 信息,无效 client_token 抛出异常 // 1、请求参数 client_token,2、请求头 Authorization Bearer client_token SaOAuth2Util.currentClientToken(); ``` 详情请参考源码:[码云:SaOAuth2Util.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java) ### OAuth2-Server 所有可重写策略 #### 权限处理器 ``` java // 根据 scope 信息对一个 AccessTokenModel 进行加工处理 SaOAuth2Strategy.instance.workAccessTokenByScope = at -> { // ... } // 当使用 RefreshToken 刷新 AccessToken 时,根据 scope 信息对一个 AccessTokenModel 进行加工处理 SaOAuth2Strategy.instance.refreshAccessTokenWorkByScope = at -> { // ... } // 根据 scope 信息对一个 ClientTokenModel 进行加工处理 SaOAuth2Strategy.instance.workClientTokenByScope = at -> { // ... } ``` #### grant_type 处理器 ``` java // 根据 grantType 构造一个 AccessTokenModel SaOAuth2Strategy.instance.grantTypeAuth = req -> { // ... } ``` #### 凭证创建 ``` java // 创建一个 code value SaOAuth2Strategy.instance.createCodeValue = (clientId, loginId, scopes) -> { // ... } // 创建一个 AccessToken value SaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> { // ... } // 创建一个 RefreshToken value SaOAuth2Strategy.instance.createRefreshToken = (clientId, loginId, scopes) -> { // ... } // 创建一个 ClientToken value SaOAuth2Strategy.instance.createClientToken = (clientId, scopes) -> { // ... } ``` #### 认证流程回调 ``` java // OAuth-Server端:未登录时返回的View SaOAuth2Strategy.instance.notLoginView = () -> { // ... } // OAuth-Server端:确认授权时返回的View SaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> { // ... } // OAuth-Server端:登录函数 SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> { // ... } // OAuth-Server端:用户在授权指定 client 前的检查,如果检查不通过,请直接抛出异常 SaOAuth2Strategy.instance.userAuthorizeClientCheck = (loginId, clientId) -> { // ... } ``` #### 其它 ``` java // 在创建 SaClientModel 时,设置其默认字段 SaOAuth2Strategy.instance.setSaClientModelDefaultFields = (clientModel) -> { // ... } ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-h5.md ================================================ # OAuth2-Server 端前后台分离 ### 1、设计分析 要使 OAuth2-Server 端做到前后台分离,则需要对接口进行一部分改造: - 改造前的接口列表: - `http:{后端主机}/oauth2/authorize` - `http:{后端主机}/oauth2/token` - `http:{后端主机}/oauth2/refresh` - 更多... - 改造后的接口列表: - `http:{前端主机}/oauth2/authorize` - `http:{后端主机}/oauth2/token` - `http:{后端主机}/oauth2/refresh` - 更多... 也就是,只需要重点改造 `/oauth2/authorize` 一个接口即可,`/oauth2/authorize` 接口主要做了三件事: 1. 判断用户在 oauth2-server 端是否登录,未登录会进入 [登录页面],已登录则进入下一步。 2. 判断应用请求的 scope 是否需要用户手动确认授权,需要会进入 [确认授权页面],不需要则进入下一步。 3. 重定向至 `redirect_uri` 指定的 url 地址,并携带 code 授权码参数。 我们只需要把上述逻辑从 oauth2-server 的后端搬到 oauth2-server 的前端即可。 ### 2、OAuth2-Server 后端添加接口 首先在 `oauth2-server` 的后端添加一个接口,用于获取最终授权重定向地址: ``` java /** * Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口) */ @RestController public class SaOAuth2ServerH5Controller { /** * 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx * *

情况1:客户端未登录,返回 code=401,提示用户登录

*

情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认

*

情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数)

* * @return / */ @PostMapping("/oauth2/getRedirectUri") public Object getRedirectUri() { // 获取变量 SaRequest req = SaHolder.getRequest(); SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type); // 1、先判断是否开启了指定的授权模式 SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg); // 2、如果尚未登录, 则先去登录 long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L); if(loginId == 0L) { return SaResult.get(401, "need login", null); } // 3、构建请求 Model RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); // 4、开发者自定义的授权前置检查 SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); // 5、校验:重定向域名是否合法 oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); // 6、校验:此次申请的Scope,该Client是否已经签约 oauth2Template.checkContractScope(ra.clientId, ra.scopes); // 7、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); if(isNeedCarefulConfirm) { SaClientModel cm = oauth2Template.checkClientModel(ra.clientId); if( ! cm.getIsAutoConfirm()) { // code=411,需要用户手动确认授权 return SaResult.get(411, "need confirm", null); } } // 8、判断授权类型,重定向到不同地址 // 如果是 授权码式,则:开始重定向授权,下放code if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) { CodeModel codeModel = dataGenerate.generateCode(ra); String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); return SaResult.ok().set("redirect_uri", redirectUri); } // 如果是 隐藏式,则:开始重定向授权,下放 token if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) { AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); return SaResult.ok().set("redirect_uri", redirectUri); } // 默认返回 throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); } } ``` ### 3、新建前端项目 既然是前后台分离,那肯定要有一个独立的前端项目,所需代码比较冗长,不便于在文档处直接展示,大家可以参考在线仓库示例: [sa-token-demo-oauth2-server-h5/](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/) ### 4、运行测试 在前端 ide 中导入 demo 案例的 `sa-token-demo-oauth2-server-h5` 项目,然后直接预览 `oauth2-authorize.html` 页面,如图所示: sa-oauth2-server-authorize-h5.png 复制上述地址,然后将其配置到 “OAuth2前端测试页” 的 “OAuth2 Server 授权页地址” 选项中,其它选项保持默认不变: sa-oauth2-client-test-h5-page-setting.png 然后根据 “OAuth2前端测试页” 的页面提示进行测试即可,此处不再赘述。 ================================================ FILE: sa-token-doc/oauth2/oauth2-interworking.md ================================================ # OAuth2 与登录会话实现数据互通 --- ### 前提 前提,我们: - 把 OAuth2 模块生成的令牌称作资源令牌(access_token), - 把 StpUtil 登录会话生成的令牌称作会话令牌(satoken)。 正常情况下,资源令牌 与 会话令牌 的数据是不互通的,具体表现就是:当我们拿着 access_token 去访问 satoken 令牌的接口,会被抛出异常:`无效Token:xxxxx` 那么,有什么办法可以做到这两个模块的数据互通呢? ### OAuth2-Server 端数据互通 很简单,你只需要在 `configOAuth2Server` 中重写 Access-Token 的生成策略: ``` java // Sa-Token OAuth2 定制化配置 @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 其它配置 ... // 重写 AccessToken 创建策略,返回会话令牌 SaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> { System.out.println("----返回会话令牌"); return StpUtil.getOrCreateLoginSession(loginId); }; } ``` 重启项目,然后在 OAuth2 模块授权登录,现在生成的 `access_token` ,可以用来访问 `satoken` 的会话接口了。 > [!WARNING| label:注意点] > 数据互通,让前端与后端的交互更加方便,一个 token 即可访问所有接口,但也一定程度上失去了OAuth2的 “不同 Client 不同权限” 的设计意义, > 同时也默认每个 Client 都拥有了账号的会话权限(access_token 与 satoken 为同一个)。 > > 应该根据自己的架构合理分析是否应该整合数据互通。 ### OAuth2-Client 数据互通 除了Server端,Client端也可以打通 `access_token` 与 `satoken` 会话。做法是在 Client 端拿到 `access_token` 后进行登录时,使用 `SaLoginParameter` 预定登录生成的 Token 值 ``` java // 1. 获取到access_token String access_token = ... // 2. 登录时预定生成的token StpUtil.login(uid, new SaLoginParameter().setToken(access_token)); // 3. 其它代码... ``` **疑问:数据互通后,两个 token 的过期策略是什么?** 会话 token 由 `sa-token.timeout` 决定,`access_token` 由 `sa-token.oauth2-server.access-token-timeout` 决定。 数据互通只是将 token 拷贝一份进行复用,动作完成之后两者不再有任何联系。 ================================================ FILE: sa-token-doc/oauth2/oauth2-oidc.md ================================================ # OAuth2 开启 OIDC 协议 (OpenID Connect) --- ### 1、开启步骤 1、引入 `sa-token-jwt` 依赖,用来签发 `id_token` ``` xml cn.dev33 sa-token-jwt ${sa.top.version} ``` ``` gradle // sa-token-jwt 签发 OIDC id_token 令牌 implementation 'cn.dev33:sa-token-jwt:${sa.top.version}' ``` 2、在 `SaOAuth2DataLoader` 实现类中,返回的 `SaClientModel` 中添加 `oidc` 的签约权限。 ``` java @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { @Override public SaClientModel getClientModel(String clientId) { // 此为模拟数据,真实环境需要从数据库查询 if("1001".equals(clientId)) { return new SaClientModel() // .... .addContractScopes("openid", "userid", "userinfo", "oidc") // 此处添加上签约权限:oidc .addAllowGrantTypes( // ... ) ; } return null; } // 其它代码 ... } ``` 3、在 `application.yml` 配置文件中配置 jwt 生成秘钥: ``` yaml sa-token: # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk ``` ``` properties # jwt秘钥 sa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk ``` 注:为了安全起见请不要直接复制官网示例这个字符串(随便按几个字符就好了) ### 2、测试 1、在 OAuth2-Client 端申请授权码时,添加上 `oidc` 权限: ``` url http://sa-oauth-server.com:8000/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=oidc ``` 2、得到授权码后,然后拿着 `code` 换 `access_token` ``` url http://sa-oauth-server.com:8000/oauth2/token ?grant_type=authorization_code &client_id=1001 &client_secret=aaaa-bbbb-cccc-dddd-eeee &code=${code} ``` 3、返回的结果中将包含 `id_token` 字段: ``` js { "code": 200, "msg": "ok", "data": null, "token_type": "bearer", "access_token": "WdpjZdGlXdOzsAcr7gqPwmLVInHrhpznQa2pDOVqZmLXQynBflkcWqE6f5o2", "refresh_token": "hKHwBm3eH6iqSHlXRGWQaziV8OoyHvzmUb97lKEEZnZJLt3NunBFx7rVZWbT", "expires_in": 7199, "refresh_expires_in": 2591999, "client_id": "1001", "scope": "oidc", "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzI0NDI1OTg5LCJpYXQiOjE3MjQ0MjUzODksImF1dGhfdGltZSI6MTcyNDQwMDUyNiwibm9uY2UiOiJLTHlNR08zZ1R0YVdhMEFRcHF0RUNpTk9SWkY1QkhvRCIsImF6cCI6IjEwMDEifQ.gP3UYMexaQ9v0huKUuqhV9-dPxPpaEuFPIlPb2UZaOI" } ``` 4、解析 `id_token` 将得到以下载荷 ``` js { "iss": "http://sa-oauth-server.com:8000", // 签发人 "sub": "10001", // userId "aud": "1001", // clientId "exp": 1724425989, // 令牌到期时间,10位时间戳 "iat": 1724425389, // 签发此令牌的时间,10位时间戳 "auth_time": 1724400526, // 用户认证时间,10位时间戳 "nonce": "KLyMGO3gTtaWa0AQpqtECiNORZF5BHoD", // 随机数,防止重放攻击 "azp": "1001" // clientId } ``` 如果默认携带的载荷无法满足你的业务需求,你还可以自定义追加扩展字段,让 `id_token` 返回更多信息 ### 3、扩展 id_token 载荷 新建 `CustomOidcScopeHandler` 集成 `OidcScopeHandler`,扩展 OIDC 权限处理器,返回更多字段: ``` java /** * 扩展 OIDC 权限处理器,返回更多字段 */ @Component public class CustomOidcScopeHandler extends OidcScopeHandler { @Override public IdTokenModel workExtraData(IdTokenModel idToken) { Object userId = idToken.sub; System.out.println("----- 为 idToken 追加扩展字段 ----- "); idToken.extraData.put("uid", userId); // 用户id idToken.extraData.put("nickname", "lin_xiao_lin"); // 昵称 idToken.extraData.put("picture", "https://sa-token.cc/logo.png"); // 头像 idToken.extraData.put("email", "456456@xx.com"); // 邮箱 idToken.extraData.put("phone_number", "13144556677"); // 手机号 // 更多字段 ... // 可参考:https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims return idToken; } } ``` 重启项目,再次请求授权,返回的 `id_token` 载荷将包含更多字段: ``` js { "iss": "http://sa-oauth-server.com:8000", "sub": "10001", "aud": "1001", "exp": 1724430149, "iat": 1724429549, "auth_time": 1724400526, "nonce": "SBRLOcfeo9FFmLTB8OINvuulam5FMOre", "azp": "1001", "uid": "10001", "nickname": "lin_xiao_lin", "picture": "https://sa-token.cc/logo.png", "email": "456456@xx.com", "phone_number": "13144556677" } ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-openid.md ================================================ # OpenId 与 UnionId

参考视频:OAuth2 授权流程中的 clientId、openId、unionId、userId 都是干嘛的?

### 1、OpenId openid 是用户在某一 client 下的唯一标识,其有如下特点: - 一个用户在同一个 client 下,openid 是固定的,每次请求都会返回相同的值。 - 一个用户在不同的 client 下,openid 是不同的,会返回不同的值。 oauth2-client 在每次授权时可根据返回的 openid 值来确定用户身份。 框架默认的 openid 生成算法为: ``` java md5(prefix + "_" + clientId + "_" + loginId); ``` 其中的 prefix 前缀默认值为:`openid_default_digest_prefix`,你可以通过以下方式配置: ``` yaml # sa-token配置 sa-token: oauth2-server: # 默认 openid 生成算法中使用的摘要前缀 openid-digest-prefix: xxxxxx ``` ``` properties # 默认 openid 生成算法中使用的摘要前缀 sa-token.oauth2-server.openid-digest-prefix=xxxxxx ``` 你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 OpenId 生成算法: ``` java /** * Sa-Token OAuth2:自定义数据加载器 */ @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { // 自定义 openid 生成算法 @Override public String getOpenid(String clientId, Object loginId) { // 此种写法代表使用框架默认算法生成 openid,真实环境建议改为从数据库查询 return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); } } ``` #### openid 算法要求 正常来讲,openid 算法需要保证: 1. 单个 clientId 下同一 loginId 生成的 `openid` 一致。[必须] 2. 多个 clientId 下同一 loginId 生成的 `openid` 不一致。[非常建议] 3. 客户端无法通过 clientId + loginId 推测 `openid` 值。[建议] 4. 客户端无法通过 clientId + loginId + openid 推测该 loginId 在其它 clientId 下的 `openid` 值。[建议] 5. oauth2-server 自身由 `openid` 可以反查出对应的 clientId 和 loginId。[根据业务需求而定是否满足] 框架内置的算法,可以满足 1和2,如果自定义了 `sa-token.oauth2-server.openid-digest-prefix` 配置,可以满足3。 如果自定义配置的 prefix 长度较短,或比较简单呈现规律性,则有客户端根据 clientId + loginId + openid 穷举爆破出 `prefix` 的风险, 从而获得提前计算彩虹表来推测出其它 clientId、loginId 对应 openid 值的能力。 如果自定义的 prefix 前缀比较复杂,让客户端无法爆破,则可以满足4。但依然无法满足5。 所以 openid 算法的最优解,应该是 oauth2-server 采用随机字符串作为 openid,然后自建数据库表来维护其映射关系,这样可以同时满足12345。 表结构参考如下: - id:数据id,主键。 - client_id:应用id。 - user_id:用户账号id。 - openid:对应的 openid 值,随机字符串。 - create_time:数据创建时间。 - xxx:其它需要扩展的字段。 ### 2、UnionId `UnionId` 的特点与 `OpenId` 几乎一致:同一用户在不同 client 里的 UnionId 值是不同的,除非这些应用属于同一主体。 例如:甲公司申请了`应用A`、`应用B`、`应用C`,乙公司申请了`应用D`、`应用F`,那么用户张三: - 在应用 A、B、C 里的 UnionId 值一致。 - 在应用 D、F 里的 UnionId 值一致。 - 在应用 A 和 应用 D 之间,UnionId 值不一致。 那么 Sa-Token 框架是如何识别到某两个应用是否为同一主体的呢?这就需要你在注册应用时指定 `subjectId` 属性了: ``` java /** * Sa-Token OAuth2:自定义数据加载器 */ @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { // 此为模拟数据,真实环境需要从数据库查询 if("1001".equals(clientId)) { return new SaClientModel() .setClientId("xxxx") .setClientSecret("xxxx") .setSubjectId("1000001") // ⚠️ 关键代码:主体 id (可选) // .... ; } return null; } } ``` `subjectId` 代表此应用的拥有者,相同 `subjectId` 值的应用将被识别为同一主体,在授权中返回的 `unionid` 值也将一致。 框架默认的 `unionid` 生成算法为: ``` java md5(prefix + "_" + subjectId + "_" + loginId); ``` 其中的 prefix 前缀默认值为:`unionid_default_digest_prefix`,你可以通过以下方式配置: ``` yaml # sa-token配置 sa-token: oauth2-server: # 默认 unionid 生成算法中使用的摘要前缀 unionid-digest-prefix: xxxxxx ``` ``` properties # 默认 unionid 生成算法中使用的摘要前缀 sa-token.oauth2-server.unionid-digest-prefix=xxxxxx ``` 你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 UnionId 生成算法: ``` java /** * Sa-Token OAuth2:自定义数据加载器 */ @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { // 自定义 unionid 生成算法 @Override public String getUnionid(String subjectId, Object loginId) { // 此种写法代表使用框架默认算法生成 unionid,真实环境建议改为从数据库查询 return SaOAuth2DataLoader.super.getUnionid(subjectId, loginId); } } ``` unionid 算法要求与 openid 基本一致,可参考上述 openid 算法要求介绍,此处暂不赘述。 ### 3、总结 | 类型 | 概念 | | :-------- | :-------- | | userid | 在 oauth2-server 端的用户,其唯一标识 | | clientid | 第三方公司在 oauth2-server 开放平台申请的应用,其唯一标识 | | openid | 用户在某个应用下的唯一标识 | | unionid | 用户在某一组应用下的唯一标识 (按照主体id分组) | ================================================ FILE: sa-token-doc/oauth2/oauth2-questions.md ================================================ # Sa-Token-OAuth2整合-常见问题总结 OAuth2 集成常见问题整理 [[toc]] --- ### 问:搭建好 oauth2-server 服务后,访问返回:`{"msg": "not handle"}`。 返回这个信息,代表你访问的路由有错误,比如说: - 统一认证登录地址是:`http://{host}:{port}/oauth2/authorize`。 - 而你访问的却是:`http://{host}:{port}/oauth2/authorize2`。 地址写错了,框架就不会处理这个请求,会直接返回 `{"msg": "not handle"}`,所有开放地址可参考:[OAuth2 开放接口](/oauth2/oauth2-apidoc) 如果仔细检查地址后没有写错,却依然返回了这个信息,那有可能是对应的接口没有打开,比如说: - sso-server 端的单点注销地址:`http://{host}:{port}/sso/signout`; - sso-client 端的注销地址:`http://{host}:{port}/sso/logout`; 都需要在配置文件配置:`sa-token.sso.is-slo=true`后,才会打开。 ### 问:我参照文档搭建 oauth2-server,一直提示:code 无效,请问怎么回事? 一个 code 码只能使用一次,多次使用就会报这个错。 ### 问:Sa-Token-OAuth2 怎么集成多账号模式? 在 `configOAuth2Server` 里指定 oauth2 模块使用的 `StpLogic` 对象即可: ``` java // Sa-Token OAuth2 定制化配置 @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 其它配置 ... // 指定 oauth2 模块使用的 `StpLogic` 对象 SaOAuth2Manager.setStpLogic(StpUserUtil.stpLogic); } ``` ### 问:授权码流程中 state 参数是干吗用的? state 参数用于验证授权码流程的发起端和接受端是否为同一个客户端,以防止OAuth-Server账号伪装攻击。 授权流程发起端必须保证: - state 参数必须足够随机,不可被预测。 - state 参数与授权码流程发起客户端 一 一 对 应,授权流程发起时创建的 state 必须与接受时返回的 state 值一致。 - 安全起见,一个 state 参数只允许使用一次。 ================================================ FILE: sa-token-doc/oauth2/oauth2-scope-level.md ================================================ # OAuth2 - 为 Scope 划分等级 ### 1、划分等级 我们可以通过配置文件来为 scope 划分等级 ``` yaml # sa-token 配置 sa-token: # OAuth2.0 配置 oauth2-server: # 定义哪些 scope 是高级权限,多个用逗号隔开 higher-scope: openid,userid # 定义哪些 scope 是低级权限,多个用逗号隔开 lower-scope: userinfo ``` ``` properties # 定义哪些 scope 是高级权限,多个用逗号隔开 sa-token.oauth2-server.higher-scope=openid,userid # 定义哪些 scope 是低级权限,多个用逗号隔开 sa-token.oauth2-server.lower-scope=userinfo ``` 如上所示: - 通过 `sa-token.oauth2-server.higher-scope` 配置项指定的 `scope` 将变成 **高级权限**。 - 通过 `sa-token.oauth2-server.lower-scope` 配置项指定的 `scope` 将变成 **低级权限**。 - 其它未指定的 `scope` 将默认为 **一般权限**。 不同的权限等级其差异主要表现在:oauth2-client 授权时是否需要用户手动确认授权。 | 权限等级 | 申请授权时表现 | | :-------- | :-------- | | 高级权限 | 申请授权时:每次都需要用户手动点击确认授权按钮,才会下放 code 授权码 | | 一般权限 | 申请授权时:如果申请的 scope 用户近期授权过,则静默授权,如果近期未授权过,则需要手动点击确认授权按钮 | | 低级权限 | 申请授权时:不需要用户手动点击确认授权,程序自动完成静默授权 | ### 2、详细举例 1、如下例子,oauth2-client 申请的 `openid` 权限为**高级权限**,每次都需要用户手动点击确认授权按钮,才会下放 code 授权码。 ``` url http://{host}:{port}/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=openid ``` 2、如下例子,oauth2-client 申请的 `userinfo` 权限为**低级权限**,此时不需要用户手动点击确认授权,程序自动完成静默授权。 ``` url http://{host}:{port}/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=userinfo ``` 3、如下例子,oauth2-client 申请的 `fans_list` 权限为**一般权限**,首次申请时,需要用户手动点击确认授权,第二次再申请则是静默授权。 ``` url http://{host}:{port}/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=fans_list ``` 4、如下例子,oauth2-client 申请的 `openid,userid,userinfo,fans_list` 权限同时包括 **高级权限**、**低级权限**、**一般权限**: ``` url http://{host}:{port}/oauth2/authorize ?response_type=code &client_id=1001 &redirect_uri=http://sa-oauth-client.com:8002/ &scope=openid,userid,userinfo,fans_list ``` 此时是否需要用户手动点击确认授权按钮?具体规则表现为: - 如果请求的 scope 列表包括高级权限,则必须用户手动点击确认授权。 - 如果 scope 列表不包括高级权限,则将 scope 列表中的所有低级权限剔除。 - 剔除后的 list 大小如果为零,则直接静默授权通过。 - 剔除后的 list 大小如果不为零,则判断剩余的这些 scope 是否全部已近期授权过: - 如果是,则静默授权。 - 如果否,则需要用户手动点击确认授权。 ### 3、申请高级权限时 `/oauth2/authorize` 无法通过验证 由于申请高级权限时,每次都必须用户手动点击确认授权,`/oauth2/authorize` 路由接口是无法完成权限验证操作的。 此时需要将构建 `redirect_uri` 的动作提前,在 `/oauth2/doConfirm` 确认授权接口时额外追加 `build_redirect_uri: true` 等参数: ``` url http://{host}:{port}/oauth2/doConfirm ?client={value} &scope={value} &build_redirect_uri=true &response_type={value} &redirect_uri={value} &state={value} ``` 返回结果示例: ``` js { code: 200, msg: 'ok', data: null, redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC' } ``` 其中 `redirect_uri` 参数为授权挂载code地址,直接在 ajax 回调函数中使用 `location.href=res.redirect_uri` 跳转即可。 自定义确认授权视图修改参考: ``` java // 授权确认视图 cfg.confirmView = (clientId, scopes)->{ String scopeStr = SaFoxUtil.convertListToString(scopes); String yesCode = "fetch('/oauth2/doConfirm' + location.search + '&build_redirect_uri=true', {method: 'POST'})" + ".then(res => res.json())" + ".then(res => location.href=res.redirect_uri)"; String res = "

应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?

" + "

" + " " + " " + "

"; return res; }; ``` ================================================ FILE: sa-token-doc/oauth2/oauth2-server.md ================================================ # 搭建OAuth2-Server --- ### 1、准备工作 首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: ``` url 127.0.0.1 sa-oauth-server.com 127.0.0.1 sa-oauth-client.com ``` ### 2、引入依赖 创建SpringBoot项目 `sa-token-demo-oauth2-server`(不会的同学自行百度或参考仓库示例),引入 `pom.xml` 依赖: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-oauth2 ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token OAuth2.0 模块 implementation 'cn.dev33:sa-token-oauth2:${sa.top.version}' // Sa-Token 整合 RedisTemplate (可选) implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' ``` 注:Redis 相关依赖是非必须的,如果集成了 redis,可以让你更细致的观察到 sa-token-oauth2 的底层数据格式。 ### 3、开放服务 1、新建`SaOAuth2ServerController` ``` java /** * Sa-Token OAuth2 Server端 控制器 */ @RestController public class SaOAuth2ServerController { // OAuth2-Server 端:处理所有 OAuth2 相关请求 @RequestMapping("/oauth2/*") public Object request() { System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl()); return SaOAuth2ServerProcessor.instance.dister(); } // Sa-Token OAuth2 定制化配置 @Autowired public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 添加 client 信息 oauth2Server.addClient( new SaClientModel() .setClientId("1001") // client id .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 .addAllowRedirectUris("*") // 所有允许授权的 url .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 .addAllowGrantTypes( // 所有允许的授权模式 GrantType.authorization_code, // 授权码式 GrantType.implicit, // 隐式式 GrantType.refresh_token, // 刷新令牌 GrantType.password, // 密码式 GrantType.client_credentials // 客户端模式 ) ); // 可以添加更多 client 信息,只要保持 clientId 唯一就行了 // oauth2Server.addClient(...) // 配置:未登录时返回的View SaOAuth2Strategy.instance.notLoginView = () -> { // 简化模拟表单 String doLoginCode = "fetch(`/oauth2/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) " + " .then(res => res.json()) " + " .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )"; String res = "

当前客户端在 OAuth-Server 认证中心尚未登录,请先登录

" + "用户:
" + "密码:
" + ""; return res; }; // 配置:登录处理函数 SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> { if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok(); } return SaResult.error("账号名或密码错误"); }; // 配置:确认授权时返回的 view SaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> { String scopeStr = SaFoxUtil.convertListToString(scopes); String yesCode = "fetch('/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scopeStr + "', {method: 'POST'})" + ".then(res => res.json())" + ".then(res => location.reload())"; String res = "

应用 " + clientId + " 请求授权:" + scopeStr + ",是否同意?

" + "

" + " " + " " + "

"; return res; }; } } ``` 注意: - 在 `doLoginHandle` 函数里如果要获取 name, pwd 以外的参数,可通过 `SaHolder.getRequest().getParam("xxx")` 来获取。 - 你可以在 [框架配置](/use/config?id=SaClientModel属性定义) 了解有关 `SaClientModel` 对象所有属性的详细定义。 2、全局异常处理 ``` java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ``` 3、创建启动类: ``` java /** * 启动:Sa-OAuth2 Server端 */ @SpringBootApplication public class SaOAuth2ServerApplication { public static void main(String[] args) { SpringApplication.run(SaOAuth2ServerApplication.class, args); System.out.println("\nSa-Token-OAuth2 Server端启动成功,配置如下:"); System.out.println(SaOAuth2Manager.getServerConfig()); } } ``` 启动项目 ### 4、访问测试 1、由于暂未搭建Client端,我们可以使用 Sa-Token 官网作为重定向URL进行测试: ``` url http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.cc&scope=openid ``` 2、由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图 sa-oauth2-server-login-view 3、输入 `sa/123456` 进行登录之后,会提示我们确认授权 sa-oauth2-server-scope 4、点击同意授权之后,我们会被重定向至 redirect_uri 页面,并携带了code参数 sa-oauth2-server-code 4、我们拿着code参数,访问以下地址: ``` url http://sa-oauth-server.com:8000/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code} ``` 将得到 `Access-Token`、`Refresh-Token`、`openid`等授权信息: ``` js { "code": 200, "msg": "ok", "data": null, "token_type": "bearer", "access_token": "cAls8jnBLmeo5yuCUMwb8zxaSsQPPzGINXF3NOCjCqFHplr6hagdT6A5HeR2", "refresh_token": "L2rPbJ3aaOXwaB4Zu0EGWNz5EjVNpw5u2oMP9CS2IEap7rR3Hb76ZqqHS07J", "expires_in": 7199, "refresh_expires_in": 2591999, "client_id": "1001", "scope": "openid", "openid": "ded91dc189a437dd1bac2274be167d50" } ``` 测试完毕 ### 5、运行官方示例 以上代码只是简单模拟了一下OAuth2.0的授权流程,现在,我们运行一下官方示例,里面有制作好的UI界面 - OAuth2-Server端: `/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/` [源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server)
- OAuth2-Client端: `/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/` [源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client)
依次启动`OAuth2-Server` 与 `OAuth2-Client`,然后从浏览器访问:[http://sa-oauth-client.com:8002](http://sa-oauth-client.com:8002) sa-oauth2-client-index 如图,可以针对OAuth2.0四种模式进行详细测试 ### 6、OAuth2 前端测试页 OAuth2 前端测试页: `/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/` [源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5)
此示例允许你在前端自由配置 OAuth-Client 端所需的各个参数,方便对 OAuth2 四种模式的测试。 sa-oauth2-client-index

参考视频:OAuth2 四种模式 前端测试页

================================================ FILE: sa-token-doc/oauth2/readme.md ================================================ # Sa-Token-OAuth2.0 模块 --- ### 什么是 OAuth2.0 ?解决什么问题? OAuth2.0 与 SSO 相比,增加了对应用授权范围的控制,减弱了应用之间数据同步的能力。 有关 OAuth2.0 的设计思想网上教程较多,此处不再重复赘述,详细可参考博客: [OAuth2.0 简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html) 如果你还不知道你的项目应该选择 SSO 还是 OAuth2.0,可以参考这篇:[技术选型:[ 单点登录 ] VS [ OAuth2.0 ]](/fun/sso-vs-oauth2) ### OAuth2.0 四种模式 基于不同的使用场景,OAuth2.0设计了四种模式: 1. 授权码(Authorization Code):OAuth2.0 标准授权步骤,Server 端向 Client 端下放 `Code` 码,Client 端再用 `Code` 码换取授权 `Access-Token`。 2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server 端使用 URL 重定向方式直接将 `Access-Token` 下放到 Client 端页面。 3. 密码式(Password):Client 端直接拿着用户的账号密码换取授权 `Access-Token`。 4. 客户端凭证(Client Credentials):Server 端针对 Client 级别的 Token,代表应用自身的资源授权。 sa-oauth2-setup.png 接下来我们将通过简单示例演示如何在 Sa-Token-OAuth2 中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server) ### OAuth2.0 第三方开放平台完整开发流程参考 1. oauth2-server 平台端 1. 搭建 oauth2-server 数据后台管理端,也称:后台管理。(后台人员对底层数据增删改查维护的地方)。 2. 搭建 oauth2-server 数据前台申请端,也称:开放平台。(给第三方公司提供一个申请注册 client 的地方) 3. 搭建 oauth2-server 授权端 以及其接口文档,也称:认证中心。(让第三方公司拿到 access_token) 4. 搭建 oauth2-server 资源端 以及其接口文档,也称:资源中心。(让第三方公司通过 access_token 拿到对应的资源数据) 5. 以上四端可以是一个项目,也可以是四个独立的项目,也可以是一个后端 + 多个前端的形式。 2. oauth2-client 第三方公司端 1. 第三方公司登录 oauth-server 数据前台申请端,申请注册应用,拿到 `clientId`、`clientSecret` 等数据。 2. 根据自己的业务选择对应的 scope 申请签约,等待平台端审核通过。 3. 在自己系统通过 `clientId`、`clientSecret` 等参数对接 oauth2-server 授权端,拿到 `access_token`。 4. 通过 `access_token` 调用 oauth2-server 资源端接口,拿到对应资源数据。 3. 用户端操作 1. 打开第三方公司开发的网站或APP等程序。 2. 一般有个“通过xx第三方登录”的按钮,点它。 3. 跳转到了 oauth2-server 端的网站,在此网站用 oauth2-server 的账号开始登录。 4. 登录完成,继续跳转到授权页,点击确认授权。 5. 授权完成,oauth2-server 端生成一个 code 码,重定向回 oauth2-client 的网站,把 code 参数挂到对应的 url 上。 6. oauth2-client 从 url 中读取 code 参数,提交到 oauth2-client 的后端。 7. oauth2-client 后端拿着 `code`、`clientId`、`clientSecret` 等信息调用 oauth2-server 授权端 的接口,得到 `access_token`。 8. 继续拿着 `access_token` 调用 oauth2-server 资源端获取此用户对应的数据。 9. 一般最终目的拿到一个 openid 值,oauth2-client 根据 openid 进行登录。生成自己的会话 token ,返回到数据到前端。 10. 前端拿到自己 oauth2-client 生成的会话 token ,完成登录。开始进行业务操作。 ================================================ FILE: sa-token-doc/plugin/alone-redis.md ================================================ # Sa-Token-Alone-Redis 独立Redis插件 --- Sa-Token默认的Redis集成方式会把权限数据和业务缓存放在一起,但在部分场景下我们需要将他们彻底分离开来,比如: > [!NOTE| label:业务场景] > 搭建两个Redis服务器,一个专门用来做业务缓存,另一台专门存放Sa-Token权限数据 要将Sa-Token的数据单独抽离出来很简单,你只需要为Sa-Token单独配置一个Redis连接信息即可 --- ### 1、首先引入Alone-Redis依赖 > [!WARNING| label:Spring Boot 4 用户] > 若使用 Spring Boot 4.x,请引入 `sa-token-alone-redis-by-spring-boot4` 替代 `sa-token-alone-redis`。 > 注:当前版本下(v1.45.0),此包尚未发布到 Maven 中央仓库,如需使用请下载源码手动自行打包或直接将源码复制到你的项目中进行使用。 ``` xml cn.dev33 sa-token-alone-redis ${sa.top.version} ``` ``` gradle // Sa-Token 整合 Redis (使用 jackson 序列化方式) implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' ``` ### 2、然后在application.yml中增加配置 ``` yaml # Sa-Token 配置 sa-token: # Token名称 token-name: satoken # Token有效期 timeout: 2592000 # Token风格 token-style: uuid # 配置 Sa-Token 单独使用的 Redis 连接 alone-redis: # Redis数据库索引(默认为0) database: 2 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s spring: # 配置业务使用的 Redis 连接 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s ``` ``` properties ############## Sa-Token 配置 ############## # Token名称 sa-token.token-name=satoken # Token有效期 sa-token.timeout=2592000 # Token风格 sa-token.token-style=uuid ############## 配置 Sa-Token 单独使用的 Redis 连接 ############## # Redis数据库索引(默认为0) sa-token.alone-redis.database=2 # Redis服务器地址 sa-token.alone-redis.host=127.0.0.1 # Redis服务器连接端口 sa-token.alone-redis.port=6379 # Redis服务器连接密码(默认为空) sa-token.alone-redis.password= # 连接超时时间 sa-token.alone-redis.timeout=10s ############## 配置业务使用的 Redis 连接 ############## # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接超时时间 spring.redis.timeout=10s ``` 具体可参考示例:[码云:application.yml](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml) 集群配置说明: alone-redis同样可以配置集群(cluster模式和sentinel模式), 且基础配置参数和spring redis集群配置别无二致 集群配置示例可参考demo项目sa-token-demo-alone-redis-cluster ### 3、测试 新建Controller测试一下 ``` java @RestController @RequestMapping("/test/") public class TestController { @Autowired StringRedisTemplate stringRedisTemplate; // 测试Sa-Token缓存 @RequestMapping("login") public SaResult login(@RequestParam(defaultValue="10001") String id) { System.out.println("--------------- 测试Sa-Token缓存"); StpUtil.login(id); return SaResult.ok(); } // 测试业务缓存 @RequestMapping("test") public SaResult test() { System.out.println("--------------- 测试业务缓存"); stringRedisTemplate.opsForValue().set("hello", "Hello World"); return SaResult.ok(); } } ``` 分别访问两个接口,观察Redis中增加的数据 alone-redis 测试完毕! ### 4、注意点 目前 Sa-Token-Alone-Redis 仅对以下插件有 Redis 分离效果: - sa-token-redis-template - sa-token-redis-template-jdk-serializer ================================================ FILE: sa-token-doc/plugin/aop-at.md ================================================ # AOP注解鉴权 --- 在 [注解式鉴权](/use/at-check) 章节,我们非常轻松的实现了注解鉴权, 但是默认的拦截器模式却有一个缺点,那就是无法在`Controller层`以外的代码使用进行校验 因此Sa-Token提供AOP插件,你只需在`pom.xml`里添加如下依赖,便可以在任意层级使用注解鉴权 ``` xml cn.dev33 sa-token-spring-aop ${sa.top.version} ``` ``` gradle // Sa-Token 整合 SpringAOP 实现注解鉴权 implementation 'cn.dev33:sa-token-spring-aop:${sa.top.version}' ``` #### 注意点: - 使用拦截器模式,只能把注解写在`Controller层`,使用AOP模式,可以将注解写在任意层级
- **拦截器模式和AOP模式不可同时集成**,否则会在`Controller层`发生一个注解校验两次的bug ================================================ FILE: sa-token-doc/plugin/api-key.md ================================================ # API Key 接口调用秘钥 API Key(应用程序编程接口密钥) 是一种用于身份验证和授权的字符串代码,通常由服务提供商生成并分配给开发者或用户。它的主要作用是标识调用 API(应用程序编程接口)的请求来源,确保请求的合法性,并控制访问权限。 以上是官话,简单理解:API Key 是一种接口调用密钥,类似于会话 token ,但比会话 token 具有更灵活的权限控制。 示例仓库地址:[sa-token-demo-apikey](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-apikey) 🔗 sa-api-key ### 1、需求场景 为了帮助大家更好的理解 API Key 的应用场景,我们假设具有以下业务场景: > [!NOTE| label:业务场景] > 你们公司开发了一款论坛网站,非常火爆。 > > 某日,你发现一位用户的头像可以随着日期而变化,Ta 的头像总是显示当前最新日期。 > > 这并未引起你的警觉,因为你是一个程序员,在你看来,写一个任务脚本,每天定时调用 API 更新自己的头像是一件非常简单的事情。 > > 一个月后,越来越多的账号“具有了此功能”,仿佛发生了人传人,Ta 们的头像都可以随着日期而变化,而且颜色各不相同,DIY 的不亦乐乎。 > > 这引起了你的怀疑,如此大批账号的自动化更新行为,显然不是 “某个程序员利用定时脚本更新账号信息” 可以解释的。 > > 一番调查之后,你发现了事情的真相,没有灰产公司捣乱,这批账号也不是机器账号,只是有一个公司为你们的网站开发了一款插件。 > > 这款插件的作用是:用户把自己的 账号+密码 保存在插件中,插件便可以定时更新该账号的头像、昵称、资料等信息。 > > 你觉得插件很有意思,但是插件“要求用户提交账号密码”的行为,让你感到很不爽。 > > 总有一些用户为了得到“些许便利”,而出卖自己的账号密码给插件。 > > 随着时间推移,越来越多的第三方公司或个人为你的网站开发插件:有的可以自动更新账号资料、有的可以自动发帖,有的检测到新粉丝就发送消息通知... > > 最终,不守规矩的插件出现了:一款插件在提供功能的同时,大量收集用户密码等隐私信息,作为不法用途。 > > 为了遏制这种现象,你们公司升级了系统,增加了 IP 校验等风控判断,阻断了这些插件的 API 调用。 > > 似乎……解决了问题?用户再也不会把账号密码交给第三方插件了。 > > 但是插件的需求总是存在的呀,有些用户确实很需要这些插件的能力来提高网站使用体验。 > > 俗话说的好,堵不如疏,既然用户有需求,第三方公司愿意免费打工开发插件,我们何不设计一套授权架构, > 既不需要让用户把账号密码交给第三方插件,又能让插件得到一些权限来调用特定 API 为用户服务。 > > API Key 就是为了完成这种“可控式部分授权” 而设计的一种身份凭证。 为了让第三方插件为用户工作,用户必定是要为插件提供一个“凭证”信息的,然后插件利用“凭证”信息,代替用户调用特定 API 完成一些功能。 不同的凭证信息将会带来不同的后果: | 提供的凭证 | 后果 | | :-------- | :-------- | | 账号密码 | 插件可以得到账号所有权限,安全风险极高 | | 会话 token | 插件可以调用几乎所有 API,安全风险极高,且容易受到用户退出登录导致 token 失效的影响 | | API Key | 在可控的范围内进行部分授权,且可以方便的随时取消授权,只要设计得当,不会造成安全问题 | API Key 具有以下特点: - 1、格式类似于会话 token,是一个随机字符串。 - 2、每个 API Key 都会和具体的用户 id 发生绑定,后端可以查询到此 API Key 的授权人是谁。 - 3、一个用户可以创建多个 API Key,用作不同的插件中。 - 4、每个 API Key 都可以赋予不同的 scope 权限,以做到最小化授权。 - 5、API Key 可以设置有效期,并且随时删除回收,做到灵活控制。 ### 2、引入依赖 在使用 API Key 模块之前,你必须先引入依赖: ``` xml cn.dev33 sa-token-apikey ${sa.top.version} ``` ### 3、创建 API Key 理解了应用场景后,让我们看看 Sa-Token 为 API Key 提供了哪些方法: ``` java // 为指定用户创建一个新的 API Key ApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(10001).setTitle("test"); System.out.println("API Key 值:" + akModel.getApiKey()); // 保存 API Key SaApiKeyUtil.saveApiKey(akModel); // 删除 API Key SaApiKeyUtil.deleteApiKey(apiKey); ``` 一个 ApiKeyModel 可设置以下属性: ``` java ApiKeyModel akModel = new ApiKeyModel(); akModel.setLoginId(10001); // 设置绑定的用户 id akModel.setApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); // 设置 API Key 值 akModel.setTitle("commit"); // 设置名称 akModel.setIntro("提交代码专用"); // 设置描述 akModel.addScope("commit", "pull"); // 设置权限范围 akModel.setExpiresTime(System.currentTimeMillis() + 2592000); // 设置失效时间,13位时间戳,-1=永不失效 akModel.setIsValid(true); // 设置是否有效 akModel.addExtra("name", "张三"); // 设置扩展信息 // 保存 SaApiKeyUtil.saveApiKey(akModel); ``` 查询: ``` java // 获取 API Key 详细信息 ApiKeyModel akModel = SaApiKeyUtil.getApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); // 直接获取 ApiKey 所代表的 loginId Object loginId = SaApiKeyUtil.getLoginIdByApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp"); // 获取指定 loginId 的 ApiKey 列表记录 List apiKeyList = SaApiKeyUtil.getApiKeyList(10001); ``` ### 4、校验 API Key ``` java // 校验指定 API Key 是否有效,无效会抛出异常 ApiKeyException SaApiKeyUtil.checkApiKey("AK-XxxXxxXxx"); // 校验指定 API Key 是否具有指定 Scope 权限,不具有会抛出异常 ApiKeyScopeException SaApiKeyUtil.checkApiKeyScope("AK-XxxXxxXxx", "userinfo"); // 校验指定 API Key 是否具有指定 Scope 权限,返回 true 或 false SaApiKeyUtil.hasApiKeyScope("AK-XxxXxxXxx", "userinfo"); // 校验指定 API Key 是否属于指定账号 id SaApiKeyUtil.checkApiKeyLoginId("AK-XxxXxxXxx", 10001); ``` 注解鉴权示例: ``` java /** * API Key 资源 相关接口 */ @RestController public class ApiKeyResourcesController { // 必须携带有效的 ApiKey 才能访问 @SaCheckApiKey @RequestMapping("/akRes1") public SaResult akRes1() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } // 必须携带有效的 ApiKey ,且具有 userinfo 权限 @SaCheckApiKey(scope = "userinfo") @RequestMapping("/akRes2") public SaResult akRes2() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } // 必须携带有效的 ApiKey ,且同时具有 userinfo、chat 权限 @SaCheckApiKey(scope = {"userinfo", "chat"}) @RequestMapping("/akRes3") public SaResult akRes3() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } // 必须携带有效的 ApiKey ,且具有 userinfo、chat 其中之一权限 @SaCheckApiKey(scope = {"userinfo", "chat"}, mode = SaMode.OR) @RequestMapping("/akRes4") public SaResult akRes4() { ApiKeyModel akModel = SaApiKeyUtil.currentApiKey(); System.out.println("当前 ApiKey: " + akModel); return SaResult.ok("调用成功"); } } ``` ### 5、前端如何提交 API Key? 默认情况下,前端可以从任意途径提交 API Key 字符串,只要后端能接受到。 但是如果后端是通过 `SaApiKeyUtil.currentApiKey()` 方法获取,或者 `@SaCheckApiKey` 注解校验,则需要前端按照一定的格式来提交了: 方式一:通过请求参数或请求头,参数名为 `apikey`(全小写) ``` url /user/getInfo?apikey=AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp ``` 方式二:通过 Basic 参数提交 ``` url http://AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp@localhost:8081/user/getInfo ``` ### 6、打开数据库模式 框架默认将所有 API Key 信息保存在缓存中,这可以称之为“缓存模式”,这种模式下,重启缓存库后,数据将丢失。 如果你想改为“数据库模式”,可以通过 `implements SaApiKeyDataLoader` 实现从数据库加载的逻辑。 ``` java /** * API Key 数据加载器实现类 (从数据库查询) */ @Component public class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader { @Autowired SaApiKeyMapper apiKeyMapper; // 指定框架不再维护 API Key 索引信息,而是由我们手动从数据库维护 @Override public Boolean getIsRecordIndex() { return false; } // 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑) @Override public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) { return apiKeyMapper.getApiKeyModel(apiKey); } } ``` 参考上述代码实现后,框架内部逻辑将会做出一些改变,请注意以下事项: - 1、调用 `SaApiKeyUtil.getApiKey("ApiKey")` 时,会先从缓存中查询,查询不到时调用 `getApiKeyModelFromDatabase` 从数据库加载。 - 2、框架不再维护 API Key 索引数据,这意味着无法再调用 `SaApiKeyUtil.getApiKeyList(10001)` 来获取一个用户的所有的 API Key 数据,请自行从数据库查询。 - 3、调用 `SaApiKeyUtil.saveApiKey(akModel)` 保存时,只会把 API Key 数据保存到缓存中,请自行补充额外代码向数据库保存数据。 - 4、调用 `SaApiKeyUtil.deleteApiKey("ApiKey")` 时,只会删除这个 API Key 在缓存中的数据,不会删除数据库的数据,请自行补充相关代码保证数据双删。 - 5、其它诸如查询 `SaApiKeyUtil.getApiKey("ApiKey")` 或校验 `SaApiKeyUtil.checkApiKeyScope("ApiKey", "userinfo")` 等方法,依旧可以正常调用。 ### 7、多账号模式使用 如果系统有多套账号表,比如 Admin 和 User,只需要指定不同的命名空间即可: 例如 User 账号的 API Key,我们使用原生 `SaApiKeyUtil` 进行创建与校验。 对于 Admin 账号的 API Key,我们则新建一个 `SaApiKeyTemplate` 实例 ``` java // 新建 Admin 账号的 apiKeyTemplate 对象,命名空间为 "admin-apikey" public static SaApiKeyTemplate adminApiKeyTemplate = new SaApiKeyTemplate("admin-apikey"); // 创建一个新的 ApiKey,并返回 @RequestMapping("/createApiKey") public SaResult createApiKey() { ApiKeyModel akModel = adminApiKeyTemplate.createApiKeyModel(StpUtil.getLoginId()).setTitle("test"); adminApiKeyTemplate.saveApiKey(akModel); return SaResult.data(akModel); } // ...校验、查询等操作,均使用新创建的 adminApiKeyTemplate,而非原生 `SaApiKeyUtil` ``` ================================================ FILE: sa-token-doc/plugin/api-sign.md ================================================ # API 接口参数签名

观看本节视频讲解(B站:抓蛙师)

在涉及跨系统接口调用时,我们容易碰到以下安全问题: - 请求身份被伪造。 - 请求参数被篡改。 - 请求被抓包,然后重放攻击。 sa-token-sign 模块将帮你轻松解决以上难题。 本篇将根据假设的需求场景,循序渐进讲明白跨系统接口调用时必做的几个步骤,以及为什么要有这些步骤的原因。 ### 1、需求场景 假设我们有如下业务需求: > [!NOTE| label:业务场景] > 用户在 A 系统参与活动成功后,活动奖励以余额的形式下发到 B 系统。 ### 2、初始方案:直接裸奔 在不考虑安全问题的情况下,我们很容易完成这个需求: 1、在 B 系统开放一个接口。 ``` java /** * 为指定用户添加指定余额 * * @param userId 用户 id * @param money 要添加的余额,单位:分 * @return / */ @RequestMapping("addMoney") public SaResult addMoney(long userId, long money) { // 处理业务 // ... // 返回 return SaResult.ok(); } ``` 2、在 A 系统使用 http 工具类调用这个接口。 ``` java long userId = 10001; long money = 1000; String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money); ``` 上述代码简单的完成了需求,但是很明显它有一个安全问题: B 系统开放的接口不仅可以被 A 系统调用,还可以被其它任何人调用,甚至别人可以本地跑一个 for 循环调用这个接口,为自己无限充值金额。 ### 3、方案升级:增加 secretKey 校验 为防止 B 系统开放的接口被陌生人任意调用,我们增加一个 secretKey 参数 ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money, String secretKey) { // 1、先校验 secretKey 参数是否正确,如果不正确直接拒绝响应请求 if( ! check(secretKey) ) { return SaResult.error("无效 secretKey,无法响应请求"); } // 2、业务代码 // ... // 3、返回 return SaResult.ok(); } ``` 由于 A 系统是我们 “自己人”,所以它可以拿着 `secretKey` 进行合法请求: ``` java long userId = 10001; long money = 1000; String secretKey = "xxxxxxxxxxxxxxxxxxxx"; String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "&secretKey=" + secretKey); ``` 现在,即使 B 系统的接口被暴露了,也不会被陌生人任意调用了,安全性得到了一定的保证,但是仍然存在一些问题: - 如果请求被抓包,secretKey 就会泄露,因为每次请求都在 url 中明文传输了 secretKey 参数。 - 如果请求被抓包,请求的其它参数就可以被任意修改,例如可以将 money 参数修改为 9999999,B系统无法确定参数是否被修改过。 ### 4、方案再升级:使用摘要算法生成参数签名 首先,在 A 系统不要直接发起请求,而是先计算一个 sign 参数: ``` java // 声明变量 long userId = 10001; long money = 1000; String secretKey = "xxxxxxxxxxxxxxxxxxxx"; // 计算 sign 参数 String sign = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey); // 将 sign 拼接在请求地址后面 String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "&sign=" + sign); ``` **注意此处计算签名时,需要将所有参数按照字典顺序依次排列(key除外,挂在最后面)。**以下所有计算签名时同理,不再赘述。 然后在 B 系统接收请求时,使用同样的算法、同样的秘钥,生成 sign 字符串,与参数中 sign 值进行比较: ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money, String sign) { // 在 B 系统,使用同样的算法、同样的密钥,计算出 sign2,与传入的 sign 进行比对 String sign2 = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey); if( ! sign2.equals(sign)) { return SaResult.error("无效 sign,无法响应请求"); } // 2、业务代码 // ... // 3、返回 return SaResult.ok(); } ``` 因为 sign 的值是由 userId、money、secretKey 三个参数共同决定的,所以只要有一个参数不一致,就会造成最终生成 sign 也是不一致的,所以,根据比对结果: - 如果 sign 一致,说明这是个合法请求。 - 如果 sign 不一致,说明发起请求的客户端秘钥不正确,或者请求参数被篡改过,是个不合法请求。 此方案优点: - 不在 url 中直接传递 secretKey 参数了,避免了泄露风险。 - 由于 sign 参数的限制,请求中的参数也不可被篡改,B 系统可放心的使用这些参数。 此方案仍然存在以下缺陷: - 被抓包后,请求可以被无限重放,B 系统无法判断请求是真正来自于 A 系统发出的,还是被抓包后重放的。 ### 5、方案再再升级:追加 nonce 随机字符串 首先,在 A 系统发起调用前,追加一个 nonce 参数,一起参与到签名中: ``` java // 声明变量 long userId = 10001; long money = 1000; String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串 String secretKey = "xxxxxxxxxxxxxxxxxxxx"; // 计算 sign 参数 String sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); // 将 sign 拼接在请求地址后面 String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "nonce=" + nonce + "&sign=" + sign); ``` 然后在 B 系统接收请求时,也把 nonce 参数加进去生成 sign 字符串,进行比较: ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money, String nonce, String sign) { // 1、检查此 nonce 是否已被使用过了 if(CacheUtil.get("nonce_" + nonce) != null) { return SaResult.error("此 nonce 已被使用过了,请求无效"); } // 2、验证签名 String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); if( ! sign2.equals(sign)) { return SaResult.error("无效 sign,无法响应请求"); } // 3、将 nonce 记入缓存,防止重复使用 CacheUtil.set("nonce_" + nonce, "1"); // 4、业务代码 // ... // 5、返回 return SaResult.ok(); } ``` 代码分析: - 为方便理解,我们先看第 3 步:此处在校验签名成功后,将 nonce 随机字符串记入缓存中。 - 再看第 1 步:每次请求进来,先查看一下缓存中是否已经记录了这个随机字符串,如果是,则立即返回:无效请求。 这两步的组合,保证了一个 nonce 随机字符串只能被使用一次,如果请求被抓包后重放,是无法通过 nonce 校验的。 至此,问题似乎已被解决了 …… 吗? 别急,我们还有一个问题没有考虑:这个 nonce 在字符串在缓存应该被保存多久呢? - 保存 15 分钟?那抓包的人只需要等待 15 分钟,你的 nonce 记录在缓存中消失,请求就可以被重放了。 - 那保存 24 小时?保存一周?保存半个月?好像无论保存多久,都无法从根本上解决这个问题。 你可能会想到,那我永久保存吧。这样确实能解决问题,但显然服务器承载不了这么做,即使再微小的数据量,在时间的累加下,也总一天会超出服务器能够承载的上限。 ### 6、方案再再再升级:追加 timestamp 时间戳 我们可以再追加一个 timestamp 时间戳参数,将请求的有效性限定在一个有限时间范围内,例如 15分钟。 首先,在 A 系统追加 timestamp 参数: ``` java // 声明变量 long userId = 10001; long money = 1000; String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串 long timestamp = System.currentTimeMillis(); // 系统当前时间戳 String secretKey = "xxxxxxxxxxxxxxxxxxxx"; // 计算 sign 参数 String sign = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); // 将 sign 拼接在请求地址后面 String res = HttpUtil.request("http://b.com/api/addMoney" + "?userId=" + userId + "&money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&sign=" + sign); ``` 在 B 系统检测这个 timestamp 是否超出了允许的范围 ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) { // 1、检查 timestamp 是否超出允许的范围(此处假定最大允许15分钟差距) long timestampDisparity = System.currentTimeMillis() - timestamp; // 实际的时间差 if(timestampDisparity > 1000 * 60 * 15) { return SaResult.error("timestamp 时间差超出允许的范围,请求无效"); } // 2、检查此 nonce 是否已被使用过了 // 代码同上,不再赘述 // 3、验证签名 // 代码同上,不再赘述 // 4、将 nonce 记入缓存,ttl 有效期和 allowDisparity 允许时间差一致 CacheUtil.set("nonce_" + nonce, "1", 1000 * 60 * 15); // 5、业务代码 ... // 6、返回 return SaResult.ok(); } ``` 至此,抓包者: - 如果在 15 分钟内重放攻击,nonce 参数不答应:缓存中可以查出 nonce 值,直接拒绝响应请求。 - 如果在 15 分钟后重放攻击,timestamp 参数不答应:超出了允许的 timestamp 时间差,直接拒绝响应请求。 ### 7、服务器的时钟差异造成安全问题 以上的代码,均假设 A 系统服务器与 B 系统服务器的时钟一致,才可以正常完成安全校验,但在实际的开发场景中,有些服务器会存在时钟不准确的问题。 假设 A 服务器与 B 服务器的时钟差异为 10 分钟,即:在 A 服务器为 8:00 的时候,B 服务器为 7:50。 1. A 系统发起请求,其生成的时间戳也是代表 8:00。 2. B 系统接受到请求后,完成业务处理,此时 nonce 的 ttl 为 15分钟,到期时间为 7:50 + 15分 = 8:05。 3. 8.05 后,nonce 缓存消失,抓包者重放请求攻击: - timestamp 校验通过:因为时间戳差距仅有 8.05 - 8.00 = 5分钟,小于 15 分钟,校验通过。 - nonce 校验通过:因为此时 nonce 缓存已经消失,可以通过校验。 - sign 校验通过:因为这本来就是由 A 系统构建的一个合法签名。 - 攻击完成。 要解决上述问题,有两种方案: - 方案一:修改服务器时钟,使两个服务器时钟保持一致。 - 方案二:在代码层面兼容时钟不一致的场景。 要采用方案一的同学可自行搜索一下同步时钟的方法,在此暂不赘述,此处详细阐述一下方案二。 我们只需简单修改一下,B 系统校验参数的代码即可: ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) { // 1、检查 timestamp 是否超出允许的范围 (⚠️ 重点一:此处需要取绝对值) long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); if(timestampDisparity > 1000 * 60 * 15) { return SaResult.error("timestamp 时间差超出允许的范围,请求无效"); } // 2、检查此 nonce 是否已被使用过了 // 代码同上,不再赘述 // 3、验证签名 // 代码同上,不再赘述 // 4、将 nonce 记入缓存,防止重复使用(⚠️ 重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) CacheUtil.set("nonce_" + nonce, "1", (1000 * 60 * 15) * 2); // 5、业务代码 ... // 6、返回 return SaResult.ok(); } ``` ### 8、最终版方案 此处再贴一下完整的代码。 A 系统(发起请求端): ``` java // 声明变量 long userId = 10001; long money = 1000; String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串 long timestamp = System.currentTimeMillis(); // 当前时间戳 String secretKey = "xxxxxxxxxxxxxxxxxxxx"; // 计算 sign 参数 String sign = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); // 将 sign 拼接在请求地址后面 String res = HttpUtil.request("http://b.com/api/addMoney" + "?userId=" + userId + "&money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&sign=" + sign); ``` B 系统(接收请求端): ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) { // 1、检查 timestamp 是否超出允许的范围 long allowDisparity = 1000 * 60 * 15; // 允许的时间差:15分钟 long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); // 实际的时间差 if(timestampDisparity > allowDisparity) { return SaResult.error("timestamp 时间差超出允许的范围,请求无效"); } // 2、检查此 nonce 是否已被使用过了 if(CacheUtil.get("nonce_" + nonce) != null) { return SaResult.error("此 nonce 已被使用过了,请求无效"); } // 3、验证签名 String sign2 = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); if( ! sign2.equals(sign)) { return SaResult.error("无效 sign,无法响应请求"); } // 4、将 nonce 记入缓存,防止重复使用,注意此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 CacheUtil.set("nonce_" + nonce, "1", allowDisparity * 2); // 5、业务代码 ... // 6、返回 return SaResult.ok(); } ``` ### 9、使用 Sa-Token 框架完成 API 参数签名 接下来步入正题,使用 sa-token-sign 模块,方便的完成 API 签名创建、校验等步骤: - 不限制请求的参数数量,方便组织业务需求代码。 - 自动补全 nonce、timestamp 参数,省时省力。 - 自动构建签名,并序列化参数为字符串。 - 一句代码完成 nonce、timestamp、sign 的校验,防伪造请求调用、防参数篡改、防重放攻击。 #### 9.1、引入依赖 请求发起端和接收端都需要引入: ``` xml cn.dev33 sa-token-sign ${sa.top.version} ``` ``` gradle // Sa-Token 整合 API 参数签名校验 implementation 'cn.dev33:sa-token-sign:${sa.top.version}' ``` #### 9.2、配置秘钥 请求发起端和接收端需要配置一个相同的秘钥,在 `application.yml` 中配置: ``` yml sa-token: sign: # API 接口签名秘钥 (随便乱摁几个字母即可) secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` #### 9.3、请求发起端构建签名 ``` java // 请求地址 String url = "http://b.com/api/addMoney"; // 请求参数 Map paramMap = new LinkedHashMap<>(); paramMap.put("userId", 10001); paramMap.put("money", 1000); // 更多参数,不限制数量... // 补全 timestamp、nonce、sign 参数,并序列化为 kv 字符串 String paramStr = SaSignUtil.addSignParamsAndJoin(paramMap); // 将参数字符串拼接在请求地址后面 url += "?" + paramStr; // 发送请求 String res = HttpUtil.request(url); // 根据返回值做后续处理 System.out.println("server 端返回信息:" + res); ``` #### 9.4、请求接受端校验签名 ``` java // 为指定用户添加指定余额 @RequestMapping("addMoney") public SaResult addMoney(long userId, long money) { // 1、校验请求中的签名 SaSignUtil.checkRequest(SaHolder.getRequest()); // 2、校验通过,处理业务 System.out.println("userId=" + userId); System.out.println("money=" + money); // 3、返回 return SaResult.ok(); } ``` 如上代码便可简单方便的完成 API 接口参数签名校验,当请求端的秘钥不对,或者请求参数被篡改、请求被重放时,均无法通过 `SaSignUtil.checkRequest` 校验。 ``` js { "code": 500, "msg": "无效签名:9c3e3e98c7d543fb599766c9d3f3b5ff", "data": null } ``` ### 10、使用注解校验签名 `@SaCheckSign` 注解用于为一个接口提供签名校验,用于替代 `SaSignUtil.checkRequest(SaHolder.getRequest())`,示例如下: ``` java // 校验全部参数:效果等同于 SaSignUtil.checkRequest(SaHolder.getRequest()) @SaCheckSign @RequestMapping("test1") public SaResult test1() { // code ... return SaResult.ok(); } // 指定参与签名的参数有哪些:效果等同于 SaSignUtil.checkRequest(SaHolder.getRequest(), "id", "name"); @SaCheckSign(verifyParams = {"id", "name"}) @RequestMapping("test2") public SaResult test2() { // code ... return SaResult.ok(); } // 指定: 在多应用模式下,使用的 appid,详情见下 @SaCheckSign(appid = "xm-shop") @RequestMapping("test3") public SaResult test3() { // code ... return SaResult.ok(); } ``` ### 11、多应用模式 有时候我们可能需要同时与多个应用对接,每个应用都需要使用不同的秘钥: 首先在配置文件配置多个应用信息: ``` yaml sa-token: # API 签名配置 多应用模式 sign-many: # 应用1 xm-shop: secret-key: 0123456789abcdefg digest-algo: md5 # 应用2 xm-forum: secret-key: 0123456789hijklmnopq digest-algo: sha256 # 应用3 xm-video: secret-key: 12341234aaaaccccdddd digest-algo: sha512 ``` ``` properties # API 签名配置 多应用模式 # 应用1 sa-token.sign-many.xm-shop.secret-key=0123456789abcdefg sa-token.sign-many.xm-shop.digest-algo=md5 # 应用2 sa-token.sign-many.xm-forum.secret-key=0123456789hijklmnopq sa-token.sign-many.xm-forum.digest-algo=sha256 # 应用3 sa-token.sign-many.xm-video.secret-key=12341234aaaaccccdddd sa-token.sign-many.xm-video.digest-algo=sha512 ``` ``` java @Autowired public void configSaToken(SaTokenConfig config) { // API 签名配置 多应用模式 // 应用1 config.getSignMany().put("xm-shop", new SaSignConfig() .setSecretKey("0123456789abcdefg") // 秘钥 .setDigestAlgo("md5") // 签名算法 ); // 应用2 config.getSignMany().put("xm-forum", new SaSignConfig() .setSecretKey("0123456789hijklmnopq") .setDigestAlgo("sha256") ); // 应用3 config.getSignMany().put("xm-video", new SaSignConfig() .setSecretKey("12341234aaaaccccdddd") // 自定义签名算法示例 .setDigestMethod(fullStr -> { return SaSecureUtil.sha384(fullStr); }) ); } ``` 然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作: ``` java // 创建签名示例 String paramStr = SaSignMany.getSignTemplate("xm-shop").addSignParamsAndJoin(paramMap); // 校验签名示例 SaSignMany.getSignTemplate("xm-shop").checkRequest(SaHolder.getRequest()); ``` ================================================ FILE: sa-token-doc/plugin/custom-serializer.md ================================================ # 序列化插件扩展包 --- 引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。(娱乐向,不建议上生产) ``` xml cn.dev33 sa-token-serializer-features ${sa.top.version} ``` ``` gradle // Sa-Token 自定义 String 序列化方案合集 implementation 'cn.dev33:sa-token-serializer-features:${sa.top.version}' ``` #### 1、SaSerializerForBase64UseTianGan base64 编码,采用 十大天干、十二地支 等64个中文字符作为元字符集 ``` java // 设置序列化方案: base64 编码,采用 十大天干、十二地支 等64个中文字符作为元字符集 @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan()); } ``` 效果图: sa-custom-serializer-tiangan.png #### 2、SaSerializerForBase64UsePeriodicTable base64 编码,采用 元素周期表 前六十四位作为元字符集 ``` java // 设置序列化方案: base64 编码,采用 元素周期表 前六十四位作为元字符集 @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable()); } ``` 效果图: sa-custom-serializer-yszqb.png #### 3、SaSerializerForBase64UseSpecialSymbols base64 编码,采用64个特殊符号作为元字符集 ``` java // 设置序列化方案: base64 编码,采用64个特殊符号作为元字符集 @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols()); } ``` 效果图: sa-custom-serializer-tsfh.png #### 4、SaSerializerForBase64UseEmoji base64 编码,采用 64 个 Emoji 小黄脸作为元字符集,无填充字符 ``` java // 设置序列化方案: base64 编码,采用 64 个 Emoji 小黄脸作为元字符集,无填充字符 @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji()); } ``` 效果图: sa-custom-serializer-emoji.png sa-custom-serializer-emoji2.png ================================================ FILE: sa-token-doc/plugin/dao-extend.md ================================================ # 缓存层扩展 --- 对于权限框架来讲,最容易碰到的扩展点便是数据存储方式,为了方便对接不同的缓存中间件,Sa-Token将所有数据持久化操作抽象到SaTokenDao接口, 开发者要对接不同的平台只需要实现此接口即可,接口签名:[SaTokenDao.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java) 框架已提供的集成包包括: - 默认方式:储存在内存中,位于core核心包。 - sa-token-redis-template:Redis Template 集成包。 - sa-token-redis-template-jdk-serializer:Redis 集成包,使用 jdk 默认序列化方式。 - sa-token-hutool-timed-cache:集成 hutool 框架的 Timed-Cache 缓存方案(基于内存)。 - sa-token-caffeine:集成 Caffeine 缓存方案(基于内存)。 - sa-token-redisson:集成 Redisson 客户端。 - sa-token-redisson-spring-boot-starter:集成 Redisson 客户端 - SpringBoot 自动配置包 。 - sa-token-redisx:Redisx 集成包。 有关 Redis 集成,详细参考:[集成Redis](/up/integ-redis),更多存储方式欢迎提交PR **扩展:集成 MongoDB** - [集成 MongoDB 参考一](/up/integ-spring-mongod-1) - [集成 MongoDB 参考二](/up/integ-spring-mongod-2) ================================================ FILE: sa-token-doc/plugin/dubbo-extend.md ================================================ # 和 Dubbo 集成 本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 --- ### 先说说要解决的问题 在 Dubbo 的整个调用链中,代码被分为 Consumer 端和 Provider 端,为方便理解我们可以称其为 `[调用端]` 和 `[被调用端]`。 RPC 模式的调用,可以让我们像调用本地方法一样完成服务通信,然而这种便利下却隐藏着两个问题: - 上下文环境的丢失。 - 上下文参数的丢失。 这种问题作用在 Sa-Token 框架上就是,在 [ 被调用端 ] 调用 Sa-Token 相关API会抛出异常: **`无效上下文`** 。 所以本插件的目的也就是解决上述两个问题: - 在 [ 被调用端 ] 提供以 Dubbo 为基础的上下文环境 - 在 RPC 调用时将 Token 传递至 [ 被调用端 ],同时在调用结束时将 Token 回传至 [ 调用端 ]。 ### 引入插件 在项目已经引入 Dubbo 的基础上,继续添加依赖(Consumer 端和 Provider 端都需要引入): ``` xml cn.dev33 sa-token-dubbo ${sa.top.version} ``` ``` gradle // Sa-Token 整合 Dubbo implementation 'cn.dev33:sa-token-dubbo:${sa.top.version}' ``` 注:如果使用的是 dubbo3,只需要将 `sa-token-dubbo` 修改为 `sa-token-dubbo3` 即可。 然后我们就可以愉快的做到以下事情: 1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。 2. 在 [ 调用端 ] 登录的会话,其登录状态可以自动传递到 [ 被调用端 ] 。 3. 在 [ 被调用端 ] 登录的会话,其登录状态也会自动回传到 [ 调用端 ] 。 但是我们仍具有以下限制: 1. [ 调用端 ] 与 [ 被调用端 ] 的 `SaStorage` 数据无法互通。 2. [ 被调用端 ] 执行的 `SaResponse.setHeader()`、`setStatus()` 等代码无效。 应该合理避开以上 API 的使用。 ### RPC调用鉴权 在之前的 [Same-Token](/micro/same-token) 章节,我们演示了基于 Feign 的 RPC 调用鉴权,下面我们演示一下在 Dubbo 中如何集成 Same-Token 模块。 其实思路和 Feign 模式一致,在 [ 调用端 ] 追加 Same-Token 参数,在 [ 被调用端 ] 校验这个 Same-Token 参数: - 校验通过:调用成功。 - 校验不通过:调用失败,抛出异常。 我们有两种方式完成整合。 ##### 方式一、使用配置(推荐) 直接在 `application.yml` 配置即可: ``` yaml sa-token: # 打开 RPC 调用鉴权 check-same-token: true ``` ``` properties # 打开 RPC 调用鉴权 sa-token.check-same-token=true ``` ##### 方式二、自建 Dubbo 过滤器校验 此方式略显繁琐,好处是除了Same-Token,我们还可以添加其它自定义参数 (attachment)。 1、在 [ 调用端 ] 的 `\resources\META-INF\dubbo\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件 ``` html dubboConsumerFilter=com.pj.DubboConsumerFilter ``` 新建 `DubboConsumerFilter.java` 过滤器 ``` java package com.pj; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; import cn.dev33.satoken.same.SaSameUtil; /** * Sa-Token 整合 Dubbo Consumer端过滤器 */ @Activate(group = {CommonConstants.CONSUMER}, order = -10000) public class DubboConsumerFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { // 追加 Same-Token 参数 RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); // 如果有其他自定义附加数据,如租户 // RpcContext.getContext().setAttachment("tenantContext", tenantContext); // 开始调用 return invoker.invoke(invocation); } } ``` 2、在 [ 被调用端 ] 的 `\resources\META-INF\dubbo\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件 ``` html dubboProviderFilter=com.pj.DubboProviderFilter ``` 新建 `DubboProviderFilter.java` 过滤器 ``` java package com.pj; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; import cn.dev33.satoken.same.SaSameUtil; /** * Sa-Token 整合 Dubbo Provider端过滤器 */ @Activate(group = {CommonConstants.PROVIDER}, order = -10000) public class DubboProviderFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { // 取出 Same-Token 进行校验 String sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN); SaSameUtil.checkToken(sameToken); // 取出其他自定义附加数据 // TenantContext tenantContext = invocation.getAttachment("tenantContext"); // 开始调用 return invoker.invoke(invocation); } } ``` 然后我们就可以进行安全的 RPC 调用了,不带有 Same-Token 参数的调用都会抛出异常,无法调用成功。 ================================================ FILE: sa-token-doc/plugin/freemarker-extend.md ================================================ # Freemarker 自定义标签 本插件的作用是让我们可以在 Freemarker 页面中使用 Sa-Token 自定义标签以及相关API。 --- ### 1、引入依赖 首先我们确保项目已经引入 Freemarker 依赖,然后在此基础上继续添加: ``` xml cn.dev33 sa-token-freemarker ${sa.top.version} ``` ``` gradle // 在 Freemarker 页面中使用 Sa-Token 自定义标签 implementation 'cn.dev33:sa-token-freemarker:${sa.top.version}' ``` ### 2、注入 Sa-Token Freemarker 标签模板模型 对象 在 SaTokenConfigure 配置类中增加配置 ``` java @Configuration public class SaTokenConfigure { @Autowired FreeMarkerConfigurer configurer; /** * 注入 Sa-Token Freemarker 标签模板模型 对象 */ @PostConstruct public void setSaTokenTemplateModel() throws TemplateModelException { // 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签, // 例如:<#if sa.login()>... configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel()); // 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法, // 例如:${stp.getSession().get('name')} configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic); } } ``` ### 3、使用自定义标签 然后我们就可以愉快的使用在 Freemarker 页面中使用 Sa-Token 自定义标签了 ##### 3.1、登录判断 ``` html

标签方言测试页面

登录之后才能显示: <@sa.login>value

不登录才能显示: <@sa.notLogin>value

``` ##### 3.2、角色判断 ``` html

具有角色 admin 才能显示: <@sa.hasRole value="admin">value

同时具备多个角色才能显示: <@sa.hasRoleAnd value="admin, ceo, cto">value

只要具有其中一个角色就能显示: <@sa.hasRoleOr value="admin, ceo, cto">value

不具有角色 admin 才能显示: <@sa.notRole value="admin">value

``` ##### 3.3、权限判断 ``` html

具有权限 user-add 才能显示: <@sa.hasPermission value="user-add">value

同时具备多个权限才能显示: <@sa.hasPermissionAnd value="user-add, user-delete, user-get">value

只要具有其中一个权限就能显示: <@sa.hasPermissionOr value="user-add, user-delete, user-get">value

不具有权限 user-add 才能显示: <@sa.notPermission value="user-add">value

``` ### 4、调用 Sa-Token 相关API 以上的自定义标签,可以满足我们大多数场景下的权限判断,然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API : ``` html

从SaSession中取值: <#if stp.isLogin()> ${stp.getSession().get('name')}

``` ================================================ FILE: sa-token-doc/plugin/grpc-extend.md ================================================ # 和 grpc 集成 本插件的作用是让 Sa-Token 和 grpc 做一个整合。 --- 和dubbo插件一样,解决了以下问题 1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。 2. 在 [ 调用端 ] 登录的会话,其登录状态可以自动传递到 [ 被调用端 ] ;在 [ 被调用端 ] 登录的会话,其登录状态可以自动回传到 [ 调用端 ] 3. Same-Token 安全校验 --- 和dubbo插件一样,具有以下限制: 1. [ 调用端 ] 与 [ 被调用端 ] 的 `SaStorage` 数据无法互通。 2. [ 被调用端 ] 执行的 `SaResponse.setHeader()`、`setStatus()` 等代码无效。 ### 引入插件 需要springboot环境,添加依赖(调用端和被调用端都需要引入): ``` xml cn.dev33 sa-token-grpc ${sa.top.version} ``` ``` gradle // Sa-Token 整合 grpc implementation 'cn.dev33:sa-token-grpc:${sa.top.version}' ``` --- ### 开启 Same-Token 校验: 直接在 `application.yml` 配置即可: ``` yaml sa-token: # 打开 RPC 调用鉴权 check-same-token: true ``` ``` properties # 打开 RPC 调用鉴权 sa-token.check-same-token=true ``` ================================================ FILE: sa-token-doc/plugin/json-extend.md ================================================ # JSON 序列化扩展 --- Sa-Token 在 Session 存储、Redis 缓存等场景下需要对对象进行 JSON 序列化与反序列化。框架将 JSON 转换逻辑抽象到 `SaJsonTemplate` 接口, 开发者只需引入对应的 JSON 插件依赖,框架会通过 SPI 机制自动完成注入,接口签名:[SaJsonTemplate.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java) 框架已提供的 JSON 序列化插件包括: - **sa-token-jackson**:集成 Jackson(com.fasterxml.jackson),适用于 SpringBoot2/3 等环境。 - **sa-token-jackson3**:集成 Jackson 3(tools.jackson.core),适用于 SpringBoot4、Java 17+ 等环境。 - **sa-token-fastjson**:集成 Fastjson。 - **sa-token-fastjson2**:集成 Fastjson2。 - **sa-token-snack3**:集成 Snack3。 - **sa-token-snack4**:集成 Snack4。 > 若使用 `sa-token-spring-boot-starter` 集成包(含 SpringBoot3),框架会自动引入 Jackson 作为默认 JSON 方案,一般无需额外配置。如需更换为其它 JSON 框架,引入对应插件依赖即可。 ``` xml cn.dev33 sa-token-jackson ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-jackson:${sa.top.version}'` ``` xml cn.dev33 sa-token-jackson3 ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-jackson3:${sa.top.version}'` ``` xml cn.dev33 sa-token-fastjson ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'` ``` xml cn.dev33 sa-token-fastjson2 ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'` ``` xml cn.dev33 sa-token-snack3 ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'` ``` xml cn.dev33 sa-token-snack4 ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-snack4:${sa.top.version}'` 有关 Redis 集成与序列化配置,详细参考:[集成 Redis](/up/integ-redis) 更多自定义序列化方案(如 Base64、天干地支等),可参考:[序列化插件扩展包](/plugin/custom-serializer) ================================================ FILE: sa-token-doc/plugin/jwt-extend.md ================================================ # 和 jwt 集成 本插件的作用是让 Sa-Token 和 jwt 做一个整合。 --- ### 1、引入依赖 首先在项目已经引入 Sa-Token 的基础上,继续添加: ``` xml cn.dev33 sa-token-jwt ${sa.top.version} ``` ``` gradle // Sa-Token 整合 jwt implementation 'cn.dev33:sa-token-jwt:${sa.top.version}' ``` > [!WARNING| label:版本兼容性] > 1. 注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,保险起见:你的项目中要么不引入 hutool,要么引入版本 >= 5.7.14 的 hutool 版本。 > 2. hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题,[关联issue](https://gitee.com/dromara/sa-token/issues/I6L429)。 ### 2、配置秘钥 在 `application.yml` 配置文件中配置 jwt 生成秘钥: ``` yaml sa-token: # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk ``` ``` properties # jwt秘钥 sa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk ``` 注:为了安全起见请不要直接复制官网示例这个字符串(随便按几个字符就好了) ### 3、注入jwt实现 根据不同的整合规则,插件提供了三种不同的模式,你需要 **选择其中一种** 注入到你的项目中 Simple 模式:Token 风格替换 ``` java @Configuration public class SaTokenConfigure { // Sa-Token 整合 jwt (Simple 简单模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForSimple(); } } ``` Mixin 模式:混入部分逻辑 ``` java @Configuration public class SaTokenConfigure { // Sa-Token 整合 jwt (Mixin 混入模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForMixin(); } } ``` Stateless 模式:服务器完全无状态 ``` java @Configuration public class SaTokenConfigure { // Sa-Token 整合 jwt (Stateless 无状态模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForStateless(); } } ``` ### 4、开始使用 然后我们就可以像之前一样使用 Sa-Token 了 ``` java /** * 登录测试 */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 @RequestMapping("login") public SaResult login() { StpUtil.login(10001); return SaResult.ok("登录成功"); } // 查询登录状态 @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 测试注销 @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ``` 访问上述接口,观察Token生成的样式 ``` java eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEiLCJybiI6IjZYYzgySzBHVWV3Uk5NTTl1dFdjbnpFZFZHTVNYd3JOIn0.F_7fbHsFsDZmckHlGDaBuwDotZwAjZ0HB14DRujQfOQ ``` ### 5、不同模式策略对比 注入不同模式会让框架具有不同的行为策略,以下是三种模式的差异点(为方便叙述,以下比较以同时引入 jwt 与 Redis 作为前提): | 功能点 | Simple 简单模式 | Mixin 混入模式 | Stateless 无状态模式 | | :-------- | :-------- | :-------- | :-------- | | Token风格 | jwt风格 | jwt风格 | jwt风格 | | 登录数据存储 | Redis中存储 | Token中存储 | Token中存储 | | Session存储 | Redis中存储 | Redis中存储 | 无Session | | 注销下线 | 前后端双清数据 | 前后端双清数据 | 前端清除数据 | | 踢人下线API | 支持 | 不支持 | 不支持 | | 顶人下线API | 支持 | 不支持 | 不支持 | | 登录认证 | 支持 | 支持 | 支持 | | 角色认证 | 支持 | 支持 | 支持 | | 权限认证 | 支持 | 支持 | 支持 | | timeout 有效期 | 支持 | 支持 | 支持 | | active-timeout 有效期 | 支持 | 支持 | 不支持 | | id反查Token | 支持 | 支持 | 不支持 | | 会话管理 | 支持 | 部分支持 | 不支持 | | 注解鉴权 | 支持 | 支持 | 支持 | | 路由拦截鉴权 | 支持 | 支持 | 支持 | | 账号封禁 | 支持 | 支持 | 不支持 | | 身份切换 | 支持 | 支持 | 支持 | | 二级认证 | 支持 | 支持 | 支持 | | 模式总结 | Token风格替换 | jwt 与 Redis 逻辑混合 | 完全舍弃Redis,只用jwt | ### 6、扩展参数 你可以通过以下方式在登录时注入扩展参数: ``` java // 登录10001账号,并为生成的 Token 追加扩展参数name StpUtil.login(10001, new SaLoginParameter().setExtra("name", "zhangsan")); // 连缀写法追加多个 StpUtil.login(10001, new SaLoginParameter() .setExtra("name", "zhangsan") .setExtra("age", 18) .setExtra("role", "超级管理员")); // 获取扩展参数 String name = StpUtil.getExtra("name"); // 获取任意 Token 的扩展参数 String name = StpUtil.getExtra("tokenValue", "name"); ``` ### 7、在多账户模式中集成 jwt sa-token-jwt 插件默认只为 `StpUtil` 注入 `StpLogicJwtFoxXxx` 实现,自定义的 `StpUserUtil` 是不会自动注入的,我们需要帮其手动注入: ``` java /** * 为 StpUserUtil 注入 StpLogicJwt 实现 */ @PostConstruct public void setUserStpLogic() { StpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE)); } ``` ### 8、自定义 SaJwtUtil 生成 token 的算法 如果需要自定义生成 token 的算法(例如更换sign方式),直接重写 SaJwtTemplate 对象即可: ``` java /** * 自定义 SaJwtUtil 生成 token 的算法 */ @PostConstruct public void setSaJwtTemplate() { SaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() { @Override public String generateToken(JWT jwt, String keyt) { System.out.println("------ 自定义了 token 生成算法"); return super.generateToken(jwt, keyt); } }); } ``` ### 9、注意点 ##### 1、使用 jwt-simple 模式后,is-share=false 恒等于 false。 `is-share=true` 的意思是每次登录都产生一样的 token,这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的, 为保证正确设定 Extra 数据,当使用 `jwt-simple` 模式后,`is-share` 配置项 恒等于 `false`。 ##### 2、使用 jwt-mixin 模式后,is-concurrent 必须为 true。 `is-concurrent=false` 代表每次登录都把旧登录顶下线,但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中, 技术上来讲无法将其踢下线,所以此时顶人下线和踢人下线等 API 都属于不可用状态,所以此时 `is-concurrent` 配置项必须配置为 `true`。 ##### 3、使用 jwt-mixin 模式后,max-try-times 恒等于 -1。 为防止框架错误判断 token 唯一性,当使用 jwt-mixin 模式后,`max-try-times` 恒等于 -1。 ================================================ FILE: sa-token-doc/plugin/plugin-dev.md ================================================ # Sa-Token 插件开发指南 本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 --- ================================================ FILE: sa-token-doc/plugin/quick-login.md ================================================ # Sa-Token-Quick-Login 快速登录认证 --- ### 解决什么问题 Sa-Token-Quick-Login 可以为一个系统快速的、零代码 注入一个登录页面 试想一下,假如我们开发了一个非常简单的小系统,比如说:服务器性能监控页面, 我们将它部署在服务器上,通过访问这个页面,我们可以随时了解服务器性能信息,非常方便 然而,这个页面方便我们的同时,也方便了一些不法的攻击者,由于这个页面毫无防护的暴露在公网中,任何一台安装了浏览器的电脑都可以随时访问它! 为此,我们必须给这个系统加上一个登录认证,只有知晓了后台密码的人员才可以进行访问 细细想来,完成这个功能你需要: 1. 编写前端登录页面,手写各种表单样式 2. 寻找合适的ajax类库,`jQuery`?`Axios`?还是直接前后台不分离? 3. 寻找合适的模板引擎,比如`jsp`、`Thymeleaf`、`FreeMarker`、`Velocity`……选哪个呢? 4. 处理后台各种拦截认证逻辑,前后台接口对接 5. 你可能还会遇到令人头痛欲裂的模板引擎中`ContextPath`处理 6. …… 你马上就会发现,写个监控页你一下午就可以搞定,然而这个登录页你却可能需要花上两三天的时间,这是一笔及其不划算的时间浪费 那么现在你可能就会有个疑问,难道就没有什么方法给我的小项目快速增加一个登录功能吗? Sa-Token-Quick-Login便是为了解决这个问题! ### 适用场景 Sa-Token-Quick-Login 旨在用最小的成本为项目增加一个登录认证功能 - **简单**:只需要引入一个依赖便可为系统注入登录功能,快速、简单、零代码! - **不可定制**:由于登录页面不可定制,所以Sa-Token-Quick-Login非常不适合普通项目的登录认证模块,STQL也无意去解决所有项目的登录认证模块 Sa-Token-Quick-Login的定位是这样的场景:你的项目需要一个登录认证功能、这个认证页面可以不华丽、可以烂,但是一定要有,同时你又不想花费太多的时间浪费在登录页面上, 那么你便可以尝试一下`Sa-Token-Quick-Login` ### 集成步骤 首先我们需要创建一个SpringBoot的demo项目,比如:`sa-token-demo-quick-login` ##### 1、添加pom依赖 ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-quick-login ${sa.top.version} ``` ``` gradle // Sa-Token 启动依赖 implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token-Quick-Login 插件 implementation 'cn.dev33:sa-token-quick-login:${sa.top.version}' ``` ##### 2、启动类 ``` java @SpringBootApplication public class SaTokenQuickDemoApplication { public static void main(String[] args) { SpringApplication.run(SaTokenQuickDemoApplication.class, args); System.out.println("\n------ 启动成功 ------"); System.out.println("name: " + SaQuickManager.getConfig().getName()); System.out.println("pwd: " + SaQuickManager.getConfig().getPwd()); } } ``` ##### 3、新建测试Controller ``` java /** * 测试专用Controller */ @RestController public class TestController { // 浏览器访问测试: http://localhost:8081 @RequestMapping({"/", "/index"}) public String index() { String str = "
" + "

资源页 (登录后才可进入本页面)

" + "
" + "

Sa-Token " + SaTokenConsts.VERSION_NO + "

"; return str; } } ``` ### 测试访问 启动项目,使用浏览器访问:`http://localhost:8081`,首次访问时,由于处于未登录状态,会被强制进入登录页面 登录 使用默认账号:`sa / 123456`进行登录,会看到资源页面 登录 也可以通过 Http Basic 的方式直接进行认证 (一般需要在专门的 API 测试工具下才能正常测试,浏览器会自动忽略@之前的信息) ``` url http://sa:123456@localhost:8081/ ``` ### 可配置信息 你可以在yml中添加如下配置 (所有配置都是可选的) ``` yaml # Sa-Token-Quick-Login 配置 sa: # 登录账号 name: sa # 登录密码 pwd: 123456 # 是否自动随机生成账号密码 (此项为true时, name与pwd失效) auto: false # 是否开启全局认证(关闭后将不再强行拦截) auth: true # 登录页标题 title: Sa-Token 登录 # 是否显示底部版权信息 copr: true # 指定拦截路径 # include: /** # 指定排除路径 # exclude: /1.jpg ``` ``` properties ####### Sa-Token-Quick-Login 配置 ####### # 登录账号 sa.name=sa # 登录密码 sa.pwd=123456 # 是否自动随机生成账号密码 (此项为true时, name与pwd失效) sa.auto=false # 是否开启全局认证(关闭后将不再强行拦截) sa.auth=true # 登录页标题 sa.title=Sa-Token 登录 # 是否显示底部版权信息 sa.copr=true # 指定拦截路径 # sa.include=/** # 指定排除路径 # sa.exclude=/1.jpg ```
**注:**示例源码在`/sa-token-demo/sa-token-demo-quick-login`目录下,可结合源码查看学习 ### 使用独立jar包运行 使用`sa-token-quick-login`只需引入一个依赖即可为系统注入登录模块,现在我们更进一步,将这个项目打成一个独立的jar包 通过这个jar包,我们可以方便的部署任意静态网站!做到真正的零编码注入登录功能。 ##### 打包步骤 首先放上懒人链接:[sa-quick-dist.jar](https://pan.quark.cn/s/04fd34a24928),不想手动操作的同学可以直接点此链接下载打包后的jar文件 1、首先将 `sa-token-demo-quick-login` 模块添加到顶级父模块的``节点中 ``` xml sa-token-core sa-token-starter sa-token-plugin sa-token-demo\sa-token-demo-quick-login ``` 2、在项目根目录进入cmd执行打包命令 ``` cmd mvn clean package ``` 3、进入`\sa-token-demo\sa-token-demo-quick-login\target` 文件夹,找到打包好的jar文件 ``` cmd sa-token-demo-quick-login-0.0.1-SNAPSHOT.jar ``` 4、我们将其重命名为`sa-quick-dist.jar`,现在这个jar包就是我们的最终程序,我们在这个`\target`目录直接进入cmd,执行如下命令启动jar包 ``` cmd java -jar sa-quick-dist.jar ``` 5、测试访问,根据控制台输出提示,我们使用浏览器访问测试: `http://localhost:8080` sa-quick-start 如果可以进入登录界面,则代表打包运行成功
当然仅仅运行成功还不够,下面我们演示一下如何使用这个jar包进行静态网站部署 ### 所有功能示例 ##### Case 1. 指定静态资源路径 ``` cmd java -jar sa-quick-dist.jar --sa.dir file:E:\www ``` 使用dir参数指定`E:\www`目录作为资源目录进行部署 (现在我们可以通过浏览器访问`E:\www`目录下的文件了!) ##### Case 2. 指定登录名与密码 ``` cmd java -jar sa-quick-dist.jar --sa.name=zhang --sa.pwd=zhang123 ``` 现在,默认的账号`sa/123456`将被废弃,而是使用`zhang/zhang123`进行账号校验 ##### Case 3. 指定其自动生成账号密码 ``` cmd java -jar sa-quick-dist.jar --sa.auto=true ``` 每次启动时随机生成账号密码(会在启动成功时打印到控制台上) ##### Case 4. 指定登录页的标题 ``` cmd java -jar sa-quick-dist.jar --sa.title="XXX 系统登录" ``` ##### Case 5. 关闭账号校验,仅作为静态资源部署使用 ``` cmd java -jar sa-quick-dist.jar --sa.auth=false ``` ##### Case 6. 指定启动端口(默认8080) ``` cmd java -jar sa-quick-dist.jar --server.port=80 ``` 注:所有参数可组合使用 ### 使用SpringBoot默认资源路径 SpringBoot默认开放了一些路径作为资源目录,比如`classpath:/static/`, 怎么使用呢?我们只需要在jar包同目录创建一个`\static`文件夹,将静态资源文件复制到此目录下,然后启动jar包即可访问 同时,我们还可以在jar包同目录创建yml配置文件,来覆盖jar包内的yml配置,如下图所示: sa-quick-case.png 例如如上目录中`/static`中有一个`1.jpg`文件,我们启动jar包后访问`http://localhost:8080/1.jpg`即可查看到此文件,这是Springboot自带的功能,在此不再赘述 ================================================ FILE: sa-token-doc/plugin/spel-at.md ================================================ # SpEL 表达式注解鉴权 Sa-Token 提供一个 `@SaCheckEL` 鉴权注解,该注解允许你使用 SpEL 表达式进行鉴权。 ### 1、引入插件 由于该注解的工作底层需要依赖 SpringAOP 切面编程,因此你需要单独引入插件包 `sa-token-spring-el` 才可以使用此注解。 ``` xml cn.dev33 sa-token-spring-el ${sa.top.version} ``` ``` gradle // Sa-Token 注解鉴权使用 EL 表达式 implementation 'cn.dev33:sa-token-spring-el:${sa.top.version}' ``` ### 2、简单示例 以下是一些使用示例: ``` java @RestController @RequestMapping("/check-el/") public class SaCheckELController { // 登录校验 @SaCheckEL("stp.checkLogin()") @RequestMapping("test1") public SaResult test1() { return SaResult.ok(); } // 权限校验 @SaCheckEL("stp.checkPermission('user:edit')") @RequestMapping("test3") public SaResult test3() { return SaResult.ok(); } // 参数长度校验 @SaCheckEL("NEED( #name.length() > 3 )") @RequestMapping("test5") public SaResult test5(@RequestParam(defaultValue = "") String name) { return SaResult.ok().set("name", name); } // SaSession 里取值校验 @SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )") @RequestMapping("test8") public SaResult test8() { return SaResult.ok(); } } ``` ### 3、多账号体系鉴权 要在 EL 表达式中使用多账号体系鉴权模式,你需要在配置类中重写 `SaCheckELRootMap 扩展函数`,增加 EL 表达式可使用的根对象: ``` java @Configuration public class SaTokenConfigure { /** * 重写 Sa-Token 框架内部算法策略 */ @PostConstruct public void rewriteSaStrategy() { // 重写 SaCheckELRootMap 扩展函数,增加注解鉴权 EL 表达式可使用的根对象 SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> { System.out.println("--------- 执行 SaCheckELRootMap 增强,目前已包含的的跟对象包括:" + rootMap.keySet()); // 新增 stpUser 根对象,使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权 rootMap.put("stpUser", StpUserUtil.getStpLogic()); }; } } ``` 然后就可以使用多账号体系鉴权模式了 ``` java // 多账号体系鉴权测试 @SaCheckEL("stpUser.checkLogin()") @RequestMapping("test9") public SaResult test9() { return SaResult.ok(); } ``` ### 4、调用本类成员变量 ``` java // 本模块需要鉴权的权限码 public String permissionCode = "article:add"; // 调用本类的成员变量 @SaCheckEL("stp.checkPermission( this.permissionCode )") @RequestMapping("test10") public SaResult test10() { return SaResult.ok(); } ``` ### 5、忽略鉴权 配合 `@SaIgnore` 注解做到忽略某接口的鉴权 ``` java // 忽略鉴权测试 @SaIgnore @SaCheckEL("stp.checkPermission( 'abc' )") @RequestMapping("test11") public SaResult test11() { return SaResult.ok(); } ``` ### 6、代码提示 如果在书写 SpEL 表达式时需要代码提示: sa-check-el-code-tips.png 可以在 idea 中安装 **SpEL Assistant** 插件,该插件由 `@ly-chn` 提供,允许为自定义注解书写 SpEL 表达式时增加代码提示功能, 开源地址:[https://github.com/ly-chn/SpEL-Assistant](https://github.com/ly-chn/SpEL-Assistant) 安装方式:直接在 idea 插件商店中搜索 “**SpEL Assistant**” 即可 sa-check-el-code-tips.png 本章代码示例:Sa-Token SpEL表达式注解鉴权 —— [ SaCheckELController.java ] ================================================ FILE: sa-token-doc/plugin/temp-token.md ================================================ # 临时 Token 令牌认证 --- ### 1、适用场景 在部分业务场景,我们需要一种临时授权的能力,即:一个token的有效期并不需要像登录有效期那样需要[七天、三十天],而是仅仅需要 [五分钟、半小时]。 举个比较明显的例子:超链接邀请机制。 > [!NOTE| label:业务场景] > > 你在一个游戏中创建一个公会 `(id=10014)`,现在你想邀请你的好朋友加入这个公会,在你点击 **`[邀请]`** 按钮时,系统为你生成一个连接: > > ``` xml > http://xxx.com/apply?id=10014 > ``` > > 接着,你的好朋友点击这个链接,加入了你的工会。 > > 那么,系统是如何识别这个链接对应的工会是10014呢?很明显,我们可以观察出,这个链接的尾部有个id参数值为10014,这便是系统识别的关键。 > > 此时你可能眉头一紧,就这么简单?那我如果手动更改一下尾部的参数改成10015,然后我再一点,岂不是就可以偷偷加入别人的工会了? > > 你想的没错,如果这个游戏的架构设计者采用上述方案完成功能的话,这个邀请机制就轻松的被你攻破了。 > > 但是很明显,正常的商业项目一般不会拉跨到这种地步,比较常见的方案是,对这个公会id做一个token映射,最终你看到链接一般是这样的: > > ``` xml > http://xxx.com/apply?token=oEwQBnglXDoGraSJdGaLooPZnGrk > ``` > > 后面那一串字母是乱打出来的,目的是为了突出它的随机性,即:使用一个随机的token来代替明文显示真正的数据。 > > 在用户点击这个链接之后,服务器便可根据这个token解析出真正公会id (10014) ,至于伪造?全是随机的你怎么伪造?你又不知道10015会随机出一个什么样的Token 。 > > 而且为了安全性,这个token的有效期一般不会太长,给你预留五分钟、半小时的时间足够你点击它即可。 ### 2、创建临时 token **[sa-token-temp 临时 token 认证模块]** 已内嵌到核心包,无需引入其它依赖即可使用: ``` java // 根据 value 创建一个 token String token = SaTempUtil.createToken("10014", 200); // 解析 token 获取 value,并转换为指定类型 String value = SaTempUtil.parseToken(token, String.class); // 获取指定 token 的剩余有效期,单位:秒 SaTempUtil.getTimeout(token); // 删除指定 token SaTempUtil.deleteToken(token); ``` ### 3、前缀拼接与裁剪 ``` java // 如果由多条业务线都需要生成临时 token,可以加个前缀进行区分 String token = SaTempUtil.createToken("shop_1001", 1200); ``` 在获取时可以自行裁剪前缀,也可以调用: ``` java // 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 SaTempUtil.parseToken(token, "shop_", Long.class) ``` 如果指定了错误的前缀,即使 token 正确,上述方法也将返回 null ### 4、根据 value 反查 token 在创建 token 时,框架默认只会保存 `token -> value` 的映射,而不会记录 `value -> token` 的索引信息。 如果想要做到反查 token,则必须在创建 token 指定框架记录 token 索引信息: ``` java // 在创建 token 时,指定第三个参数 true,即可让框架在保存 token 时同时记录 token 索引信息 String token1 = SaTempUtil.createToken(10004, 1200, true); String token2 = SaTempUtil.createToken(10004, 1300, true); String token3 = SaTempUtil.createToken(10004, -1, true); // 获取 10004 对应的所有 token List list = SaTempUtil.getTempTokenList(10004); System.out.println(list); ``` ### 5、集成jwt 提到 [临时Token认证],你是不是想到一个专门干这件事的框架?对,就是JWT! **[sa-token-temp]** 模块允许以JWT作为逻辑内核完成工作,你只需要引入以下依赖,所有上层API保持不变 ``` xml cn.dev33 sa-token-temp-jwt ${sa.top.version} ``` ``` gradle implementation 'cn.dev33:sa-token-temp-jwt:${sa.top.version}' ``` 并在配置文件中配置上jwt秘钥 **`(必填!)`** ``` yml sa-token: # sa-token-temp-jwt 模块的秘钥 (随便乱摁几个字母就行了) jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk ``` ================================================ FILE: sa-token-doc/plugin/thymeleaf-extend.md ================================================ # Thymeleaf 标签方言 本插件的作用是让我们可以在 Thymeleaf 页面中使用 Sa-Token 相关API,俗称 —— 标签方言。 --- ### 1、引入依赖 首先我们确保项目已经引入 Thymeleaf 依赖,然后在此基础上继续添加: ``` xml cn.dev33 sa-token-thymeleaf ${sa.top.version} ``` ``` gradle // 在 thymeleaf 标签中使用 Sa-Token implementation 'cn.dev33:sa-token-thymeleaf:${sa.top.version}' ``` ### 2、注册标签方言对象 在 SaTokenConfigure 配置类中注册 Bean ``` java @Configuration public class SaTokenConfigure { // Sa-Token 标签方言 (Thymeleaf版) @Bean public SaTokenDialect getSaTokenDialect() { return new SaTokenDialect(); } } ``` ### 3、使用标签方言 然后我们就可以愉快的使用在 Thymeleaf 页面中使用标签方言了 ##### 3.1、登录判断 ``` html

标签方言测试页面

登录之后才能显示: value

不登录才能显示: value

``` ##### 3.2、角色判断 ``` html

具有角色 admin 才能显示: value

同时具备多个角色才能显示: value

只要具有其中一个角色就能显示: value

不具有角色 admin 才能显示: value

``` ##### 3.3、权限判断 ``` html

具有权限 user-add 才能显示: value

同时具备多个权限才能显示: value

只要具有其中一个权限就能显示: value

不具有权限 user-add 才能显示: value

``` ### 4、调用 Sa-Token 相关API 以上的标签方言,可以满足我们大多数场景下的权限判断,然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API 首先在 SaTokenConfigure 配置类中为 Thymeleaf 配置全局对象: ``` java @Configuration public class SaTokenConfigure{ // ... 其它代码 // 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法 @Autowired private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { viewResolver.addStaticVariable("stp", StpUtil.stpLogic); } } ``` > [!WARNING| label:注意] > 如果`SaTokenConfigure`继承了`WebMvcConfigurer`等类,可能会造成循环依赖,如果遇到,请新建一个其他配置类完成此项配置 然后我们就可以在页面上调用 StpLogic 的 API 了,例如: ``` html

调用 StpLogic 方法调用测试

从SaSession中取值:

``` ### 5、代码提示 如果想在写标签属性时增加代码提示: thymeleaf-code-tips.png 只需在头部声明增加上对应的命名空间即可: ``` html ``` ================================================ FILE: sa-token-doc/pro/st_doc_top.md ================================================ # Sa-Max 统一认证商业版 ### 项目介绍 根据 SSO / OAuth2 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。 然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此我们特意准备了项目: [[ Sa-Max 统一认证商业版 ]](https://sa-pro.yun94.cn?way=st_doc_top)。 项目基于 Sa-Token 搭建,集成了统一认证常见技术点,可大大缩短您的项目接入统一授权认证的开发周期: - 支持:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 - 支持:API Key、OAuth2.0 统一认证能力, 并提供开放平台,供第三方公司申请应用对接平台能力。 全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 ### 释疑 ##### 1、Sa-Max 是收费项目吗?与 Sa-Token 有什么不同? `Sa-Max` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 `Sa-Max` 与 `Sa-Token` 的区别,简单来讲: - `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 - `Sa-Max` 是一个完整项目,下载源码后可直接启动 ##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? 首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, 比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 Sa-Max 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 另外:即使您没有购买 `Sa-Max`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 ##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! 答案是不会。 再次强调一下:`Sa-Token` 与 `Sa-Max` 是两个独立的项目,两者互不影响。 付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 ================================================ FILE: sa-token-doc/pro/st_index_top.md ================================================ # Sa-Max 统一认证商业版 ### 项目介绍 根据 SSO / OAuth2 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。 然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此我们特意准备了项目: [[ Sa-Max 统一认证商业版 ]](https://sa-pro.yun94.cn?way=st_index_top)。 项目基于 Sa-Token 搭建,集成了统一认证常见技术点,可大大缩短您的项目接入统一授权认证的开发周期: - 支持:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 - 支持:API Key、OAuth2.0 统一认证能力, 并提供开放平台,供第三方公司申请应用对接平台能力。 全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 ### 释疑 ##### 1、Sa-Max 是收费项目吗?与 Sa-Token 有什么不同? `Sa-Max` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 `Sa-Max` 与 `Sa-Token` 的区别,简单来讲: - `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 - `Sa-Max` 是一个完整项目,下载源码后可直接启动 ##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? 首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, 比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 Sa-Max 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 另外:即使您没有购买 `Sa-Max`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 ##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! 答案是不会。 再次强调一下:`Sa-Token` 与 `Sa-Max` 是两个独立的项目,两者互不影响。 付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 ================================================ FILE: sa-token-doc/pro/st_oauth2.md ================================================ # Sa-Max 统一认证商业版 ### 项目介绍 根据 OAuth2 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个 OAuth2 模式的认证 Demo 示例。 然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此我们特意准备了项目: [[ Sa-Max 统一认证商业版 ]](https://sa-pro.yun94.cn?way=st_oauth2)。 项目基于 Sa-Token 搭建,集成了统一认证常见技术点,可大大缩短您的项目接入统一授权认证的开发周期: - 支持:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 - 支持:API Key、OAuth2.0 统一认证能力, 并提供开放平台,供第三方公司申请应用对接平台能力。 全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 ### 释疑 ##### 1、Sa-Max 是收费项目吗?与 Sa-Token 有什么不同? `Sa-Max` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 `Sa-Max` 与 `Sa-Token` 的区别,简单来讲: - `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 - `Sa-Max` 是一个完整项目,下载源码后可直接启动 ##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? 首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, 比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 Sa-Max 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 另外:即使您没有购买 `Sa-Max`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 ##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! 答案是不会。 再次强调一下:`Sa-Token` 与 `Sa-Max` 是两个独立的项目,两者互不影响。 付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 ================================================ FILE: sa-token-doc/pro/st_sso.md ================================================ # Sa-Pro 单点登录商业版 ### 项目介绍 根据 SSO 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个SSO模式的认证 Demo 示例。 然而,要真正开发一个商业级项目的统一认证中心系统,绝非一朝一夕可以搭建完毕,为此,我们特意准备了项目: [[ Sa-Pro 单点登录商业版 ]](https://sa-pro.yun94.cn?way=st_sso)。 项目基于 Sa-Token 搭建,集成了单点登录常见技术点,可解决: 同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 可大大缩短您的项目接入单点登录的开发周期。 全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。 ### 释疑 ##### 1、Sa-Pro 是收费项目吗?与 Sa-Token 有什么不同? `Sa-Pro` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 `Sa-Pro` 与 `Sa-Token` 的区别,简单来讲: - `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 - `Sa-Pro` 是一个完整项目,下载源码后可直接启动 ##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? 首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, 比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 Sa-Pro 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 另外:即使您没有购买 `Sa-Pro`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 ##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Pro 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! 答案是不会。 再次强调一下:`Sa-Token` 与 `Sa-Pro` 是两个独立的项目,两者互不影响。 付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 ================================================ FILE: sa-token-doc/sso/anon-client.md ================================================ # 匿名 Client 接入 匿名 Client 就是指在客户端没有配置 `sso-client` 的应用,没有一个明确的 “Client” 标识名称。 匿名 Client 在一些关键步骤中不会构建 `client` 参数,如:“重定向至认证中心授权地址”、“校验 ticket”、“单点注销” 等。 要想匿名 client 接入,你需要做一些特殊配置。 ### 1、在 sso-server 端开启匿名 client 接入 开启方式一,通过配置项方式: ``` yaml # Sa-Token 配置 sa-token: # SSO-Server 配置 sso-server: # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) allow-anon-client: true # 所有允许的授权回调地址 (匿名 client 使用) allow-url: "*" # API 接口调用秘钥 (全局默认 + 匿名 client 使用) secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` ``` properties # SSO-Server 配置 # 是否启用匿名 client (开启匿名 client 后,允许客户端接入时不提交 client 参数) sa-token.sso-server.allow-anon-client=true # 所有允许的授权回调地址 (匿名 client 使用) sa-token.sso-server.allow-url=* # API 接口调用秘钥 (全局默认 + 匿名 client 使用) sa-token.sso-server.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` 开启方式二,通过代码重写方式: ``` java /** * 重写 SaSsoServerTemplate 部分方法,增强功能 */ @Component public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { /** * 获取配置项:是否允许匿名 client 接入 */ @Override public boolean getConfigOfAllowAnonClient() { return true; } /** * 获取匿名 client 配置信息 */ @Override public SaSsoClientModel getAnonClient() { SaSsoClientModel scm = new SaSsoClientModel(); scm.setAllowUrl("*"); // 允许的授权地址 scm.setIsSlo(true); // 是否允许单点注销 scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); // 客户端密钥 return scm; } } ``` ### 2、在 sso-client 端不要配置 client 字段 然后在对应的应用端不要配置 client 字段,例如: ``` yml # sa-token配置 sa-token: # 配置一个不同的 token-name,以避免在和模式三 demo 一起测试时发生数据覆盖 token-name: satoken-client-anon # sso-client 相关配置 sso-client: # client 标识 匿名应用就是指不配置 client 标识的应用 # client: sso-client3 # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 使用 Http 请求校验ticket (模式三) is-http: true # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个) reg-logout-call: true # API 接口调用秘钥 secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` > [!TIP| label:demo] > 匿名 Client 接入的 Demo 示例地址:[sa-token-demo-sso3-client-anon](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon) 这里有个值得注意的配置项:`reg-logout-call: true`,是干嘛的? 简单来讲,就是匿名应用不包含 client 字段信息,因此 sso-server 端也无法配置此 client 的消息推送地址,所以此 client 无法接受到消息推送,也就无法参与到单点注销的环路中来。 因此,新增一个配置项 `reg-logout-call: true`,代表在登录的同时把当前项目的单点注销回调地址 `/sso/logoutCall` 发送到 sso-server 端, 这样 sso-server 端有了备案,也就可以成功通知此应用发起单点注销掉了。 如果当前应用不需要单点注销可以不配置此字段。 ================================================ FILE: sa-token-doc/sso/message-push.md ================================================ # 消息推送机制 消息推送机制简单来讲就是:sso-client 端按照特点格式构建一个 http 请求,调用 sso-server 端的 `/sso/pushS` 接口,sso-server 接收到消息后做出处理回应 sso-client 端。 消息推送是相互的,sso-server 端也可以构建 http 请求,调用 sso-client 端的 `/sso/pushC` 接口。 消息推送机制是应用与认证中心相互沟通的桥梁,ticket 校验、单点注销等行为都是依赖消息推送机制来实现的。 本篇将介绍在 Sa-Token SSO 模块中,sso-server 端和 sso-client 端分别内置了哪些消息模块,以及如何自定义消息处理器。 ### 1、sso-server 端内置消息处理器 #### 1.1、checkTicket(ticket 校验) 作用:在 SSO 模式三下为 sso-client 提供 ticket 校验能力,返回 loginId 等数据 ``` url http://{sso-server主机地址}/sso/pushS ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | msgType | 是 | 消息类型,此处填 `checkTicket` | | ticket | 是 | ticket 码 | | client | 否 | 客户端标识,可不填,代表是一个匿名应用 | | ssoLogoutCall | 否 | Client 端单点注销时 - 回调 URL 参数名称 (匿名 Client 时使用) | | timestamp | 是 | 当前时间戳,13位 | | nonce | 是 | 随机字符串 | | sign | 是 | 签名,生成算法示例:`md5( client={client值}&msgType={checkTicket}&nonce={随机字符串}&ticket={ticket码}×tamp={13位时间戳}&key={secretkey秘钥} )` | **签名算法规则:将所有参数按照字典顺序依次排列(key除外,挂在最后面),然后进行 md5 摘要。以下不再赘述。** 返回值示例: ``` js { "code": 200, // 返回 200=成功,500=失败 "msg": "ok", "data": "10001", "loginId": "10001", // 此 ticket 对应的认证中心 loginId "tokenValue": "5db12b02-9c8e-4e36-8ed9-bf295caed80e", // 对应的认证中心会话 token "deviceId": "MxOTCLWi5NXGqFQZBFdsH66Ni5YTJ8q0", // 对应的认证中心登录设备 id "remainTokenTimeout": 2591999, // token 剩余有效期 "remainSessionTimeout": 2591999 // Access-Session 会话剩余有效期 } ``` #### 1.2、signout(单点注销) 作用:为 sso-client 提供单点注销能力 ``` url http://{sso-server主机地址}/sso/pushS ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | msgType | 是 | 消息类型,此处填 `signout` | | loginId | 是 | 账号id | | client | 否 | 客户端标识,可不填,代表是一个匿名应用 | | deviceId | 否 | 客户端设备 id | | timestamp | 是 | 当前时间戳,13位 | | nonce | 是 | 随机字符串 | | sign | 是 | 签名,生成算法示例:`md5( client={client值}&deviceId={设备id}&msgType={signout}&nonce={随机字符串}&loginId={loginId}×tamp={13位时间戳}&key={secretkey秘钥} )` | 返回值示例: ``` js { "code": 200, // 返回 200=成功,500=失败 "msg": "ok", "data": null } ``` ### 2、sso-client 端内置消息处理器 #### 2.1、logoutCall(单点注销回调) 作用:接收来自 sso-server 的单点注销回调通知 ``` url http://{sso-client主机地址}/sso/pushC ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | msgType | 是 | 消息类型,此处填 `logoutCall` | | loginId | 是 | 账号id | | deviceId | 否 | 客户端设备 id | | timestamp | 是 | 当前时间戳,13位 | | nonce | 是 | 随机字符串 | | sign | 是 | 签名,生成算法示例:`md5( deviceId={设备id}&msgType={logoutCall}&nonce={随机字符串}&loginId={loginId}×tamp={13位时间戳}&key={secretkey秘钥} )` | 返回值示例: ``` js { "code": 200, // 返回 200=成功,500=失败 "msg": "单点注销回调成功", "data": null } ``` ### 3、认证中心自定义消息处理器 当然你也可以通过自定义消息处理器的方式,来扩展消息推送能力,这将非常有助于你完成一些应用与认证中心的自定义数据交互。 假设我们现在有如下需求:在 sso-client 获取 sso-server 端指定账号 id 的昵称、头像等信息,即:用户资料的拉取。 首先,我们需要在 sso-server 实现一个消息处理器: ``` java @RestController public class SsoServerController { // 配置SSO相关参数 @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 添加消息处理器:userinfo (获取用户资料) (用于为 client 端开放拉取数据的接口) ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> { System.out.println("收到消息:" + message); // 自定义返回结果(模拟) return SaResult.ok() .set("id", message.get("loginId")) .set("name", "LinXiaoYu") .set("sex", "女") .set("age", 18); }); } } ``` ### 4、应用端调用消息推送接口获取数据 首先保证在配置文件里要配置上消息推送的具体地址 ``` yaml # sa-token配置 sa-token: # sso-client 相关配置 sso-client: # 应用标识 client: sso-client3 # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # sso-server 端推送消息地址 # 配置 server-url 后,框架可自动计算对应的 push-url 地址,也可以单独配置 push-url 地址,两者选其一即可 # push-url: http://sa-sso-server.com:9000/sso/pushS # API 接口调用秘钥 secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` ``` properties # sso-client 相关配置 # 应用标识 sa-token.sso-client.client=sso-client3 # sso-server 端主机地址 sa-token.sso-client.server-url=http://sa-sso-server.com:9000 # sso-server 端推送消息地址 # 配置 server-url 后,框架可自动计算对应的 push-url 地址,也可以单独配置 push-url 地址,两者选其一即可 sa-token.sso-client.push-url=http://sa-sso-server.com:9000/sso/pushS # API 接口调用秘钥 sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` 然后在需要拉取资料的地方: ``` java // 查询我的账号信息:sso-client 前端 -> sso-client 后端 -> sso-server 后端 @RequestMapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 获取本地 loginId Object loginId = StpUtil.getLoginId(); // 构建消息对象 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", loginId); // 推送至 sso-server,并接收响应数据 SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } ``` ================================================ FILE: sa-token-doc/sso/readme.md ================================================ # Sa-Token-SSO 单点登录模块

观看 SSO 模块视频讲解(B站:王清江唷)

凡是稍微上点规模的系统,统一认证中心都是绕不过去的槛。而单点登录——便是我们搭建统一认证中心的关键。 --- ### 什么是单点登录?解决什么问题? 举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。 单点登录——就是为了解决这个问题而生! 简而言之,单点登录可以做到: **`在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。`** ### 架构选型 Sa-Token-SSO 由简入难划分为三种模式,解决不同架构下的 SSO 接入问题: | 系统架构 | 采用模式 | 简介 | 文档链接 | | :-------- | :-------- | :-------- | :-------- | | 前端同域 + 后端同 Redis | 模式一 | 共享 Cookie 同步会话 | [文档](/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client) | | 前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | [文档](/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client) | | 前端不同域 + 后端不同 Redis | 模式三 | Http请求获取会话 | [文档](/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client) | 1. 前端同域:就是指多个系统可以部署在同一个主域名之下,比如:`c1.domain.com`、`c2.domain.com`、`c3.domain.com`。 2. 后端同Redis:就是指多个系统可以连接同一个Redis。PS:这里并不需要把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案,详情戳: Alone独立Redis插件。 3. 如果既无法做到前端同域,也无法做到后端同Redis,那么只能走模式三,Http请求获取会话(Sa-Token对SSO提供了完整的封装,你只需要按照示例从文档上复制几段代码便可以轻松集成)。 sa-token-jss ### Sa-Token-SSO 特性 1. API 简单易用,文档介绍详细,且提供直接可用的集成示例。 2. 支持三种模式,不论是否跨域、是否共享Redis、是否前后端分离,都可以完美解决。 3. 安全性高:内置域名校验、Ticket校验、秘钥校验等,杜绝`Ticket劫持`、`Token窃取`等常见攻击手段(文档讲述攻击原理和防御手段)。 4. 不丢参数:笔者曾试验多个单点登录框架,均有参数丢失的情况,比如重定向之前是:`http://a.com?id=1&name=2`,登录成功之后就变成了:`http://a.com?id=1`,Sa-Token-SSO内有专门的算法保证了参数不丢失,登录成功之后原路返回页面。 5. 无缝集成:由于Sa-Token本身就是一个权限认证框架,因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题,让你不再到处搜索:xxx单点登录与xxx权限认证如何整合…… 6. 高可定制:Sa-Token-SSO模块对代码架构侵入性极低,结合Sa-Token本身的路由拦截特性,你可以非常轻松的定制化开发。 ### 学习注意点 1. sa-token-sso 虽然是个单独的插件,但其本质仍是对 Sa-Token 本身各个功能的组合使用,所以先熟练掌握 Sa-Token 可有效降低 SSO 章节的学习压力。 2. 相比单体系统的会话管理,SSO 登录与注销的整体链路较长,出现 bug 时调试步骤也更复杂,因此建议先通过 demo 打通各个技术细节,再正式集成到项目中。 3. 文档对 跨Redis、跨域、前后端分离 等常见场景提供直接可用的示例,但真实项目往往是多种特殊场景交叉组合存在,每个项目各不相同。 所以文档无法依次列出所有技术点交叉组合的 demo 示例,文档会更注重解释清楚每一种特殊场景的特殊点所在,以及其解决原理, 所以推荐大家细心阅读相关段落,以便在真实项目中可以做到灵活组合、举一反三。 ================================================ FILE: sa-token-doc/sso/signout.md ================================================ # 单点注销 Sa-Token SSO 提供多种注销模式: 从注销范围上可以分为: - 单端注销:会话只在当前应用注销,其它应用和认证中心不受影响。 - 全端注销:一处注销,全端下线。也即:单点注销。 - 单浏览器注销:该账号的只在当前浏览器登录的应用注销,其它浏览器/设备不受影响。 从注销方式上可以分为: - ajax 无刷单点注销:调用指定的 RestAPI 接口完成注销。 - 跳页面注销:跳转到指定接口进行注销,注销完成后原路返回或跳转到指定页面。 --- ### 1、单端注销 在后端添加接口: ``` java // 当前应用独自注销 (不退出其它应用) @RequestMapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } ``` 在前端或跳转或 ajax 异步调用此接口即可。 如果是跳转可指定 back 参数,代表注销成功后跳转的地址,例如:`http://sso-client.com/sso/logoutByAlone?back=https://sa-token.cc` ### 2、全端注销 此处先简单看一下 Sa-Token SSO 的单点注销链路过程: 1. sso-client 的前端向 sso-client 的后端发起单点注销请求。(调用 `http://{sso-client}/sso/logout`) 2. sso-client 的后端向 sso-server 的后端发送单点注销请求。(调用 `http://{sso-server}/sso/pushS?msgType=signout`) 3. sso-server 端遍历 client 列表,逐个推送消息通知 sso-client 端下线。(`http://{sso-client}/sso/pushC?msgType=logoutCall`) 4. sso-server 端注销下线。 5. sso-server 后端响应 sso-client 后端:注销完成。 6. sso-client 后端响应 sso-client 前端:注销完成。 7. 整体完成。 这些逻辑 Sa-Token 内部已经封装完毕,你只需按照文档步骤集成即可。以模式三 demo 为例: #### 2.1、更改注销方案 单点注销是 Sa-Token SSO 内部已封装的接口,无需手动再添加,只需要在前端调用即可。 ``` java // SSO-Client端:首页 @RequestMapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "全端注销 " + "

"; return str; } ``` 重点在第 9 行。 #### 2.2、启动测试 重启项目,依次登录三个 client: - [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/) - [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/) - [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/) sso-type3-client-index.png 在任意一个 client 里,点击 **`[注销]`** 按钮,即可单点注销成功(打开另外两个client,刷新一下页面,登录态丢失)。 sso-type3-slo-index.png PS:这里我们为了方便演示,使用的是超链接跳页面的形式,正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。 例如,我们使用 [Apifox 接口测试工具](https://www.apifox.cn/) 可以做到同样的效果: sso-slo-apifox.png ### 3、单浏览器注销 单浏览器注销的前提是在登录时按照 `deviceId` 设备ID 参数为登录进行分组,这样在发起注销时即可格局设备ID参数做到单浏览器注销功能。 #### 3.1、sso-server 端加上设备ID参数登录 首先在 sso-server 的登录方法内,加上 deviceId 参数,例如: ``` java @RestController public class SsoServerController { // 其它代码,非重点,省略展示... // 配置SSO相关参数 @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:登录处理函数 ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { // 此处仅做模拟登录,真实环境应该查询数据库进行登录 if("sa".equals(name) && "123456".equals(pwd)) { String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32)); StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId)); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }; } } ``` 如上代码,在登录时获取前端提交的 deviceId 参数,如果前端没有提交则随机生成一个。 #### 3.2、sso-client 端发起注销时指定单设备注销参数 然后在 sso-client 发起单点注销时,加上 `singleDeviceIdLogout=true` 参数,代表按照设备 id 进行分组注销,非本设备id的会话不参与注销行为: ``` java // SSO-Client端:首页 @RequestMapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式三)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "单浏览器注销 - " + "全端注销 " + "

"; return str; } ``` 重点在第 9 行。 > [!WARNING| label:测试注意点] > 在进行测试时,同时将一个浏览器双击打开两次,是不算 “不同浏览器” 的,虽然你打开了两个浏览器窗口,但是这两个浏览器的会话数据是互通的。 > > 必须打开两个不同的浏览器来测试,或者按快捷键 `ctrl + shift + N` 打开隐私模式,才可以做到会话相互隔离。 ================================================ FILE: sa-token-doc/sso/sso-apidoc.md ================================================ # SSO-Server 认证中心开放接口 --- ## 一、对接方式说明 在前一章节,我们成功搭建了 SSO-Server 端。SSO-Server 可以为各个子系统提供中央认证服务。 在默认代码架构下,SSO-Server 将提供一套标准 HTTP 接口对外开放。要对接 SSO-Server,有两种方式: - SDK 方式:在你的 SSO-Client 端,也引入 Sa-Token SSO 框架,通过框架提供的 API 方法完成对接。 - NoSDK 方式:在你的 SSO-Client 端,不引入 Sa-Token SSO 框架,通过工具库调用 Http 接口的方式完成对接。 如果你的 SSO-Client 端是 java 项目,且支持引入 sa-token-sso 框架,我们强烈推荐你使用 SDK 方式对接。**你可以直接跳过本章**,开始 [SSO模式一 共享Cookie同步会话](/sso/sso-type1) 的学习。 如果你的 SSO-Client 端是 非java 项目,或不支持引入 sa-token-sso 框架,那么你可以使用 NoSDK 方式进行对接。 在之后的 [SSO整合 - NoSdk 模式对接](/sso/sso-nosdk) 章节我们会详细介绍对接步骤,下面的 API 文档将给你的对接步骤做一份参考。 ## 二、STS 协议 Sa-Token SSO 模块的开放接口标准为 STS 协议。 STS 协议并非一套公共授权协议,而是 sa-token-sso 框架本身内化、抽离出的一套授权协议标准。 - 一般的公共协议遵循的路线是:发现需求 -> 先为解决方案定义一套协议 -> 再进行框架实现。 - 而 Sa-Token SSO 的路线为:发现需求 -> 先进行框架实现 -> 再基于框架实现定义一套协议,将解决方案进行标准化。 定义 STS 协议将有助于让 Sa-Token SSO 模块进行标准化,也为日后实现多语言 SDK 提供基础支持。 目前 STS 协议规定了应用进行 单点登录、单点注销、消息推送 等动作的标准流程。 可解决:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、Sa-Token 项目、非 Sa-Token 项目、java 项目、非 java 项目 等架构下的 SSO 认证需求。 下面两节将介绍 STS 协议在 SSO-Server 端和 SSO-Client 端开放的接口标准。 ## 三、SSO-Server 认证中心接口 ### 1、单点登录授权地址 ``` url http://{host}:{port}/sso/auth ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | redirect | 否 | 登录成功后的重定向地址,一般填写 location.href(从哪来回哪去),如不填,则跳转至 home-route | | mode | 否 | 授权模式,取值 [simple, ticket],simple=登录后直接重定向,ticket=带着ticket参数重定向,默认值为ticket | | client | 否 | 客户端标识,可不填,代表是一个匿名应用,若填写了,则校验 ticket 时也必须是这个 client 才可以校验成功 | 访问接口后有两种情况: - 情况一:当前会话在 SSO 认证中心未登录,会进入登录页开始登录。 - 情况二:当前会话在 SSO 认证中心已登录,会被重定向至 `redirect` 地址,并携带 `ticket` 参数。 Ticket 码具有以下特点: 1. 每次授权产生的 `ticket` 码都不一样。 2. `ticket` 码用完即废,不能二次使用。 3. 一个 `ticket` 的有效期默认为五分钟,超时自动作废。 4. 每次授权产生新 `ticket` 码,会导致旧 `ticket` 码立即作废,即使旧 `ticket` 码尚未使用。 ### 2、RestAPI 登录接口 ``` url http://{host}:{port}/sso/doLogin ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | name | 是 | 用户名 | | pwd | 是 | 密码 | 此接口属于 RestAPI (使用ajax访问),会进入后端配置的 `ssoServerTemplate.strategy.doLoginHandle` 函数中,此函数的返回值即是此接口的响应值。 另外需要注意:此接口并非只能携带 name、pwd 参数,因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。 ### 3、单点注销接口 ``` url http://{host}:{port}/sso/signout ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | back | 否 | 注销成功后的重定向地址,一般填写 location.href(从哪来回哪去),也可以填写 self 字符串,含义同上 | ### 4、消息推送接口 ``` url http://{host}:{port}/sso/pushS ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | client | 否 | 客户端标识,可不填,代表是一个匿名应用 | | timestamp | 是 | 当前时间戳,13位 | | msgType | 是 | 消息类型 | | nonce | 是 | 随机字符串 | | sign | 是 | 签名,生成算法:`md5( client={client值}&msgType={消息类型}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | 此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。 返回值示例: - 推送成功时: ``` js { "code": 200, "msg": "ok", "data": "10001", // 返回的数据 } ``` - 推送失败时: ``` js { "code": 500, // 200表示请求成功,非200标识请求失败 "msg": "签名无效:xxx", // 失败原因 "data": null } ``` - 也有可能消息推送成功了,但是处理消息失败,例如校验 ticket 时: ``` js { "code": 500, "msg": "无效ticket:vESj0MtqrtSoucz4DDHJnsqU3u7AKFzbj0KH57EfJvuhkX1uAH23DuNrMYSjTnEq", "data": null } ``` 详细可参考:[消息推送机制](/sso/message-push)
--- ## 四、SSO-Client 应用端开放接口 ### 1、登录地址 ``` url http://{host}:{port}/sso/login ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | back | 是 | 登录成功后的重定向地址,一般填写 location.href(从哪来回哪去) | | ticket | 否 | 授权 ticket 码 | 此接口有两种访问方式: - 方式一:我们需要登录操作,所以带着 back 参数主动访问此接口,框架会拼接好参数后再次将用户重定向至认证中心。 - 方式二:用户在认证中心登录成功后,带着 ticket 参数重定向而来,此为框架自动处理的逻辑,开发者无需关心。 ### 2、注销地址 ``` url http://{host}:{port}/sso/logout ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | back | 否 | 注销成功后的重定向地址,一般填写 location.href(从哪来回哪去),也可以填写 self 字符串,含义同上 | 此接口有两种访问方式: - 方式一:直接 `location.href` 网页跳转,此时可携带 back 参数。 - 方式二:使用 Ajax 异步调用(此方式不可携带 back 参数,但是需要提交会话 Token ),注销成功将返回以下内容: ``` js { "code": 200, // 200表示请求成功,非200标识请求失败 "msg": "单点注销成功", "data": null } ``` ### 3、单点注销回调接口 此接口仅配置 `(reg-logout-call=true)` 时打开,且为框架回调,开发者无需关心 ``` url http://{host}:{port}/sso/logoutCall ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | loginId | 是 | 要注销的账号 id | | timestamp | 是 | 当前时间戳,13位 | | nonce | 是 | 随机字符串 | | sign | 是 | 签名,生成算法:`md5( loginId={账号id}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | | client | 否 | 客户端标识,如果你在登录时向 sso-server 端传递了 client 值,那么在此处 sso-server 也会给你回传过来,否则此参数无值。如果此参数有值,则此参数也要参与签名,放在 loginId 参数前面(字典顺序) | | autoLogout | 否 | 是否为“登录client超过最大数量”引起的自动注销(true=超限系统自动注销,false=用户主动发起注销)。如果此参数有值,则此参数也要参与签名,放在 client 参数前面(字典顺序) | 返回数据: ``` js { "code": 200, // 200表示请求成功,非200标识请求失败 "msg": "单点注销回调成功", "data": null } ``` ### 4、消息推送接口 ``` url http://{host}:{port}/sso/pushC ``` 接收参数: | 参数 | 是否必填 | 说明 | | :-------- | :-------- | :-------- | | timestamp | 是 | 当前时间戳,13位 | | msgType | 是 | 消息类型 | | nonce | 是 | 随机字符串 | | sign | 是 | 签名,生成算法:`md5( msgType={消息类型}&nonce={随机字符串}×tamp={13位时间戳}&key={secretkey秘钥} )` | 此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。 返回值示例: - 推送成功时: ``` js { "code": 200, "msg": "ok", "data": "10001", // 返回的数据 } ``` - 推送失败时: ``` js { "code": 500, // 200表示请求成功,非200标识请求失败 "msg": "签名无效:xxx", // 失败原因 "data": null } ``` 详细可参考:[消息推送机制](/sso/message-push) ================================================ FILE: sa-token-doc/sso/sso-check-domain.md ================================================ # SSO整合-配置域名校验 --- ### 1、Ticket劫持攻击 在前面章节的 SSO-Server 示例中,配置项 `sa-token.sso-server.clients.sso-client3.allow-url=*` 意为该 client 所有允许的授权地址,不在此配置项中的 URL 将无法单点登录成功。 为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 Ticket 劫持的风险。 假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL: > [http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/) 当不知情的小红被诱导访问了这个URL时,它将被重定向至百度首页: sso-ticket-jc 可以看到,代表着用户身份的 Ticket 码也显现到了 URL 之中,借此漏洞,攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器,伪造小红身份登录网站 ### 2、防范方法 造成此漏洞的直接原因就是SSO-Server认证中心没有对 `redirect地址` 进行任何的限制,防范的方法也很简单,就是对`redirect参数`进行校验,如果其不在指定的URL列表中时,拒绝下放ticket 我们将其配置为一个具体的URL: ``` yaml sa-token: sso-server: clients: sso-client3: # 配置允许单点登录的 url allow-url: http://sa-sso-client1.com:9003/sso/login ``` ``` properties # 配置允许单点登录的 url sa-token.sso-server.clients.so-client3.allow-url=http://sa-sso-client1.com:9003/sso/login ``` 再次访问上述链接: sso-feifa-rf 域名没有通过校验,拒绝授权! ### 3、配置安全性参考表 | 配置方式 | 举例 | 安全性 | 建议 | | :-------- | :-------- | :-------- | :-------- | | 配置为* | `*` | | **禁止在生产环境下使用** | | 配置到域名 | `http://sa-sso-client1.com/*` | | 不建议在生产环境下使用 | | 配置到详细地址| `http://sa-sso-client1.com:9001/sso/login` | | 可以在生产环境下使用 | ### 4、疑问:为什么不直接回传 Token,而是先回传 Ticket,再用 Ticket 去查询对应的账号id? Token 作为长时间有效的会话凭证,在任何时候都不应该直接暴露在 URL 之中(虽然 Token 直接的暴露本身不会造成安全漏洞,但会为很多漏洞提供可乘之机) 为了不让系统安全处于亚健康状态,Sa-Token-SSO 选择先回传 Ticket,再由 Ticket 获取账号id,且 Ticket 一次性用完即废,提高安全性。 ================================================ FILE: sa-token-doc/sso/sso-custom-api.md ================================================ # SSO整合-自定义 API 路由 --- ### 方式一:修改全局变量 在之前的章节中,我们演示了如何搭建一个SSO认证中心: ``` java /** * Sa-Token-SSO Server端 Controller */ @RestController public class SsoServerController { // SSO-Server端:处理所有SSO相关请求 @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoServerProcessor.instance.dister(); } // ... 其它代码 } ``` 这种写法集成简单但却不够灵活。例如认证中心地址只能是:`http://{host}:{port}/sso/auth`,如果我们想要自定义其API地址,应该怎么做呢? 打开SSO模块相关源码,有关 API 的设计都定义在: [ApiName.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java) 中,我们可以对其进行二次修改。 例如,我们可以在 Main 方法启动类或者 SSO 配置方法中修改变量值: ``` java // 配置SSO相关参数 @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 自定义API地址 SaSsoServerProcessor.instance.ssoServerTemplate.apiName.ssoAuth = "/sso/auth2"; // ... } ``` 启动项目,统一认证地址就被我们修改成了:`http://{host}:{port}/sso/auth2` ### 方式二:拆分路由入口 根据上述路由入口:`@RequestMapping("/sso/*")`,我们给它起一个合适的名字 —— 聚合式路由。 与之对应的,我们可以将其修改为拆分式路由: ``` java /** * Sa-Token-SSO Server端 Controller */ @RestController public class SsoServerController { // SSO-Server:统一认证地址 @RequestMapping("/sso/auth") public Object ssoAuth() { return SaSsoServerProcessor.instance.ssoAuth(); } // SSO-Server:RestAPI 登录接口 @RequestMapping("/sso/doLogin") public Object ssoDoLogin() { return SaSsoServerProcessor.instance.ssoDoLogin(); } // SSO-Server:接收推送消息地址 @RequestMapping("/sso/pushS") public Object ssoPushS() { return SaSsoServerProcessor.instance.ssoPushS(); } // SSO-Server:单点注销 @RequestMapping("/sso/signout") public Object ssoSignout() { return SaSsoServerProcessor.instance.ssoSignout(); } // ... 其它方法 } ``` 拆分式路由 与 聚合式路由 在功能上完全等价,且提供了更为细致的路由管控。 ### SSO-Client 端拆分路由入口示例 ``` java /** * Sa-Token-SSO Client端 Controller */ @RestController public class SsoClientController { // SSO-Client:登录地址 @RequestMapping("/sso/login") public Object ssoLogin() { return SaSsoClientProcessor.instance.ssoLogin(); } // SSO-Client:单点注销地址 @RequestMapping("/sso/logout") public Object ssoLogout() { return SaSsoClientProcessor.instance.ssoLogout(); } // SSO-Client:单点注销回调 @RequestMapping("/sso/logoutCall") public Object ssoLogoutCall() { return SaSsoClientProcessor.instance.ssoLogoutCall(); } // SSO-Client:接收消息推送地址 @RequestMapping("/sso/ssoPushC") public Object ssoPushC() { return SaSsoClientProcessor.instance.ssoPushC(); } // ... 其它方法 } ``` ================================================ FILE: sa-token-doc/sso/sso-custom-login.md ================================================ # SSO整合-定制化登录页面 --- ### 1、何时引导用户去登录? #### 方案一:前端按钮跳转 前端页面准备一个 **`[登录]`** 按钮,当用户点击按钮时,跳转到登录接口 ``` html 登录 ``` #### 方案二:后端拦截重定向 在后端注册全局过滤器(或拦截器、或全局异常处理),拦截需要登录后才能访问的页面资源,将未登录的访问重定向至登录接口 ``` java /** * Sa-Token 配置类 */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** 注册 [Sa-Token全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/**") .addExclude("/sso/*", "/favicon.ico") .setAuth(obj -> { if(StpUtil.isLogin() == false) { String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString()); SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back)); SaRouter.back(); } }) ; } } ``` #### 方案三:后端拦截 + 前端跳转 首先,后端仍需要提供拦截,但是不直接引导用户重定向,而是返回未登录的提示信息 ``` java /** * Sa-Token 配置类 */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** 注册 [Sa-Token全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/**") .addExclude("/sso/*", "/favicon.ico") .setAuth(obj -> { if(StpUtil.isLogin() == false) { // 与前端约定好,code=401时代表会话未登录 SaRouter.back(SaResult.ok().setCode(401)); } }) ; } } ``` 前端接受到返回结果 `code=401` 时,开始跳转至登录接口 ``` js if(res.code == 401) { location.href = '/sso/login?back=' + encodeURIComponent(location.href); } ``` 这种方案比较适合以 Ajax 访问的 RestAPI 接口重定向 ### 2、如何自定义登录视图? #### 方式一:在demo示例中直接更改 login.html 页面代码即可 #### 方式二:在配置中配置登录视图地址 ``` java // 配置:未登录时返回的View ssoServerTemplate.strategy.notLoginView = () -> { return new ModelAndView("xxx.html"); } ``` ### 3、如何自定义登录API的接口地址? 根据需求点选择解决方案: #### 3.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数? ``` java // 在任意代码处获取前端提交的参数 String xxx = SaHolder.getRequest().getParam("xxx"); ``` #### 3.2、想完全自定义一个接口来接受前端登录请求? ``` java // 直接定义一个拦截路由为 `/sso/doLogin` 的接口即可 @RequestMapping("/sso/doLogin") public SaResult ss(String name, String pwd) { System.out.println("------ 请求进入了自定义的API接口 ---------- "); if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功!"); } return SaResult.error("登录失败!"); } ``` #### 3.3、不想使用`/sso/doLogin`这个接口,想自定义一个API地址? 答:直接在前端更改点击按钮时 Ajax 的请求地址即可 ### 4、不同 client 不同登录页 如果你的不同应用覆盖的用户群体差异极大,此时你可能想针对不同的应用跳转到不同的登录页,让每个应用的用户在登录时能够看到当前应用的专属信息,怎么做呢? 首先,保证每个 sso-client 端都配置了不同的 client 标识: ``` yaml sa-token: sso-client: # 当前 client 标识 client: sso-client-shop ``` ``` properties # 当前 client 标识 sa-token.sso-client.client=sso-client-shop ``` 然后在 `sso-server` 里为每个系统开发不同的登录页,并在 `configSso` 方法里 `notLoginView` 函数中根据 client 值,返回不同的登录视图: ``` java // 配置SSO相关参数 @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:未登录时返回的View ssoServerTemplate.strategy.notLoginView = () -> { String client = SaHolder.getRequest().getParam("client"); if("sso-client-shop".equals(client)) { return new ModelAndView("sa-shop-login.html"); } if("sso-client-video".equals(client)) { return new ModelAndView("sa-video-login.html"); } // 更多 ... // 都不匹配,返回一个默认的 return new ModelAndView("sa-login.html"); }; // ... } ``` ================================================ FILE: sa-token-doc/sso/sso-dev.md ================================================ # Sa-Token SSO Server端 二次开发用到的所有函数说明 本篇展示一下 SSO 模块常用的工具类、方法 ## Sso-Server 工具类 ### Ticket 操作 ``` java // 增删改 // 删除 Ticket SaSsoServerUtil.deleteTicket(String ticket); // 根据参数创建一个 ticket 码,并保存 SaSsoServerUtil.createTicketAndSave(String client, Object loginId, String tokenValue); // 查 // 查询 ticket ,如果 ticket 无效则返回 null SaSsoServerUtil.getTicket(String ticket); // 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null SaSsoServerUtil.getLoginId(String ticket); // 查询 ticket 指向的 loginId,并转换为指定类型 SaSsoServerUtil.getLoginId(String ticket, Class cs); // 校验 // 校验 Ticket,无效 ticket 会抛出异常 SaSsoServerUtil.checkTicket(String ticket); // 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 SaSsoServerUtil.checkTicketParamAndDelete(String ticket); // 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 SaSsoServerUtil.checkTicketParamAndDelete(String ticket, String client); // ticket 索引 // 查询 指定 client、loginId 其所属的 ticket 值 SaSsoServerUtil.getTicketValue(String client, Object loginId); ``` ### Client 信息获取 ``` java // 获取所有 Client SaSsoServerUtil.getClients(); // 获取应用信息,无效 client 返回 null SaSsoServerUtil.getClient(String client); // 获取应用信息,无效 client 则抛出异常 SaSsoServerUtil.getClientNotNull(String client); // 获取匿名 client 信息 SaSsoServerUtil.getAnonClient(); // 获取所有需要接收消息推送的 Client SaSsoServerUtil.getNeedPushClients(); ``` ### 重定向 URL 构建与校验 ``` java // 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 SaSsoServerUtil.buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue); // 校验重定向 url 合法性 SaSsoServerUtil.checkRedirectUrl(String client, String url); ``` ### 单点注销 ``` java // 指定账号单点注销 SaSsoServerUtil.ssoLogout(Object loginId); // 指定账号单点注销 SaSsoServerUtil.ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient); ``` ### 消息推送 ``` java // 向指定 Client 推送消息 SaSsoServerUtil.pushMessage(SaSsoClientModel clientModel, SaSsoMessage message); // 向指定 client 推送消息,并将返回值转为 SaResult SaSsoServerUtil.pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message); // 向指定 Client 推送消息 SaSsoServerUtil.pushMessage(String client, SaSsoMessage message); // 向指定 client 推送消息,并将返回值转为 SaResult SaSsoServerUtil.pushMessageAsSaResult(String client, SaSsoMessage message); // 向所有 Client 推送消息 SaSsoServerUtil.pushToAllClient(SaSsoMessage message); // 向所有 Client 推送消息,并忽略掉某个 client SaSsoServerUtil.pushToAllClient(SaSsoMessage message, String ignoreClient); ``` 详情请参考源码:[码云:SaSsoServerUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java) ## Sso-Client 工具类 ### 构建交互地址 ``` java // 构建URL:Server端 单点登录授权地址 SaSsoClientUtil.buildServerAuthUrl(String clientLoginUrl, String back); ``` ### 消息推送 ``` java // 向 sso-server 推送消息 SaSsoClientUtil.pushMessage(SaSsoMessage message); // 向 sso-server 推送消息,并将返回值转为 SaResult SaSsoClientUtil.pushMessageAsSaResult(SaSsoMessage message); // 构建消息:校验 ticket SaSsoClientUtil.buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl); // 构建消息:单点注销 SaSsoClientUtil.buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter); ``` 详情请参考源码:[码云:SaSsoClientUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java) ## Sso-Server 所有可重写策略 ``` java // 发送 Http 请求的处理函数 SaSsoServerProcessor.instance.ssoServerTemplate.strategy.sendRequest = url -> { // ... } // 使用异步模式执行一个任务 SaSsoServerProcessor.instance.ssoServerTemplate.strategy.asyncRun = fun -> { // ... } // 未登录时返回的 View SaSsoServerProcessor.instance.ssoServerTemplate.strategy.notLoginView = () -> { // ... } // SSO-Server端:登录函数 SaSsoServerProcessor.instance.ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { // ... } //SSO-Server端:在授权重定向之前的通知 SaSsoServerProcessor.instance.ssoServerTemplate.strategy.jumpToRedirectUrlNotice = (redirectUrl) -> { // ... } // SSO-Server端:在校验 ticket 后,给 sso-client 端追加返回信息的函数 SaSsoServerProcessor.instance.ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> { // ... } ``` ## Sso-Client 所有可重写策略 ``` java // 发送 Http 请求的处理函数 SaSsoClientProcessor.instance.ssoClientTemplate.strategy.sendRequest = url -> { // ... } // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) SaSsoClientProcessor.instance.ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { // ... } // 转换:认证中心 centerId > 本地 loginId SaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { // ... } // 转换:本地 loginId > 认证中心 centerId SaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { // ... } ``` ================================================ FILE: sa-token-doc/sso/sso-diff-key.md ================================================ # 不同 SSO Client 配置不同秘钥 在校验 ticket、单点注销等操作发起的 http 调用时,需要配置秘钥参数,像这样: ``` yaml sa-token: sign: # API 接口调用秘钥 secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` ``` properties # 接口调用秘钥 sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` 如果 SSO Client 端和 SSO Server 端配置的秘钥不同,则无法调通请求,显示无效签名: ``` js { "code": 500, "msg": "无效签名:9f1b453817bfeac56d2f772a66c01eb2", "data": null } ``` 如果你有多个 SSO Client,你可能想让每个应用配置不同的秘钥,让它们彼此之间不能互相“冒充”,怎么做呢? ### 1、首先在 SSO Client 端,你需要配置上不同的 Client 标识参数: 例如在 client1 我们配置上: ``` yaml sa-token: sso-client: # 当前 client 标识 client: sso-client1 # ... sign: # sso-client1 使用的秘钥 secret-key: secret-key-xxxx-1 ``` ``` properties # 当前 client 标识 sa-token.sso-client.client=sso-client1 # sso-client1 使用的秘钥 sa-token.sign.secret-key=secret-key-xxxx-1 ``` 在 client2 我们配置上: ``` yaml sa-token: sso-client: # 当前 client 标识 client: sso-client2 # ... sign: # sso-client2 使用的秘钥 secret-key: secret-key-xxxx-2 ``` ``` properties # 当前 client 标识 sa-token.sso-client.client=sso-client2 # sso-client2 使用的秘钥 sa-token.sign.secret-key=secret-key-xxxx-2 ``` ### 2、然后在 SSO Server 端,重写获取秘钥的函数 在 SSO Server 端新建 `CustomSaSsoServerTemplate.java`,继承 `SaSsoServerTemplate`,重写其 `getSignTemplate` 函数: ``` java /** * 自定义 SaSsoServerTemplate 子类 */ @Component public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { // 存储所有 client 的秘钥 static Map signMap = new HashMap<>(); static { signMap.put("sso-client1", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-1"))); signMap.put("sso-client2", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-2"))); signMap.put("sso-client3", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-3"))); // ... } @Override public SaSignTemplate getSignTemplate(String client) { // 先从自定义的 signMap 中获取 SaSignTemplate saSignTemplate = signMap.get(client); if (saSignTemplate != null) { return saSignTemplate; } // 找不到就返回全局默认的 SaSignTemplate return SaManager.getSaSignTemplate(); } } ``` 至此完成。 ### 3、其它注意点 有同学反馈,集成 “不同 SSO Client 配置不同秘钥” 模式后,客户端发起调用 `/sso/getData` 调用时会报如下错误: ``` 无效签名:5a7fc42836deba12d96527d43c1301ea ``` 或者: ``` 参与参数签名的秘钥不可为空 ``` 这大概率是因为在 sso-server 端的 `/sso/getData` 接口在校验签名时忘了加 client 参数导致的,修改为如下代码即可: ``` java // 示例:获取数据接口(用于在模式三下,为 client 端开放拉取数据的接口) @RequestMapping("/sso/getData") public SaResult getData(String apiType, String loginId) { System.out.println("---------------- 获取数据 ----------------"); System.out.println("apiType=" + apiType); System.out.println("loginId=" + loginId); // ↓↓↓ ⚠️ 重点代码 ↓↓↓ // 校验签名:只有拥有正确秘钥发起的请求才能通过校验 String client = SaHolder.getRequest().getHeader("client"); SaSsoServerProcessor.instance.ssoServerTemplate.getSignTemplate(client).checkRequest(SaHolder.getRequest()); // ↑↑↑ ⚠️ 重点代码 ↑↑↑ // 自定义返回结果(模拟) return SaResult.ok() .set("id", loginId) .set("name", "LinXiaoYu") .set("sex", "女") .set("age", 18); } ``` ================================================ FILE: sa-token-doc/sso/sso-h5.md ================================================ # SSO整合-前后端分离架构下的整合方案 --- ## SSO-Client 前后端分离 要在前后端分离的环境中接入 SSO,思路不难,主要的工作是把后端 `/sso/login` 接口的路由中转工作拿到前端来,以`sa-token-demo-sso3-client`为例: ### 1、在 sso-client 后端新建`H5Controller`,开放接口: ``` java /** * 前后台分离架构下集成 SSO 所需的代码 (SSO-Client端) */ @RestController public class H5Controller { // 判断当前是否登录 @RequestMapping("/sso/isLogin") public Object isLogin() { return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull()); } // 返回SSO认证中心登录地址 @RequestMapping("/sso/getSsoAuthUrl") public SaResult getSsoAuthUrl(String clientLoginUrl) { String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, ""); return SaResult.data(serverAuthUrl); } // 根据 ticket 进行登录 @RequestMapping("/sso/doLoginByTicket") public SaResult doLoginByTicket(String ticket) { SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket); StpUtil.login(ctr.loginId, new SaLoginParameter() .setTimeout(ctr.remainTokenTimeout) .setDeviceId(ctr.deviceId) ); return SaResult.data(StpUtil.getTokenValue()); } } ``` ### 2、增加跨域处理策略 ``` java /** * [Sa-Token 权限认证] 配置类 */ @Configuration public class SaTokenConfigure { /** * CORS 跨域处理策略 */ @Bean public SaCorsHandleFunction corsHandle() { return (req, res, sto) -> { res. // 允许指定域访问跨域资源 setHeader("Access-Control-Allow-Origin", "*") // 允许所有请求方式 .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE") // 有效时间 .setHeader("Access-Control-Max-Age", "3600") // 允许的header参数 .setHeader("Access-Control-Allow-Headers", "*"); // 如果是预检请求,则立即返回到前端 SaRouter.match(SaHttpMethod.OPTIONS) .free(r -> System.out.println("--------OPTIONS预检请求,不做处理")) .back(); }; } } ``` 详细参考:[解决跨域问题](/fun/cors-filter) ### 3、新建前端项目 任意文件夹新建前端项目:`sa-token-demo-sso-client-h5`,在根目录添加测试文件:`index.html` ``` html Sa-Token-SSO-Client端-测试页(前后端分离版-原生h5)

Sa-Token SSO-Client 应用端(前后端分离版-原生h5)

当前是否登录:

登录 - 单应用注销 - 单浏览器注销 - 全端注销 - 账号资料

``` ### 4、添加单点登录登录中转页 在根目录创建文件:`sso-login.html` ``` html Sa-Token-SSO-Client端-登录中转页页 ``` ### 5、添加公共 js文件 新建 `sso-common.js`: ``` js // 服务器接口主机地址 // var baseUrl = "http://sa-sso-client1.com:9002"; // 模式二后端 var baseUrl = "http://sa-sso-client1.com:9003"; // 模式三后端 // 封装一下Ajax function ajax(path, data, successFn, errorFn) { console.log('发起请求:', baseUrl + path, JSON.stringify(data)); fetch(baseUrl + path, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'satoken': localStorage.getItem('satoken') }, body: serializeToQueryString(data), }) .then(response => response.json()) .then(res => { console.log('返回数据:', res); if(res.code === 500) { return alert(res.msg); } successFn(res); }) .catch(error => { console.error('请求失败:', error); return alert("异常:" + JSON.stringify(error)); }); } // ------------ 工具方法 --------------- // 从url中查询到指定名称的参数值 function getParam(name, defaultValue) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split("="); if (pair[0] == name) { return pair[1]; } } return (defaultValue == undefined ? null : defaultValue); } // 将 json 对象序列化为kv字符串,形如:name=Joh&age=30&active=true function serializeToQueryString(obj) { return Object.entries(obj) .filter(([_, value]) => value != null) // 过滤 null 和 undefined .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } // 向指定标签里 set 内容 function setHtml(select, html) { const dom = document.querySelector('.is-login'); if(dom) { dom.innerHTML = html; } } ``` ### 6、测试运行 先启动 Server 服务端与 Client 服务端,再随便找个能预览html的工具打开前端项目(比如[HBuilderX](https://www.dcloud.io/hbuilderx.html)),测试流程与一体版一致,暂不赘述。 > [!TIP| label:另附其它技术栈的前后端分离 demo 示例:] > - sso-client 前后端分离 - 原生h5:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5) > - sso-client 前后端分离 - vue2:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2) > - sso-client 前后端分离 - vue3:[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3) ## SSO-Server 前后端分离 解决思路与 SSO-Client 一样,我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。 由于集成代码与 Client 端类似,这里暂不贴详细代码,我们可以下载官方仓库,里面有搭建好的demo。 使用前端 ide 导入项目 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5`,浏览器访问 `sso-auth.html` 页面: sso-type2-server-h5-auth.png 复制上述地址,将其配置到 Client 端的配置项 `sa-token.sso-client.auth-url` ,例如: ``` yaml sa-token: sso-client: # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html ``` ``` properties # SSO-Server端 统一认证地址 sa-token.sso-client.server-url=http://sa-sso-server.com:9000 # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数(上面的不要注释,auth-url 配置项和 server-url 要同时存在) sa-token.sso-client.auth-url=http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html ``` 然后我们启动项目 sso-server 与 sso-client ,按照之前的测试步骤访问: [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/),即可以前后端分离模式完成 SSO-Server 端的授权登录。 ================================================ FILE: sa-token-doc/sso/sso-home-jump.md ================================================ # SSO 平台中心跳转模式,点连接跳入子系统 --- 有的时候,我们需要把 sso-server 搭建成一个平台中心,效果图大致如下: sso-home-jump.png 如图所示,用户先从 sso-server 登录进入平台首页,在首页上有各个子系统的进入链接,用户点击链接进入子系统(免登录)。 怎么做到如上效果呢?当然,加个超链接跳到子系统并不难,难点在于跳转的同时我们需要让用户自动登录上子系统,从而达到:平台中心一处登录,所有子系统无障碍通行的效果。 怎么做到跳转的时候自动登录呢?直接跳转肯定是不会自动登录的,我们需要对链接改造一下: 假设子系统的地址是: ``` url http://sa-sso-client1.com:9003/ ``` 那么我们改造后的地址就是: ``` url /sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/ ``` 格式形如:`/sso/auth?client={client标识}&redirect=${子系统首页}/sso/login?back=${子系统首页}` --- ### 完整代码示例: 1、在 sso-server 中配置 `home-route` 字段: ``` yaml # Sa-Token 配置 sa-token: # SSO-Server 配置 sso-server: # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 home-route: /home ``` ``` properties # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 sa-token.sso-server.home-route: /home ``` 2、在 sso-server 中添加 `HomeController`,作为平台中心首页: ``` java /** * SSO 平台中心模式示例,跳连接进入子系统 */ @RestController public class HomeController { // 平台化首页 @RequestMapping({"/", "/home"}) public Object index() { // 如果未登录,则先去登录 if(!StpUtil.isLogin()) { return SaHolder.getResponse().redirect("/sso/auth"); } // 拼接各个子系统的地址,格式形如:/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页} String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/"; String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/"; String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/"; // 组织网页结构返回到前端 String title = "

SSO 平台首页 (平台中心模式)

"; String client1 = "

进入Client1系统

"; String client2 = "

进入Client2系统

"; String client3 = "

进入Client3系统

"; return title + client1 + client2 + client3; } } ``` ### 测试访问 启动项目,访问:[http://sa-sso-server.com:9000](http://sa-sso-server.com:9000) 首次访问,因为我们没有登录,所以会被重定向到 `/sso/auth` 登录页,我们登录上之后,便会跳转到平台中心首页: sso-home-jump-do.png 依次点击三个链接,便可在跳转的同时自动登录上子系统。 ================================================ FILE: sa-token-doc/sso/sso-nosdk.md ================================================ # SSO整合 - NoSdk、ReSdk 模式与非 java 项目 --- 经常有小伙伴提问:客户端不使用 Sa-Token,能否接入 SSO 认证中心?当然是可以的。 SSO-Server 所有接口都是通过 http 协议开放的,这意味着原则上只要一个语言支持 http 请求调用就可以对接 SSO-Server,参考: [SSO 认证中心开放接口](/sso/sso-apidoc) ### NoSdk 模式 NoSdk 模式(不使用SDK):通过 http 工具类调用接口的方式来对接 SSO-Server。 参考 demo:[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk) 该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 `/sso/login`、 `/sso/logout`、 `/sso/logoutCall` 三个接口的处理逻辑。 > [!WARNING| label:NoSdk 示例不再主维护] > 基于以下原因: > - 1、NoSdk demo 相当于通过 http 工具类再次重写了一遍 Sa-Token SSO 模块代码,繁琐且冗余。 > - 2、重写的代码无法拥有 Sa-Token SSO 模块全部能力,仅能完成基本对接,算是一个简化版 SDK。 > > 自 v1.43.0 版本起,不再主维护 NoSdk 模式,仓库示例仅做留档参考,大家可以转为 ReSdk 模式。 ### ReSdk 模式 ReSdk 模式(重写SDK部分方法):通过重写框架关键步骤点,来对接 SSO-Server。 参考 demo:[sa-token-demo-sso3-client-resdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk) > [!INFO| label:ReSdk 模式优点] > - 1、依然支持客户端使用任意技术栈。 > - 2、仅重写少量部分关键代码,即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。 建议新项目首选 ReSdk 模式作为参考。 ### 非 java 语言项目 sso-server 的所有接口均以 http 协议对外开放,因此原则上支持任何语言对接,只要这个语言支持 http 请求调用。 例如 PHP、.NET、Node.js 等语言的项目,无法集成 Sa-Token,同上,也可以通过 http 工具类调用接口的方式来对接 SSO-Server。 建议各位同学先搞懂 NoSdk 模式的对接流程,再参照 [SSO 认证中心开放接口](/sso/sso-apidoc) 章节进行对接。 ================================================ FILE: sa-token-doc/sso/sso-pro.md ================================================ # Sa-Sso-Pro 单点登录商业版 ### 项目介绍 根据 Sa-Token SSO 模块文档,以及官网提供的源码示例,您可以很方便的搭建一个SSO模式的认证Demo。 然而,要真正开发一个商业级项目的认证中心系统,绝非一朝一夕可以搭建完毕,其必不可少的一些功能, 比如:用户账号增删改查维护、登录日志统计、新增用户数据报表、新增 Client 应用接入域名配置……等等, 仍需要大量的开发时间。 为此,我们特意准备了项目:[[ Sa-Sso-Pro 单点登录商业版 ]](https://sa-pro.yun94.cn?way=st_md), 项目集成了单点登录常见技术点, 绝大多数功能无需二次开发,直接可用,可大大缩短您的项目接入单点登录的开发周期。 ### 释疑 ##### 1、Sa-Sso-Pro 是收费项目吗?与 Sa-Token 有什么不同? `Sa-Sso-Pro` 是付费项目,暂不开放源码,如需使用需要购买项目授权,您可以在其主页了解更多详细信息。 `Sa-Sso-Pro` 与 `Sa-Token` 的区别,简单来讲: - `Sa-Token` 是一个框架,需要在项目中通过 pom.xml 引入 - `Sa-Sso-Pro` 是一个完整项目,下载源码后可直接启动 ##### 2、Sa-Token 会不会在某一天收费?导致我们项目无法正常运行? 首先我们需要了解一点:**已经发布到 Maven 中央仓库的代码,是不可以删除的**,所以这部分代码是无法做到收费的 其次,像中间件框架,业界没有收费的先例,也没有对应的商业模式,一般的付费项目都是一些成型的完整项目,以解决特定场景的业务需求为目的, 比如:聊天通信、刷脸认证、短信验证码、聚合支付……等等。 Sa-Sso-Pro 并非随意收费,只有当您的系统需要 **统一认证中心** 时您才会用到它,花一笔小钱节省大量开发工期,整体来看,这是非常划算的。 另外:即使您没有购买 `Sa-Sso-Pro`,也不会影响到您对 `Sa-Token` 的使用,举个例子:MySQL具有社区版与企业版,即使您没有购买其付费版,也不会影响到您对免费 MySql 的使用。 ##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Sso-Pro 上,降低对 Sa-Token 的支持?毕竟 Sa-Token 是免费的! 答案是不会。 再次强调一下:`Sa-Token` 与 `Sa-Sso-Pro` 是两个独立的项目,两者互不影响。 付费项目的出现不会降低对 `Sa-Token` 的支持,`Sa-Token`将会按照原有的发展继续升级迭代。 实际结果可能会恰恰相反:有了盈利来源,`Sa-Token`将发展的更快。 ================================================ FILE: sa-token-doc/sso/sso-questions.md ================================================ # Sa-Token-SSO整合-常见问题总结 SSO 集成常见问题整理 [[toc]] --- ### 问:在模式一与模式二中,Client端 必须通过 Alone-Redis 插件来访问 Redis 吗? 答:不必须,只是推荐,权限缓存与业务缓存分离后会减少 `SSO-Redis` 的访问压力,且可以避免多个 `Client端` 的缓存读写冲突。 ### 问:搭建好 sso-server 或 sso-client 服务后,访问返回:`{"msg": "not handle"}`。 返回这个信息,代表你访问的路由有错误,比如说: - 统一认证登录地址是:`http://{host}:{port}/sso/auth`。 - 而你访问的却是:`http://{host}:{port}/sso/auth2`。 地址写错了,框架就不会处理这个请求,会直接返回 `{"msg": "not handle"}`,所有开放地址可参考:[SSO 开放接口](/sso/sso-apidoc) 如果仔细检查地址后没有写错,却依然返回了这个信息,那有可能是对应的接口没有打开,比如说: - sso-server 端的单点注销地址:`http://{host}:{port}/sso/signout`; - sso-client 端的注销地址:`http://{host}:{port}/sso/logout`; 都需要在配置文件配置:`sa-token.sso-server.is-slo=true`(client端为 `sa-token.sso-client.is-slo=true` )后,才会打开。 ### 问:我参照文档搭建SSO-Client,一直提示:Ticket无效,请问怎么回事? 如果使用的是模式二,出现此异常概率最大的原因是因为 `Client` 与 `Server` 没有连接同一个Redis,SSO模式二中两者必须连接同一个 Redis 才可以登录成功。 你可能会问:我看配置文件明明是同一个啊? 我的建议是:排查时不要仅凭肉眼判断,分别在你的 `Client` 与 `Server` 启动后调用 `SaManager.getSaTokenDao().set("name", "value", 100000);` 随便写入一个值,看看能不能根据你的预期写进同一个Redis里,如果能的话才能证明 `Client` 与 `Server` 连接的Reids 是同一个,再进行下一步排查。 ``` java @SpringBootApplication public class SaSsoServerApplication { public static void main(String[] args) { SpringApplication.run(SaSsoServerApplication.class, args); System.out.println("\n------ Sa-Token-SSO 统一认证中心启动成功 "); // 分别在 Client 与 Server 启动后调用 set 数据代码,看看能否根据预期写入同一个 reids SaManager.getSaTokenDao().set("name", "value", 100000); } } ``` 如果使用的是模式三,则排查是否有重复校验 ticket 的代码,一个 ticket 码只能使用一次,多次重复使用就会提示这个。 ### 问:模式一或者模式二报错:Could not write JSON: No serializer found for class com.pj.sso.SysUser and no properties discovered to create BeanSerializer 一般是因为在 sso-server 端往 session 上写入了某个实体类(比如 User),而在 sso-client 端没有这个实体类,导致反序列化失败。 解决方案:在 sso-client 也新建上这个类,而且包名需要与 sso-server 端的一致(直接从 sso-server 把实体类复制过来就好了) ### 在测试模式一时,出现一些难以理解的现象 测试模式一时,三种异常现象: 1、在 sso-client 端点击登录,可以成功跳转到 sso-server 端,登录后可以跳回 sso-client 端,但显示 sso-client 端未登录 - 原因:sso-server 后端没有配置 sa-token.cookie.domain 值。 2、在 sso-client 端点击登录,可以成功跳转到 sso-server 端,登录页面刷新一下,并没有跳转回 sso-client 端,依然提示让你登录 - 可能1:sso-server 后端配置了错误的 sa-token.cookie.domain 值。 - 可能2:sso-server 后端没有打开 sa-token.is-read-cookie 值。(测试模式二三时发生这种现象,有时候也是因为这个) 3、在 sso-client 端点击登录,页面只是闪了一下,肉眼没有观察到页面跳转,页面也没有显示登录上。 原因:sso-server 的页面存储了不带 . 的有效 Cookie,为什么会这样:常常是因为测试模式二三后,没有清除redis记录或者浏览器记录,直接再开始测试模式一, 模式二三登录成功后遗留的有效cookie,影响了模式一的行为逻辑。 解决方案: - 1、手动清空 redis里的所有数据, - 2、或者手动清空 sso-server 域名下的所有 Cookie - 3、换一个新的干净浏览器来测试。 ### 问:模式三配置一堆 xxx-url ,有办法简化一下吗? 可以使用 `sa-token.sso-client.server-url` 来简化: 配置含义:配置 Server 端主机总地址,拼接在 authUrl、getDataUrl、sloUrl 属性前面,用以简化各种 url 配置。 在开发 SSO 模块时,我们需要在 sso-client 配置认证中心的各种地址,特别是在模式三下,一般代码会变成这样: ``` yaml sa-token: sso-client: # SSO-Server端 统一认证地址 auth-url: http://sa-sso-server.com:9000/sso/auth # 单点注销地址 slo-url: http://sa-sso-server.com:9000/sso/signout # SSO-Server端 查询数据地址 get-data-url: http://sa-sso-server.com:9000/sso/getData ``` 一堆 xxx-url 配置比较繁琐,且含有大量重复字符,现在我们可以将其简化为: ``` yaml sa-token: sso-client: server-url: http://sa-sso-server.com:9000 ``` 只要你配置了 `server-url` 地址,Sa-Token 就可以自动拼接出其它四个地址: **例1,使用 server-url 简化:** - 你配置的 server-url 值是:`http://sa-sso-server.com:9000`。 - 框架拼接出的 auth-url 值就是:`http://sa-sso-server.com:9000/sso/auth`,其它三个 url 配置项同理。 **例2,使用 server-url + auth-url 简化:** - 你配置的 server-url 值是:`http://sa-sso-server.com:9000`,auth-url 是:`/sso/auth2`。 - 框架拼接出的 auth-url 值就是:`http://sa-sso-server.com:9000/sso/auth2`,其它三个 url 配置项同理。 **例3,auth-url 地址以 http 字符开头:** - 你配置的 server-url 值是:`http://sa-sso-server.com:9000`,auth-url 是:`http://my-site.com/sso/auth2`。 - 此时框架只以 auth-url 值为准,得到的 auth-url 值是:`http://my-site.com/sso/auth2`,其它三个 url 配置项同理。 ### 问:我接手了一个项目,里面集成了 Sa-Token SSO ,请问怎么快速分辨它用的模式几? **方法一:看代码注释。** 如果开发这个项目的人没有写清楚注释那就 gg 了。 **方法二,根据配置项来分析,例如:** - 先看配置项 `sa-token.cookie.domain`,如果此配置项有值,一般是在使用模式一开发,否则就是模式二或者模式三。 - 再看配置项 `sa-token.sso-client.is-http` ,如果有值且为 true,一般是在使用模式三,否则就是模式二。 **方法三,根据配置项 `sa-token.sso-client.mode` 的提示来判断** `sa-token.sso-client.mode` 是框架预留的约定型配置项,此配置项不对代码逻辑产生任何影响,只为系统做一个标记,标注此系统用到了SSO的哪个模式。 例如你可以将其配置为 `sa-token.sso-client.mode=client-2`,代表当前系统为 sso-client 端,使用 SSO 模式二来对接。 需要注意,这个配置项不是必须的,你不写也不会对代码造成任何影响,只有在你需要为系统做一个明确的标记时才需要去配置它,方便后人阅读代码时快速分析使用的模式。 例如我们可以使用以下约定: - `sa-token.sso-client.mode=client-2`:代表当前系统为 sso-client 端,使用 SSO 模式二来对接。 - `sa-token.sso-client.mode=client-2,h5`:代表当前系统为 sso-client 端,使用 SSO 模式二来对接,并且是前后端分离模式。 - `sa-token.sso-server.mode=server-123`:代表当前系统为 sso-server 端,同时开放了 SSO 模式一、模式二、模式三。 - `sa-token.sso-server.mode=server-2,client-2`:代表当前系统既是 sso-server 端,又是 sso-clent 端,使用模式二来对接。 - 等等等等... 此配置项可以是任意字符串,你也可以自己整理一套合适的表达规则。 ### 问:SSO模式二或模式三,第一个 client 登录成功之后再访问其它两个 client 不会自动登录,需要点一下登录按钮才会登录上? 答:这是正常现象,系统 1 登录成功之后,系统 2 与系统 3 需要点击登录按钮,才会登录成功。 > 第一个系统,需要:点击 [登录] 按钮 -> 跳转到登录页 -> 输账号密码 -> 登录成功
> 第二个系统,需要:点击 [登录] 按钮 -> 登录成功
> 第三个系统,需要:点击 [登录] 按钮 -> 登录成功 > > (系统二、三 免去重复跳转登录页输入账号密码的步骤) ### 追问:那我是否可以设计成不需要点登录按钮的,只要访问页面,它就能登录成功? 可以的。 其实思路很简单,我们只需要给 client 项目加个过滤器,拦截所有请求,只要检测到未登录就将其重定向至登录页面: ``` java /** * Sa-Token 配置类 */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** 注册 [Sa-Token全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/**") .addExclude("/sso/*", "/favicon.ico") // 这里需要注意排除掉 /sso/* 相关请求不拦截,否则就会触发无限重定向 .setAuth(obj -> { /* * 这里会被分为两种情况: * 情况1:这个请求在当前 client 已经登录,此时会顺利进入网站 * 情况2:这个请求在当前 client 尚未登录,此时会被拦截,重定向至当前系统的 /sso/login?back=当前地址 * * 情况2会带领着用户继续重定向至 sso-server 认证中心,此时又分为两种情况: * 情况2.1:此用户在 sso-server 尚未登录,此时会停留在登录页面,开始输入账号密码进行登录 * 情况2.2:此用户在 sso-server 已经登录(这证明此用户已经在其它至少一个 sso-client 处完成了登录) * 此时用户会继续重定向回当前 client,并携带 ticket 参数,完成登录。 */ if(StpUtil.isLogin() == false) { String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString()); SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back)); SaRouter.back(); } }) ; } } ``` 更多登录姿势可以参考 [[何时引导用户去登录]](/sso/sso-custom-login) 给出的建议进行设计。 ### 问:Client 信息可以做成从数据库读取的吗? 可以,自定义 `SaSsoServerTemplate` 实现类,重写 `getClient` 与 `getClients` 方法即可: ``` java /** * 重写 SaSsoServerTemplate 部分方法,增强功能 */ @Component public class CustomSaSsoServerTemplate extends SaSsoServerTemplate { // 获取指定 client 的配置信息 @Override public SaSsoClientModel getClient(String client) { if("sso-client1".equals(client)) { SaSsoClientModel scm = new SaSsoClientModel(); scm.setAllowUrl("sso-client1"); scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); return scm; } // ... return null; } // 返回所有 client 信息 @Override public List getClients() { // 模拟示例代码,真实项目可改为从数据查询 SaSsoClientModel scm1 = new SaSsoClientModel(); scm1.setAllowUrl("sso-client1"); scm1.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); SaSsoClientModel scm2 = new SaSsoClientModel(); scm2.setAllowUrl("sso-client2"); scm2.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor"); // ... return Arrays.asList(scm1, scm2); } } ``` ### 问:如果 sso-client 端我没有集成 sa-token-sso,如何对接? 需要手动调用 http 请求来对接 sso-server 开放的接口,参考示例: [sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk) ### 问:如果 sso-client 端不是 java语言,可以对接吗? 可以,只不过有点麻烦,基本思路和上个问题一致,需要手动调用 http 请求来对接 sso-server 开放的接口,参考: [SSO-Server 认证中心开放接口](/sso/sso-apidoc) ### 问:将旧有系统改造为单点登录时,应该注意哪些? 答:建议不要把其中一个系统改造为SSO服务端,而是新起一个项目作为 SSO-Server 端,所有旧有项目全部作为 Client 端与此对接。 ### 问:怎么在一个项目里同时搭建 sso-server 和 sso-client? 难点在于解决两边的路由冲突,示例代码: ``` java // Sa-Token SSO Controller @RestController public class SsoController { // 处理 SSO-Server 端所有请求 @RequestMapping({"/sso/auth", "/sso/doLogin", "/sso/signout", "/sso/pushS"}) public Object ssoServerRequest() { return SaSsoServerProcessor.instance.dister(); } // 处理 SSO-Client 端所有请求 @RequestMapping({"/sso/login", "/sso/logout", "/sso/logoutCall", "/sso/pushC"}) public Object ssoClientRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired private void configSsoServer(SaSsoServerTemplate ssoServerTemplate) { // SSO Server 配置代码,参考文档前几章 ... } @Autowired private void configSsoClient(SaSsoClientTemplate ssoClientTemplate) { // SSO Client 配置代码,参考文档前几章 ... } } ``` ### 问:我一个项目里有两套账号体系,都需要单点登录,怎么在一个项目里同时搭建两个 sso-server 服务? 首先推荐你不要在一个项目里同时搭建两个 sso-server,建议创建两个项目,分别搭建各自的 sso-server 服务。 如果一定要在一个项目中搭建两套 sso-server 服务,参考方案如下: 第一套,还是用前面几章文档给出的示例代码。 第二套,修改一些参数属性,使之与第一套不产生冲突,参考代码如下: ``` java /** * Sa-Token-SSO 第二套 SSO-Server端 Controller */ @RestController public class SsoUserServerController { /** * 新建一个 SaSsoServerProcessor 请求处理器 */ public static SaSsoServerProcessor ssoUserServerProcessor = new SaSsoServerProcessor(); static { // 自定义一个 getServerConfig SaSsoServerConfig serverConfig = new SaSsoServerConfig(); serverConfig.setSecretKey("xxx"); // 更多配置 ... // 自定义一个 SaSsoServerTemplate 对象 SaSsoServerTemplate ssoUserTemplate = new SaSsoServerTemplate() { @Override public SaSsoServerConfig getServerConfig() { return serverConfig; } }; // 使用自定义的 StpLogic 会话对象 ssoUserTemplate.setStpLogic(StpUserUtil.stpLogic); // 让这个SSO请求处理器,使用的路由前缀是 /sso-user,而不是原先的 /sso ssoUserTemplate.apiName.replacePrefix("/sso-user"); // 给这个 SSO 请求处理器使用自定义的 SaSsoTemplate 对象 ssoUserServerProcessor.ssoServerTemplate = ssoUserTemplate; } /* * 第二套 sso-server 服务:处理所有SSO相关请求 * http://{host}:{port}/sso-user/auth -- 单点登录授权地址 * http://{host}:{port}/sso-user/doLogin -- 账号密码登录接口 * http://{host}:{port}/sso-user/signout -- 单点注销地址(isSlo=true时打开) */ @RequestMapping("/sso-user/*") public Object ssoUserRequest() { return ssoUserServerProcessor.dister(); } // 自定义 doLogin 方法 */ // 注意点: // 1、第2套 sso-server 对应的 RestApi 登录接口也应该更换为 /sso-user/doLogin,而不是原先的 /sso/doLogin // 2、在这里,登录函数要使用自定义的 StpUserUtil.login(),而不是原先的 StpUtil.login() @RequestMapping("/sso-user/doLogin") public Object ssoUserRequest(String name, String pwd) { if("sa".equals(name) && "123456".equals(pwd)) { StpUserUtil.login(10001); return SaResult.ok("登录成功!").setData(StpUserUtil.getTokenValue()); } return SaResult.error("登录失败!"); } } ```
---
还有其它问题?

可以加群反馈一下,比较典型的问题我们解决之后都会提交到此页面方便大家快速排查

================================================ FILE: sa-token-doc/sso/sso-server.md ================================================ # 搭建统一认证中心 SSO-Server 在开始SSO三种模式的对接之前,我们必须先搭建一个 SSO-Server 认证中心 > [!TIP| label:demo] > 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`,如遇到难点可结合源码进行测试学习,demo里有制作好的登录页面 --- ### 1、添加依赖 创建 SpringBoot 项目 `sa-token-demo-sso-server`,在引入 SpringBoot 依赖的基础上,继续引入: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-sso ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 org.springframework.boot spring-boot-starter-thymeleaf cn.dev33 sa-token-forest ${sa.top.version} ``` ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 插件:整合SSO implementation 'cn.dev33:sa-token-sso:${sa.top.version}' // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' // 视图引擎(在前后端不分离模式下提供视图支持) implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Sa-Token 插件:整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) implementation 'cn.dev33:sa-token-forest:${sa.top.version}' ``` > [!NOTE| label:引包简化] > 除了 `sa-token-spring-boot-starter` 和 `sa-token-sso` 以外,其它包都是可选的: > > - 在 SSO 模式三时 Redis 相关包是可选的。 > - 在前后端分离模式下可以删除 thymeleaf 相关包。 > - 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包。 > > 建议先完整测试三种模式之后再对 pom 依赖进行酌情删减。 ### 2、开放认证接口 新建 `SsoServerController`,用于对外开放接口: ``` java /** * Sa-Token-SSO Server端 Controller */ @RestController public class SsoServerController { /** * SSO-Server端:处理所有SSO相关请求 * http://{host}:{port}/sso/auth -- 单点登录授权地址 * http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd * http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开) */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoServerProcessor.instance.dister(); } /** * 配置SSO相关参数 */ @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 配置:未登录时返回的View ssoServerTemplate.strategy.notLoginView = () -> { // 简化模拟表单 String doLoginCode = "fetch(`/sso/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) " + " .then(res => res.json()) " + " .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )"; String res = "

当前客户端在 SSO-Server 认证中心尚未登录,请先登录

" + "用户:
" + "密码:
" + ""; return res; }; // 配置:登录处理函数 ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> { // 此处仅做模拟登录,真实环境应该查询数据库进行登录 if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }; } } ``` 注意:在`doLoginHandle`函数里如果要获取 name, pwd 以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取。 全局异常处理: ``` java @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ``` ### 3、application.yml配置 ``` yml # 端口 server: port: 9000 # Sa-Token 配置 sa-token: # 打印操作日志 is-log: true # SSO-模式一相关配置 (非模式一不需要配置) # cookie: # 配置 Cookie 作用域 # domain: stp.com # SSO-Server 配置 sso-server: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 应用列表:配置接入的应用信息 clients: # 应用 sso-client1:采用模式一对接 (同域、同Redis) sso-client1: client: sso-client1 allow-url: "*" # 应用 sso-client2:采用模式二对接 (跨域、同Redis) sso-client2: client: sso-client2 allow-url: "*" secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) sso-client3: # 应用名称 client: sso-client3 # 允许授权地址 allow-url: "*" # 是否接收消息推送 is-push: true # 消息推送地址 push-url: http://sa-sso-client1.com:9003/sso/pushC # 接口调用秘钥 (如果不配置则使用全局默认秘钥) secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor spring: # Redis配置 (SSO模式一和模式二使用Redis来同步会话) redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: ``` ``` properties # 端口 server.port=9000 ################## Sa-Token 配置 ################## # 打印操作日志 sa-token.is-log=true # SSO-模式一相关配置 (非模式一不需要配置) # 配置 Cookie 作用域 # sa-token.cookie.domain=stp.com # SSO-Server 配置 # Ticket有效期 (单位: 秒),默认五分钟 sa-token.sso-server.ticket-timeout=300 # 应用列表:配置接入的应用信息 # 应用 sso-client1:采用模式一对接 (同域、同Redis) sa-token.sso-server.clients.sso-client1.client=sso-client1 sa-token.sso-server.clients.sso-client1.allow-url=* # 应用 sso-client2:采用模式二对接 (跨域、同Redis) sa-token.sso-server.clients.sso-client2.client=sso-client2 sa-token.sso-server.clients.sso-client2.allow-url=* sa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client3:采用模式三对接 (跨域、跨Redis) # 应用名称 sa-token.sso-server.clients.sso-client3.client=sso-client3 # 允许授权地址 sa-token.sso-server.clients.sso-client3.allow-url=* # 是否接收消息推送 sa-token.sso-server.clients.sso-client3.is-push=true # 消息推送地址 sa-token.sso-server.clients.sso-client3.push-url=http://sa-sso-client1.com:9003/sso/pushC # 接口调用秘钥 (如果不配置则使用全局默认秘钥) sa-token.sso-server.clients.sso-client3.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # Redis配置 (SSO模式一和模式二使用Redis来同步会话) # Redis数据库索引(默认为0) spring.redis.database=1 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= ``` 注意点:`sa-token.sso-server.clients.xxx.allow-url`为了方便测试配置为`*`,线上生产环境一定要配置为详细 URL 地址 (之后的章节我们会详细阐述此配置项) ### 4、创建启动类 ``` java @SpringBootApplication public class SaSsoServerApplication { public static void main(String[] args) { SpringApplication.run(SaSsoServerApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getServerConfig()); System.out.println("统一认证登录地址:http://sa-sso-server.com:9000/sso/auth"); System.out.println("测试前需要根据官网文档修改 hosts 文件,测试账号密码:sa / 123456"); System.out.println(); } } ``` 启动项目,不出意外的情况下我们将看到如下输出: sso-server-start 访问统一授权地址(仅测试 SSO-Server 是否部署成功,暂时还不需要点击登录): - [http://localhost:9000/sso/auth](http://localhost:9000/sso/auth) sso-server-init-login.png 可以看到这个页面目前非常简陋,这是因为我们以上的代码示例,主要目标是为了带大家从零搭建一个可用的SSO认证服务端,所以就对一些不太必要的步骤做了简化。 大家可以下载运行一下官方仓库里的示例`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`,里面有制作好的登录页面: sso-server-init-login2.png 默认账号密码为:`sa / 123456`,先别着急点击登录,因为我们还没有搭建对应的 Client 端项目, 真实项目中我们一般不会直接从浏览器访问 `/sso/auth` 授权地址的,我们需要在 Client 端点击登录按钮重定向而来。 --- 现在我们先来看看除了 `/sso/auth` 统一授权地址,这个 SSO-Server 认证中心还开放了哪些API:[SSO-Server 认证中心开放接口](/sso/sso-apidoc)。 ================================================ FILE: sa-token-doc/sso/sso-type1.md ================================================ # SSO模式一 共享Cookie同步会话 如果我们的多个系统可以做到:前端同域、后端同Redis,那么便可以使用 **`[共享Cookie同步会话]`** 的方式做到单点登录。 --- ### 1、设计思路 首先我们分析一下多个系统之间,为什么无法同步登录状态? 1. 前端的 `Token` 无法在多个系统下共享。 2. 后端的 `Session` 无法在多个系统间共享。 所以单点登录第一招,就是对症下药: 1. 使用 `共享Cookie` 来解决 Token 共享问题。 2. 使用 `Redis` 来解决 Session 共享问题。 所谓共享Cookie,就是主域名Cookie在二级域名下的共享,举个例子:写在父域名`stp.com`下的Cookie,在`s1.stp.com`、`s2.stp.com`等子域名都是可以共享访问的。 而共享Redis,并不需要我们把所有项目的数据都放在同一个Redis中,Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案,详情戳:[Alone独立Redis插件](/plugin/alone-redis)。 OK,所有理论就绪,下面开始实战: ### 2、准备工作 首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: ``` url 127.0.0.1 sso.stp.com 127.0.0.1 s1.stp.com 127.0.0.1 s2.stp.com 127.0.0.1 s3.stp.com ``` 其中:`sso.stp.com`为统一认证中心地址,当用户在其它 Client 端发起登录请求时,均将其重定向至认证中心,待到登录成功之后再原路返回到 Client 端。 [Some Name](../include/include-qa.md#hostsInvalid ':include') ### 3、指定Cookie的作用域 在`sso.stp.com`访问服务器,其Cookie也只能写入到`sso.stp.com`下,为了将Cookie写入到其父级域名`stp.com`下,我们需要更改 SSO-Server 端的 yml 配置: ``` yaml sa-token: cookie: # 配置 Cookie 作用域 domain: stp.com ``` ``` properties # 配置 Cookie 作用域 sa-token.cookie.domain=stp.com ``` **这个配置原本是被注释掉的,现在将其打开。**另外我们格外需要注意: 在SSO模式一测试完毕之后,一定要将这个配置再次注释掉,因为模式一与模式二三使用不同的授权流程,这行配置会影响到我们模式二和模式三的正常运行。 ### 4、搭建 Client 端项目 > 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/`,如遇到难点可结合源码进行测试学习。 #### 4.1、引入依赖 新建项目 sa-token-demo-sso1-client,并添加以下依赖: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-sso ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis ${sa.top.version} ``` ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 插件:整合SSO implementation 'cn.dev33:sa-token-sso:${sa.top.version}' // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' // Sa-Token插件:权限缓存与业务缓存分离 implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' ``` #### 4.2、新建 Controller 控制器 ``` java /** * Sa-Token-SSO Client端 Controller * @author click33 */ @RestController public class SsoClientController { // SSO-Client端:首页 @RequestMapping("/") public String index(HttpServletRequest request) { String url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) ); SaSsoClientConfig cfg = SaSsoManager.getClientConfig(); String str = "

Sa-Token SSO-Client 应用端 (模式一)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "注销 " + "

"; return str; } // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ``` #### 4.3、application.yml 配置 ``` yaml # 端口 server: port: 9001 # Sa-Token 配置 sa-token: # SSO-相关配置 sso-client: # client 标识 client: sso-client1 # SSO-Server端主机地址 server-url: http://sso.stp.com:9000 # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 alone-redis: # Redis数据库索引 database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s ``` ``` properties # 端口 server.port=9001 ######### Sa-Token 配置 ######### # client 标识 sa-token.sso-client.client=sso-client1 # SSO-Server端主机地址 sa-token.sso-client.server-url=http://sso.stp.com:9000 # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 # Redis数据库索引 sa-token.alone-redis.database=1 # Redis服务器地址 sa-token.alone-redis.host=127.0.0.1 # Redis服务器连接端口 sa-token.alone-redis.port=6379 # Redis服务器连接密码(默认为空) sa-token.alone-redis.password= # 连接超时时间 sa-token.alone-redis.timeout=10s ``` #### 4.4、启动类 ``` java /** * SSO模式一,Client端 Demo */ @SpringBootApplication public class SaSso1ClientApplication { public static void main(String[] args) { SpringApplication.run(SaSso1ClientApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://s1.stp.com:9001"); System.out.println("测试访问应用端二: http://s2.stp.com:9001"); System.out.println("测试访问应用端三: http://s3.stp.com:9001"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); } } ``` ### 5、访问测试 启动项目,依次访问三个应用端: - [http://s1.stp.com:9001/](http://s1.stp.com:9001/) - [http://s2.stp.com:9001/](http://s2.stp.com:9001/) - [http://s3.stp.com:9001/](http://s3.stp.com:9001/) 均返回: sso1--index.png 然后点击登录,被重定向至SSO认证中心: sso1--login-page2.png 我们登录之后,然后刷新页面: sso1-login-ok.png 刷新另外两个Client端,均显示已登录 sso1-login-ok2.png 测试完成 ### 6、跨域模式下的解决方案 如上,我们使用简单的步骤实现了同域下的单点登录,聪明如你😏,马上想到了这种模式有着一个不小的限制: > [!TIP| style:callout] > 所有子系统的域名,必须同属一个父级域名 如果我们的子系统在完全不同的域名下,我们又该怎么完成单点登录功能呢? 且往下看,[SSO模式二:URL重定向传播会话](/sso/sso-type2) ================================================ FILE: sa-token-doc/sso/sso-type2.md ================================================ # SSO模式二 URL重定向传播会话 如果我们的多个系统:部署在不同的域名之下,但是后端可以连接同一个Redis,那么便可以使用 **`[URL重定向传播会话]`** 的方式做到单点登录。 ### 1、设计思路 首先我们再次复习一下,多个系统之间为什么无法同步登录状态? 1. 前端的`Token`无法在多个系统下共享。 2. 后端的`Session`无法在多个系统间共享。 关于第二点,我们已在 "SSO模式一" 章节中阐述,使用 [Alone独立Redis插件](/plugin/alone-redis) 做到权限缓存直连 SSO-Redis 数据中心,在此不再赘述。 而第一点,才是我们解决问题的关键所在,在跨域模式下,意味着 "共享Cookie方案" 的失效,我们必须采用一种新的方案来传递Token。 1. 用户在 子系统 点击 `[登录]` 按钮。 2. 用户跳转到子系统登录接口 `/sso/login`,并携带 `back参数` 记录初始页面URL。 - 形如:`http://{sso-client}/sso/login?back=xxx` 3. 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带`redirect参数`记录子系统的登录页URL。 - 形如:`http://{sso-server}/sso/auth?redirect=xxx?back=xxx` 4. 用户进入了 SSO认证中心 的登录页面,开始登录。 5. 用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口`/sso/login`,并携带`ticket码`参数。 - 形如:`http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx` 6. 子系统根据 `ticket码` 从 `SSO-Redis` 中获取账号id,并在子系统登录此账号会话。 7. 子系统将用户再次重定向至最初始的 `back` 页面。 整个过程,除了第四步用户在SSO认证中心登录时会被打断,其余过程均是自动化的,当用户在另一个子系统再次点击`[登录]`按钮,由于此用户在SSO认证中心已有会话存在, 所以第四步也将自动化,也就是单点登录的最终目的 —— 一次登录,处处通行。 下面我们按照步骤依次完成上述过程: ### 2、准备工作 首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`,添加以下IP映射,方便我们进行测试: ``` url 127.0.0.1 sa-sso-server.com 127.0.0.1 sa-sso-client1.com 127.0.0.1 sa-sso-client2.com 127.0.0.1 sa-sso-client3.com ``` [Some Name](../include/include-qa.md#hostsInvalid ':include') ### 3、搭建 Client 端项目 > [!TIP| label:demo | style:callout] > 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/`,如遇到难点可结合源码进行测试学习 #### 3.1、去除 SSO-Server 的 Cookie 作用域配置 在SSO模式一章节中我们打开了配置: ``` yaml sa-token: cookie: # 配置 Cookie 作用域 domain: stp.com ``` ``` properties # 配置 Cookie 作用域 sa-token.cookie.domain=stp.com ``` 此为模式一专属配置,现在我们将其注释掉**(一定要注释掉!)** #### 3.2、创建 SSO-Client 端项目 创建一个 SpringBoot 项目 `sa-token-demo-sso2-client`,引入依赖: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} cn.dev33 sa-token-sso ${sa.top.version} cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 cn.dev33 sa-token-alone-redis ${sa.top.version} cn.dev33 sa-token-forest ${sa-token.version} ``` ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' // Sa-Token 插件:整合SSO implementation 'cn.dev33:sa-token-sso:${sa.top.version}' // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' implementation 'org.apache.commons:commons-pool2' // Sa-Token插件:权限缓存与业务缓存分离 implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' // Sa-Token插件:整合 Forest 请求工具 implementation 'cn.dev33:sa-token-forest:${sa.top.version}' ``` #### 3.3、创建 SSO-Client 端认证接口 同 SSO-Server 一样,Sa-Token 为 SSO-Client 端所需代码也提供了完整的封装,你只需提供一个访问入口,接入 Sa-Token 的方法即可。 ``` java /** * Sa-Token-SSO Client端 Controller */ @RestController public class SsoClientController { // 首页 @RequestMapping("/") public String index() { String str = "

Sa-Token SSO-Client 应用端 (模式二)

" + "

当前会话是否登录:" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")

" + "

" + "登录 - " + "单应用注销 - " + "全端注销 - " + "账号资料" + "

"; return str; } /* * SSO-Client端:处理所有SSO相关请求 * http://{host}:{port}/sso/login -- Client 端登录地址 * http://{host}:{port}/sso/logout -- Client 端注销地址(isSlo=true时打开) * http://{host}:{port}/sso/pushC -- Client 端接收消息推送地址 */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoClientProcessor.instance.dister(); } // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { } // 当前应用独自注销 (不退出其它应用) @RequestMapping("/sso/logoutByAlone") public Object logoutByAlone() { StpUtil.logout(); return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse()); } } ``` 全局异常处理: ``` java @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ``` ##### 3.4、配置SSO认证中心地址 你需要在 `application.yml` 配置如下信息: ``` yaml # 端口 server: port: 9002 # sa-token配置 sa-token: # 打印操作日志 is-log: true # SSO-相关配置 sso-client: # 应用标识 client: sso-client2 # SSO-Server 端主机地址 server-url: http://sa-sso-server.com:9000 # API 接口调用秘钥 (单点注销时会用到) secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 alone-redis: # Redis数据库索引 (默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s ``` ``` properties # 端口 server.port=9002 ######### Sa-Token 配置 ######### # 打印操作日志 sa-token.is-log=true # 应用标识 sa-token.sso-client.client=sso-client2 # SSO-Server端 统一认证地址 sa-token.sso-client.server-url=http://sa-sso-server.com:9000 # API 接口调用秘钥 (单点注销时会用到) sa-token.sso-client.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Sa-Token 单独使用的Redis连接(此处需要和 SSO-Server 端连接同一个 Redis) # 注:使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖 # Redis数据库索引 sa-token.alone-redis.database=1 # Redis服务器地址 sa-token.alone-redis.host=127.0.0.1 # Redis服务器连接端口 sa-token.alone-redis.port=6379 # Redis服务器连接密码(默认为空) sa-token.alone-redis.password= # 连接超时时间 sa-token.alone-redis.timeout=10s ``` 注意点:`sa-token.alone-redis` 的配置需要和SSO-Server端连接同一个Redis**(database 值也要一样!database 值也要一样!database 值也要一样!重说三!)** #### 3.5、写启动类 ``` java @SpringBootApplication public class SaSso2ClientApplication { public static void main(String[] args) { SpringApplication.run(SaSso2ClientApplication.class, args); System.out.println(); System.out.println("---------------------- Sa-Token SSO 模式二 Client 端启动成功 ----------------------"); System.out.println("配置信息:" + SaSsoManager.getClientConfig()); System.out.println("测试访问应用端一: http://sa-sso-client1.com:9002"); System.out.println("测试访问应用端二: http://sa-sso-client2.com:9002"); System.out.println("测试访问应用端三: http://sa-sso-client3.com:9002"); System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456"); System.out.println(); } } ``` 启动项目 ### 4、测试访问 (1) 依次启动 `SSO-Server` 与 `SSO-Client`,然后从浏览器访问:[http://sa-sso-client1.com:9002/](http://sa-sso-client1.com:9002/) (注:先前版本文档测试demo端口号为9001,后为了方便区分三种模式改为了9002,因此出现文字描述与截图端口号不一致情况,请注意甄别,后不再赘述) sso-client-index.png (2) 首次打开,提示当前未登录,我们点击 **`登录`** 按钮,页面会被重定向到登录中心 sso-server-auth.png (3) SSO-Server提示我们在认证中心尚未登录,我们点击 **`登录`** 按钮进行模拟登录 (4) SSO-Server认证中心登录成功,系统重定向回 client sso-client-index-ok.png (5) 页面被重定向至`Client`端首页,并提示登录成功,至此,`Client1`应用已单点登录成功! (6) 我们再次访问`Client2`:[http://sa-sso-client2.com:9002/](http://sa-sso-client2.com:9002/) sso-client2-index.png (7) 提示未登录,我们点击 **`登录`** 按钮,会直接提示登录成功 sso-client2-index-ok.png (8) 同样的方式,我们打开`Client3`,也可以直接登录成功:[http://sa-sso-client3.com:9002/](http://sa-sso-client3.com:9002/) sso-client3-index-ok.png 至此,测试完毕! 可以看出,除了在`Client1`端我们需要手动登录一次之外,在`Client2端`和`Client3端`都是可以无需再次认证,直接登录成功的。 我们可以通过 F12控制台 Network 跟踪整个过程 sso-genzong ### 5、跨 Redis 的单点登录 以上流程解决了跨域模式下的单点登录,但是后端仍然采用了共享Redis来同步会话,如果我们的架构设计中Client端与Server端无法共享Redis,又该怎么完成单点登录? 这就要采用模式三了,且往下看:[SSO模式三:Http请求获取会话](/sso/sso-type3) ================================================ FILE: sa-token-doc/sso/sso-type3.md ================================================ # SSO模式三 Http请求获取会话 如果既无法做到前端同域,也无法做到后端同Redis,那么可以使用模式三完成单点登录 > [!WARNING| label:小提示] > 阅读本篇之前请务必先熟读SSO模式二!因为模式三仅仅属于模式二的一个特殊场景,熟读模式二有助于您快速理解本章内容 ### 1、问题分析 我们先来分析一下,当后端不使用共享 Redis 时,会对架构产生哪些影响: 1. sso-client 端无法直连 Redis 校验 ticket,取出账号id。 2. sso-client 端无法与 sso-server 端共用一套会话,需要自行维护子会话。 3. 由于不是一套会话,所以无法“一次注销,全端下线”,需要额外编写代码完成单点注销。 所以模式三的主要目标:也就是在 模式二的基础上 解决上述 三个难题 > [!TIP| label:demo | style:callout] > 模式三的 Demo 示例地址:`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/` > [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client),如遇难点可参考示例 ### 2、在Client 端更改 Ticket 校验方式 如果想要更直观的感受模式二与模式三的差距,可以把前面章节创建的模式二 demo 代码复制一份,在新复制的项目上继续更改来测试模式三。 #### 2.1、去除 Alone-Redis 依赖 模式三要求 sso-client 与 sso-server 连接不同的 redis,所以此处没有必要再引入 sa-token-alone-redis 机制,可以去除相关依赖: ``` xml cn.dev33 sa-token-alone-redis ${sa.top.version} ``` ``` gradle // Sa-Token插件:权限缓存与业务缓存分离 implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}' ``` #### 2.2、SSO-Client 端更改配置 更改 `application.yml` : ``` yaml # 端口 server: port: 9003 # sa-token配置 sa-token: # 打印操作日志 is-log: true # sso-client 相关配置 sso-client: # 应用标识 client: sso-client3 # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 使用 Http 请求校验 ticket (模式三) is-http: true # API 接口调用秘钥 secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor spring: # 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) redis: # Redis数据库索引 database: 3 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间 timeout: 10s ``` ``` properties # 端口 server.port=9003 # sa-token配置 # 打印操作日志 sa-token.is-log=true # sso-client 相关配置 # 应用标识 sa-token.sso-client.client=sso-client3 # sso-server 端主机地址 sa-token.sso-client.server-url=http://sa-sso-server.com:9000 # 使用 Http 请求校验 ticket (模式三) sa-token.sso-client.is-http=true # API 接口调用秘钥 sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 配置 Redis 连接 (此处与 SSO-Server 端连接不同的 Redis) # Redis数据库索引 spring.redis.database=3 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接超时时间 spring.redis.timeout=10s ``` #### 2.3、测试 重启项目,访问测试: - [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/) - [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/) - [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/) > [!WARNING| label:小提示] > 注:如果已测试运行模式二,可先将Redis中的数据清空,以防旧数据对测试造成干扰 测试步骤同模式二,不再赘述。 ### 3、后记 当我们熟读三种模式的单点登录之后,其实不难发现:所谓单点登录,其本质就是多个系统之间的会话共享。 当我们理解这一点之后,三种模式的工作原理也浮出水面: - 模式一:采用共享 Cookie 来做到前端 Token 的共享,从而达到后端的 Session 会话共享。 - 模式二:采用 URL 重定向,以 ticket 码为授权中介,做到多个系统间的会话传播。 - 模式三:采用 Http 请求主动查询会话,做到 Client 端与 Server 端的会话同步。 ================================================ FILE: sa-token-doc/sso/user-data-sync.md ================================================ # 用户数据同步 / 迁移 本篇文章仅提供架构设计的略微参考,真实场景中每个公司的架构设计都是千差万别的,一套设计理论未必能够适应所有公司的项目。 所以如果你觉着本篇文章的设计理念不能契合你公司的需求,请以你公司的原设计为准。 --- ### 数据同步需求 在前面的不同架构 SSO 对接示例中,我们均假设了一个前提: -- _所有的 sso-client 只负责业务操作,不存储 user 数据,user 数据全部来源于 sso-server,包括登录认证也都是基于 sso-server 里的 user 账号进行校验操作。_ 这种架构比较简洁、清晰,是一种理想化的 SSO 架构模型。 然而更多时候,我们遇到的实际情况是: -- _公司已经有了 N 多个系统,每个系统都有自己独立的一套账号认证体系,现在老板要让这 N 个毫无关系的系统集成单点登录。_ 要完成这种需求,首先你得考虑两个问题: 1. 问题一:sso-client 需不需要保留 user 数据。 - sso-client 不涉及 user 信息连表查的业务,就可以不保留 user 信息。 - sso-client 涉及 user 信息连表查业务,就需要在 sso-client 保留 user 数据。 2. 问题二:如果保留的话,是和 sso-server 强同步,还是弱同步。 - 强同步就是指 sso-client 的 user 数据和 sso-server 的 user 数据,字段值必须保持一致。比如说:一个用户在server端昵称修改为“张三”,那么在 client 端也要实时同步修改。 - 弱同步就是指两边可以各改各的。比如说:一个用户 server 端修改了昵称为 “张三”,他在 client 端依然可以昵称为 “李四”。 由此可大致分为三种设计方案: | 方案序号 | 方案名称 | 简单说明 | 适用系统 | | :-------- | :-------- | :-------- | :-------- | | 方案一 | 统一迁移 | 统一把用户数据迁移到 sso-server 认证中心再进行对接 | 比较简单的系统,业务上不需要 user 信息连表查 | | 方案二 | 实时同步 | 按照一定的规则,使 sso-client 和 sso-server 保持 user 信息实时同步 | 一般业务上需要 user 信息连表查的系统 | | 方案三 | 字段关联 | 不同步,但找一个关键字段,将 sso-client 和 sso-server 的 user 账号进行关联起来 | sso-client 不打算过分依赖 sso-server 的 user 数据,只是想借助 sso-server 完成一下统一登录 | 下面逐一拆解三种方案具体实现。 ### 1、方案一:统一迁移 对接工作开发前,sso-client 的 user 数据完全迁移到 sso-server 中,且自身不再保留 user 数据,只进行业务数据处理操作。 这种方案其实不必过多讲解,因为数据完成迁移后整个架构就转化为了上述的“理想化SSO模型”,后续对接也比较方便。 迁移方式可以选择数据库同步工具,或者手写代码从 sso-client 库读取数据然后 insert 到 sso-server 库中。 这并非此文探讨的重点,因此不再过多赘述了。 - 方案优点:架构简洁明了,SSO 登录、注销对接起来非常方便 - 方案缺点:sso-client 不存储 user 信息,因此业务上需要连表查询 user 信息的地方会比较麻烦(例如:拉取帖子列表时需要附加显示用户头像和昵称信息) 方案适用范围:适合业务比较简单,不涉及 user资料连表查业务 的子系统。 ### 2、方案二:实时同步 首先,对接前,数据还是要迁移的,只不过迁移后 sso-client 的 user 数据不删除掉,依然保留。 然后在项目运行阶段,每当 sso-server 的 user 数据发生变动时(增删改),逐一向每个 sso-client 推送变化信息。使 sso-client 与 sso-server 的 user 数据保持强同步。 你可能会有疑问,那 sso-client 的 user 数据发生变动时,要不要向 sso-server 推送信息,我的建议是:尽量不要让 sso-client 的 user 信息主动发生变化。 举个例子: > 公司有电商、论坛、短视频 3 个子系统 + 1 个 sso-server 认证中心,无论用户从哪个子系统点击 “修改我的资料” 按钮时,都应该统一跳转到 sso-server 认证中心进行修改, > 修改完毕后再由 sso-server 将 user 信息推送至 3 个子系统。以此来保证 4 个系统间的 user 信息同步。 - 方案优点:sso-client 存储了 user 信息,可以比较方便的进行 user 连表查操作。 - 方案缺点:sso-server 与 sso-client 的 user 数据同步功能不算简单,开发起来可能要耗费一段不小的工期。 方案适用范围:一般业务上需要 user 信息连表查的子系统都适合。 ### 3、方案三:字段关联 如果子系统不需要和 sso-server 做到信息强同步,可以使用字段关联法做到账户关联进行登录。 举个例子:公司有三个子系统,电商、论坛、短视频。同一个用户可以在这三个子系统以及 sso-server 认证中心拥有不同的昵称、头像等信息,互不干扰。 例如,在 sso-server 认证中心里,张三的数据库信息为: | id | username | avatar | password | age | email | | :-------- | :-------- | :-------- | :-------- | :-------- | :-------- | | 10001 | ... | ... | ... | ... | ... | | 10002 | 小明 | cat.jpg | 123456 | 18 | `23397@xx.com` | | 10003 | ... | ... | ... | ... | ... | 在电商系统里中,张三的数据库信息为: | id | name | avatar | money | email | | :-------- | :-------- | :-------- | :-------- | :-------- | | 100334 | ... | ... | ... | ... | | 100335 | 二明 | dog.jpg | 1000 | `23397@xx.com` | | 100336 | ... | ... | ... | ... | 这里的关键点在于,虽然用户 “张三” 在每个系统里的资料都是不同的,但是程序要想办法将它们识别为同一个用户, 要做到这一点,就需要我们准备一个关键字段将信息打通串联起来。例如表中的 “邮箱” 信息可以作为这个“关联字段”。 (注:此处仅展示使用邮箱作为关联字段的操作,实际上除了邮箱以外,手机号、身份证号等具有唯一性的信息都可以作为关联字段) 首先,在 sso-server 端,我们需要重写一下 `checkTicketAppendData` 函数,使其在 “校验 ticket 返回 loginId” 时,追加返回 email 字段。 ``` java // 配置SSO相关参数 @Autowired private void configSso(SaSsoServerTemplate ssoServerTemplate) { // 其它配置 ... // 配置:Ticket校验函数 ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> { System.out.println("-------- 追加返回信息到 sso-client --------"); // 在校验 ticket 后,给 sso-client 端追加返回信息的函数 SysUser user = sysUserMapper.getById(loginId); result.set("email", user.getEmail()); // result.set("user", user); // 你也可以将整个user 对象的信息都返回到 sso-client,自由决定 return result; }; } ``` 在 sso-client 端,重写 ticketResultHandle 函数,根据 sso-server 返回的信息查询本地 user 信息并登录: ``` java // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 其它配置 ... // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------"); System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId); System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒"); System.out.println("此账号返回的 email 信息:" + ctr.result.get("email")); // 模拟代码: // 根据 email 字段找到此账号在本系统对应的 user 信息 String email = (String) ctr.result.get("email"); SysUser user = sysUserMapper.getByEmail(email); // 如果找不到,说明是首次登录本系统的新用户,需要自动注册一个新账号给他 if(user == null) { // 涉及到数据库操作,此处仅做模拟代码 // 1、构建 user 信息 // 2、插入到数据库 // 3、查询出最新刚插入的这条 user 信息 user = sysUserMapper.getByEmail(email); } // 进行登录 StpUtil.login(user.getId(), ctr.remainSessionTimeout); StpUtil.getSession().set("user", user); // 一切工作完毕,重定向回 back 页面 return SaHolder.getResponse().redirect(back); }; } ``` 至此完毕。 - 方案优点: - 1、sso-client 不需要和 sso-server 保持信息强同步,实现起来不复杂,架构也比较清晰易维护。 - 2、同一个用户的信息,sso-client 可以和 sso-client 保持不同,各自维护各自的,互不干扰。 - 方案缺点:好像没啥缺点,除非你觉着上述的第2条优点属于缺点。 方案适用范围:在 user 信息方面不打算过分依赖 sso-server 的系统,希望自己维护自己的 user 信息,只是想借助 sso-server 完成一下统一登录。 ### 4、扩展:没有关联字段 如果我们的子系统 user 表没有邮箱、手机号等唯一性字段和 sso-server 的 user 表进行关联,该怎么办呢? 没有字段,那就创造个字段,例如: | id | name | avatar | age | center_id | | :-------- | :-------- | :-------- | :-------- | :-------- | | 205421 | ... | ... | ... | ... | | 205422 | 小风筝 | dog.jpg | 21 | 10002 | | 205423 | ... | ... | ... | ... | 如上表所示,我们可以在子系统的 user 表新增一列 `center_id`,记录这个用户在认证中心所属的账号id。然后在登录时根据这个 `center_id` 来查找相应的用户。 由于 sso-server 端默认就是会返回 loginId 参数的,因此在 sso-server 端不必再重写一下 `checkTicketAppendData` 函数来追加返回信息了, 我们只需要重写 sso-client 端的 `ticketResultHandle` 函数即可: ``` java // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 其它配置 ... // 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> { System.out.println("--------- 自定义 ticket 校验结果处理函数 ---------"); System.out.println("此账号在 sso-server 的 userId:" + ctr.loginId); System.out.println("此账号在 sso-server 会话剩余有效期:" + ctr.remainSessionTimeout + " 秒"); // 模拟代码: // 根据 center_id 字段找到此账号在本系统对应的 user 信息 long centerId = SaFoxUtil.getValueByType(ctr.loginId, long.class); SysUser user = sysUserMapper.getByCenterId(centerId); // 如果找不到,说明是首次登录本系统的新用户,需要自动注册一个新账号给他 if(user == null) { // 涉及到数据库操作,此处仅做模拟 // 1、构建 user 信息 // 2、插入到数据库 // 3、查询出最新刚插入的这条 user 信息 user = sysUserMapper.getByCenterId(userId); } // 进行登录 // 注意此处需要使用 centerId 进行登录,否则该账号将无法正常完成单点注销功能 StpUtil.login(centerId, ctr.remainSessionTimeout); StpUtil.getSession().set("user", user); // 一切工作完毕,重定向回 back 页面 return SaHolder.getResponse().redirect(back); }; } ``` 至此完毕。 > [!INFO| label:提问:按照方案三,一个用户登录过程中,sso-server 和 sso-client 对这个用户账号的完整处理步骤是怎样的?] > 1. 用户进入 sso-client 登录页面,点击上面的 [ 使用 xx 认证中心快捷登录 ] 按钮,浏览器跳转至 sso-server 认证中心。 > 2. 如果用户在 sso-server 有账号,则直接登录,如果没有,则注册账号并登录。 > 3. sso-server 重定向回 sso-client 端,并携带 ticket 参数。 > 4. sso-client 获取 ticket 参数,并解析出 center_id 值。 > 5. 根据 center_id 从 user 表查数据: > - 5.1 查的到,证明有账号,直接登录。 > - 5.2 查不到,证明无账号,程序自动给他添加一条 user 账号,并登录。 > 6. 登录完成。 ### 5、解决模式三下,loginId 与 centerId 不一致的问题 按照字段关联法登录之后,如果一个用户在本地应用端的 userId 和认证中心端的 userId 不一致,则可能发生单点注销失败的情况: 假设,一个用户在认证中心的 userId=10002,在本地应用端的 userId=100335, 则在本地应用端发起单点注销时,其传递的 loginId 值是 100335,在 sso-server 是找不到 userId=100335 用户的,自然无法单点注销成功。 解决方案是在本地应用端重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射: ``` java @RestController public class SsoClientController { // 配置SSO相关参数 @Autowired private void configSso(SaSsoClientTemplate ssoClientTemplate) { // 重写 loginId 与 centerId 转换策略函数,做到本地应用 userId 与认证中心 userId 的互相映射 // 将 centerId 转换为 loginId 的函数 ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> { return "Stu" + centerId; }; // 将 loginId 转换为 centerId 的函数 ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> { return loginId.toString().substring(3); }; } } ``` 如上代码,演示了应用本地 loginId 与认证中心 centerId 不一致时的转换写法(演示的逻辑为添加和裁剪指定前缀),真实项目中,应该根据用户表存储的映射关系来做查询返回。 值得注意的是,在重写转换策略后,我们在消息推送时也应该严格按照转换写法提交 loginId 参数,例如: ``` java // 查询我的账号信息:sso-client 前端 -> sso-center 后端 -> sso-server 后端 @RequestMapping("/sso/myInfo") public Object myInfo() { // 如果尚未登录 if( ! StpUtil.isLogin()) { return "尚未登录,无法获取"; } // 原写法:直接调用 StpUtil.getLoginId() 当做 centerId 来提交 // Object centerId = StpUtil.getLoginId(); // 新写法:获取本地 loginId 对应的认证中心 centerId Object centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId()); // 推送消息 SaSsoMessage message = new SaSsoMessage(); message.setType("userinfo"); message.set("loginId", centerId); SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message); // 返回给前端 return result; } ``` ================================================ FILE: sa-token-doc/start/download.md ================================================ # 其它环境引入 Sa-Token 的示例 目前已实现的对接框架综合 ------ ## Maven依赖 根据不同基础框架引入不同的 Sa-Token 依赖: 如果你使用的框架基于 ServletAPI 构建( SpringMVC、SpringBoot等 ),请引入此包 ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 注:如果你使用的框架基于 Reactor 模型构建(WebFlux、SpringCloud Gateway 等),请引入此包 ``` xml cn.dev33 sa-token-reactor-spring-boot-starter ${sa.top.version} ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 参考:[Solon官网](https://solon.noear.org/) ``` xml cn.dev33 sa-token-solon-plugin ${sa.top.version} ``` 参考:[JFinal官网](https://jfinal.com/) ``` xml cn.dev33 sa-token-jfinal-plugin ${sa.top.version} ``` 参考:[Jboot官网](http://www.jboot.com.cn/) ``` xml cn.dev33 sa-token-jboot-plugin ${sa.top.version} ``` 参考:[LoveQQ-Framework](https://gitee.com/kfyty725/loveqq-framework) ``` xml cn.dev33 sa-token-loveqq-boot-starter ${sa.top.version} ``` 参考:[quarkus-sa-token](https://github.com/quarkiverse/quarkus-sa-token) ``` xml io.quarkiverse.satoken quarkus-satoken-resteasy 1.30.0 ``` 注:如果你的项目没有使用Spring,但是Web框架是基于 ServletAPI 规范的,可以引入此包 ``` xml cn.dev33 sa-token-servlet ${sa.top.version} ``` 引入此依赖需要自定义 SaTokenContext 实现,参考:[自定义 SaTokenContext 指南](/fun/sa-token-context) 注:如果你的项目既没有使用 SpringMVC、WebFlux,也不是基于 ServletAPI 规范,那么可以引入core核心包 ``` xml cn.dev33 sa-token-core ${sa.top.version} ``` 引入此依赖需要自定义 SaTokenContext 实现,参考:[自定义 SaTokenContext 指南](/fun/sa-token-context) ## Gradle依赖 ``` gradle implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 ``` gradle implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ``` gradle implementation 'cn.dev33:sa-token-solon-plugin:${sa.top.version}' ``` ``` gradle implementation 'cn.dev33:sa-token-jfinal-plugin:${sa.top.version}' ``` ``` gradle implementation 'cn.dev33:sa-token-jboot-plugin:${sa.top.version}' ``` ``` gradle implementation 'cn.dev33:sa-token-loveqq-boot-starter:${sa.top.version}' ``` ``` gradle implementation 'io.quarkiverse.satoken:quarkus-satoken-resteasy:1.30.0' ``` ``` gradle implementation 'cn.dev33:sa-token-servlet:${sa.top.version}' ``` ``` gradle implementation 'cn.dev33:sa-token-core:${sa.top.version}' ``` 注:JDK版本:`v1.8+`,SpringBoot:`建议2.0以上` ## 测试版 更多内测版本了解:[Sa-Token 最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md) Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull) ## jar包下载 [点击下载:sa-token-1.6.0.jar](https://pan.quark.cn/s/85e4d75f500c) 注:当前仅提供 `v1.6.0` 版本jar包下载,更多版本请前往 maven 中央仓库获取,[直达链接](https://search.maven.org/search?q=sa-token) ## 获取源码 如果你想深入了解 Sa-Token,你可以通过`Gitee`或者`GitHub`来获取源码 (**学习测试请拉取 master 分支**,dev为正在开发的分支,有很多特性并不稳定) - **Gitee**地址:[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token) - **GitHub**地址:[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token) - 开源不易,求鼓励,点个`star`吧 - 源码目录介绍: - [仓库目录](/arch/dir-intro) ## 运行示例 - 1、下载代码(学习测试用 master 分支)。 - 2、从根目录导入项目。 - 3、选择相应的示例添加为 Maven 项目,打开 XxxApplication.java 运行。 运行示例 ================================================ FILE: sa-token-doc/start/example.md ================================================ # SpringBoot 集成 Sa-Token 示例 本篇带你从零开始集成 Sa-Token,只需简单 5 步,你就可以快速熟悉框架的使用姿势。 整合示例在官方仓库的`/sa-token-demo/sa-token-demo-springboot`文件夹下,如遇到难点可结合源码进行学习测试。 --- ### 1、创建项目 在 IDE 中新建一个 SpringBoot 项目,例如:`sa-token-demo-springboot`(不会的同学请自行百度或者参考:[SpringBoot-Pure](https://gitee.com/click33/springboot-pure)) ### 2、添加依赖 在项目中添加依赖: ``` xml cn.dev33 sa-token-spring-boot-starter ${sa.top.version} ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-spring-boot4-starter`。 Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull) 更多内测版本了解:[Sa-Token最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md) ### 3、设置配置文件 你可以**零配置启动项目** ,但同时你也可以在 `application.yml` 中增加如下配置,定制性使用框架: ``` yaml server: # 端口 port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称(同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true ``` ``` properties # 端口 server.port=8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## # token 名称(同时也是 cookie 名称) sa-token.token-name=satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 sa-token.timeout=2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 sa-token.active-timeout=-1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) sa-token.is-concurrent=true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) sa-token.is-share=false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) sa-token.token-style=uuid # 是否输出操作日志 sa-token.is-log=true ``` ### 4、创建启动类 在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApplication.java`,复制以下代码: ``` java @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) throws JsonProcessingException { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); } } ``` ### 5、创建测试Controller ``` java @RestController @RequestMapping("/user/") public class UserController { // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public String doLogin(String username, String password) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登录成功"; } return "登录失败"; } // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin @RequestMapping("isLogin") public String isLogin() { return "当前会话是否登录:" + StpUtil.isLogin(); } } ``` ### 6、运行 启动代码,从浏览器依次访问上述测试接口: 运行结果 运行结果 ### 出发 通过这个示例,你已经对 Sa-Token 有了初步的了解。那么,坐稳扶好,让我们开始吧:[登录认证](/use/login-auth) ================================================ FILE: sa-token-doc/start/maven-pull.md ================================================ # Maven 依赖一直无法拉取成功? --- 方法1、先重启一下试试。 --- 方法2、可能依赖还没有下载完毕,请看一下编辑器下方是否有正在构建项目的进度条。 --- 方法3、可能是网络不太稳定,导致本地下载了一些残碎文件,先把这些残碎文件删除了,再重新构建项目试试。 一般本地的文件都在 `C:\Users\你的电脑用户名\.m2\repository\cn\dev33`,打开后,把文件全部删除。注:如果你修改过 Maven jar 下载目录,就按照你修改的来。 --- 方法4、可能你给你的 Maven 配置了阿里云镜像,而部分 jar 包无法通过阿里云镜像加载成功。 打开你的 Maven setting.xml 文件,看看有没有以下配置: ``` xml nexus-aliyun central Nexus aliyun http://maven.aliyun.com/nexus/content/groups/public ``` 如果有的话,先把它注释掉(注释掉就直连 Maven 中央仓库了),或者修改为其它的镜像,例如腾讯云的: ``` xml tencent tencent maven http://mirrors.cloud.tencent.com/nexus/repository/maven-public/ central ``` 然后重启你的代码编辑器,重新构建项目。 --- --- 方法5、如果使用的是父子Maven项目,在父项目导入该依赖后,Pom无法识别的情况: 需要先在子项目中引用该依赖,再进行重新加载。 若还是不行,可以新建先一个小的Maven项目尝试将该依赖下载后,再返回原父子项目中将该依赖导入。 --- --- 再不行的话,就加群反馈吧。 ================================================ FILE: sa-token-doc/start/new-version.md ================================================ # Sa-Token 最新版本 在线文档:[https://sa-token.cc/](https://sa-token.cc/) --- ### 正式版本 v1.45.0 正式版,可上生产: ``` xml cn.dev33 sa-token-spring-boot-starter 1.45.0 ``` Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull) --- ### 内测版本 暂无内测版本。 ================================================ FILE: sa-token-doc/start/solon-example.md ================================================ # Solon 集成 Sa-Token 示例 本篇介绍在 Solon 应用中如何集成 Sa-Token。 整合示例在官方仓库的 `/sa-token-demo/sa-token-demo-solon` 文件夹下,如遇到难点可结合源码进行学习测试。 > [!tip| label:Solon 是什么?] > Solon 是一个高效的国产应用开发框架:更快、更小、更简单。 > > - 启动快 5 ~ 10 倍; > - qps 高 2~ 3 倍; > - 运行时内存节省 1/3 ~ 1/2; > - 打包可以缩到 1/2 ~ 1/10; > - 同时支持 jdk8、jdk11、jdk17、jdk20。 > > 详情可参考:[https://solon.noear.org/](https://solon.noear.org/) --- ### 1、创建项目 在 IDE 中新建一个 Solon 项目,例如:sa-token-demo-solon (可以借助 [Solon Initializr](https://solon.noear.org/start/) 生成) ### 2、添加依赖 在项目中添加依赖: ``` xml cn.dev33 sa-token-solon-plugin ${sa.top.version} ``` ``` gradle // Sa-Token 权限认证,在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-solon-plugin:${sa.top.version}' ``` Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull) 更多内测版本了解:[Sa-Token最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md) ### 3、设置配置文件 你可以**零配置启动项目** ,但同时你也可以在 `app.yml` 中增加如下配置,定制性使用框架: ```yaml server: # 端口 port: 8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称(同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true ``` ```properties # 端口 server.port=8081 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## # token 名称(同时也是 cookie 名称) sa-token.token-name=satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 sa-token.timeout=2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 sa-token.active-timeout=-1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) sa-token.is-concurrent=true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) sa-token.is-share=false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) sa-token.token-style=uuid # 是否输出操作日志 sa-token.is-log=true ``` ### 4、创建启动类 在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApp.java`,复制以下代码: ```java @SolonMain public class SaTokenDemoApp { public static void main(String[] args) { Solon.start(SaTokenDemoApp.class, args); System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); } } ``` ### 5、创建测试Controller ```java @Mapping("/user/") @Controller public class UserController { // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 @Mapping("doLogin") public String doLogin(String username, String password) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登录成功"; } return "登录失败"; } // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin @Mapping("isLogin") public String isLogin() { return "当前会话是否登录:" + StpUtil.isLogin(); } } ``` ### 6、运行 启动代码,从浏览器依次访问上述测试接口: 运行结果 运行结果 ### 详细了解 通过这个示例,你已经对 Sa-Token 有了初步的了解,那么现在开始详细了解一下它都有哪些吧: [登录认证](/use/login-auth) (与 Springboot 处理类似) ================================================ FILE: sa-token-doc/start/webflux-example.md ================================================ # Spring WebFlux 集成 Sa-Token 示例 **Reactor** 是一种非阻塞的响应式模型,本篇将以 **WebFlux** 为例,展示 Sa-Token 与 Reactor 响应式模型框架相整合的示例, **你可以用同样方式去对接其它 Reactor 模型框架(例如 SpringCloud Gateway)** 整合示例在官方仓库的`/sa-token-demo/sa-token-demo-webflux`文件夹下,如遇到难点可结合源码进行测试学习 > [!WARNING| label:小提示 ] > WebFlux 常用于微服务网关架构中,如果您的应用基于单体架构且非 Reactor 模型,可以先跳过本章 --- ### 1、创建项目 在 IDE 中新建一个 SpringBoot 项目,例如:`sa-token-demo-webflux` ### 2、添加依赖 在项目中添加依赖: ``` xml cn.dev33 sa-token-reactor-spring-boot-starter ${sa.top.version} ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}' ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ``` gradle // Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc implementation("cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}") ``` - 如果你使用的 `SpringBoot 3.x`,请引入 `sa-token-reactor-spring-boot3-starter`。 - 如果你使用的 `SpringBoot 4.x`,请引入 `sa-token-reactor-spring-boot4-starter`。 ### 3、创建启动类 在项目中新建包 `com.pj` ,在此包内新建主类 `SaTokenDemoApplication.java`,输入以下代码: ``` java @SpringBootApplication public class SaTokenDemoApplication { public static void main(String[] args) throws JsonProcessingException { SpringApplication.run(SaTokenDemoApplication.class, args); System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); } } ``` ```kotlin @SpringBootApplication class SaTokenDemoApplication fun main(args: Array) { runApplication(*args) println(SaManager.getConfig()) } ``` ### 4、创建全局过滤器 新建`SaTokenConfigure.java`,注册 Sa-Token 的全局过滤器 ``` java /** * [Sa-Token 权限认证] 全局配置类 */ @Configuration public class SaTokenConfigure { /** * 注册 [Sa-Token全局过滤器] */ @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 指定 [拦截路由] .addInclude("/**") /* 拦截所有path */ // 指定 [放行路由] .addExclude("/favicon.ico") // 指定[认证函数]: 每次请求执行 .setAuth(obj -> { System.out.println("---------- sa全局认证"); // SaRouter.match("/test/test", () -> StpUtil.checkLogin()); }) // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError(e -> { System.out.println("---------- sa全局异常 "); return SaResult.error(e.getMessage()); }) ; } } ``` ```kotlin @Configuration class SaTokenConfigure { /** * 注册 [Sa-Token全局过滤器] */ @Bean fun saReactorFilter(): SaReactorFilter = SaReactorFilter() // 指定 [拦截路由](此处为拦截所有path) .addInclude("/**") // 指定 [放行路由] .addExclude("/favicon.ico") // 指定[认证函数]: 每次请求执行 .setAuth { println("---------- sa全局认证") // SaRouter.match("/test/test", SaFunction { StpUtil.checkLogin() }) } // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 .setError { e: Throwable -> println("---------- sa全局异常 ") SaResult.error(e.message) } } ``` 你只需要按照此格式复制代码即可,有关过滤器的详细用法,会在之后的章节详细介绍。 ### 5、创建测试Controller ``` java @RestController @RequestMapping("/user/") public class UserController { // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public String doLogin(String username, String password) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登录成功"; } return "登录失败"; } // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin @RequestMapping("isLogin") public String isLogin() { return "当前会话是否登录:" + StpUtil.isLogin(); } } ``` ```kotlin @RestController @RequestMapping("/user/") class UserController { @RequestMapping("doLogin") fun doLogin(username: String, password: String) = // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if ("zhang" == username && "123456" == password) { StpUtil.login(10001) "登录成功" } else "登录失败" @RequestMapping("isLogin") fun isLogin() = "当前会话是否登录:" + StpUtil.isLogin() } ``` ### 6、运行 启动代码,从浏览器依次访问上述测试接口: 运行结果 运行结果 **注意事项:** 更多使用示例请参考官方仓库demo ================================================ FILE: sa-token-doc/static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js ================================================ // 章节锁定插件 // 声明 docsify 插件 var docLockPlugin = function(hook, vm) { // 钩子函数:解析之前执行 hook.beforeEach(function(content) { return content; }); // 钩子函数:每次路由切换时,解析内容之后执行 hook.afterEach(function(html) { return html; }); // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 hook.doneEach(function() { isShowTanChuang(vm); }); // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 hook.ready(function() { }); // ======================================== 弹窗方法 // 检查成功后,多少天不再检查 const dl_AllowDisparity = 1000 * 60 * 60 * 24 * 30 * 1; // 1个月 // 拦截 path ,如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 const dl_exeArray = [ '/sso/*', '/oauth2/*', '/more/common-questions', '/up/*', '/micro/*', '/plugin/*' ]; // 排除 path const dl_excludeArray = [ '/sso/readme', '/oauth2/readme' ]; // 本次存储时,使用的标记 key const dl_saveKey = 'dl_saveKey'; // 判断当前是否应该弹出 function isShowTanChuang(vm) { // 非PC端不检查 // if(document.body.offsetWidth < 800) { // console.log('small screen ... wj '); // return; // } // 判断是否需要拦截 const isExe = isExePath(vm.route.path, dl_exeArray, dl_excludeArray); if(!isExe) { return; } // 判断是否近期已经判断过了 try{ const flagTime = localStorage[dl_saveKey]; if(flagTime) { // 记录 存储 的时间,和当前时间的差距 const disparity = new Date().getTime() - parseInt(flagTime); // 差距小于指定时间,不再检测 if(disparity < dl_AllowDisparity) { console.log('checked ... docLock '); return; } } }catch(e){ console.error(e); } // 本次打开页面的内存内已经弹出了的话,也不再弹了 // if(window.isYtcXsjfkasjdaaaa) { // return; // } // window.isYtcXsjfkasjdaaaa = true; // 验证成功的回调 const okFn = function() { console.log('ok 了'); localStorage.setItem(dl_saveKey, new Date().getTime() ); $('body').css({'overflow': 'auto'}); layer.msg('感谢你的支持,Sa-Token 将努力变得更加完善! ❤️ ❤️ ❤️ '); } // 点了返回的回调 const backFu = function() { $('body').css({'overflow': 'auto'}); location.href = '#/'; } // 弹窗验证 showDocLock(okFn, backFu); $('body').css({'overflow': 'hidden'}); // 弹出弹框,邀请填写 return; } // ======================================== 路径判断 /** * 判断一个路径,是否会被成功拦截,返回 true 或 false * @param {Object} path 要判断的路径,例如:/sso/apidoc * @param {Object} exeArray 要拦截的路径数组,例如:['/sso/*', '/oauth2/*', '/more/common-questions' ],如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 * @param {Object} excludeArray 要排除的路径数组,规则同上 */ function isExePath( path, exeArray, excludeArray) { // 参数验证和初始化 exeArray = exeArray || []; excludeArray = excludeArray || []; // 标准化路径,确保以 / 开头 path = normalizePath(path); // 先检查排除规则(优先级更高) for (let pattern of excludeArray) { if (matchPattern(path, pattern)) { return false; // 被排除,不拦截 } } // 再检查拦截规则 for (let pattern of exeArray) { if (matchPattern(path, pattern)) { return true; // 需要拦截 } } return false; // 默认不拦截 } /** * 标准化路径 */ function normalizePath(path) { if (!path) return '/'; if (!path.startsWith('/')) return '/' + path; return path; } /** * 增强版模式匹配 */ function matchPattern(path, pattern) { // 处理空值 if (!pattern) return false; pattern = pattern.trim(); // /** 匹配所有 if (pattern === '/**' || pattern === '**') { return true; } // 处理前置和后置通配符 if (pattern === '*' || pattern === '/*') { return true; } // 精确匹配 if (!pattern.includes('*')) { return path === pattern || path === normalizePath(pattern); } // 转换模式为正则表达式 const regexStr = pattern // 转义正则特殊字符 .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // 处理 ** 通配符(匹配多级目录) .replace(/\/\*\*/g, '(/.*)?') // 处理 * 通配符(匹配单级目录) .replace(/\*/g, '[^/]*') // 确保匹配完整路径 .replace(/^\//, '^/') .replace(/$/, '$'); try { const regex = new RegExp(regexStr); return regex.test(path); } catch (e) { console.error(`Invalid pattern: ${pattern}`, e); return false; } } } // =========================== AI 生成的弹窗代码 function initTanChuangFun() { // 配置项 const CONFIG = { correctPassword: 'sa-token yyds', // 正确密码 gzhImageUrl: './big-file/contact/lykj-gzh.jpg', wechatImageUrl: './big-file/doc/zong/doc-lock-pre-wx.png' }; // 弹窗HTML模板 const modalHTML = `
放大图片
`; // 初始化变量 let passwordModal, passwordInput, errorMessage, verifyBtn, backBtn; let imageOverlay, enlargedImage; let okCallFn = null; let backCallFn = null; /** * 初始化弹窗 * 将弹窗HTML插入到页面中,并绑定事件 */ function initModal() { // 插入弹窗HTML到页面 document.body.insertAdjacentHTML('beforeend', modalHTML); // 获取DOM元素 passwordModal = document.getElementById('passwordModal'); passwordInput = document.getElementById('passwordInput'); errorMessage = document.getElementById('errorMessage'); verifyBtn = document.getElementById('verifyBtn'); backBtn = document.getElementById('backBtn'); imageOverlay = document.getElementById('imageOverlay'); enlargedImage = document.getElementById('enlargedImage'); // 绑定事件 bindEvents(); } /** * 绑定所有事件 */ function bindEvents() { // 触发按钮点击事件 // document.getElementById('accessBtn').addEventListener('click', openModal); // 验证按钮点击事件 verifyBtn.addEventListener('click', validatePassword); // 返回按钮点击事件 backBtn.addEventListener('click', function(){ closeModal(); backCallFn(); }); // 输入框回车事件 passwordInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { validatePassword(); } }); // 只在非移动端绑定图片点击事件 if (!isMobileDevice()) { // 图片点击放大事件 document.querySelectorAll('.qr-image').forEach(img => { img.addEventListener('click', function() { enlargeImage(this.src); }); }); // 放大图片关闭事件 imageOverlay.addEventListener('click', function(e) { if (e.target === this || e.target === enlargedImage) { closeImageOverlay(); } }); // ESC键关闭放大图片 document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && imageOverlay.classList.contains('active')) { closeImageOverlay(); } }); } } /** * 检测是否为移动设备 * @returns {boolean} 是否为移动设备 */ function isMobileDevice() { return window.innerWidth <= 768; } /** * 打开密码弹窗 */ function openModal() { passwordModal.classList.add('active'); passwordInput.focus(); errorMessage.classList.remove('show'); passwordInput.value = ''; } /** * 关闭密码弹窗 */ function closeModal() { passwordModal.classList.remove('active'); } /** * 密码验证函数 * 宽松验证策略:允许左右空格,中间空格可省略 */ function validatePassword() { let enteredPassword = passwordInput.value.trim(); // 去除左右空格 // 标准化:移除所有空格 const normalizedEntered = enteredPassword.replace(/\s+/g, ''); const normalizedCorrect = CONFIG.correctPassword.replace(/\s+/g, ''); if (normalizedEntered === normalizedCorrect) { // 密码正确,解锁章节 unlockChapter(); } else { // 密码错误,显示错误信息 showError(); } } /** * 显示密码错误提示 */ function showError() { errorMessage.classList.add('show'); passwordInput.value = ''; passwordInput.focus(); // 添加抖动效果 passwordInput.classList.add('shake'); setTimeout(() => { passwordInput.classList.remove('shake'); }, 500); } /** * 解锁章节 */ function unlockChapter() { closeModal(); okCallFn(); // // 更新章节内容 // const lockedSection = document.querySelector('.locked-section'); // const tocLockedItems = document.querySelectorAll('.toc a.locked'); // // 更新章节显示 // lockedSection.innerHTML = ` //

章节已解锁

//

感谢您加入我们的社区!现在您可以查看高级配置指南的全部内容。

//
//

高级配置内容示例:

//

1. 自定义插件开发:详细讲解如何为项目开发自定义插件,包括插件结构、API接口和最佳实践。

//

2. 性能调优指南:深入分析项目性能瓶颈,并提供多种优化方案和调优技巧。

//

3. 高级集成方案:介绍如何将项目与其他流行框架和工具进行深度集成。

//

4. 企业级部署:针对生产环境的企业级部署方案,包括高可用、负载均衡和监控配置。

//
//

// 您现在可以访问所有高级章节内容了! //

// `; // // 更新目录状态 // tocLockedItems.forEach(item => { // if (item.textContent.includes('高级配置指南')) { // item.classList.remove('locked'); // item.innerHTML = ' 高级配置指南'; // } // }); // // 显示成功通知 // showNotification('章节解锁成功!您现在可以访问高级配置指南。'); } /** * 放大图片 * @param {string} src - 图片地址 */ function enlargeImage(src) { enlargedImage.src = src; imageOverlay.classList.add('active'); } /** * 关闭图片放大层 */ function closeImageOverlay() { imageOverlay.classList.remove('active'); } /** * 显示通知 * @param {string} message - 通知内容 */ function showNotification(message) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background-color: #2ecc71; color: white; padding: 15px 25px; border-radius: 4px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); z-index: 1001; font-weight: 600; display: flex; align-items: center; gap: 10px; transform: translateX(150%); transition: transform 0.5s ease; `; notification.innerHTML = `${message}`; document.body.appendChild(notification); // 显示通知 setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 10); // 3秒后隐藏 setTimeout(() => { notification.style.transform = 'translateX(150%)'; setTimeout(() => { document.body.removeChild(notification); }, 500); }, 3000); } /** * 添加抖动动画样式 */ function addShakeAnimation() { const style = document.createElement('style'); style.textContent = ` @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .shake { animation: shake 0.5s; border-color: #e74c3c !important; } `; document.head.appendChild(style); } // 初始化 document.addEventListener('DOMContentLoaded', function() { initModal(); addShakeAnimation(); }); // 显示弹窗: 验证成功的回调、点击返回的回调 window.showDocLock = function(okFn, backFn) { okCallFn = okFn; backCallFn = backFn; // 打开 openModal(); } }; initTanChuangFun(); ================================================ FILE: sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.css ================================================ /* 弹窗遮罩层样式 */ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 1000; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; } .modal-overlay.active { opacity: 1; visibility: visible; } /* 弹窗主体样式 */ .modal { background: white; border-radius: 4px; /* 弹窗圆角4px */ width: 90%; max-width: 500px; padding: 25px 30px; box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); transform: translateY(-20px); transition: transform 0.3s; } .modal-overlay.active .modal { transform: translateY(0); } /* 弹窗标题样式 */ .modal-header { display: flex; align-items: center; margin-bottom: 20px; /* color: green; */ /* color: #2d8cf0; */ color: green; } .modal-header i { font-size: 1.8rem; margin-right: 12px; } .modal-header h2 { font-size: 16px; /* color: #e74c3c; */ margin: 0; } /* 密码输入区域样式 */ .password-form { margin-bottom: 20px; } .password-input { width: 100%; padding: 14px; border: 2px solid #ddd; border-radius: 2px; /* 输入框圆角2px */ font-size: 1rem; transition: border-color 0.3s; margin-bottom: 15px; } .password-input:focus { border-color: #2d8cf0; outline: none; } /* 按钮区域样式 */ .form-actions { display: flex; gap: 15px; justify-content: flex-start; } .form-btn { color: white; border: none; padding: 12px 24px; border-radius: 3px; /* 按钮圆角2px */ font-weight: 400; cursor: pointer; font-size: 1rem; transition: all 0.3s; white-space: nowrap; width: calc(50% - 5px); } .btn-verify { background-color: #2d8cf0; } .btn-verify:hover { background-color: #1c7ae0; } .btn-back { background-color: #aaa; } .btn-back:hover { background-color: #888; } /* 错误提示样式 */ .error-message { color: #e74c3c; margin-bottom: 15px; text-align: center; font-weight: 600; display: none; background-color: #ffebee; padding: 10px; border-radius: 2px; border-left: 3px solid #e74c3c; } .error-message.show { display: block; } /* 提示区域样式 */ .password-help-section { margin-top: 25px; border-top: 1px solid #eee; padding-top: 20px; } .help-text { text-align: left; margin-bottom: 20px; color: #666; font-weight: 400; font-size: 16px; } .help-text a { color: #0c6ae0; text-decoration: none; transition: color 0.2s; } .help-text a:hover { color: blue; text-decoration: underline; } /* 二维码图片区域样式 */ .images-container { display: flex; gap: 15px; justify-content: center; align-items: center; } .qr-image-container { flex: 1; position: relative; overflow: hidden; border-radius: 0px; /* box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1); */ border: 1px #ddd solid; cursor: pointer; transition: transform 0.3s; padding-bottom: 5px; } .qr-image { width: 100%; height: 110px; /* 3:2比例 */ object-fit: cover; display: block; transition: transform 0.3s; } .qr-image-container:hover { transform: translateY(-5px); } .qr-image-container:hover .qr-image { transform: scale(1.05); } .image-label { text-align: center; font-size: 0.85rem; color: #7f8c8d; margin-top: 8px; font-weight: 500; } /* 图片放大效果样式 */ .image-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; z-index: 2000; cursor: pointer; opacity: 0; visibility: hidden; transition: opacity 0.4s ease, visibility 0.4s ease; } .image-overlay.active { opacity: 1; visibility: visible; } .enlarged-image { max-width: 80%; max-height: 80%; border-radius: 8px; box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5); transform: scale(0.8); transition: transform 0.4s ease; } .image-overlay.active .enlarged-image { transform: scale(1); } /* ============================ */ /* 移动端适配样式 */ /* ============================ */ /* 移动端:屏幕宽度小于等于768px时应用 */ @media (max-width: 768px) { /* 弹窗宽度调整,左右留出边距 */ .modal { width: calc(100% - 30px); /* 左右各15px边距 */ max-width: 100%; padding: 20px; margin: 0 15px; } /* 弹窗标题调整 */ .modal-header { margin-bottom: 15px; } .modal-header h2 { font-size: 1.1rem; line-height: 1.3; } .modal-header i { font-size: 1.5rem; margin-right: 10px; } /* 密码输入框调整 */ .password-input { padding: 12px; font-size: 0.95rem; margin-bottom: 10px; } /* 按钮区域调整 */ .form-actions { flex-direction: column; /* 按钮垂直排列 */ gap: 10px; justify-content: stretch; } .form-btn { width: 100%; padding: 14px; font-size: 1rem; } /* 错误提示调整 */ .error-message { font-size: 0.9rem; padding: 8px; margin-bottom: 10px; } /* 提示区域调整 */ .password-help-section { margin-top: 15px; padding-top: 15px; } .help-text { font-size: 0.95rem; margin-bottom: 15px; } /* 移动端隐藏图片区域 */ .images-container { display: none; /* 移动端隐藏图片 */ } /* 图片放大效果在移动端隐藏 */ .image-overlay { display: none; } /* 弹窗内容垂直居中优化 */ .modal { max-height: 80vh; overflow-y: auto; } } /* 超小屏幕适配:屏幕宽度小于等于480px时应用 */ @media (max-width: 480px) { .modal { width: calc(100% - 20px); /* 左右各10px边距 */ margin: 0 10px; padding: 15px; } .modal-header h2 { font-size: 1rem; } .help-text { font-size: 0.9rem; line-height: 1.4; } .password-input { font-size: 0.9rem; } .form-btn { font-size: 0.95rem; } } ================================================ FILE: sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.js ================================================ // 章节锁定插件 // 声明 docsify 插件 var docLockPlugin = function(hook, vm) { // 钩子函数:解析之前执行 hook.beforeEach(function(content) { return content; }); // 钩子函数:每次路由切换时,解析内容之后执行 hook.afterEach(function(html) { return html; }); // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 hook.doneEach(function() { // isShowTanChuang(vm); }); // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 hook.ready(function() { }); // ======================================== 弹窗方法 // 检查成功后,多少天不再检查 const dl_AllowDisparity = 1000 * 60 * 60 * 24 * 30 * 1; // 1个月 // 拦截 path ,如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 const dl_exeArray = [ '/sso/*', '/oauth2/*', '/more/common-questions', '/up/*', '/micro/*', '/plugin/*' ]; // 排除 path const dl_excludeArray = [ '/sso/readme', '/oauth2/readme' ]; // 本次存储时,使用的标记 key const dl_saveKey = 'dl_saveKey'; // 判断当前是否应该弹出 function isShowTanChuang(vm) { // 非PC端不检查 // if(document.body.offsetWidth < 800) { // console.log('small screen ... wj '); // return; // } // 判断是否需要拦截 const isExe = isExePath(vm.route.path, dl_exeArray, dl_excludeArray); if(!isExe) { return; } // 判断是否近期已经判断过了 try{ const flagTime = localStorage[dl_saveKey]; if(flagTime) { // 记录 存储 的时间,和当前时间的差距 const disparity = new Date().getTime() - parseInt(flagTime); // 差距小于指定时间,不再检测 if(disparity < dl_AllowDisparity) { console.log('checked ... docLock '); return; } } }catch(e){ console.error(e); } // 本次打开页面的内存内已经弹出了的话,也不再弹了 // if(window.isYtcXsjfkasjdaaaa) { // return; // } // window.isYtcXsjfkasjdaaaa = true; // 验证成功的回调 const okFn = function() { console.log('ok 了'); localStorage.setItem(dl_saveKey, new Date().getTime() ); $('body').css({'overflow': 'auto'}); layer.msg('感谢你的支持,Sa-Token 将努力变得更加完善! ❤️ ❤️ ❤️ '); } // 点了返回的回调 const backFu = function() { $('body').css({'overflow': 'auto'}); location.href = '#/'; } // 弹窗验证 showDocLock(okFn, backFu); $('body').css({'overflow': 'hidden'}); // 弹出弹框,邀请填写 return; } // ======================================== 路径判断 /** * 判断一个路径,是否会被成功拦截,返回 true 或 false * @param {Object} path 要判断的路径,例如:/sso/apidoc * @param {Object} exeArray 要拦截的路径数组,例如:['/sso/*', '/oauth2/*', '/more/common-questions' ],如果填 /** 代表所有路径,填 /sso/* 代表 /sso/ 目录下所有路径 * @param {Object} excludeArray 要排除的路径数组,规则同上 */ function isExePath( path, exeArray, excludeArray) { // 参数验证和初始化 exeArray = exeArray || []; excludeArray = excludeArray || []; // 标准化路径,确保以 / 开头 path = normalizePath(path); // 先检查排除规则(优先级更高) for (let pattern of excludeArray) { if (matchPattern(path, pattern)) { return false; // 被排除,不拦截 } } // 再检查拦截规则 for (let pattern of exeArray) { if (matchPattern(path, pattern)) { return true; // 需要拦截 } } return false; // 默认不拦截 } /** * 标准化路径 */ function normalizePath(path) { if (!path) return '/'; if (!path.startsWith('/')) return '/' + path; return path; } /** * 增强版模式匹配 */ function matchPattern(path, pattern) { // 处理空值 if (!pattern) return false; pattern = pattern.trim(); // /** 匹配所有 if (pattern === '/**' || pattern === '**') { return true; } // 处理前置和后置通配符 if (pattern === '*' || pattern === '/*') { return true; } // 精确匹配 if (!pattern.includes('*')) { return path === pattern || path === normalizePath(pattern); } // 转换模式为正则表达式 const regexStr = pattern // 转义正则特殊字符 .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // 处理 ** 通配符(匹配多级目录) .replace(/\/\*\*/g, '(/.*)?') // 处理 * 通配符(匹配单级目录) .replace(/\*/g, '[^/]*') // 确保匹配完整路径 .replace(/^\//, '^/') .replace(/$/, '$'); try { const regex = new RegExp(regexStr); return regex.test(path); } catch (e) { console.error(`Invalid pattern: ${pattern}`, e); return false; } } } // =========================== AI 生成的弹窗代码 function initTanChuangFun() { // 配置项 const CONFIG = { correctPassword: 'sa-token yyds', // 正确密码 qqImageUrl: './big-file/doc/zong/doc-lock-pre-qq.png', wechatImageUrl: './big-file/doc/zong/doc-lock-pre-wx.png' }; // 弹窗HTML模板 const modalHTML = `
放大图片
`; // 初始化变量 let passwordModal, passwordInput, errorMessage, verifyBtn, backBtn; let imageOverlay, enlargedImage; let okCallFn = null; let backCallFn = null; /** * 初始化弹窗 * 将弹窗HTML插入到页面中,并绑定事件 */ function initModal() { // 插入弹窗HTML到页面 document.body.insertAdjacentHTML('beforeend', modalHTML); // 获取DOM元素 passwordModal = document.getElementById('passwordModal'); passwordInput = document.getElementById('passwordInput'); errorMessage = document.getElementById('errorMessage'); verifyBtn = document.getElementById('verifyBtn'); backBtn = document.getElementById('backBtn'); imageOverlay = document.getElementById('imageOverlay'); enlargedImage = document.getElementById('enlargedImage'); // 绑定事件 bindEvents(); } /** * 绑定所有事件 */ function bindEvents() { // 触发按钮点击事件 // document.getElementById('accessBtn').addEventListener('click', openModal); // 验证按钮点击事件 verifyBtn.addEventListener('click', validatePassword); // 返回按钮点击事件 backBtn.addEventListener('click', function(){ closeModal(); backCallFn(); }); // 输入框回车事件 passwordInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { validatePassword(); } }); // 只在非移动端绑定图片点击事件 if (!isMobileDevice()) { // 图片点击放大事件 document.querySelectorAll('.qr-image').forEach(img => { img.addEventListener('click', function() { enlargeImage(this.src); }); }); // 放大图片关闭事件 imageOverlay.addEventListener('click', function(e) { if (e.target === this || e.target === enlargedImage) { closeImageOverlay(); } }); // ESC键关闭放大图片 document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && imageOverlay.classList.contains('active')) { closeImageOverlay(); } }); } } /** * 检测是否为移动设备 * @returns {boolean} 是否为移动设备 */ function isMobileDevice() { return window.innerWidth <= 768; } /** * 打开密码弹窗 */ function openModal() { passwordModal.classList.add('active'); passwordInput.focus(); errorMessage.classList.remove('show'); passwordInput.value = ''; } /** * 关闭密码弹窗 */ function closeModal() { passwordModal.classList.remove('active'); } /** * 密码验证函数 * 宽松验证策略:允许左右空格,中间空格可省略 */ function validatePassword() { let enteredPassword = passwordInput.value.trim(); // 去除左右空格 // 标准化:移除所有空格 const normalizedEntered = enteredPassword.replace(/\s+/g, ''); const normalizedCorrect = CONFIG.correctPassword.replace(/\s+/g, ''); if (normalizedEntered === normalizedCorrect) { // 密码正确,解锁章节 unlockChapter(); } else { // 密码错误,显示错误信息 showError(); } } /** * 显示密码错误提示 */ function showError() { errorMessage.classList.add('show'); passwordInput.value = ''; passwordInput.focus(); // 添加抖动效果 passwordInput.classList.add('shake'); setTimeout(() => { passwordInput.classList.remove('shake'); }, 500); } /** * 解锁章节 */ function unlockChapter() { closeModal(); okCallFn(); // // 更新章节内容 // const lockedSection = document.querySelector('.locked-section'); // const tocLockedItems = document.querySelectorAll('.toc a.locked'); // // 更新章节显示 // lockedSection.innerHTML = ` //

章节已解锁

//

感谢您加入我们的社区!现在您可以查看高级配置指南的全部内容。

//
//

高级配置内容示例:

//

1. 自定义插件开发:详细讲解如何为项目开发自定义插件,包括插件结构、API接口和最佳实践。

//

2. 性能调优指南:深入分析项目性能瓶颈,并提供多种优化方案和调优技巧。

//

3. 高级集成方案:介绍如何将项目与其他流行框架和工具进行深度集成。

//

4. 企业级部署:针对生产环境的企业级部署方案,包括高可用、负载均衡和监控配置。

//
//

// 您现在可以访问所有高级章节内容了! //

// `; // // 更新目录状态 // tocLockedItems.forEach(item => { // if (item.textContent.includes('高级配置指南')) { // item.classList.remove('locked'); // item.innerHTML = ' 高级配置指南'; // } // }); // // 显示成功通知 // showNotification('章节解锁成功!您现在可以访问高级配置指南。'); } /** * 放大图片 * @param {string} src - 图片地址 */ function enlargeImage(src) { enlargedImage.src = src; imageOverlay.classList.add('active'); } /** * 关闭图片放大层 */ function closeImageOverlay() { imageOverlay.classList.remove('active'); } /** * 显示通知 * @param {string} message - 通知内容 */ function showNotification(message) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background-color: #2ecc71; color: white; padding: 15px 25px; border-radius: 4px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); z-index: 1001; font-weight: 600; display: flex; align-items: center; gap: 10px; transform: translateX(150%); transition: transform 0.5s ease; `; notification.innerHTML = `${message}`; document.body.appendChild(notification); // 显示通知 setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 10); // 3秒后隐藏 setTimeout(() => { notification.style.transform = 'translateX(150%)'; setTimeout(() => { document.body.removeChild(notification); }, 500); }, 3000); } /** * 添加抖动动画样式 */ function addShakeAnimation() { const style = document.createElement('style'); style.textContent = ` @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .shake { animation: shake 0.5s; border-color: #e74c3c !important; } `; document.head.appendChild(style); } // 初始化 document.addEventListener('DOMContentLoaded', function() { initModal(); addShakeAnimation(); }); // 显示弹窗: 验证成功的回调、点击返回的回调 window.showDocLock = function(okFn, backFn) { okCallFn = okFn; backCallFn = backFn; // 打开 openModal(); } }; initTanChuangFun(); ================================================ FILE: sa-token-doc/static/doc.css ================================================ /* 调整一下左侧树的样式 */ body{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;} #main {padding-bottom: 100px;} #main h2 {font-size: 1.6rem;} #main h3 {font-size: 1.25rem;} .main-box .markdown-section{ /* padding: 38px 20px; */ max-width: 100%; /* margin-left: 12%; */} .main-box .markdown-section h4{font-size: 1rem;} .main-box red, .main-box red *{ color: red !important; } .main-box green, .main-box green *{ color: green !important; } .main-box blue, .main-box blue *{ color: blue;!important; } .main-box purple, .main-box purple *{ color: purple;!important; } .main-box question, .main-box question *{ color: purple;!important; } /* ------- 多设备适配 start ------- */ .sub-nav-draw-box{ display: none; } body{ --doc-left-width: 300px; --doc-context-width: 1000px; --doc-right-width: 300px; } /* 大于 1100px,就显示左中右结构 */ @media screen and (min-width: 1100px) { .doc-right-bj-box{ display: block; } .main-box .content{left: 0;} .main-box .markdown-section{width: var(--doc-context-width); padding: 38px 20px; border: 0px green solid;} .main-box .doc-right-bj-box{left: calc(50% + (var(--doc-context-width) / 2) + 10px);} .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar{ position: fixed; top: 120px; left: calc(50% + (var(--doc-context-width) / 2) + 10px); width: var(--doc-right-width) !important; border: 0px #000 solid; line-height: 1.4em; width: calc(300px - 25px); max-height: 50vh; overflow: auto; } .main-box .sidebar{width: var(--doc-left-width);} .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar::-webkit-scrollbar{ width: 0px; } .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar li.active a{ color: #42B983; } .main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar li a{ font-size: 12px; color: #888; } /* .main-box .app-sub-sidebar{display: none;} */ } /* 小于 1100px时 */ @media screen and (max-width: 1100px) { .doc-right-bj-box{ display: none; } } /* 大于 1600px */ @media screen and (min-width: 1600px) { body{ --doc-left-width: 300px; --doc-context-width: 1000px; --doc-right-width: 300px; } } /* 小于 1100px - 1600px 之间 */ @media screen and (max-width: 1600px) { body{ --doc-left-width: 200px; --doc-context-width: calc( 100vw - 400px - 50px); --doc-right-width: calc(200px - 20px); /* 窄屏幕时,广告更换为上下显示 */ .main-box .top-ad-box .mad-img{ float: none; margin-right: 10px; border: 0px; } .main-box .top-ad-box .mad-text{ float: none; display: block; width: 100%; margin-top: 10px; color: #000; word-break: normal; } } } /* 小于 1100px时 */ @media screen and (max-width: 1100px) { .doc-right-bj-box{ display: none; } } /* 小于 800px时 */ /* @media screen and (max-width: 800px) { .doc-right-bj-box{ display: none; } } */ /* 媒体查询 */ @media screen and (max-width: 800px) { .nav-left .logo-box .logo-text, .nav-left .logo-box sub{display: none;} /* .main-box .markdown-section{max-width: 1000px; margin-left: auto; margin-top: 40px;} */ } /* 手机端不显示广告,和一些其它东西 */ @media (max-width: 576px) {.wwads-cn,.p-none{display:none!important}} /* ------- 多设备适配 end ------- */ /* 右侧盒子 */ .doc-right-bj-box{ width: var(--doc-right-width); padding: 10px; position: fixed; margin-top: 10px; top: 60px; border: 0px #000 solid; font-size: 12px; } .doc-right-bj-box-title{ font-size: 14px; color: #888; padding-bottom: 8px; border-bottom: 1px #aaa solid; } .doc-right-more-item{ position: absolute; border: 0px #000 solid; color: #000; width: 100%;} /* ------- 头部样式 ------- */ .doc-header{position: fixed; top: 0; z-index: 1000; width: 100%; height: 60px; line-height: 60px;} .doc-header{/* background-color: hsla(0,0%,100%,0.97); */ background-color: rgba(255, 255, 255, 0.97); box-shadow: 0 1px 3px rgba(26,26,26,0.1);} /* 左边导航 */ .nav-left{display: inline-block; float: left;} .logo-box {display: inline-block; cursor: pointer; color: #000; padding-left: 24px; height: 60px; line-height: 60px;} .logo-box img {width: 50px; height: 50px; vertical-align: middle; position: relative; top: -1px; margin-right: 5px;} .logo-box .logo-text {display: inline-block; margin: 0; padding: 0; color: #000; vertical-align: middle; font-size: 26px;font-weight: 500;} .logo-box sub{margin-left: 5px; color: #666;} /* 右边导航 */ .doc-header .nav-right{margin: 0; float: right; padding-right: 3em; margin-right: 20px !important;} .doc-header .nav-right>*{padding: 0px; margin: 0 10px;} .doc-header .nav-right>*:last-child{position: relative; z-index: 1002;} .doc-header .nav-right>select{border-color: #999; color: #666; outline: 0; cursor: pointer; transition: all 0.2s; background-color: #FFF; border-width: 1px; outline: 0;} .doc-header .nav-right>select:hover{box-shadow: 0 0 10px #aaa;} .github-corner{z-index: 1001 !important;} .doc-header .nav-right .wzi{font-size: 14px; line-height: 61px; transition: color 0.2s; padding-bottom: 4px;} .doc-header .nav-right .wzi:hover{border-bottom: 2px var(--a-color) solid;} .nav-right a{color: #34495E;} /* 搜索框 */ .sear-box{display: inline-block; width: 180px; margin-right: 20px; line-height: 26px; text-align: left;} .sear-box{/* position: fixed; */ } .sear-box .search{margin-bottom: 0px; padding: 0; border: 0;} .results-panel{border: 1px #aaa solid; border-radius: 2px; padding: 10px; max-height: 60vh; overflow: auto; position: absolute; background-color: #FFF; width: 266px;width: 316px;} .sear-box .search input{border: 1px solid #e3e3e3; color: #345; border-radius: 15px; line-height: 30px; padding-left: 30px; transition: all 0.2s;} .sear-box .search input{background: #fff url(./search-icon.svg) 10px 8px no-repeat; background-size: 14px;} .clear-button{display: none !important;} /* 工具栏超链接 展开、收缩div */ .zk-box{display: inline-block;} /* 外层盒 */ .zk-box .zk-context{max-height: 0px; position: absolute; overflow: hidden;} .zk-box:hover .zk-context{max-height: 400px;} /* 内层盒 */ .zk-context>div{padding: 1em 0.5em 1em 1em; border: 1px #ccc solid; border-radius: 2px; background-color: #FFF; font-size: 12px; transition: all 0.2s; opacity: 0;} .zk-box:hover .zk-context>div{opacity: 1;} /* 小链接 */ .zk-box .zk-context a{font-size: 14px; display: block; line-height: 32px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;} .zk-box .zk-context a{text-align: left; padding: 0 1.5em 0 1em;} .zk-box .zk-context .zk-fengexian{border-bottom: 1px #d9d9d9 solid; margin: 10px 0;} /* 下三角小图标 */ .zk-icon{display: inline-block; width: 0px; height: 0px; position: relative;top: 3px; margin-left: 4px;} .zk-icon{border-style: solid; border-width: 5px; border-color: #aaa transparent transparent transparent; } /* 版本选择按钮 */ .select-version{background-color: transparent !important;} /* ------- 调整一下左侧树的字体样式 ------- */ .main-box .sidebar{padding-top: 25px; margin-top: 60px;} .sidebar .sidebar-nav>ul>li>p{/* font-size: 1.2em; */ margin-top: 10px;} .sidebar .sidebar-nav>ul>li> strong{/* font-size: 1.2em; */ margin-top: 10px;} /* .sidebar ul li a{color: #222;} */ .sidebar .sidebar-nav>ul>li>ul>li>a{/* color: #222; */font-size: 14px; /* font-weight: 700; */} .main-box .sidebar-nav ul li{margin-top: 0; margin-bottom: 0;} .main-box .sidebar ul li a{color: #00323c;} /* 做到悬浮出现下划线的效果 */ .main-box .sidebar>.sidebar-nav>ul{padding-left: 6px;} .main-box .sidebar li a:hover{color: #42b983;} /* .main-box .sidebar li{white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin: 5px 0;} .main-box .sidebar li a{display: inline; line-height: 30px; padding: 5px 0 2px;} .main-box .sidebar li a:hover{text-decoration: none; color: #42b983; border-bottom: 1px #42b983 solid;} .main-box .sidebar li.active>a{border: 0px;} .main-box .sidebar li.active>a:after{content: ''; position: absolute; height: 30px; right: 0; border-right: 3px #42b983 solid;} */ .sidebar .sidebar-nav>ul>li>ul>li.active-rep>a{ color: #42B983; font-weight: 700; } /* .main-box .sidebar .app-sub-sidebar li:before{float: none;} */ /* ============== code代码样式优化 ================ */ .main-box .markdown-section code, .main-box .markdown-section pre{background-color: rgba(0, 0, 0, 0.04);} /* 背景变黑 */ .main-box [data-lang]{padding: 0px !important; border-radius: 2px;overflow-x: auto; overflow-y: hidden;} .main-box [v-pre] code{border: 0px red solid; border-radius: 0px; /* background-color: #282828; */ background-color: #191919; color: #FFF;} .main-box [v-pre] code{padding: 1.5em 1.3em; margin-left: 40px !important;} /* .main-box h2{margin-top: 70px;} */ /* 代码行号盒子样式 */ .code-line-box {list-style-type: none; border-right: 1px solid #000; position: absolute; top: 0; left: 0; width: 40px; user-select: none;} .code-line-box {padding: calc(1.5em + 1px) 0px !important; padding-bottom: calc(1.5em + 20px) !important; margin: 0px !important;} .code-line-box {line-height: inherit !important; background-color: #191919; color: #aaa;font-weight: 400;font-size: 0.85em;text-align: center;} /* xml语言样式优化 */ /* .lang-xml .token.comment{color: #CDAB53;} */ .lang-xml .token.tag *{color: #db2d20;} .lang-xml .token.attr-value{color: #A6E22E;} /* html语言样式优化 */ .lang-html .token.comment{color: #CDAB53;} .lang-html .token.tag *{color: #db2d20;} .lang-html .token.tag .attr-name, .lang-html .token.tag .attr-name *{color: #A6E22E; opacity: 0.9;} .lang-html .token.tag .attr-value, .lang-html .token.tag .attr-value *{color: #E6DB74; opacity: 0.9;} .lang-html .token.annotation.punctuation{color: #ddd;} .lang-html .token.punctuation{color: #ddd;} /* java语言样式优化 */ .main-box .lang-java{color: #01a252 !important;; opacity: 1;} .lang-java .token.keyword{color: #db2d20;} .lang-java .token.namespace,.lang-java .token.namespace *{color: #01A252; opacity: 1;} .lang-java .token.class-name,.lang-java .cm-variable{color: #55b5db; opacity: 1;} /* .lang-java .token.comment{color: #CDAB53;} */ .lang-java .token.annotation.punctuation{color: #ddd;} .lang-java .token.punctuation{color: #ddd;} /* cmd语言样式优化 */ .main-box .lang-cmd{color: #01A252 !important; opacity: 1;} /* url语言样式优化 */ .main-box .lang-url{color: #E96917 !important; opacity: 1;} /* js语言样式优化 */ .main-box .lang-js{color: #01a252 !important;} /* .lang-js .token.comment{color: #CDAB53;} */ /* .lang-js .token.string{color: #fded02;} */ .lang-js .token.string{color: #ddd;} .lang-js .token.punctuation{color: #ddd;} /* yaml 和 properties 语言优化 */ .lang-yaml .token.punctuation{color: #eee;} .lang-properties .token.attr-name{color: #22a2c9;} /* ------- markdown 内容样式优化 ------- */ /* GitHub折线图最大宽度 */ [alt=github-chart]{max-width: 897px;} /* 大屏幕时,某些图片限制一下宽度 */ @media screen and (min-width: 800px) { [title=s-w],[title=s-w-sh]{max-width: 80%;} .s-w,.s-w-sh{ max-width: 80%; } } .s-w-sh, [title=s-w-sh]{display: inline-block; border: 1px #eee solid;} .w-100, [title=w-100]{display: inline-block; border: 1px #eee solid; max-width: 100%;} /* 鼠标悬浮时切换img */ .hover-change-img {border: 1px #eee solid; max-width: 100%; } .hover-change-img:hover img:first-child{ display: none; } .hover-change-img img:last-child{ display: none; } .hover-change-img img:first-child{ display: inline-block; } .hover-change-img:hover img:last-child{ display: inline-block; } /* 公众号table */ .gzh-table{ /* table-layout:fixed !important; */} /* .gzh-table,.gzh-table tr,.gzh-table td{display: block !important;} */ /* .gzh-table tbody{display: block !important; width: 100% !important;} */ #main .gzh-table tr{background-color: #FFF;} .gzh-table td{padding: 20px !important; width: 20%; border: 0;} .gzh-table td b{display: block; margin-bottom: 10px; } /* tab选项卡优化 */ /* .docsify-tabs--classic{background-color: rgba(255, 255, 255, 0.2);} */ .docsify-tabs__tab{outline: 0; cursor: pointer;} .docsify-tabs--classic .docsify-tabs__tab--active{box-shadow: 0 0 0;} /* tab卡片插件样式优化 */ .main-box{ --docsifytabs-border-color: #ddd; --docsifytabs-tab-color: #777; } /* 调整表格的响应式 */ #main table{margin-left: 0px;} @media screen and (min-width: 800px) { #main table tr th{min-width: 100px;} } /* 提示框加上灰色背景 */ .main-box .markdown-section blockquote{padding: 1px 24px 1px 30px; background-color: #f8f8f8;} /* 行级代码样式 */ blockquote code {font-weight: 400;} /* 赞助列表 */ .zanzhu-box{margin-top: -10px;} .zanzhu-box table tr td:nth-child(2){color: red;} #main .zanzhu-box table tr td:first-child a{border-color: rgba(0,0,0,0); color: inherit;} #main .zanzhu-box table tr td:first-child a:hover{border-color: var(--a-hover-color); color: var(--a-hover-color);} /* 展开和收起 */ #main .zanzhu-box{/* height: 500px; */ overflow-y: hidden; transition: all 1.5s;} #main .zanzhu-box table{display: table;} .zhankai-btn-box{margin-top: 10px;} .zk-btn--1,.zk-btn--2{cursor: pointer;} .zk-btn--2{display: none;} /* 角标位置修复 */ .badge-box a:nth-child(-n+2) img{position: relative; top: 1px;} body { --a-color: #01a252; --a-hover-color: #0969da; } /* 超链接样式 */ #main *:not(h1,h2,h3,h4,h5,h6) a{font-weight: 400; text-decoration: none; font-family: "思源黑体";} #main *:not(h1,h2,h3,h4,h5,h6) a{color: var(--a-color); border-bottom: 1px var(--a-color) solid;} #main *:not(h1,h2,h3,h4,h5,h6) a:hover{color: var(--a-hover-color); border-bottom: 1px var(--a-hover-color) solid;} #main .un-dec-a-pre+p a, #main p[align=center] a{border-bottom:0px;} /* toc目录树 */ .toc-box>li{margin-bottom: 15px;} .toc-box .toc-h2{list-style-type: none; font-size: 18px; margin-top: 20px;} .toc-box .toc-h3,.toc-box .toc-h4{margin-left: 1em;} .toc-box .toc-h5,.toc-box .toc-h6{margin-left: 2em;} #main .toc-box .toc-h2 a span{color: #34495e;} #main .toc-box a{border-color: rgba(0,0,0,0); transition: 0s;} #main .toc-box a span{color: inherit;} /* 加载图片的按钮 */ .show-img{ background-color: #FFF; padding: 8px 15px; border: 1px #42b983 solid; color: #42b983; cursor: pointer; border-radius: 2px; transition: all 0.2s; } .show-img:hover{ background-color: #eaf6eb; } .show-to-img{cursor: pointer;} /* 导航栏悬浮时出现下滑条条 */ /* .doc-header .nav-right .wzi::after { content: ''; width: 0%; float: left; display: inline-block; text-align: center; margin-top: -15px; border-bottom: 2px var(--a-color) solid; transition: all 0.2s; } .doc-header .nav-right .wzi:hover::after {width: 100%;} */ /* 保证点开图片时在最上面 */ .medium-zoom-image.medium-zoom-image--opened{ z-index: 10000; } /* ------------- 答题按钮 ------------- */ #main .dt-btn,#main .case-btn{ background-color: #e7ecf3; color: #385481; display: inline-block; border: 1px #d7dce3 solid !important; border-radius: 1px; /* border-bottom-width: 0px !important; */ margin-top: 10px; width: 100%; padding: 8px 14px; font-size: 14px; transition: all 0.15s; text-decoration: none !important; font-weight: 400; /* 背景 */ background-image: url(icon/dati.svg); background-repeat: no-repeat; background-size: 20px 20px; background-position: 1em 12px; text-indent: 2em; } /* 代码示例按钮 */ #main .case-btn{ background-color: #feedeb; color: #dd4949; border: 1px #decdcb solid !important; background-size: 18px 18px; background-image: url(icon/code.svg); } #main .case-btn.case-btn-video{ background-color: #ECF5FF; color: #1979DA; border: 1px #49A9DA solid !important; background-image: url(icon/video.svg); } #main .dt-btn{display: none;} /* ------------- 背景色相关 ------------- */ /* 侧边栏需要透明 */ .sidebar-toggle{background-color: transparent !important;} .sidebar{background-color: transparent !important;} /* 变色的动画 */ .doc-header{transition: background-color 0.3s !important;} /* 调色按钮 */ .theme-btn{width: 25px; height: 25px; line-height: 60px; vertical-align: middle; position: relative; top: -1px;} .theme-box{width: 156px; text-align: left; line-height: 20px; margin-top: -20px;} .theme-box span{ display: inline-block; width: 20px; height: 20px; margin: 1px 2px; border: 1px #ccc solid; cursor: pointer; border-radius: 1px; } /* ------------- details标签 ------------- */ .main-box details{ border: 1px #42B983 solid; background-color: #f4fdef; overflow: hidden; max-height: 44px; margin-bottom: 1em; /* transition: all 1s; */ } .main-box details[open]{ /* max-height: 1000px; */ overflow: auto; animation: slideDown 0.6s linear both;} @keyframes slideDown { 0% { max-height: 44px; overflow: hidden; } 99% { max-height: 1500px; overflow: hidden; } 100% { max-height: 1500px; overflow: auto; } } .main-box details summary{ padding: 11px 14px; background-color: #f4fdef; color: #01a252; cursor: pointer; } .main-box details pre{ margin-left: 1em; margin-right: 1em; } .main-box details table{margin-left: 1em !important; margin-right: 1em; width: auto;} .main-box details p{padding: 0 14px;} /* 广告盒子 */ .ad-title{ font-size: 14px; color: #aaa; padding-bottom: 8px; margin-bottom: 14px; border-bottom: 1px #aaa solid; } .ad-tips{margin-bottom: 5px;} .ad-close{float: right;} .ad-close:hover{cursor: pointer; text-decoration: underline; color: red;} .main-box .top-ad-box{font-size: 12px;} .main-box .top-ad-box a{border-bottom: 0px; text-decoration: none;} .main-box .top-ad-box a:hover{border-bottom: 0px;} .main-box .top-ad-box a img{border: 1px #eee solid; width: 100%; /* max-height: 80px; */ border-radius: 2px; transition: all 0.1s !important;} .main-box .top-ad-box a img:hover{box-shadow: 0 0 20px #ddd;} .mad-bg-box{ padding: 10px; background-color: #F4F8FA; /* border: 1px #eee solid; */ overflow: hidden; } .mad-wh-box{ width: 100%; } .main-box .top-ad-box .mad-img{ width: 130px; float: left; margin-right: 10px; border: 0px; } .main-box .top-ad-box .mad-text{ float: left; width: calc(100% - 140px); color: #000; font-size: 14px; line-height: 20px; word-break: break-all; } .main-box .top-ad-box2 a img{ width: 48.5%; margin-bottom: 2px; } .main-box .top-ad-box2 a:nth-child(2n+1) img{margin-right: 2px; } /* 帮助按钮 */ .help-btn{transition: all 0.5s; text-align: center; border: 1px #42b983 solid; background-color: rgba(255, 255, 255, 0.5); cursor: pointer; font-size: 13px; color: #42b983; line-height: 40px;} .help-btn:hover{box-shadow: 0 0 20px #D1EEE1 !important;} .xiaozhushou-intro p{line-height: 14px;} /* ew-wa */ .ew-wa{ margin-top: 14px; line-height: 18px; color: #aaa; } .ew-wa a{ margin-right: 5px; text-decoration: none; color: #8693A7; } .ew-wa a:hover{text-decoration: underline; color: #44f; } /* 按钮发光动画 */ /* .help-btn{animation: helpbtnanimation 3s infinite;} @keyframes helpbtnanimation{ 0%{box-shadow: 0 0 1px #42B983;} 50%{box-shadow: 0 0 20px #42B983;} 100%{box-shadow: 0 0 20px #FFF;} } */ /* ********** 赞助者名单 ******** */ .zanzhu-table{text-align: left;} /* 赞助排序盒子 */ .zanzhu-sort-box{font-size: 14px; margin-bottom: 10px;} .zanzhu-sort-box .zanzhu-sort-btn{text-decoration: none; color: #999; cursor: pointer;} .zanzhu-sort-box .zanzhu-sort-btn:hover{text-decoration: underline; color: #557;} .zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native{text-decoration: underline; color: #557;} /* 底部按钮盒子 */ .zz-btn-box{color: #666; font-size: 14px;} .zz-btn-box button{padding: 5px 10px; cursor: pointer; border: 1px #ccc solid; color: #999; background-color: #FFF;} .zz-btn-box button:hover{box-shadow: 0 0 10px #ddd;} .syzz-show-btn{border: 1px #ccc solid; padding: 5px 10px; background-color: #FFF; color: #666; cursor: pointer;} .syzz-show-btn:hover{box-shadow: 0 0 10px #ddd;} /* ********** 团队成员名单 ******** */ .markdown-section .team-table{ display: table; text-align: left; } .team-table img{ width: 45px; height: 45px; } /* ajax加载时的转圈圈样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } /* 万维广告 */ .wwads-cn{margin-top: 0px !important;} .wwads-cn>a>img{width: 80px !important;} /* 提示框 */ .main-box .alert{ border-radius: 0px !important; } /* .main-box .alert ul,.main-box .alert ol{ margin-top: -5px; margin-bottom: -10px; } */ .main-box .alert.tip .title .icon.icon-tip{ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%2301354d' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E"); } .main-box .alert.flat.note{background-color: #E8F4FF;} .main-box .alert.flat.tip{background-color: #F0F9EB;} .main-box .alert.flat.warning{background-color: #FDF6EC;} ================================================ FILE: sa-token-doc/static/docsify-plugin.js ================================================ // 声明 docsify 插件 var myDocsifyPlugin = function(hook, vm) { // 钩子函数:解析之前执行 hook.beforeEach(function(content) { try{ // 功能 1,替换全局变量 content = content.replace(/\$\{sa.top.version\}/g, window.saTokenTopVersion); // 添加 [toc] 标记 content = content.replace(/\[\[toc\]\]/g, '
'); }catch(e){ // } return content; }); // 钩子函数:每次路由切换时,解析内容之后执行 hook.afterEach(function(html) { // 功能 2,文章底部添加仓库地址 var url = 'https://gitee.com/dromara/sa-token/tree/dev/sa-token-doc/' + vm.route.file; var url2 = 'https://github.com/dromara/sa-token/tree/dev/sa-token-doc/' + vm.route.file; var footer = [ '







', '' ].join(''); return html + footer; }); // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 hook.doneEach(function() { // 功能3,给代码盒子,添加行数样式 $('pre code').each(function(){ var lines = $(this).text().split('\n').length; var $numbering = $('
    ').addClass('code-line-box'); $(this) .addClass('has-numbering') .parent() .append($numbering); for(i=1;i<=lines;i++){ $numbering.append($('
  • ').text(i)); } }); // 功能4,添加 toc 目录 var dStr = ""; $('#main h2, #main h3, #main h4, #main h5, #main h6').each(function() { $('.toc-box').append('
  • ' + this.innerHTML + '
  • '); }); // 功能5,统计赞助人数 // if($('.zanzhu-count').length && $('.zanzhu-box table').length) { // $('.zanzhu-count').html($('.zanzhu-box table tr').length); // } // 功能5,渲染赞助数据 if($('.zanzhu-table').length) { // $('.zanzhu-count').html($('.zanzhu-box table tr').length); // console.log(123); renderDonateTable(); onZanzhuSortClick(); } // 功能6:标题下面的广告 // if(vm.route.path !== '/' && $(window).width() >= 800) { // var ad = `

    // 推广信息: // 关闭 // // // //

    `; // // 没有下划线就先补个下划线 // // if($('#main h1').next().prop('tagName') !== 'HR') { // // $('#main h1').after('
    '); // // } // // 如果一周内用户点击过关闭广告,则不再展现 // let allowJg = 1000 * 60 * 60 * 24 * 7; // // allowJg = 1000 * 10; // try{ // const closeAdTime = localStorage.closeAdTime; // if(closeAdTime) { // // 点击广告关闭的时间,和当前时间的差距 // const closeAdJg = new Date().getTime() - parseInt(closeAdTime); // // 差距小于七天,不再展示 // if(closeAdJg < allowJg) { // console.log('not show ad ...'); // return; // } // } // }catch(e){ // console.error(e); // } // // 添加广告 // // $('#main h1').after(ad); // $('.ssp-ad-box').append(ad) // // 添加关闭事件 // $('.top-ad-box .ad-close').click(function(){ // console.log('关闭广告'); // // $('.top-ad-box').slideUp(); // 折叠收起 // layer.confirm('关闭后,一周内不再展现此信息', function(){ // $(".top-ad-box").fadeOut(1000); // 淡出效果 // layer.msg('关闭成功'); // localStorage.closeAdTime = new Date().getTime(); // }) // }) // } }); // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 hook.ready(function() { // 将搜索框转移到右上角 document.querySelector(".sear-box").innerHTML = ''; document.querySelector(".sear-box").append(document.querySelector(".search")); document.querySelector(".search input").placeholder = '搜索…'; // 点击input时,展开 $('.sear-box input').click(function() { if($('.search input').val() != '') { $('.results-panel').addClass('show'); } }); // 失去焦点时,收缩 $('.sear-box input').blur(function() { setTimeout(function() { $('.results-panel').removeClass('show'); }, 200); }) // 选择一项时,收缩 $('.sear-box').on('click', '.matching-post', function() { console.log('click……'); // $('.search input').val(''); $('.results-panel').removeClass('show'); }); // 点击按钮,加载图片 $(document).on('click', '.show-img', function(){ var src = $(this).attr('img-src'); var img = ''; $(this).after(img); $(this).remove(); }) // 点击按钮,加载图片 $(document).on('click', '.show-to-img', function(){ open(this.src); }) }); } ================================================ FILE: sa-token-doc/static/docsify-plugins/docsify-betterembed-1.1.1.js ================================================ const PMEregexGetImport=/(.*?)/gs,PMEregexReplaceImport=e=>new RegExp(`(.*?)`,"gs"),PMEregexGetImportName=//g,PMEregexGetEmbedImportName=/^(.*?).md#(.*?) ':include'\)$/gm;function PMEcreateElementFromHTML(e){var t=document.createElement("div");return t.innerHTML=e.trim(),t}function partialMarkdownEmbed(n,e){n.beforeEach(m=>{if(PMEregexGetEmbedImportName.test(m))return m.match(PMEregexGetEmbedImportName).forEach(e=>{var t=e.split(".md#")[1].split(" ':include')")[0],r=e.replace("#"+t,"");m=m.replace(e,` ${r} `)}),m}),n.afterEach(a=>{if(PMEregexGetImport.test(a))return a.match(PMEregexGetImport).forEach(e=>{var t,r=PMEcreateElementFromHTML(e);const m=[];for(let e=1;e<6;e++)0===m.length&&0!==(t=r.querySelectorAll("div > h"+e)).length&&t.forEach(e=>m.push(e.id));n.doneEach(()=>{const t=window.location.hash.split("?id=")[0];m.forEach(e=>{document.querySelectorAll(`.section-link[href='${t}?id=${e}']`).forEach(e=>{e.parentElement.nextElementSibling.remove(),e.parentElement.remove()})})});var o=e.match(PMEregexGetImportName)[0].split("\x3c!-- embedImport:start:")[1].split(" --\x3e")[0],e=e.split(``)[1].split(``)[0];a=a.replace(PMEregexReplaceImport(o),e)}),a})}window.$docsify=window.$docsify||{},$docsify.plugins=[partialMarkdownEmbed,...$docsify.plugins||[]]; ================================================ FILE: sa-token-doc/static/docsify-plugins/docsify-plugin-flexible-alerts.min-1.1.1.js ================================================ /*! * docsify-plugin-flexible-alerts * v1.1.1 * https://github.com/fzankl/docsify-plugin-flexible-alerts#readme * (c) 2022 Fabian Zankl * MIT license */ !function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}!function(t,e){void 0===e&&(e={});var a=e.insertAt;if(t&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],l=document.createElement("style");l.type="text/css","top"===a&&o.firstChild?o.insertBefore(l,o.firstChild):o.appendChild(l),l.styleSheet?l.styleSheet.cssText=t:l.appendChild(document.createTextNode(t))}}(".alert{word-wrap:break-word;display:block;margin-bottom:1rem!important;padding:.75rem 1.25rem!important;position:relative;word-break:break-word}.alert>*{max-width:100%}.alert>:first-child{margin-top:0}.alert>:last-child{margin-bottom:0}.alert:before{content:unset!important}.alert+.alert{margin-top:-.25rem!important}.alert p{margin-bottom:.5rem;margin-top:.5rem}.alert .title{align-items:center;display:flex;flex-wrap:wrap;font-weight:600;margin:0}.icon{background-repeat:no-repeat;display:inline-block;height:16px;margin-right:.5rem;width:16px}.alert.callout{background:var(--background);border:1px solid #eee;border-left-width:.25rem;border-radius:.25rem}.alert.callout.note{border-left-color:#17a2b8!important}.alert.callout.note .title{color:#17a2b8}.alert.callout.note .icon-note{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%2317a2b8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E\")}.alert.callout.tip{border-left-color:#28a745!important}.alert.callout.tip .title{color:#28a745}.alert.callout.tip .icon-tip{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 352 512' fill='%2328a745' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z'/%3E%3C/svg%3E\")}.alert.callout.warning{border-left-color:#f0ad4e!important}.alert.callout.warning .title{color:#f0ad4e}.alert.callout.warning .icon-warning{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 17 16' fill='%23f0ad4e' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 5zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z'/%3E%3C/svg%3E\")}.alert.callout.attention{border-left-color:#dc3545!important}.alert.callout.attention .title{color:#dc3545}.alert.callout.attention .icon-attention{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%23dc3545' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath fill-rule='evenodd' d='M11.354 4.646a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708l6-6a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E\")}.alert.flat{background-color:#e2e3e5;border:1px solid #d6d8db;border-radius:.125rem;color:#383d41}.alert.flat.note{background-color:#cdeefd;border-color:#b4e6fc;color:#02587f}.alert.flat.note .title{color:#01354d}.alert.flat.note .icon-note{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%2301354d' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E\")}.alert.flat.tip{background-color:#dbefdc;border-color:#c9e7cb;color:#285b2a}.alert.flat.tip .title{color:#18381a}.alert.flat.tip .icon-tip{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 352 512' fill='%2318381a' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z'/%3E%3C/svg%3E\")}.alert.flat.warning{background-color:#ffddd3;border-color:#ffc9ba;color:#852d12}.alert.flat.warning .title{color:#581e0c}.alert.flat.warning .icon-warning{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 17 16' fill='%23581e0c' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 5zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z'/%3E%3C/svg%3E\")}.alert.flat.attention{background-color:#fdd9d7;border-color:#fcc2bf;color:#7f231c}.alert.flat.attention .title{color:#551713}.alert.flat.attention .icon-attention{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='1em' height='1em' viewBox='0 0 16 16' fill='%23551713' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath fill-rule='evenodd' d='M11.354 4.646a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708l6-6a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E\")}"),function(){var e={style:"callout",note:{label:"Note",icon:"icon-note",className:"note"},tip:{label:"Tip",icon:"icon-tip",className:"tip"},warning:{label:"Warning",icon:"icon-warning",className:"warning"},attention:{label:"Attention",icon:"icon-attention",className:"attention"},typeMappings:{info:"note",danger:"attention"}};function a(t,e){var o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;for(var l in e)try{e[l].constructor===Object&&o<1?t[l]=a(t[l],e[l],o+1):t[l]=e[l]}catch(a){t[l]=e[l]}return t}window.$docsify=window.$docsify||{},window.$docsify.plugins=[].concat((function(o,l){var r=a(e,l.config["flexible-alerts"]||l.config.flexibleAlerts),i=function(t,e,a,o){var l=(t||"").match(new RegExp("".concat(e,":(([\\s\\w\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF-]*))")));return l?o?o(l[1]):l[1]:o?o(a):a};o.afterEach((function(e,a){a(e.replace(/<\s*blockquote[^>]*>[\s]+?(?:

    )?\[!(\w*)((?:\|[\w*:[\s\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]*)*?)\]([\s\S]*?)(?:<\/p>)?<\s*\/\s*blockquote>/g,(function(e,a,o,n){!r[a.toLowerCase()]&&r.typeMappings[a.toLowerCase()]&&(a=r.typeMappings[a.toLowerCase()]);var c=r[a.toLowerCase()];if(!c)return e;var d=i(o,"style",r.style),s=i(o,"iconVisibility","visible",(function(t){return"hidden"!==t})),g=i(o,"labelVisibility","visible",(function(t){return"hidden"!==t})),m=i(o,"label",c.label),u=i(o,"icon",c.icon),f=i(o,"className",c.className);if("object"===t(m)){var p=Object.keys(m).filter((function(t){return l.route.path.indexOf(t)>-1}));p&&p.length>0?m=m[p[0]]:(g=!1,s=!1)}var w=''),h='

    '.concat(s?w:"").concat(g?m:"","

    ");return'
    \n ').concat(s||g?h:"","\n

    ").concat(n,"

    \n
    ")})))}))}),window.$docsify.plugins)}()}(); //# sourceMappingURL=docsify-plugin-flexible-alerts.min.js.map ================================================ FILE: sa-token-doc/static/docsify-plugins/progress.update.js ================================================ // 显示文档阅读进度的进度条 // // 修改于:https://github.com/HerbertHe/docsify-progress // // 1、将最外层盒子的 z-index 值从 999 修改为 9999999999 function plugin(hook, vm) { let marginTop hook.mounted(function () { const content = document.getElementsByClassName("content")[0] marginTop = parseFloat( window.getComputedStyle(content).paddingTop.replace("px", "") ) let insertDOM = `
    ` const mainDOM = document.getElementsByTagName("body")[0] mainDOM.innerHTML = mainDOM.innerHTML + insertDOM function switcher() { const body = document.getElementsByTagName("body")[0] if (!body.classList.contains("close")) { body.classList.add("close") } else { body.classList.remove("close") } } const btn = document.querySelector("div.sidebar-toggle-button") btn.addEventListener("click", function (e) { e.stopPropagation() switcher() }) }) hook.ready(function () { window.addEventListener("scroll", function (e) { let totalHeight = marginTop + parseFloat( window .getComputedStyle(document.getElementById("main")) .height.replace("px", "") ) let scrollTop = document.body.scrollTop + document.documentElement.scrollTop let remain = totalHeight - document.body.offsetHeight document.getElementById("progress-display").style.width = Math.ceil((scrollTop / remain) * 100) + "%" }) }) } // Docsify plugin options window.$docsify["progress"] = Object.assign( { position: "top", color: "var(--theme-color,#42b983)", height: "3px", }, window.$docsify["progress"] ) window.$docsify.plugins = [].concat(plugin, window.$docsify.plugins) ================================================ FILE: sa-token-doc/static/docsify-plugins/sub-nav-draw.js ================================================ // 提取次级导航栏显示到右上角 // // 是否都开右边菜单 let isOpenRightSubTitle = false; // 重新定位 active-rep 对应的菜单 function positioningVmActiveRep(vm) { const vmPath = '#' + vm.route.path; $('.sidebar-nav>ul>li>ul>li>a').each(function(item) { if($(this).attr('href') === vmPath) { // $(this).parent().attr('active-rep', true); $(this).parent().addClass('active-rep') // console.log($(this)); } }) } function subNavDraw(hook, vm) { // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 hook.doneEach(function () { // 只在宽屏下展现,太小的屏幕不展现 if(document.body.clientWidth < 1100) { isOpenRightSubTitle = false; return; } else { isOpenRightSubTitle = true; } // 修改高度 const $dom = $('.app-sub-sidebar'); console.log($dom, $dom.height()); $('.doc-right-more-item').css({ top: (($dom.height() ?? 0) + 80) + 'px' }) // 重新定位 active-rep 对应的菜单 positioningVmActiveRep(vm); }) // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 hook.ready(function () { }) } window.$docsify.plugins = [].concat(subNavDraw, window.$docsify.plugins) // 滚动时设置一下左侧滚动条高度,不要超出可视区域 $(document).scroll(function(){ if(isOpenRightSubTitle) { try{ const offsetTop = $('.active-rep').get(0).offsetTop; $('.sidebar').scrollTop(offsetTop - ($('.sidebar').height() / 2)) } catch (e) { // console.log(e); } } }) ================================================ FILE: sa-token-doc/static/donate/donate-fun.js ================================================ // --------------------- 工具方法 --------------------- // 打开 loading loadingIcon = function(msg) { layer.closeAll(); // 开始前先把所有弹窗关了 return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); }; // 隐藏 loading hideLoadingIcon = function() { layer.closeAll(); }; // --------------------- 渲染赞助者名单 --------------------- // 返回赞助者名单副本 function getCopyDonateList() { var arr = []; for (var i = 0; i < donateList.length; i++) { var item = donateList[i]; // 时间转时间戳,方便排序 item.dateT = new Date(item.date).getTime(); // 金额补 .0 item.moneyS = item.money + ''; if(item.moneyS.indexOf('.') == -1) { item.moneyS = item.moneyS + '.0'; } arr.push(item); } return arr; } // 返回赞助者名单副本,根据日期倒叙排列 function getCopyDonateListByDateSort() { var arr = getCopyDonateList(); arr.sort(function(a, b){ var value = b.dateT - a.dateT; if(value == 0) { value = -1; } return value; }) return arr; } // 返回赞助者名单副本,根据赞助金额倒叙排列 function getCopyDonateListByMoneySort() { var arr = getCopyDonateList(); arr.sort(function(a, b){ var value = b.money - a.money; if(value == 0) { value = b.dateT - a.dateT; } return value; }) return arr; } // console.log(getCopyDonateListByMoneySort()); // 赞助配置 var zzCfg = { curr: 1, // 当前页 size: 15, // 页大小 pageCount: 0, // 页总数 dataCount: 0, // 数据总数 sort: 1, // 排序方式(1=按照日期倒叙,2=按照金额倒叙) } // 将赞助者名单渲染到页面上 function renderDonateTable() { // 先清空旧数据 $('.zanzhu-table tbody').empty(); // 拼接 tr 字符串 var trArrStr = ''; var arr = zzCfg.sort == 1 ? getCopyDonateListByDateSort() : getCopyDonateListByMoneySort(); // 按照页参数进行遍历 let index = (zzCfg.curr - 1) * zzCfg.size; // 起始索引 let end = index + zzCfg.size; // 结束索引 if(end > arr.length) { end = arr.length; } zzCfg.pageCount = parseInt(arr.length / zzCfg.size); // 页总数 if(arr.length % zzCfg.size != 0) { zzCfg.pageCount++; } zzCfg.dataCount = arr.length; // 数据总数 // 开始拼接字符串 for (let i = index; i < end; i++) { // console.log(item); let item = arr[i]; let name = item.name; if(item.link) { name = '' + name + '' } var trStr = ` ${name} ¥ ${item.moneyS} ${item.msg} ${item.date} `; trArrStr += trStr; } // 渲染到 table 里 $('.zanzhu-table tbody').html(trArrStr); // 重置分页信息 const pageInfo = `第 ${zzCfg.curr}/${zzCfg.pageCount} 页(共${zzCfg.dataCount}位)`; $('.zz-pageInfo').text(pageInfo); } // 带动画的渲染 function renderDonateTable2() { // 模拟ajax的延时 loadingIcon('努力加载中...'); setTimeout(function() { hideLoadingIcon(); // 隐藏掉转圈圈 renderDonateTable(); }, 300); } // renderDonateTable(); // 上一页 function prevPageRDT(){ if(zzCfg.curr <= 1) { return layer.msg('达咩,不能再往前了'); } zzCfg.curr--; renderDonateTable2(); } // 下一页 function nextPageRDT(){ if(zzCfg.curr >= zzCfg.pageCount) { return layer.msg('嘿,到底了'); } zzCfg.curr++; renderDonateTable2(); } // 绑定事件:切换排序 function onZanzhuSortClick(){ $('.zanzhu-sort-btn').click(function(){ // 切换 class $('.zz-sort-native').removeClass('zz-sort-native'); $(this).addClass('zz-sort-native'); // 切换数据 zzCfg.curr = 1; // 重置为第1页 zzCfg.sort = parseInt($(this).attr('sort-value')); renderDonateTable2(); }) } onZanzhuSortClick(); // 读取 sa-token-donate 页数据为 json function readDataToJson() { var arr = []; var trList = $('.zanzhu-box table tbody tr'); for (let tr of trList) { var tdArr = $(tr).find('td'); var item = { name: $(tdArr[0]).text(), link: $(tdArr[0]).find('a').attr('href') || '', money: parseFloat($(tdArr[1]).text().replaceAll('¥', '')), msg: $(tdArr[2]).html(), date: $(tdArr[3]).text(), }; arr.push(item); } return arr; } function readDataToJsonStr() { var arr = readDataToJson(); var str = ''; for (let item of arr) { str = JSON.stringify(item) + ',' + str; } return str; } ================================================ FILE: sa-token-doc/static/donate/donate-list.js ================================================ // 赞助者名单 var donateList = [ { "name": "省长", "link": "https://gitee.com/click33", "money": 10, "msg": "java中最好用的权限认证框架!", "date": "2020-12-15" }, { "name": "知知", "link": "https://gitee.com/double_zhi", "money": 10, "msg": "感谢您的开源项目!", "date": "2020-12-15" }, { "name": "zhangjiaxiaozhuo", "link": "https://gitee.com/zhangjiaxiaozhuo", "money": 10, "msg": "感谢您的开源项目!", "date": "2020-12-15" }, { "name": "RockMan", "link": "https://gitee.com/njx33", "money": 10, "msg": "感谢您的开源项目!", "date": "2020-12-17" }, { "name": "whcrow", "link": "https://gitee.com/whcrow", "money": 20, "msg": "军师加油!", "date": "2021-03-16" }, { "name": "xue1992wz", "link": "https://gitee.com/xue1992wz", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-03-16" }, { "name": "萧瑟", "link": "https://gitee.com/fengduidui", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-03-16" }, { "name": "二范先生", "link": "https://gitee.com/mr-er-fan", "money": 20, "msg": "省长加油啊 喝杯茶", "date": "2021-03-16" }, { "name": "Wizzer", "link": "https://gitee.com/wizzer", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-05-22" }, { "name": "孔孔的空空", "link": "https://gitee.com/kongmr", "money": 500, "msg": "感谢您的开源项目!", "date": "2021-07-30" }, { "name": "xiaoyan", "link": "https://gitee.com/l-yun", "money": 50, "msg": "be better", "date": "2021-07-31" }, { "name": "xiaoyan", "link": "https://gitee.com/l-yun", "money": 200, "msg": "好的作者理应被认可", "date": "2021-08-24" }, { "name": "苏永晓", "link": "https://gitee.com/suyongxiao", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-09-01" }, { "name": "永夜", "link": "https://gitee.com/cn-src", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-09-18" }, { "name": "apifox001", "link": "https://gitee.com/apifox001", "money": 200, "msg": "Apifox:API 文档、API 调试、API Mock、API 自动化测试", "date": "2021-10-15" }, { "name": "xiaoyan", "link": "https://gitee.com/l-yun", "money": 200, "msg": "节日快乐", "date": "2021-10-24" }, { "name": "ithorns", "link": "https://gitee.com/ithorns", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-10-25" }, { "name": "songfazhun", "link": "https://gitee.com/fzsong", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-10-28" }, { "name": "孔孔的空空", "link": "https://gitee.com/kongmr", "money": 100, "msg": "感谢您的开源项目!", "date": "2021-11-02" }, { "name": "铂赛东", "link": "https://gitee.com/bryan31", "money": 20, "msg": "开源加油!", "date": "2021-11-08" }, { "name": "公子骏", "link": "https://gitee.com/dt_flys", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-11-08" }, { "name": "Taller", "link": "https://gitee.com/evilatom", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-11-13" }, { "name": "万声鹉", "link": "https://gitee.com/wanshengwu", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-11-15" }, { "name": "yijunzhao", "link": "https://gitee.com/yijunzhao", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-11-21" }, { "name": "xiaoyan", "link": "https://gitee.com/l-yun", "money": 200, "msg": "感谢您的开源项目!", "date": "2021-11-26" }, { "name": "luyuan", "link": "https://gitee.com/meitesi", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-11-29" }, { "name": "图灵谷", "link": "https://gitee.com/stephenson37", "money": 20, "msg": "感谢您的开源项目!", "date": "2021-11-30" }, { "name": "fuhouyin", "link": "https://gitee.com/fuhouyin", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-12-01" }, { "name": "liu", "link": "https://gitee.com/liuliuliu123456", "money": 50, "msg": "感谢您的开源项目!", "date": "2021-12-15" }, { "name": "duyiliu", "link": "https://gitee.com/duyiliu", "money": 10, "msg": "化繁为简,是门艺术。", "date": "2021-12-16" }, { "name": "MrXionGe", "link": "https://gitee.com/MrXionGe", "money": 10, "msg": "SA加油~~", "date": "2021-12-17" }, { "name": "周周周杨", "link": "https://gitee.com/ChaoGeWanJiu", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-12-18" }, { "name": "网络小渣渣", "link": "https://gitee.com/a9777", "money": 10, "msg": "感谢您的开源项目!", "date": "2021-12-24" }, { "name": "刚子 (微信打赏)", "link": "", "money": 50, "msg": "微信打赏", "date": "2021-12-27" }, { "name": "两岁 (微信打赏)", "link": "", "money": 188, "msg": "微信打赏", "date": "2021-12-27" }, { "name": "前世男友", "link": "https://gitee.com/lanbaba666", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-02-17" }, { "name": "赵津 (微信打赏)", "link": "", "money": 16, "msg": "微信打赏", "date": "2022-02-20" }, { "name": "老杨", "link": "https://gitee.com/yangs914", "money": 6.66, "msg": "感谢您的开源项目!", "date": "2022-03-01" }, { "name": "晓辉", "link": "https://gitee.com/zxhShow", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-03-07" }, { "name": "Charles7c", "link": "https://gitee.com/Charles7c", "money": 20, "msg": "感谢您的开源项目!希望 SSO 模块越来越好!", "date": "2022-03-17" }, { "name": "黎子豪 (微信打赏)", "link": "", "money": 18.88, "msg": "请你喝杯咖啡", "date": "2022-03-21" }, { "name": "秦政 (微信打赏)", "link": "", "money": 6.66, "msg": "微信打赏", "date": "2022-03-22" }, { "name": "秦政 (微信打赏)", "link": "", "money": 20, "msg": "微信打赏", "date": "2022-03-22" }, { "name": "刘嘉威", "link": "https://gitee.com/liu_jiawei", "money": 6.66, "msg": "真滴好用~", "date": "2022-03-23" }, { "name": "Robin Tin (微信打赏)", "link": "", "money": 28.88, "msg": "微信打赏", "date": "2022-03-24" }, { "name": "lele", "link": "https://gitee.com/lelez", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-03-29" }, { "name": "alkinn", "link": "https://gitee.com/alkinn", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-03-29" }, { "name": "yukihane", "link": "https://gitee.com/yukihane", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-04-07" }, { "name": "xq584", "link": "https://gitee.com/xq584", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-04-08" }, { "name": "行长 (微信打赏)", "link": "", "money": 20, "msg": "微信打赏", "date": "2022-04-15" }, { "name": "阿文", "link": "https://gitee.com/qq921124136", "money": 20, "msg": "很好的框架,在开发文档里学到了很多知识点", "date": "2022-04-21" }, { "name": "Horatio201", "link": "https://gitee.com/horatio201", "money": 20, "msg": "太牛了!", "date": "2022-04-25" }, { "name": "乡村阿土哥", "link": "https://gitee.com/895995040", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-04-29" }, { "name": "李洪星", "link": "https://gitee.com/li_hong_xing", "money": 10, "msg": "解决了很多之前项目中遇到的问题。感谢您的开源项目!", "date": "2022-04-29" }, { "name": "别处理", "link": "https://gitee.com/zshnb", "money": 10, "msg": "非常好的项目,希望能一直做下去", "date": "2022-05-01" }, { "name": "cray", "link": "https://gitee.com/hyy6300", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-05-10" }, { "name": "LZ", "link": "https://gitee.com/FUNKBOY", "money": 6.66, "msg": "感谢您的开源项目!顺便踩一脚Spring Security,sa加油!", "date": "2022-05-18" }, { "name": "sun_2020", "link": "https://gitee.com/sun-two-thousand-and-twenty", "money": 50, "msg": "感谢您的开源项目!", "date": "2022-06-08" }, { "name": "yuncai929", "link": "https://gitee.com/null_448_5562", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-06-10" }, { "name": "刘时立", "link": "https://gitee.com/liu-shili", "money": 10, "msg": "非常棒的开源项目!", "date": "2022-06-13" }, { "name": "qiuyue", "link": "https://gitee.com/bmlt", "money": 10, "msg": "satoken牛逼", "date": "2022-06-16" }, { "name": "风如歌", "link": "https://gitee.com/the-wind-is-like-a-song", "money": 10, "msg": "这个框架简直满足了我所有对于安全框架的需求,赞一个,加油sa-token加油中国开源!", "date": "2022-06-17" }, { "name": "zhihong", "link": "https://gitee.com/zzh13520704819", "money": 20, "msg": "感谢您的开源项目!", "date": "2022-06-20" }, { "name": "jwc_gitee", "link": "https://gitee.com/jwc-gitee", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-07-07" }, { "name": "小北宸呀", "link": "https://gitee.com/a_aas", "money": 10, "msg": "感谢您的开源项目!我就喜欢你这种把我当白痴的官方文档", "date": "2022-07-08" }, { "name": "jerrydo", "link": "https://gitee.com/jerrydo", "money": 10, "msg": "感谢您的开源项目!很强大!", "date": "2022-08-10" }, { "name": "邱道长", "link": "https://gitee.com/qiudaozhang", "money": 20, "msg": "优秀的项目,赞", "date": "2022-09-09" }, { "name": "BlueRose", "link": "https://gitee.com/Bluerose_2", "money": 20, "msg": "感谢您的付出,项目非常棒!", "date": "2022-09-22" }, { "name": "西东", "link": "https://gitee.com/noear_admin", "money": 99, "msg": "感谢您的开源项目!", "date": "2022-10-05" }, { "name": "xueshize", "link": "https://gitee.com/xueshize", "money": 20, "msg": "感谢您的开源项目!", "date": "2022-10-12" }, { "name": "feyong", "link": "https://gitee.com/feyong", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-10-18" }, { "name": "王文博", "link": "https://gitee.com/rl520", "money": 20, "msg": "感谢您的开源项目!", "date": "2022-10-24" }, { "name": "就眠儀式", "link": "https://gitee.com/Jmysy", "money": 50, "msg": "感谢您的开源项目!", "date": "2022-10-26" }, { "name": "laruui", "link": "https://gitee.com/laruui", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-10-28" }, { "name": "feel", "link": "https://gitee.com/xujiahuim", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-11-17" }, { "name": "IlovePea", "link": "https://gitee.com/IlovePea", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-11-22" }, { "name": "ThatYear", "link": "https://gitee.com/wangmuqing", "money": 20, "msg": "感谢您的开源项目!", "date": "2022-11-24" }, { "name": "时间很快", "link": "https://gitee.com/frsimple", "money": 50, "msg": "感谢您的开源项目!", "date": "2022-11-29" }, { "name": "刘涛", "link": "https://gitee.com/doILike", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-12-13" }, { "name": "ken", "link": "https://gitee.com/affction", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-12-19" }, { "name": "Peter Z", "link": "https://gitee.com/zj1995", "money": 10, "msg": "感谢您的开源项目!", "date": "2022-12-26" }, { "name": "SWmachine", "link": "https://gitee.com/SWmachine", "money": 10, "msg": "您的开源很好用!", "date": "2023-01-07" }, { "name": "tsing", "link": "https://gitee.com/tsing666", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-01-08" }, { "name": "不问烟雨", "link": "https://gitee.com/xiaominfagui", "money": 10, "msg": "牛", "date": "2023-01-12" }, { "name": "熊孩子", "link": "https://gitee.com/xhz1230", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-02-17" }, { "name": "陈乾", "link": "https://gitee.com/qianpou", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-03-05" }, { "name": "陈乾", "link": "https://gitee.com/qianpou", "money": 50, "msg": "感谢您的开源项目!", "date": "2023-03-07" }, { "name": "李一博", "link": "https://gitee.com/haust_lyb", "money": 8.88, "msg": "感谢您的开源项目!", "date": "2023-03-07" }, { "name": "空空(微信打赏)", "link": "", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-03-08" }, { "name": "Java_小生", "link": "https://gitee.com/zhang_hanzhe", "money": 10, "msg": "感谢Sa-Token让我不用去B站肯几十个小时的教程,框架很优秀文档更优秀", "date": "2023-03-09" }, { "name": "zhou", "link": "https://gitee.com/mrzhou1", "money": 50, "msg": "感谢答疑", "date": "2023-03-29" }, { "name": "F(微信打赏)", "link": "", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-04-09" }, { "name": "王宁波", "link": "https://gitee.com/wang-ningbo", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-04-10" }, { "name": "Admin", "link": "https://gitee.com/jinan-jimeng-network_0", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-04-12" }, { "name": "李广龙", "link": "https://gitee.com/ak47-b", "money": 20, "msg": "跟大哥学习一辈子学不完", "date": "2023-04-14" }, { "name": "hurumo", "link": "https://gitee.com/hurumo", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-04-17" }, { "name": "c(微信打赏)", "link": "", "money": 100, "msg": "感谢您的开源项目!", "date": "2023-04-17" }, { "name": "bootx", "link": "https://gitee.com/bootx", "money": 100, "msg": "Bootx-Platform:支付收单、三方对接、后端基于 Spring Boot、Spring Cloud 应用脚手架", "date": "2023-04-18" }, { "name": "gdl", "link": "https://gitee.com/gdl97", "money": 20, "msg": "感谢您的开源项目!作者牛逼!", "date": "2023-04-29" }, { "name": "SummerHy", "link": "https://gitee.com/hurumo", "money": 10, "msg": "国产,就是棒,:)", "date": "2023-05-07" }, { "name": "BeckJin", "link": "https://gitee.com/beckjin666", "money": 100, "msg": "明道云-零代码开发平台,快速响应业务需求。从“IT背锅侠”,变成“IT英雄”。", "date": "2023-05-08" }, { "name": "xc_Moving", "link": "https://gitee.com/fireZhang", "money": 20, "msg": "感谢您的开源项目!感谢SA-token帮我度过项目的难关", "date": "2023-05-11" }, { "name": "砰嚓嚓(QQ打赏)", "link": "", "money": 20, "msg": "一点打赏不成敬意", "date": "2023-05-15" }, { "name": "dyjgitdyjgit", "link": "https://gitee.com/qtinfogit", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-05-22" }, { "name": "javahuang", "link": "https://gitee.com/javahrp", "money": 200, "msg": "SurveyKing:功能最强大的调查问卷系统和考试系统,开源", "date": "2023-06-08" }, { "name": "SP", "link": "https://gitee.com/LSP1999", "money": 10, "msg": "就是需要这种简单上手的项目", "date": "2023-06-15" }, { "name": "Dear胜哥", "link": "https://gitee.com/DearShengGe", "money": 10, "msg": "有幸在摸鱼时间认真看完了全文档,感觉很是不错。开源不易,望作者继续扩展该框架功能!", "date": "2023-06-30" }, { "name": "吴其敏(微信打赏)", "link": "", "money": 200, "msg": "CAT 是基于 Java 开发的实时应用监控平台,为美团点评提供了全面的实时监控告警服务。", "date": "2023-07-11" }, { "name": "mikeinshanghai", "link": "https://gitee.com/mikeinshanghai", "money": 50, "msg": "Sa-Token, MeterSphere共成长, 共辉煌! ", "date": "2023-07-14" }, { "name": "张兆伟", "link": "https://gitee.com/zhang865700", "money": 50, "msg": "感谢您的开源项目!", "date": "2023-07-24" }, { "name": "XiaoYi", "link": "https://gitee.com/getianit", "money": 100, "msg": "亚洲云深圳BGP云服务器", "date": "2023-07-24" }, { "name": "好心肠的老哥", "link": "https://gitee.com/ntdm", "money": 10, "msg": "非常好的开源项目,希望越来越好!", "date": "2023-08-02" }, { "name": "结弦奏(微信打赏)", "link": "", "money": 50, "msg": "感谢您的开源项目!", "date": "2023-08-07" }, { "name": "失败女神", "link": "https://gitee.com/failedgoddess", "money": 50, "msg": "感谢您的开源项目!", "date": "2023-08-03" }, { "name": "快快乐乐小码农", "link": "https://gitee.com/happy-little-farmer", "money": 1, "msg": "感谢您的开源项目!", "date": "2023-08-17" }, { "name": "刘斌", "link": "https://gitee.com/xuanfather", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-08-17" }, { "name": "Meteor", "link": "https://gitee.com/meteoroc", "money": 2.5, "msg": "感谢您的开源项目!", "date": "2023-08-23" }, { "name": "上下求索(微信打赏)", "link": "", "money": 12, "msg": "明天请你吃个早餐吧", "date": "2023-08-31" }, { "name": "T_T", "link": "https://gitee.com/wm26hua", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-09-07" }, { "name": "huni", "link": "https://gitee.com/simin_sizi", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-09-11" }, { "name": "lostyue", "link": "https://gitee.com/lostyue", "money": 20, "msg": "感谢您的开源项目!", "date": "2023-09-14" }, { "name": "shenlicao", "link": "https://gitee.com/shenlicao", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-09-15" }, { "name": "明道云", "link": "https://gitee.com/lunan-yn", "money": 200, "msg": "明道云2023年伙伴大会,报名链接", "date": "2023-09-25" }, { "name": "yang", "link": "https://gitee.com/hansdm", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-09-27" }, { "name": "lee", "link": "https://gitee.com/cngeeklee", "money": 10, "msg": "真正的轻量级权限安全框架,希望继续更新", "date": "2023-10-06" }, { "name": "yangs2w", "link": "https://gitee.com/yangs2w", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-10-10" }, { "name": "老马(微信打赏)", "link": "", "money": 99, "msg": "我使用过的开源项目,作者我都给过红包了。请收下", "date": "2023-10-16" }, { "name": "ly-chn", "link": "https://gitee.com/ly-chn", "money": 99, "msg": "一定的资金支持有助于开源项目走的更加长远", "date": "2023-10-17" }, { "name": "PotatoLoofah", "link": "https://gitee.com/PotatoLoofah", "money": 10, "msg": "感谢您的开源项目!", "date": "2023-10-27" }, { "name": "立秋", "link": "https://gitee.com/code_wh", "money": 2.5, "msg": "感谢您的开源项目!", "date": "2023-10-27" }, { "name": "时间很快", "link": "https://gitee.com/frsimple", "money": 220, "msg": "感谢您的开源项目!", "date": "2023-10-27" }, { "name": "flydongdong", "link": "https://gitee.com/flydongdong", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2023-10-31" }, { "name": "MetaLowCode", "link": "https://gitee.com/meta_low_code_admin", "money": 220.0, "msg": '可能是最适合Java程序员的低代码平台 -- 美乐低代码 https://melecode.com/', "date": "2023-11-23" }, { "name": "rednettle", "link": "https://gitee.com/rednettle", "money": 5.0, "msg": "感谢您的开源项目!", "date": "2023-11-24" }, { "name": "郑志强", "link": "https://gitee.com/zhi_qiang_zheng", "money": 20.0, "msg": "感谢您的开源项目!", "date": "2023-12-01" }, { "name": "Justin Chia", "link": "https://gitee.com/justin-chia", "money": 218.0, "msg": '可以二开的国产低代码表单 https://vform666.com/', "date": "2023-12-05" }, { "name": "asalan570", "link": "https://gitee.com/asalan570", "money": 2.0, "msg": "感谢您的开源项目!", "date": "2023-12-12" }, { "name": "guwq", "link": "https://gitee.com/guweiqiang2016", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2023-12-14" }, { "name": "少年", "link": "https://gitee.com/tingfengBlog", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-01-10" }, { "name": "mshk", "link": "https://gitee.com/yueguangshuiyan", "money": 50.0, "msg": "Thank you for your open source repository!", "date": "2024-02-21" }, { "name": "CSpy", "link": "https://gitee.com/cspy", "money": 10.0, "msg": "希望在线文档网站能有个“我已点赞”的跳过按钮,互相尊重一下,谢谢。", "date": "2024-03-07" }, { "name": "EtSKY", "link": "https://gitee.com/ecoiyun", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-03-08" }, { "name": "李富康", "link": "https://gitee.com/li-fukang0719", "money": 5.0, "msg": "感谢您的开源项目!", "date": "2024-03-15" }, { "name": "Jacky", "link": "https://gitee.com/jackywjj", "money": 50.0, "msg": "感谢您的开源项目!", "date": "2024-03-20" }, { "name": "yuluo", "link": "https://gitee.com/hlzha", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-03-20" }, { "name": "ai稞", "link": "https://gitee.com/bbpla", "money": 300.0, "msg": "感谢您的开源项目!", "date": "2024-03-20" }, { "name": "Smile丶掩饰", "link": "https://gitee.com/smile_gjy", "money": 50.0, "msg": "感谢您的开源项目!加油!", "date": "2024-03-20" }, { "name": "小雪纷飞", "link": "https://gitee.com/wujiangwu", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-03-20" }, { "name": "dengyuanke", "link": "https://gitee.com/dengyuanke", "money": 10.0, "msg": "感谢", "date": "2024-03-20" }, { "name": "Brath", "link": "https://gitee.com/Guoqing-Li", "money": 230.0, "msg": '感谢SaToken开源!荔知AI是一款优秀的AI网站,地址:https://www.brath.cn', "date": "2024-03-20" }, { "name": "厉军", "link": "https://gitee.com/shlijun", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-03-20" }, { "name": "Blue", "link": "https://gitee.com/my-blue", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-03-20" }, { "name": "Cole Xu(微信打赏)", "link": "", "money": 50.0, "msg": "一直在使用satoken,感谢你的付出", "date": "2024-03-20" }, { "name": "cy42", "link": "https://gitee.com/third-party-framework", "money": 50.0, "msg": "感谢您的开源项目!", "date": "2024-03-26" }, { "name": "YaeMivo(微信打赏)", "link": "", "money": 20.0, "msg": "祝越做越好", "date": "2024-03-29" }, { "name": "炮孩子", "link": "https://gitee.com/paohaizi", "money": 10.0, "msg": "拳打Apach shiro,脚踢 Spring Security。", "date": "2024-03-30" }, { "name": "孤独的造梦者", "link": "https://gitee.com/dpxz", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-04-01" }, { "name": "HiSin", "link": "https://gitee.com/HisinLx", "money": 20.0, "msg": "感谢您的开源项目!", "date": "2024-05-07" }, { "name": "INS6", "link": "https://gitee.com/feiyuchuixue", "money": 188.0, "msg": '感谢Sa-Token开源!Sz-Admin一个轻量化RBAC开源框架。', "date": "2024-06-05" }, { "name": "Zongyy", "link": "https://gitee.com/zongyY11", "money": 10.0, "msg": "感谢您的开源项目!", "date": "2024-06-05" }, { "name": "驰骋BPM", "link": "https://gitee.com/chichengsoft", "money": 100.0, "msg": '感谢开源, 欢迎下载:驰骋低代码BPM https://gitee.com/opencc/JFlow', "date": "2024-06-11" }, { "name": "flydongdong", "link": "https://gitee.com/flydongdong", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2024-06-18" }, { "name": "驰骋BPM", "link": "https://gitee.com/chichengsoft", "money": 100.0, "msg": '感谢您的开源项目!欢迎了解驰骋BPM低代码. https://gitee.com/opencc/JFlow', "date": "2024-06-20" }, { "name": "Mall4j商城系统", "link": "https://gitee.com/gz-yami_admin", "money": 218.0, "msg": '感谢开源!Mall4j商城系统: https://gitee.com/gz-yami/mall4j', "date": "2024-06-21" }, { "name": "FlyFlow", "link": "https://gitee.com/junyue", "money": 200.0, "msg": '感谢开源!FlyFlow工作流: https://gitee.com/junyue/flyflow', "date": "2024-06-25" }, { "name": "immortal", "link": "https://gitee.com/immortal-wang", "money": 10.0, "msg": '感谢您的开源项目,内部项目鉴权框架参考了您的部分设计思想(用户会话和令牌会话)。', "date": "2024-07-20" }, { "name": "张磊", "link": "https://gitee.com/zl18282425038", "money": 1.0, "msg": '感谢您的开源项目!', "date": "2024-08-03" }, { "name": "老黄H", "link": "https://gitee.com/lao-huang-h", "money": 1.0, "msg": '感谢您的开源项目我是王攀', "date": "2024-08-04" }, { "name": "Chat2DB", "link": "https://gitee.com/jipengfei001", "money": 10.0, "msg": 'https://github.com/chat2db/Chat2DB/ 数据库客户端', "date": "2024-08-05" }, { "name": "gentleman", "link": "https://gitee.com/guoweiweigege", "money": 10.0, "msg": '设计简单 功能多且强大 我杜伟坤为你代言', "date": "2024-08-09" }, { "name": "june", "link": "https://gitee.com/june_home", "money": 50.0, "msg": '非常方便简单易用,感谢您的开源项目!', "date": "2024-08-14" }, { "name": "kaka", "link": "https://gitee.com/blueair", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2024-08-30" }, { "name": "有锦", "link": "https://gitee.com/mushi00", "money": 1.0, "msg": '好厉害的项目啊 我郭威虽然没什么钱但是我郭威还是捐赠一下我郭威真的很认可这个项目,我郭威太崇拜了', "date": "2024-09-03" }, { "name": "zhangboyang", "link": "https://gitee.com/zhangboyangos", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2024-09-04" }, { "name": "读钓", "link": "https://gitee.com/songyinyin", "money": 50.0, "msg": '感谢您的开源项目!致敬用爱发电', "date": "2024-09-14" }, { "name": "sswiki", "link": "https://gitee.com/sswiki", "money": 50.0, "msg": '感谢开源!私有化部署的企业知识库:https://doc.zyplayer.com', "date": "2024-09-24" }, { "name": "坚持就是胜利", "link": "https://gitee.com/insistppp", "money": 1.0, "msg": '感谢您的开源项目!', "date": "2024-09-27" }, { "name": "StrawberryerBlue", "link": "https://gitee.com/strawberryerblue", "money": 50.0, "msg": '感谢您的开源项目!', "date": "2024-10-14" }, { "name": "qing", "link": "https://gitee.com/haomao1", "money": 20.0, "msg": '非常好用,感谢您的开源项目!', "date": "2024-10-15" }, { "name": "厉飞雨", "link": "https://gitee.com/david666a", "money": 58.0, "msg": '感谢道友,深有启发。', "date": "2024-10-16" }, { "name": "李嘉辉", "link": "https://gitee.com/lee_kiahwee", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2024-10-17" }, { "name": "不问烟雨", "link": "https://gitee.com/xiaominfagui", "money": 20.0, "msg": '加油', "date": "2024-11-04" }, { "name": "zonglinjiang", "link": "https://gitee.com/jiang-zonglin0427", "money": 5.0, "msg": '已经在至少两个商业项目里面使用了 ,非常好用,感谢作者的开源精神', "date": "2024-11-05" }, { "name": "当下", "link": "https://gitee.com/carl1974", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2024-11-18" }, { "name": "唐醋鱼(微信打赏)", "link": "", "money": 8.8, "msg": '小小心意,群主请受纳', "date": "2024-11-19" }, { "name": "cunyun", "link": "https://gitee.com/cunyun", "money": 1.0, "msg": '感谢您的开源项目!', "date": "2024-11-27" }, { "name": "guwq", "link": "https://gitee.com/guweiqiang2016", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2024-12-05" }, { "name": "kingkick", "link": "https://gitee.com/kingkick", "money": 10.0, "msg": '文档真好!学习到不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。', "date": "2024-12-12" }, { "name": "JavaBean", "link": "https://gitee.com/DearShengGe", "money": 6.6, "msg": '跟着Sa的文档一点点理解仿佛有位老师在带领着一步步去学,尤其是SSO单点登录部分!好东西不能被埋没!', "date": "2024-12-19" }, { "name": "焱枫", "link": "https://gitee.com/dellibrunaway", "money": 10.0, "msg": '开心快乐每一天', "date": "2024-12-20" }, { "name": "dmyi", "link": "https://gitee.com/dmyi", "money": 20.0, "msg": '感谢您的开源项目!', "date": "2025-01-03" }, { "name": "费雷", "link": "https://gitee.com/feileier", "money": 20.0, "msg": '感谢您的开源项目!', "date": "2025-01-09" }, { "name": "苏俊", "link": "https://gitee.com/fareuwell", "money": 50.0, "msg": '感谢您的开源项目!', "date": "2025-01-10" }, { "name": "阡陌兮", "link": "https://gitee.com/i_kang", "money": 9.9, "msg": '感谢您的开源项目!', "date": "2025-01-16" }, { "name": "main", "link": "https://gitee.com/zgx1179399522", "money": 50.0, "msg": '感谢您的开源项目!', "date": "2025-01-22" }, { "name": "shalixiaohu", "link": "https://gitee.com/jiaruozhi", "money": 10.0, "msg": '感谢您的开源项目!', "date": "2025-02-06" }, { "name": "林佳奇", "link": "https://gitee.com/ljq1307", "money": 20.0, "msg": '感谢您的开源项目!', "date": "2025-02-15" }, { "name": "AAA方一翻(微信打赏)", "link": "", "money": 28.8, "msg": '请你喝杯奶茶', "date": "2025-04-07" }, { "name": "16群群友 错别字先生(微信打赏)", "link": "", "money": 50, "msg": '感谢您的开源项目!', "date": "2025-05-27" }, { "name": "李猛(微信打赏)", "link": "", "money": 100, "msg": '小小红包不成敬意,就当支持 satoken 的社区了哈', "date": "2025-09-17" }, { "name": "Owen(微信打赏)", "link": "", "money": 66.0, "msg": '感谢您的开源项目!', "date": "2025-09-19" }, { "name": "Linex(赞赏码)", "link": "", "money": 0.01, "msg": '感谢您的开源项目!', "date": "2026-01-27" }, { "name": "Json.张(赞赏码)", "link": "", "money": 1.0, "msg": '小小心意', "date": "2026-01-27" }, { "name": "Nafil-鱼泡直聘运营(微信打赏)", "link": "", "money": 18.88, "msg": '感谢您的开源项目!', "date": "2026-01-29" }, { "name": "偶T啊M(赞赏码)", "link": "", "money": 5.0, "msg": '感谢您的开源项目!', "date": "2026-02-06" }, { "name": "马潮毅(赞赏码)", "link": "", "money": 50.0, "msg": '希望 sa-token 社区越做越好', "date": "2026-02-08" }, { "name": "秋末-(赞赏码)", "link": "", "money": 29.9, "msg": '非常棒的项目,加油', "date": "2026-02-09" }, { "name": "飘飘(赞赏码)", "link": "", "money": 88.88, "msg": '做的很好,不白嫖,希望收费', "date": "2026-02-25" }, { "name": "Nafil-鱼泡直聘运营(微信打赏)", "link": "", "money": 16.88, "msg": '感谢您的开源项目!', "date": "2026-02-26" }, { "name": "美人鱼(微信打赏)", "link": "", "money": 18.88, "msg": '感谢您的开源项目!', "date": "2026-03-3" }, ] ================================================ FILE: sa-token-doc/static/index.css ================================================ /* ================================== 内容 ====================================== */ /* 总 */ *{margin: 0px; padding: 0px;} body{font-size: 16px; color: #34495E; font-family: "Source Sans Pro","Helvetica Neue","Arial,sans-serif";} .z-div{} .s-width{width: 1000px; margin: auto;} /* 栏目标题 */ .s-title{color: #000; margin-top: 90px; margin-bottom: 50px;} /* 解释性文字 */ .re-text{color: #4e6e8e;} /* 分割线 */ .s-fenge{width: 80%; margin: auto; border-top: 1px #ddd solid;} /* ------- 头部样式 ------- */ .doc-header{position: fixed; top: 0; z-index: 1000; width: 100%; height: 60px; line-height: 60px;} .doc-header{background-color: hsla(0,0%,100%,0.97); box-shadow: 0 1px 3px rgba(26,26,26,0.1);} /* 左边logo */ .nav-left{display: inline-block; float: left;} .logo-box {display: inline-block; cursor: pointer; color: #000; padding-left: 24px; height: 60px; line-height: 60px;} .logo-box img {width: 50px; height: 50px; vertical-align: middle; position: relative; top: -1px;} .logo-box .logo-text {display: inline-block; margin: 0; padding: 0; padding-left: 5px; vertical-align: middle; font-size: 22px;/* font-weight: 700; */} /* 右边导航 */ .doc-header .nav-right{margin: 0; float: right; line-height: 60px; padding-right: 4em; white-space: nowrap; } .doc-header .nav-right>*{padding: 0px; margin: 0 9px;} .doc-header .nav-right>*:last-child{position: relative; z-index: 1002; } .nav-right a{color: #34495E; text-decoration: none; transition: all 0.2s;} .nav-right a:hover{color: #42B983;} .doc-header .nav-right .wzi{font-size: 14px; line-height: 61px; transition: color 0.2s; padding-bottom: 4px;} .doc-header .nav-right .wzi:hover{border-bottom: 2px #42B983 solid;} /* 小章鱼 */ .github-corner svg{color: #fff; fill: var(--theme-color, #42b983); height: 80px; width: 80px; z-index: 1001 !important;} /* -------- 海报部分 --------- */ .main-box{width: 100%; /* min-height: 70vh; */ /* height: 80vh; */ text-align: center; } .main-box{display: flex; align-items: center; text-align: center; } .fenge{min-height: 90px;} .content-box{color: #000; flex: 1; padding: 120px 1em 70px;} .content-box h1{font-size: 100px; font-weight: 400; position: relative; margin-top: 40px; /* margin-top: 15vh; */} .content-box h1 small{font-size: 18px; position: absolute; bottom: 10px; margin-left: 5px; font-weight: 100;} /* .title-logo{width: 221px; cursor: pointer; transition: all 0.2s;} .title-logo:hover{transform: scale(1.2, 1.2);} */ .sub-title{font-size: 22px; font-weight: 400; margin-top: 30px; margin-bottom: 25px; color: #6a8bad; color: #234;} /* .content-box p{line-height: 30px; padding: 0px 1em;} */ /* 角标位置修复 */ .badge-box a:nth-child(-n+2) img{position: relative; top: 1px;} /* 模拟副标题的光标闪烁 */ .gb-cursor {display: inline-block;width: 2px;height: 22px;position: relative;top: 4px;left: -4px;background-color: black;animation: blink 0.7s infinite alternate;} @keyframes blink { from {opacity: 0;} to {opacity: 1;} } .main-box{background-image: url(/big-file/index/home-bg3.jpg); background-size: 120% 100%;} .main-box{animation: changes 30s 0.2s linear infinite normal; /* background-attachment: ; */} /* normal | alternate */ @keyframes changes { from {background-position: 0vw 0%;} to {background-position: -20vw 0%;} } /* 几个按钮 */ .btn-box{margin-top: 50px; margin-bottom: 40px;} .btn-box a{border: 1px #1e8f5c solid; border-radius: 2em; box-sizing: border-box; color: #1e8f5c; display: inline-block;transition: all 0.1s;} .btn-box a{font-size: 14px; background-color: rgba(0,255,0,0.06); letter-spacing: 1px; padding: 1em 2em; margin: 0 0.5em; margin-bottom: 14px; text-decoration: none; } .btn-box a:hover{/* transform: scale(1.05, 1.05); */padding: 1em 2.3em; margin-left: 0.2em; margin-right: 0.2em;} /* 最后一个加深底色 */ .btn-box .doc-btn {color: #fff; background-color: #42B983; border: 1px green solid;} /* 按钮发光动画 */ .btn-box .doc-btn{animation: bganimation 3s infinite;} @keyframes bganimation{ 0%{box-shadow: 0 0 1px #42B983;} 50%{box-shadow: 0 0 20px #42B983;} 100%{box-shadow: 0 0 20px #FFF;} } /* 其它平台链接 */ .qt-pt-box{margin-top: 40px;} .qt-pt-box a{text-decoration: none; margin-right: 20px;} .qt-pt-box>a img{height: 40px;} .qt-pt-box a img{/* width: 130px; */ /* height: 40px; */ transition: all 0.2s !important;} .qt-pt-box a img:hover{transform: scale(1.1, 1.1);} /* .img-gitcode{width: 140px;} */ /* 多媒体平台 */ .qt-pt-box .dmt-link{ } .qt-pt-box .dmt-img{width: 40px; height: 40px; } .qt-pt-box .dmt-tips{ vertical-align: 100%; color: #888; font-size: 12px; margin-left: 5px; } .dmt-detail h4{padding-top: 10px; padding-bottom: 20px;} /* 悬浮时展开 */ .dmt-link{display: inline-block; position: relative;} .dmt-detail{position: absolute; transform: translate(-300px, 0px); background-color: #FFF; overflow: hidden; display: none; transition: all 0.2s;} .dmt-link:hover .dmt-detail{display: block; } .dmt-detail{padding: 30px; padding-top: 15px; border: 1px #ccc solid;} .dmt-item-box{display: flex; width: 600px;} .dmt-detail .dmt-item{width: 200px; flex: 1; text-align: center; cursor: pointer;} .dmt-item .dmt-qr-img{ max-width: 180px; } .dmt-item .dmt-logo-img{ max-width: 180px; max-height: 50px; margin-top: 10px;} .dmt-item-douyin,.dmt-item-wxsph{ margin-top: 15px; } .dmt-item-bilibili .dmt-logo-img{margin-top: 20px;} .dmt-item-wxsph .dmt-logo-img{margin-top: 15px;} /* 微信二维码 */ .wx-qr-box{margin-top: 50px;} .qr-item{display: inline-block;} .qr-item p{font-size: 12px; padding: 0 0.5em;} /* .qr-item a{color: #42B983;} */ .wx-qr{width: 150px;} .wx-qr-box p{margin-top: 10px; color: #666; margin-bottom: 20px;} .wx-qr,.dro-qr{cursor: pointer;} /* -------- 支持特性 --------- */ .feature-z{padding: 0em 1em; padding-top: 0px; padding-bottom: 60px; text-align: center; color: #000;} .feature-z .s-title{font-size: 30px; font-weight: 400; margin-top: 70px; margin-bottom: 40px;} .feature-z{color: rgb(128, 128, 128); text-align: center; box-sizing: border-box; line-height: 24px; font-size: 16px;} .feature-box{margin-top: 10px; margin-bottom: 70px; display: flex; flex-wrap: wrap; justify-content: space-between; /* justify-content: flex-start; */} .feature{border: 0px #000 solid; flex: 0 0 33%; text-align: left; padding: 1.8em 1.2em; box-sizing: border-box;} .feature h2{font-size: 22px; color: #000; font-weight: 400;} .feature p{margin-top: 14px; font-size: 16px; color: #4e6e8e;} .sa-token-jss-img{ width: 100%; } /* 功能结构图介绍 */ /* .sa-token-js-box{margin-bottom: 50px; transition: all 0.2s;} .sa-token-js-box img{max-width: 100%;} .sa-token-js-box:hover{cursor: pointer; box-shadow: 0 0 20px #ccc;} */ .re-text{padding: 0 1em;} .re-text a{color: #0969da; text-decoration: none;} .re-text a:hover{border-bottom: 1px #0969da solid;} /* -------- 集成案例 --------- */ .s-case-box{justify-content: space-between;} .s-case{border: 1px #e5e5e5 solid; flex: 0 0 31.5%; margin-top: 30px; text-align: left; box-sizing: border-box; padding-bottom: 16px; overflow: hidden;} .s-case{position: relative; transition: all 0.2s; background-color: #FFF;} .s-case-link{display: block; width: 100%; height: 0px; padding-bottom: 50%; position: relative; overflow: hidden;} .s-case-link img{width: 100%; height: 100%; object-fit: cover; object-position: center; position: absolute;} .s-case-title,.s-case-intro{padding: 0 16px;} .s-case-title{margin-top: 20px; font-size: 18px; font-weight: 400; color: #333; font-family: "microsoft yahei";} .s-case-intro{margin-top: 15px; font-size: 14px; line-height: 20px; color: #777; word-break:break-all;} .s-author{color: #ff5722; border: 1px #ff5722 solid; position: absolute; right: 20px; display: inline-block;} .s-author{padding: 0 5px; font-size: 12px; transform: translate(0, -25px);} /* 悬浮动画 */ .s-case:hover{box-shadow: 0 0 20px #ccc;} .s-case:hover img{transform: scale(1.3, 1.3); } .s-case-link img{transition: transform 0.3s !important;} .s-case img:hover{cursor: pointer;} .s-case:hover .s-case-link:after {background-color: rgba(0, 0, 0, .35); color: #FFF;} .s-case .s-case-link:after { content: "详情"; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0); transition: all .4s; text-align: center; line-height: 10; color: rgba(0,0,0,0); cursor: pointer; } /* -------- 赞助者名单 --------- */ .zanzhu-box{font-size: 14px;} .zanzhu-table{width: 100%; margin-top: 20px; /* border-color: #fff; */ text-align: left; color: #333;} .zanzhu-table th,.zanzhu-table td{padding: 5px 10px;} .zanzhu-table tr:nth-child(even){background: #F8F8F8;} .zanzhu-table .zanzhu-money{color: red;} .zanzhu-table .zanzhu-name a{text-decoration: none; color: #333;} .zanzhu-table .zanzhu-name a:hover{text-decoration: underline; color: blue;} /* 赞助排序盒子 */ .zanzhu-sort-box{font-size: 14px; margin-top: -10px;} .zanzhu-sort-box .zanzhu-sort-btn{text-decoration: none; color: #999; cursor: pointer;} .zanzhu-sort-box .zanzhu-sort-btn:hover{text-decoration: underline; color: #55a;} .zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native{text-decoration: underline; color: #55a;} /* 底部按钮盒子 */ .zz-btn-box{text-align: center; margin-top: 20px; font-size: 14px;} .zz-btn-box button{padding: 5px 10px; cursor: pointer; border: 1px #ccc solid; color: #999;} .zz-btn-box button:hover{box-shadow: 0 0 10px #ddd;} /* -------- 使用公司 --------- */ .com-box-f{padding: 1em 1em; padding-bottom: 30px; text-align: center;} .com-box-f h2{font-size: 30px; color: #000; font-weight: 400;} .com-box{display: flex; flex-wrap: wrap; width: 100%; margin-bottom: 50px; justify-content: flex-start;} .com-box a{display: block; flex: 0 0 13%; margin: 5px; cursor: pointer; border: 0px #ddd solid;} .com-box a{line-height: 75px;} .com-box a img{transition: transform 0.2s !important; vertical-align: middle; min-width: 60%; max-width: 100%; max-height: 100%;} .com-box a img:hover{transform: scale(1.05, 1.05);} .com-box-you a img:hover{transform: none;} /* -------- 友情链接 --------- */ .com-box-you a{flex: 0 0 14.5%; line-height: 60px; height: 60px; margin: 10px;} .com-box-you a img{min-width: 60%; max-width: 85%; vertical-align: middle; max-height: 100%;} /* -------- Dromara 成员项目 --------- */ .table-show-pj{border: 1px #d5d5d5 solid; border-width: 1px 0 0 1px ;} .table-show-pj a{flex: 0 0 16.5%; border: 1px #d5d5d5 solid; margin: 0; padding: 7px 0; overflow: hidden;} .table-show-pj a{border-width: 0 1px 1px 0px;} .table-show-pj a img{min-width: 60%; max-width: 70%; } /* -------- 底部 - 连接 --------- */ #footer{background-color: #181818;} #footer h3{font-weight: 400; font-size: 16px; color: #ccc; margin-top: 20px; margin-bottom: 20px;} #footer{border-top: 1px #666 solid;} .footer-r-b{display: flex; padding: 40px 0;} .ss-box{display: inline-block; flex: 1; color: #595959; margin: 0 50px; font-size: 14px;} .ss-box a{color: #595959; text-decoration: none;} .ss-box a:hover{color: #EEE; text-decoration: underline;} .ss-box ul{margin: 0; padding: 0;} .ss-box li{list-style: none; line-height: 28px;} /* -------- 底部 - 版权 --------- */ .foot-box{background-color: #000; color: #ddd; padding: 2em 0px; line-height: 28px; overflow: hidden; position: relative; z-index: 100;} .foot-box{border-top: 0px #666 solid;} .foot-box p{text-indent: 1em;} .foot-box b{font-size: 1.1em;} .foot-box a{color: #ddd; font-size: 0.9em;} .foot-box a:hover{text-decoration: underline;} /* -------- 自适应 --------- */ /* 一般的笔记本 */ @media screen and (max-width: 1700px) { .content-box{padding-top: 100px;} .content-box h1{font-size: 80px;} /* 支持特性部分的间距 */ .s-title.s-title-tx{margin-top: 50px; margin-bottom: 20px; /* display: none; */} } /* 一般的手机 */ @media screen and (max-width: 800px) { .s-title{padding: 0 16px;} .s-width{width: 100%;} .logo-box .logo-text,.copyright {display: none;} .main-box{ height: auto;} .content-box{padding-top: 100px;} .content-box h1{font-size: 50px;} .feature-z{padding: 0em; padding-bottom: 50px;} .feature{min-width: 100%;} .com-box-f{padding: 0em;} .com-box{justify-content: space-around;} .com-box a{flex: 0 0 90%;} .s-case-box{justify-content: space-around;} .s-case{flex: 0 0 90%;} .footer-r-b{display: block;} .ss-box{display: block; text-align: center; width: 90%; margin: 0px; padding-left: 1.5em;} /* .s-header{position: static;} */ footer{position: static; line-height: 40px;} /* 手机端不显示广告,和一些其它东西 */ .wwads-cn,.p-none{display:none!important} } /* 手机端不显示广告,和一些其它东西 */ /* @media (max-width: 576px) {.wwads-cn,.p-none{display:none!important}} */ /* 工具栏超链接 展开、收缩div */ .zk-box{display: inline-block; padding-right: 0px; margin-right: -25px;} /* 外层盒 */ .zk-box .zk-context{max-height: 0px; position: absolute; overflow: hidden;} .zk-box:hover .zk-context{max-height: 400px;} /* 内层盒 */ .zk-context>div{padding: 1em 0.5em 1em 1em; border: 1px #ccc solid; border-radius: 2px; background-color: #FFF; font-size: 12px; transition: all 0.2s; opacity: 0;} .zk-box:hover .zk-context>div{opacity: 1;} /* 小链接 */ .zk-box .zk-context a{font-size: 14px; display: block; line-height: 32px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;} .zk-box .zk-context a{text-align: left; padding: 0 1.5em 0 1em;} .zk-box .zk-context .zk-fengexian{border-bottom: 1px #d9d9d9 solid; margin: 10px 0;} /* 下三角小图标 */ .zk-icon{display: inline-block; width: 0px; height: 0px; position: relative;top: 3px; margin-left: 4px;} .zk-icon{border-style: solid; border-width: 5px; border-color: #aaa transparent transparent transparent; } /* ------------- 背景色相关 ------------- */ /* 侧边栏需要透明 */ .sidebar-toggle{background-color: transparent !important;} .sidebar{background-color: transparent !important;} /* 变色的动画 */ .doc-header,body{transition: all 0.5s !important;} /* 调色按钮 */ .theme-btn{width: 25px; height: 25px; line-height: 60px; vertical-align: middle; position: relative; top: -1px;} .theme-box{width: 160px; text-align: left; line-height: 20px; margin-top: -20px; white-space: normal;} .theme-box span{ display: inline-block; width: 20px; height: 20px; margin: 1px 2px; border: 1px #ccc solid; cursor: pointer; border-radius: 1px; box-sizing: border-box; } /* ajax加载时的转圈圈样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } ================================================ FILE: sa-token-doc/static/is-fill-in-wj-plugin.js ================================================ // // 声明 docsify 插件 var isFillInWjPlugin = function(hook, vm) { // 钩子函数:解析之前执行 hook.beforeEach(function(content) { return content; }); // 钩子函数:每次路由切换时,解析内容之后执行 hook.afterEach(function(html) { return html; }); // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 hook.doneEach(function() { isFillIn(vm); }); // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 hook.ready(function() { }); } // 检查成功后,多少天不再检查 const wjAllowDisparity = 1000 * 60 * 60 * 24 * 30 * 3; // const allowDisparity = 1000 * 10; // 判断当前是否已填写 function isFillIn(vm) { // 非PC端不检查 if(document.body.offsetWidth < 800) { console.log('small screen ... wj '); return; } // 白名单路由不判断 const whiteList = ['/', '/more/link', '/more/demand-commit', '/more/join-group', '/more/sa-token-donate', '/more/wenjuan', '/sso/sso-pro', '/more/update-log', '/more/common-questions', '/fun/sa-token-test', '/fun/issue-template']; if(whiteList.indexOf(vm.route.path) >= 0) { console.log('white route ... wj'); return; } // 判断是否近期已经判断过了 try{ const isFillIn = localStorage.isFillIn; if(isFillIn) { // 记录 star 的时间,和当前时间的差距 const disparity = new Date().getTime() - parseInt(isFillIn); // 差距小于一月,不再检测,大于一月,再检测一下 if(disparity < wjAllowDisparity) { console.log('checked ... wj '); return; } } }catch(e){ console.error(e); } // 本次打开页面的内存内已经弹出了的话,也不再弹了 if(window.isYtcXsjfkasjda) { return; } window.isYtcXsjfkasjda = true; // 弹出弹框,邀请填写 const tipStr = `

    嗨,同学你好!

    我们想以运营一款产品的心态来运营一个开源框架,所以我们迫切希望您能够填写这份问卷,这有 6 道选择题, 应该只会略微占用您 1~3 分钟的时间。

    问卷地址:https://wj.qq.com/s2/14587150/b5b4/

    Sa-Token 将会非常重视每一位粉丝的宝贵意见!😇😇😇

    `; const index = layer.confirm(tipStr, { title: '问卷调查填写邀请', btn: ['我已填写 (1月内不再弹出)', '暂时不要 (1天内不再弹出)'], // btn: ['同意授权检测', '暂时不要,我先看看文档'], area: '480px', offset: '30%' }, // 点击确定 function(index) { layer.close(index); localStorage.isFillIn = new Date().getTime(); layer.msg('感谢你的支持,Sa-Token 将努力变得更加完善! ❤️ ❤️ ❤️ ') }, // 点击取消 function(){ // 一天内不再检查 const ygTime = allowDisparity - (1000 * 60 * 60 * 24); localStorage.isFillIn = new Date().getTime() - ygTime; layer.alert('你可以随时在右上角 [ 相关资源 -> 问卷调查 ] 处找到问卷链接', function(index) { layer.close(index); }) } ); } ================================================ FILE: sa-token-doc/static/is-star-plugin.js ================================================ // // 声明 docsify 插件 var isStarPlugin = function(hook, vm) { // 钩子函数:解析之前执行 hook.beforeEach(function(content) { return content; }); // 钩子函数:每次路由切换时,解析内容之后执行 hook.afterEach(function(html) { return html; }); // 钩子函数:每次路由切换时数据全部加载完成后调用,没有参数。 hook.doneEach(function() { //isStarRepo(vm); }); // 钩子函数:初始化并第一次加载完成数据后调用,没有参数。 hook.ready(function() { }); } // 应用参数 const client_id = '0cc618beb08db99bff50e500e38c2144d95ada9abb51c00c44592726ecd583f4'; const client_secret = 'xxx'; const redirect_uri = 'https://sa-token.cc/doc.html'; const docDomain = 'sa-token.cc'; // const redirect_uri = 'http://127.0.0.1:8848/sa-token-doc/doc.html'; // const docDomain = '127.0.0.1:8848'; // 检查成功后,多少天不再检查 const allowDisparity = 1000 * 60 * 60 * 24 * 30 * 3; // const allowDisparity = 1000 * 10; // 判断当前是否已 star function isStarRepo(vm) { // 非PC端不检查 if(document.body.offsetWidth < 800) { console.log('small screen ...'); return; } // 判断是否在主域名下 if(location.host !== docDomain) { console.log('not domain, no check...'); return; } // 判断是否近期已经判断过了 try{ const isStarRepo = localStorage.isStarRepo; if(isStarRepo) { // 记录 star 的时间,和当前时间的差距 const disparity = new Date().getTime() - parseInt(isStarRepo); // 差距小于一月,不再检测,大于一月,再检测一下 if(disparity < allowDisparity) { console.log('checked ...'); return; } } }catch(e){ console.error(e); } // 白名单路由不判断 const whiteList = ['/a', '/more/link', '/more/demand-commit', '/more/join-group', '/more/sa-token-donate', '/sso/sso-pro', '/more/update-log', '/more/common-questions', '/fun/sa-token-test', '/fun/issue-template']; if(whiteList.indexOf(vm.route.path) >= 0 && getParam('code') === null) { console.log('white route ...'); return; } // 开始获取 code $('body').css({'overflow': 'hidden'}); getCode(); } // 去请求授权 function getCode() { // 检查url中是否有code const code = getParam('code'); if(code) { // 有 code,进一步去请求 access_token getAccessToken(code); } else { // 不存在code,弹窗提示询问 confirmStar(); } } // 弹窗提示点 star function confirmStar() { // 弹窗提示文字 const tipStr = `

    嗨,同学,来支持一下 Sa-Token 吧,为项目点个 star !

    仅需两步即可完成:
    1、打开 Sa-Token 开源仓库主页,在右上角点个 star 。
    2、点击下方 [ 同意授权检测 ] 按钮,同意 Sa-Token 获取 API 权限进行检测。

    本章节文档将在 star 后正常开放展示。

    开源不易,希望您不吝支持,激励开源项目走的更加长远 😇😇😇

    `; const index = layer.confirm(tipStr, { title: '提示', btn: ['同意授权检测'], // btn: ['同意授权检测', '暂时不要,我先看看文档'], area: '460px', offset: '25%', closeBtn: false }, function(index) { // layer.close(index); // 用户点了确认,去 gitee 官方请求授权获取 goAuth(); } ); // 源码注释提示 const closeLayer = ` `; $('#layui-layer' + index).prepend(closeLayer) } // 跳转到 gitee 授权界面 function goAuth() { const authUrl = "https://gitee.com/oauth/authorize" + "?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&response_type=code"; location.href = authUrl; } // 获取 access_token function getAccessToken(code) { // 根据 code 获取 access_token $.ajax({ url: 'https://sa-token.cc/server/oauth/token', method: 'post', data: { grant_type: 'authorization_code', code: code, client_id: client_id, redirect_uri: redirect_uri, client_secret: client_secret, }, success: function(res) { // 如果返回的不是 200 if(res.code !== 200) { return layer.alert(res.msg, {closeBtn: false}, function(){ // 刷新url,去掉 code 参数 location.href = 'doc.html'; }); } // 拿到 access_token const access_token = res.access_token; // 根据 access_token 判断是否 star 了仓库 $.ajax({ url: 'https://gitee.com/api/v5/user/starred/dromara/sa-token', method: 'get', data: { access_token: access_token }, success: function(res) { // success 回调即代表已经 star,gitee API 请求体不返回任何数据 console.log('-> stared ...'); // 记录本次检查时间 localStorage.isStarRepo = new Date().getTime(); // layer.alert('感谢你的支持 ❤️ ❤️ ❤️ ,Sa-Token 将努力变得更加完善!', function(index) { layer.close(index); // 刷新url,去掉 code 参数 location.href = location.href.replace("?code=" + code, ''); }) }, error: function(e) { // console.log('ff请求错误 ', e); // 如下返回,代表没有 star if(e.statusText = 'Not Found'){ console.log('not star ...'); layer.alert('未检测到 star 数据...', {closeBtn: false}, function() { // 刷新url,去掉 code 参数 location.href = location.href.replace("?code=" + code, ''); }); } } }); }, error: function(e) { console.log('请求错误 ', e); // 如果请求地址有错,可能是服务器宕机了,暂停一天检测 if(e.status === 0 || e.status === 502) { return layer.alert(JSON.stringify(e), {closeBtn: false}, function(){ // 一天内不再检查 const ygTime = allowDisparity - (1000 * 60 * 60 * 24); localStorage.isStarRepo = new Date().getTime() - ygTime; // 刷新 url,去掉 code 参数 location.href = location.href.replace("?code=" + code, ''); }); } // 无效授权,可能是 code 无效 const errorMsg = (e.responseJSON && e.responseJSON.error) || JSON.stringify(e); if(errorMsg == 'invalid_grant') { console.log('无效code', code); } layer.alert('check error... ' + errorMsg, function(index) { layer.close(index); // 刷新url,去掉 code 参数 let url = location.href.replace("?code=" + code, ''); url = url.replace("&code=" + code, ''); location.href = url; }); } }) } // 疑问 function authDetails() { const str = "用于检测的凭证信息将仅保存你的浏览器本地,Sa-Token 文档已完整开源,源码可查"; alert(str); } // 获取 url 携带的参数 function getParam(name, defaultValue){ var query = window.location.search.substring(1); var vars = query.split("&"); for (var i=0;i settings.failure_limit) { return false; } } }); } if(options) { /* Maintain BC for a couple of versions. */ if (undefined !== options.failurelimit) { options.failure_limit = options.failurelimit; delete options.failurelimit; } if (undefined !== options.effectspeed) { options.effect_speed = options.effectspeed; delete options.effectspeed; } $.extend(settings, options); } /* Cache container as jQuery as object. */ $container = (settings.container === undefined || settings.container === window) ? $window : $(settings.container); /* Fire one scroll event per scroll. Not one scroll event per image. */ if (0 === settings.event.indexOf("scroll")) { $container.bind(settings.event, function() { return update(); }); } this.each(function() { var self = this; var $self = $(self); self.loaded = false; /* If no src attribute given use data:uri. */ if ($self.attr("src") === undefined || $self.attr("src") === false) { if ($self.is("img")) { $self.attr("src", settings.placeholder); } } /* When appear is triggered load original image. */ $self.one("appear", function() { if (!this.loaded) { if (settings.appear) { var elements_left = elements.length; settings.appear.call(self, elements_left, settings); } $("") .bind("load", function() { var original = $self.attr("data-" + settings.data_attribute); $self.hide(); if ($self.is("img")) { $self.attr("src", original); } else { $self.css("background-image", "url('" + original + "')"); } $self[settings.effect](settings.effect_speed); self.loaded = true; /* Remove image from array so it is not looped next time. */ var temp = $.grep(elements, function(element) { return !element.loaded; }); elements = $(temp); if (settings.load) { var elements_left = elements.length; settings.load.call(self, elements_left, settings); } }) .attr("src", $self.attr("data-" + settings.data_attribute)); } }); /* When wanted event is triggered load original image */ /* by triggering appear. */ if (0 !== settings.event.indexOf("scroll")) { $self.bind(settings.event, function() { if (!self.loaded) { $self.trigger("appear"); } }); } }); /* Check if something appears when window is resized. */ $window.bind("resize", function() { update(); }); /* With IOS5 force loading images when navigating with back button. */ /* Non optimal workaround. */ if ((/(?:iphone|ipod|ipad).*os 5/gi).test(navigator.appVersion)) { $window.bind("pageshow", function(event) { if (event.originalEvent && event.originalEvent.persisted) { elements.each(function() { $(this).trigger("appear"); }); } }); } /* Force initial check if images should appear. */ $(document).ready(function() { update(); }); return this; }; /* Convenience methods in jQuery namespace. */ /* Use as $.belowthefold(element, {threshold : 100, container : window}) */ $.belowthefold = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = (window.innerHeight ? window.innerHeight : $window.height()) + $window.scrollTop(); } else { fold = $(settings.container).offset().top + $(settings.container).height(); } return fold <= $(element).offset().top - settings.threshold; }; $.rightoffold = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.width() + $window.scrollLeft(); } else { fold = $(settings.container).offset().left + $(settings.container).width(); } return fold <= $(element).offset().left - settings.threshold; }; $.abovethetop = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.scrollTop(); } else { fold = $(settings.container).offset().top; } return fold >= $(element).offset().top + settings.threshold + $(element).height(); }; $.leftofbegin = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.scrollLeft(); } else { fold = $(settings.container).offset().left; } return fold >= $(element).offset().left + settings.threshold + $(element).width(); }; $.inviewport = function(element, settings) { return !$.rightoffold(element, settings) && !$.leftofbegin(element, settings) && !$.belowthefold(element, settings) && !$.abovethetop(element, settings); }; /* Custom selectors for your convenience. */ /* Use as $("img:below-the-fold").something() or */ /* $("img").filter(":below-the-fold").something() which is faster */ $.extend($.expr[":"], { "below-the-fold" : function(a) { return $.belowthefold(a, {threshold : 0}); }, "above-the-top" : function(a) { return !$.belowthefold(a, {threshold : 0}); }, "right-of-screen": function(a) { return $.rightoffold(a, {threshold : 0}); }, "left-of-screen" : function(a) { return !$.rightoffold(a, {threshold : 0}); }, "in-viewport" : function(a) { return $.inviewport(a, {threshold : 0}); }, /* Maintain BC for couple of versions. */ "above-the-fold" : function(a) { return !$.belowthefold(a, {threshold : 0}); }, "right-of-fold" : function(a) { return $.rightoffold(a, {threshold : 0}); }, "left-of-fold" : function(a) { return !$.rightoffold(a, {threshold : 0}); } }); })(jQuery, window, document); ================================================ FILE: sa-token-doc/static/layer-v3.1.1/layer.js ================================================ /*! layer-v3.1.1 Web弹层组件 MIT License http://layer.layui.com/ By 贤心 */ ;!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
    '+(f?r.title[0]:r.title)+"
    ":"";return r.zIndex=s,t([r.shade?'
    ':"",'
    '+(e&&2!=r.type?"":u)+'
    '+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
    '+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
    '+e+"
    "}():"")+(r.resize?'':"")+"
    "],u,i('
    ')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
      '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
    • '+(t[0].content||"no content")+"
    • ";i'+(t[i].content||"no content")+"";return a}()+"
    ",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
    '+(u.length>1?'':"")+'
    '+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
    ",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
    是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); ================================================ FILE: sa-token-doc/static/layer-v3.1.1/mobile/layer.js ================================================ /*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

    '+(e?n.title[0]:n.title)+"

    ":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
    '+e+"
    "):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

    '+(n.content||"")+"

    "),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
    ':"")+'
    "+l+'
    '+n.content+"
    "+c+"
    ",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} ================================================ FILE: sa-token-doc/static/layer-v3.1.1/theme/default/layer.css ================================================ .layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1E9FFF;background-color:#1E9FFF;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#E9E7E7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:230px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;overflow:hidden;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:43px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} ================================================ FILE: sa-token-doc/static/page-com/github-stars-vs/echarts.min-5.4.3.js ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).echarts={})}(this,(function(t){"use strict"; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},e(t,n)};function n(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var i=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},r=new function(){this.browser=new i,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(r.wxa=!0,r.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?r.worker=!0:"undefined"==typeof navigator?(r.node=!0,r.svgSupported=!0):function(t,e){var n=e.browser,i=t.match(/Firefox\/([\d.]+)/),r=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),o=t.match(/Edge?\/([\d.]+)/),a=/micromessenger/i.test(t);i&&(n.firefox=!0,n.version=i[1]);r&&(n.ie=!0,n.version=r[1]);o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),e.domSupported="undefined"!=typeof document;var s=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,r);var o="sans-serif",a="12px "+o;var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n=0)o=r*t.length;else for(var c=0;c>1)%2;a.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",r[l]+":0",i[1-s]+":auto",r[1-l]+":auto",""].join("!important;"),t.appendChild(a),n.push(a)}return n}(e,a),l=function(t,e,n){for(var i=n?"invTrans":"trans",r=e[i],o=e.srcCoords,a=[],s=[],l=!0,u=0;u<4;u++){var h=t[u].getBoundingClientRect(),c=2*u,p=h.left,d=h.top;a.push(p,d),l=l&&o&&p===o[c]&&d===o[c+1],s.push(t[u].offsetLeft,t[u].offsetTop)}return l&&r?r:(e.srcCoords=a,e[i]=n?$t(s,a):$t(a,s))}(s,a,o);if(l)return l(t,n,i),!0}return!1}function ee(t){return"CANVAS"===t.nodeName.toUpperCase()}var ne=/([&<>"'])/g,ie={"&":"&","<":"<",">":">",'"':""","'":"'"};function re(t){return null==t?"":(t+"").replace(ne,(function(t,e){return ie[e]}))}var oe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ae=[],se=r.browser.firefox&&+r.browser.version.split(".")[0]<39;function le(t,e,n,i){return n=n||{},i?ue(t,e,n):se&&null!=e.layerX&&e.layerX!==e.offsetX?(n.zrX=e.layerX,n.zrY=e.layerY):null!=e.offsetX?(n.zrX=e.offsetX,n.zrY=e.offsetY):ue(t,e,n),n}function ue(t,e,n){if(r.domSupported&&t.getBoundingClientRect){var i=e.clientX,o=e.clientY;if(ee(t)){var a=t.getBoundingClientRect();return n.zrX=i-a.left,void(n.zrY=o-a.top)}if(te(ae,t,i,o))return n.zrX=ae[0],void(n.zrY=ae[1])}n.zrX=n.zrY=0}function he(t){return t||window.event}function ce(t,e,n){if(null!=(e=he(e)).zrX)return e;var i=e.type;if(i&&i.indexOf("touch")>=0){var r="touchend"!==i?e.targetTouches[0]:e.changedTouches[0];r&&le(t,r,e,n)}else{le(t,e,e,n);var o=function(t){var e=t.wheelDelta;if(e)return e;var n=t.deltaX,i=t.deltaY;if(null==n||null==i)return e;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(e);e.zrDelta=o?o/120:-(e.detail||0)/3}var a=e.button;return null==e.which&&void 0!==a&&oe.test(e.type)&&(e.which=1&a?1:2&a?3:4&a?2:0),e}function pe(t,e,n,i){t.addEventListener(e,n,i)}var de=function(t){t.preventDefault(),t.stopPropagation(),t.cancelBubble=!0};function fe(t){return 2===t.which||3===t.which}var ge=function(){function t(){this._track=[]}return t.prototype.recognize=function(t,e,n){return this._doTrack(t,e,n),this._recognize(t)},t.prototype.clear=function(){return this._track.length=0,this},t.prototype._doTrack=function(t,e,n){var i=t.touches;if(i){for(var r={points:[],touches:[],target:e,event:t},o=0,a=i.length;o1&&r&&r.length>1){var a=ye(r)/ye(o);!isFinite(a)&&(a=1),e.pinchScale=a;var s=[((i=r)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return e.pinchX=s[0],e.pinchY=s[1],{type:"pinch",target:t[0].target,event:e}}}}};function me(){return[1,0,0,1,0,0]}function xe(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t}function _e(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t}function be(t,e,n){var i=e[0]*n[0]+e[2]*n[1],r=e[1]*n[0]+e[3]*n[1],o=e[0]*n[2]+e[2]*n[3],a=e[1]*n[2]+e[3]*n[3],s=e[0]*n[4]+e[2]*n[5]+e[4],l=e[1]*n[4]+e[3]*n[5]+e[5];return t[0]=i,t[1]=r,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t}function we(t,e,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4]+n[0],t[5]=e[5]+n[1],t}function Se(t,e,n){var i=e[0],r=e[2],o=e[4],a=e[1],s=e[3],l=e[5],u=Math.sin(n),h=Math.cos(n);return t[0]=i*h+a*u,t[1]=-i*u+a*h,t[2]=r*h+s*u,t[3]=-r*u+h*s,t[4]=h*o+u*l,t[5]=h*l-u*o,t}function Me(t,e,n){var i=n[0],r=n[1];return t[0]=e[0]*i,t[1]=e[1]*r,t[2]=e[2]*i,t[3]=e[3]*r,t[4]=e[4]*i,t[5]=e[5]*r,t}function Ie(t,e){var n=e[0],i=e[2],r=e[4],o=e[1],a=e[3],s=e[5],l=n*a-o*i;return l?(l=1/l,t[0]=a*l,t[1]=-o*l,t[2]=-i*l,t[3]=n*l,t[4]=(i*s-a*r)*l,t[5]=(o*r-n*s)*l,t):null}function Te(t){var e=[1,0,0,1,0,0];return _e(e,t),e}var Ce=Object.freeze({__proto__:null,create:me,identity:xe,copy:_e,mul:be,translate:we,rotate:Se,scale:Me,invert:Ie,clone:Te}),De=function(){function t(t,e){this.x=t||0,this.y=e||0}return t.prototype.copy=function(t){return this.x=t.x,this.y=t.y,this},t.prototype.clone=function(){return new t(this.x,this.y)},t.prototype.set=function(t,e){return this.x=t,this.y=e,this},t.prototype.equal=function(t){return t.x===this.x&&t.y===this.y},t.prototype.add=function(t){return this.x+=t.x,this.y+=t.y,this},t.prototype.scale=function(t){this.x*=t,this.y*=t},t.prototype.scaleAndAdd=function(t,e){this.x+=t.x*e,this.y+=t.y*e},t.prototype.sub=function(t){return this.x-=t.x,this.y-=t.y,this},t.prototype.dot=function(t){return this.x*t.x+this.y*t.y},t.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},t.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},t.prototype.normalize=function(){var t=this.len();return this.x/=t,this.y/=t,this},t.prototype.distance=function(t){var e=this.x-t.x,n=this.y-t.y;return Math.sqrt(e*e+n*n)},t.prototype.distanceSquare=function(t){var e=this.x-t.x,n=this.y-t.y;return e*e+n*n},t.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},t.prototype.transform=function(t){if(t){var e=this.x,n=this.y;return this.x=t[0]*e+t[2]*n+t[4],this.y=t[1]*e+t[3]*n+t[5],this}},t.prototype.toArray=function(t){return t[0]=this.x,t[1]=this.y,t},t.prototype.fromArray=function(t){this.x=t[0],this.y=t[1]},t.set=function(t,e,n){t.x=e,t.y=n},t.copy=function(t,e){t.x=e.x,t.y=e.y},t.len=function(t){return Math.sqrt(t.x*t.x+t.y*t.y)},t.lenSquare=function(t){return t.x*t.x+t.y*t.y},t.dot=function(t,e){return t.x*e.x+t.y*e.y},t.add=function(t,e,n){t.x=e.x+n.x,t.y=e.y+n.y},t.sub=function(t,e,n){t.x=e.x-n.x,t.y=e.y-n.y},t.scale=function(t,e,n){t.x=e.x*n,t.y=e.y*n},t.scaleAndAdd=function(t,e,n,i){t.x=e.x+n.x*i,t.y=e.y+n.y*i},t.lerp=function(t,e,n,i){var r=1-i;t.x=r*e.x+i*n.x,t.y=r*e.y+i*n.y},t}(),Ae=Math.min,ke=Math.max,Le=new De,Pe=new De,Oe=new De,Re=new De,Ne=new De,Ee=new De,ze=function(){function t(t,e,n,i){n<0&&(t+=n,n=-n),i<0&&(e+=i,i=-i),this.x=t,this.y=e,this.width=n,this.height=i}return t.prototype.union=function(t){var e=Ae(t.x,this.x),n=Ae(t.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=ke(t.x+t.width,this.x+this.width)-e:this.width=t.width,isFinite(this.y)&&isFinite(this.height)?this.height=ke(t.y+t.height,this.y+this.height)-n:this.height=t.height,this.x=e,this.y=n},t.prototype.applyTransform=function(e){t.applyTransform(this,this,e)},t.prototype.calculateTransform=function(t){var e=this,n=t.width/e.width,i=t.height/e.height,r=[1,0,0,1,0,0];return we(r,r,[-e.x,-e.y]),Me(r,r,[n,i]),we(r,r,[t.x,t.y]),r},t.prototype.intersect=function(e,n){if(!e)return!1;e instanceof t||(e=t.create(e));var i=this,r=i.x,o=i.x+i.width,a=i.y,s=i.y+i.height,l=e.x,u=e.x+e.width,h=e.y,c=e.y+e.height,p=!(of&&(f=x,gf&&(f=_,v=n.x&&t<=n.x+n.width&&e>=n.y&&e<=n.y+n.height},t.prototype.clone=function(){return new t(this.x,this.y,this.width,this.height)},t.prototype.copy=function(e){t.copy(this,e)},t.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},t.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},t.prototype.isZero=function(){return 0===this.width||0===this.height},t.create=function(e){return new t(e.x,e.y,e.width,e.height)},t.copy=function(t,e){t.x=e.x,t.y=e.y,t.width=e.width,t.height=e.height},t.applyTransform=function(e,n,i){if(i){if(i[1]<1e-5&&i[1]>-1e-5&&i[2]<1e-5&&i[2]>-1e-5){var r=i[0],o=i[3],a=i[4],s=i[5];return e.x=n.x*r+a,e.y=n.y*o+s,e.width=n.width*r,e.height=n.height*o,e.width<0&&(e.x+=e.width,e.width=-e.width),void(e.height<0&&(e.y+=e.height,e.height=-e.height))}Le.x=Oe.x=n.x,Le.y=Re.y=n.y,Pe.x=Re.x=n.x+n.width,Pe.y=Oe.y=n.y+n.height,Le.transform(i),Re.transform(i),Pe.transform(i),Oe.transform(i),e.x=Ae(Le.x,Pe.x,Oe.x,Re.x),e.y=Ae(Le.y,Pe.y,Oe.y,Re.y);var l=ke(Le.x,Pe.x,Oe.x,Re.x),u=ke(Le.y,Pe.y,Oe.y,Re.y);e.width=l-e.x,e.height=u-e.y}else e!==n&&t.copy(e,n)},t}(),Ve="silent";function Be(){de(this.event)}var Fe=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.handler=null,e}return n(e,t),e.prototype.dispose=function(){},e.prototype.setCursor=function(){},e}(jt),Ge=function(t,e){this.x=t,this.y=e},We=["click","dblclick","mousewheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],He=new ze(0,0,0,0),Ye=function(t){function e(e,n,i,r,o){var a=t.call(this)||this;return a._hovered=new Ge(0,0),a.storage=e,a.painter=n,a.painterRoot=r,a._pointerSize=o,i=i||new Fe,a.proxy=null,a.setHandlerProxy(i),a._draggingMgr=new Zt(a),a}return n(e,t),e.prototype.setHandlerProxy=function(t){this.proxy&&this.proxy.dispose(),t&&(E(We,(function(e){t.on&&t.on(e,this[e],this)}),this),t.handler=this),this.proxy=t},e.prototype.mousemove=function(t){var e=t.zrX,n=t.zrY,i=Ze(this,e,n),r=this._hovered,o=r.target;o&&!o.__zr&&(o=(r=this.findHover(r.x,r.y)).target);var a=this._hovered=i?new Ge(e,n):this.findHover(e,n),s=a.target,l=this.proxy;l.setCursor&&l.setCursor(s?s.cursor:"default"),o&&s!==o&&this.dispatchToElement(r,"mouseout",t),this.dispatchToElement(a,"mousemove",t),s&&s!==o&&this.dispatchToElement(a,"mouseover",t)},e.prototype.mouseout=function(t){var e=t.zrEventControl;"only_globalout"!==e&&this.dispatchToElement(this._hovered,"mouseout",t),"no_globalout"!==e&&this.trigger("globalout",{type:"globalout",event:t})},e.prototype.resize=function(){this._hovered=new Ge(0,0)},e.prototype.dispatch=function(t,e){var n=this[t];n&&n.call(this,e)},e.prototype.dispose=function(){this.proxy.dispose(),this.storage=null,this.proxy=null,this.painter=null},e.prototype.setCursorStyle=function(t){var e=this.proxy;e.setCursor&&e.setCursor(t)},e.prototype.dispatchToElement=function(t,e,n){var i=(t=t||{}).target;if(!i||!i.silent){for(var r="on"+e,o=function(t,e,n){return{type:t,event:n,target:e.target,topTarget:e.topTarget,cancelBubble:!1,offsetX:n.zrX,offsetY:n.zrY,gestureEvent:n.gestureEvent,pinchX:n.pinchX,pinchY:n.pinchY,pinchScale:n.pinchScale,wheelDelta:n.zrDelta,zrByTouch:n.zrByTouch,which:n.which,stop:Be}}(e,t,n);i&&(i[r]&&(o.cancelBubble=!!i[r].call(i,o)),i.trigger(e,o),i=i.__hostTarget?i.__hostTarget:i.parent,!o.cancelBubble););o.cancelBubble||(this.trigger(e,o),this.painter&&this.painter.eachOtherLayer&&this.painter.eachOtherLayer((function(t){"function"==typeof t[r]&&t[r].call(t,o),t.trigger&&t.trigger(e,o)})))}},e.prototype.findHover=function(t,e,n){var i=this.storage.getDisplayList(),r=new Ge(t,e);if(Ue(i,r,t,e,n),this._pointerSize&&!r.target){for(var o=[],a=this._pointerSize,s=a/2,l=new ze(t-s,e-s,a,a),u=i.length-1;u>=0;u--){var h=i[u];h===n||h.ignore||h.ignoreCoarsePointer||h.parent&&h.parent.ignoreCoarsePointer||(He.copy(h.getBoundingRect()),h.transform&&He.applyTransform(h.transform),He.intersect(l)&&o.push(h))}if(o.length)for(var c=Math.PI/12,p=2*Math.PI,d=0;d=0;o--){var a=t[o],s=void 0;if(a!==r&&!a.ignore&&(s=Xe(a,n,i))&&(!e.topTarget&&(e.topTarget=a),s!==Ve)){e.target=a;break}}}function Ze(t,e,n){var i=t.painter;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}E(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],(function(t){Ye.prototype[t]=function(e){var n,i,r=e.zrX,o=e.zrY,a=Ze(this,r,o);if("mouseup"===t&&a||(i=(n=this.findHover(r,o)).target),"mousedown"===t)this._downEl=i,this._downPoint=[e.zrX,e.zrY],this._upEl=i;else if("mouseup"===t)this._upEl=i;else if("click"===t){if(this._downEl!==this._upEl||!this._downPoint||Vt(this._downPoint,[e.zrX,e.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,t,e)}}));function je(t,e,n,i){var r=e+1;if(r===n)return 1;if(i(t[r++],t[e])<0){for(;r=0;)r++;return r-e}function qe(t,e,n,i,r){for(i===e&&i++;i>>1])<0?l=o:s=o+1;var u=i-s;switch(u){case 3:t[s+3]=t[s+2];case 2:t[s+2]=t[s+1];case 1:t[s+1]=t[s];break;default:for(;u>0;)t[s+u]=t[s+u-1],u--}t[s]=a}}function Ke(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])>0){for(s=i-r;l0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a>>1);o(t,e[n+h])>0?a=h+1:l=h}return l}function $e(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])<0){for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a>>1);o(t,e[n+h])<0?l=h:a=h+1}return l}function Je(t,e){var n,i,r=7,o=0;t.length;var a=[];function s(s){var l=n[s],u=i[s],h=n[s+1],c=i[s+1];i[s]=u+c,s===o-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),o--;var p=$e(t[h],t,l,u,0,e);l+=p,0!==(u-=p)&&0!==(c=Ke(t[l+u-1],t,h,c,c-1,e))&&(u<=c?function(n,i,o,s){var l=0;for(l=0;l=7||d>=7);if(f)break;g<0&&(g=0),g+=2}if((r=g)<1&&(r=1),1===i){for(l=0;l=0;l--)t[d+l]=t[p+l];return void(t[c]=a[h])}var f=r;for(;;){var g=0,y=0,v=!1;do{if(e(a[h],t[u])<0){if(t[c--]=t[u--],g++,y=0,0==--i){v=!0;break}}else if(t[c--]=a[h--],y++,g=0,1==--s){v=!0;break}}while((g|y)=0;l--)t[d+l]=t[p+l];if(0===i){v=!0;break}}if(t[c--]=a[h--],1==--s){v=!0;break}if(0!==(y=s-Ke(t[u],a,0,s,s-1,e))){for(s-=y,d=(c-=y)+1,p=(h-=y)+1,l=0;l=7||y>=7);if(v)break;f<0&&(f=0),f+=2}(r=f)<1&&(r=1);if(1===s){for(d=(c-=i)+1,p=(u-=i)+1,l=i-1;l>=0;l--)t[d+l]=t[p+l];t[c]=a[h]}else{if(0===s)throw new Error;for(p=c-(s-1),l=0;l1;){var t=o-2;if(t>=1&&i[t-1]<=i[t]+i[t+1]||t>=2&&i[t-2]<=i[t]+i[t-1])i[t-1]i[t+1])break;s(t)}},forceMergeRuns:function(){for(;o>1;){var t=o-2;t>0&&i[t-1]=32;)e|=1&t,t>>=1;return t+e}(r);do{if((o=je(t,n,i,e))s&&(l=s),qe(t,n,n+l,n+o,e),o=l}a.pushRun(n,o),a.mergeRuns(),r-=o,n+=o}while(0!==r);a.forceMergeRuns()}}}var tn=!1;function en(){tn||(tn=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function nn(t,e){return t.zlevel===e.zlevel?t.z===e.z?t.z2-e.z2:t.z-e.z:t.zlevel-e.zlevel}var rn=function(){function t(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=nn}return t.prototype.traverse=function(t,e){for(var n=0;n0&&(u.__clipPaths=[]),isNaN(u.z)&&(en(),u.z=0),isNaN(u.z2)&&(en(),u.z2=0),isNaN(u.zlevel)&&(en(),u.zlevel=0),this._displayList[this._displayListLen++]=u}var h=t.getDecalElement&&t.getDecalElement();h&&this._updateAndAddDisplayable(h,e,n);var c=t.getTextGuideLine();c&&this._updateAndAddDisplayable(c,e,n);var p=t.getTextContent();p&&this._updateAndAddDisplayable(p,e,n)}},t.prototype.addRoot=function(t){t.__zr&&t.__zr.storage===this||this._roots.push(t)},t.prototype.delRoot=function(t){if(t instanceof Array)for(var e=0,n=t.length;e=0&&this._roots.splice(i,1)}},t.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},t.prototype.getRoots=function(){return this._roots},t.prototype.dispose=function(){this._displayList=null,this._roots=null},t}(),on=r.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){return setTimeout(t,16)},an={linear:function(t){return t},quadraticIn:function(t){return t*t},quadraticOut:function(t){return t*(2-t)},quadraticInOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)},cubicIn:function(t){return t*t*t},cubicOut:function(t){return--t*t*t+1},cubicInOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},quarticIn:function(t){return t*t*t*t},quarticOut:function(t){return 1- --t*t*t*t},quarticInOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},quinticIn:function(t){return t*t*t*t*t},quinticOut:function(t){return--t*t*t*t*t+1},quinticInOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},sinusoidalIn:function(t){return 1-Math.cos(t*Math.PI/2)},sinusoidalOut:function(t){return Math.sin(t*Math.PI/2)},sinusoidalInOut:function(t){return.5*(1-Math.cos(Math.PI*t))},exponentialIn:function(t){return 0===t?0:Math.pow(1024,t-1)},exponentialOut:function(t){return 1===t?1:1-Math.pow(2,-10*t)},exponentialInOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))},circularIn:function(t){return 1-Math.sqrt(1-t*t)},circularOut:function(t){return Math.sqrt(1- --t*t)},circularInOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},elasticIn:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4))},elasticOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/.4)+1)},elasticInOut:function(t){var e,n=.1,i=.4;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=i*Math.asin(1/n)/(2*Math.PI),(t*=2)<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},backIn:function(t){var e=1.70158;return t*t*((e+1)*t-e)},backOut:function(t){var e=1.70158;return--t*t*((e+1)*t+e)+1},backInOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)},bounceIn:function(t){return 1-an.bounceOut(1-t)},bounceOut:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},bounceInOut:function(t){return t<.5?.5*an.bounceIn(2*t):.5*an.bounceOut(2*t-1)+.5}},sn=Math.pow,ln=Math.sqrt,un=1e-8,hn=1e-4,cn=ln(3),pn=1/3,dn=Mt(),fn=Mt(),gn=Mt();function yn(t){return t>-1e-8&&tun||t<-1e-8}function mn(t,e,n,i,r){var o=1-r;return o*o*(o*t+3*r*e)+r*r*(r*i+3*o*n)}function xn(t,e,n,i,r){var o=1-r;return 3*(((e-t)*o+2*(n-e)*r)*o+(i-n)*r*r)}function _n(t,e,n,i,r,o){var a=i+3*(e-n)-t,s=3*(n-2*e+t),l=3*(e-t),u=t-r,h=s*s-3*a*l,c=s*l-9*a*u,p=l*l-3*s*u,d=0;if(yn(h)&&yn(c)){if(yn(s))o[0]=0;else(M=-l/s)>=0&&M<=1&&(o[d++]=M)}else{var f=c*c-4*h*p;if(yn(f)){var g=c/h,y=-g/2;(M=-s/a+g)>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y)}else if(f>0){var v=ln(f),m=h*s+1.5*a*(-c+v),x=h*s+1.5*a*(-c-v);(M=(-s-((m=m<0?-sn(-m,pn):sn(m,pn))+(x=x<0?-sn(-x,pn):sn(x,pn))))/(3*a))>=0&&M<=1&&(o[d++]=M)}else{var _=(2*h*s-3*a*c)/(2*ln(h*h*h)),b=Math.acos(_)/3,w=ln(h),S=Math.cos(b),M=(-s-2*w*S)/(3*a),I=(y=(-s+w*(S+cn*Math.sin(b)))/(3*a),(-s+w*(S-cn*Math.sin(b)))/(3*a));M>=0&&M<=1&&(o[d++]=M),y>=0&&y<=1&&(o[d++]=y),I>=0&&I<=1&&(o[d++]=I)}}return d}function bn(t,e,n,i,r){var o=6*n-12*e+6*t,a=9*e+3*i-3*t-9*n,s=3*e-3*t,l=0;if(yn(a)){if(vn(o))(h=-s/o)>=0&&h<=1&&(r[l++]=h)}else{var u=o*o-4*a*s;if(yn(u))r[0]=-o/(2*a);else if(u>0){var h,c=ln(u),p=(-o-c)/(2*a);(h=(-o+c)/(2*a))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}function wn(t,e,n,i,r,o){var a=(e-t)*r+t,s=(n-e)*r+e,l=(i-n)*r+n,u=(s-a)*r+a,h=(l-s)*r+s,c=(h-u)*r+u;o[0]=t,o[1]=a,o[2]=u,o[3]=c,o[4]=c,o[5]=h,o[6]=l,o[7]=i}function Sn(t,e,n,i,r,o,a,s,l,u,h){var c,p,d,f,g,y=.005,v=1/0;dn[0]=l,dn[1]=u;for(var m=0;m<1;m+=.05)fn[0]=mn(t,n,r,a,m),fn[1]=mn(e,i,o,s,m),(f=Ft(dn,fn))=0&&f=0&&y=1?1:_n(0,i,o,1,t,s)&&mn(0,r,a,1,s[0])}}}var On=function(){function t(t){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=t.life||1e3,this._delay=t.delay||0,this.loop=t.loop||!1,this.onframe=t.onframe||bt,this.ondestroy=t.ondestroy||bt,this.onrestart=t.onrestart||bt,t.easing&&this.setEasing(t.easing)}return t.prototype.step=function(t,e){if(this._inited||(this._startTime=t+this._delay,this._inited=!0),!this._paused){var n=this._life,i=t-this._startTime-this._pausedTime,r=i/n;r<0&&(r=0),r=Math.min(r,1);var o=this.easingFunc,a=o?o(r):r;if(this.onframe(a),1===r){if(!this.loop)return!0;var s=i%n;this._startTime=t-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=e},t.prototype.pause=function(){this._paused=!0},t.prototype.resume=function(){this._paused=!1},t.prototype.setEasing=function(t){this.easing=t,this.easingFunc=X(t)?t:an[t]||Pn(t)},t}(),Rn=function(t){this.value=t},Nn=function(){function t(){this._len=0}return t.prototype.insert=function(t){var e=new Rn(t);return this.insertEntry(e),e},t.prototype.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},t.prototype.remove=function(t){var e=t.prev,n=t.next;e?e.next=n:this.head=n,n?n.prev=e:this.tail=e,t.next=t.prev=null,this._len--},t.prototype.len=function(){return this._len},t.prototype.clear=function(){this.head=this.tail=null,this._len=0},t}(),En=function(){function t(t){this._list=new Nn,this._maxSize=10,this._map={},this._maxSize=t}return t.prototype.put=function(t,e){var n=this._list,i=this._map,r=null;if(null==i[t]){var o=n.len(),a=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var s=n.head;n.remove(s),delete i[s.key],r=s.value,this._lastRemovedEntry=s}a?a.value=e:a=new Rn(e),a.key=t,n.insertEntry(a),i[t]=a}return r},t.prototype.get=function(t){var e=this._map[t],n=this._list;if(null!=e)return e!==n.tail&&(n.remove(e),n.insertEntry(e)),e.value},t.prototype.clear=function(){this._list.clear(),this._map={}},t.prototype.len=function(){return this._list.len()},t}(),zn={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function Vn(t){return(t=Math.round(t))<0?0:t>255?255:t}function Bn(t){return t<0?0:t>1?1:t}function Fn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?Vn(parseFloat(e)/100*255):Vn(parseInt(e,10))}function Gn(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?Bn(parseFloat(e)/100):Bn(parseFloat(e))}function Wn(t,e,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?t+(e-t)*n*6:2*n<1?e:3*n<2?t+(e-t)*(2/3-n)*6:t}function Hn(t,e,n){return t+(e-t)*n}function Yn(t,e,n,i,r){return t[0]=e,t[1]=n,t[2]=i,t[3]=r,t}function Xn(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}var Un=new En(20),Zn=null;function jn(t,e){Zn&&Xn(Zn,e),Zn=Un.put(t,Zn||e.slice())}function qn(t,e){if(t){e=e||[];var n=Un.get(t);if(n)return Xn(e,n);var i=(t+="").replace(/ /g,"").toLowerCase();if(i in zn)return Xn(e,zn[i]),jn(t,e),e;var r,o=i.length;if("#"===i.charAt(0))return 4===o||5===o?(r=parseInt(i.slice(1,4),16))>=0&&r<=4095?(Yn(e,(3840&r)>>4|(3840&r)>>8,240&r|(240&r)>>4,15&r|(15&r)<<4,5===o?parseInt(i.slice(4),16)/15:1),jn(t,e),e):void Yn(e,0,0,0,1):7===o||9===o?(r=parseInt(i.slice(1,7),16))>=0&&r<=16777215?(Yn(e,(16711680&r)>>16,(65280&r)>>8,255&r,9===o?parseInt(i.slice(7),16)/255:1),jn(t,e),e):void Yn(e,0,0,0,1):void 0;var a=i.indexOf("("),s=i.indexOf(")");if(-1!==a&&s+1===o){var l=i.substr(0,a),u=i.substr(a+1,s-(a+1)).split(","),h=1;switch(l){case"rgba":if(4!==u.length)return 3===u.length?Yn(e,+u[0],+u[1],+u[2],1):Yn(e,0,0,0,1);h=Gn(u.pop());case"rgb":return u.length>=3?(Yn(e,Fn(u[0]),Fn(u[1]),Fn(u[2]),3===u.length?h:Gn(u[3])),jn(t,e),e):void Yn(e,0,0,0,1);case"hsla":return 4!==u.length?void Yn(e,0,0,0,1):(u[3]=Gn(u[3]),Kn(u,e),jn(t,e),e);case"hsl":return 3!==u.length?void Yn(e,0,0,0,1):(Kn(u,e),jn(t,e),e);default:return}}Yn(e,0,0,0,1)}}function Kn(t,e){var n=(parseFloat(t[0])%360+360)%360/360,i=Gn(t[1]),r=Gn(t[2]),o=r<=.5?r*(i+1):r+i-r*i,a=2*r-o;return Yn(e=e||[],Vn(255*Wn(a,o,n+1/3)),Vn(255*Wn(a,o,n)),Vn(255*Wn(a,o,n-1/3)),1),4===t.length&&(e[3]=t[3]),e}function $n(t,e){var n=qn(t);if(n){for(var i=0;i<3;i++)n[i]=e<0?n[i]*(1-e)|0:(255-n[i])*e+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return ri(n,4===n.length?"rgba":"rgb")}}function Jn(t,e,n){if(e&&e.length&&t>=0&&t<=1){n=n||[];var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=e[r],s=e[o],l=i-r;return n[0]=Vn(Hn(a[0],s[0],l)),n[1]=Vn(Hn(a[1],s[1],l)),n[2]=Vn(Hn(a[2],s[2],l)),n[3]=Bn(Hn(a[3],s[3],l)),n}}var Qn=Jn;function ti(t,e,n){if(e&&e.length&&t>=0&&t<=1){var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=qn(e[r]),s=qn(e[o]),l=i-r,u=ri([Vn(Hn(a[0],s[0],l)),Vn(Hn(a[1],s[1],l)),Vn(Hn(a[2],s[2],l)),Bn(Hn(a[3],s[3],l))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:i}:u}}var ei=ti;function ni(t,e,n,i){var r=qn(t);if(t)return r=function(t){if(t){var e,n,i=t[0]/255,r=t[1]/255,o=t[2]/255,a=Math.min(i,r,o),s=Math.max(i,r,o),l=s-a,u=(s+a)/2;if(0===l)e=0,n=0;else{n=u<.5?l/(s+a):l/(2-s-a);var h=((s-i)/6+l/2)/l,c=((s-r)/6+l/2)/l,p=((s-o)/6+l/2)/l;i===s?e=p-c:r===s?e=1/3+h-p:o===s&&(e=2/3+c-h),e<0&&(e+=1),e>1&&(e-=1)}var d=[360*e,n,u];return null!=t[3]&&d.push(t[3]),d}}(r),null!=e&&(r[0]=function(t){return(t=Math.round(t))<0?0:t>360?360:t}(e)),null!=n&&(r[1]=Gn(n)),null!=i&&(r[2]=Gn(i)),ri(Kn(r),"rgba")}function ii(t,e){var n=qn(t);if(n&&null!=e)return n[3]=Bn(e),ri(n,"rgba")}function ri(t,e){if(t&&t.length){var n=t[0]+","+t[1]+","+t[2];return"rgba"!==e&&"hsva"!==e&&"hsla"!==e||(n+=","+t[3]),e+"("+n+")"}}function oi(t,e){var n=qn(t);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*e:0}var ai=Object.freeze({__proto__:null,parse:qn,lift:$n,toHex:function(t){var e=qn(t);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)},fastLerp:Jn,fastMapToColor:Qn,lerp:ti,mapToColor:ei,modifyHSL:ni,modifyAlpha:ii,stringify:ri,lum:oi,random:function(){return ri([Math.round(255*Math.random()),Math.round(255*Math.random()),Math.round(255*Math.random())],"rgb")}}),si=Math.round;function li(t){var e;if(t&&"transparent"!==t){if("string"==typeof t&&t.indexOf("rgba")>-1){var n=qn(t);n&&(t="rgb("+n[0]+","+n[1]+","+n[2]+")",e=n[3])}}else t="none";return{color:t,opacity:null==e?1:e}}var ui=1e-4;function hi(t){return t-1e-4}function ci(t){return si(1e3*t)/1e3}function pi(t){return si(1e4*t)/1e4}var di={left:"start",right:"end",center:"middle",middle:"middle"};function fi(t){return t&&!!t.image}function gi(t){return fi(t)||function(t){return t&&!!t.svgElement}(t)}function yi(t){return"linear"===t.type}function vi(t){return"radial"===t.type}function mi(t){return t&&("linear"===t.type||"radial"===t.type)}function xi(t){return"url(#"+t+")"}function _i(t){var e=t.getGlobalScale(),n=Math.max(e[0],e[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function bi(t){var e=t.x||0,n=t.y||0,i=(t.rotation||0)*wt,r=rt(t.scaleX,1),o=rt(t.scaleY,1),a=t.skewX||0,s=t.skewY||0,l=[];return(e||n)&&l.push("translate("+e+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===r&&1===o||l.push("scale("+r+","+o+")"),(a||s)&&l.push("skew("+si(a*wt)+"deg, "+si(s*wt)+"deg)"),l.join(" ")}var wi=r.hasGlobalWindow&&X(window.btoa)?function(t){return window.btoa(unescape(encodeURIComponent(t)))}:"undefined"!=typeof Buffer?function(t){return Buffer.from(t).toString("base64")}:function(t){return null},Si=Array.prototype.slice;function Mi(t,e,n){return(e-t)*n+t}function Ii(t,e,n,i){for(var r=e.length,o=0;oi?e:t,o=Math.min(n,i),a=r[o-1]||{color:[0,0,0,0],offset:0},s=o;sa)i.length=a;else for(var s=o;s=1},t.prototype.getAdditiveTrack=function(){return this._additiveTrack},t.prototype.addKeyframe=function(t,e,n){this._needsSort=!0;var i=this.keyframes,r=i.length,o=!1,a=6,s=e;if(N(e)){var l=function(t){return N(t&&t[0])?2:1}(e);a=l,(1===l&&!j(e[0])||2===l&&!j(e[0][0]))&&(o=!0)}else if(j(e)&&!nt(e))a=0;else if(U(e))if(isNaN(+e)){var u=qn(e);u&&(s=u,a=3)}else a=0;else if(Q(e)){var h=A({},s);h.colorStops=z(e.colorStops,(function(t){return{offset:t.offset,color:qn(t.color)}})),yi(e)?a=4:vi(e)&&(a=5),s=h}0===r?this.valType=a:a===this.valType&&6!==a||(o=!0),this.discrete=this.discrete||o;var c={time:t,value:s,rawValue:e,percent:0};return n&&(c.easing=n,c.easingFunc=X(n)?n:an[n]||Pn(n)),i.push(c),c},t.prototype.prepare=function(t,e){var n=this.keyframes;this._needsSort&&n.sort((function(t,e){return t.time-e.time}));for(var i=this.valType,r=n.length,o=n[r-1],a=this.discrete,s=Oi(i),l=Pi(i),u=0;u=0&&!(l[n].percent<=e);n--);n=d(n,u-2)}else{for(n=p;ne);n++);n=d(n-1,u-2)}r=l[n+1],i=l[n]}if(i&&r){this._lastFr=n,this._lastFrP=e;var f=r.percent-i.percent,g=0===f?1:d((e-i.percent)/f,1);r.easingFunc&&(g=r.easingFunc(g));var y=o?this._additiveValue:c?Ri:t[h];if(!Oi(s)&&!c||y||(y=this._additiveValue=[]),this.discrete)t[h]=g<1?i.rawValue:r.rawValue;else if(Oi(s))1===s?Ii(y,i[a],r[a],g):function(t,e,n,i){for(var r=e.length,o=r&&e[0].length,a=0;a0&&s.addKeyframe(0,ki(l),i),this._trackKeys.push(a)}s.addKeyframe(t,ki(e[a]),i)}return this._maxTime=Math.max(this._maxTime,t),this},t.prototype.pause=function(){this._clip.pause(),this._paused=!0},t.prototype.resume=function(){this._clip.resume(),this._paused=!1},t.prototype.isPaused=function(){return!!this._paused},t.prototype.duration=function(t){return this._maxTime=t,this._force=!0,this},t.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var t=this._doneCbs;if(t)for(var e=t.length,n=0;n0)){this._started=1;for(var e=this,n=[],i=this._maxTime||0,r=0;r1){var a=o.pop();r.addKeyframe(a.time,t[i]),r.prepare(this._maxTime,r.getAdditiveTrack())}}}},t}();function zi(){return(new Date).getTime()}var Vi,Bi,Fi=function(t){function e(e){var n=t.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,e=e||{},n.stage=e.stage||{},n}return n(e,t),e.prototype.addClip=function(t){t.animation&&this.removeClip(t),this._head?(this._tail.next=t,t.prev=this._tail,t.next=null,this._tail=t):this._head=this._tail=t,t.animation=this},e.prototype.addAnimator=function(t){t.animation=this;var e=t.getClip();e&&this.addClip(e)},e.prototype.removeClip=function(t){if(t.animation){var e=t.prev,n=t.next;e?e.next=n:this._head=n,n?n.prev=e:this._tail=e,t.next=t.prev=t.animation=null}},e.prototype.removeAnimator=function(t){var e=t.getClip();e&&this.removeClip(e),t.animation=null},e.prototype.update=function(t){for(var e=zi()-this._pausedTime,n=e-this._time,i=this._head;i;){var r=i.next;i.step(e,n)?(i.ondestroy(),this.removeClip(i),i=r):i=r}this._time=e,t||(this.trigger("frame",n),this.stage.update&&this.stage.update())},e.prototype._startLoop=function(){var t=this;this._running=!0,on((function e(){t._running&&(on(e),!t._paused&&t.update())}))},e.prototype.start=function(){this._running||(this._time=zi(),this._pausedTime=0,this._startLoop())},e.prototype.stop=function(){this._running=!1},e.prototype.pause=function(){this._paused||(this._pauseStart=zi(),this._paused=!0)},e.prototype.resume=function(){this._paused&&(this._pausedTime+=zi()-this._pauseStart,this._paused=!1)},e.prototype.clear=function(){for(var t=this._head;t;){var e=t.next;t.prev=t.next=t.animation=null,t=e}this._head=this._tail=null},e.prototype.isFinished=function(){return null==this._head},e.prototype.animate=function(t,e){e=e||{},this.start();var n=new Ei(t,e.loop);return this.addAnimator(n),n},e}(jt),Gi=r.domSupported,Wi=(Bi={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:Vi=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:z(Vi,(function(t){var e=t.replace("mouse","pointer");return Bi.hasOwnProperty(e)?e:t}))}),Hi=["mousemove","mouseup"],Yi=["pointermove","pointerup"],Xi=!1;function Ui(t){var e=t.pointerType;return"pen"===e||"touch"===e}function Zi(t){t&&(t.zrByTouch=!0)}function ji(t,e){for(var n=e,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==e&&n===t.painterRoot);)n=n.parentNode;return i}var qi=function(t,e){this.stopPropagation=bt,this.stopImmediatePropagation=bt,this.preventDefault=bt,this.type=e.type,this.target=this.currentTarget=t.dom,this.pointerType=e.pointerType,this.clientX=e.clientX,this.clientY=e.clientY},Ki={mousedown:function(t){t=ce(this.dom,t),this.__mayPointerCapture=[t.zrX,t.zrY],this.trigger("mousedown",t)},mousemove:function(t){t=ce(this.dom,t);var e=this.__mayPointerCapture;!e||t.zrX===e[0]&&t.zrY===e[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",t)},mouseup:function(t){t=ce(this.dom,t),this.__togglePointerCapture(!1),this.trigger("mouseup",t)},mouseout:function(t){ji(this,(t=ce(this.dom,t)).toElement||t.relatedTarget)||(this.__pointerCapturing&&(t.zrEventControl="no_globalout"),this.trigger("mouseout",t))},wheel:function(t){Xi=!0,t=ce(this.dom,t),this.trigger("mousewheel",t)},mousewheel:function(t){Xi||(t=ce(this.dom,t),this.trigger("mousewheel",t))},touchstart:function(t){Zi(t=ce(this.dom,t)),this.__lastTouchMoment=new Date,this.handler.processGesture(t,"start"),Ki.mousemove.call(this,t),Ki.mousedown.call(this,t)},touchmove:function(t){Zi(t=ce(this.dom,t)),this.handler.processGesture(t,"change"),Ki.mousemove.call(this,t)},touchend:function(t){Zi(t=ce(this.dom,t)),this.handler.processGesture(t,"end"),Ki.mouseup.call(this,t),+new Date-+this.__lastTouchMoment<300&&Ki.click.call(this,t)},pointerdown:function(t){Ki.mousedown.call(this,t)},pointermove:function(t){Ui(t)||Ki.mousemove.call(this,t)},pointerup:function(t){Ki.mouseup.call(this,t)},pointerout:function(t){Ui(t)||Ki.mouseout.call(this,t)}};E(["click","dblclick","contextmenu"],(function(t){Ki[t]=function(e){e=ce(this.dom,e),this.trigger(t,e)}}));var $i={pointermove:function(t){Ui(t)||$i.mousemove.call(this,t)},pointerup:function(t){$i.mouseup.call(this,t)},mousemove:function(t){this.trigger("mousemove",t)},mouseup:function(t){var e=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",t),e&&(t.zrEventControl="only_globalout",this.trigger("mouseout",t))}};function Ji(t,e){var n=e.domHandlers;r.pointerEventsSupported?E(Wi.pointer,(function(i){tr(e,i,(function(e){n[i].call(t,e)}))})):(r.touchEventsSupported&&E(Wi.touch,(function(i){tr(e,i,(function(r){n[i].call(t,r),function(t){t.touching=!0,null!=t.touchTimer&&(clearTimeout(t.touchTimer),t.touchTimer=null),t.touchTimer=setTimeout((function(){t.touching=!1,t.touchTimer=null}),700)}(e)}))})),E(Wi.mouse,(function(i){tr(e,i,(function(r){r=he(r),e.touching||n[i].call(t,r)}))})))}function Qi(t,e){function n(n){tr(e,n,(function(i){i=he(i),ji(t,i.target)||(i=function(t,e){return ce(t.dom,new qi(t,e),!0)}(t,i),e.domHandlers[n].call(t,i))}),{capture:!0})}r.pointerEventsSupported?E(Yi,n):r.touchEventsSupported||E(Hi,n)}function tr(t,e,n,i){t.mounted[e]=n,t.listenerOpts[e]=i,pe(t.domTarget,e,n,i)}function er(t){var e,n,i,r,o=t.mounted;for(var a in o)o.hasOwnProperty(a)&&(e=t.domTarget,n=a,i=o[a],r=t.listenerOpts[a],e.removeEventListener(n,i,r));t.mounted={}}var nr=function(t,e){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=t,this.domHandlers=e},ir=function(t){function e(e,n){var i=t.call(this)||this;return i.__pointerCapturing=!1,i.dom=e,i.painterRoot=n,i._localHandlerScope=new nr(e,Ki),Gi&&(i._globalHandlerScope=new nr(document,$i)),Ji(i,i._localHandlerScope),i}return n(e,t),e.prototype.dispose=function(){er(this._localHandlerScope),Gi&&er(this._globalHandlerScope)},e.prototype.setCursor=function(t){this.dom.style&&(this.dom.style.cursor=t||"default")},e.prototype.__togglePointerCapture=function(t){if(this.__mayPointerCapture=null,Gi&&+this.__pointerCapturing^+t){this.__pointerCapturing=t;var e=this._globalHandlerScope;t?Qi(this,e):er(e)}},e}(jt),rr=1;r.hasGlobalWindow&&(rr=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var or=rr,ar="#333",sr="#ccc",lr=xe,ur=5e-5;function hr(t){return t>ur||t<-5e-5}var cr=[],pr=[],dr=[1,0,0,1,0,0],fr=Math.abs,gr=function(){function t(){}return t.prototype.getLocalTransform=function(e){return t.getLocalTransform(this,e)},t.prototype.setPosition=function(t){this.x=t[0],this.y=t[1]},t.prototype.setScale=function(t){this.scaleX=t[0],this.scaleY=t[1]},t.prototype.setSkew=function(t){this.skewX=t[0],this.skewY=t[1]},t.prototype.setOrigin=function(t){this.originX=t[0],this.originY=t[1]},t.prototype.needLocalTransform=function(){return hr(this.rotation)||hr(this.x)||hr(this.y)||hr(this.scaleX-1)||hr(this.scaleY-1)||hr(this.skewX)||hr(this.skewY)},t.prototype.updateTransform=function(){var t=this.parent&&this.parent.transform,e=this.needLocalTransform(),n=this.transform;e||t?(n=n||[1,0,0,1,0,0],e?this.getLocalTransform(n):lr(n),t&&(e?be(n,t,n):_e(n,t)),this.transform=n,this._resolveGlobalScaleRatio(n)):n&&(lr(n),this.invTransform=null)},t.prototype._resolveGlobalScaleRatio=function(t){var e=this.globalScaleRatio;if(null!=e&&1!==e){this.getGlobalScale(cr);var n=cr[0]<0?-1:1,i=cr[1]<0?-1:1,r=((cr[0]-n)*e+n)/cr[0]||0,o=((cr[1]-i)*e+i)/cr[1]||0;t[0]*=r,t[1]*=r,t[2]*=o,t[3]*=o}this.invTransform=this.invTransform||[1,0,0,1,0,0],Ie(this.invTransform,t)},t.prototype.getComputedTransform=function(){for(var t=this,e=[];t;)e.push(t),t=t.parent;for(;t=e.pop();)t.updateTransform();return this.transform},t.prototype.setLocalTransform=function(t){if(t){var e=t[0]*t[0]+t[1]*t[1],n=t[2]*t[2]+t[3]*t[3],i=Math.atan2(t[1],t[0]),r=Math.PI/2+i-Math.atan2(t[3],t[2]);n=Math.sqrt(n)*Math.cos(r),e=Math.sqrt(e),this.skewX=r,this.skewY=0,this.rotation=-i,this.x=+t[4],this.y=+t[5],this.scaleX=e,this.scaleY=n,this.originX=0,this.originY=0}},t.prototype.decomposeTransform=function(){if(this.transform){var t=this.parent,e=this.transform;t&&t.transform&&(be(pr,t.invTransform,e),e=pr);var n=this.originX,i=this.originY;(n||i)&&(dr[4]=n,dr[5]=i,be(pr,e,dr),pr[4]-=n,pr[5]-=i,e=pr),this.setLocalTransform(e)}},t.prototype.getGlobalScale=function(t){var e=this.transform;return t=t||[],e?(t[0]=Math.sqrt(e[0]*e[0]+e[1]*e[1]),t[1]=Math.sqrt(e[2]*e[2]+e[3]*e[3]),e[0]<0&&(t[0]=-t[0]),e[3]<0&&(t[1]=-t[1]),t):(t[0]=1,t[1]=1,t)},t.prototype.transformCoordToLocal=function(t,e){var n=[t,e],i=this.invTransform;return i&&Wt(n,n,i),n},t.prototype.transformCoordToGlobal=function(t,e){var n=[t,e],i=this.transform;return i&&Wt(n,n,i),n},t.prototype.getLineScale=function(){var t=this.transform;return t&&fr(t[0]-1)>1e-10&&fr(t[3]-1)>1e-10?Math.sqrt(fr(t[0]*t[3]-t[2]*t[1])):1},t.prototype.copyTransform=function(t){vr(this,t)},t.getLocalTransform=function(t,e){e=e||[];var n=t.originX||0,i=t.originY||0,r=t.scaleX,o=t.scaleY,a=t.anchorX,s=t.anchorY,l=t.rotation||0,u=t.x,h=t.y,c=t.skewX?Math.tan(t.skewX):0,p=t.skewY?Math.tan(-t.skewY):0;if(n||i||a||s){var d=n+a,f=i+s;e[4]=-d*r-c*f*o,e[5]=-f*o-p*d*r}else e[4]=e[5]=0;return e[0]=r,e[3]=o,e[1]=p*r,e[2]=c*o,l&&Se(e,e,l),e[4]+=n+u,e[5]+=i+h,e},t.initDefaultProps=function(){var e=t.prototype;e.scaleX=e.scaleY=e.globalScaleRatio=1,e.x=e.y=e.originX=e.originY=e.skewX=e.skewY=e.rotation=e.anchorX=e.anchorY=0}(),t}(),yr=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function vr(t,e){for(var n=0;n=0?parseFloat(t)/100*e:parseFloat(t):t}function Tr(t,e,n){var i=e.position||"inside",r=null!=e.distance?e.distance:5,o=n.height,a=n.width,s=o/2,l=n.x,u=n.y,h="left",c="top";if(i instanceof Array)l+=Ir(i[0],n.width),u+=Ir(i[1],n.height),h=null,c=null;else switch(i){case"left":l-=r,u+=s,h="right",c="middle";break;case"right":l+=r+a,u+=s,c="middle";break;case"top":l+=a/2,u-=r,h="center",c="bottom";break;case"bottom":l+=a/2,u+=o+r,h="center";break;case"inside":l+=a/2,u+=s,h="center",c="middle";break;case"insideLeft":l+=r,u+=s,c="middle";break;case"insideRight":l+=a-r,u+=s,h="right",c="middle";break;case"insideTop":l+=a/2,u+=r,h="center";break;case"insideBottom":l+=a/2,u+=o-r,h="center",c="bottom";break;case"insideTopLeft":l+=r,u+=r;break;case"insideTopRight":l+=a-r,u+=r,h="right";break;case"insideBottomLeft":l+=r,u+=o-r,c="bottom";break;case"insideBottomRight":l+=a-r,u+=o-r,h="right",c="bottom"}return(t=t||{}).x=l,t.y=u,t.align=h,t.verticalAlign=c,t}var Cr="__zr_normal__",Dr=yr.concat(["ignore"]),Ar=V(yr,(function(t,e){return t[e]=!0,t}),{ignore:!1}),kr={},Lr=new ze(0,0,0,0),Pr=function(){function t(t){this.id=M(),this.animators=[],this.currentStates=[],this.states={},this._init(t)}return t.prototype._init=function(t){this.attr(t)},t.prototype.drift=function(t,e,n){switch(this.draggable){case"horizontal":e=0;break;case"vertical":t=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=t,i[5]+=e,this.decomposeTransform(),this.markRedraw()},t.prototype.beforeUpdate=function(){},t.prototype.afterUpdate=function(){},t.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},t.prototype.updateInnerText=function(t){var e=this._textContent;if(e&&(!e.ignore||t)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,r=e.innerTransformable,o=void 0,a=void 0,s=!1;r.parent=i?this:null;var l=!1;if(r.copyTransform(e),null!=n.position){var u=Lr;n.layoutRect?u.copy(n.layoutRect):u.copy(this.getBoundingRect()),i||u.applyTransform(this.transform),this.calculateTextPosition?this.calculateTextPosition(kr,n,u):Tr(kr,n,u),r.x=kr.x,r.y=kr.y,o=kr.align,a=kr.verticalAlign;var h=n.origin;if(h&&null!=n.rotation){var c=void 0,p=void 0;"center"===h?(c=.5*u.width,p=.5*u.height):(c=Ir(h[0],u.width),p=Ir(h[1],u.height)),l=!0,r.originX=-r.x+c+(i?0:u.x),r.originY=-r.y+p+(i?0:u.y)}}null!=n.rotation&&(r.rotation=n.rotation);var d=n.offset;d&&(r.x+=d[0],r.y+=d[1],l||(r.originX=-d[0],r.originY=-d[1]));var f=null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside,g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={}),y=void 0,v=void 0,m=void 0;f&&this.canBeInsideText()?(y=n.insideFill,v=n.insideStroke,null!=y&&"auto"!==y||(y=this.getInsideTextFill()),null!=v&&"auto"!==v||(v=this.getInsideTextStroke(y),m=!0)):(y=n.outsideFill,v=n.outsideStroke,null!=y&&"auto"!==y||(y=this.getOutsideFill()),null!=v&&"auto"!==v||(v=this.getOutsideStroke(y),m=!0)),(y=y||"#000")===g.fill&&v===g.stroke&&m===g.autoStroke&&o===g.align&&a===g.verticalAlign||(s=!0,g.fill=y,g.stroke=v,g.autoStroke=m,g.align=o,g.verticalAlign=a,e.setDefaultTextStyle(g)),e.__dirty|=1,s&&e.dirtyStyle(!0)}},t.prototype.canBeInsideText=function(){return!0},t.prototype.getInsideTextFill=function(){return"#fff"},t.prototype.getInsideTextStroke=function(t){return"#000"},t.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?sr:ar},t.prototype.getOutsideStroke=function(t){var e=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof e&&qn(e);n||(n=[255,255,255,1]);for(var i=n[3],r=this.__zr.isDarkMode(),o=0;o<3;o++)n[o]=n[o]*i+(r?0:255)*(1-i);return n[3]=1,ri(n,"rgba")},t.prototype.traverse=function(t,e){},t.prototype.attrKV=function(t,e){"textConfig"===t?this.setTextConfig(e):"textContent"===t?this.setTextContent(e):"clipPath"===t?this.setClipPath(e):"extra"===t?(this.extra=this.extra||{},A(this.extra,e)):this[t]=e},t.prototype.hide=function(){this.ignore=!0,this.markRedraw()},t.prototype.show=function(){this.ignore=!1,this.markRedraw()},t.prototype.attr=function(t,e){if("string"==typeof t)this.attrKV(t,e);else if(q(t))for(var n=G(t),i=0;i0},t.prototype.getState=function(t){return this.states[t]},t.prototype.ensureState=function(t){var e=this.states;return e[t]||(e[t]={}),e[t]},t.prototype.clearStates=function(t){this.useState(Cr,!1,t)},t.prototype.useState=function(t,e,n,i){var r=t===Cr;if(this.hasState()||!r){var o=this.currentStates,a=this.stateTransition;if(!(P(o,t)>=0)||!e&&1!==o.length){var s;if(this.stateProxy&&!r&&(s=this.stateProxy(t)),s||(s=this.states&&this.states[t]),s||r){r||this.saveCurrentToNormalState(s);var l=!!(s&&s.hoverLayer||i);l&&this._toggleHoverLayerFlag(!0),this._applyStateObj(t,s,this._normalState,e,!n&&!this.__inHover&&a&&a.duration>0,a);var u=this._textContent,h=this._textGuide;return u&&u.useState(t,e,n,l),h&&h.useState(t,e,n,l),r?(this.currentStates=[],this._normalState={}):e?this.currentStates.push(t):this.currentStates=[t],this._updateAnimationTargets(),this.markRedraw(),!l&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2),s}I("State "+t+" not exists.")}}},t.prototype.useStates=function(t,e,n){if(t.length){var i=[],r=this.currentStates,o=t.length,a=o===r.length;if(a)for(var s=0;s0,d);var f=this._textContent,g=this._textGuide;f&&f.useStates(t,e,c),g&&g.useStates(t,e,c),this._updateAnimationTargets(),this.currentStates=t.slice(),this.markRedraw(),!c&&this.__inHover&&(this._toggleHoverLayerFlag(!1),this.__dirty&=-2)}else this.clearStates()},t.prototype._updateAnimationTargets=function(){for(var t=0;t=0){var n=this.currentStates.slice();n.splice(e,1),this.useStates(n)}},t.prototype.replaceState=function(t,e,n){var i=this.currentStates.slice(),r=P(i,t),o=P(i,e)>=0;r>=0?o?i.splice(r,1):i[r]=e:n&&!o&&i.push(e),this.useStates(i)},t.prototype.toggleState=function(t,e){e?this.useState(t,!0):this.removeState(t)},t.prototype._mergeStates=function(t){for(var e,n={},i=0;i=0&&e.splice(n,1)})),this.animators.push(t),n&&n.animation.addAnimator(t),n&&n.wakeUp()},t.prototype.updateDuringAnimation=function(t){this.markRedraw()},t.prototype.stopAnimation=function(t,e){for(var n=this.animators,i=n.length,r=[],o=0;o0&&n.during&&o[0].during((function(t,e){n.during(e)}));for(var p=0;p0||r.force&&!a.length){var w,S=void 0,M=void 0,I=void 0;if(s){M={},p&&(S={});for(_=0;_=0&&(n.splice(i,0,t),this._doAdd(t))}return this},e.prototype.replace=function(t,e){var n=P(this._children,t);return n>=0&&this.replaceAt(e,n),this},e.prototype.replaceAt=function(t,e){var n=this._children,i=n[e];if(t&&t!==this&&t.parent!==this&&t!==i){n[e]=t,i.parent=null;var r=this.__zr;r&&i.removeSelfFromZr(r),this._doAdd(t)}return this},e.prototype._doAdd=function(t){t.parent&&t.parent.remove(t),t.parent=this;var e=this.__zr;e&&e!==t.__zr&&t.addSelfToZr(e),e&&e.refresh()},e.prototype.remove=function(t){var e=this.__zr,n=this._children,i=P(n,t);return i<0||(n.splice(i,1),t.parent=null,e&&t.removeSelfFromZr(e),e&&e.refresh()),this},e.prototype.removeAll=function(){for(var t=this._children,e=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},t.prototype.wakeUp=function(){this.animation.start(),this._stillFrameAccum=0},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._needsRefreshHover=!1,this.painter.refreshHover&&"canvas"===this.painter.getType()&&this.painter.refreshHover()},t.prototype.resize=function(t){t=t||{},this.painter.resize(t.width,t.height),this.handler.resize()},t.prototype.clearAnimation=function(){this.animation.clear()},t.prototype.getWidth=function(){return this.painter.getWidth()},t.prototype.getHeight=function(){return this.painter.getHeight()},t.prototype.setCursorStyle=function(t){this.handler.setCursorStyle(t)},t.prototype.findHover=function(t,e){return this.handler.findHover(t,e)},t.prototype.on=function(t,e,n){return this.handler.on(t,e,n),this},t.prototype.off=function(t,e){this.handler.off(t,e)},t.prototype.trigger=function(t,e){this.handler.trigger(t,e)},t.prototype.clear=function(){for(var t=this.storage.getRoots(),e=0;e0){if(t<=r)return a;if(t>=o)return s}else{if(t>=r)return a;if(t<=o)return s}else{if(t===r)return a;if(t===o)return s}return(t-r)/l*u+a}function Ur(t,e){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return U(t)?(n=t,n.replace(/^\s+|\s+$/g,"")).match(/%$/)?parseFloat(t)/100*e:parseFloat(t):null==t?NaN:+t;var n}function Zr(t,e,n){return null==e&&(e=10),e=Math.min(Math.max(0,e),20),t=(+t).toFixed(e),n?t:+t}function jr(t){return t.sort((function(t,e){return t-e})),t}function qr(t){if(t=+t,isNaN(t))return 0;if(t>1e-14)for(var e=1,n=0;n<15;n++,e*=10)if(Math.round(t*e)/e===t)return n;return Kr(t)}function Kr(t){var e=t.toString().toLowerCase(),n=e.indexOf("e"),i=n>0?+e.slice(n+1):0,r=n>0?n:e.length,o=e.indexOf("."),a=o<0?0:r-1-o;return Math.max(0,a-i)}function $r(t,e){var n=Math.log,i=Math.LN10,r=Math.floor(n(t[1]-t[0])/i),o=Math.round(n(Math.abs(e[1]-e[0]))/i),a=Math.min(Math.max(-r+o,0),20);return isFinite(a)?a:20}function Jr(t,e){var n=V(t,(function(t,e){return t+(isNaN(e)?0:e)}),0);if(0===n)return[];for(var i=Math.pow(10,e),r=z(t,(function(t){return(isNaN(t)?0:t)/n*i*100})),o=100*i,a=z(r,(function(t){return Math.floor(t)})),s=V(a,(function(t,e){return t+e}),0),l=z(r,(function(t,e){return t-a[e]}));su&&(u=l[c],h=c);++a[h],l[h]=0,++s}return z(a,(function(t){return t/i}))}function Qr(t,e){var n=Math.max(qr(t),qr(e)),i=t+e;return n>20?i:Zr(i,n)}var to=9007199254740991;function eo(t){var e=2*Math.PI;return(t%e+e)%e}function no(t){return t>-1e-4&&t=10&&e++,e}function so(t,e){var n=ao(t),i=Math.pow(10,n),r=t/i;return t=(e?r<1.5?1:r<2.5?2:r<4?3:r<7?5:10:r<1?1:r<2?2:r<3?3:r<5?5:10)*i,n>=-20?+t.toFixed(n<0?-n:0):t}function lo(t,e){var n=(t.length-1)*e+1,i=Math.floor(n),r=+t[i-1],o=n-i;return o?r+o*(t[i]-r):r}function uo(t){t.sort((function(t,e){return s(t,e,0)?-1:1}));for(var e=-1/0,n=1,i=0;i=0||r&&P(r,s)<0)){var l=n.getShallow(s,e);null!=l&&(o[t[a][0]]=l)}}return o}}var Qo=Jo([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),ta=function(){function t(){}return t.prototype.getAreaStyle=function(t,e){return Qo(this,t,e)},t}(),ea=new En(50);function na(t){if("string"==typeof t){var e=ea.get(t);return e&&e.image}return t}function ia(t,e,n,i,r){if(t){if("string"==typeof t){if(e&&e.__zrImageSrc===t||!n)return e;var o=ea.get(t),a={hostEl:n,cb:i,cbPayload:r};return o?!oa(e=o.image)&&o.pending.push(a):((e=h.loadImage(t,ra,ra)).__zrImageSrc=t,ea.put(t,e.__cachedImgObj={image:e,pending:[a]})),e}return t}return e}function ra(){var t=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var e=0;e=a;l++)s-=a;var u=xr(n,e);return u>s&&(n="",u=0),s=t-u,r.ellipsis=n,r.ellipsisWidth=u,r.contentWidth=s,r.containerWidth=t,r}function ua(t,e){var n=e.containerWidth,i=e.font,r=e.contentWidth;if(!n)return"";var o=xr(t,i);if(o<=n)return t;for(var a=0;;a++){if(o<=r||a>=e.maxIterations){t+=e.ellipsis;break}var s=0===a?ha(t,r,e.ascCharWidth,e.cnCharWidth):o>0?Math.floor(t.length*r/o):0;o=xr(t=t.substr(0,s),i)}return""===t&&(t=e.placeholder),t}function ha(t,e,n,i){for(var r=0,o=0,a=t.length;o0&&f+i.accumWidth>i.width&&(o=e.split("\n"),c=!0),i.accumWidth=f}else{var g=va(e,h,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+d,a=g.linesWidths,o=g.lines}}else o=e.split("\n");for(var y=0;y=32&&e<=591||e>=880&&e<=4351||e>=4608&&e<=5119||e>=7680&&e<=8303}(t)||!!ga[t]}function va(t,e,n,i,r){for(var o=[],a=[],s="",l="",u=0,h=0,c=0;cn:r+h+d>n)?h?(s||l)&&(f?(s||(s=l,l="",h=u=0),o.push(s),a.push(h-u),l+=p,s="",h=u+=d):(l&&(s+=l,l="",u=0),o.push(s),a.push(h),s=p,h=d)):f?(o.push(l),a.push(u),l=p,u=d):(o.push(p),a.push(d)):(h+=d,f?(l+=p,u+=d):(l&&(s+=l,l="",u=0),s+=p))}else l&&(s+=l,h+=u),o.push(s),a.push(h),s="",l="",u=0,h=0}return o.length||s||(s=t,l="",u=0),l&&(s+=l),s&&(o.push(s),a.push(h)),1===o.length&&(h+=r),{accumWidth:h,lines:o,linesWidths:a}}var ma="__zr_style_"+Math.round(10*Math.random()),xa={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},_a={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};xa[ma]=!0;var ba=["z","z2","invisible"],wa=["invisible"],Sa=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype._init=function(e){for(var n=G(e),i=0;i1e-4)return s[0]=t-n,s[1]=e-i,l[0]=t+n,void(l[1]=e+i);if(La[0]=Aa(r)*n+t,La[1]=Da(r)*i+e,Pa[0]=Aa(o)*n+t,Pa[1]=Da(o)*i+e,u(s,La,Pa),h(l,La,Pa),(r%=ka)<0&&(r+=ka),(o%=ka)<0&&(o+=ka),r>o&&!a?o+=ka:rr&&(Oa[0]=Aa(d)*n+t,Oa[1]=Da(d)*i+e,u(s,Oa,s),h(l,Oa,l))}var Ga={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},Wa=[],Ha=[],Ya=[],Xa=[],Ua=[],Za=[],ja=Math.min,qa=Math.max,Ka=Math.cos,$a=Math.sin,Ja=Math.abs,Qa=Math.PI,ts=2*Qa,es="undefined"!=typeof Float32Array,ns=[];function is(t){return Math.round(t/Qa*1e8)/1e8%2*Qa}function rs(t,e){var n=is(t[0]);n<0&&(n+=ts);var i=n-t[0],r=t[1];r+=i,!e&&r-n>=ts?r=n+ts:e&&n-r>=ts?r=n-ts:!e&&n>r?r=n+(ts-is(n-r)):e&&n0&&(this._ux=Ja(n/or/t)||0,this._uy=Ja(n/or/e)||0)},t.prototype.setDPR=function(t){this.dpr=t},t.prototype.setContext=function(t){this._ctx=t},t.prototype.getContext=function(){return this._ctx},t.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},t.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},t.prototype.moveTo=function(t,e){return this._drawPendingPt(),this.addData(Ga.M,t,e),this._ctx&&this._ctx.moveTo(t,e),this._x0=t,this._y0=e,this._xi=t,this._yi=e,this},t.prototype.lineTo=function(t,e){var n=Ja(t-this._xi),i=Ja(e-this._yi),r=n>this._ux||i>this._uy;if(this.addData(Ga.L,t,e),this._ctx&&r&&this._ctx.lineTo(t,e),r)this._xi=t,this._yi=e,this._pendingPtDist=0;else{var o=n*n+i*i;o>this._pendingPtDist&&(this._pendingPtX=t,this._pendingPtY=e,this._pendingPtDist=o)}return this},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){return this._drawPendingPt(),this.addData(Ga.C,t,e,n,i,r,o),this._ctx&&this._ctx.bezierCurveTo(t,e,n,i,r,o),this._xi=r,this._yi=o,this},t.prototype.quadraticCurveTo=function(t,e,n,i){return this._drawPendingPt(),this.addData(Ga.Q,t,e,n,i),this._ctx&&this._ctx.quadraticCurveTo(t,e,n,i),this._xi=n,this._yi=i,this},t.prototype.arc=function(t,e,n,i,r,o){this._drawPendingPt(),ns[0]=i,ns[1]=r,rs(ns,o),i=ns[0];var a=(r=ns[1])-i;return this.addData(Ga.A,t,e,n,n,i,a,0,o?0:1),this._ctx&&this._ctx.arc(t,e,n,i,r,o),this._xi=Ka(r)*n+t,this._yi=$a(r)*n+e,this},t.prototype.arcTo=function(t,e,n,i,r){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(t,e,n,i,r),this},t.prototype.rect=function(t,e,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(t,e,n,i),this.addData(Ga.R,t,e,n,i),this},t.prototype.closePath=function(){this._drawPendingPt(),this.addData(Ga.Z);var t=this._ctx,e=this._x0,n=this._y0;return t&&t.closePath(),this._xi=e,this._yi=n,this},t.prototype.fill=function(t){t&&t.fill(),this.toStatic()},t.prototype.stroke=function(t){t&&t.stroke(),this.toStatic()},t.prototype.len=function(){return this._len},t.prototype.setData=function(t){var e=t.length;this.data&&this.data.length===e||!es||(this.data=new Float32Array(e));for(var n=0;nu.length&&(this._expandData(),u=this.data);for(var h=0;h0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},t.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var t=[],e=0;e11&&(this.data=new Float32Array(t)))}},t.prototype.getBoundingRect=function(){Ya[0]=Ya[1]=Ua[0]=Ua[1]=Number.MAX_VALUE,Xa[0]=Xa[1]=Za[0]=Za[1]=-Number.MAX_VALUE;var t,e=this.data,n=0,i=0,r=0,o=0;for(t=0;tn||Ja(y)>i||c===e-1)&&(f=Math.sqrt(A*A+y*y),r=g,o=x);break;case Ga.C:var v=t[c++],m=t[c++],x=(g=t[c++],t[c++]),_=t[c++],b=t[c++];f=Mn(r,o,v,m,g,x,_,b,10),r=_,o=b;break;case Ga.Q:f=kn(r,o,v=t[c++],m=t[c++],g=t[c++],x=t[c++],10),r=g,o=x;break;case Ga.A:var w=t[c++],S=t[c++],M=t[c++],I=t[c++],T=t[c++],C=t[c++],D=C+T;c+=1;t[c++];d&&(a=Ka(T)*M+w,s=$a(T)*I+S),f=qa(M,I)*ja(ts,Math.abs(C)),r=Ka(D)*M+w,o=$a(D)*I+S;break;case Ga.R:a=r=t[c++],s=o=t[c++],f=2*t[c++]+2*t[c++];break;case Ga.Z:var A=a-r;y=s-o;f=Math.sqrt(A*A+y*y),r=a,o=s}f>=0&&(l[h++]=f,u+=f)}return this._pathLen=u,u},t.prototype.rebuildPath=function(t,e){var n,i,r,o,a,s,l,u,h,c,p=this.data,d=this._ux,f=this._uy,g=this._len,y=e<1,v=0,m=0,x=0;if(!y||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,u=e*this._pathLen))t:for(var _=0;_0&&(t.lineTo(h,c),x=0),b){case Ga.M:n=r=p[_++],i=o=p[_++],t.moveTo(r,o);break;case Ga.L:a=p[_++],s=p[_++];var S=Ja(a-r),M=Ja(s-o);if(S>d||M>f){if(y){if(v+(j=l[m++])>u){var I=(u-v)/j;t.lineTo(r*(1-I)+a*I,o*(1-I)+s*I);break t}v+=j}t.lineTo(a,s),r=a,o=s,x=0}else{var T=S*S+M*M;T>x&&(h=a,c=s,x=T)}break;case Ga.C:var C=p[_++],D=p[_++],A=p[_++],k=p[_++],L=p[_++],P=p[_++];if(y){if(v+(j=l[m++])>u){wn(r,C,A,L,I=(u-v)/j,Wa),wn(o,D,k,P,I,Ha),t.bezierCurveTo(Wa[1],Ha[1],Wa[2],Ha[2],Wa[3],Ha[3]);break t}v+=j}t.bezierCurveTo(C,D,A,k,L,P),r=L,o=P;break;case Ga.Q:C=p[_++],D=p[_++],A=p[_++],k=p[_++];if(y){if(v+(j=l[m++])>u){Dn(r,C,A,I=(u-v)/j,Wa),Dn(o,D,k,I,Ha),t.quadraticCurveTo(Wa[1],Ha[1],Wa[2],Ha[2]);break t}v+=j}t.quadraticCurveTo(C,D,A,k),r=A,o=k;break;case Ga.A:var O=p[_++],R=p[_++],N=p[_++],E=p[_++],z=p[_++],V=p[_++],B=p[_++],F=!p[_++],G=N>E?N:E,W=Ja(N-E)>.001,H=z+V,Y=!1;if(y)v+(j=l[m++])>u&&(H=z+V*(u-v)/j,Y=!0),v+=j;if(W&&t.ellipse?t.ellipse(O,R,N,E,B,z,H,F):t.arc(O,R,G,z,H,F),Y)break t;w&&(n=Ka(z)*N+O,i=$a(z)*E+R),r=Ka(H)*N+O,o=$a(H)*E+R;break;case Ga.R:n=r=p[_],i=o=p[_+1],a=p[_++],s=p[_++];var X=p[_++],U=p[_++];if(y){if(v+(j=l[m++])>u){var Z=u-v;t.moveTo(a,s),t.lineTo(a+ja(Z,X),s),(Z-=X)>0&&t.lineTo(a+X,s+ja(Z,U)),(Z-=U)>0&&t.lineTo(a+qa(X-Z,0),s+U),(Z-=X)>0&&t.lineTo(a,s+qa(U-Z,0));break t}v+=j}t.rect(a,s,X,U);break;case Ga.Z:if(y){var j;if(v+(j=l[m++])>u){I=(u-v)/j;t.lineTo(r*(1-I)+n*I,o*(1-I)+i*I);break t}v+=j}t.closePath(),r=n,o=i}}},t.prototype.clone=function(){var e=new t,n=this.data;return e.data=n.slice?n.slice():Array.prototype.slice.call(n),e._len=this._len,e},t.CMD=Ga,t.initDefaultProps=function(){var e=t.prototype;e._saveData=!0,e._ux=0,e._uy=0,e._pendingPtDist=0,e._version=0}(),t}();function as(t,e,n,i,r,o,a){if(0===r)return!1;var s=r,l=0;if(a>e+s&&a>i+s||at+s&&o>n+s||oe+c&&h>i+c&&h>o+c&&h>s+c||ht+c&&u>n+c&&u>r+c&&u>a+c||ue+u&&l>i+u&&l>o+u||lt+u&&s>n+u&&s>r+u||sn||h+ur&&(r+=cs);var p=Math.atan2(l,s);return p<0&&(p+=cs),p>=i&&p<=r||p+cs>=i&&p+cs<=r}function ds(t,e,n,i,r,o){if(o>e&&o>i||or?s:0}var fs=os.CMD,gs=2*Math.PI;var ys=[-1,-1,-1],vs=[-1,-1];function ms(t,e,n,i,r,o,a,s,l,u){if(u>e&&u>i&&u>o&&u>s||u1&&(h=void 0,h=vs[0],vs[0]=vs[1],vs[1]=h),f=mn(e,i,o,s,vs[0]),d>1&&(g=mn(e,i,o,s,vs[1]))),2===d?ve&&s>i&&s>o||s=0&&h<=1&&(r[l++]=h);else{var u=a*a-4*o*s;if(yn(u))(h=-a/(2*o))>=0&&h<=1&&(r[l++]=h);else if(u>0){var h,c=ln(u),p=(-a-c)/(2*o);(h=(-a+c)/(2*o))>=0&&h<=1&&(r[l++]=h),p>=0&&p<=1&&(r[l++]=p)}}return l}(e,i,o,s,ys);if(0===l)return 0;var u=Cn(e,i,o);if(u>=0&&u<=1){for(var h=0,c=In(e,i,o,u),p=0;pn||s<-n)return 0;var l=Math.sqrt(n*n-s*s);ys[0]=-l,ys[1]=l;var u=Math.abs(i-r);if(u<1e-4)return 0;if(u>=gs-1e-4){i=0,r=gs;var h=o?1:-1;return a>=ys[0]+t&&a<=ys[1]+t?h:0}if(i>r){var c=i;i=r,r=c}i<0&&(i+=gs,r+=gs);for(var p=0,d=0;d<2;d++){var f=ys[d];if(f+t>a){var g=Math.atan2(s,f);h=o?1:-1;g<0&&(g=gs+g),(g>=i&&g<=r||g+gs>=i&&g+gs<=r)&&(g>Math.PI/2&&g<1.5*Math.PI&&(h=-h),p+=h)}}return p}function bs(t,e,n,i,r){for(var o,a,s,l,u=t.data,h=t.len(),c=0,p=0,d=0,f=0,g=0,y=0;y1&&(n||(c+=ds(p,d,f,g,i,r))),m&&(f=p=u[y],g=d=u[y+1]),v){case fs.M:p=f=u[y++],d=g=u[y++];break;case fs.L:if(n){if(as(p,d,u[y],u[y+1],e,i,r))return!0}else c+=ds(p,d,u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case fs.C:if(n){if(ss(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=ms(p,d,u[y++],u[y++],u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case fs.Q:if(n){if(ls(p,d,u[y++],u[y++],u[y],u[y+1],e,i,r))return!0}else c+=xs(p,d,u[y++],u[y++],u[y],u[y+1],i,r)||0;p=u[y++],d=u[y++];break;case fs.A:var x=u[y++],_=u[y++],b=u[y++],w=u[y++],S=u[y++],M=u[y++];y+=1;var I=!!(1-u[y++]);o=Math.cos(S)*b+x,a=Math.sin(S)*w+_,m?(f=o,g=a):c+=ds(p,d,o,a,i,r);var T=(i-x)*w/b+x;if(n){if(ps(x,_,w,S,S+M,I,e,T,r))return!0}else c+=_s(x,_,w,S,S+M,I,T,r);p=Math.cos(S+M)*b+x,d=Math.sin(S+M)*w+_;break;case fs.R:if(f=p=u[y++],g=d=u[y++],o=f+u[y++],a=g+u[y++],n){if(as(f,g,o,g,e,i,r)||as(o,g,o,a,e,i,r)||as(o,a,f,a,e,i,r)||as(f,a,f,g,e,i,r))return!0}else c+=ds(o,g,o,a,i,r),c+=ds(f,a,f,g,i,r);break;case fs.Z:if(n){if(as(p,d,f,g,e,i,r))return!0}else c+=ds(p,d,f,g,i,r);p=f,d=g}}return n||(s=d,l=g,Math.abs(s-l)<1e-4)||(c+=ds(p,d,f,g,i,r)||0),0!==c}var ws=k({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},xa),Ss={style:k({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},_a.style)},Ms=yr.concat(["invisible","culling","z","z2","zlevel","parent"]),Is=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype.update=function(){var n=this;t.prototype.update.call(this);var i=this.style;if(i.decal){var r=this._decalEl=this._decalEl||new e;r.buildPath===e.prototype.buildPath&&(r.buildPath=function(t){n.buildPath(t,n.shape)}),r.silent=!0;var o=r.style;for(var a in i)o[a]!==i[a]&&(o[a]=i[a]);o.fill=i.fill?i.decal:null,o.decal=null,o.shadowColor=null,i.strokeFirst&&(o.stroke=null);for(var s=0;s.5?ar:e>.2?"#eee":sr}if(t)return sr}return ar},e.prototype.getInsideTextStroke=function(t){var e=this.style.fill;if(U(e)){var n=this.__zr;if(!(!n||!n.isDarkMode())===oi(t,0)<.4)return e}},e.prototype.buildPath=function(t,e,n){},e.prototype.pathUpdated=function(){this.__dirty&=-5},e.prototype.getUpdatedPathProxy=function(t){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,t),this.path},e.prototype.createPathProxy=function(){this.path=new os(!1)},e.prototype.hasStroke=function(){var t=this.style,e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.getBoundingRect=function(){var t=this._rect,e=this.style,n=!t;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var r=this.path;(i||4&this.__dirty)&&(r.beginPath(),this.buildPath(r,this.shape,!1),this.pathUpdated()),t=r.getBoundingRect()}if(this._rect=t,this.hasStroke()&&this.path&&this.path.len()>0){var o=this._rectStroke||(this._rectStroke=t.clone());if(this.__dirty||n){o.copy(t);var a=e.strokeNoScale?this.getLineScale():1,s=e.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}a>1e-10&&(o.width+=s/a,o.height+=s/a,o.x-=s/a/2,o.y-=s/a/2)}return o}return t},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect(),r=this.style;if(t=n[0],e=n[1],i.contain(t,e)){var o=this.path;if(this.hasStroke()){var a=r.lineWidth,s=r.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),function(t,e,n,i){return bs(t,e,!0,n,i)}(o,a/s,t,e)))return!0}if(this.hasFill())return function(t,e,n){return bs(t,0,!1,e,n)}(o,t,e)}return!1},e.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},e.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},e.prototype.animateShape=function(t){return this.animate("shape",t)},e.prototype.updateDuringAnimation=function(t){"style"===t?this.dirtyStyle():"shape"===t?this.dirtyShape():this.markRedraw()},e.prototype.attrKV=function(e,n){"shape"===e?this.setShape(n):t.prototype.attrKV.call(this,e,n)},e.prototype.setShape=function(t,e){var n=this.shape;return n||(n=this.shape={}),"string"==typeof t?n[t]=e:A(n,t),this.dirtyShape(),this},e.prototype.shapeChanged=function(){return!!(4&this.__dirty)},e.prototype.createStyle=function(t){return mt(ws,t)},e.prototype._innerSaveToNormal=function(e){t.prototype._innerSaveToNormal.call(this,e);var n=this._normalState;e.shape&&!n.shape&&(n.shape=A({},this.shape))},e.prototype._applyStateObj=function(e,n,i,r,o,a){t.prototype._applyStateObj.call(this,e,n,i,r,o,a);var s,l=!(n&&r);if(n&&n.shape?o?r?s=n.shape:(s=A({},i.shape),A(s,n.shape)):(s=A({},r?this.shape:i.shape),A(s,n.shape)):l&&(s=i.shape),s)if(o){this.shape=A({},this.shape);for(var u={},h=G(s),c=0;c0},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.createStyle=function(t){return mt(Ts,t)},e.prototype.setBoundingRect=function(t){this._rect=t},e.prototype.getBoundingRect=function(){var t=this.style;if(!this._rect){var e=t.text;null!=e?e+="":e="";var n=br(e,t.font,t.textAlign,t.textBaseline);if(n.x+=t.x||0,n.y+=t.y||0,this.hasStroke()){var i=t.lineWidth;n.x-=i/2,n.y-=i/2,n.width+=i,n.height+=i}this._rect=n}return this._rect},e.initDefaultProps=void(e.prototype.dirtyRectTolerance=10),e}(Sa);Cs.prototype.type="tspan";var Ds=k({x:0,y:0},xa),As={style:k({x:!0,y:!0,width:!0,height:!0,sx:!0,sy:!0,sWidth:!0,sHeight:!0},_a.style)};var ks=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.createStyle=function(t){return mt(Ds,t)},e.prototype._getSize=function(t){var e=this.style,n=e[t];if(null!=n)return n;var i,r=(i=e.image)&&"string"!=typeof i&&i.width&&i.height?e.image:this.__image;if(!r)return 0;var o="width"===t?"height":"width",a=e[o];return null==a?r[t]:r[t]/r[o]*a},e.prototype.getWidth=function(){return this._getSize("width")},e.prototype.getHeight=function(){return this._getSize("height")},e.prototype.getAnimationStyleProps=function(){return As},e.prototype.getBoundingRect=function(){var t=this.style;return this._rect||(this._rect=new ze(t.x||0,t.y||0,this.getWidth(),this.getHeight())),this._rect},e}(Sa);ks.prototype.type="image";var Ls=Math.round;function Ps(t,e,n){if(e){var i=e.x1,r=e.x2,o=e.y1,a=e.y2;t.x1=i,t.x2=r,t.y1=o,t.y2=a;var s=n&&n.lineWidth;return s?(Ls(2*i)===Ls(2*r)&&(t.x1=t.x2=Rs(i,s,!0)),Ls(2*o)===Ls(2*a)&&(t.y1=t.y2=Rs(o,s,!0)),t):t}}function Os(t,e,n){if(e){var i=e.x,r=e.y,o=e.width,a=e.height;t.x=i,t.y=r,t.width=o,t.height=a;var s=n&&n.lineWidth;return s?(t.x=Rs(i,s,!0),t.y=Rs(r,s,!0),t.width=Math.max(Rs(i+o,s,!1)-t.x,0===o?0:1),t.height=Math.max(Rs(r+a,s,!1)-t.y,0===a?0:1),t):t}}function Rs(t,e,n){if(!e)return t;var i=Ls(2*t);return(i+Ls(e))%2==0?i/2:(i+(n?1:-1))/2}var Ns=function(){this.x=0,this.y=0,this.width=0,this.height=0},Es={},zs=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Ns},e.prototype.buildPath=function(t,e){var n,i,r,o;if(this.subPixelOptimize){var a=Os(Es,e,this.style);n=a.x,i=a.y,r=a.width,o=a.height,a.r=e.r,e=a}else n=e.x,i=e.y,r=e.width,o=e.height;e.r?function(t,e){var n,i,r,o,a,s=e.x,l=e.y,u=e.width,h=e.height,c=e.r;u<0&&(s+=u,u=-u),h<0&&(l+=h,h=-h),"number"==typeof c?n=i=r=o=c:c instanceof Array?1===c.length?n=i=r=o=c[0]:2===c.length?(n=r=c[0],i=o=c[1]):3===c.length?(n=c[0],i=o=c[1],r=c[2]):(n=c[0],i=c[1],r=c[2],o=c[3]):n=i=r=o=0,n+i>u&&(n*=u/(a=n+i),i*=u/a),r+o>u&&(r*=u/(a=r+o),o*=u/a),i+r>h&&(i*=h/(a=i+r),r*=h/a),n+o>h&&(n*=h/(a=n+o),o*=h/a),t.moveTo(s+n,l),t.lineTo(s+u-i,l),0!==i&&t.arc(s+u-i,l+i,i,-Math.PI/2,0),t.lineTo(s+u,l+h-r),0!==r&&t.arc(s+u-r,l+h-r,r,0,Math.PI/2),t.lineTo(s+o,l+h),0!==o&&t.arc(s+o,l+h-o,o,Math.PI/2,Math.PI),t.lineTo(s,l+n),0!==n&&t.arc(s+n,l+n,n,Math.PI,1.5*Math.PI)}(t,e):t.rect(n,i,r,o)},e.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},e}(Is);zs.prototype.type="rect";var Vs={fill:"#000"},Bs={style:k({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},_a.style)},Fs=function(t){function e(e){var n=t.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=Vs,n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.update=function(){t.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var e=0;ed&&h){var f=Math.floor(d/l);n=n.slice(0,f)}if(t&&a&&null!=c)for(var g=la(c,o,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),y=0;y0,T=null!=t.width&&("truncate"===t.overflow||"break"===t.overflow||"breakAll"===t.overflow),C=i.calculatedLineHeight,D=0;Dl&&fa(n,t.substring(l,u),e,s),fa(n,i[2],e,s,i[1]),l=aa.lastIndex}lo){b>0?(m.tokens=m.tokens.slice(0,b),y(m,_,x),n.lines=n.lines.slice(0,v+1)):n.lines=n.lines.slice(0,v);break t}var C=w.width,D=null==C||"auto"===C;if("string"==typeof C&&"%"===C.charAt(C.length-1))P.percentWidth=C,h.push(P),P.contentWidth=xr(P.text,I);else{if(D){var A=w.backgroundColor,k=A&&A.image;k&&oa(k=na(k))&&(P.width=Math.max(P.width,k.width*T/k.height))}var L=f&&null!=r?r-_:null;null!=L&&L=0&&"right"===(C=x[T]).align;)this._placeToken(C,t,b,f,I,"right",y),w-=C.width,I-=C.width,T--;for(M+=(n-(M-d)-(g-I)-w)/2;S<=T;)C=x[S],this._placeToken(C,t,b,f,M+C.width/2,"center",y),M+=C.width,S++;f+=b}},e.prototype._placeToken=function(t,e,n,i,r,o,s){var l=e.rich[t.styleName]||{};l.text=t.text;var u=t.verticalAlign,h=i+n/2;"top"===u?h=i+t.height/2:"bottom"===u&&(h=i+n-t.height/2),!t.isLineHolder&&Js(l)&&this._renderBackground(l,e,"right"===o?r-t.width:"center"===o?r-t.width/2:r,h-t.height/2,t.width,t.height);var c=!!l.backgroundColor,p=t.textPadding;p&&(r=Ks(r,o,p),h-=t.height/2-p[0]-t.innerHeight/2);var d=this._getOrCreateChild(Cs),f=d.createStyle();d.useStyle(f);var g=this._defaultStyle,y=!1,v=0,m=qs("fill"in l?l.fill:"fill"in e?e.fill:(y=!0,g.fill)),x=js("stroke"in l?l.stroke:"stroke"in e?e.stroke:c||s||g.autoStroke&&!y?null:(v=2,g.stroke)),_=l.textShadowBlur>0||e.textShadowBlur>0;f.text=t.text,f.x=r,f.y=h,_&&(f.shadowBlur=l.textShadowBlur||e.textShadowBlur||0,f.shadowColor=l.textShadowColor||e.textShadowColor||"transparent",f.shadowOffsetX=l.textShadowOffsetX||e.textShadowOffsetX||0,f.shadowOffsetY=l.textShadowOffsetY||e.textShadowOffsetY||0),f.textAlign=o,f.textBaseline="middle",f.font=t.font||a,f.opacity=ot(l.opacity,e.opacity,1),Xs(f,l),x&&(f.lineWidth=ot(l.lineWidth,e.lineWidth,v),f.lineDash=rt(l.lineDash,e.lineDash),f.lineDashOffset=e.lineDashOffset||0,f.stroke=x),m&&(f.fill=m);var b=t.contentWidth,w=t.contentHeight;d.setBoundingRect(new ze(wr(f.x,b,f.textAlign),Sr(f.y,w,f.textBaseline),b,w))},e.prototype._renderBackground=function(t,e,n,i,r,o){var a,s,l,u=t.backgroundColor,h=t.borderWidth,c=t.borderColor,p=u&&u.image,d=u&&!p,f=t.borderRadius,g=this;if(d||t.lineHeight||h&&c){(a=this._getOrCreateChild(zs)).useStyle(a.createStyle()),a.style.fill=null;var y=a.shape;y.x=n,y.y=i,y.width=r,y.height=o,y.r=f,a.dirtyShape()}if(d)(l=a.style).fill=u||null,l.fillOpacity=rt(t.fillOpacity,1);else if(p){(s=this._getOrCreateChild(ks)).onload=function(){g.dirtyStyle()};var v=s.style;v.image=u.image,v.x=n,v.y=i,v.width=r,v.height=o}h&&c&&((l=a.style).lineWidth=h,l.stroke=c,l.strokeOpacity=rt(t.strokeOpacity,1),l.lineDash=t.borderDash,l.lineDashOffset=t.borderDashOffset||0,a.strokeContainThreshold=0,a.hasFill()&&a.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var m=(a||s).style;m.shadowBlur=t.shadowBlur||0,m.shadowColor=t.shadowColor||"transparent",m.shadowOffsetX=t.shadowOffsetX||0,m.shadowOffsetY=t.shadowOffsetY||0,m.opacity=ot(t.opacity,e.opacity,1)},e.makeFont=function(t){var e="";return Us(t)&&(e=[t.fontStyle,t.fontWeight,Ys(t.fontSize),t.fontFamily||"sans-serif"].join(" ")),e&&ut(e)||t.textFont||t.font},e}(Sa),Gs={left:!0,right:1,center:1},Ws={top:1,bottom:1,middle:1},Hs=["fontStyle","fontWeight","fontSize","fontFamily"];function Ys(t){return"string"!=typeof t||-1===t.indexOf("px")&&-1===t.indexOf("rem")&&-1===t.indexOf("em")?isNaN(+t)?"12px":t+"px":t}function Xs(t,e){for(var n=0;n=0,o=!1;if(t instanceof Is){var a=il(t),s=r&&a.selectFill||a.normalFill,l=r&&a.selectStroke||a.normalStroke;if(dl(s)||dl(l)){var u=(i=i||{}).style||{};"inherit"===u.fill?(o=!0,i=A({},i),(u=A({},u)).fill=s):!dl(u.fill)&&dl(s)?(o=!0,i=A({},i),(u=A({},u)).fill=gl(s)):!dl(u.stroke)&&dl(l)&&(o||(i=A({},i),u=A({},u)),u.stroke=gl(l)),i.style=u}}if(i&&null==i.z2){o||(i=A({},i));var h=t.z2EmphasisLift;i.z2=t.z2+(null!=h?h:sl)}return i}(this,0,e,n);if("blur"===t)return function(t,e,n){var i=P(t.currentStates,e)>=0,r=t.style.opacity,o=i?null:function(t,e,n,i){for(var r=t.style,o={},a=0;a0){var o={dataIndex:r,seriesIndex:t.seriesIndex};null!=i&&(o.dataType=i),e.push(o)}}))})),e}function Hl(t,e,n){ql(t,!0),Ml(t,Cl),Xl(t,e,n)}function Yl(t,e,n,i){i?function(t){ql(t,!1)}(t):Hl(t,e,n)}function Xl(t,e,n){var i=Qs(t);null!=e?(i.focus=e,i.blurScope=n):i.focus&&(i.focus=null)}var Ul=["emphasis","blur","select"],Zl={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function jl(t,e,n,i){n=n||"itemStyle";for(var r=0;r1&&(a*=ru(f),s*=ru(f));var g=(r===o?-1:1)*ru((a*a*(s*s)-a*a*(d*d)-s*s*(p*p))/(a*a*(d*d)+s*s*(p*p)))||0,y=g*a*d/s,v=g*-s*p/a,m=(t+n)/2+au(c)*y-ou(c)*v,x=(e+i)/2+ou(c)*y+au(c)*v,_=hu([1,0],[(p-y)/a,(d-v)/s]),b=[(p-y)/a,(d-v)/s],w=[(-1*p-y)/a,(-1*d-v)/s],S=hu(b,w);if(uu(b,w)<=-1&&(S=su),uu(b,w)>=1&&(S=0),S<0){var M=Math.round(S/su*1e6)/1e6;S=2*su+M%2*su}h.addData(u,m,x,a,s,_,S,c,o)}var pu=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,du=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var fu=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.applyTransform=function(t){},e}(Is);function gu(t){return null!=t.setData}function yu(t,e){var n=function(t){var e=new os;if(!t)return e;var n,i=0,r=0,o=i,a=r,s=os.CMD,l=t.match(pu);if(!l)return e;for(var u=0;uk*k+L*L&&(M=T,I=C),{cx:M,cy:I,x0:-h,y0:-c,x1:M*(r/b-1),y1:I*(r/b-1)}}function Nu(t,e){var n,i=Lu(e.r,0),r=Lu(e.r0||0,0),o=i>0;if(o||r>0){if(o||(i=r,r=0),r>i){var a=i;i=r,r=a}var s=e.startAngle,l=e.endAngle;if(!isNaN(s)&&!isNaN(l)){var u=e.cx,h=e.cy,c=!!e.clockwise,p=Au(l-s),d=p>Mu&&p%Mu;if(d>Ou&&(p=d),i>Ou)if(p>Mu-Ou)t.moveTo(u+i*Tu(s),h+i*Iu(s)),t.arc(u,h,i,s,l,!c),r>Ou&&(t.moveTo(u+r*Tu(l),h+r*Iu(l)),t.arc(u,h,r,l,s,c));else{var f=void 0,g=void 0,y=void 0,v=void 0,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0,S=void 0,M=void 0,I=void 0,T=void 0,C=void 0,D=void 0,A=void 0,k=i*Tu(s),L=i*Iu(s),P=r*Tu(l),O=r*Iu(l),R=p>Ou;if(R){var N=e.cornerRadius;N&&(n=function(t){var e;if(Y(t)){var n=t.length;if(!n)return t;e=1===n?[t[0],t[0],0,0]:2===n?[t[0],t[0],t[1],t[1]]:3===n?t.concat(t[2]):t}else e=[t,t,t,t];return e}(N),f=n[0],g=n[1],y=n[2],v=n[3]);var E=Au(i-r)/2;if(m=Pu(E,y),x=Pu(E,v),_=Pu(E,f),b=Pu(E,g),M=w=Lu(m,x),I=S=Lu(_,b),(w>Ou||S>Ou)&&(T=i*Tu(l),C=i*Iu(l),D=r*Tu(s),A=r*Iu(s),pOu){var X=Pu(y,M),U=Pu(v,M),Z=Ru(D,A,k,L,i,X,c),j=Ru(T,C,P,O,i,U,c);t.moveTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),M0&&t.arc(u+Z.cx,h+Z.cy,X,Du(Z.y0,Z.x0),Du(Z.y1,Z.x1),!c),t.arc(u,h,i,Du(Z.cy+Z.y1,Z.cx+Z.x1),Du(j.cy+j.y1,j.cx+j.x1),!c),U>0&&t.arc(u+j.cx,h+j.cy,U,Du(j.y1,j.x1),Du(j.y0,j.x0),!c))}else t.moveTo(u+k,h+L),t.arc(u,h,i,s,l,!c);else t.moveTo(u+k,h+L);if(r>Ou&&R)if(I>Ou){X=Pu(f,I),Z=Ru(P,O,T,C,r,-(U=Pu(g,I)),c),j=Ru(k,L,D,A,r,-X,c);t.lineTo(u+Z.cx+Z.x0,h+Z.cy+Z.y0),I0&&t.arc(u+Z.cx,h+Z.cy,U,Du(Z.y0,Z.x0),Du(Z.y1,Z.x1),!c),t.arc(u,h,r,Du(Z.cy+Z.y1,Z.cx+Z.x1),Du(j.cy+j.y1,j.cx+j.x1),c),X>0&&t.arc(u+j.cx,h+j.cy,X,Du(j.y1,j.x1),Du(j.y0,j.x0),!c))}else t.lineTo(u+P,h+O),t.arc(u,h,r,l,s,c);else t.lineTo(u+P,h+O)}else t.moveTo(u,h);t.closePath()}}}var Eu=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},zu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Eu},e.prototype.buildPath=function(t,e){Nu(t,e)},e.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},e}(Is);zu.prototype.type="sector";var Vu=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Bu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Vu},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=2*Math.PI;t.moveTo(n+e.r,i),t.arc(n,i,e.r,0,r,!1),t.moveTo(n+e.r0,i),t.arc(n,i,e.r0,0,r,!0)},e}(Is);function Fu(t,e,n){var i=e.smooth,r=e.points;if(r&&r.length>=2){if(i){var o=function(t,e,n,i){var r,o,a,s,l=[],u=[],h=[],c=[];if(i){a=[1/0,1/0],s=[-1/0,-1/0];for(var p=0,d=t.length;poh[1]){if(a=!1,r)return a;var u=Math.abs(oh[0]-rh[1]),h=Math.abs(rh[0]-oh[1]);Math.min(u,h)>i.len()&&(u0){var c={duration:h.duration,delay:h.delay||0,easing:h.easing,done:o,force:!!o||!!a,setToFinal:!u,scope:t,during:a};l?e.animateFrom(n,c):e.animateTo(n,c)}else e.stopAnimation(),!l&&e.attr(n),a&&a(1),o&&o()}function fh(t,e,n,i,r,o){dh("update",t,e,n,i,r,o)}function gh(t,e,n,i,r,o){dh("enter",t,e,n,i,r,o)}function yh(t){if(!t.__zr)return!0;for(var e=0;eMath.abs(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function Bh(t){return!t.isGroup}function Fh(t,e,n){if(t&&e){var i,r=(i={},t.traverse((function(t){Bh(t)&&t.anid&&(i[t.anid]=t)})),i);e.traverse((function(t){if(Bh(t)&&t.anid){var e=r[t.anid];if(e){var i=o(t);t.attr(o(e)),fh(t,i,n,Qs(t).dataIndex)}}}))}function o(t){var e={x:t.x,y:t.y,rotation:t.rotation};return function(t){return null!=t.shape}(t)&&(e.shape=A({},t.shape)),e}}function Gh(t,e){return z(t,(function(t){var n=t[0];n=bh(n,e.x),n=wh(n,e.x+e.width);var i=t[1];return i=bh(i,e.y),[n,i=wh(i,e.y+e.height)]}))}function Wh(t,e){var n=bh(t.x,e.x),i=wh(t.x+t.width,e.x+e.width),r=bh(t.y,e.y),o=wh(t.y+t.height,e.y+e.height);if(i>=n&&o>=r)return{x:n,y:r,width:i-n,height:o-r}}function Hh(t,e,n){var i=A({rectHover:!0},e),r=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},t)return 0===t.indexOf("image://")?(r.image=t.slice(8),k(r,n),new ks(i)):Ah(t.replace("path://",""),i,n,"center")}function Yh(t,e,n,i,r){for(var o=0,a=r[r.length-1];o=-1e-6)return!1;var f=t-r,g=e-o,y=Uh(f,g,u,h)/d;if(y<0||y>1)return!1;var v=Uh(f,g,c,p)/d;return!(v<0||v>1)}function Uh(t,e,n,i){return t*i-n*e}function Zh(t){var e=t.itemTooltipOption,n=t.componentModel,i=t.itemName,r=U(e)?{formatter:e}:e,o=n.mainType,a=n.componentIndex,s={componentType:o,name:i,$vars:["name"]};s[o+"Index"]=a;var l=t.formatterParamsExtra;l&&E(G(l),(function(t){_t(s,t)||(s[t]=l[t],s.$vars.push(t))}));var u=Qs(t.el);u.componentMainType=o,u.componentIndex=a,u.tooltipConfig={name:i,option:k({content:i,formatterParams:s},r)}}function jh(t,e){var n;t.isGroup&&(n=e(t)),n||t.traverse(e)}function qh(t,e){if(t)if(Y(t))for(var n=0;n-1?Dc:kc;function Rc(t,e){t=t.toUpperCase(),Pc[t]=new Mc(e),Lc[t]=e}function Nc(t){return Pc[t]}Rc(Ac,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),Rc(Dc,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var Ec=1e3,zc=6e4,Vc=36e5,Bc=864e5,Fc=31536e6,Gc={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}",none:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},Wc="{yyyy}-{MM}-{dd}",Hc={year:"{yyyy}",month:"{yyyy}-{MM}",day:Wc,hour:Wc+" "+Gc.hour,minute:Wc+" "+Gc.minute,second:Wc+" "+Gc.second,millisecond:Gc.none},Yc=["year","month","day","hour","minute","second","millisecond"],Xc=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function Uc(t,e){return"0000".substr(0,e-(t+="").length)+t}function Zc(t){switch(t){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return t}}function jc(t){return t===Zc(t)}function qc(t,e,n,i){var r=ro(t),o=r[Jc(n)](),a=r[Qc(n)]()+1,s=Math.floor((a-1)/3)+1,l=r[tp(n)](),u=r["get"+(n?"UTC":"")+"Day"](),h=r[ep(n)](),c=(h-1)%12+1,p=r[np(n)](),d=r[ip(n)](),f=r[rp(n)](),g=(i instanceof Mc?i:Nc(i||Oc)||Pc[kc]).getModel("time"),y=g.get("month"),v=g.get("monthAbbr"),m=g.get("dayOfWeek"),x=g.get("dayOfWeekAbbr");return(e||"").replace(/{yyyy}/g,o+"").replace(/{yy}/g,Uc(o%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,y[a-1]).replace(/{MMM}/g,v[a-1]).replace(/{MM}/g,Uc(a,2)).replace(/{M}/g,a+"").replace(/{dd}/g,Uc(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,m[u]).replace(/{ee}/g,x[u]).replace(/{e}/g,u+"").replace(/{HH}/g,Uc(h,2)).replace(/{H}/g,h+"").replace(/{hh}/g,Uc(c+"",2)).replace(/{h}/g,c+"").replace(/{mm}/g,Uc(p,2)).replace(/{m}/g,p+"").replace(/{ss}/g,Uc(d,2)).replace(/{s}/g,d+"").replace(/{SSS}/g,Uc(f,3)).replace(/{S}/g,f+"")}function Kc(t,e){var n=ro(t),i=n[Qc(e)]()+1,r=n[tp(e)](),o=n[ep(e)](),a=n[np(e)](),s=n[ip(e)](),l=0===n[rp(e)](),u=l&&0===s,h=u&&0===a,c=h&&0===o,p=c&&1===r;return p&&1===i?"year":p?"month":c?"day":h?"hour":u?"minute":l?"second":"millisecond"}function $c(t,e,n){var i=j(t)?ro(t):t;switch(e=e||Kc(t,n)){case"year":return i[Jc(n)]();case"half-year":return i[Qc(n)]()>=6?1:0;case"quarter":return Math.floor((i[Qc(n)]()+1)/4);case"month":return i[Qc(n)]();case"day":return i[tp(n)]();case"half-day":return i[ep(n)]()/24;case"hour":return i[ep(n)]();case"minute":return i[np(n)]();case"second":return i[ip(n)]();case"millisecond":return i[rp(n)]()}}function Jc(t){return t?"getUTCFullYear":"getFullYear"}function Qc(t){return t?"getUTCMonth":"getMonth"}function tp(t){return t?"getUTCDate":"getDate"}function ep(t){return t?"getUTCHours":"getHours"}function np(t){return t?"getUTCMinutes":"getMinutes"}function ip(t){return t?"getUTCSeconds":"getSeconds"}function rp(t){return t?"getUTCMilliseconds":"getMilliseconds"}function op(t){return t?"setUTCFullYear":"setFullYear"}function ap(t){return t?"setUTCMonth":"setMonth"}function sp(t){return t?"setUTCDate":"setDate"}function lp(t){return t?"setUTCHours":"setHours"}function up(t){return t?"setUTCMinutes":"setMinutes"}function hp(t){return t?"setUTCSeconds":"setSeconds"}function cp(t){return t?"setUTCMilliseconds":"setMilliseconds"}function pp(t){if(!co(t))return U(t)?t:"-";var e=(t+"").split(".");return e[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(e.length>1?"."+e[1]:"")}function dp(t,e){return t=(t||"").toLowerCase().replace(/-(.)/g,(function(t,e){return e.toUpperCase()})),e&&t&&(t=t.charAt(0).toUpperCase()+t.slice(1)),t}var fp=st;function gp(t,e,n){function i(t){return t&&ut(t)?t:"-"}function r(t){return!(null==t||isNaN(t)||!isFinite(t))}var o="time"===e,a=t instanceof Date;if(o||a){var s=o?ro(t):t;if(!isNaN(+s))return qc(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(a)return"-"}if("ordinal"===e)return Z(t)?i(t):j(t)&&r(t)?t+"":"-";var l=ho(t);return r(l)?pp(l):Z(t)?i(t):"boolean"==typeof t?t+"":"-"}var yp=["a","b","c","d","e","f","g"],vp=function(t,e){return"{"+t+(null==e?"":e)+"}"};function mp(t,e,n){Y(e)||(e=[e]);var i=e.length;if(!i)return"";for(var r=e[0].$vars||[],o=0;o':'':{renderMode:o,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===r?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function _p(t,e){return e=e||"transparent",U(t)?t:q(t)&&t.colorStops&&(t.colorStops[0]||{}).color||e}function bp(t,e){if("_blank"===e||"blank"===e){var n=window.open();n.opener=null,n.location.href=t}else window.open(t,e)}var wp=E,Sp=["left","right","top","bottom","width","height"],Mp=[["width","left","right"],["height","top","bottom"]];function Ip(t,e,n,i,r){var o=0,a=0;null==i&&(i=1/0),null==r&&(r=1/0);var s=0;e.eachChild((function(l,u){var h,c,p=l.getBoundingRect(),d=e.childAt(u+1),f=d&&d.getBoundingRect();if("horizontal"===t){var g=p.width+(f?-f.x+p.x:0);(h=o+g)>i||l.newline?(o=0,h=g,a+=s+n,s=p.height):s=Math.max(s,p.height)}else{var y=p.height+(f?-f.y+p.y:0);(c=a+y)>r||l.newline?(o+=s+n,a=0,c=y,s=p.width):s=Math.max(s,p.width)}l.newline||(l.x=o,l.y=a,l.markRedraw(),"horizontal"===t?o=h+n:a=c+n)}))}var Tp=Ip;H(Ip,"vertical"),H(Ip,"horizontal");function Cp(t,e,n){n=fp(n||0);var i=e.width,r=e.height,o=Ur(t.left,i),a=Ur(t.top,r),s=Ur(t.right,i),l=Ur(t.bottom,r),u=Ur(t.width,i),h=Ur(t.height,r),c=n[2]+n[0],p=n[1]+n[3],d=t.aspect;switch(isNaN(u)&&(u=i-s-p-o),isNaN(h)&&(h=r-l-c-a),null!=d&&(isNaN(u)&&isNaN(h)&&(d>i/r?u=.8*i:h=.8*r),isNaN(u)&&(u=d*h),isNaN(h)&&(h=u/d)),isNaN(o)&&(o=i-s-u-p),isNaN(a)&&(a=r-l-h-c),t.left||t.right){case"center":o=i/2-u/2-n[3];break;case"right":o=i-u-p}switch(t.top||t.bottom){case"middle":case"center":a=r/2-h/2-n[0];break;case"bottom":a=r-h-c}o=o||0,a=a||0,isNaN(u)&&(u=i-p-o-(s||0)),isNaN(h)&&(h=r-c-a-(l||0));var f=new ze(o+n[3],a+n[0],u,h);return f.margin=n,f}function Dp(t,e,n,i,r,o){var a,s=!r||!r.hv||r.hv[0],l=!r||!r.hv||r.hv[1],u=r&&r.boundingMode||"all";if((o=o||t).x=t.x,o.y=t.y,!s&&!l)return!1;if("raw"===u)a="group"===t.type?new ze(0,0,+e.width||0,+e.height||0):t.getBoundingRect();else if(a=t.getBoundingRect(),t.needLocalTransform()){var h=t.getLocalTransform();(a=a.clone()).applyTransform(h)}var c=Cp(k({width:a.width,height:a.height},e),n,i),p=s?c.x-a.x:0,d=l?c.y-a.y:0;return"raw"===u?(o.x=p,o.y=d):(o.x+=p,o.y+=d),o===t&&t.markRedraw(),!0}function Ap(t){var e=t.layoutMode||t.constructor.layoutMode;return q(e)?e:e?{type:e}:null}function kp(t,e,n){var i=n&&n.ignoreSize;!Y(i)&&(i=[i,i]);var r=a(Mp[0],0),o=a(Mp[1],1);function a(n,r){var o={},a=0,u={},h=0;if(wp(n,(function(e){u[e]=t[e]})),wp(n,(function(t){s(e,t)&&(o[t]=u[t]=e[t]),l(o,t)&&a++,l(u,t)&&h++})),i[r])return l(e,n[1])?u[n[2]]=null:l(e,n[2])&&(u[n[1]]=null),u;if(2!==h&&a){if(a>=2)return o;for(var c=0;c=0;a--)o=C(o,n[a],!0);e.defaultOption=o}return e.defaultOption},e.prototype.getReferringComponents=function(t,e){var n=t+"Index",i=t+"Id";return Bo(this.ecModel,t,{index:this.get(n,!0),id:this.get(i,!0)},e)},e.prototype.getBoxLayoutParams=function(){var t=this;return{left:t.get("left"),top:t.get("top"),right:t.get("right"),bottom:t.get("bottom"),width:t.get("width"),height:t.get("height")}},e.prototype.getZLevelKey=function(){return""},e.prototype.setZLevel=function(t){this.option.zlevel=t},e.protoInitialize=function(){var t=e.prototype;t.type="component",t.id="",t.name="",t.mainType="",t.subType="",t.componentIndex=0}(),e}(Mc);Zo(Rp,Mc),$o(Rp),function(t){var e={};t.registerSubTypeDefaulter=function(t,n){var i=Xo(t);e[i.main]=n},t.determineSubType=function(n,i){var r=i.type;if(!r){var o=Xo(n).main;t.hasSubTypes(n)&&e[o]&&(r=e[o](i))}return r}}(Rp),function(t,e){function n(t,e){return t[e]||(t[e]={predecessor:[],successor:[]}),t[e]}t.topologicalTravel=function(t,i,r,o){if(t.length){var a=function(t){var i={},r=[];return E(t,(function(o){var a=n(i,o),s=function(t,e){var n=[];return E(t,(function(t){P(e,t)>=0&&n.push(t)})),n}(a.originalDeps=e(o),t);a.entryCount=s.length,0===a.entryCount&&r.push(o),E(s,(function(t){P(a.predecessor,t)<0&&a.predecessor.push(t);var e=n(i,t);P(e.successor,t)<0&&e.successor.push(o)}))})),{graph:i,noEntryList:r}}(i),s=a.graph,l=a.noEntryList,u={};for(E(t,(function(t){u[t]=!0}));l.length;){var h=l.pop(),c=s[h],p=!!u[h];p&&(r.call(o,h,c.originalDeps.slice()),delete u[h]),E(c.successor,p?f:d)}E(u,(function(){var t="";throw new Error(t)}))}function d(t){s[t].entryCount--,0===s[t].entryCount&&l.push(t)}function f(t){u[t]=!0,d(t)}}}(Rp,(function(t){var e=[];E(Rp.getClassesByMainType(t),(function(t){e=e.concat(t.dependencies||t.prototype.dependencies||[])})),e=z(e,(function(t){return Xo(t).main})),"dataset"!==t&&P(e,"dataset")<=0&&e.unshift("dataset");return e}));var Np="";"undefined"!=typeof navigator&&(Np=navigator.platform||"");var Ep="rgba(0, 0, 0, 0.2)",zp={darkMode:"auto",colorBy:"series",color:["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],gradientColor:["#f6efa6","#d88273","#bf444c"],aria:{decal:{decals:[{color:Ep,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Ep,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Ep,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Ep,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Ep,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Ep,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:Np.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},Vp=yt(["tooltip","label","itemName","itemId","itemGroupId","seriesName"]),Bp="original",Fp="arrayRows",Gp="objectRows",Wp="keyedColumns",Hp="typedArray",Yp="unknown",Xp="column",Up="row",Zp=1,jp=2,qp=3,Kp=Oo();function $p(t,e,n){var i={},r=Qp(e);if(!r||!t)return i;var o,a,s=[],l=[],u=e.ecModel,h=Kp(u).datasetMap,c=r.uid+"_"+n.seriesLayoutBy;E(t=t.slice(),(function(e,n){var r=q(e)?e:t[n]={name:e};"ordinal"===r.type&&null==o&&(o=n,a=f(r)),i[r.name]=[]}));var p=h.get(c)||h.set(c,{categoryWayDim:a,valueWayDim:0});function d(t,e,n){for(var i=0;ie)return t[i];return t[n-1]}(i,a):n;if((h=h||n)&&h.length){var c=h[l];return r&&(u[r]=c),s.paletteIdx=(l+1)%h.length,c}}var cd="\0_ec_inner";var pd=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(t,e,n,i,r,o){i=i||{},this.option=null,this._theme=new Mc(i),this._locale=new Mc(r),this._optionManager=o},e.prototype.setOption=function(t,e,n){var i=gd(e);this._optionManager.setOption(t,n,i),this._resetOption(null,i)},e.prototype.resetOption=function(t,e){return this._resetOption(t,gd(e))},e.prototype._resetOption=function(t,e){var n=!1,i=this._optionManager;if(!t||"recreate"===t){var r=i.mountOption("recreate"===t);0,this.option&&"recreate"!==t?(this.restoreData(),this._mergeOption(r,e)):od(this,r),n=!0}if("timeline"!==t&&"media"!==t||this.restoreData(),!t||"recreate"===t||"timeline"===t){var o=i.getTimelineOption(this);o&&(n=!0,this._mergeOption(o,e))}if(!t||"recreate"===t||"media"===t){var a=i.getMediaOption(this);a.length&&E(a,(function(t){n=!0,this._mergeOption(t,e)}),this)}return n},e.prototype.mergeOption=function(t){this._mergeOption(t,null)},e.prototype._mergeOption=function(t,e){var n=this.option,i=this._componentsMap,r=this._componentsCount,o=[],a=yt(),s=e&&e.replaceMergeMainTypeMap;Kp(this).datasetMap=yt(),E(t,(function(t,e){null!=t&&(Rp.hasClass(e)?e&&(o.push(e),a.set(e,!0)):n[e]=null==n[e]?T(t):C(n[e],t,!0))})),s&&s.each((function(t,e){Rp.hasClass(e)&&!a.get(e)&&(o.push(e),a.set(e,!0))})),Rp.topologicalTravel(o,Rp.getAllClassMainTypes(),(function(e){var o=function(t,e,n){var i=nd.get(e);if(!i)return n;var r=i(t);return r?n.concat(r):n}(this,e,bo(t[e])),a=i.get(e),l=a?s&&s.get(e)?"replaceMerge":"normalMerge":"replaceAll",u=To(a,o,l);(function(t,e,n){E(t,(function(t){var i=t.newOption;q(i)&&(t.keyInfo.mainType=e,t.keyInfo.subType=function(t,e,n,i){return e.type?e.type:n?n.subType:i.determineSubType(t,e)}(e,i,t.existing,n))}))})(u,e,Rp),n[e]=null,i.set(e,null),r.set(e,0);var h,c=[],p=[],d=0;E(u,(function(t,n){var i=t.existing,r=t.newOption;if(r){var o="series"===e,a=Rp.getClass(e,t.keyInfo.subType,!o);if(!a)return;if("tooltip"===e){if(h)return void 0;h=!0}if(i&&i.constructor===a)i.name=t.keyInfo.name,i.mergeOption(r,this),i.optionUpdated(r,!1);else{var s=A({componentIndex:n},t.keyInfo);A(i=new a(r,this,this,s),s),t.brandNew&&(i.__requireNewView=!0),i.init(r,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(c.push(i.option),p.push(i),d++):(c.push(void 0),p.push(void 0))}),this),n[e]=c,i.set(e,p),r.set(e,d),"series"===e&&id(this)}),this),this._seriesIndices||id(this)},e.prototype.getOption=function(){var t=T(this.option);return E(t,(function(e,n){if(Rp.hasClass(n)){for(var i=bo(e),r=i.length,o=!1,a=r-1;a>=0;a--)i[a]&&!Lo(i[a])?o=!0:(i[a]=null,!o&&r--);i.length=r,t[n]=i}})),delete t[cd],t},e.prototype.getTheme=function(){return this._theme},e.prototype.getLocaleModel=function(){return this._locale},e.prototype.setUpdatePayload=function(t){this._payload=t},e.prototype.getUpdatePayload=function(){return this._payload},e.prototype.getComponent=function(t,e){var n=this._componentsMap.get(t);if(n){var i=n[e||0];if(i)return i;if(null==e)for(var r=0;r=e:"max"===n?t<=e:t===e})(i[a],t,o)||(r=!1)}})),r}var Sd=E,Md=q,Id=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function Td(t){var e=t&&t.itemStyle;if(e)for(var n=0,i=Id.length;n=0;g--){var y=t[g];if(s||(p=y.data.rawIndexOf(y.stackedByDimension,c)),p>=0){var v=y.data.getByRawIndex(y.stackResultDimension,p);if("all"===l||"positive"===l&&v>0||"negative"===l&&v<0||"samesign"===l&&d>=0&&v>0||"samesign"===l&&d<=0&&v<0){d=Qr(d,v),f=v;break}}}return i[0]=d,i[1]=f,i}))}))}var Yd,Xd,Ud,Zd,jd,qd=function(t){this.data=t.data||(t.sourceFormat===Wp?{}:[]),this.sourceFormat=t.sourceFormat||Yp,this.seriesLayoutBy=t.seriesLayoutBy||Xp,this.startIndex=t.startIndex||0,this.dimensionsDetectedCount=t.dimensionsDetectedCount,this.metaRawOption=t.metaRawOption;var e=this.dimensionsDefine=t.dimensionsDefine;if(e)for(var n=0;nu&&(u=d)}s[0]=l,s[1]=u}},i=function(){return this._data?this._data.length/this._dimSize:0};function r(t){for(var e=0;e=0&&(s=o.interpolatedValue[l])}return null!=s?s+"":""})):void 0},t.prototype.getRawValue=function(t,e){return gf(this.getData(e),t)},t.prototype.formatTooltip=function(t,e,n){},t}();function mf(t){var e,n;return q(t)?t.type&&(n=t):e=t,{text:e,frag:n}}function xf(t){return new _f(t)}var _f=function(){function t(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0}return t.prototype.perform=function(t){var e,n=this._upstream,i=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(e=this._plan(this.context));var o,a=h(this._modBy),s=this._modDataCount||0,l=h(t&&t.modBy),u=t&&t.modDataCount||0;function h(t){return!(t>=1)&&(t=1),t}a===l&&s===u||(e="reset"),(this._dirty||"reset"===e)&&(this._dirty=!1,o=this._doReset(i)),this._modBy=l,this._modDataCount=u;var c=t&&t.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var p=this._dueIndex,d=Math.min(null!=c?this._dueIndex+c:1/0,this._dueEnd);if(!i&&(o||p1&&i>0?s:a}};return o;function a(){return e=t?null:oe},gte:function(t,e){return t>=e}},Tf=function(){function t(t,e){if(!j(e)){var n="";0,vo(n)}this._opFn=If[t],this._rvalFloat=ho(e)}return t.prototype.evaluate=function(t){return j(t)?this._opFn(t,this._rvalFloat):this._opFn(ho(t),this._rvalFloat)},t}(),Cf=function(){function t(t,e){var n="desc"===t;this._resultLT=n?1:-1,null==e&&(e=n?"min":"max"),this._incomparable="min"===e?-1/0:1/0}return t.prototype.evaluate=function(t,e){var n=j(t)?t:ho(t),i=j(e)?e:ho(e),r=isNaN(n),o=isNaN(i);if(r&&(n=this._incomparable),o&&(i=this._incomparable),r&&o){var a=U(t),s=U(e);a&&(n=s?t:0),s&&(i=a?e:0)}return ni?-this._resultLT:0},t}(),Df=function(){function t(t,e){this._rval=e,this._isEQ=t,this._rvalTypeof=typeof e,this._rvalFloat=ho(e)}return t.prototype.evaluate=function(t){var e=t===this._rval;if(!e){var n=typeof t;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(e=ho(t)===this._rvalFloat)}return this._isEQ?e:!e},t}();function Af(t,e){return"eq"===t||"ne"===t?new Df("eq"===t,e):_t(If,t)?new Tf(t,e):null}var kf=function(){function t(){}return t.prototype.getRawData=function(){throw new Error("not supported")},t.prototype.getRawDataItem=function(t){throw new Error("not supported")},t.prototype.cloneRawData=function(){},t.prototype.getDimensionInfo=function(t){},t.prototype.cloneAllDimensionInfo=function(){},t.prototype.count=function(){},t.prototype.retrieveValue=function(t,e){},t.prototype.retrieveValueFromItem=function(t,e){},t.prototype.convertValue=function(t,e){return wf(t,e)},t}();function Lf(t){var e=t.sourceFormat;if(!zf(e)){var n="";0,vo(n)}return t.data}function Pf(t){var e=t.sourceFormat,n=t.data;if(!zf(e)){var i="";0,vo(i)}if(e===Fp){for(var r=[],o=0,a=n.length;o65535?Ff:Gf}function Uf(t,e,n,i,r){var o=Yf[n||"float"];if(r){var a=t[e],s=a&&a.length;if(s!==i){for(var l=new o(i),u=0;ug[1]&&(g[1]=f)}return this._rawCount=this._count=s,{start:a,end:s}},t.prototype._initDataFromProvider=function(t,e,n){for(var i=this._provider,r=this._chunks,o=this._dimensions,a=o.length,s=this._rawExtent,l=z(o,(function(t){return t.property})),u=0;uy[1]&&(y[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=e,this._extent=[]},t.prototype.count=function(){return this._count},t.prototype.get=function(t,e){if(!(e>=0&&e=0&&e=this._rawCount||t<0)return-1;if(!this._indices)return t;var e=this._indices,n=e[t];if(null!=n&&nt))return o;r=o-1}}return-1},t.prototype.indicesOfNearest=function(t,e,n){var i=this._chunks[t],r=[];if(!i)return r;null==n&&(n=1/0);for(var o=1/0,a=-1,s=0,l=0,u=this.count();l=0&&a<0)&&(o=c,a=h,s=0),h===a&&(r[s++]=l))}return r.length=s,r},t.prototype.getIndices=function(){var t,e=this._indices;if(e){var n=e.constructor,i=this._count;if(n===Array){t=new n(i);for(var r=0;r=u&&x<=h||isNaN(x))&&(a[s++]=d),d++}p=!0}else if(2===r){f=c[i[0]];var y=c[i[1]],v=t[i[1]][0],m=t[i[1]][1];for(g=0;g=u&&x<=h||isNaN(x))&&(_>=v&&_<=m||isNaN(_))&&(a[s++]=d),d++}p=!0}}if(!p)if(1===r)for(g=0;g=u&&x<=h||isNaN(x))&&(a[s++]=b)}else for(g=0;gt[M][1])&&(w=!1)}w&&(a[s++]=e.getRawIndex(g))}return sy[1]&&(y[1]=g)}}}},t.prototype.lttbDownSample=function(t,e){var n,i,r,o=this.clone([t],!0),a=o._chunks[t],s=this.count(),l=0,u=Math.floor(1/e),h=this.getRawIndex(0),c=new(Xf(this._rawCount))(Math.min(2*(Math.ceil(s/u)+2),s));c[l++]=h;for(var p=1;pn&&(n=i,r=I)}M>0&&M<_-x&&(c[l++]=Math.min(S,r),r=Math.max(S,r)),c[l++]=r,h=r}return c[l++]=this.getRawIndex(s-1),o._count=l,o._indices=c,o.getRawIndex=this._getRawIdx,o},t.prototype.downSample=function(t,e,n,i){for(var r=this.clone([t],!0),o=r._chunks,a=[],s=Math.floor(1/e),l=o[t],u=this.count(),h=r._rawExtent[t]=[1/0,-1/0],c=new(Xf(this._rawCount))(Math.ceil(u/s)),p=0,d=0;du-d&&(s=u-d,a.length=s);for(var f=0;fh[1]&&(h[1]=y),c[p++]=v}return r._count=p,r._indices=c,r._updateGetRawIdx(),r},t.prototype.each=function(t,e){if(this._count)for(var n=t.length,i=this._chunks,r=0,o=this.count();ra&&(a=l)}return i=[o,a],this._extent[t]=i,i},t.prototype.getRawDataItem=function(t){var e=this.getRawIndex(t);if(this._provider.persistent)return this._provider.getItem(e);for(var n=[],i=this._chunks,r=0;r=0?this._indices[t]:-1},t.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},t.internalField=function(){function t(t,e,n,i){return wf(t[i],this._dimensions[i])}Vf={arrayRows:t,objectRows:function(t,e,n,i){return wf(t[e],this._dimensions[i])},keyedColumns:t,original:function(t,e,n,i){var r=t&&(null==t.value?t:t.value);return wf(r instanceof Array?r[i]:r,this._dimensions[i])},typedArray:function(t,e,n,i){return t[i]}}}(),t}(),jf=function(){function t(t){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=t}return t.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},t.prototype._setLocalSource=function(t,e){this._sourceList=t,this._upstreamSignList=e,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},t.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},t.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},t.prototype._createSource=function(){this._setLocalSource([],[]);var t,e,n=this._sourceHost,i=this._getUpstreamSourceManagers(),r=!!i.length;if(Kf(n)){var o=n,a=void 0,s=void 0,l=void 0;if(r){var u=i[0];u.prepareSource(),a=(l=u.getSource()).data,s=l.sourceFormat,e=[u._getVersionSign()]}else s=$(a=o.get("data",!0))?Hp:Bp,e=[];var h=this._getSourceMetaRawOption()||{},c=l&&l.metaRawOption||{},p=rt(h.seriesLayoutBy,c.seriesLayoutBy)||null,d=rt(h.sourceHeader,c.sourceHeader),f=rt(h.dimensions,c.dimensions);t=p!==c.seriesLayoutBy||!!d!=!!c.sourceHeader||f?[$d(a,{seriesLayoutBy:p,sourceHeader:d,dimensions:f},s)]:[]}else{var g=n;if(r){var y=this._applyTransform(i);t=y.sourceList,e=y.upstreamSignList}else{t=[$d(g.get("source",!0),this._getSourceMetaRawOption(),null)],e=[]}}this._setLocalSource(t,e)},t.prototype._applyTransform=function(t){var e,n=this._sourceHost,i=n.get("transform",!0),r=n.get("fromTransformResult",!0);if(null!=r){var o="";1!==t.length&&$f(o)}var a,s=[],l=[];return E(t,(function(t){t.prepareSource();var e=t.getSource(r||0),n="";null==r||e||$f(n),s.push(e),l.push(t._getVersionSign())})),i?e=function(t,e,n){var i=bo(t),r=i.length,o="";r||vo(o);for(var a=0,s=r;a1||n>0&&!t.noHeader;return E(t.blocks,(function(t){var n=og(t);n>=e&&(e=n+ +(i&&(!n||ig(t)&&!t.noHeader)))})),e}return 0}function ag(t,e,n,i){var r,o=e.noHeader,a=(r=og(e),{html:tg[r],richText:eg[r]}),s=[],l=e.blocks||[];lt(!l||Y(l)),l=l||[];var u=t.orderMode;if(e.sortBlocks&&u){l=l.slice();var h={valueAsc:"asc",valueDesc:"desc"};if(_t(h,u)){var c=new Cf(h[u],null);l.sort((function(t,e){return c.evaluate(t.sortParam,e.sortParam)}))}else"seriesDesc"===u&&l.reverse()}E(l,(function(n,r){var o=e.valueFormatter,l=rg(n)(o?A(A({},t),{valueFormatter:o}):t,n,r>0?a.html:0,i);null!=l&&s.push(l)}));var p="richText"===t.renderMode?s.join(a.richText):ug(s.join(""),o?n:a.html);if(o)return p;var d=gp(e.header,"ordinal",t.useUTC),f=Qf(i,t.renderMode).nameStyle;return"richText"===t.renderMode?hg(t,d,f)+a.richText+p:ug('
    '+re(d)+"
    "+p,n)}function sg(t,e,n,i){var r=t.renderMode,o=e.noName,a=e.noValue,s=!e.markerType,l=e.name,u=t.useUTC,h=e.valueFormatter||t.valueFormatter||function(t){return z(t=Y(t)?t:[t],(function(t,e){return gp(t,Y(d)?d[e]:d,u)}))};if(!o||!a){var c=s?"":t.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||"#333",r),p=o?"":gp(l,"ordinal",u),d=e.valueType,f=a?[]:h(e.value),g=!s||!o,y=!s&&o,v=Qf(i,r),m=v.nameStyle,x=v.valueStyle;return"richText"===r?(s?"":c)+(o?"":hg(t,p,m))+(a?"":function(t,e,n,i,r){var o=[r],a=i?10:20;return n&&o.push({padding:[0,0,0,a],align:"right"}),t.markupStyleCreator.wrapRichTextStyle(Y(e)?e.join(" "):e,o)}(t,f,g,y,x)):ug((s?"":c)+(o?"":function(t,e,n){return''+re(t)+""}(p,!s,m))+(a?"":function(t,e,n,i){var r=n?"10px":"20px",o=e?"float:right;margin-left:"+r:"";return t=Y(t)?t:[t],''+z(t,(function(t){return re(t)})).join("  ")+""}(f,g,y,x)),n)}}function lg(t,e,n,i,r,o){if(t)return rg(t)({useUTC:r,renderMode:n,orderMode:i,markupStyleCreator:e,valueFormatter:t.valueFormatter},t,0,o)}function ug(t,e){return'
    '+t+'
    '}function hg(t,e,n){return t.markupStyleCreator.wrapRichTextStyle(e,n)}function cg(t,e){return _p(t.getData().getItemVisual(e,"style")[t.visualDrawType])}function pg(t,e){var n=t.get("padding");return null!=n?n:"richText"===e?[8,10]:10}var dg=function(){function t(){this.richTextStyles={},this._nextStyleNameId=po()}return t.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},t.prototype.makeTooltipMarker=function(t,e,n){var i="richText"===n?this._generateStyleName():null,r=xp({color:e,type:t,renderMode:n,markerId:i});return U(r)?r:(this.richTextStyles[i]=r.style,r.content)},t.prototype.wrapRichTextStyle=function(t,e){var n={};Y(e)?E(e,(function(t){return A(n,t)})):A(n,e);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+t+"}"},t}();function fg(t){var e,n,i,r,o=t.series,a=t.dataIndex,s=t.multipleSeries,l=o.getData(),u=l.mapDimensionsAll("defaultedTooltip"),h=u.length,c=o.getRawValue(a),p=Y(c),d=cg(o,a);if(h>1||p&&!h){var f=function(t,e,n,i,r){var o=e.getData(),a=V(t,(function(t,e,n){var i=o.getDimensionInfo(n);return t||i&&!1!==i.tooltip&&null!=i.displayName}),!1),s=[],l=[],u=[];function h(t,e){var n=o.getDimensionInfo(e);n&&!1!==n.otherDims.tooltip&&(a?u.push(ng("nameValue",{markerType:"subItem",markerColor:r,name:n.displayName,value:t,valueType:n.type})):(s.push(t),l.push(n.type)))}return i.length?E(i,(function(t){h(gf(o,n,t),t)})):E(t,h),{inlineValues:s,inlineValueTypes:l,blocks:u}}(c,o,a,u,d);e=f.inlineValues,n=f.inlineValueTypes,i=f.blocks,r=f.inlineValues[0]}else if(h){var g=l.getDimensionInfo(u[0]);r=e=gf(l,a,u[0]),n=g.type}else r=e=p?c[0]:c;var y=ko(o),v=y&&o.name||"",m=l.getName(a),x=s?v:m;return ng("section",{header:v,noHeader:s||!y,sortParam:r,blocks:[ng("nameValue",{markerType:"item",markerColor:d,name:x,noName:!ut(x),value:e,valueType:n})].concat(i||[])})}var gg=Oo();function yg(t,e){return t.getName(e)||t.getId(e)}var vg="__universalTransitionEnabled",mg=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._selectedDataIndicesMap={},e}return n(e,t),e.prototype.init=function(t,e,n){this.seriesIndex=this.componentIndex,this.dataTask=xf({count:_g,reset:bg}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n),(gg(this).sourceManager=new jf(this)).prepareSource();var i=this.getInitialData(t,n);Sg(i,this),this.dataTask.context.data=i,gg(this).dataBeforeProcessed=i,xg(this),this._initSelectedMapFromData(i)},e.prototype.mergeDefaultAndTheme=function(t,e){var n=Ap(this),i=n?Lp(t):{},r=this.subType;Rp.hasClass(r)&&(r+="Series"),C(t,e.getTheme().get(this.subType)),C(t,this.getDefaultOption()),wo(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&kp(t,i,n)},e.prototype.mergeOption=function(t,e){t=C(this.option,t,!0),this.fillDataTextStyle(t.data);var n=Ap(this);n&&kp(this.option,t,n);var i=gg(this).sourceManager;i.dirty(),i.prepareSource();var r=this.getInitialData(t,e);Sg(r,this),this.dataTask.dirty(),this.dataTask.context.data=r,gg(this).dataBeforeProcessed=r,xg(this),this._initSelectedMapFromData(r)},e.prototype.fillDataTextStyle=function(t){if(t&&!$(t))for(var e=["show"],n=0;nthis.getShallow("animationThreshold")&&(e=!1),!!e},e.prototype.restoreData=function(){this.dataTask.dirty()},e.prototype.getColorFromPalette=function(t,e,n){var i=this.ecModel,r=ld.prototype.getColorFromPalette.call(this,t,e,n);return r||(r=i.getColorFromPalette(t,e,n)),r},e.prototype.coordDimToDataDim=function(t){return this.getRawData().mapDimensionsAll(t)},e.prototype.getProgressive=function(){return this.get("progressive")},e.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},e.prototype.select=function(t,e){this._innerSelect(this.getData(e),t)},e.prototype.unselect=function(t,e){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,r=this.getData(e);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var o=0;o=0&&n.push(r)}return n},e.prototype.isSelected=function(t,e){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(e);return("all"===n||n[yg(i,t)])&&!i.getItemModel(t).get(["select","disabled"])},e.prototype.isUniversalTransitionEnabled=function(){if(this[vg])return!0;var t=this.option.universalTransition;return!!t&&(!0===t||t&&t.enabled)},e.prototype._innerSelect=function(t,e){var n,i,r=this.option,o=r.selectedMode,a=e.length;if(o&&a)if("series"===o)r.selectedMap="all";else if("multiple"===o){q(r.selectedMap)||(r.selectedMap={});for(var s=r.selectedMap,l=0;l0&&this._innerSelect(t,e)}},e.registerClass=function(t){return Rp.registerClass(t)},e.protoInitialize=function(){var t=e.prototype;t.type="series.__base__",t.seriesIndex=0,t.ignoreStyleOnData=!1,t.hasSymbolVisual=!1,t.defaultSymbol="circle",t.visualStyleAccessPath="itemStyle",t.visualDrawType="fill"}(),e}(Rp);function xg(t){var e=t.name;ko(t)||(t.name=function(t){var e=t.getRawData(),n=e.mapDimensionsAll("seriesName"),i=[];return E(n,(function(t){var n=e.getDimensionInfo(t);n.displayName&&i.push(n.displayName)})),i.join(" ")}(t)||e)}function _g(t){return t.model.getRawData().count()}function bg(t){var e=t.model;return e.setData(e.getRawData().cloneShallow()),wg}function wg(t,e){e.outputData&&t.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function Sg(t,e){E(vt(t.CHANGABLE_METHODS,t.DOWNSAMPLE_METHODS),(function(n){t.wrapMethod(n,H(Mg,e))}))}function Mg(t,e){var n=Ig(t);return n&&n.setOutputEnd((e||this).count()),e}function Ig(t){var e=(t.ecModel||{}).scheduler,n=e&&e.getPipeline(t.uid);if(n){var i=n.currentTask;if(i){var r=i.agentStubMap;r&&(i=r.get(t.uid))}return i}}R(mg,vf),R(mg,ld),Zo(mg,Rp);var Tg=function(){function t(){this.group=new zr,this.uid=Tc("viewComponent")}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){},t.prototype.updateLayout=function(t,e,n,i){},t.prototype.updateVisual=function(t,e,n,i){},t.prototype.toggleBlurSeries=function(t,e,n){},t.prototype.eachRendered=function(t){var e=this.group;e&&e.traverse(t)},t}();function Cg(){var t=Oo();return function(e){var n=t(e),i=e.pipelineContext,r=!!n.large,o=!!n.progressiveRender,a=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(r===a&&o===s)&&"reset"}}Uo(Tg),$o(Tg);var Dg=Oo(),Ag=Cg(),kg=function(){function t(){this.group=new zr,this.uid=Tc("viewChart"),this.renderTask=xf({plan:Og,reset:Rg}),this.renderTask.context={view:this}}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){0},t.prototype.highlight=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&Pg(r,i,"emphasis")},t.prototype.downplay=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&Pg(r,i,"normal")},t.prototype.remove=function(t,e){this.group.removeAll()},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateLayout=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateVisual=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.eachRendered=function(t){qh(this.group,t)},t.markUpdateMethod=function(t,e){Dg(t).updateMethod=e},t.protoInitialize=void(t.prototype.type="chart"),t}();function Lg(t,e,n){t&&Kl(t)&&("emphasis"===e?kl:Ll)(t,n)}function Pg(t,e,n){var i=Po(t,e),r=e&&null!=e.highlightKey?function(t){var e=nl[t];return null==e&&el<=32&&(e=nl[t]=el++),e}(e.highlightKey):null;null!=i?E(bo(i),(function(e){Lg(t.getItemGraphicEl(e),n,r)})):t.eachItemGraphicEl((function(t){Lg(t,n,r)}))}function Og(t){return Ag(t.model)}function Rg(t){var e=t.model,n=t.ecModel,i=t.api,r=t.payload,o=e.pipelineContext.progressiveRender,a=t.view,s=r&&Dg(r).updateMethod,l=o?"incrementalPrepareRender":s&&a[s]?s:"render";return"render"!==l&&a[l](e,n,i,r),Ng[l]}Uo(kg),$o(kg);var Ng={incrementalPrepareRender:{progress:function(t,e){e.view.incrementalRender(t,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(t,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},Eg="\0__throttleOriginMethod",zg="\0__throttleRate",Vg="\0__throttleType";function Bg(t,e,n){var i,r,o,a,s,l=0,u=0,h=null;function c(){u=(new Date).getTime(),h=null,t.apply(o,a||[])}e=e||0;var p=function(){for(var t=[],p=0;p=0?c():h=setTimeout(c,-r),l=i};return p.clear=function(){h&&(clearTimeout(h),h=null)},p.debounceNextCall=function(t){s=t},p}function Fg(t,e,n,i){var r=t[e];if(r){var o=r[Eg]||r,a=r[Vg];if(r[zg]!==n||a!==i){if(null==n||!i)return t[e]=o;(r=t[e]=Bg(o,n,"debounce"===i))[Eg]=o,r[Vg]=i,r[zg]=n}return r}}function Gg(t,e){var n=t[e];n&&n[Eg]&&(n.clear&&n.clear(),t[e]=n[Eg])}var Wg=Oo(),Hg={itemStyle:Jo(bc,!0),lineStyle:Jo(mc,!0)},Yg={lineStyle:"stroke",itemStyle:"fill"};function Xg(t,e){var n=t.visualStyleMapper||Hg[e];return n||(console.warn("Unknown style type '"+e+"'."),Hg.itemStyle)}function Ug(t,e){var n=t.visualDrawType||Yg[e];return n||(console.warn("Unknown style type '"+e+"'."),"fill")}var Zg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=t.getModel(i),o=Xg(t,i)(r),a=r.getShallow("decal");a&&(n.setVisual("decal",a),a.dirty=!0);var s=Ug(t,i),l=o[s],u=X(l)?l:null,h="auto"===o.fill||"auto"===o.stroke;if(!o[s]||u||h){var c=t.getColorFromPalette(t.name,null,e.getSeriesCount());o[s]||(o[s]=c,n.setVisual("colorFromPalette",!0)),o.fill="auto"===o.fill||X(o.fill)?c:o.fill,o.stroke="auto"===o.stroke||X(o.stroke)?c:o.stroke}if(n.setVisual("style",o),n.setVisual("drawType",s),!e.isSeriesFiltered(t)&&u)return n.setVisual("colorFromPalette",!1),{dataEach:function(e,n){var i=t.getDataParams(n),r=A({},o);r[s]=u(i),e.setItemVisual(n,"style",r)}}}},jg=new Mc,qg={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){if(!t.ignoreStyleOnData&&!e.isSeriesFiltered(t)){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=Xg(t,i),o=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(t,e){var n=t.getRawDataItem(e);if(n&&n[i]){jg.option=n[i];var a=r(jg);A(t.ensureUniqueItemVisual(e,"style"),a),jg.option.decal&&(t.setItemVisual(e,"decal",jg.option.decal),jg.option.decal.dirty=!0),o in a&&t.setItemVisual(e,"colorFromPalette",!1)}}:null}}}},Kg={performRawSeries:!0,overallReset:function(t){var e=yt();t.eachSeries((function(t){var n=t.getColorBy();if(!t.isColorBySeries()){var i=t.type+"-"+n,r=e.get(i);r||(r={},e.set(i,r)),Wg(t).scope=r}})),t.eachSeries((function(e){if(!e.isColorBySeries()&&!t.isSeriesFiltered(e)){var n=e.getRawData(),i={},r=e.getData(),o=Wg(e).scope,a=e.visualStyleAccessPath||"itemStyle",s=Ug(e,a);r.each((function(t){var e=r.getRawIndex(t);i[e]=t})),n.each((function(t){var a=i[t];if(r.getItemVisual(a,"colorFromPalette")){var l=r.ensureUniqueItemVisual(a,"style"),u=n.getName(t)||t+"",h=n.count();l[s]=e.getColorFromPalette(u,o,h)}}))}}))}},$g=Math.PI;var Jg=function(){function t(t,e,n,i){this._stageTaskMap=yt(),this.ecInstance=t,this.api=e,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return t.prototype.restoreData=function(t,e){t.restoreData(e),this._stageTaskMap.each((function(t){var e=t.overallTask;e&&e.dirty()}))},t.prototype.getPerformArgs=function(t,e){if(t.__pipeline){var n=this._pipelineMap.get(t.__pipeline.id),i=n.context,r=!e&&n.progressiveEnabled&&(!i||i.progressiveRender)&&t.__idxInPipeline>n.blockIndex?n.step:null,o=i&&i.modDataCount;return{step:r,modBy:null!=o?Math.ceil(o/r):null,modDataCount:o}}},t.prototype.getPipeline=function(t){return this._pipelineMap.get(t)},t.prototype.updateStreamModes=function(t,e){var n=this._pipelineMap.get(t.uid),i=t.getData().count(),r=n.progressiveEnabled&&e.incrementalPrepareRender&&i>=n.threshold,o=t.get("large")&&i>=t.get("largeThreshold"),a="mod"===t.get("progressiveChunkMode")?i:null;t.pipelineContext=n.context={progressiveRender:r,modDataCount:a,large:o}},t.prototype.restorePipelines=function(t){var e=this,n=e._pipelineMap=yt();t.eachSeries((function(t){var i=t.getProgressive(),r=t.uid;n.set(r,{id:r,head:null,tail:null,threshold:t.getProgressiveThreshold(),progressiveEnabled:i&&!(t.preventIncremental&&t.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),e._pipe(t,t.dataTask)}))},t.prototype.prepareStageTasks=function(){var t=this._stageTaskMap,e=this.api.getModel(),n=this.api;E(this._allHandlers,(function(i){var r=t.get(i.uid)||t.set(i.uid,{}),o="";lt(!(i.reset&&i.overallReset),o),i.reset&&this._createSeriesStageTask(i,r,e,n),i.overallReset&&this._createOverallStageTask(i,r,e,n)}),this)},t.prototype.prepareView=function(t,e,n,i){var r=t.renderTask,o=r.context;o.model=e,o.ecModel=n,o.api=i,r.__block=!t.incrementalPrepareRender,this._pipe(e,r)},t.prototype.performDataProcessorTasks=function(t,e){this._performStageTasks(this._dataProcessorHandlers,t,e,{block:!0})},t.prototype.performVisualTasks=function(t,e,n){this._performStageTasks(this._visualHandlers,t,e,n)},t.prototype._performStageTasks=function(t,e,n,i){i=i||{};var r=!1,o=this;function a(t,e){return t.setDirty&&(!t.dirtyMap||t.dirtyMap.get(e.__pipeline.id))}E(t,(function(t,s){if(!i.visualType||i.visualType===t.visualType){var l=o._stageTaskMap.get(t.uid),u=l.seriesTaskMap,h=l.overallTask;if(h){var c,p=h.agentStubMap;p.each((function(t){a(i,t)&&(t.dirty(),c=!0)})),c&&h.dirty(),o.updatePayload(h,n);var d=o.getPerformArgs(h,i.block);p.each((function(t){t.perform(d)})),h.perform(d)&&(r=!0)}else u&&u.each((function(s,l){a(i,s)&&s.dirty();var u=o.getPerformArgs(s,i.block);u.skip=!t.performRawSeries&&e.isSeriesFiltered(s.context.model),o.updatePayload(s,n),s.perform(u)&&(r=!0)}))}})),this.unfinished=r||this.unfinished},t.prototype.performSeriesTasks=function(t){var e;t.eachSeries((function(t){e=t.dataTask.perform()||e})),this.unfinished=e||this.unfinished},t.prototype.plan=function(){this._pipelineMap.each((function(t){var e=t.tail;do{if(e.__block){t.blockIndex=e.__idxInPipeline;break}e=e.getUpstream()}while(e)}))},t.prototype.updatePayload=function(t,e){"remain"!==e&&(t.context.payload=e)},t.prototype._createSeriesStageTask=function(t,e,n,i){var r=this,o=e.seriesTaskMap,a=e.seriesTaskMap=yt(),s=t.seriesType,l=t.getTargetSeries;function u(e){var s=e.uid,l=a.set(s,o&&o.get(s)||xf({plan:iy,reset:ry,count:sy}));l.context={model:e,ecModel:n,api:i,useClearVisual:t.isVisual&&!t.isLayout,plan:t.plan,reset:t.reset,scheduler:r},r._pipe(e,l)}t.createOnAllSeries?n.eachRawSeries(u):s?n.eachRawSeriesByType(s,u):l&&l(n,i).each(u)},t.prototype._createOverallStageTask=function(t,e,n,i){var r=this,o=e.overallTask=e.overallTask||xf({reset:Qg});o.context={ecModel:n,api:i,overallReset:t.overallReset,scheduler:r};var a=o.agentStubMap,s=o.agentStubMap=yt(),l=t.seriesType,u=t.getTargetSeries,h=!0,c=!1,p="";function d(t){var e=t.uid,n=s.set(e,a&&a.get(e)||(c=!0,xf({reset:ty,onDirty:ny})));n.context={model:t,overallProgress:h},n.agent=o,n.__block=h,r._pipe(t,n)}lt(!t.createOnAllSeries,p),l?n.eachRawSeriesByType(l,d):u?u(n,i).each(d):(h=!1,E(n.getSeries(),d)),c&&o.dirty()},t.prototype._pipe=function(t,e){var n=t.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=e),i.tail&&i.tail.pipe(e),i.tail=e,e.__idxInPipeline=i.count++,e.__pipeline=i},t.wrapStageHandler=function(t,e){return X(t)&&(t={overallReset:t,seriesType:ly(t)}),t.uid=Tc("stageHandler"),e&&(t.visualType=e),t},t}();function Qg(t){t.overallReset(t.ecModel,t.api,t.payload)}function ty(t){return t.overallProgress&&ey}function ey(){this.agent.dirty(),this.getDownstream().dirty()}function ny(){this.agent&&this.agent.dirty()}function iy(t){return t.plan?t.plan(t.model,t.ecModel,t.api,t.payload):null}function ry(t){t.useClearVisual&&t.data.clearAllVisual();var e=t.resetDefines=bo(t.reset(t.model,t.ecModel,t.api,t.payload));return e.length>1?z(e,(function(t,e){return ay(e)})):oy}var oy=ay(0);function ay(t){return function(e,n){var i=n.data,r=n.resetDefines[t];if(r&&r.dataEach)for(var o=e.start;o0&&h===r.length-u.length){var c=r.slice(0,h);"data"!==c&&(e.mainType=c,e[u.toLowerCase()]=t,s=!0)}}a.hasOwnProperty(r)&&(n[r]=t,s=!0),s||(i[r]=t)}))}return{cptQuery:e,dataQuery:n,otherQuery:i}},t.prototype.filter=function(t,e){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,r=n.packedEvent,o=n.model,a=n.view;if(!o||!a)return!0;var s=e.cptQuery,l=e.dataQuery;return u(s,o,"mainType")&&u(s,o,"subType")&&u(s,o,"index","componentIndex")&&u(s,o,"name")&&u(s,o,"id")&&u(l,r,"name")&&u(l,r,"dataIndex")&&u(l,r,"dataType")&&(!a.filterForExposedEvent||a.filterForExposedEvent(t,e.otherQuery,i,r));function u(t,e,n,i){return null==t[n]||e[i||n]===t[n]}},t.prototype.afterTrigger=function(){this.eventInfo=null},t}(),by=["symbol","symbolSize","symbolRotate","symbolOffset"],wy=by.concat(["symbolKeepAspect"]),Sy={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData();if(t.legendIcon&&n.setVisual("legendIcon",t.legendIcon),t.hasSymbolVisual){for(var i={},r={},o=!1,a=0;a=0&&Xy(l)?l:.5,t.createRadialGradient(a,s,0,a,s,l)}(t,e,n):function(t,e,n){var i=null==e.x?0:e.x,r=null==e.x2?1:e.x2,o=null==e.y?0:e.y,a=null==e.y2?0:e.y2;return e.global||(i=i*n.width+n.x,r=r*n.width+n.x,o=o*n.height+n.y,a=a*n.height+n.y),i=Xy(i)?i:0,r=Xy(r)?r:1,o=Xy(o)?o:0,a=Xy(a)?a:0,t.createLinearGradient(i,o,r,a)}(t,e,n),r=e.colorStops,o=0;o0&&(e=i.lineDash,n=i.lineWidth,e&&"solid"!==e&&n>0?"dashed"===e?[4*n,2*n]:"dotted"===e?[n]:j(e)?[e]:Y(e)?e:null:null),o=i.lineDashOffset;if(r){var a=i.strokeNoScale&&t.getLineScale?t.getLineScale():1;a&&1!==a&&(r=z(r,(function(t){return t/a})),o/=a)}return[r,o]}var Ky=new os(!0);function $y(t){var e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))}function Jy(t){return"string"==typeof t&&"none"!==t}function Qy(t){var e=t.fill;return null!=e&&"none"!==e}function tv(t,e){if(null!=e.fillOpacity&&1!==e.fillOpacity){var n=t.globalAlpha;t.globalAlpha=e.fillOpacity*e.opacity,t.fill(),t.globalAlpha=n}else t.fill()}function ev(t,e){if(null!=e.strokeOpacity&&1!==e.strokeOpacity){var n=t.globalAlpha;t.globalAlpha=e.strokeOpacity*e.opacity,t.stroke(),t.globalAlpha=n}else t.stroke()}function nv(t,e,n){var i=ia(e.image,e.__image,n);if(oa(i)){var r=t.createPattern(i,e.repeat||"repeat");if("function"==typeof DOMMatrix&&r&&r.setTransform){var o=new DOMMatrix;o.translateSelf(e.x||0,e.y||0),o.rotateSelf(0,0,(e.rotation||0)*wt),o.scaleSelf(e.scaleX||1,e.scaleY||1),r.setTransform(o)}return r}}var iv=["shadowBlur","shadowOffsetX","shadowOffsetY"],rv=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function ov(t,e,n,i,r){var o=!1;if(!i&&e===(n=n||{}))return!1;if(i||e.opacity!==n.opacity){lv(t,r),o=!0;var a=Math.max(Math.min(e.opacity,1),0);t.globalAlpha=isNaN(a)?xa.opacity:a}(i||e.blend!==n.blend)&&(o||(lv(t,r),o=!0),t.globalCompositeOperation=e.blend||xa.blend);for(var s=0;s0&&t.unfinished);t.unfinished||this._zr.flush()}}},e.prototype.getDom=function(){return this._dom},e.prototype.getId=function(){return this.id},e.prototype.getZr=function(){return this._zr},e.prototype.isSSR=function(){return this._ssr},e.prototype.setOption=function(t,e,n){if(!this[Iv])if(this._disposed)nm(this.id);else{var i,r,o;if(q(e)&&(n=e.lazyUpdate,i=e.silent,r=e.replaceMerge,o=e.transition,e=e.notMerge),this[Iv]=!0,!this._model||e){var a=new bd(this._api),s=this._theme,l=this._model=new pd;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,a)}this._model.setOption(t,{replaceMerge:r},am);var u={seriesTransition:o,optionChanged:!0};if(n)this[Tv]={silent:i,updateParams:u},this[Iv]=!1,this.getZr().wakeUp();else{try{Ov(this),Ev.update.call(this,null,u)}catch(t){throw this[Tv]=null,this[Iv]=!1,t}this._ssr||this._zr.flush(),this[Tv]=null,this[Iv]=!1,Fv.call(this,i),Gv.call(this,i)}}},e.prototype.setTheme=function(){yo()},e.prototype.getModel=function(){return this._model},e.prototype.getOption=function(){return this._model&&this._model.getOption()},e.prototype.getWidth=function(){return this._zr.getWidth()},e.prototype.getHeight=function(){return this._zr.getHeight()},e.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||r.hasGlobalWindow&&window.devicePixelRatio||1},e.prototype.getRenderedCanvas=function(t){return this.renderToCanvas(t)},e.prototype.renderToCanvas=function(t){t=t||{};var e=this._zr.painter;return e.getRenderedCanvas({backgroundColor:t.backgroundColor||this._model.get("backgroundColor"),pixelRatio:t.pixelRatio||this.getDevicePixelRatio()})},e.prototype.renderToSVGString=function(t){t=t||{};var e=this._zr.painter;return e.renderToString({useViewBox:t.useViewBox})},e.prototype.getSvgDataURL=function(){if(r.svgSupported){var t=this._zr;return E(t.storage.getDisplayList(),(function(t){t.stopAnimation(null,!0)})),t.painter.toDataURL()}},e.prototype.getDataURL=function(t){if(!this._disposed){var e=(t=t||{}).excludeComponents,n=this._model,i=[],r=this;E(e,(function(t){n.eachComponent({mainType:t},(function(t){var e=r._componentsMap[t.__viewId];e.group.ignore||(i.push(e),e.group.ignore=!0)}))}));var o="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return E(i,(function(t){t.group.ignore=!1})),o}nm(this.id)},e.prototype.getConnectedDataURL=function(t){if(!this._disposed){var e="svg"===t.type,n=this.group,i=Math.min,r=Math.max,o=1/0;if(cm[n]){var a=o,s=o,l=-1/0,u=-1/0,c=[],p=t&&t.pixelRatio||this.getDevicePixelRatio();E(hm,(function(o,h){if(o.group===n){var p=e?o.getZr().painter.getSvgDom().innerHTML:o.renderToCanvas(T(t)),d=o.getDom().getBoundingClientRect();a=i(d.left,a),s=i(d.top,s),l=r(d.right,l),u=r(d.bottom,u),c.push({dom:p,left:d.left,top:d.top})}}));var d=(l*=p)-(a*=p),f=(u*=p)-(s*=p),g=h.createCanvas(),y=Gr(g,{renderer:e?"svg":"canvas"});if(y.resize({width:d,height:f}),e){var v="";return E(c,(function(t){var e=t.left-a,n=t.top-s;v+=''+t.dom+""})),y.painter.getSvgRoot().innerHTML=v,t.connectedBackgroundColor&&y.painter.setBackgroundColor(t.connectedBackgroundColor),y.refreshImmediately(),y.painter.toDataURL()}return t.connectedBackgroundColor&&y.add(new zs({shape:{x:0,y:0,width:d,height:f},style:{fill:t.connectedBackgroundColor}})),E(c,(function(t){var e=new ks({style:{x:t.left*p-a,y:t.top*p-s,image:t.dom}});y.add(e)})),y.refreshImmediately(),g.toDataURL("image/"+(t&&t.type||"png"))}return this.getDataURL(t)}nm(this.id)},e.prototype.convertToPixel=function(t,e){return zv(this,"convertToPixel",t,e)},e.prototype.convertFromPixel=function(t,e){return zv(this,"convertFromPixel",t,e)},e.prototype.containPixel=function(t,e){var n;if(!this._disposed)return E(No(this._model,t),(function(t,i){i.indexOf("Models")>=0&&E(t,(function(t){var r=t.coordinateSystem;if(r&&r.containPoint)n=n||!!r.containPoint(e);else if("seriesModels"===i){var o=this._chartsMap[t.__viewId];o&&o.containPoint&&(n=n||o.containPoint(e,t))}else 0}),this)}),this),!!n;nm(this.id)},e.prototype.getVisual=function(t,e){var n=No(this._model,t,{defaultMainType:"series"}),i=n.seriesModel;var r=i.getData(),o=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?r.indexOfRawIndex(n.dataIndex):null;return null!=o?Iy(r,o,e):Ty(r,e)},e.prototype.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},e.prototype.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]},e.prototype._initEvents=function(){var t,e,n,i=this;E(em,(function(t){var e=function(e){var n,r=i.getModel(),o=e.target,a="globalout"===t;if(a?n={}:o&&ky(o,(function(t){var e=Qs(t);if(e&&null!=e.dataIndex){var i=e.dataModel||r.getSeriesByIndex(e.seriesIndex);return n=i&&i.getDataParams(e.dataIndex,e.dataType,o)||{},!0}if(e.eventData)return n=A({},e.eventData),!0}),!0),n){var s=n.componentType,l=n.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=n.seriesIndex);var u=s&&null!=l&&r.getComponent(s,l),h=u&&i["series"===u.mainType?"_chartsMap":"_componentsMap"][u.__viewId];0,n.event=e,n.type=t,i._$eventProcessor.eventInfo={targetEl:o,packedEvent:n,model:u,view:h},i.trigger(t,n)}};e.zrEventfulCallAtLast=!0,i._zr.on(t,e,i)})),E(rm,(function(t,e){i._messageCenter.on(e,(function(t){this.trigger(e,t)}),i)})),E(["selectchanged"],(function(t){i._messageCenter.on(t,(function(e){this.trigger(t,e)}),i)})),t=this._messageCenter,e=this,n=this._api,t.on("selectchanged",(function(t){var i=n.getModel();t.isFromClick?(Ay("map","selectchanged",e,i,t),Ay("pie","selectchanged",e,i,t)):"select"===t.fromAction?(Ay("map","selected",e,i,t),Ay("pie","selected",e,i,t)):"unselect"===t.fromAction&&(Ay("map","unselected",e,i,t),Ay("pie","unselected",e,i,t))}))},e.prototype.isDisposed=function(){return this._disposed},e.prototype.clear=function(){this._disposed?nm(this.id):this.setOption({series:[]},!0)},e.prototype.dispose=function(){if(this._disposed)nm(this.id);else{this._disposed=!0,this.getDom()&&Fo(this.getDom(),fm,"");var t=this,e=t._api,n=t._model;E(t._componentsViews,(function(t){t.dispose(n,e)})),E(t._chartsViews,(function(t){t.dispose(n,e)})),t._zr.dispose(),t._dom=t._model=t._chartsMap=t._componentsMap=t._chartsViews=t._componentsViews=t._scheduler=t._api=t._zr=t._throttledZrFlush=t._theme=t._coordSysMgr=t._messageCenter=null,delete hm[t.id]}},e.prototype.resize=function(t){if(!this[Iv])if(this._disposed)nm(this.id);else{this._zr.resize(t);var e=this._model;if(this._loadingFX&&this._loadingFX.resize(),e){var n=e.resetOption("media"),i=t&&t.silent;this[Tv]&&(null==i&&(i=this[Tv].silent),n=!0,this[Tv]=null),this[Iv]=!0;try{n&&Ov(this),Ev.update.call(this,{type:"resize",animation:A({duration:0},t&&t.animation)})}catch(t){throw this[Iv]=!1,t}this[Iv]=!1,Fv.call(this,i),Gv.call(this,i)}}},e.prototype.showLoading=function(t,e){if(this._disposed)nm(this.id);else if(q(t)&&(e=t,t=""),t=t||"default",this.hideLoading(),um[t]){var n=um[t](this._api,e),i=this._zr;this._loadingFX=n,i.add(n)}},e.prototype.hideLoading=function(){this._disposed?nm(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},e.prototype.makeActionFromEvent=function(t){var e=A({},t);return e.type=rm[t.type],e},e.prototype.dispatchAction=function(t,e){if(this._disposed)nm(this.id);else if(q(e)||(e={silent:!!e}),im[t.type]&&this._model)if(this[Iv])this._pendingActions.push(t);else{var n=e.silent;Bv.call(this,t,n);var i=e.flush;i?this._zr.flush():!1!==i&&r.browser.weChat&&this._throttledZrFlush(),Fv.call(this,n),Gv.call(this,n)}},e.prototype.updateLabelLayout=function(){xv.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},e.prototype.appendData=function(t){if(this._disposed)nm(this.id);else{var e=t.seriesIndex,n=this.getModel().getSeriesByIndex(e);0,n.appendData(t),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},e.internalField=function(){function t(t){t.clearColorPalette(),t.eachSeries((function(t){t.clearColorPalette()}))}function e(t){for(var e=[],n=t.currentStates,i=0;i0?{duration:o,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered((function(t){if(t.states&&t.states.emphasis){if(yh(t))return;if(t instanceof Is&&function(t){var e=il(t);e.normalFill=t.style.fill,e.normalStroke=t.style.stroke;var n=t.states.select||{};e.selectFill=n.style&&n.style.fill||null,e.selectStroke=n.style&&n.style.stroke||null}(t),t.__dirty){var n=t.prevStates;n&&t.useStates(n)}if(r){t.stateTransition=a;var i=t.getTextContent(),o=t.getTextGuideLine();i&&(i.stateTransition=a),o&&(o.stateTransition=a)}t.__dirty&&e(t)}}))}Ov=function(t){var e=t._scheduler;e.restorePipelines(t._model),e.prepareStageTasks(),Rv(t,!0),Rv(t,!1),e.plan()},Rv=function(t,e){for(var n=t._model,i=t._scheduler,r=e?t._componentsViews:t._chartsViews,o=e?t._componentsMap:t._chartsMap,a=t._zr,s=t._api,l=0;le.get("hoverLayerThreshold")&&!r.node&&!r.worker&&e.eachSeries((function(e){if(!e.preventUsingHoverLayer){var n=t._chartsMap[e.__viewId];n.__alive&&n.eachRendered((function(t){t.states.emphasis&&(t.states.emphasis.hoverLayer=!0)}))}}))}(t,e),xv.trigger("series:afterupdate",e,n,l)},qv=function(t){t[Cv]=!0,t.getZr().wakeUp()},Kv=function(t){t[Cv]&&(t.getZr().storage.traverse((function(t){yh(t)||e(t)})),t[Cv]=!1)},Zv=function(t){return new(function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return n(i,e),i.prototype.getCoordinateSystems=function(){return t._coordSysMgr.getCoordinateSystems()},i.prototype.getComponentByElement=function(e){for(;e;){var n=e.__ecComponentInfo;if(null!=n)return t._model.getComponent(n.mainType,n.index);e=e.parent}},i.prototype.enterEmphasis=function(e,n){kl(e,n),qv(t)},i.prototype.leaveEmphasis=function(e,n){Ll(e,n),qv(t)},i.prototype.enterBlur=function(e){Pl(e),qv(t)},i.prototype.leaveBlur=function(e){Ol(e),qv(t)},i.prototype.enterSelect=function(e){Rl(e),qv(t)},i.prototype.leaveSelect=function(e){Nl(e),qv(t)},i.prototype.getModel=function(){return t.getModel()},i.prototype.getViewOfComponentModel=function(e){return t.getViewOfComponentModel(e)},i.prototype.getViewOfSeriesModel=function(e){return t.getViewOfSeriesModel(e)},i}(vd))(t)},jv=function(t){function e(t,e){for(var n=0;n=0)){Dm.push(n);var o=Jg.wrapStageHandler(n,r);o.__prio=e,o.__raw=n,t.push(o)}}function km(t,e){um[t]=e}function Lm(t,e,n){var i=bv("registerMap");i&&i(t,e,n)}var Pm=function(t){var e=(t=T(t)).type,n="";e||vo(n);var i=e.split(":");2!==i.length&&vo(n);var r=!1;"echarts"===i[0]&&(e=i[1],r=!0),t.__isBuiltIn=r,Nf.set(e,t)};Cm(wv,Zg),Cm(Sv,qg),Cm(Sv,Kg),Cm(wv,Sy),Cm(Sv,My),Cm(7e3,(function(t,e){t.eachRawSeries((function(n){if(!t.isSeriesFiltered(n)){var i=n.getData();i.hasItemVisual()&&i.each((function(t){var n=i.getItemVisual(t,"decal");n&&(i.ensureUniqueItemVisual(t,"style").decal=gv(n,e))}));var r=i.getVisual("decal");if(r)i.getVisual("style").decal=gv(r,e)}}))})),xm(Wd),_m(900,(function(t){var e=yt();t.eachSeries((function(t){var n=t.get("stack");if(n){var i=e.get(n)||e.set(n,[]),r=t.getData(),o={stackResultDimension:r.getCalculationInfo("stackResultDimension"),stackedOverDimension:r.getCalculationInfo("stackedOverDimension"),stackedDimension:r.getCalculationInfo("stackedDimension"),stackedByDimension:r.getCalculationInfo("stackedByDimension"),isStackedByIndex:r.getCalculationInfo("isStackedByIndex"),data:r,seriesModel:t};if(!o.stackedDimension||!o.isStackedByIndex&&!o.stackedByDimension)return;i.length&&r.setCalculationInfo("stackedOnSeries",i[i.length-1].seriesModel),i.push(o)}})),e.each(Hd)})),km("default",(function(t,e){k(e=e||{},{text:"loading",textColor:"#000",fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255, 255, 255, 0.8)",showSpinner:!0,color:"#5470c6",spinnerRadius:10,lineWidth:5,zlevel:0});var n=new zr,i=new zs({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4});n.add(i);var r,o=new Fs({style:{text:e.text,fill:e.textColor,fontSize:e.fontSize,fontWeight:e.fontWeight,fontStyle:e.fontStyle,fontFamily:e.fontFamily},zlevel:e.zlevel,z:10001}),a=new zs({style:{fill:"none"},textContent:o,textConfig:{position:"right",distance:10},zlevel:e.zlevel,z:10001});return n.add(a),e.showSpinner&&((r=new Qu({shape:{startAngle:-$g/2,endAngle:-$g/2+.1,r:e.spinnerRadius},style:{stroke:e.color,lineCap:"round",lineWidth:e.lineWidth},zlevel:e.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*$g/2}).start("circularInOut"),r.animateShape(!0).when(1e3,{startAngle:3*$g/2}).delay(300).start("circularInOut"),n.add(r)),n.resize=function(){var n=o.getBoundingRect().width,s=e.showSpinner?e.spinnerRadius:0,l=(t.getWidth()-2*s-(e.showSpinner&&n?10:0)-n)/2-(e.showSpinner&&n?0:5+n/2)+(e.showSpinner?0:n/2)+(n?0:s),u=t.getHeight()/2;e.showSpinner&&r.setShape({cx:l,cy:u}),a.setShape({x:l-s,y:u-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:t.getWidth(),height:t.getHeight()})},n.resize(),n})),Mm({type:ll,event:ll,update:ll},bt),Mm({type:ul,event:ul,update:ul},bt),Mm({type:hl,event:hl,update:hl},bt),Mm({type:cl,event:cl,update:cl},bt),Mm({type:pl,event:pl,update:pl},bt),mm("light",fy),mm("dark",xy);var Om=[],Rm={registerPreprocessor:xm,registerProcessor:_m,registerPostInit:bm,registerPostUpdate:wm,registerUpdateLifecycle:Sm,registerAction:Mm,registerCoordinateSystem:Im,registerLayout:Tm,registerVisual:Cm,registerTransform:Pm,registerLoading:km,registerMap:Lm,registerImpl:function(t,e){_v[t]=e},PRIORITY:Mv,ComponentModel:Rp,ComponentView:Tg,SeriesModel:mg,ChartView:kg,registerComponentModel:function(t){Rp.registerClass(t)},registerComponentView:function(t){Tg.registerClass(t)},registerSeriesModel:function(t){mg.registerClass(t)},registerChartView:function(t){kg.registerClass(t)},registerSubTypeDefaulter:function(t,e){Rp.registerSubTypeDefaulter(t,e)},registerPainter:function(t,e){Wr(t,e)}};function Nm(t){Y(t)?E(t,(function(t){Nm(t)})):P(Om,t)>=0||(Om.push(t),X(t)&&(t={install:t}),t.install(Rm))}function Em(t){return null==t?0:t.length||1}function zm(t){return t}var Vm=function(){function t(t,e,n,i,r,o){this._old=t,this._new=e,this._oldKeyGetter=n||zm,this._newKeyGetter=i||zm,this.context=r,this._diffModeMultiple="multiple"===o}return t.prototype.add=function(t){return this._add=t,this},t.prototype.update=function(t){return this._update=t,this},t.prototype.updateManyToOne=function(t){return this._updateManyToOne=t,this},t.prototype.updateOneToMany=function(t){return this._updateOneToMany=t,this},t.prototype.updateManyToMany=function(t){return this._updateManyToMany=t,this},t.prototype.remove=function(t){return this._remove=t,this},t.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},t.prototype._executeOneToOne=function(){var t=this._old,e=this._new,n={},i=new Array(t.length),r=new Array(e.length);this._initIndexMap(t,null,i,"_oldKeyGetter"),this._initIndexMap(e,n,r,"_newKeyGetter");for(var o=0;o1){var u=s.shift();1===s.length&&(n[a]=s[0]),this._update&&this._update(u,o)}else 1===l?(n[a]=null,this._update&&this._update(s,o)):this._remove&&this._remove(o)}this._performRestAdd(r,n)},t.prototype._executeMultiple=function(){var t=this._old,e=this._new,n={},i={},r=[],o=[];this._initIndexMap(t,n,r,"_oldKeyGetter"),this._initIndexMap(e,i,o,"_newKeyGetter");for(var a=0;a1&&1===c)this._updateManyToOne&&this._updateManyToOne(u,l),i[s]=null;else if(1===h&&c>1)this._updateOneToMany&&this._updateOneToMany(u,l),i[s]=null;else if(1===h&&1===c)this._update&&this._update(u,l),i[s]=null;else if(h>1&&c>1)this._updateManyToMany&&this._updateManyToMany(u,l),i[s]=null;else if(h>1)for(var p=0;p1)for(var a=0;a30}var Km,$m,Jm,Qm,tx,ex,nx,ix=q,rx=z,ox="undefined"==typeof Int32Array?Array:Int32Array,ax=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],sx=["_approximateExtent"],lx=function(){function t(t,e){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","lttbDownSample"];var i=!1;Um(t)?(n=t.dimensions,this._dimOmitted=t.isDimensionOmitted(),this._schema=t):(i=!0,n=t),n=n||["x","y"];for(var r={},o=[],a={},s=!1,l={},u=0;u=e)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,r=this._idList;if(n.getSource().sourceFormat===Bp&&!n.pure)for(var o=[],a=t;a0},t.prototype.ensureUniqueItemVisual=function(t,e){var n=this._itemVisuals,i=n[t];i||(i=n[t]={});var r=i[e];return null==r&&(Y(r=this.getVisual(e))?r=r.slice():ix(r)&&(r=A({},r)),i[e]=r),r},t.prototype.setItemVisual=function(t,e,n){var i=this._itemVisuals[t]||{};this._itemVisuals[t]=i,ix(e)?A(i,e):i[e]=n},t.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},t.prototype.setLayout=function(t,e){ix(t)?A(this._layout,t):this._layout[t]=e},t.prototype.getLayout=function(t){return this._layout[t]},t.prototype.getItemLayout=function(t){return this._itemLayouts[t]},t.prototype.setItemLayout=function(t,e,n){this._itemLayouts[t]=n?A(this._itemLayouts[t]||{},e):e},t.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},t.prototype.setItemGraphicEl=function(t,e){var n=this.hostModel&&this.hostModel.seriesIndex;tl(n,this.dataType,t,e),this._graphicEls[t]=e},t.prototype.getItemGraphicEl=function(t){return this._graphicEls[t]},t.prototype.eachItemGraphicEl=function(t,e){E(this._graphicEls,(function(n,i){n&&t&&t.call(e,n,i)}))},t.prototype.cloneShallow=function(e){return e||(e=new t(this._schema?this._schema:rx(this.dimensions,this._getDimInfo,this),this.hostModel)),tx(e,this),e._store=this._store,e},t.prototype.wrapMethod=function(t,e){var n=this[t];X(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var t=n.apply(this,arguments);return e.apply(this,[t].concat(at(arguments)))})},t.internalField=(Km=function(t){var e=t._invertedIndicesMap;E(e,(function(n,i){var r=t._dimInfos[i],o=r.ordinalMeta,a=t._store;if(o){n=e[i]=new ox(o.categories.length);for(var s=0;s1&&(s+="__ec__"+u),i[e]=s}})),t}();function ux(t,e){Kd(t)||(t=Jd(t));var n=(e=e||{}).coordDimensions||[],i=e.dimensionsDefine||t.dimensionsDefine||[],r=yt(),o=[],a=function(t,e,n,i){var r=Math.max(t.dimensionsDetectedCount||1,e.length,n.length,i||0);return E(e,(function(t){var e;q(t)&&(e=t.dimsDef)&&(r=Math.max(r,e.length))})),r}(t,n,i,e.dimensionsCount),s=e.canOmitUnusedDimensions&&qm(a),l=i===t.dimensionsDefine,u=l?jm(t):Zm(i),h=e.encodeDefine;!h&&e.encodeDefaulter&&(h=e.encodeDefaulter(t,a));for(var c=yt(h),p=new Wf(a),d=0;d0&&(i.name=r+(o-1)),o++,e.set(r,o)}}(o),new Xm({source:t,dimensions:o,fullDimensionCount:a,dimensionOmitted:s})}function hx(t,e,n){if(n||e.hasKey(t)){for(var i=0;e.hasKey(t+i);)i++;t+=i}return e.set(t,!0),t}var cx=function(t){this.coordSysDims=[],this.axisMap=yt(),this.categoryAxisMap=yt(),this.coordSysName=t};var px={cartesian2d:function(t,e,n,i){var r=t.getReferringComponents("xAxis",zo).models[0],o=t.getReferringComponents("yAxis",zo).models[0];e.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),dx(r)&&(i.set("x",r),e.firstCategoryDimIndex=0),dx(o)&&(i.set("y",o),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},singleAxis:function(t,e,n,i){var r=t.getReferringComponents("singleAxis",zo).models[0];e.coordSysDims=["single"],n.set("single",r),dx(r)&&(i.set("single",r),e.firstCategoryDimIndex=0)},polar:function(t,e,n,i){var r=t.getReferringComponents("polar",zo).models[0],o=r.findAxisModel("radiusAxis"),a=r.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",a),dx(o)&&(i.set("radius",o),e.firstCategoryDimIndex=0),dx(a)&&(i.set("angle",a),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},geo:function(t,e,n,i){e.coordSysDims=["lng","lat"]},parallel:function(t,e,n,i){var r=t.ecModel,o=r.getComponent("parallel",t.get("parallelIndex")),a=e.coordSysDims=o.dimensions.slice();E(o.parallelAxisIndex,(function(t,o){var s=r.getComponent("parallelAxis",t),l=a[o];n.set(l,s),dx(s)&&(i.set(l,s),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=o))}))}};function dx(t){return"category"===t.get("type")}function fx(t,e,n){var i,r,o,a=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(t){return!Um(t.schema)}(e)?(r=e.schema,i=r.dimensions,o=e.store):i=e;var l,u,h,c,p=!(!t||!t.get("stack"));if(E(i,(function(t,e){U(t)&&(i[e]=t={name:t}),p&&!t.isExtraCoord&&(a||l||!t.ordinalMeta||(l=t),u||"ordinal"===t.type||"time"===t.type||s&&s!==t.coordDim||(u=t))})),!u||a||l||(a=!0),u){h="__\0ecstackresult_"+t.id,c="__\0ecstackedover_"+t.id,l&&(l.createInvertedIndices=!0);var d=u.coordDim,f=u.type,g=0;E(i,(function(t){t.coordDim===d&&g++}));var y={name:h,coordDim:d,coordDimIndex:g,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},v={name:c,coordDim:c,coordDimIndex:g+1,type:f,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};r?(o&&(y.storeDimIndex=o.ensureCalculationDimension(c,f),v.storeDimIndex=o.ensureCalculationDimension(h,f)),r.appendCalculationDimension(y),r.appendCalculationDimension(v)):(i.push(y),i.push(v))}return{stackedDimension:u&&u.name,stackedByDimension:l&&l.name,isStackedByIndex:a,stackedOverDimension:c,stackResultDimension:h}}function gx(t,e){return!!e&&e===t.getCalculationInfo("stackedDimension")}function yx(t,e){return gx(t,e)?t.getCalculationInfo("stackResultDimension"):e}function vx(t,e,n){n=n||{};var i,r=e.getSourceManager(),o=!1;t?(o=!0,i=Jd(t)):o=(i=r.getSource()).sourceFormat===Bp;var a=function(t){var e=t.get("coordinateSystem"),n=new cx(e),i=px[e];if(i)return i(t,n,n.axisMap,n.categoryAxisMap),n}(e),s=function(t,e){var n,i=t.get("coordinateSystem"),r=xd.get(i);return e&&e.coordSysDims&&(n=z(e.coordSysDims,(function(t){var n={name:t},i=e.axisMap.get(t);if(i){var r=i.get("type");n.type=Gm(r)}return n}))),n||(n=r&&(r.getDimensionsInfo?r.getDimensionsInfo():r.dimensions.slice())||["x","y"]),n}(e,a),l=n.useEncodeDefaulter,u=X(l)?l:l?H($p,s,e):null,h=ux(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:e.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!o}),c=function(t,e,n){var i,r;return n&&E(t,(function(t,o){var a=t.coordDim,s=n.categoryAxisMap.get(a);s&&(null==i&&(i=o),t.ordinalMeta=s.getOrdinalMeta(),e&&(t.createInvertedIndices=!0)),null!=t.otherDims.itemName&&(r=!0)})),r||null==i||(t[i].otherDims.itemName=0),i}(h.dimensions,n.createInvertedIndices,a),p=o?null:r.getSharedDataStore(h),d=fx(e,{schema:h,store:p}),f=new lx(h,e);f.setCalculationInfo(d);var g=null!=c&&function(t){if(t.sourceFormat===Bp){var e=function(t){var e=0;for(;ee[1]&&(e[1]=t[1])},t.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=t),isNaN(e)||(n[1]=e)},t.prototype.isInExtentRange=function(t){return this._extent[0]<=t&&this._extent[1]>=t},t.prototype.isBlank=function(){return this._isBlank},t.prototype.setBlank=function(t){this._isBlank=t},t}();$o(mx);var xx=0,_x=function(){function t(t){this.categories=t.categories||[],this._needCollect=t.needCollect,this._deduplication=t.deduplication,this.uid=++xx}return t.createByAxisModel=function(e){var n=e.option,i=n.data,r=i&&z(i,bx);return new t({categories:r,needCollect:!r,deduplication:!1!==n.dedplication})},t.prototype.getOrdinal=function(t){return this._getOrCreateMap().get(t)},t.prototype.parseAndCollect=function(t){var e,n=this._needCollect;if(!U(t)&&!n)return t;if(n&&!this._deduplication)return e=this.categories.length,this.categories[e]=t,e;var i=this._getOrCreateMap();return null==(e=i.get(t))&&(n?(e=this.categories.length,this.categories[e]=t,i.set(t,e)):e=NaN),e},t.prototype._getOrCreateMap=function(){return this._map||(this._map=yt(this.categories))},t}();function bx(t){return q(t)&&null!=t.value?t.value:t+""}function Sx(t){return"interval"===t.type||"log"===t.type}function Mx(t,e,n,i){var r={},o=t[1]-t[0],a=r.interval=so(o/e,!0);null!=n&&ai&&(a=r.interval=i);var s=r.intervalPrecision=Tx(a);return function(t,e){!isFinite(t[0])&&(t[0]=e[0]),!isFinite(t[1])&&(t[1]=e[1]),Cx(t,0,e),Cx(t,1,e),t[0]>t[1]&&(t[0]=t[1])}(r.niceTickExtent=[Zr(Math.ceil(t[0]/a)*a,s),Zr(Math.floor(t[1]/a)*a,s)],t),r}function Ix(t){var e=Math.pow(10,ao(t)),n=t/e;return n?2===n?n=3:3===n?n=5:n*=2:n=1,Zr(n*e)}function Tx(t){return qr(t)+2}function Cx(t,e,n){t[e]=Math.max(Math.min(t[e],n[1]),n[0])}function Dx(t,e){return t>=e[0]&&t<=e[1]}function Ax(t,e){return e[1]===e[0]?.5:(t-e[0])/(e[1]-e[0])}function kx(t,e){return t*(e[1]-e[0])+e[0]}var Lx=function(t){function e(e){var n=t.call(this,e)||this;n.type="ordinal";var i=n.getSetting("ordinalMeta");return i||(i=new _x({})),Y(i)&&(i=new _x({categories:z(i,(function(t){return q(t)?t.value:t}))})),n._ordinalMeta=i,n._extent=n.getSetting("extent")||[0,i.categories.length-1],n}return n(e,t),e.prototype.parse=function(t){return null==t?NaN:U(t)?this._ordinalMeta.getOrdinal(t):Math.round(t)},e.prototype.contain=function(t){return Dx(t=this.parse(t),this._extent)&&null!=this._ordinalMeta.categories[t]},e.prototype.normalize=function(t){return Ax(t=this._getTickNumber(this.parse(t)),this._extent)},e.prototype.scale=function(t){return t=Math.round(kx(t,this._extent)),this.getRawOrdinalNumber(t)},e.prototype.getTicks=function(){for(var t=[],e=this._extent,n=e[0];n<=e[1];)t.push({value:n}),n++;return t},e.prototype.getMinorTicks=function(t){},e.prototype.setSortInfo=function(t){if(null!=t){for(var e=t.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],r=0,o=this._ordinalMeta.categories.length,a=Math.min(o,e.length);r=0&&t=0&&t=t},e.prototype.getOrdinalMeta=function(){return this._ordinalMeta},e.prototype.calcNiceTicks=function(){},e.prototype.calcNiceExtent=function(){},e.type="ordinal",e}(mx);mx.registerClass(Lx);var Px=Zr,Ox=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="interval",e._interval=0,e._intervalPrecision=2,e}return n(e,t),e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return Dx(t,this._extent)},e.prototype.normalize=function(t){return Ax(t,this._extent)},e.prototype.scale=function(t){return kx(t,this._extent)},e.prototype.setExtent=function(t,e){var n=this._extent;isNaN(t)||(n[0]=parseFloat(t)),isNaN(e)||(n[1]=parseFloat(e))},e.prototype.unionExtent=function(t){var e=this._extent;t[0]e[1]&&(e[1]=t[1]),this.setExtent(e[0],e[1])},e.prototype.getInterval=function(){return this._interval},e.prototype.setInterval=function(t){this._interval=t,this._niceExtent=this._extent.slice(),this._intervalPrecision=Tx(t)},e.prototype.getTicks=function(t){var e=this._interval,n=this._extent,i=this._niceExtent,r=this._intervalPrecision,o=[];if(!e)return o;n[0]1e4)return[];var s=o.length?o[o.length-1].value:i[1];return n[1]>s&&(t?o.push({value:Px(s+e,r)}):o.push({value:n[1]})),o},e.prototype.getMinorTicks=function(t){for(var e=this.getTicks(!0),n=[],i=this.getExtent(),r=1;ri[0]&&h0&&(o=null===o?s:Math.min(o,s))}n[i]=o}}return n}(t),n=[];return E(t,(function(t){var i,r=t.coordinateSystem.getBaseAxis(),o=r.getExtent();if("category"===r.type)i=r.getBandWidth();else if("value"===r.type||"time"===r.type){var a=r.dim+"_"+r.index,s=e[a],l=Math.abs(o[1]-o[0]),u=r.scale.getExtent(),h=Math.abs(u[1]-u[0]);i=s?l/h*s:l}else{var c=t.getData();i=Math.abs(o[1]-o[0])/c.count()}var p=Ur(t.get("barWidth"),i),d=Ur(t.get("barMaxWidth"),i),f=Ur(t.get("barMinWidth")||(Ux(t)?.5:1),i),g=t.get("barGap"),y=t.get("barCategoryGap");n.push({bandWidth:i,barWidth:p,barMaxWidth:d,barMinWidth:f,barGap:g,barCategoryGap:y,axisKey:Bx(r),stackId:Vx(t)})})),Wx(n)}function Wx(t){var e={};E(t,(function(t,n){var i=t.axisKey,r=t.bandWidth,o=e[i]||{bandWidth:r,remainedWidth:r,autoWidthCount:0,categoryGap:null,gap:"20%",stacks:{}},a=o.stacks;e[i]=o;var s=t.stackId;a[s]||o.autoWidthCount++,a[s]=a[s]||{width:0,maxWidth:0};var l=t.barWidth;l&&!a[s].width&&(a[s].width=l,l=Math.min(o.remainedWidth,l),o.remainedWidth-=l);var u=t.barMaxWidth;u&&(a[s].maxWidth=u);var h=t.barMinWidth;h&&(a[s].minWidth=h);var c=t.barGap;null!=c&&(o.gap=c);var p=t.barCategoryGap;null!=p&&(o.categoryGap=p)}));var n={};return E(e,(function(t,e){n[e]={};var i=t.stacks,r=t.bandWidth,o=t.categoryGap;if(null==o){var a=G(i).length;o=Math.max(35-4*a,15)+"%"}var s=Ur(o,r),l=Ur(t.gap,1),u=t.remainedWidth,h=t.autoWidthCount,c=(u-s)/(h+(h-1)*l);c=Math.max(c,0),E(i,(function(t){var e=t.maxWidth,n=t.minWidth;if(t.width){i=t.width;e&&(i=Math.min(i,e)),n&&(i=Math.max(i,n)),t.width=i,u-=i+l*i,h--}else{var i=c;e&&ei&&(i=n),i!==c&&(t.width=i,u-=i+l*i,h--)}})),c=(u-s)/(h+(h-1)*l),c=Math.max(c,0);var p,d=0;E(i,(function(t,e){t.width||(t.width=c),p=t,d+=t.width*(1+l)})),p&&(d-=p.width*l);var f=-d/2;E(i,(function(t,i){n[e][i]=n[e][i]||{bandWidth:r,offset:f,width:t.width},f+=t.width*(1+l)}))})),n}function Hx(t,e){var n=Fx(t,e),i=Gx(n);E(n,(function(t){var e=t.getData(),n=t.coordinateSystem.getBaseAxis(),r=Vx(t),o=i[Bx(n)][r],a=o.offset,s=o.width;e.setLayout({bandWidth:o.bandWidth,offset:a,size:s})}))}function Yx(t){return{seriesType:t,plan:Cg(),reset:function(t){if(Xx(t)){var e=t.getData(),n=t.coordinateSystem,i=n.getBaseAxis(),r=n.getOtherAxis(i),o=e.getDimensionIndex(e.mapDimension(r.dim)),a=e.getDimensionIndex(e.mapDimension(i.dim)),s=t.get("showBackground",!0),l=e.mapDimension(r.dim),u=e.getCalculationInfo("stackResultDimension"),h=gx(e,l)&&!!e.getCalculationInfo("stackedOnSeries"),c=r.isHorizontal(),p=function(t,e){return e.toGlobalCoord(e.dataToCoord("log"===e.type?1:0))}(0,r),d=Ux(t),f=t.get("barMinHeight")||0,g=u&&e.getDimensionIndex(u),y=e.getLayout("size"),v=e.getLayout("offset");return{progress:function(t,e){for(var i,r=t.count,l=d&&Ex(3*r),u=d&&s&&Ex(3*r),m=d&&Ex(r),x=n.master.getRect(),_=c?x.width:x.height,b=e.getStore(),w=0;null!=(i=t.next());){var S=b.get(h?g:o,i),M=b.get(a,i),I=p,T=void 0;h&&(T=+S-b.get(o,i));var C=void 0,D=void 0,A=void 0,k=void 0;if(c){var L=n.dataToPoint([S,M]);if(h)I=n.dataToPoint([T,M])[0];C=I,D=L[1]+v,A=L[0]-I,k=y,Math.abs(A)0)for(var s=0;s=0;--s)if(l[u]){o=l[u];break}o=o||a.none}if(Y(o)){var h=null==t.level?0:t.level>=0?t.level:o.length+t.level;o=o[h=Math.min(h,o.length-1)]}}return qc(new Date(t.value),o,r,i)}(t,e,n,this.getSetting("locale"),i)},e.prototype.getTicks=function(){var t=this._interval,e=this._extent,n=[];if(!t)return n;n.push({value:e[0],level:0});var i=this.getSetting("useUTC"),r=function(t,e,n,i){var r=1e4,o=Xc,a=0;function s(t,e,n,r,o,a,s){for(var l=new Date(e),u=e,h=l[r]();u1&&0===u&&o.unshift({value:o[0].value-p})}}for(u=0;u=i[0]&&v<=i[1]&&c++)}var m=(i[1]-i[0])/e;if(c>1.5*m&&p>m/1.5)break;if(u.push(g),c>m||t===o[d])break}h=[]}}0;var x=B(z(u,(function(t){return B(t,(function(t){return t.value>=i[0]&&t.value<=i[1]&&!t.notAdd}))})),(function(t){return t.length>0})),_=[],b=x.length-1;for(d=0;dn&&(this._approxInterval=n);var o=jx.length,a=Math.min(function(t,e,n,i){for(;n>>1;t[r][1]16?16:t>7.5?7:t>3.5?4:t>1.5?2:1}function Kx(t){return(t/=2592e6)>6?6:t>3?3:t>2?2:1}function $x(t){return(t/=Vc)>12?12:t>6?6:t>3.5?4:t>2?2:1}function Jx(t,e){return(t/=e?zc:Ec)>30?30:t>20?20:t>15?15:t>10?10:t>5?5:t>2?2:1}function Qx(t){return so(t,!0)}function t_(t,e,n){var i=new Date(t);switch(Zc(e)){case"year":case"month":i[ap(n)](0);case"day":i[sp(n)](1);case"hour":i[lp(n)](0);case"minute":i[up(n)](0);case"second":i[hp(n)](0),i[cp(n)](0)}return i.getTime()}mx.registerClass(Zx);var e_=mx.prototype,n_=Ox.prototype,i_=Zr,r_=Math.floor,o_=Math.ceil,a_=Math.pow,s_=Math.log,l_=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="log",e.base=10,e._originalScale=new Ox,e._interval=0,e}return n(e,t),e.prototype.getTicks=function(t){var e=this._originalScale,n=this._extent,i=e.getExtent();return z(n_.getTicks.call(this,t),(function(t){var e=t.value,r=Zr(a_(this.base,e));return r=e===n[0]&&this._fixMin?h_(r,i[0]):r,{value:r=e===n[1]&&this._fixMax?h_(r,i[1]):r}}),this)},e.prototype.setExtent=function(t,e){var n=s_(this.base);t=s_(Math.max(0,t))/n,e=s_(Math.max(0,e))/n,n_.setExtent.call(this,t,e)},e.prototype.getExtent=function(){var t=this.base,e=e_.getExtent.call(this);e[0]=a_(t,e[0]),e[1]=a_(t,e[1]);var n=this._originalScale.getExtent();return this._fixMin&&(e[0]=h_(e[0],n[0])),this._fixMax&&(e[1]=h_(e[1],n[1])),e},e.prototype.unionExtent=function(t){this._originalScale.unionExtent(t);var e=this.base;t[0]=s_(t[0])/s_(e),t[1]=s_(t[1])/s_(e),e_.unionExtent.call(this,t)},e.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},e.prototype.calcNiceTicks=function(t){t=t||10;var e=this._extent,n=e[1]-e[0];if(!(n===1/0||n<=0)){var i=oo(n);for(t/n*i<=.5&&(i*=10);!isNaN(i)&&Math.abs(i)<1&&Math.abs(i)>0;)i*=10;var r=[Zr(o_(e[0]/i)*i),Zr(r_(e[1]/i)*i)];this._interval=i,this._niceExtent=r}},e.prototype.calcNiceExtent=function(t){n_.calcNiceExtent.call(this,t),this._fixMin=t.fixMin,this._fixMax=t.fixMax},e.prototype.parse=function(t){return t},e.prototype.contain=function(t){return Dx(t=s_(t)/s_(this.base),this._extent)},e.prototype.normalize=function(t){return Ax(t=s_(t)/s_(this.base),this._extent)},e.prototype.scale=function(t){return t=kx(t,this._extent),a_(this.base,t)},e.type="log",e}(mx),u_=l_.prototype;function h_(t,e){return i_(t,qr(e))}u_.getMinorTicks=n_.getMinorTicks,u_.getLabel=n_.getLabel,mx.registerClass(l_);var c_=function(){function t(t,e,n){this._prepareParams(t,e,n)}return t.prototype._prepareParams=function(t,e,n){n[1]0&&s>0&&!l&&(a=0),a<0&&s<0&&!u&&(s=0));var c=this._determinedMin,p=this._determinedMax;return null!=c&&(a=c,l=!0),null!=p&&(s=p,u=!0),{min:a,max:s,minFixed:l,maxFixed:u,isBlank:h}},t.prototype.modifyDataMinMax=function(t,e){this[d_[t]]=e},t.prototype.setDeterminedMinMax=function(t,e){var n=p_[t];this[n]=e},t.prototype.freeze=function(){this.frozen=!0},t}(),p_={min:"_determinedMin",max:"_determinedMax"},d_={min:"_dataMin",max:"_dataMax"};function f_(t,e,n){var i=t.rawExtentInfo;return i||(i=new c_(t,e,n),t.rawExtentInfo=i,i)}function g_(t,e){return null==e?null:nt(e)?NaN:t.parse(e)}function y_(t,e){var n=t.type,i=f_(t,e,t.getExtent()).calculate();t.setBlank(i.isBlank);var r=i.min,o=i.max,a=e.ecModel;if(a&&"time"===n){var s=Fx("bar",a),l=!1;if(E(s,(function(t){l=l||t.getBaseAxis()===e.axis})),l){var u=Gx(s),h=function(t,e,n,i){var r=n.axis.getExtent(),o=r[1]-r[0],a=function(t,e,n){if(t&&e){var i=t[Bx(e)];return null!=i&&null!=n?i[Vx(n)]:i}}(i,n.axis);if(void 0===a)return{min:t,max:e};var s=1/0;E(a,(function(t){s=Math.min(t.offset,s)}));var l=-1/0;E(a,(function(t){l=Math.max(t.offset+t.width,l)})),s=Math.abs(s),l=Math.abs(l);var u=s+l,h=e-t,c=h/(1-(s+l)/o)-h;return e+=c*(l/u),t-=c*(s/u),{min:t,max:e}}(r,o,e,u);r=h.min,o=h.max}}return{extent:[r,o],fixMin:i.minFixed,fixMax:i.maxFixed}}function v_(t,e){var n=e,i=y_(t,n),r=i.extent,o=n.get("splitNumber");t instanceof l_&&(t.base=n.get("logBase"));var a=t.type,s=n.get("interval"),l="interval"===a||"time"===a;t.setExtent(r[0],r[1]),t.calcNiceExtent({splitNumber:o,fixMin:i.fixMin,fixMax:i.fixMax,minInterval:l?n.get("minInterval"):null,maxInterval:l?n.get("maxInterval"):null}),null!=s&&t.setInterval&&t.setInterval(s)}function m_(t,e){if(e=e||t.get("type"))switch(e){case"category":return new Lx({ordinalMeta:t.getOrdinalMeta?t.getOrdinalMeta():t.getCategories(),extent:[1/0,-1/0]});case"time":return new Zx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new(mx.getClass(e)||Ox)}}function x_(t){var e,n,i=t.getLabelModel().get("formatter"),r="category"===t.type?t.scale.getExtent()[0]:null;return"time"===t.scale.type?(n=i,function(e,i){return t.scale.getFormattedLabel(e,i,n)}):U(i)?function(e){return function(n){var i=t.scale.getLabel(n);return e.replace("{value}",null!=i?i:"")}}(i):X(i)?(e=i,function(n,i){return null!=r&&(i=n.value-r),e(__(t,n),i,null!=n.level?{level:n.level}:null)}):function(e){return t.scale.getLabel(e)}}function __(t,e){return"category"===t.type?t.scale.getLabel(e):e.value}function b_(t,e){var n=e*Math.PI/180,i=t.width,r=t.height,o=i*Math.abs(Math.cos(n))+Math.abs(r*Math.sin(n)),a=i*Math.abs(Math.sin(n))+Math.abs(r*Math.cos(n));return new ze(t.x,t.y,o,a)}function w_(t){var e=t.get("interval");return null==e?"auto":e}function S_(t){return"category"===t.type&&0===w_(t.getLabelModel())}function M_(t,e){var n={};return E(t.mapDimensionsAll(e),(function(e){n[yx(t,e)]=!0})),G(n)}var I_=function(){function t(){}return t.prototype.getNeedCrossZero=function(){return!this.option.scale},t.prototype.getCoordSysModel=function(){},t}();var T_={isDimensionStacked:gx,enableDataStack:fx,getStackedDimension:yx};var C_=Object.freeze({__proto__:null,createList:function(t){return vx(null,t)},getLayoutRect:Cp,dataStack:T_,createScale:function(t,e){var n=e;e instanceof Mc||(n=new Mc(e));var i=m_(n);return i.setExtent(t[0],t[1]),v_(i,n),i},mixinAxisModelCommonMethods:function(t){R(t,I_)},getECData:Qs,createTextStyle:function(t,e){return nc(t,null,null,"normal"!==(e=e||{}).state)},createDimensions:function(t,e){return ux(t,e).dimensions},createSymbol:Wy,enableHoverEmphasis:Hl});function D_(t,e){return Math.abs(t-e)<1e-8}function A_(t,e,n){var i=0,r=t[0];if(!r)return!1;for(var o=1;on&&(t=r,n=a)}if(t)return function(t){for(var e=0,n=0,i=0,r=t.length,o=t[r-1][0],a=t[r-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),r=s+=r,o=l+=o,i.push([s/n,l/n])}return i}function F_(t,e){return z(B((t=function(t){if(!t.UTF8Encoding)return t;var e=t,n=e.UTF8Scale;return null==n&&(n=1024),E(e.features,(function(t){var e=t.geometry,i=e.encodeOffsets,r=e.coordinates;if(i)switch(e.type){case"LineString":e.coordinates=B_(r,i,n);break;case"Polygon":case"MultiLineString":V_(r,i,n);break;case"MultiPolygon":E(r,(function(t,e){return V_(t,i[e],n)}))}})),e.UTF8Encoding=!1,e}(t)).features,(function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0})),(function(t){var n=t.properties,i=t.geometry,r=[];switch(i.type){case"Polygon":var o=i.coordinates;r.push(new R_(o[0],o.slice(1)));break;case"MultiPolygon":E(i.coordinates,(function(t){t[0]&&r.push(new R_(t[0],t.slice(1)))}));break;case"LineString":r.push(new N_([i.coordinates]));break;case"MultiLineString":r.push(new N_(i.coordinates))}var a=new E_(n[e||"name"],r,n.cp);return a.properties=n,a}))}var G_=Object.freeze({__proto__:null,linearMap:Xr,round:Zr,asc:jr,getPrecision:qr,getPrecisionSafe:Kr,getPixelPrecision:$r,getPercentWithPrecision:function(t,e,n){return t[e]&&Jr(t,n)[e]||0},MAX_SAFE_INTEGER:to,remRadian:eo,isRadianAroundZero:no,parseDate:ro,quantity:oo,quantityExponent:ao,nice:so,quantile:lo,reformIntervals:uo,isNumeric:co,numericToNumber:ho}),W_=Object.freeze({__proto__:null,parse:ro,format:qc}),H_=Object.freeze({__proto__:null,extendShape:Mh,extendPath:Th,makePath:Ah,makeImage:kh,mergePath:Ph,resizePath:Oh,createIcon:Hh,updateProps:fh,initProps:gh,getTransform:Eh,clipPointsByRect:Gh,clipRectByRect:Wh,registerShape:Ch,getShapeClass:Dh,Group:zr,Image:ks,Text:Fs,Circle:_u,Ellipse:wu,Sector:zu,Ring:Bu,Polygon:Wu,Polyline:Yu,Rect:zs,Line:Zu,BezierCurve:$u,Arc:Qu,IncrementalDisplayable:hh,CompoundPath:th,LinearGradient:nh,RadialGradient:ih,BoundingRect:ze}),Y_=Object.freeze({__proto__:null,addCommas:pp,toCamelCase:dp,normalizeCssArray:fp,encodeHTML:re,formatTpl:mp,getTooltipMarker:xp,formatTime:function(t,e,n){"week"!==t&&"month"!==t&&"quarter"!==t&&"half-year"!==t&&"year"!==t||(t="MM-dd\nyyyy");var i=ro(e),r=n?"getUTC":"get",o=i[r+"FullYear"](),a=i[r+"Month"]()+1,s=i[r+"Date"](),l=i[r+"Hours"](),u=i[r+"Minutes"](),h=i[r+"Seconds"](),c=i[r+"Milliseconds"]();return t=t.replace("MM",Uc(a,2)).replace("M",a).replace("yyyy",o).replace("yy",Uc(o%100+"",2)).replace("dd",Uc(s,2)).replace("d",s).replace("hh",Uc(l,2)).replace("h",l).replace("mm",Uc(u,2)).replace("m",u).replace("ss",Uc(h,2)).replace("s",h).replace("SSS",Uc(c,3))},capitalFirst:function(t){return t?t.charAt(0).toUpperCase()+t.substr(1):t},truncateText:sa,getTextRect:function(t,e,n,i,r,o,a,s){return new Fs({style:{text:t,font:e,align:n,verticalAlign:i,padding:r,rich:o,overflow:a?"truncate":null,lineHeight:s}}).getBoundingRect()}}),X_=Object.freeze({__proto__:null,map:z,each:E,indexOf:P,inherits:O,reduce:V,filter:B,bind:W,curry:H,isArray:Y,isString:U,isObject:q,isFunction:X,extend:A,defaults:k,clone:T,merge:C}),U_=Oo();function Z_(t){return"category"===t.type?function(t){var e=t.getLabelModel(),n=q_(t,e);return!e.get("show")||t.scale.isBlank()?{labels:[],labelCategoryInterval:n.labelCategoryInterval}:n}(t):function(t){var e=t.scale.getTicks(),n=x_(t);return{labels:z(e,(function(e,i){return{level:e.level,formattedLabel:n(e,i),rawLabel:t.scale.getLabel(e),tickValue:e.value}}))}}(t)}function j_(t,e){return"category"===t.type?function(t,e){var n,i,r=K_(t,"ticks"),o=w_(e),a=$_(r,o);if(a)return a;e.get("show")&&!t.scale.isBlank()||(n=[]);if(X(o))n=tb(t,o,!0);else if("auto"===o){var s=q_(t,t.getLabelModel());i=s.labelCategoryInterval,n=z(s.labels,(function(t){return t.tickValue}))}else n=Q_(t,i=o,!0);return J_(r,o,{ticks:n,tickCategoryInterval:i})}(t,e):{ticks:z(t.scale.getTicks(),(function(t){return t.value}))}}function q_(t,e){var n,i,r=K_(t,"labels"),o=w_(e),a=$_(r,o);return a||(X(o)?n=tb(t,o):(i="auto"===o?function(t){var e=U_(t).autoInterval;return null!=e?e:U_(t).autoInterval=t.calculateCategoryInterval()}(t):o,n=Q_(t,i)),J_(r,o,{labels:n,labelCategoryInterval:i}))}function K_(t,e){return U_(t)[e]||(U_(t)[e]=[])}function $_(t,e){for(var n=0;n1&&h/l>2&&(u=Math.round(Math.ceil(u/l)*l));var c=S_(t),p=a.get("showMinLabel")||c,d=a.get("showMaxLabel")||c;p&&u!==o[0]&&g(o[0]);for(var f=u;f<=o[1];f+=l)g(f);function g(t){var e={value:t};s.push(n?t:{formattedLabel:i(e),rawLabel:r.getLabel(e),tickValue:t})}return d&&f-l!==o[1]&&g(o[1]),s}function tb(t,e,n){var i=t.scale,r=x_(t),o=[];return E(i.getTicks(),(function(t){var a=i.getLabel(t),s=t.value;e(t.value,a)&&o.push(n?s:{formattedLabel:r(t),rawLabel:a,tickValue:s})})),o}var eb=[0,1],nb=function(){function t(t,e,n){this.onBand=!1,this.inverse=!1,this.dim=t,this.scale=e,this._extent=n||[0,0]}return t.prototype.contain=function(t){var e=this._extent,n=Math.min(e[0],e[1]),i=Math.max(e[0],e[1]);return t>=n&&t<=i},t.prototype.containData=function(t){return this.scale.contain(t)},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.getPixelPrecision=function(t){return $r(t||this.scale.getExtent(),this._extent)},t.prototype.setExtent=function(t,e){var n=this._extent;n[0]=t,n[1]=e},t.prototype.dataToCoord=function(t,e){var n=this._extent,i=this.scale;return t=i.normalize(t),this.onBand&&"ordinal"===i.type&&ib(n=n.slice(),i.count()),Xr(t,eb,n,e)},t.prototype.coordToData=function(t,e){var n=this._extent,i=this.scale;this.onBand&&"ordinal"===i.type&&ib(n=n.slice(),i.count());var r=Xr(t,n,eb,e);return this.scale.scale(r)},t.prototype.pointToData=function(t,e){},t.prototype.getTicksCoords=function(t){var e=(t=t||{}).tickModel||this.getTickModel(),n=z(j_(this,e).ticks,(function(t){return{coord:this.dataToCoord("ordinal"===this.scale.type?this.scale.getRawOrdinalNumber(t):t),tickValue:t}}),this);return function(t,e,n,i){var r=e.length;if(!t.onBand||n||!r)return;var o,a,s=t.getExtent();if(1===r)e[0].coord=s[0],o=e[1]={coord:s[1]};else{var l=e[r-1].tickValue-e[0].tickValue,u=(e[r-1].coord-e[0].coord)/l;E(e,(function(t){t.coord-=u/2})),a=1+t.scale.getExtent()[1]-e[r-1].tickValue,o={coord:e[r-1].coord+u*a},e.push(o)}var h=s[0]>s[1];c(e[0].coord,s[0])&&(i?e[0].coord=s[0]:e.shift());i&&c(s[0],e[0].coord)&&e.unshift({coord:s[0]});c(s[1],o.coord)&&(i?o.coord=s[1]:e.pop());i&&c(o.coord,s[1])&&e.push({coord:s[1]});function c(t,e){return t=Zr(t),e=Zr(e),h?t>e:t0&&t<100||(t=5),z(this.scale.getMinorTicks(t),(function(t){return z(t,(function(t){return{coord:this.dataToCoord(t),tickValue:t}}),this)}),this)},t.prototype.getViewLabels=function(){return Z_(this).labels},t.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},t.prototype.getTickModel=function(){return this.model.getModel("axisTick")},t.prototype.getBandWidth=function(){var t=this._extent,e=this.scale.getExtent(),n=e[1]-e[0]+(this.onBand?1:0);0===n&&(n=1);var i=Math.abs(t[1]-t[0]);return Math.abs(i)/n},t.prototype.calculateCategoryInterval=function(){return function(t){var e=function(t){var e=t.getLabelModel();return{axisRotate:t.getRotate?t.getRotate():t.isHorizontal&&!t.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}(t),n=x_(t),i=(e.axisRotate-e.labelRotate)/180*Math.PI,r=t.scale,o=r.getExtent(),a=r.count();if(o[1]-o[0]<1)return 0;var s=1;a>40&&(s=Math.max(1,Math.floor(a/40)));for(var l=o[0],u=t.dataToCoord(l+1)-t.dataToCoord(l),h=Math.abs(u*Math.cos(i)),c=Math.abs(u*Math.sin(i)),p=0,d=0;l<=o[1];l+=s){var f,g,y=br(n({value:l}),e.font,"center","top");f=1.3*y.width,g=1.3*y.height,p=Math.max(p,f,7),d=Math.max(d,g,7)}var v=p/h,m=d/c;isNaN(v)&&(v=1/0),isNaN(m)&&(m=1/0);var x=Math.max(0,Math.floor(Math.min(v,m))),_=U_(t.model),b=t.getExtent(),w=_.lastAutoInterval,S=_.lastTickCount;return null!=w&&null!=S&&Math.abs(w-x)<=1&&Math.abs(S-a)<=1&&w>x&&_.axisExtent0===b[0]&&_.axisExtent1===b[1]?x=w:(_.lastTickCount=a,_.lastAutoInterval=x,_.axisExtent0=b[0],_.axisExtent1=b[1]),x}(this)},t}();function ib(t,e){var n=(t[1]-t[0])/e/2;t[0]+=n,t[1]-=n}var rb=2*Math.PI,ob=os.CMD,ab=["top","right","bottom","left"];function sb(t,e,n,i,r){var o=n.width,a=n.height;switch(t){case"top":i.set(n.x+o/2,n.y-e),r.set(0,-1);break;case"bottom":i.set(n.x+o/2,n.y+a+e),r.set(0,1);break;case"left":i.set(n.x-e,n.y+a/2),r.set(-1,0);break;case"right":i.set(n.x+o+e,n.y+a/2),r.set(1,0)}}function lb(t,e,n,i,r,o,a,s,l){a-=t,s-=e;var u=Math.sqrt(a*a+s*s),h=(a/=u)*n+t,c=(s/=u)*n+e;if(Math.abs(i-r)%rb<1e-4)return l[0]=h,l[1]=c,u-n;if(o){var p=i;i=hs(r),r=hs(p)}else i=hs(i),r=hs(r);i>r&&(r+=rb);var d=Math.atan2(s,a);if(d<0&&(d+=rb),d>=i&&d<=r||d+rb>=i&&d+rb<=r)return l[0]=h,l[1]=c,u-n;var f=n*Math.cos(i)+t,g=n*Math.sin(i)+e,y=n*Math.cos(r)+t,v=n*Math.sin(r)+e,m=(f-a)*(f-a)+(g-s)*(g-s),x=(y-a)*(y-a)+(v-s)*(v-s);return m0){e=e/180*Math.PI,fb.fromArray(t[0]),gb.fromArray(t[1]),yb.fromArray(t[2]),De.sub(vb,fb,gb),De.sub(mb,yb,gb);var n=vb.len(),i=mb.len();if(!(n<.001||i<.001)){vb.scale(1/n),mb.scale(1/i);var r=vb.dot(mb);if(Math.cos(e)1&&De.copy(bb,yb),bb.toArray(t[1])}}}}function Sb(t,e,n){if(n<=180&&n>0){n=n/180*Math.PI,fb.fromArray(t[0]),gb.fromArray(t[1]),yb.fromArray(t[2]),De.sub(vb,gb,fb),De.sub(mb,yb,gb);var i=vb.len(),r=mb.len();if(!(i<.001||r<.001))if(vb.scale(1/i),mb.scale(1/r),vb.dot(e)=a)De.copy(bb,yb);else{bb.scaleAndAdd(mb,o/Math.tan(Math.PI/2-s));var l=yb.x!==gb.x?(bb.x-gb.x)/(yb.x-gb.x):(bb.y-gb.y)/(yb.y-gb.y);if(isNaN(l))return;l<0?De.copy(bb,gb):l>1&&De.copy(bb,yb)}bb.toArray(t[1])}}}function Mb(t,e,n,i){var r="normal"===n,o=r?t:t.ensureState(n);o.ignore=e;var a=i.get("smooth");a&&!0===a&&(a=.3),o.shape=o.shape||{},a>0&&(o.shape.smooth=a);var s=i.getModel("lineStyle").getLineStyle();r?t.useStyle(s):o.style=s}function Ib(t,e){var n=e.smooth,i=e.points;if(i)if(t.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var r=Vt(i[0],i[1]),o=Vt(i[1],i[2]);if(!r||!o)return t.lineTo(i[1][0],i[1][1]),void t.lineTo(i[2][0],i[2][1]);var a=Math.min(r,o)*n,s=Gt([],i[1],i[0],a/r),l=Gt([],i[1],i[2],a/o),u=Gt([],s,l,.5);t.bezierCurveTo(s[0],s[1],s[0],s[1],u[0],u[1]),t.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var h=1;h0&&o&&_(-h/a,0,a);var f,g,y=t[0],v=t[a-1];return m(),f<0&&b(-f,.8),g<0&&b(g,.8),m(),x(f,g,1),x(g,f,-1),m(),f<0&&w(-f),g<0&&w(g),u}function m(){f=y.rect[e]-i,g=r-v.rect[e]-v.rect[n]}function x(t,e,n){if(t<0){var i=Math.min(e,-t);if(i>0){_(i*n,0,a);var r=i+t;r<0&&b(-r*n,1)}else b(-t*n,1)}}function _(n,i,r){0!==n&&(u=!0);for(var o=i;o0)for(l=0;l0;l--){_(-(o[l-1]*c),l,a)}}}function w(t){var e=t<0?-1:1;t=Math.abs(t);for(var n=Math.ceil(t/(a-1)),i=0;i0?_(n,0,i+1):_(-n,a-i-1,a),(t-=n)<=0)return}}function kb(t,e,n,i){return Ab(t,"y","height",e,n,i)}function Lb(t){var e=[];t.sort((function(t,e){return e.priority-t.priority}));var n=new ze(0,0,0,0);function i(t){if(!t.ignore){var e=t.ensureState("emphasis");null==e.ignore&&(e.ignore=!1)}t.ignore=!0}for(var r=0;r=0&&n.attr(d.oldLayoutSelect),P(u,"emphasis")>=0&&n.attr(d.oldLayoutEmphasis)),fh(n,s,e,a)}else if(n.attr(s),!uc(n).valueAnimation){var h=rt(n.style.opacity,1);n.style.opacity=0,gh(n,{style:{opacity:h}},e,a)}if(d.oldLayout=s,n.states.select){var c=d.oldLayoutSelect={};Vb(c,s,Bb),Vb(c,n.states.select,Bb)}if(n.states.emphasis){var p=d.oldLayoutEmphasis={};Vb(p,s,Bb),Vb(p,n.states.emphasis,Bb)}cc(n,a,l,e,e)}if(i&&!i.ignore&&!i.invisible){r=(d=zb(i)).oldLayout;var d,f={points:i.shape.points};r?(i.attr({shape:r}),fh(i,{shape:f},e)):(i.setShape(f),i.style.strokePercent=0,gh(i,{style:{strokePercent:1}},e)),d.oldLayout=f}},t}(),Gb=Oo();var Wb=Math.sin,Hb=Math.cos,Yb=Math.PI,Xb=2*Math.PI,Ub=180/Yb,Zb=function(){function t(){}return t.prototype.reset=function(t){this._start=!0,this._d=[],this._str="",this._p=Math.pow(10,t||4)},t.prototype.moveTo=function(t,e){this._add("M",t,e)},t.prototype.lineTo=function(t,e){this._add("L",t,e)},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){this._add("C",t,e,n,i,r,o)},t.prototype.quadraticCurveTo=function(t,e,n,i){this._add("Q",t,e,n,i)},t.prototype.arc=function(t,e,n,i,r,o){this.ellipse(t,e,n,n,0,i,r,o)},t.prototype.ellipse=function(t,e,n,i,r,o,a,s){var l=a-o,u=!s,h=Math.abs(l),c=hi(h-Xb)||(u?l>=Xb:-l>=Xb),p=l>0?l%Xb:l%Xb+Xb,d=!1;d=!!c||!hi(h)&&p>=Yb==!!u;var f=t+n*Hb(o),g=e+i*Wb(o);this._start&&this._add("M",f,g);var y=Math.round(r*Ub);if(c){var v=1/this._p,m=(u?1:-1)*(Xb-v);this._add("A",n,i,y,1,+u,t+n*Hb(o+m),e+i*Wb(o+m)),v>.01&&this._add("A",n,i,y,0,+u,f,g)}else{var x=t+n*Hb(a),_=e+i*Wb(a);this._add("A",n,i,y,+d,+u,x,_)}},t.prototype.rect=function(t,e,n,i){this._add("M",t,e),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},t.prototype.closePath=function(){this._d.length>0&&this._add("Z")},t.prototype._add=function(t,e,n,i,r,o,a,s,l){for(var u=[],h=this._p,c=1;c"}(r,o)+("style"!==r?re(a):a||"")+(i?""+n+z(i,(function(e){return t(e)})).join(n)+n:"")+("")}(t)}function rw(t){return{zrId:t,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssClassIdx:0,cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function ow(t,e,n,i){return nw("svg","root",{width:t,height:e,xmlns:Qb,"xmlns:xlink":tw,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+t+" "+e},n)}var aw={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},sw="transform-origin";function lw(t,e,n){var i=A({},t.shape);A(i,e),t.buildPath(n,i);var r=new Zb;return r.reset(_i(t)),n.rebuildPath(r,1),r.generateStr(),r.getStr()}function uw(t,e){var n=e.originX,i=e.originY;(n||i)&&(t[sw]=n+"px "+i+"px")}var hw={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function cw(t,e){var n=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[n]=t,n}function pw(t){return U(t)?aw[t]?"cubic-bezier("+aw[t]+")":Pn(t)?t:"":""}function dw(t,e,n,i){var r=t.animators,o=r.length,a=[];if(t instanceof th){var s=function(t,e,n){var i,r,o=t.shape.paths,a={};if(E(o,(function(t){var e=rw(n.zrId);e.animation=!0,dw(t,{},e,!0);var o=e.cssAnims,s=e.cssNodes,l=G(o),u=l.length;if(u){var h=o[r=l[u-1]];for(var c in h){var p=h[c];a[c]=a[c]||{d:""},a[c].d+=p.d||""}for(var d in s){var f=s[d].animation;f.indexOf(r)>=0&&(i=f)}}})),i){e.d=!1;var s=cw(a,n);return i.replace(r,s)}}(t,e,n);if(s)a.push(s);else if(!o)return}else if(!o)return;for(var l={},u=0;u0})).length)return cw(h,n)+" "+r[0]+" both"}for(var y in l){(s=g(l[y]))&&a.push(s)}if(a.length){var v=n.zrId+"-cls-"+n.cssClassIdx++;n.cssNodes["."+v]={animation:a.join(",")},e.class=v}}var fw=Math.round;function gw(t){return t&&U(t.src)}function yw(t){return t&&X(t.toDataURL)}function vw(t,e,n,i){Jb((function(r,o){var a="fill"===r||"stroke"===r;a&&mi(o)?Cw(e,t,r,i):a&&gi(o)?Dw(n,t,r,i):t[r]=o}),e,n,!1),function(t,e,n){var i=t.style;if(function(t){return t&&(t.shadowBlur||t.shadowOffsetX||t.shadowOffsetY)}(i)){var r=function(t){var e=t.style,n=t.getGlobalScale();return[e.shadowColor,(e.shadowBlur||0).toFixed(2),(e.shadowOffsetX||0).toFixed(2),(e.shadowOffsetY||0).toFixed(2),n[0],n[1]].join(",")}(t),o=n.shadowCache,a=o[r];if(!a){var s=t.getGlobalScale(),l=s[0],u=s[1];if(!l||!u)return;var h=i.shadowOffsetX||0,c=i.shadowOffsetY||0,p=i.shadowBlur,d=li(i.shadowColor),f=d.opacity,g=d.color,y=p/2/l+" "+p/2/u;a=n.zrId+"-s"+n.shadowIdx++,n.defs[a]=nw("filter",a,{id:a,x:"-100%",y:"-100%",width:"300%",height:"300%"},[nw("feDropShadow","",{dx:h/l,dy:c/u,stdDeviation:y,"flood-color":g,"flood-opacity":f})]),o[r]=a}e.filter=xi(a)}}(n,t,i)}function mw(t){return hi(t[0]-1)&&hi(t[1])&&hi(t[2])&&hi(t[3]-1)}function xw(t,e,n){if(e&&(!function(t){return hi(t[4])&&hi(t[5])}(e)||!mw(e))){var i=n?10:1e4;t.transform=mw(e)?"translate("+fw(e[4]*i)/i+" "+fw(e[5]*i)/i+")":function(t){return"matrix("+ci(t[0])+","+ci(t[1])+","+ci(t[2])+","+ci(t[3])+","+pi(t[4])+","+pi(t[5])+")"}(e)}}function _w(t,e,n){for(var i=t.points,r=[],o=0;ol?Hw(t,null==n[c+1]?null:n[c+1].elm,n,s,c):Yw(t,e,a,l))}(n,i,r):Bw(r)?(Bw(t.text)&&Ew(n,""),Hw(n,null,r,0,r.length-1)):Bw(i)?Yw(n,i,0,i.length-1):Bw(t.text)&&Ew(n,""):t.text!==e.text&&(Bw(i)&&Yw(n,i,0,i.length-1),Ew(n,e.text)))}var Zw=0,jw=function(){function t(t,e,n){if(this.type="svg",this.refreshHover=qw("refreshHover"),this.configLayer=qw("configLayer"),this.storage=e,this._opts=n=A({},n),this.root=t,this._id="zr"+Zw++,this._oldVNode=ow(n.width,n.height),t&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var r=this._svgDom=this._oldVNode.elm=ew("svg");Xw(null,this._oldVNode),i.appendChild(r),t.appendChild(i)}this.resize(n.width,n.height)}return t.prototype.getType=function(){return this.type},t.prototype.getViewportRoot=function(){return this._viewport},t.prototype.getViewportRootOffset=function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},t.prototype.getSvgDom=function(){return this._svgDom},t.prototype.refresh=function(){if(this.root){var t=this.renderToVNode({willUpdate:!0});t.attrs.style="position:absolute;left:0;top:0;user-select:none",function(t,e){if(Gw(t,e))Uw(t,e);else{var n=t.elm,i=Rw(n);Ww(e),null!==i&&(Lw(i,e.elm,Nw(n)),Yw(i,[t],0,0))}}(this._oldVNode,t),this._oldVNode=t}},t.prototype.renderOneToVNode=function(t){return Tw(t,rw(this._id))},t.prototype.renderToVNode=function(t){t=t||{};var e=this.storage.getDisplayList(!0),n=this._width,i=this._height,r=rw(this._id);r.animation=t.animation,r.willUpdate=t.willUpdate,r.compress=t.compress;var o=[],a=this._bgVNode=function(t,e,n,i){var r;if(n&&"none"!==n)if(r=nw("rect","bg",{width:t,height:e,x:"0",y:"0",id:"0"}),mi(n))Cw({fill:n},r.attrs,"fill",i);else if(gi(n))Dw({style:{fill:n},dirty:bt,getBoundingRect:function(){return{width:t,height:e}}},r.attrs,"fill",i);else{var o=li(n),a=o.color,s=o.opacity;r.attrs.fill=a,s<1&&(r.attrs["fill-opacity"]=s)}return r}(n,i,this._backgroundColor,r);a&&o.push(a);var s=t.compress?null:this._mainVNode=nw("g","main",{},[]);this._paintList(e,r,s?s.children:o),s&&o.push(s);var l=z(G(r.defs),(function(t){return r.defs[t]}));if(l.length&&o.push(nw("defs","defs",{},l)),t.animation){var u=function(t,e,n){var i=(n=n||{}).newline?"\n":"",r=" {"+i,o=i+"}",a=z(G(t),(function(e){return e+r+z(G(t[e]),(function(n){return n+":"+t[e][n]+";"})).join(i)+o})).join(i),s=z(G(e),(function(t){return"@keyframes "+t+r+z(G(e[t]),(function(n){return n+r+z(G(e[t][n]),(function(i){var r=e[t][n][i];return"d"===i&&(r='path("'+r+'")'),i+":"+r+";"})).join(i)+o})).join(i)+o})).join(i);return a||s?[""].join(i):""}(r.cssNodes,r.cssAnims,{newline:!0});if(u){var h=nw("style","stl",{},[],u);o.push(h)}}return ow(n,i,o,t.useViewBox)},t.prototype.renderToString=function(t){return t=t||{},iw(this.renderToVNode({animation:rt(t.cssAnimation,!0),willUpdate:!1,compress:!0,useViewBox:rt(t.useViewBox,!0)}),{newline:!0})},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t},t.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},t.prototype._paintList=function(t,e,n){for(var i,r,o=t.length,a=[],s=0,l=0,u=0;u=0&&(!c||!r||c[f]!==r[f]);f--);for(var g=d-1;g>f;g--)i=a[--s-1];for(var y=f+1;y=a)}}for(var h=this.__startIndex;h15)break}n.prevElClipPaths&&u.restore()};if(p)if(0===p.length)s=l.__endIndex;else for(var _=d.dpr,b=0;b0&&t>i[0]){for(s=0;st);s++);a=n[i[s]]}if(i.splice(s+1,0,t),n[t]=e,!e.virtual)if(a){var l=a.dom;l.nextSibling?o.insertBefore(e.dom,l.nextSibling):o.appendChild(e.dom)}else o.firstChild?o.insertBefore(e.dom,o.firstChild):o.appendChild(e.dom);e.__painter=this}},t.prototype.eachLayer=function(t,e){for(var n=this._zlevelList,i=0;i0?tS:0),this._needsManuallyCompositing),u.__builtin__||I("ZLevel "+l+" has been used by unkown layer "+u.id),u!==o&&(u.__used=!0,u.__startIndex!==r&&(u.__dirty=!0),u.__startIndex=r,u.incremental?u.__drawIndex=-1:u.__drawIndex=r,e(r),o=u),1&s.__dirty&&!s.__inHover&&(u.__dirty=!0,u.incremental&&u.__drawIndex<0&&(u.__drawIndex=r))}e(r),this.eachBuiltinLayer((function(t,e){!t.__used&&t.getElementCount()>0&&(t.__dirty=!0,t.__startIndex=t.__endIndex=t.__drawIndex=0),t.__dirty&&t.__drawIndex<0&&(t.__drawIndex=t.__startIndex)}))},t.prototype.clear=function(){return this.eachBuiltinLayer(this._clearLayer),this},t.prototype._clearLayer=function(t){t.clear()},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t,E(this._layers,(function(t){t.setUnpainted()}))},t.prototype.configLayer=function(t,e){if(e){var n=this._layerConfig;n[t]?C(n[t],e,!0):n[t]=e;for(var i=0;i-1&&(s.style.stroke=s.style.fill,s.style.fill="#fff",s.style.lineWidth=2),e},e.type="series.line",e.dependencies=["grid","polar"],e.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:4,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1},e}(mg);function iS(t,e){var n=t.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var r=gf(t,e,n[0]);return null!=r?r+"":null}if(i){for(var o=[],a=0;a=0&&i.push(e[o])}return i.join(" ")}var oS=function(t){function e(e,n,i,r){var o=t.call(this)||this;return o.updateData(e,n,i,r),o}return n(e,t),e.prototype._createSymbol=function(t,e,n,i,r){this.removeAll();var o=Wy(t,-1,-1,2,2,null,r);o.attr({z2:100,culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),o.drift=aS,this._symbolType=t,this.add(o)},e.prototype.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(null,t)},e.prototype.getSymbolType=function(){return this._symbolType},e.prototype.getSymbolPath=function(){return this.childAt(0)},e.prototype.highlight=function(){kl(this.childAt(0))},e.prototype.downplay=function(){Ll(this.childAt(0))},e.prototype.setZ=function(t,e){var n=this.childAt(0);n.zlevel=t,n.z=e},e.prototype.setDraggable=function(t,e){var n=this.childAt(0);n.draggable=t,n.cursor=!e&&t?"move":n.cursor},e.prototype.updateData=function(t,n,i,r){this.silent=!1;var o=t.getItemVisual(n,"symbol")||"circle",a=t.hostModel,s=e.getSymbolSize(t,n),l=o!==this._symbolType,u=r&&r.disableAnimation;if(l){var h=t.getItemVisual(n,"symbolKeepAspect");this._createSymbol(o,t,n,s,h)}else{(p=this.childAt(0)).silent=!1;var c={scaleX:s[0]/2,scaleY:s[1]/2};u?p.attr(c):fh(p,c,a,n),_h(p)}if(this._updateCommon(t,n,s,i,r),l){var p=this.childAt(0);if(!u){c={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:p.style.opacity}};p.scaleX=p.scaleY=0,p.style.opacity=0,gh(p,c,a,n)}}u&&this.childAt(0).stopAnimation("leave")},e.prototype._updateCommon=function(t,e,n,i,r){var o,a,s,l,u,h,c,p,d,f=this.childAt(0),g=t.hostModel;if(i&&(o=i.emphasisItemStyle,a=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,u=i.blurScope,c=i.labelStatesModels,p=i.hoverScale,d=i.cursorStyle,h=i.emphasisDisabled),!i||t.hasItemOption){var y=i&&i.itemModel?i.itemModel:t.getItemModel(e),v=y.getModel("emphasis");o=v.getModel("itemStyle").getItemStyle(),s=y.getModel(["select","itemStyle"]).getItemStyle(),a=y.getModel(["blur","itemStyle"]).getItemStyle(),l=v.get("focus"),u=v.get("blurScope"),h=v.get("disabled"),c=ec(y),p=v.getShallow("scale"),d=y.getShallow("cursor")}var m=t.getItemVisual(e,"symbolRotate");f.attr("rotation",(m||0)*Math.PI/180||0);var x=Yy(t.getItemVisual(e,"symbolOffset"),n);x&&(f.x=x[0],f.y=x[1]),d&&f.attr("cursor",d);var _=t.getItemVisual(e,"style"),b=_.fill;if(f instanceof ks){var w=f.style;f.useStyle(A({image:w.image,x:w.x,y:w.y,width:w.width,height:w.height},_))}else f.__isEmptyBrush?f.useStyle(A({},_)):f.useStyle(_),f.style.decal=null,f.setColor(b,r&&r.symbolInnerColor),f.style.strokeNoScale=!0;var S=t.getItemVisual(e,"liftZ"),M=this._z2;null!=S?null==M&&(this._z2=f.z2,f.z2+=S):null!=M&&(f.z2=M,this._z2=null);var I=r&&r.useNameLabel;tc(f,c,{labelFetcher:g,labelDataIndex:e,defaultText:function(e){return I?t.getName(e):iS(t,e)},inheritColor:b,defaultOpacity:_.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var T=f.ensureState("emphasis");T.style=o,f.ensureState("select").style=s,f.ensureState("blur").style=a;var C=null==p||!0===p?Math.max(1.1,3/this._sizeY):isFinite(p)&&p>0?+p:1;T.scaleX=this._sizeX*C,T.scaleY=this._sizeY*C,this.setSymbolScale(1),Yl(this,l,u,h)},e.prototype.setSymbolScale=function(t){this.scaleX=this.scaleY=t},e.prototype.fadeOut=function(t,e,n){var i=this.childAt(0),r=Qs(this).dataIndex,o=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var a=i.getTextContent();a&&vh(a,{style:{opacity:0}},e,{dataIndex:r,removeOpt:o,cb:function(){i.removeTextContent()}})}else i.removeTextContent();vh(i,{style:{opacity:0},scaleX:0,scaleY:0},e,{dataIndex:r,cb:t,removeOpt:o})},e.getSymbolSize=function(t,e){return Hy(t.getItemVisual(e,"symbolSize"))},e}(zr);function aS(t,e){this.parent.drift(t,e)}function sS(t,e,n,i){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(i.isIgnore&&i.isIgnore(n))&&!(i.clipShape&&!i.clipShape.contain(e[0],e[1]))&&"none"!==t.getItemVisual(n,"symbol")}function lS(t){return null==t||q(t)||(t={isIgnore:t}),t||{}}function uS(t){var e=t.hostModel,n=e.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:e.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:e.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:ec(e),cursorStyle:e.get("cursor")}}var hS=function(){function t(t){this.group=new zr,this._SymbolCtor=t||oS}return t.prototype.updateData=function(t,e){this._progressiveEls=null,e=lS(e);var n=this.group,i=t.hostModel,r=this._data,o=this._SymbolCtor,a=e.disableAnimation,s=uS(t),l={disableAnimation:a},u=e.getSymbolPoint||function(e){return t.getItemLayout(e)};r||n.removeAll(),t.diff(r).add((function(i){var r=u(i);if(sS(t,r,i,e)){var a=new o(t,i,s,l);a.setPosition(r),t.setItemGraphicEl(i,a),n.add(a)}})).update((function(h,c){var p=r.getItemGraphicEl(c),d=u(h);if(sS(t,d,h,e)){var f=t.getItemVisual(h,"symbol")||"circle",g=p&&p.getSymbolType&&p.getSymbolType();if(!p||g&&g!==f)n.remove(p),(p=new o(t,h,s,l)).setPosition(d);else{p.updateData(t,h,s,l);var y={x:d[0],y:d[1]};a?p.attr(y):fh(p,y,i)}n.add(p),t.setItemGraphicEl(h,p)}else n.remove(p)})).remove((function(t){var e=r.getItemGraphicEl(t);e&&e.fadeOut((function(){n.remove(e)}),i)})).execute(),this._getSymbolPoint=u,this._data=t},t.prototype.updateLayout=function(){var t=this,e=this._data;e&&e.eachItemGraphicEl((function(e,n){var i=t._getSymbolPoint(n);e.setPosition(i),e.markRedraw()}))},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=uS(t),this._data=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e,n){function i(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[],n=lS(n);for(var r=t.start;r0?n=i[0]:i[1]<0&&(n=i[1]);return n}(r,n),a=i.dim,s=r.dim,l=e.mapDimension(s),u=e.mapDimension(a),h="x"===s||"radius"===s?1:0,c=z(t.dimensions,(function(t){return e.mapDimension(t)})),p=!1,d=e.getCalculationInfo("stackResultDimension");return gx(e,c[0])&&(p=!0,c[0]=d),gx(e,c[1])&&(p=!0,c[1]=d),{dataDimsForPoint:c,valueStart:o,valueAxisDim:s,baseAxisDim:a,stacked:!!p,valueDim:l,baseDim:u,baseDataOffset:h,stackedOverDimension:e.getCalculationInfo("stackedOverDimension")}}function pS(t,e,n,i){var r=NaN;t.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(r)&&(r=t.valueStart);var o=t.baseDataOffset,a=[];return a[o]=n.get(t.baseDim,i),a[1-o]=r,e.dataToPoint(a)}var dS=Math.min,fS=Math.max;function gS(t,e){return isNaN(t)||isNaN(e)}function yS(t,e,n,i,r,o,a,s,l){for(var u,h,c,p,d,f,g=n,y=0;y=r||g<0)break;if(gS(v,m)){if(l){g+=o;continue}break}if(g===n)t[o>0?"moveTo":"lineTo"](v,m),c=v,p=m;else{var x=v-u,_=m-h;if(x*x+_*_<.5){g+=o;continue}if(a>0){for(var b=g+o,w=e[2*b],S=e[2*b+1];w===v&&S===m&&y=i||gS(w,S))d=v,f=m;else{T=w-u,C=S-h;var k=v-u,L=w-v,P=m-h,O=S-m,R=void 0,N=void 0;if("x"===s){var E=T>0?1:-1;d=v-E*(R=Math.abs(k))*a,f=m,D=v+E*(N=Math.abs(L))*a,A=m}else if("y"===s){var z=C>0?1:-1;d=v,f=m-z*(R=Math.abs(P))*a,D=v,A=m+z*(N=Math.abs(O))*a}else R=Math.sqrt(k*k+P*P),d=v-T*a*(1-(I=(N=Math.sqrt(L*L+O*O))/(N+R))),f=m-C*a*(1-I),A=m+C*a*I,D=dS(D=v+T*a*I,fS(w,v)),A=dS(A,fS(S,m)),D=fS(D,dS(w,v)),f=m-(C=(A=fS(A,dS(S,m)))-m)*R/N,d=dS(d=v-(T=D-v)*R/N,fS(u,v)),f=dS(f,fS(h,m)),D=v+(T=v-(d=fS(d,dS(u,v))))*N/R,A=m+(C=m-(f=fS(f,dS(h,m))))*N/R}t.bezierCurveTo(c,p,d,f,v,m),c=D,p=A}else t.lineTo(v,m)}u=v,h=m,g+=o}return y}var vS=function(){this.smooth=0,this.smoothConstraint=!0},mS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polyline",n}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new vS},e.prototype.buildPath=function(t,e){var n=e.points,i=0,r=n.length/2;if(e.connectNulls){for(;r>0&&gS(n[2*r-2],n[2*r-1]);r--);for(;i=0){var y=a?(h-i)*g+i:(u-n)*g+n;return a?[t,y]:[y,t]}n=u,i=h;break;case o.C:u=r[l++],h=r[l++],c=r[l++],p=r[l++],d=r[l++],f=r[l++];var v=a?_n(n,u,c,d,t,s):_n(i,h,p,f,t,s);if(v>0)for(var m=0;m=0){y=a?mn(i,h,p,f,x):mn(n,u,c,d,x);return a?[t,y]:[y,t]}}n=d,i=f}}},e}(Is),xS=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e}(vS),_S=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polygon",n}return n(e,t),e.prototype.getDefaultShape=function(){return new xS},e.prototype.buildPath=function(t,e){var n=e.points,i=e.stackedOnPoints,r=0,o=n.length/2,a=e.smoothMonotone;if(e.connectNulls){for(;o>0&&gS(n[2*o-2],n[2*o-1]);o--);for(;r=0;a--){var s=t.getDimensionInfo(i[a].dimension);if("x"===(r=s&&s.coordDim)||"y"===r){o=i[a];break}}if(o){var l=e.getAxis(r),u=z(o.stops,(function(t){return{coord:l.toGlobalCoord(l.dataToCoord(t.value)),color:t.color}})),h=u.length,c=o.outerColors.slice();h&&u[0].coord>u[h-1].coord&&(u.reverse(),c.reverse());var p=function(t,e){var n,i,r=[],o=t.length;function a(t,e,n){var i=t.coord;return{coord:n,color:ti((n-i)/(e.coord-i),[t.color,e.color])}}for(var s=0;se){i?r.push(a(i,l,e)):n&&r.push(a(n,l,0),a(n,l,e));break}n&&(r.push(a(n,l,0)),n=null),r.push(l),i=l}}return r}(u,"x"===r?n.getWidth():n.getHeight()),d=p.length;if(!d&&h)return u[0].coord<0?c[1]?c[1]:u[h-1].color:c[0]?c[0]:u[0].color;var f=p[0].coord-10,g=p[d-1].coord+10,y=g-f;if(y<.001)return"transparent";E(p,(function(t){t.offset=(t.coord-f)/y})),p.push({offset:d?p[d-1].offset:.5,color:c[1]||"transparent"}),p.unshift({offset:d?p[0].offset:.5,color:c[0]||"transparent"});var v=new nh(0,0,0,0,p,!0);return v[r]=f,v[r+"2"]=g,v}}}function LS(t,e,n){var i=t.get("showAllSymbol"),r="auto"===i;if(!i||r){var o=n.getAxesByScale("ordinal")[0];if(o&&(!r||!function(t,e){var n=t.getExtent(),i=Math.abs(n[1]-n[0])/t.scale.count();isNaN(i)&&(i=0);for(var r=e.count(),o=Math.max(1,Math.round(r/5)),a=0;ai)return!1;return!0}(o,e))){var a=e.mapDimension(o.dim),s={};return E(o.getViewLabels(),(function(t){var e=o.scale.getRawOrdinalNumber(t.tickValue);s[e]=1})),function(t){return!s.hasOwnProperty(e.get(a,t))}}}}function PS(t,e){return[t[2*e],t[2*e+1]]}function OS(t){if(t.get(["endLabel","show"]))return!0;for(var e=0;e0&&"bolder"===t.get(["emphasis","lineStyle","width"]))&&(d.getState("emphasis").style.lineWidth=+d.style.lineWidth+1);Qs(d).seriesIndex=t.seriesIndex,Yl(d,L,P,O);var R=DS(t.get("smooth")),N=t.get("smoothMonotone");if(d.setShape({smooth:R,smoothMonotone:N,connectNulls:w}),f){var E=a.getCalculationInfo("stackedOnSeries"),z=0;f.useStyle(k(l.getAreaStyle(),{fill:C,opacity:.7,lineJoin:"bevel",decal:a.getVisual("style").decal})),E&&(z=DS(E.get("smooth"))),f.setShape({smooth:R,stackedOnSmooth:z,smoothMonotone:N,connectNulls:w}),jl(f,t,"areaStyle"),Qs(f).seriesIndex=t.seriesIndex,Yl(f,L,P,O)}var V=function(t){i._changePolyState(t)};a.eachItemGraphicEl((function(t){t&&(t.onHoverStateChange=V)})),this._polyline.onHoverStateChange=V,this._data=a,this._coordSys=r,this._stackedOnPoints=_,this._points=u,this._step=T,this._valueOrigin=m,t.get("triggerLineEvent")&&(this.packEventData(t,d),f&&this.packEventData(t,f))},e.prototype.packEventData=function(t,e){Qs(e).eventData={componentType:"series",componentSubType:"line",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"line"}},e.prototype.highlight=function(t,e,n,i){var r=t.getData(),o=Po(r,i);if(this._changePolyState("emphasis"),!(o instanceof Array)&&null!=o&&o>=0){var a=r.getLayout("points"),s=r.getItemGraphicEl(o);if(!s){var l=a[2*o],u=a[2*o+1];if(isNaN(l)||isNaN(u))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,u))return;var h=t.get("zlevel")||0,c=t.get("z")||0;(s=new oS(r,o)).x=l,s.y=u,s.setZ(h,c);var p=s.getSymbolPath().getTextContent();p&&(p.zlevel=h,p.z=c,p.z2=this._polyline.z2+1),s.__temp=!0,r.setItemGraphicEl(o,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else kg.prototype.highlight.call(this,t,e,n,i)},e.prototype.downplay=function(t,e,n,i){var r=t.getData(),o=Po(r,i);if(this._changePolyState("normal"),null!=o&&o>=0){var a=r.getItemGraphicEl(o);a&&(a.__temp?(r.setItemGraphicEl(o,null),this.group.remove(a)):a.downplay())}else kg.prototype.downplay.call(this,t,e,n,i)},e.prototype._changePolyState=function(t){var e=this._polygon;Il(this._polyline,t),e&&Il(e,t)},e.prototype._newPolyline=function(t){var e=this._polyline;return e&&this._lineGroup.remove(e),e=new mS({shape:{points:t},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(e),this._polyline=e,e},e.prototype._newPolygon=function(t,e){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new _S({shape:{points:t,stackedOnPoints:e},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},e.prototype._initSymbolLabelAnimation=function(t,e,n){var i,r,o=e.getBaseAxis(),a=o.inverse;"cartesian2d"===e.type?(i=o.isHorizontal(),r=!1):"polar"===e.type&&(i="angle"===o.dim,r=!0);var s=t.hostModel,l=s.get("animationDuration");X(l)&&(l=l(null));var u=s.get("animationDelay")||0,h=X(u)?u(null):u;t.eachItemGraphicEl((function(t,o){var s=t;if(s){var c=[t.x,t.y],p=void 0,d=void 0,f=void 0;if(n)if(r){var g=n,y=e.pointToCoord(c);i?(p=g.startAngle,d=g.endAngle,f=-y[1]/180*Math.PI):(p=g.r0,d=g.r,f=y[0])}else{var v=n;i?(p=v.x,d=v.x+v.width,f=t.x):(p=v.y+v.height,d=v.y,f=t.y)}var m=d===p?0:(f-p)/(d-p);a&&(m=1-m);var x=X(u)?u(o):l*m+h,_=s.getSymbolPath(),b=_.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),b&&b.animateFrom({style:{opacity:0}},{duration:300,delay:x}),_.disableLabelAnimation=!0}}))},e.prototype._initOrUpdateEndLabel=function(t,e,n){var i=t.getModel("endLabel");if(OS(t)){var r=t.getData(),o=this._polyline,a=r.getLayout("points");if(!a)return o.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new Fs({z2:200})).ignoreClip=!0,o.setTextContent(this._endLabel),o.disableLabelAnimation=!0);var l=function(t){for(var e,n,i=t.length/2;i>0&&(e=t[2*i-2],n=t[2*i-1],isNaN(e)||isNaN(n));i--);return i-1}(a);l>=0&&(tc(o,ec(t,"endLabel"),{inheritColor:n,labelFetcher:t,labelDataIndex:l,defaultText:function(t,e,n){return null!=n?rS(r,n):iS(r,t)},enableTextSetter:!0},function(t,e){var n=e.getBaseAxis(),i=n.isHorizontal(),r=n.inverse,o=i?r?"right":"left":"center",a=i?"middle":r?"top":"bottom";return{normal:{align:t.get("align")||o,verticalAlign:t.get("verticalAlign")||a}}}(i,e)),o.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},e.prototype._endLabelOnDuring=function(t,e,n,i,r,o,a){var s=this._endLabel,l=this._polyline;if(s){t<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var u=n.getLayout("points"),h=n.hostModel,c=h.get("connectNulls"),p=o.get("precision"),d=o.get("distance")||0,f=a.getBaseAxis(),g=f.isHorizontal(),y=f.inverse,v=e.shape,m=y?g?v.x:v.y+v.height:g?v.x+v.width:v.y,x=(g?d:0)*(y?-1:1),_=(g?0:-d)*(y?-1:1),b=g?"x":"y",w=function(t,e,n){for(var i,r,o=t.length/2,a="x"===n?0:1,s=0,l=-1,u=0;u=e||i>=e&&r<=e){l=u;break}s=u,i=r}else i=r;return{range:[s,l],t:(e-i)/(r-i)}}(u,m,b),S=w.range,M=S[1]-S[0],I=void 0;if(M>=1){if(M>1&&!c){var T=PS(u,S[0]);s.attr({x:T[0]+x,y:T[1]+_}),r&&(I=h.getRawValue(S[0]))}else{(T=l.getPointOn(m,b))&&s.attr({x:T[0]+x,y:T[1]+_});var C=h.getRawValue(S[0]),D=h.getRawValue(S[1]);r&&(I=Wo(n,p,C,D,w.t))}i.lastFrameIndex=S[0]}else{var A=1===t||i.lastFrameIndex>0?S[0]:0;T=PS(u,A);r&&(I=h.getRawValue(A)),s.attr({x:T[0]+x,y:T[1]+_})}if(r){var k=uc(s);"function"==typeof k.setLabelText&&k.setLabelText(I)}}},e.prototype._doUpdateAnimation=function(t,e,n,i,r,o,a){var s=this._polyline,l=this._polygon,u=t.hostModel,h=function(t,e,n,i,r,o,a,s){for(var l=function(t,e){var n=[];return e.diff(t).add((function(t){n.push({cmd:"+",idx:t})})).update((function(t,e){n.push({cmd:"=",idx:e,idx1:t})})).remove((function(t){n.push({cmd:"-",idx:t})})).execute(),n}(t,e),u=[],h=[],c=[],p=[],d=[],f=[],g=[],y=cS(r,e,a),v=t.getLayout("points")||[],m=e.getLayout("points")||[],x=0;x3e3||l&&CS(p,f)>3e3)return s.stopAnimation(),s.setShape({points:d}),void(l&&(l.stopAnimation(),l.setShape({points:d,stackedOnPoints:f})));s.shape.__points=h.current,s.shape.points=c;var g={shape:{points:d}};h.current!==c&&(g.shape.__points=h.next),s.stopAnimation(),fh(s,g,u),l&&(l.setShape({points:c,stackedOnPoints:p}),l.stopAnimation(),fh(l,{shape:{stackedOnPoints:f}},u),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var y=[],v=h.status,m=0;me&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;n10&&"cartesian2d"===o.type&&r){var s=o.getBaseAxis(),l=o.getOtherAxis(s),u=s.getExtent(),h=n.getDevicePixelRatio(),c=Math.abs(u[1]-u[0])*(h||1),p=Math.round(a/c);if(isFinite(p)&&p>1){"lttb"===r&&t.setData(i.lttbDownSample(i.mapDimension(l.dim),1/p));var d=void 0;U(r)?d=zS[r]:X(r)&&(d=r),d&&t.setData(i.downSample(i.mapDimension(l.dim),1/p,d,VS))}}}}}var FS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(t,e){return vx(null,this,{useEncodeDefaulter:!0})},e.prototype.getMarkerPosition=function(t,e,n){var i=this.coordinateSystem;if(i&&i.clampData){var r=i.clampData(t),o=i.dataToPoint(r);if(n)E(i.getAxes(),(function(t,n){if("category"===t.type&&null!=e){var i=t.getTicksCoords(),a=r[n],s="x1"===e[n]||"y1"===e[n];if(s&&(a+=1),i.length<2)return;if(2===i.length)return void(o[n]=t.toGlobalCoord(t.getExtent()[s?1:0]));for(var l=void 0,u=void 0,h=1,c=0;ca){u=(p+l)/2;break}1===c&&(h=d-i[0].tickValue)}null==u&&(l?l&&(u=i[i.length-1].coord):u=i[0].coord),o[n]=t.toGlobalCoord(u)}}));else{var a=this.getData(),s=a.getLayout("offset"),l=a.getLayout("size"),u=i.getBaseAxis().isHorizontal()?0:1;o[u]+=s+l/2}return o}return[NaN,NaN]},e.type="series.__base_bar__",e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod"},e}(mg);mg.registerClass(FS);var GS=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(){return vx(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},e.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},e.prototype.getProgressiveThreshold=function(){var t=this.get("progressiveThreshold"),e=this.get("largeThreshold");return e>t&&(t=e),t},e.prototype.brushSelector=function(t,e,n){return n.rect(e.getItemLayout(t))},e.type="series.bar",e.dependencies=["grid","polar"],e.defaultOption=Cc(FS.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:"#212121"}},realtimeSort:!1}),e}(FS),WS=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},HS=function(t){function e(e){var n=t.call(this,e)||this;return n.type="sausage",n}return n(e,t),e.prototype.getDefaultShape=function(){return new WS},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r0||0,0),o=Math.max(e.r,0),a=.5*(o-r),s=r+a,l=e.startAngle,u=e.endAngle,h=e.clockwise,c=2*Math.PI,p=h?u-lo)return!0;o=u}return!1},e.prototype._isOrderDifferentInView=function(t,e){for(var n=e.scale,i=n.getExtent(),r=Math.max(0,i[0]),o=Math.min(i[1],n.getOrdinalMeta().categories.length-1);r<=o;++r)if(t.ordinalNumbers[r]!==n.getRawOrdinalNumber(r))return!0},e.prototype._updateSortWithinSameData=function(t,e,n,i){if(this._isOrderChangedWithinSameData(t,e,n)){var r=this._dataSort(t,n,e);this._isOrderDifferentInView(r,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:r}))}},e.prototype._dispatchInitSort=function(t,e,n){var i=e.baseAxis,r=this._dataSort(t,i,(function(n){return t.get(t.mapDimension(e.otherAxis.dim),n)}));n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:r})},e.prototype.remove=function(t,e){this._clear(this._model),this._removeOnRenderedListener(e)},e.prototype.dispose=function(t,e){this._removeOnRenderedListener(e)},e.prototype._removeOnRenderedListener=function(t){this._onRendered&&(t.getZr().off("rendered",this._onRendered),this._onRendered=null)},e.prototype._clear=function(t){var e=this.group,n=this._data;t&&t.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl((function(e){xh(e,t,Qs(e).dataIndex)}))):e.removeAll(),this._data=null,this._isFirstFrame=!0},e.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},e.type="bar",e}(kg),KS={cartesian2d:function(t,e){var n=e.width<0?-1:1,i=e.height<0?-1:1;n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height);var r=t.x+t.width,o=t.y+t.height,a=ZS(e.x,t.x),s=jS(e.x+e.width,r),l=ZS(e.y,t.y),u=jS(e.y+e.height,o),h=sr?s:a,e.y=c&&l>o?u:l,e.width=h?0:s-a,e.height=c?0:u-l,n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height),h||c},polar:function(t,e){var n=e.r0<=e.r?1:-1;if(n<0){var i=e.r;e.r=e.r0,e.r0=i}var r=jS(e.r,t.r),o=ZS(e.r0,t.r0);e.r=r,e.r0=o;var a=r-o<0;if(n<0){i=e.r;e.r=e.r0,e.r0=i}return a}},$S={cartesian2d:function(t,e,n,i,r,o,a,s,l){var u=new zs({shape:A({},i),z2:1});(u.__dataIndex=n,u.name="item",o)&&(u.shape[r?"height":"width"]=0);return u},polar:function(t,e,n,i,r,o,a,s,l){var u=!r&&l?HS:zu,h=new u({shape:i,z2:1});h.name="item";var c,p,d=rM(r);if(h.calculateTextPosition=(c=d,p=({isRoundCap:u===HS}||{}).isRoundCap,function(t,e,n){var i=e.position;if(!i||i instanceof Array)return Tr(t,e,n);var r=c(i),o=null!=e.distance?e.distance:5,a=this.shape,s=a.cx,l=a.cy,u=a.r,h=a.r0,d=(u+h)/2,f=a.startAngle,g=a.endAngle,y=(f+g)/2,v=p?Math.abs(u-h)/2:0,m=Math.cos,x=Math.sin,_=s+u*m(f),b=l+u*x(f),w="left",S="top";switch(r){case"startArc":_=s+(h-o)*m(y),b=l+(h-o)*x(y),w="center",S="top";break;case"insideStartArc":_=s+(h+o)*m(y),b=l+(h+o)*x(y),w="center",S="bottom";break;case"startAngle":_=s+d*m(f)+YS(f,o+v,!1),b=l+d*x(f)+XS(f,o+v,!1),w="right",S="middle";break;case"insideStartAngle":_=s+d*m(f)+YS(f,-o+v,!1),b=l+d*x(f)+XS(f,-o+v,!1),w="left",S="middle";break;case"middle":_=s+d*m(y),b=l+d*x(y),w="center",S="middle";break;case"endArc":_=s+(u+o)*m(y),b=l+(u+o)*x(y),w="center",S="bottom";break;case"insideEndArc":_=s+(u-o)*m(y),b=l+(u-o)*x(y),w="center",S="top";break;case"endAngle":_=s+d*m(g)+YS(g,o+v,!0),b=l+d*x(g)+XS(g,o+v,!0),w="left",S="middle";break;case"insideEndAngle":_=s+d*m(g)+YS(g,-o+v,!0),b=l+d*x(g)+XS(g,-o+v,!0),w="right",S="middle";break;default:return Tr(t,e,n)}return(t=t||{}).x=_,t.y=b,t.align=w,t.verticalAlign=S,t}),o){var f=r?"r":"endAngle",g={};h.shape[f]=r?i.r0:i.startAngle,g[f]=i[f],(s?fh:gh)(h,{shape:g},o)}return h}};function JS(t,e,n,i,r,o,a,s){var l,u;o?(u={x:i.x,width:i.width},l={y:i.y,height:i.height}):(u={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(a?fh:gh)(n,{shape:l},e,r,null),(a?fh:gh)(n,{shape:u},e?t.baseAxis.model:null,r)}function QS(t,e){for(var n=0;n0?1:-1,a=i.height>0?1:-1;return{x:i.x+o*r/2,y:i.y+a*r/2,width:i.width-o*r,height:i.height-a*r}},polar:function(t,e,n){var i=t.getItemLayout(e);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function rM(t){return function(t){var e=t?"Arc":"Angle";return function(t){switch(t){case"start":case"insideStart":case"end":case"insideEnd":return t+e;default:return t}}}(t)}function oM(t,e,n,i,r,o,a,s){var l=e.getItemVisual(n,"style");if(s){if(!o.get("roundCap")){var u=t.shape;A(u,US(i.getModel("itemStyle"),u,!0)),t.setShape(u)}}else{var h=i.get(["itemStyle","borderRadius"])||0;t.setShape("r",h)}t.useStyle(l);var c=i.getShallow("cursor");c&&t.attr("cursor",c);var p=s?a?r.r>=r.r0?"endArc":"startArc":r.endAngle>=r.startAngle?"endAngle":"startAngle":a?r.height>=0?"bottom":"top":r.width>=0?"right":"left",d=ec(i);tc(t,d,{labelFetcher:o,labelDataIndex:n,defaultText:iS(o.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:p});var f=t.getTextContent();if(s&&f){var g=i.get(["label","position"]);t.textConfig.inside="middle"===g||null,function(t,e,n,i){if(j(i))t.setTextConfig({rotation:i});else if(Y(e))t.setTextConfig({rotation:0});else{var r,o=t.shape,a=o.clockwise?o.startAngle:o.endAngle,s=o.clockwise?o.endAngle:o.startAngle,l=(a+s)/2,u=n(e);switch(u){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":r=l;break;case"startAngle":case"insideStartAngle":r=a;break;case"endAngle":case"insideEndAngle":r=s;break;default:return void t.setTextConfig({rotation:0})}var h=1.5*Math.PI-r;"middle"===u&&h>Math.PI/2&&h<1.5*Math.PI&&(h-=Math.PI),t.setTextConfig({rotation:h})}}(t,"outside"===g?p:g,rM(a),i.get(["label","rotate"]))}hc(f,d,o.getRawValue(n),(function(t){return rS(e,t)}));var y=i.getModel(["emphasis"]);Yl(t,y.get("focus"),y.get("blurScope"),y.get("disabled")),jl(t,i),function(t){return null!=t.startAngle&&null!=t.endAngle&&t.startAngle===t.endAngle}(r)&&(t.style.fill="none",t.style.stroke="none",E(t.states,(function(t){t.style&&(t.style.fill=t.style.stroke="none")})))}var aM=function(){},sM=function(t){function e(e){var n=t.call(this,e)||this;return n.type="largeBar",n}return n(e,t),e.prototype.getDefaultShape=function(){return new aM},e.prototype.buildPath=function(t,e){for(var n=e.points,i=this.baseDimIdx,r=1-this.baseDimIdx,o=[],a=[],s=this.barWidth,l=0;l=s[0]&&e<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return a[h]}return-1}(this,t.offsetX,t.offsetY);Qs(this).dataIndex=e>=0?e:null}),30,!1);function hM(t,e,n){if(MS(n,"cartesian2d")){var i=e,r=n.getArea();return{x:t?i.x:r.x,y:t?r.y:i.y,width:t?i.width:r.width,height:t?r.height:i.height}}var o=e;return{cx:(r=n.getArea()).cx,cy:r.cy,r0:t?r.r0:o.r0,r:t?r.r:o.r,startAngle:t?o.startAngle:0,endAngle:t?o.endAngle:2*Math.PI}}var cM=2*Math.PI,pM=Math.PI/180;function dM(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}function fM(t,e){var n=dM(t,e),i=t.get("center"),r=t.get("radius");Y(r)||(r=[0,r]);var o,a,s=Ur(n.width,e.getWidth()),l=Ur(n.height,e.getHeight()),u=Math.min(s,l),h=Ur(r[0],u/2),c=Ur(r[1],u/2),p=t.coordinateSystem;if(p){var d=p.dataToPoint(i);o=d[0]||0,a=d[1]||0}else Y(i)||(i=[i,i]),o=Ur(i[0],s)+n.x,a=Ur(i[1],l)+n.y;return{cx:o,cy:a,r0:h,r:c}}function gM(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.getData(),i=e.mapDimension("value"),r=dM(t,n),o=fM(t,n),a=o.cx,s=o.cy,l=o.r,u=o.r0,h=-t.get("startAngle")*pM,c=t.get("minAngle")*pM,p=0;e.each(i,(function(t){!isNaN(t)&&p++}));var d=e.getSum(i),f=Math.PI/(d||p)*2,g=t.get("clockwise"),y=t.get("roseType"),v=t.get("stillShowZeroSum"),m=e.getDataExtent(i);m[0]=0;var x=cM,_=0,b=h,w=g?1:-1;if(e.setLayout({viewRect:r,r:l}),e.each(i,(function(t,n){var i;if(isNaN(t))e.setItemLayout(n,{angle:NaN,startAngle:NaN,endAngle:NaN,clockwise:g,cx:a,cy:s,r0:u,r:y?NaN:l});else{(i="area"!==y?0===d&&v?f:t*f:cM/p)n?a:o,h=Math.abs(l.label.y-n);if(h>=u.maxY){var c=l.label.x-e-l.len2*r,p=i+l.len,f=Math.abs(c)t.unconstrainedWidth?null:d:null;i.setStyle("width",f)}var g=i.getBoundingRect();o.width=g.width;var y=(i.style.margin||0)+2.1;o.height=g.height+y,o.y-=(o.height-c)/2}}}function _M(t){return"center"===t.position}function bM(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*vM,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,h=s.x,c=s.y,p=s.height;function d(t){t.ignore=!0}i.each((function(t){var s=i.getItemGraphicEl(t),c=s.shape,p=s.getTextContent(),f=s.getTextGuideLine(),g=i.getItemModel(t),y=g.getModel("label"),v=y.get("position")||g.get(["emphasis","label","position"]),m=y.get("distanceToLabelLine"),x=y.get("alignTo"),_=Ur(y.get("edgeDistance"),u),b=y.get("bleedMargin"),w=g.getModel("labelLine"),S=w.get("length");S=Ur(S,u);var M=w.get("length2");if(M=Ur(M,u),Math.abs(c.endAngle-c.startAngle)0?"right":"left":k>0?"left":"right"}var B=Math.PI,F=0,G=y.get("rotate");if(j(G))F=G*(B/180);else if("center"===v)F=0;else if("radial"===G||!0===G){F=k<0?-A+B:-A}else if("tangential"===G&&"outside"!==v&&"outer"!==v){var W=Math.atan2(k,L);W<0&&(W=2*B+W),L>0&&(W=B+W),F=W-B}if(o=!!F,p.x=I,p.y=T,p.rotation=F,p.setStyle({verticalAlign:"middle"}),P){p.setStyle({align:D});var H=p.states.select;H&&(H.x+=p.x,H.y+=p.y)}else{var Y=p.getBoundingRect().clone();Y.applyTransform(p.getComputedTransform());var X=(p.style.margin||0)+2.1;Y.y-=X/2,Y.height+=X,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new De(k,L),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:x,edgeDistance:_,bleedMargin:b,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:p.style.width})}s.setTextConfig({inside:P})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p0){for(var l=o.getItemLayout(0),u=1;isNaN(l&&l.startAngle)&&u=n.r0}},e.type="pie",e}(kg);function MM(t,e,n){e=Y(e)&&{coordDimensions:e}||A({encodeDefine:t.getEncode()},e);var i=t.getSource(),r=ux(i,e).dimensions,o=new lx(r,t);return o.initData(i,n),o}var IM=function(){function t(t,e){this._getDataWithEncodedVisual=t,this._getRawData=e}return t.prototype.getAllNames=function(){var t=this._getRawData();return t.mapArray(t.getName)},t.prototype.containName=function(t){return this._getRawData().indexOfName(t)>=0},t.prototype.indexOfName=function(t){return this._getDataWithEncodedVisual().indexOfName(t)},t.prototype.getItemVisual=function(t,e){return this._getDataWithEncodedVisual().getItemVisual(t,e)},t}(),TM=Oo(),CM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new IM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.mergeOption=function(){t.prototype.mergeOption.apply(this,arguments)},e.prototype.getInitialData=function(){return MM(this,{coordDimensions:["value"],encodeDefaulter:H(Jp,this)})},e.prototype.getDataParams=function(e){var n=this.getData(),i=TM(n),r=i.seats;if(!r){var o=[];n.each(n.mapDimension("value"),(function(t){o.push(t)})),r=i.seats=Jr(o,n.hostModel.get("percentPrecision"))}var a=t.prototype.getDataParams.call(this,e);return a.percent=r[e]||0,a.$vars.push("percent"),a},e.prototype._defaultLabelLine=function(t){wo(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.type="series.pie",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",bleedMargin:10,distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:15,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},e}(mg);var DM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){return vx(null,this,{useEncodeDefaulter:!0})},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?5e3:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?1e4:this.get("progressiveThreshold"):t},e.prototype.brushSelector=function(t,e,n){return n.point(e.getItemLayout(t))},e.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},e.type="series.scatter",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:"#212121"}},universalTransition:{divideShape:"clone"}},e}(mg),AM=function(){},kM=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.getDefaultShape=function(){return new AM},e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.buildPath=function(t,e){var n,i=e.points,r=e.size,o=this.symbolProxy,a=o.shape,s=t.getContext?t.getContext():t,l=s&&r[0]<4,u=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,u=i[l]-o/2,h=i[l+1]-a/2;if(t>=u&&e>=h&&t<=u+o&&e<=h+a)return s}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape,n=e.points,i=e.size,r=i[0],o=i[1],a=1/0,s=1/0,l=-1/0,u=-1/0,h=0;h=0&&(l.dataIndex=n+(t.startIndex||0))}))},t.prototype.remove=function(){this._clear()},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),PM=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).updateData(i,{clipShape:this._getClipShape(t)}),this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).incrementalPrepareUpdate(i),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._symbolDraw.incrementalUpdate(t,e.getData(),{clipShape:this._getClipShape(e)}),this._finished=t.end===e.getData().count()},e.prototype.updateTransform=function(t,e,n){var i=t.getData();if(this.group.dirty(),!this._finished||i.count()>1e4)return{update:!0};var r=ES("").reset(t,e,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(i)},e.prototype.eachRendered=function(t){this._symbolDraw&&this._symbolDraw.eachRendered(t)},e.prototype._getClipShape=function(t){var e=t.coordinateSystem,n=e&&e.getArea&&e.getArea();return t.get("clip",!0)?n:null},e.prototype._updateSymbolDraw=function(t,e){var n=this._symbolDraw,i=e.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new LM:new hS,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},e.prototype.dispose=function(){},e.type="scatter",e}(kg),OM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.type="grid",e.dependencies=["xAxis","yAxis"],e.layoutMode="box",e.defaultOption={show:!1,z:0,left:"10%",top:60,right:"10%",bottom:70,containLabel:!1,backgroundColor:"rgba(0,0,0,0)",borderWidth:1,borderColor:"#ccc"},e}(Rp),RM=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",zo).models[0]},e.type="cartesian2dAxis",e}(Rp);R(RM,I_);var NM={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:!0,onZeroAxisIndex:null,lineStyle:{color:"#6E7079",width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15]},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12},splitLine:{show:!0,lineStyle:{color:["#E0E6F1"],width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:["rgba(250,250,250,0.2)","rgba(210,219,238,0.2)"]}}},EM=C({boundaryGap:!0,deduplication:null,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto"},axisLabel:{interval:"auto"}},NM),zM=C({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:"#F4F7FD",width:1}}},NM),VM={category:EM,value:zM,time:C({splitNumber:6,axisLabel:{showMinLabel:!1,showMaxLabel:!1,rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},zM),log:k({logBase:10},zM)},BM={value:1,category:1,time:1,log:1};function FM(t,e,i,r){E(BM,(function(o,a){var s=C(C({},VM[a],!0),r,!0),l=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e+"Axis."+a,n}return n(i,t),i.prototype.mergeDefaultAndTheme=function(t,e){var n=Ap(this),i=n?Lp(t):{};C(t,e.getTheme().get(a+"Axis")),C(t,this.getDefaultOption()),t.type=GM(t),n&&kp(t,i,n)},i.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=_x.createByAxisModel(this))},i.prototype.getCategories=function(t){var e=this.option;if("category"===e.type)return t?e.data:this.__ordinalMeta.categories},i.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},i.type=e+"Axis."+a,i.defaultOption=s,i}(i);t.registerComponentModel(l)})),t.registerSubTypeDefaulter(e+"Axis",GM)}function GM(t){return t.type||(t.data?"category":"value")}var WM=function(){function t(t){this.type="cartesian",this._dimList=[],this._axes={},this.name=t||""}return t.prototype.getAxis=function(t){return this._axes[t]},t.prototype.getAxes=function(){return z(this._dimList,(function(t){return this._axes[t]}),this)},t.prototype.getAxesByScale=function(t){return t=t.toLowerCase(),B(this.getAxes(),(function(e){return e.scale.type===t}))},t.prototype.addAxis=function(t){var e=t.dim;this._axes[e]=t,this._dimList.push(e)},t}(),HM=["x","y"];function YM(t){return"interval"===t.type||"time"===t.type}var XM=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="cartesian2d",e.dimensions=HM,e}return n(e,t),e.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var t=this.getAxis("x").scale,e=this.getAxis("y").scale;if(YM(t)&&YM(e)){var n=t.getExtent(),i=e.getExtent(),r=this.dataToPoint([n[0],i[0]]),o=this.dataToPoint([n[1],i[1]]),a=n[1]-n[0],s=i[1]-i[0];if(a&&s){var l=(o[0]-r[0])/a,u=(o[1]-r[1])/s,h=r[0]-n[0]*l,c=r[1]-i[0]*u,p=this._transform=[l,0,0,u,h,c];this._invTransform=Ie([],p)}}},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},e.prototype.containPoint=function(t){var e=this.getAxis("x"),n=this.getAxis("y");return e.contain(e.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},e.prototype.containData=function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},e.prototype.containZone=function(t,e){var n=this.dataToPoint(t),i=this.dataToPoint(e),r=this.getArea(),o=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]);return r.intersect(o)},e.prototype.dataToPoint=function(t,e,n){n=n||[];var i=t[0],r=t[1];if(this._transform&&null!=i&&isFinite(i)&&null!=r&&isFinite(r))return Wt(n,t,this._transform);var o=this.getAxis("x"),a=this.getAxis("y");return n[0]=o.toGlobalCoord(o.dataToCoord(i,e)),n[1]=a.toGlobalCoord(a.dataToCoord(r,e)),n},e.prototype.clampData=function(t,e){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,r=n.getExtent(),o=i.getExtent(),a=n.parse(t[0]),s=i.parse(t[1]);return(e=e||[])[0]=Math.min(Math.max(Math.min(r[0],r[1]),a),Math.max(r[0],r[1])),e[1]=Math.min(Math.max(Math.min(o[0],o[1]),s),Math.max(o[0],o[1])),e},e.prototype.pointToData=function(t,e){var n=[];if(this._invTransform)return Wt(n,t,this._invTransform);var i=this.getAxis("x"),r=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(t[0]),e),n[1]=r.coordToData(r.toLocalCoord(t[1]),e),n},e.prototype.getOtherAxis=function(t){return this.getAxis("x"===t.dim?"y":"x")},e.prototype.getArea=function(){var t=this.getAxis("x").getGlobalExtent(),e=this.getAxis("y").getGlobalExtent(),n=Math.min(t[0],t[1]),i=Math.min(e[0],e[1]),r=Math.max(t[0],t[1])-n,o=Math.max(e[0],e[1])-i;return new ze(n,i,r,o)},e}(WM),UM=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.index=0,a.type=r||"value",a.position=o||"bottom",a}return n(e,t),e.prototype.isHorizontal=function(){var t=this.position;return"top"===t||"bottom"===t},e.prototype.getGlobalExtent=function(t){var e=this.getExtent();return e[0]=this.toGlobalCoord(e[0]),e[1]=this.toGlobalCoord(e[1]),t&&e[0]>e[1]&&e.reverse(),e},e.prototype.pointToData=function(t,e){return this.coordToData(this.toLocalCoord(t["x"===this.dim?0:1]),e)},e.prototype.setCategorySortInfo=function(t){if("category"!==this.type)return!1;this.model.option.categorySortInfo=t,this.scale.setSortInfo(t)},e}(nb);function ZM(t,e,n){n=n||{};var i=t.coordinateSystem,r=e.axis,o={},a=r.getAxesOnZeroOf()[0],s=r.position,l=a?"onZero":s,u=r.dim,h=i.getRect(),c=[h.x,h.x+h.width,h.y,h.y+h.height],p={left:0,right:1,top:0,bottom:1,onZero:2},d=e.get("offset")||0,f="x"===u?[c[2]-d,c[3]+d]:[c[0]-d,c[1]+d];if(a){var g=a.toGlobalCoord(a.dataToCoord(0));f[p.onZero]=Math.max(Math.min(g,f[1]),f[0])}o.position=["y"===u?f[p[l]]:c[0],"x"===u?f[p[l]]:c[3]],o.rotation=Math.PI/2*("x"===u?0:1);o.labelDirection=o.tickDirection=o.nameDirection={top:-1,bottom:1,left:-1,right:1}[s],o.labelOffset=a?f[p[s]]-f[p.onZero]:0,e.get(["axisTick","inside"])&&(o.tickDirection=-o.tickDirection),it(n.labelInside,e.get(["axisLabel","inside"]))&&(o.labelDirection=-o.labelDirection);var y=e.get(["axisLabel","rotate"]);return o.labelRotate="top"===l?-y:y,o.z2=1,o}function jM(t){return"cartesian2d"===t.get("coordinateSystem")}function qM(t){var e={xAxisModel:null,yAxisModel:null};return E(e,(function(n,i){var r=i.replace(/Model$/,""),o=t.getReferringComponents(r,zo).models[0];e[i]=o})),e}var KM=Math.log;function $M(t,e,n){var i=Ox.prototype,r=i.getTicks.call(n),o=i.getTicks.call(n,!0),a=r.length-1,s=i.getInterval.call(n),l=y_(t,e),u=l.extent,h=l.fixMin,c=l.fixMax;if("log"===t.type){var p=KM(t.base);u=[KM(u[0])/p,KM(u[1])/p]}t.setExtent(u[0],u[1]),t.calcNiceExtent({splitNumber:a,fixMin:h,fixMax:c});var d=i.getExtent.call(t);h&&(u[0]=d[0]),c&&(u[1]=d[1]);var f=i.getInterval.call(t),g=u[0],y=u[1];if(h&&c)f=(y-g)/a;else if(h)for(y=u[0]+f*a;yu[0]&&isFinite(g)&&isFinite(u[0]);)f=Ix(f),g=u[1]-f*a;else{t.getTicks().length-1>a&&(f=Ix(f));var v=f*a;(g=Zr((y=Math.ceil(u[1]/f)*f)-v))<0&&u[0]>=0?(g=0,y=Zr(v)):y>0&&u[1]<=0&&(y=0,g=-Zr(v))}var m=(r[0].value-o[0].value)/s,x=(r[a].value-o[a].value)/s;i.setExtent.call(t,g+f*m,y+f*x),i.setInterval.call(t,f),(m||x)&&i.setNiceExtent.call(t,g+f,y-f)}var JM=function(){function t(t,e,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=HM,this._initCartesian(t,e,n),this.model=t}return t.prototype.getRect=function(){return this._rect},t.prototype.update=function(t,e){var n=this._axesMap;function i(t){var e,n=G(t),i=n.length;if(i){for(var r=[],o=i-1;o>=0;o--){var a=t[+n[o]],s=a.model,l=a.scale;Sx(l)&&s.get("alignTicks")&&null==s.get("interval")?r.push(a):(v_(l,s),Sx(l)&&(e=a))}r.length&&(e||v_((e=r.pop()).scale,e.model),E(r,(function(t){$M(t.scale,t.model,e.scale)})))}}this._updateScale(t,this.model),i(n.x),i(n.y);var r={};E(n.x,(function(t){tI(n,"y",t,r)})),E(n.y,(function(t){tI(n,"x",t,r)})),this.resize(this.model,e)},t.prototype.resize=function(t,e,n){var i=t.getBoxLayoutParams(),r=!n&&t.get("containLabel"),o=Cp(i,{width:e.getWidth(),height:e.getHeight()});this._rect=o;var a=this._axesList;function s(){E(a,(function(t){var e=t.isHorizontal(),n=e?[0,o.width]:[0,o.height],i=t.inverse?1:0;t.setExtent(n[i],n[1-i]),function(t,e){var n=t.getExtent(),i=n[0]+n[1];t.toGlobalCoord="x"===t.dim?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord="x"===t.dim?function(t){return t-e}:function(t){return i-t+e}}(t,e?o.x:o.y)}))}s(),r&&(E(a,(function(t){if(!t.model.get(["axisLabel","inside"])){var e=function(t){var e=t.model,n=t.scale;if(e.get(["axisLabel","show"])&&!n.isBlank()){var i,r,o=n.getExtent();r=n instanceof Lx?n.count():(i=n.getTicks()).length;var a,s=t.getLabelModel(),l=x_(t),u=1;r>40&&(u=Math.ceil(r/40));for(var h=0;h0&&i>0||n<0&&i<0)}(t)}var nI=Math.PI,iI=function(){function t(t,e){this.group=new zr,this.opt=e,this.axisModel=t,k(e,{labelOffset:0,nameDirection:1,tickDirection:1,labelDirection:1,silent:!0,handleAutoShown:function(){return!0}});var n=new zr({x:e.position[0],y:e.position[1],rotation:e.rotation});n.updateTransform(),this._transformGroup=n}return t.prototype.hasBuilder=function(t){return!!rI[t]},t.prototype.add=function(t){rI[t](this.opt,this.axisModel,this.group,this._transformGroup)},t.prototype.getGroup=function(){return this.group},t.innerTextLayout=function(t,e,n){var i,r,o=eo(e-t);return no(o)?(r=n>0?"top":"bottom",i="center"):no(o-nI)?(r=n>0?"bottom":"top",i="center"):(r="middle",i=o>0&&o0?"right":"left":n>0?"left":"right"),{rotation:o,textAlign:i,textVerticalAlign:r}},t.makeAxisEventDataBase=function(t){var e={componentType:t.mainType,componentIndex:t.componentIndex};return e[t.mainType+"Index"]=t.componentIndex,e},t.isLabelSilent=function(t){var e=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||e&&e.show)},t}(),rI={axisLine:function(t,e,n,i){var r=e.get(["axisLine","show"]);if("auto"===r&&t.handleAutoShown&&(r=t.handleAutoShown("axisLine")),r){var o=e.axis.getExtent(),a=i.transform,s=[o[0],0],l=[o[1],0],u=s[0]>l[0];a&&(Wt(s,s,a),Wt(l,l,a));var h=A({lineCap:"round"},e.getModel(["axisLine","lineStyle"]).getLineStyle()),c=new Zu({shape:{x1:s[0],y1:s[1],x2:l[0],y2:l[1]},style:h,strokeContainThreshold:t.strokeContainThreshold||5,silent:!0,z2:1});Rh(c.shape,c.style.lineWidth),c.anid="line",n.add(c);var p=e.get(["axisLine","symbol"]);if(null!=p){var d=e.get(["axisLine","symbolSize"]);U(p)&&(p=[p,p]),(U(d)||j(d))&&(d=[d,d]);var f=Yy(e.get(["axisLine","symbolOffset"])||0,d),g=d[0],y=d[1];E([{rotate:t.rotation+Math.PI/2,offset:f[0],r:0},{rotate:t.rotation-Math.PI/2,offset:f[1],r:Math.sqrt((s[0]-l[0])*(s[0]-l[0])+(s[1]-l[1])*(s[1]-l[1]))}],(function(e,i){if("none"!==p[i]&&null!=p[i]){var r=Wy(p[i],-g/2,-y/2,g,y,h.stroke,!0),o=e.r+e.offset,a=u?l:s;r.attr({rotation:e.rotate,x:a[0]+o*Math.cos(t.rotation),y:a[1]-o*Math.sin(t.rotation),silent:!0,z2:11}),n.add(r)}}))}}},axisTickLabel:function(t,e,n,i){var r=function(t,e,n,i){var r=n.axis,o=n.getModel("axisTick"),a=o.get("show");"auto"===a&&i.handleAutoShown&&(a=i.handleAutoShown("axisTick"));if(!a||r.scale.isBlank())return;for(var s=o.getModel("lineStyle"),l=i.tickDirection*o.get("length"),u=lI(r.getTicksCoords(),e.transform,l,k(s.getLineStyle(),{stroke:n.get(["axisLine","lineStyle","color"])}),"ticks"),h=0;hc[1]?-1:1,d=["start"===s?c[0]-p*h:"end"===s?c[1]+p*h:(c[0]+c[1])/2,sI(s)?t.labelOffset+l*h:0],f=e.get("nameRotate");null!=f&&(f=f*nI/180),sI(s)?o=iI.innerTextLayout(t.rotation,null!=f?f:t.rotation,l):(o=function(t,e,n,i){var r,o,a=eo(n-t),s=i[0]>i[1],l="start"===e&&!s||"start"!==e&&s;no(a-nI/2)?(o=l?"bottom":"top",r="center"):no(a-1.5*nI)?(o=l?"top":"bottom",r="center"):(o="middle",r=a<1.5*nI&&a>nI/2?l?"left":"right":l?"right":"left");return{rotation:a,textAlign:r,textVerticalAlign:o}}(t.rotation,s,f||0,c),null!=(a=t.axisNameAvailableWidth)&&(a=Math.abs(a/Math.sin(o.rotation)),!isFinite(a)&&(a=null)));var g=u.getFont(),y=e.get("nameTruncate",!0)||{},v=y.ellipsis,m=it(t.nameTruncateMaxWidth,y.maxWidth,a),x=new Fs({x:d[0],y:d[1],rotation:o.rotation,silent:iI.isLabelSilent(e),style:nc(u,{text:r,font:g,overflow:"truncate",width:m,ellipsis:v,fill:u.getTextColor()||e.get(["axisLine","lineStyle","color"]),align:u.get("align")||o.textAlign,verticalAlign:u.get("verticalAlign")||o.textVerticalAlign}),z2:1});if(Zh({el:x,componentModel:e,itemName:r}),x.__fullText=r,x.anid="name",e.get("triggerEvent")){var _=iI.makeAxisEventDataBase(e);_.targetType="axisName",_.name=r,Qs(x).eventData=_}i.add(x),x.updateTransform(),n.add(x),x.decomposeTransform()}}};function oI(t){t&&(t.ignore=!0)}function aI(t,e){var n=t&&t.getBoundingRect().clone(),i=e&&e.getBoundingRect().clone();if(n&&i){var r=xe([]);return Se(r,r,-t.rotation),n.applyTransform(be([],r,t.getLocalTransform())),i.applyTransform(be([],r,e.getLocalTransform())),n.intersect(i)}}function sI(t){return"middle"===t||"center"===t}function lI(t,e,n,i,r){for(var o=[],a=[],s=[],l=0;l=0||t===e}function cI(t){var e=pI(t);if(e){var n=e.axisPointerModel,i=e.axis.scale,r=n.option,o=n.get("status"),a=n.get("value");null!=a&&(a=i.parse(a));var s=dI(n);null==o&&(r.status=s?"show":"hide");var l=i.getExtent().slice();l[0]>l[1]&&l.reverse(),(null==a||a>l[1])&&(a=l[1]),a0&&!c.min?c.min=0:null!=c.min&&c.min<0&&!c.max&&(c.max=0);var p=a;null!=c.color&&(p=k({color:c.color},a));var d=C(T(c),{boundaryGap:t,splitNumber:e,scale:n,axisLine:i,axisTick:r,axisLabel:o,name:c.text,showName:s,nameLocation:"end",nameGap:u,nameTextStyle:p,triggerEvent:h},!1);if(U(l)){var f=d.name;d.name=l.replace("{value}",null!=f?f:"")}else X(l)&&(d.name=l(d.name,d));var g=new Mc(d,null,this.ecModel);return R(g,I_.prototype),g.mainType="radar",g.componentIndex=this.componentIndex,g}),this);this._indicatorModels=c},e.prototype.getIndicatorModels=function(){return this._indicatorModels},e.type="radar",e.defaultOption={z:0,center:["50%","50%"],radius:"75%",startAngle:90,axisName:{show:!0},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:C({lineStyle:{color:"#bbb"}},NI.axisLine),axisLabel:EI(NI.axisLabel,!1),axisTick:EI(NI.axisTick,!1),splitLine:EI(NI.splitLine,!0),splitArea:EI(NI.splitArea,!0),indicator:[]},e}(Rp),VI=["axisLine","axisTickLabel","axisName"],BI=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll(),this._buildAxes(t),this._buildSplitLineAndArea(t)},e.prototype._buildAxes=function(t){var e=t.coordinateSystem;E(z(e.getIndicatorAxes(),(function(t){var n=t.model.get("showName")?t.name:"";return new iI(t.model,{axisName:n,position:[e.cx,e.cy],rotation:t.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})})),(function(t){E(VI,t.add,t),this.group.add(t.getGroup())}),this)},e.prototype._buildSplitLineAndArea=function(t){var e=t.coordinateSystem,n=e.getIndicatorAxes();if(n.length){var i=t.get("shape"),r=t.getModel("splitLine"),o=t.getModel("splitArea"),a=r.getModel("lineStyle"),s=o.getModel("areaStyle"),l=r.get("show"),u=o.get("show"),h=a.get("color"),c=s.get("color"),p=Y(h)?h:[h],d=Y(c)?c:[c],f=[],g=[];if("circle"===i)for(var y=n[0].getTicksCoords(),v=e.cx,m=e.cy,x=0;x3?1.4:r>1?1.2:1.1;ZI(this,"zoom","zoomOnMouseWheel",t,{scale:i>0?s:1/s,originX:o,originY:a,isAvailableBehavior:null})}if(n){var l=Math.abs(i);ZI(this,"scrollMove","moveOnMouseWheel",t,{scrollDelta:(i>0?1:-1)*(l>3?.4:l>1?.15:.05),originX:o,originY:a,isAvailableBehavior:null})}}},e.prototype._pinchHandler=function(t){YI(this._zr,"globalPan")||ZI(this,"zoom",null,t,{scale:t.pinchScale>1?1.1:1/1.1,originX:t.pinchX,originY:t.pinchY,isAvailableBehavior:null})},e}(jt);function ZI(t,e,n,i,r){t.pointerChecker&&t.pointerChecker(i,r.originX,r.originY)&&(de(i.event),jI(t,e,n,i,r))}function jI(t,e,n,i,r){r.isAvailableBehavior=W(qI,null,n,i),t.trigger(e,r)}function qI(t,e,n){var i=n[t];return!t||i&&(!U(i)||e.event[i+"Key"])}function KI(t,e,n){var i=t.target;i.x+=e,i.y+=n,i.dirty()}function $I(t,e,n,i){var r=t.target,o=t.zoomLimit,a=t.zoom=t.zoom||1;if(a*=e,o){var s=o.min||0,l=o.max||1/0;a=Math.max(Math.min(l,a),s)}var u=a/t.zoom;t.zoom=a,r.x-=(n-r.x)*(u-1),r.y-=(i-r.y)*(u-1),r.scaleX*=u,r.scaleY*=u,r.dirty()}var JI,QI={axisPointer:1,tooltip:1,brush:1};function tT(t,e,n){var i=e.getComponentByElement(t.topTarget),r=i&&i.coordinateSystem;return i&&i!==n&&!QI.hasOwnProperty(i.mainType)&&r&&r.model!==n}function eT(t){U(t)&&(t=(new DOMParser).parseFromString(t,"text/xml"));var e=t;for(9===e.nodeType&&(e=e.firstChild);"svg"!==e.nodeName.toLowerCase()||1!==e.nodeType;)e=e.nextSibling;return e}var nT={fill:"fill",stroke:"stroke","stroke-width":"lineWidth",opacity:"opacity","fill-opacity":"fillOpacity","stroke-opacity":"strokeOpacity","stroke-dasharray":"lineDash","stroke-dashoffset":"lineDashOffset","stroke-linecap":"lineCap","stroke-linejoin":"lineJoin","stroke-miterlimit":"miterLimit","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","text-anchor":"textAlign",visibility:"visibility",display:"display"},iT=G(nT),rT={"alignment-baseline":"textBaseline","stop-color":"stopColor"},oT=G(rT),aT=function(){function t(){this._defs={},this._root=null}return t.prototype.parse=function(t,e){e=e||{};var n=eT(t);this._defsUsePending=[];var i=new zr;this._root=i;var r=[],o=n.getAttribute("viewBox")||"",a=parseFloat(n.getAttribute("width")||e.width),s=parseFloat(n.getAttribute("height")||e.height);isNaN(a)&&(a=null),isNaN(s)&&(s=null),pT(n,i,null,!0,!1);for(var l,u,h=n.firstChild;h;)this._parseNode(h,i,r,null,!1,!1),h=h.nextSibling;if(function(t,e){for(var n=0;n=4&&(l={x:parseFloat(c[0]||0),y:parseFloat(c[1]||0),width:parseFloat(c[2]),height:parseFloat(c[3])})}if(l&&null!=a&&null!=s&&(u=bT(l,{x:0,y:0,width:a,height:s}),!e.ignoreViewBox)){var p=i;(i=new zr).add(p),p.scaleX=p.scaleY=u.scale,p.x=u.x,p.y=u.y}return e.ignoreRootClip||null==a||null==s||i.setClipPath(new zs({shape:{x:0,y:0,width:a,height:s}})),{root:i,width:a,height:s,viewBoxRect:l,viewBoxTransform:u,named:r}},t.prototype._parseNode=function(t,e,n,i,r,o){var a,s=t.nodeName.toLowerCase(),l=i;if("defs"===s&&(r=!0),"text"===s&&(o=!0),"defs"===s||"switch"===s)a=e;else{if(!r){var u=JI[s];if(u&&_t(JI,s)){a=u.call(this,t,e);var h=t.getAttribute("name");if(h){var c={name:h,namedFrom:null,svgNodeTagLower:s,el:a};n.push(c),"g"===s&&(l=c)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:a});e.add(a)}}var p=sT[s];if(p&&_t(sT,s)){var d=p.call(this,t),f=t.getAttribute("id");f&&(this._defs[f]=d)}}if(a&&a.isGroup)for(var g=t.firstChild;g;)1===g.nodeType?this._parseNode(g,a,n,l,r,o):3===g.nodeType&&o&&this._parseText(g,a),g=g.nextSibling},t.prototype._parseText=function(t,e){var n=new Cs({style:{text:t.textContent},silent:!0,x:this._textX||0,y:this._textY||0});hT(e,n),pT(t,n,this._defsUsePending,!1,!1),function(t,e){var n=e.__selfStyle;if(n){var i=n.textBaseline,r=i;i&&"auto"!==i?"baseline"===i?r="alphabetic":"before-edge"===i||"text-before-edge"===i?r="top":"after-edge"===i||"text-after-edge"===i?r="bottom":"central"!==i&&"mathematical"!==i||(r="middle"):r="alphabetic",t.style.textBaseline=r}var o=e.__inheritedStyle;if(o){var a=o.textAlign,s=a;a&&("middle"===a&&(s="center"),t.style.textAlign=s)}}(n,e);var i=n.style,r=i.fontSize;r&&r<9&&(i.fontSize=9,n.scaleX*=r/9,n.scaleY*=r/9);var o=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=o;var a=n.getBoundingRect();return this._textX+=a.width,e.add(n),n},t.internalField=void(JI={g:function(t,e){var n=new zr;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n},rect:function(t,e){var n=new zs;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(t.getAttribute("x")||"0"),y:parseFloat(t.getAttribute("y")||"0"),width:parseFloat(t.getAttribute("width")||"0"),height:parseFloat(t.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(t,e){var n=new _u;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),r:parseFloat(t.getAttribute("r")||"0")}),n.silent=!0,n},line:function(t,e){var n=new Zu;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(t.getAttribute("x1")||"0"),y1:parseFloat(t.getAttribute("y1")||"0"),x2:parseFloat(t.getAttribute("x2")||"0"),y2:parseFloat(t.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(t,e){var n=new wu;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),rx:parseFloat(t.getAttribute("rx")||"0"),ry:parseFloat(t.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(t,e){var n,i=t.getAttribute("points");i&&(n=cT(i));var r=new Wu({shape:{points:n||[]},silent:!0});return hT(e,r),pT(t,r,this._defsUsePending,!1,!1),r},polyline:function(t,e){var n,i=t.getAttribute("points");i&&(n=cT(i));var r=new Yu({shape:{points:n||[]},silent:!0});return hT(e,r),pT(t,r,this._defsUsePending,!1,!1),r},image:function(t,e){var n=new ks;return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.setStyle({image:t.getAttribute("xlink:href")||t.getAttribute("href"),x:+t.getAttribute("x"),y:+t.getAttribute("y"),width:+t.getAttribute("width"),height:+t.getAttribute("height")}),n.silent=!0,n},text:function(t,e){var n=t.getAttribute("x")||"0",i=t.getAttribute("y")||"0",r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(r),this._textY=parseFloat(i)+parseFloat(o);var a=new zr;return hT(e,a),pT(t,a,this._defsUsePending,!1,!0),a},tspan:function(t,e){var n=t.getAttribute("x"),i=t.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0",a=new zr;return hT(e,a),pT(t,a,this._defsUsePending,!1,!0),this._textX+=parseFloat(r),this._textY+=parseFloat(o),a},path:function(t,e){var n=vu(t.getAttribute("d")||"");return hT(e,n),pT(t,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),t}(),sT={lineargradient:function(t){var e=parseInt(t.getAttribute("x1")||"0",10),n=parseInt(t.getAttribute("y1")||"0",10),i=parseInt(t.getAttribute("x2")||"10",10),r=parseInt(t.getAttribute("y2")||"0",10),o=new nh(e,n,i,r);return lT(t,o),uT(t,o),o},radialgradient:function(t){var e=parseInt(t.getAttribute("cx")||"0",10),n=parseInt(t.getAttribute("cy")||"0",10),i=parseInt(t.getAttribute("r")||"0",10),r=new ih(e,n,i);return lT(t,r),uT(t,r),r}};function lT(t,e){"userSpaceOnUse"===t.getAttribute("gradientUnits")&&(e.global=!0)}function uT(t,e){for(var n=t.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),r=void 0;r=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var o={};_T(n,o,o);var a=o.stopColor||n.getAttribute("stop-color")||"#000000";e.colorStops.push({offset:r,color:a})}n=n.nextSibling}}function hT(t,e){t&&t.__inheritedStyle&&(e.__inheritedStyle||(e.__inheritedStyle={}),k(e.__inheritedStyle,t.__inheritedStyle))}function cT(t){for(var e=yT(t),n=[],i=0;i0;o-=2){var a=i[o],s=i[o-1],l=yT(a);switch(r=r||[1,0,0,1,0,0],s){case"translate":we(r,r,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":Me(r,r,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":Se(r,r,-parseFloat(l[0])*mT);break;case"skewX":be(r,[1,0,Math.tan(parseFloat(l[0])*mT),1,0,0],r);break;case"skewY":be(r,[1,Math.tan(parseFloat(l[0])*mT),0,1,0,0],r);break;case"matrix":r[0]=parseFloat(l[0]),r[1]=parseFloat(l[1]),r[2]=parseFloat(l[2]),r[3]=parseFloat(l[3]),r[4]=parseFloat(l[4]),r[5]=parseFloat(l[5])}}e.setLocalTransform(r)}}(t,e),_T(t,a,s),i||function(t,e,n){for(var i=0;i0,f={api:n,geo:s,mapOrGeoModel:t,data:a,isVisualEncodedByVisualMap:d,isGeo:o,transformInfoRaw:c};"geoJSON"===s.resourceType?this._buildGeoJSON(f):"geoSVG"===s.resourceType&&this._buildSVG(f),this._updateController(t,e,n),this._updateMapSelectHandler(t,l,n,i)},t.prototype._buildGeoJSON=function(t){var e=this._regionsGroupByName=yt(),n=yt(),i=this._regionsGroup,r=t.transformInfoRaw,o=t.mapOrGeoModel,a=t.data,s=t.geo.projection,l=s&&s.stream;function u(t,e){return e&&(t=e(t)),t&&[t[0]*r.scaleX+r.x,t[1]*r.scaleY+r.y]}function h(t){for(var e=[],n=!l&&s&&s.project,i=0;i=0)&&(p=r);var d=a?{normal:{align:"center",verticalAlign:"middle"}}:null;tc(e,ec(i),{labelFetcher:p,labelDataIndex:c,defaultText:n},d);var f=e.getTextContent();if(f&&(WT(f).ignore=f.ignore,e.textConfig&&a)){var g=e.getBoundingRect().clone();e.textConfig.layoutRect=g,e.textConfig.position=[(a[0]-g.x)/g.width*100+"%",(a[1]-g.y)/g.height*100+"%"]}e.disableLabelAnimation=!0}else e.removeTextContent(),e.removeTextConfig(),e.disableLabelAnimation=null}function jT(t,e,n,i,r,o){t.data?t.data.setItemGraphicEl(o,e):Qs(e).eventData={componentType:"geo",componentIndex:r.componentIndex,geoIndex:r.componentIndex,name:n,region:i&&i.option||{}}}function qT(t,e,n,i,r){t.data||Zh({el:e,componentModel:r,itemName:n,itemTooltipOption:i.get("tooltip")})}function KT(t,e,n,i,r){e.highDownSilentOnTouch=!!r.get("selectedMode");var o=i.getModel("emphasis"),a=o.get("focus");return Yl(e,a,o.get("blurScope"),o.get("disabled")),t.isGeo&&function(t,e,n){var i=Qs(t);i.componentMainType=e.mainType,i.componentIndex=e.componentIndex,i.componentHighDownName=n}(e,r,n),a}function $T(t,e,n){var i,r=[];function o(){i=[]}function a(){i.length&&(r.push(i),i=[])}var s=e({polygonStart:o,polygonEnd:a,lineStart:o,lineEnd:a,point:function(t,e){isFinite(t)&&isFinite(e)&&i.push([t,e])},sphere:function(){}});return!n&&s.polygonStart(),E(t,(function(t){s.lineStart();for(var e=0;e-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2),n},e.type="series.map",e.dependencies=["geo"],e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:"#000"},itemStyle:{borderWidth:.5,borderColor:"#444",areaColor:"#eee"},emphasis:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{areaColor:"rgba(255,215,0,0.8)"}},select:{label:{show:!0,color:"rgb(100,0,0)"},itemStyle:{color:"rgba(255,215,0,0.8)"}},nameProperty:"name"},e}(mg);function tC(t){var e={};t.eachSeriesByType("map",(function(t){var n=t.getHostGeoModel(),i=n?"o"+n.id:"i"+t.getMapType();(e[i]=e[i]||[]).push(t)})),E(e,(function(t,e){for(var n,i,r,o=(n=z(t,(function(t){return t.getData()})),i=t[0].get("mapValueCalculation"),r={},E(n,(function(t){t.each(t.mapDimension("value"),(function(e,n){var i="ec-"+t.getName(n);r[i]=r[i]||[],isNaN(e)||r[i].push(e)}))})),n[0].map(n[0].mapDimension("value"),(function(t,e){for(var o="ec-"+n[0].getName(e),a=0,s=1/0,l=-1/0,u=r[o].length,h=0;h1?(d.width=p,d.height=p/x):(d.height=p,d.width=p*x),d.y=c[1]-d.height/2,d.x=c[0]-d.width/2;else{var b=t.getBoxLayoutParams();b.aspect=x,d=Cp(b,{width:v,height:m})}this.setViewRect(d.x,d.y,d.width,d.height),this.setCenter(t.get("center"),e),this.setZoom(t.get("zoom"))}R(sC,iC);var hC=function(){function t(){this.dimensions=aC}return t.prototype.create=function(t,e){var n=[];function i(t){return{nameProperty:t.get("nameProperty"),aspectScale:t.get("aspectScale"),projection:t.get("projection")}}t.eachComponent("geo",(function(t,r){var o=t.get("map"),a=new sC(o+r,o,A({nameMap:t.get("nameMap")},i(t)));a.zoomLimit=t.get("scaleLimit"),n.push(a),t.coordinateSystem=a,a.model=t,a.resize=uC,a.resize(t,e)})),t.eachSeries((function(t){if("geo"===t.get("coordinateSystem")){var e=t.get("geoIndex")||0;t.coordinateSystem=n[e]}}));var r={};return t.eachSeriesByType("map",(function(t){if(!t.getHostGeoModel()){var e=t.getMapType();r[e]=r[e]||[],r[e].push(t)}})),E(r,(function(t,r){var o=z(t,(function(t){return t.get("nameMap")})),a=new sC(r,r,A({nameMap:D(o)},i(t[0])));a.zoomLimit=it.apply(null,z(t,(function(t){return t.get("scaleLimit")}))),n.push(a),a.resize=uC,a.resize(t[0],e),E(t,(function(t){t.coordinateSystem=a,function(t,e){E(e.get("geoCoord"),(function(e,n){t.addGeoCoord(n,e)}))}(a,t)}))})),n},t.prototype.getFilledRegions=function(t,e,n,i){for(var r=(t||[]).slice(),o=yt(),a=0;a=0;){var o=e[n];o.hierNode.prelim+=i,o.hierNode.modifier+=i,r+=o.hierNode.change,i+=o.hierNode.shift+r}}(t);var o=(n[0].hierNode.prelim+n[n.length-1].hierNode.prelim)/2;r?(t.hierNode.prelim=r.hierNode.prelim+e(t,r),t.hierNode.modifier=t.hierNode.prelim-o):t.hierNode.prelim=o}else r&&(t.hierNode.prelim=r.hierNode.prelim+e(t,r));t.parentNode.hierNode.defaultAncestor=function(t,e,n,i){if(e){for(var r=t,o=t,a=o.parentNode.children[0],s=e,l=r.hierNode.modifier,u=o.hierNode.modifier,h=a.hierNode.modifier,c=s.hierNode.modifier;s=wC(s),o=SC(o),s&&o;){r=wC(r),a=SC(a),r.hierNode.ancestor=t;var p=s.hierNode.prelim+c-o.hierNode.prelim-u+i(s,o);p>0&&(IC(MC(s,t,n),t,p),u+=p,l+=p),c+=s.hierNode.modifier,u+=o.hierNode.modifier,l+=r.hierNode.modifier,h+=a.hierNode.modifier}s&&!wC(r)&&(r.hierNode.thread=s,r.hierNode.modifier+=c-l),o&&!SC(a)&&(a.hierNode.thread=o,a.hierNode.modifier+=u-h,n=t)}return n}(t,r,t.parentNode.hierNode.defaultAncestor||i[0],e)}function xC(t){var e=t.hierNode.prelim+t.parentNode.hierNode.modifier;t.setLayout({x:e},!0),t.hierNode.modifier+=t.parentNode.hierNode.modifier}function _C(t){return arguments.length?t:TC}function bC(t,e){return t-=Math.PI/2,{x:e*Math.cos(t),y:e*Math.sin(t)}}function wC(t){var e=t.children;return e.length&&t.isExpand?e[e.length-1]:t.hierNode.thread}function SC(t){var e=t.children;return e.length&&t.isExpand?e[0]:t.hierNode.thread}function MC(t,e,n){return t.hierNode.ancestor.parentNode===e.parentNode?t.hierNode.ancestor:n}function IC(t,e,n){var i=n/(e.hierNode.i-t.hierNode.i);e.hierNode.change-=i,e.hierNode.shift+=n,e.hierNode.modifier+=n,e.hierNode.prelim+=n,t.hierNode.change+=i}function TC(t,e){return t.parentNode===e.parentNode?1:2}var CC=function(){this.parentPoint=[],this.childPoints=[]},DC=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new CC},e.prototype.buildPath=function(t,e){var n=e.childPoints,i=n.length,r=e.parentPoint,o=n[0],a=n[i-1];if(1===i)return t.moveTo(r[0],r[1]),void t.lineTo(o[0],o[1]);var s=e.orient,l="TB"===s||"BT"===s?0:1,u=1-l,h=Ur(e.forkPosition,1),c=[];c[l]=r[l],c[u]=r[u]+(a[u]-r[u])*h,t.moveTo(r[0],r[1]),t.lineTo(c[0],c[1]),t.moveTo(o[0],o[1]),c[l]=o[l],t.lineTo(c[0],c[1]),c[l]=a[l],t.lineTo(c[0],c[1]),t.lineTo(a[0],a[1]);for(var p=1;pm.x)||(_-=Math.PI);var S=b?"left":"right",M=s.getModel("label"),I=M.get("rotate"),T=I*(Math.PI/180),C=y.getTextContent();C&&(y.setTextConfig({position:M.get("position")||S,rotation:null==I?-_:T,origin:"center"}),C.setStyle("verticalAlign","middle"))}var D=s.get(["emphasis","focus"]),A="relative"===D?vt(a.getAncestorsIndices(),a.getDescendantIndices()):"ancestor"===D?a.getAncestorsIndices():"descendant"===D?a.getDescendantIndices():null;A&&(Qs(n).focus=A),function(t,e,n,i,r,o,a,s){var l=e.getModel(),u=t.get("edgeShape"),h=t.get("layout"),c=t.getOrient(),p=t.get(["lineStyle","curveness"]),d=t.get("edgeForkPosition"),f=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===u)e.parentNode&&e.parentNode!==n&&(g||(g=i.__edge=new $u({shape:NC(h,c,p,r,r)})),fh(g,{shape:NC(h,c,p,o,a)},t));else if("polyline"===u)if("orthogonal"===h){if(e!==n&&e.children&&0!==e.children.length&&!0===e.isExpand){for(var y=e.children,v=[],m=0;me&&(e=i.height)}this.height=e+1},t.prototype.getNodeById=function(t){if(this.getId()===t)return this;for(var e=0,n=this.children,i=n.length;e=0&&this.hostTree.data.setItemLayout(this.dataIndex,t,e)},t.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(t)},t.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},t.prototype.setVisual=function(t,e){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,t,e)},t.prototype.getVisual=function(t){return this.hostTree.data.getItemVisual(this.dataIndex,t)},t.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},t.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},t.prototype.getChildIndex=function(){if(this.parentNode){for(var t=this.parentNode.children,e=0;e=0){var i=n.getData().tree.root,r=t.targetNode;if(U(r)&&(r=i.getNodeById(r)),r&&i.contains(r))return{node:r};var o=t.targetNodeId;if(null!=o&&(r=i.getNodeById(o)))return{node:r}}}function jC(t){for(var e=[];t;)(t=t.parentNode)&&e.push(t);return e.reverse()}function qC(t,e){return P(jC(t),e)>=0}function KC(t,e){for(var n=[];t;){var i=t.dataIndex;n.push({name:t.name,dataIndex:i,value:e.getRawValue(i)}),t=t.parentNode}return n.reverse(),n}var $C=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.hasSymbolVisual=!0,e.ignoreStyleOnData=!0,e}return n(e,t),e.prototype.getInitialData=function(t){var e={name:t.name,children:t.data},n=t.leaves||{},i=new Mc(n,this,this.ecModel),r=UC.createTree(e,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e);return n&&n.children.length&&n.isExpand||(t.parentModel=i),t}))}));var o=0;r.eachNode("preorder",(function(t){t.depth>o&&(o=t.depth)}));var a=t.expandAndCollapse&&t.initialTreeDepth>=0?t.initialTreeDepth:o;return r.root.eachNode("preorder",(function(t){var e=t.hostTree.data.getRawDataItem(t.dataIndex);t.isExpand=e&&null!=e.collapsed?!e.collapsed:t.depth<=a})),r.data},e.prototype.getOrient=function(){var t=this.get("orient");return"horizontal"===t?t="LR":"vertical"===t&&(t="TB"),t},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.formatTooltip=function(t,e,n){for(var i=this.getData().tree,r=i.root.children[0],o=i.getNodeByDataIndex(t),a=o.getValue(),s=o.name;o&&o!==r;)s=o.parentNode.name+"."+s,o=o.parentNode;return ng("nameValue",{name:s,value:a,noValue:isNaN(a)||null==a})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=KC(i,this),n.collapsed=!i.isExpand,n},e.type="series.tree",e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"view",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:"#ccc",width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500},e}(mg);function JC(t,e){for(var n,i=[t];n=i.pop();)if(e(n),n.isExpand){var r=n.children;if(r.length)for(var o=r.length-1;o>=0;o--)i.push(r[o])}}function QC(t,e){t.eachSeriesByType("tree",(function(t){!function(t,e){var n=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=n;var i=t.get("layout"),r=0,o=0,a=null;"radial"===i?(r=2*Math.PI,o=Math.min(n.height,n.width)/2,a=_C((function(t,e){return(t.parentNode===e.parentNode?1:2)/t.depth}))):(r=n.width,o=n.height,a=_C());var s=t.getData().tree.root,l=s.children[0];if(l){!function(t){var e=t;e.hierNode={defaultAncestor:null,ancestor:e,prelim:0,modifier:0,change:0,shift:0,i:0,thread:null};for(var n,i,r=[e];n=r.pop();)if(i=n.children,n.isExpand&&i.length)for(var o=i.length-1;o>=0;o--){var a=i[o];a.hierNode={defaultAncestor:null,ancestor:a,prelim:0,modifier:0,change:0,shift:0,i:o,thread:null},r.push(a)}}(s),function(t,e,n){for(var i,r=[t],o=[];i=r.pop();)if(o.push(i),i.isExpand){var a=i.children;if(a.length)for(var s=0;sh.getLayout().x&&(h=t),t.depth>c.depth&&(c=t)}));var p=u===h?1:a(u,h)/2,d=p-u.getLayout().x,f=0,g=0,y=0,v=0;if("radial"===i)f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),JC(l,(function(t){y=(t.getLayout().x+d)*f,v=(t.depth-1)*g;var e=bC(y,v);t.setLayout({x:e.x,y:e.y,rawX:y,rawY:v},!0)}));else{var m=t.getOrient();"RL"===m||"LR"===m?(g=o/(h.getLayout().x+p+d),f=r/(c.depth-1||1),JC(l,(function(t){v=(t.getLayout().x+d)*g,y="LR"===m?(t.depth-1)*f:r-(t.depth-1)*f,t.setLayout({x:y,y:v},!0)}))):"TB"!==m&&"BT"!==m||(f=r/(h.getLayout().x+p+d),g=o/(c.depth-1||1),JC(l,(function(t){y=(t.getLayout().x+d)*f,v="TB"===m?(t.depth-1)*g:o-(t.depth-1)*g,t.setLayout({x:y,y:v},!0)})))}}}(t,e)}))}function tD(t){t.eachSeriesByType("tree",(function(t){var e=t.getData();e.tree.eachNode((function(t){var n=t.getModel().getModel("itemStyle").getItemStyle();A(e.ensureUniqueItemVisual(t.dataIndex,"style"),n)}))}))}var eD=["treemapZoomToNode","treemapRender","treemapMove"];function nD(t){var e=t.getData().tree,n={};e.eachNode((function(e){for(var i=e;i&&i.depth>1;)i=i.parentNode;var r=ud(t.ecModel,i.name||i.dataIndex+"",n);e.setVisual("decal",r)}))}var iD=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventUsingHoverLayer=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};rD(n);var i=t.levels||[],r=this.designatedVisualItemStyle={},o=new Mc({itemStyle:r},this,e);i=t.levels=function(t,e){var n,i,r=bo(e.get("color")),o=bo(e.get(["aria","decal","decals"]));if(!r)return;t=t||[],E(t,(function(t){var e=new Mc(t),r=e.get("color"),o=e.get("decal");(e.get(["itemStyle","color"])||r&&"none"!==r)&&(n=!0),(e.get(["itemStyle","decal"])||o&&"none"!==o)&&(i=!0)}));var a=t[0]||(t[0]={});n||(a.color=r.slice());!i&&o&&(a.decal=o.slice());return t}(i,e);var a=z(i||[],(function(t){return new Mc(t,o,e)}),this),s=UC.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=s.getNodeByDataIndex(e),i=n?a[n.depth]:null;return t.parentModel=i||o,t}))}));return s.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.getRawValue(t);return ng("nameValue",{name:i.getName(t),value:r})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=KC(i,this),n.treePathInfo=n.treeAncestors,n},e.prototype.setLayoutInfo=function(t){this.layoutInfo=this.layoutInfo||{},A(this.layoutInfo,t)},e.prototype.mapIdToIndex=function(t){var e=this._idIndexMap;e||(e=this._idIndexMap=yt(),this._idIndexMapCount=0);var n=e.get(t);return null==n&&e.set(t,n=this._idIndexMapCount++),n},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){nD(this)},e.type="series.treemap",e.layoutMode="box",e.defaultOption={progressive:0,left:"center",top:"middle",width:"80%",height:"80%",sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,roam:!0,nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",top:"bottom",emptyItemWidth:25,itemStyle:{color:"rgba(0,0,0,0.7)",textStyle:{color:"#fff"}},emphasis:{itemStyle:{color:"rgba(0,0,0,0.9)"}}},label:{show:!0,distance:0,padding:5,position:"inside",color:"#fff",overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:"#fff",borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},e}(mg);function rD(t){var e=0;E(t.children,(function(t){rD(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var oD=function(){function t(t){this.group=new zr,t.add(this.group)}return t.prototype.render=function(t,e,n,i){var r=t.getModel("breadcrumb"),o=this.group;if(o.removeAll(),r.get("show")&&n){var a=r.getModel("itemStyle"),s=r.getModel("emphasis"),l=a.getModel("textStyle"),u=s.getModel(["itemStyle","textStyle"]),h={pos:{left:r.get("left"),right:r.get("right"),top:r.get("top"),bottom:r.get("bottom")},box:{width:e.getWidth(),height:e.getHeight()},emptyItemWidth:r.get("emptyItemWidth"),totalWidth:0,renderList:[]};this._prepare(n,h,l),this._renderContent(t,h,a,s,l,u,i),Dp(o,h.pos,h.box)}},t.prototype._prepare=function(t,e,n){for(var i=t;i;i=i.parentNode){var r=Ao(i.getModel().get("name"),""),o=n.getTextRect(r),a=Math.max(o.width+16,e.emptyItemWidth);e.totalWidth+=a+8,e.renderList.push({node:i,text:r,width:a})}},t.prototype._renderContent=function(t,e,n,i,r,o,a){for(var s,l,u,h,c,p,d,f,g,y=0,v=e.emptyItemWidth,m=t.get(["breadcrumb","height"]),x=(s=e.pos,l=e.box,h=l.width,c=l.height,p=Ur(s.left,h),d=Ur(s.top,c),f=Ur(s.right,h),g=Ur(s.bottom,c),(isNaN(p)||isNaN(parseFloat(s.left)))&&(p=0),(isNaN(f)||isNaN(parseFloat(s.right)))&&(f=h),(isNaN(d)||isNaN(parseFloat(s.top)))&&(d=0),(isNaN(g)||isNaN(parseFloat(s.bottom)))&&(g=c),u=fp(u||0),{width:Math.max(f-p-u[1]-u[3],0),height:Math.max(g-d-u[0]-u[2],0)}),_=e.totalWidth,b=e.renderList,w=i.getModel("itemStyle").getItemStyle(),S=b.length-1;S>=0;S--){var M=b[S],I=M.node,T=M.width,C=M.text;_>x.width&&(_-=T-v,T=v,C=null);var D=new Wu({shape:{points:aD(y,0,T,m,S===b.length-1,0===S)},style:k(n.getItemStyle(),{lineJoin:"bevel"}),textContent:new Fs({style:nc(r,{text:C})}),textConfig:{position:"inside"},z2:1e5,onclick:H(a,I)});D.disableLabelAnimation=!0,D.getTextContent().ensureState("emphasis").style=nc(o,{text:C}),D.ensureState("emphasis").style=w,Yl(D,i.get("focus"),i.get("blurScope"),i.get("disabled")),this.group.add(D),sD(D,t,I),y+=T+8}},t.prototype.remove=function(){this.group.removeAll()},t}();function aD(t,e,n,i,r,o){var a=[[r?t:t-5,e],[t+n,e],[t+n,e+i],[r?t:t-5,e+i]];return!o&&a.splice(2,0,[t+n+5,e+i/2]),!r&&a.push([t,e+i/2]),a}function sD(t,e,n){Qs(t).eventData={componentType:"series",componentSubType:"treemap",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&KC(n,e)}}var lD=function(){function t(){this._storage=[],this._elExistsMap={}}return t.prototype.add=function(t,e,n,i,r){return!this._elExistsMap[t.id]&&(this._elExistsMap[t.id]=!0,this._storage.push({el:t,target:e,duration:n,delay:i,easing:r}),!0)},t.prototype.finished=function(t){return this._finishedCallback=t,this},t.prototype.start=function(){for(var t=this,e=this._storage.length,n=function(){--e<=0&&(t._storage.length=0,t._elExistsMap={},t._finishedCallback&&t._finishedCallback())},i=0,r=this._storage.length;i3||Math.abs(t.dy)>3)){var e=this.seriesModel.getData().tree.root;if(!e)return;var n=e.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+t.dx,y:n.y+t.dy,width:n.width,height:n.height}})}},e.prototype._onZoom=function(t){var e=t.originX,n=t.originY;if("animating"!==this._state){var i=this.seriesModel.getData().tree.root;if(!i)return;var r=i.getLayout();if(!r)return;var o=new ze(r.x,r.y,r.width,r.height),a=this.seriesModel.layoutInfo,s=[1,0,0,1,0,0];we(s,s,[-(e-=a.x),-(n-=a.y)]),Me(s,s,[t.scale,t.scale]),we(s,s,[e,n]),o.applyTransform(s),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:o.x,y:o.y,width:o.width,height:o.height}})}},e.prototype._initEvents=function(t){var e=this;t.on("click",(function(t){if("ready"===e._state){var n=e.seriesModel.get("nodeClick",!0);if(n){var i=e.findTarget(t.offsetX,t.offsetY);if(i){var r=i.node;if(r.getLayout().isLeafRoot)e._rootToNode(i);else if("zoomToNode"===n)e._zoomToNode(i);else if("link"===n){var o=r.hostTree.data.getItemModel(r.dataIndex),a=o.get("link",!0),s=o.get("target",!0)||"blank";a&&bp(a,s)}}}}}),this)},e.prototype._renderBreadcrumb=function(t,e,n){var i=this;n||(n=null!=t.get("leafDepth",!0)?{node:t.getViewRoot()}:this.findTarget(e.getWidth()/2,e.getHeight()/2))||(n={node:t.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new oD(this.group))).render(t,e,n.node,(function(e){"animating"!==i._state&&(qC(t.getViewRoot(),e)?i._rootToNode({node:e}):i._zoomToNode({node:e}))}))},e.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},e.prototype.dispose=function(){this._clearController()},e.prototype._zoomToNode=function(t){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype._rootToNode=function(t){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype.findTarget=function(t,e){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},(function(i){var r=this._storage.background[i.getRawIndex()];if(r){var o=r.transformCoordToLocal(t,e),a=r.shape;if(!(a.x<=o[0]&&o[0]<=a.x+a.width&&a.y<=o[1]&&o[1]<=a.y+a.height))return!1;n={node:i,offsetX:o[0],offsetY:o[1]}}}),this),n},e.type="treemap",e}(kg);var vD=E,mD=q,xD=-1,_D=function(){function t(e){var n=e.mappingMethod,i=e.type,r=this.option=T(e);this.type=i,this.mappingMethod=n,this._normalizeData=kD[n];var o=t.visualHandlers[i];this.applyVisual=o.applyVisual,this.getColorMapper=o.getColorMapper,this._normalizedToVisual=o._normalizedToVisual[n],"piecewise"===n?(bD(r),function(t){var e=t.pieceList;t.hasSpecialVisual=!1,E(e,(function(e,n){e.originIndex=n,null!=e.visual&&(t.hasSpecialVisual=!0)}))}(r)):"category"===n?r.categories?function(t){var e=t.categories,n=t.categoryMap={},i=t.visual;if(vD(e,(function(t,e){n[t]=e})),!Y(i)){var r=[];q(i)?vD(i,(function(t,e){var i=n[e];r[null!=i?i:xD]=t})):r[-1]=i,i=AD(t,r)}for(var o=e.length-1;o>=0;o--)null==i[o]&&(delete n[e[o]],e.pop())}(r):bD(r,!0):(lt("linear"!==n||r.dataExtent),bD(r))}return t.prototype.mapValueToVisual=function(t){var e=this._normalizeData(t);return this._normalizedToVisual(e,t)},t.prototype.getNormalizer=function(){return W(this._normalizeData,this)},t.listVisualTypes=function(){return G(t.visualHandlers)},t.isValidType=function(e){return t.visualHandlers.hasOwnProperty(e)},t.eachVisual=function(t,e,n){q(t)?E(t,e,n):e.call(n,t)},t.mapVisual=function(e,n,i){var r,o=Y(e)?[]:q(e)?{}:(r=!0,null);return t.eachVisual(e,(function(t,e){var a=n.call(i,t,e);r?o=a:o[e]=a})),o},t.retrieveVisuals=function(e){var n,i={};return e&&vD(t.visualHandlers,(function(t,r){e.hasOwnProperty(r)&&(i[r]=e[r],n=!0)})),n?i:null},t.prepareVisualTypes=function(t){if(Y(t))t=t.slice();else{if(!mD(t))return[];var e=[];vD(t,(function(t,n){e.push(n)})),t=e}return t.sort((function(t,e){return"color"===e&&"color"!==t&&0===t.indexOf("color")?1:-1})),t},t.dependsOn=function(t,e){return"color"===e?!(!t||0!==t.indexOf(e)):t===e},t.findPieceIndex=function(t,e,n){for(var i,r=1/0,o=0,a=e.length;ou[1]&&(u[1]=l);var h=e.get("colorMappingBy"),c={type:a.name,dataExtent:u,visual:a.range};"color"!==c.type||"index"!==h&&"id"!==h?c.mappingMethod="linear":(c.mappingMethod="category",c.loop=!0);var p=new _D(c);return PD(p).drColorMappingBy=h,p}(0,r,o,0,u,d);E(d,(function(t,e){if(t.depth>=n.length||t===n[t.depth]){var o=function(t,e,n,i,r,o){var a=A({},e);if(r){var s=r.type,l="color"===s&&PD(r).drColorMappingBy,u="index"===l?i:"id"===l?o.mapIdToIndex(n.getId()):n.getValue(t.get("visualDimension"));a[s]=r.mapValueToVisual(u)}return a}(r,u,t,e,f,i);RD(t,o,n,i)}}))}else s=ND(u),h.fill=s}}function ND(t){var e=ED(t,"color");if(e){var n=ED(t,"colorAlpha"),i=ED(t,"colorSaturation");return i&&(e=ni(e,null,null,i)),n&&(e=ii(e,n)),e}}function ED(t,e){var n=t[e];if(null!=n&&"none"!==n)return n}function zD(t,e){var n=t.get(e);return Y(n)&&n.length?{name:e,range:n}:null}var VD=Math.max,BD=Math.min,FD=it,GD=E,WD=["itemStyle","borderWidth"],HD=["itemStyle","gapWidth"],YD=["upperLabel","show"],XD=["upperLabel","height"],UD={seriesType:"treemap",reset:function(t,e,n,i){var r=n.getWidth(),o=n.getHeight(),a=t.option,s=Cp(t.getBoxLayoutParams(),{width:n.getWidth(),height:n.getHeight()}),l=a.size||[],u=Ur(FD(s.width,l[0]),r),h=Ur(FD(s.height,l[1]),o),c=i&&i.type,p=ZC(i,["treemapZoomToNode","treemapRootToNode"],t),d="treemapRender"===c||"treemapMove"===c?i.rootRect:null,f=t.getViewRoot(),g=jC(f);if("treemapMove"!==c){var y="treemapZoomToNode"===c?function(t,e,n,i,r){var o,a=(e||{}).node,s=[i,r];if(!a||a===n)return s;var l=i*r,u=l*t.option.zoomToNodeRatio;for(;o=a.parentNode;){for(var h=0,c=o.children,p=0,d=c.length;pto&&(u=to),a=o}ua[1]&&(a[1]=e)}))):a=[NaN,NaN];return{sum:i,dataExtent:a}}(e,a,s);if(0===u.sum)return t.viewChildren=[];if(u.sum=function(t,e,n,i,r){if(!i)return n;for(var o=t.get("visibleMin"),a=r.length,s=a,l=a-1;l>=0;l--){var u=r["asc"===i?a-l-1:l].getValue();u/n*ei&&(i=a));var l=t.area*t.area,u=e*e*n;return l?VD(u*i/l,l/(u*r)):1/0}function qD(t,e,n,i,r){var o=e===n.width?0:1,a=1-o,s=["x","y"],l=["width","height"],u=n[s[o]],h=e?t.area/e:0;(r||h>n[l[a]])&&(h=n[l[a]]);for(var c=0,p=t.length;ci&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a0&&(m[0]=-m[0],m[1]=-m[1]);var _=v[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var b=-Math.atan2(v[1],v[0]);u[0].8?"left":h[0]<-.8?"right":"center",p=h[1]>.8?"top":h[1]<-.8?"bottom":"middle";break;case"start":i.x=-h[0]*f+l[0],i.y=-h[1]*g+l[1],c=h[0]>.8?"right":h[0]<-.8?"left":"center",p=h[1]>.8?"bottom":h[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=f*_+l[0],i.y=l[1]+w,c=v[0]<0?"right":"left",i.originX=-f*_,i.originY=-w;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+w,c="center",i.originY=-w;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-f*_+u[0],i.y=u[1]+w,c=v[0]>=0?"right":"left",i.originX=f*_,i.originY=-w}i.scaleX=i.scaleY=r,i.setStyle({verticalAlign:i.__verticalAlign||p,align:i.__align||c})}}}function S(t,e){var n=t.__specifiedRotation;if(null==n){var i=a.tangentAt(e);t.attr("rotation",(1===e?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else t.attr("rotation",n)}},e}(zr),RA=function(){function t(t){this.group=new zr,this._LineCtor=t||OA}return t.prototype.updateData=function(t){var e=this;this._progressiveEls=null;var n=this,i=n.group,r=n._lineData;n._lineData=t,r||i.removeAll();var o=NA(t);t.diff(r).add((function(n){e._doAdd(t,n,o)})).update((function(n,i){e._doUpdate(r,t,i,n,o)})).remove((function(t){i.remove(r.getItemGraphicEl(t))})).execute()},t.prototype.updateLayout=function(){var t=this._lineData;t&&t.eachItemGraphicEl((function(e,n){e.updateLayout(t,n)}),this)},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=NA(t),this._lineData=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e){function n(t){t.isGroup||function(t){return t.animators&&t.animators.length>0}(t)||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}this._progressiveEls=[];for(var i=t.start;i=0?i+=u:i-=u:f>=0?i-=u:i+=u}return i}function XA(t,e){var n=[],i=Dn,r=[[],[],[]],o=[[],[]],a=[];e/=2,t.eachEdge((function(t,s){var l=t.getLayout(),u=t.getVisual("fromSymbol"),h=t.getVisual("toSymbol");l.__original||(l.__original=[Tt(l[0]),Tt(l[1])],l[2]&&l.__original.push(Tt(l[2])));var c=l.__original;if(null!=l[2]){if(It(r[0],c[0]),It(r[1],c[2]),It(r[2],c[1]),u&&"none"!==u){var p=dA(t.node1),d=YA(r,c[0],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[0][0]=n[3],r[1][0]=n[4],i(r[0][1],r[1][1],r[2][1],d,n),r[0][1]=n[3],r[1][1]=n[4]}if(h&&"none"!==h){p=dA(t.node2),d=YA(r,c[1],p*e);i(r[0][0],r[1][0],r[2][0],d,n),r[1][0]=n[1],r[2][0]=n[2],i(r[0][1],r[1][1],r[2][1],d,n),r[1][1]=n[1],r[2][1]=n[2]}It(l[0],r[0]),It(l[1],r[2]),It(l[2],r[1])}else{if(It(o[0],c[0]),It(o[1],c[1]),kt(a,o[1],o[0]),Et(a,a),u&&"none"!==u){p=dA(t.node1);At(o[0],o[0],a,p*e)}if(h&&"none"!==h){p=dA(t.node2);At(o[1],o[1],a,-p*e)}It(l[0],o[0]),It(l[1],o[1])}}))}function UA(t){return"view"===t.type}var ZA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){var n=new hS,i=new RA,r=this.group;this._controller=new UI(e.getZr()),this._controllerHost={target:r},r.add(n.group),r.add(i.group),this._symbolDraw=n,this._lineDraw=i,this._firstRender=!0},e.prototype.render=function(t,e,n){var i=this,r=t.coordinateSystem;this._model=t;var o=this._symbolDraw,a=this._lineDraw,s=this.group;if(UA(r)){var l={x:r.x,y:r.y,scaleX:r.scaleX,scaleY:r.scaleY};this._firstRender?s.attr(l):fh(s,l,t)}XA(t.getGraph(),pA(t));var u=t.getData();o.updateData(u);var h=t.getEdgeData();a.updateData(h),this._updateNodeAndLinkScale(),this._updateController(t,e,n),clearTimeout(this._layoutTimeout);var c=t.forceLayout,p=t.get(["force","layoutAnimation"]);c&&this._startForceLayoutIteration(c,p);var d=t.get("layout");u.graph.eachNode((function(e){var n=e.dataIndex,r=e.getGraphicEl(),o=e.getModel();if(r){r.off("drag").off("dragend");var a=o.get("draggable");a&&r.on("drag",(function(o){switch(d){case"force":c.warmUp(),!i._layouting&&i._startForceLayoutIteration(c,p),c.setFixed(n),u.setItemLayout(n,[r.x,r.y]);break;case"circular":u.setItemLayout(n,[r.x,r.y]),e.setLayout({fixed:!0},!0),yA(t,"symbolSize",e,[o.offsetX,o.offsetY]),i.updateLayout(t);break;default:u.setItemLayout(n,[r.x,r.y]),hA(t.getGraph(),t),i.updateLayout(t)}})).on("dragend",(function(){c&&c.setUnfixed(n)})),r.setDraggable(a,!!o.get("cursor")),"adjacency"===o.get(["emphasis","focus"])&&(Qs(r).focus=e.getAdjacentDataIndices())}})),u.graph.eachEdge((function(t){var e=t.getGraphicEl(),n=t.getModel().get(["emphasis","focus"]);e&&"adjacency"===n&&(Qs(e).focus={edge:[t.dataIndex],node:[t.node1.dataIndex,t.node2.dataIndex]})}));var f="circular"===t.get("layout")&&t.get(["circular","rotateLabel"]),g=u.getLayout("cx"),y=u.getLayout("cy");u.graph.eachNode((function(t){mA(t,f,g,y)})),this._firstRender=!1},e.prototype.dispose=function(){this._controller&&this._controller.dispose(),this._controllerHost=null},e.prototype._startForceLayoutIteration=function(t,e){var n=this;!function i(){t.step((function(t){n.updateLayout(n._model),(n._layouting=!t)&&(e?n._layoutTimeout=setTimeout(i,16):i())}))}()},e.prototype._updateController=function(t,e,n){var i=this,r=this._controller,o=this._controllerHost,a=this.group;r.setPointerChecker((function(e,i,r){var o=a.getBoundingRect();return o.applyTransform(a.transform),o.contain(i,r)&&!tT(e,n,t)})),UA(t.coordinateSystem)?(r.enable(t.get("roam")),o.zoomLimit=t.get("scaleLimit"),o.zoom=t.coordinateSystem.getZoom(),r.off("pan").off("zoom").on("pan",(function(e){KI(o,e.dx,e.dy),n.dispatchAction({seriesId:t.id,type:"graphRoam",dx:e.dx,dy:e.dy})})).on("zoom",(function(e){$I(o,e.scale,e.originX,e.originY),n.dispatchAction({seriesId:t.id,type:"graphRoam",zoom:e.scale,originX:e.originX,originY:e.originY}),i._updateNodeAndLinkScale(),XA(t.getGraph(),pA(t)),i._lineDraw.updateLayout(),n.updateLabelLayout()}))):r.disable()},e.prototype._updateNodeAndLinkScale=function(){var t=this._model,e=t.getData(),n=pA(t);e.eachItemGraphicEl((function(t,e){t&&t.setSymbolScale(n)}))},e.prototype.updateLayout=function(t){XA(t.getGraph(),pA(t)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout()},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove()},e.type="graph",e}(kg);function jA(t){return"_EC_"+t}var qA=function(){function t(t){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=t||!1}return t.prototype.isDirected=function(){return this._directed},t.prototype.addNode=function(t,e){t=null==t?""+e:""+t;var n=this._nodesMap;if(!n[jA(t)]){var i=new KA(t,e);return i.hostGraph=this,this.nodes.push(i),n[jA(t)]=i,i}},t.prototype.getNodeByIndex=function(t){var e=this.data.getRawIndex(t);return this.nodes[e]},t.prototype.getNodeById=function(t){return this._nodesMap[jA(t)]},t.prototype.addEdge=function(t,e,n){var i=this._nodesMap,r=this._edgesMap;if(j(t)&&(t=this.nodes[t]),j(e)&&(e=this.nodes[e]),t instanceof KA||(t=i[jA(t)]),e instanceof KA||(e=i[jA(e)]),t&&e){var o=t.id+"-"+e.id,a=new $A(t,e,n);return a.hostGraph=this,this._directed&&(t.outEdges.push(a),e.inEdges.push(a)),t.edges.push(a),t!==e&&e.edges.push(a),this.edges.push(a),r[o]=a,a}},t.prototype.getEdgeByIndex=function(t){var e=this.edgeData.getRawIndex(t);return this.edges[e]},t.prototype.getEdge=function(t,e){t instanceof KA&&(t=t.id),e instanceof KA&&(e=e.id);var n=this._edgesMap;return this._directed?n[t+"-"+e]:n[t+"-"+e]||n[e+"-"+t]},t.prototype.eachNode=function(t,e){for(var n=this.nodes,i=n.length,r=0;r=0&&t.call(e,n[r],r)},t.prototype.eachEdge=function(t,e){for(var n=this.edges,i=n.length,r=0;r=0&&n[r].node1.dataIndex>=0&&n[r].node2.dataIndex>=0&&t.call(e,n[r],r)},t.prototype.breadthFirstTraverse=function(t,e,n,i){if(e instanceof KA||(e=this._nodesMap[jA(e)]),e){for(var r="out"===n?"outEdges":"in"===n?"inEdges":"edges",o=0;o=0&&n.node2.dataIndex>=0}));for(r=0,o=i.length;r=0&&this[t][e].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[t][e].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[t][e].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[t][e].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[t][e].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[t][e].getRawIndex(this.dataIndex)}}}function QA(t,e,n,i,r){for(var o=new qA(i),a=0;a "+p)),u++)}var d,f=n.get("coordinateSystem");if("cartesian2d"===f||"polar"===f)d=vx(t,n);else{var g=xd.get(f),y=g&&g.dimensions||[];P(y,"value")<0&&y.concat(["value"]);var v=ux(t,{coordDimensions:y,encodeDefine:n.getEncode()}).dimensions;(d=new lx(v,n)).initData(t)}var m=new lx(["value"],n);return m.initData(l,s),r&&r(d,m),zC({mainData:d,struct:o,structAttr:"graph",datas:{node:d,edge:m},datasAttr:{node:"data",edge:"edgeData"}}),o.update(),o}R(KA,JA("hostGraph","data")),R($A,JA("hostGraph","edgeData"));var tk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments);var n=this;function i(){return n._categoriesData}this.legendVisualProvider=new IM(i,i),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this.fillDataTextStyle(e.edges||e.links),this._updateCategoriesData()},e.prototype.mergeDefaultAndTheme=function(e){t.prototype.mergeDefaultAndTheme.apply(this,arguments),wo(e,"edgeLabel",["show"])},e.prototype.getInitialData=function(t,e){var n,i=t.edges||t.links||[],r=t.data||t.nodes||[],o=this;if(r&&i){iA(n=this)&&(n.__curvenessList=[],n.__edgeMap={},rA(n));var a=QA(r,i,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t){var e=o._categoriesModels[t.getShallow("category")];return e&&(e.parentModel=t.parentModel,t.parentModel=e),t}));var n=Mc.prototype.getModel;function i(t,e){var i=n.call(this,t,e);return i.resolveParentPath=r,i}function r(t){if(t&&("label"===t[0]||"label"===t[1])){var e=t.slice();return"label"===t[0]?e[0]="edgeLabel":"label"===t[1]&&(e[1]="edgeLabel"),e}return t}e.wrapMethod("getItemModel",(function(t){return t.resolveParentPath=r,t.getModel=i,t}))}));return E(a.edges,(function(t){!function(t,e,n,i){if(iA(n)){var r=oA(t,e,n),o=n.__edgeMap,a=o[aA(r)];o[r]&&!a?o[r].isForward=!0:a&&o[r]&&(a.isForward=!0,o[r].isForward=!1),o[r]=o[r]||[],o[r].push(i)}}(t.node1,t.node2,this,t.dataIndex)}),this),a.data}},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.getCategoriesData=function(){return this._categoriesData},e.prototype.formatTooltip=function(t,e,n){if("edge"===n){var i=this.getData(),r=this.getDataParams(t,n),o=i.graph.getEdgeByIndex(t),a=i.getName(o.node1.dataIndex),s=i.getName(o.node2.dataIndex),l=[];return null!=a&&l.push(a),null!=s&&l.push(s),ng("nameValue",{name:l.join(" > "),value:r.value,noValue:null==r.value})}return fg({series:this,dataIndex:t,multipleSeries:e})},e.prototype._updateCategoriesData=function(){var t=z(this.option.categories||[],(function(t){return null!=t.value?t:A({value:0},t)})),e=new lx(["value"],this);e.initData(t),this._categoriesData=e,this._categoriesModels=e.mapArray((function(t){return e.getItemModel(t)}))},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.isAnimationEnabled=function(){return t.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},e.type="series.graph",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:"#aaa",width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(mg),ek={type:"graphRoam",event:"graphRoam",update:"none"};var nk=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},ik=function(t){function e(e){var n=t.call(this,e)||this;return n.type="pointer",n}return n(e,t),e.prototype.getDefaultShape=function(){return new nk},e.prototype.buildPath=function(t,e){var n=Math.cos,i=Math.sin,r=e.r,o=e.width,a=e.angle,s=e.x-n(a)*o*(o>=r/3?1:2),l=e.y-i(a)*o*(o>=r/3?1:2);a=e.angle-Math.PI/2,t.moveTo(s,l),t.lineTo(e.x+n(a)*o,e.y+i(a)*o),t.lineTo(e.x+n(e.angle)*r,e.y+i(e.angle)*r),t.lineTo(e.x-n(a)*o,e.y-i(a)*o),t.lineTo(s,l)},e}(Is);function rk(t,e){var n=null==t?"":t+"";return e&&(U(e)?n=e.replace("{value}",n):X(e)&&(n=e(t))),n}var ok=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll();var i=t.get(["axisLine","lineStyle","color"]),r=function(t,e){var n=t.get("center"),i=e.getWidth(),r=e.getHeight(),o=Math.min(i,r);return{cx:Ur(n[0],e.getWidth()),cy:Ur(n[1],e.getHeight()),r:Ur(t.get("radius"),o/2)}}(t,n);this._renderMain(t,e,n,i,r),this._data=t.getData()},e.prototype.dispose=function(){},e.prototype._renderMain=function(t,e,n,i,r){var o=this.group,a=t.get("clockwise"),s=-t.get("startAngle")/180*Math.PI,l=-t.get("endAngle")/180*Math.PI,u=t.getModel("axisLine"),h=u.get("roundCap")?HS:zu,c=u.get("show"),p=u.getModel("lineStyle"),d=p.get("width"),f=[s,l];rs(f,!a);for(var g=(l=f[1])-(s=f[0]),y=s,v=[],m=0;c&&m=t&&(0===e?0:i[e-1][0])Math.PI/2&&(V+=Math.PI):"tangential"===z?V=-M-Math.PI/2:j(z)&&(V=z*Math.PI/180),0===V?c.add(new Fs({style:nc(x,{text:O,x:N,y:E,verticalAlign:h<-.8?"top":h>.8?"bottom":"middle",align:u<-.4?"left":u>.4?"right":"center"},{inheritColor:R}),silent:!0})):c.add(new Fs({style:nc(x,{text:O,x:N,y:E,verticalAlign:"middle",align:"center"},{inheritColor:R}),silent:!0,originX:N,originY:E,rotation:V}))}if(m.get("show")&&k!==_){P=(P=m.get("distance"))?P+l:l;for(var B=0;B<=b;B++){u=Math.cos(M),h=Math.sin(M);var F=new Zu({shape:{x1:u*(f-P)+p,y1:h*(f-P)+d,x2:u*(f-S-P)+p,y2:h*(f-S-P)+d},silent:!0,style:D});"auto"===D.stroke&&F.setStyle({stroke:i((k+B/b)/_)}),c.add(F),M+=T}M-=T}else M+=I}},e.prototype._renderPointer=function(t,e,n,i,r,o,a,s,l){var u=this.group,h=this._data,c=this._progressEls,p=[],d=t.get(["pointer","show"]),f=t.getModel("progress"),g=f.get("show"),y=t.getData(),v=y.mapDimension("value"),m=+t.get("min"),x=+t.get("max"),_=[m,x],b=[o,a];function w(e,n){var i,o=y.getItemModel(e).getModel("pointer"),a=Ur(o.get("width"),r.r),s=Ur(o.get("length"),r.r),l=t.get(["pointer","icon"]),u=o.get("offsetCenter"),h=Ur(u[0],r.r),c=Ur(u[1],r.r),p=o.get("keepAspect");return(i=l?Wy(l,h-a/2,c-s,a,s,null,p):new ik({shape:{angle:-Math.PI/2,width:a,r:s,x:h,y:c}})).rotation=-(n+Math.PI/2),i.x=r.cx,i.y=r.cy,i}function S(t,e){var n=f.get("roundCap")?HS:zu,i=f.get("overlap"),a=i?f.get("width"):l/y.count(),u=i?r.r-a:r.r-(t+1)*a,h=i?r.r:r.r-t*a,c=new n({shape:{startAngle:o,endAngle:e,cx:r.cx,cy:r.cy,clockwise:s,r0:u,r:h}});return i&&(c.z2=x-y.get(v,t)%x),c}(g||d)&&(y.diff(h).add((function(e){var n=y.get(v,e);if(d){var i=w(e,o);gh(i,{rotation:-((isNaN(+n)?b[0]:Xr(n,_,b,!0))+Math.PI/2)},t),u.add(i),y.setItemGraphicEl(e,i)}if(g){var r=S(e,o),a=f.get("clip");gh(r,{shape:{endAngle:Xr(n,_,b,a)}},t),u.add(r),tl(t.seriesIndex,y.dataType,e,r),p[e]=r}})).update((function(e,n){var i=y.get(v,e);if(d){var r=h.getItemGraphicEl(n),a=r?r.rotation:o,s=w(e,a);s.rotation=a,fh(s,{rotation:-((isNaN(+i)?b[0]:Xr(i,_,b,!0))+Math.PI/2)},t),u.add(s),y.setItemGraphicEl(e,s)}if(g){var l=c[n],m=S(e,l?l.shape.endAngle:o),x=f.get("clip");fh(m,{shape:{endAngle:Xr(i,_,b,x)}},t),u.add(m),tl(t.seriesIndex,y.dataType,e,m),p[e]=m}})).execute(),y.each((function(t){var e=y.getItemModel(t),n=e.getModel("emphasis"),r=n.get("focus"),o=n.get("blurScope"),a=n.get("disabled");if(d){var s=y.getItemGraphicEl(t),l=y.getItemVisual(t,"style"),u=l.fill;if(s instanceof ks){var h=s.style;s.useStyle(A({image:h.image,x:h.x,y:h.y,width:h.width,height:h.height},l))}else s.useStyle(l),"pointer"!==s.type&&s.setColor(u);s.setStyle(e.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===s.style.fill&&s.setStyle("fill",i(Xr(y.get(v,t),_,[0,1],!0))),s.z2EmphasisLift=0,jl(s,e),Yl(s,r,o,a)}if(g){var c=p[t];c.useStyle(y.getItemVisual(t,"style")),c.setStyle(e.getModel(["progress","itemStyle"]).getItemStyle()),c.z2EmphasisLift=0,jl(c,e),Yl(c,r,o,a)}})),this._progressEls=p)},e.prototype._renderAnchor=function(t,e){var n=t.getModel("anchor");if(n.get("show")){var i=n.get("size"),r=n.get("icon"),o=n.get("offsetCenter"),a=n.get("keepAspect"),s=Wy(r,e.cx-i/2+Ur(o[0],e.r),e.cy-i/2+Ur(o[1],e.r),i,i,null,a);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},e.prototype._renderTitleAndDetail=function(t,e,n,i,r){var o=this,a=t.getData(),s=a.mapDimension("value"),l=+t.get("min"),u=+t.get("max"),h=new zr,c=[],p=[],d=t.isAnimationEnabled(),f=t.get(["pointer","showAbove"]);a.diff(this._data).add((function(t){c[t]=new Fs({silent:!0}),p[t]=new Fs({silent:!0})})).update((function(t,e){c[t]=o._titleEls[e],p[t]=o._detailEls[e]})).execute(),a.each((function(e){var n=a.getItemModel(e),o=a.get(s,e),g=new zr,y=i(Xr(o,[l,u],[0,1],!0)),v=n.getModel("title");if(v.get("show")){var m=v.get("offsetCenter"),x=r.cx+Ur(m[0],r.r),_=r.cy+Ur(m[1],r.r);(D=c[e]).attr({z2:f?0:2,style:nc(v,{x:x,y:_,text:a.getName(e),align:"center",verticalAlign:"middle"},{inheritColor:y})}),g.add(D)}var b=n.getModel("detail");if(b.get("show")){var w=b.get("offsetCenter"),S=r.cx+Ur(w[0],r.r),M=r.cy+Ur(w[1],r.r),I=Ur(b.get("width"),r.r),T=Ur(b.get("height"),r.r),C=t.get(["progress","show"])?a.getItemVisual(e,"style").fill:y,D=p[e],A=b.get("formatter");D.attr({z2:f?0:2,style:nc(b,{x:S,y:M,text:rk(o,A),width:isNaN(I)?null:I,height:isNaN(T)?null:T,align:"center",verticalAlign:"middle"},{inheritColor:C})}),hc(D,{normal:b},o,(function(t){return rk(t,A)})),d&&cc(D,e,a,t,{getFormattedLabel:function(t,e,n,i,r,a){return rk(a?a.interpolatedValue:o,A)}}),g.add(D)}h.add(g)})),this.group.add(h),this._titleEls=c,this._detailEls=p},e.type="gauge",e}(kg),ak=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="itemStyle",n}return n(e,t),e.prototype.getInitialData=function(t,e){return MM(this,["value"])},e.type="series.gauge",e.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,"#E6EBF8"]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:"#63677A",width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:"#63677A",width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:"#464646",fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:"#fff",borderWidth:0,borderColor:"#5470c6"}},title:{show:!0,offsetCenter:[0,"20%"],color:"#464646",fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:"rgba(0,0,0,0)",borderWidth:0,borderColor:"#ccc",width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:"#464646",fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}},e}(mg);var sk=["itemStyle","opacity"],lk=function(t){function e(e,n){var i=t.call(this)||this,r=i,o=new Yu,a=new Fs;return r.setTextContent(a),i.setTextGuideLine(o),i.updateData(e,n,!0),i}return n(e,t),e.prototype.updateData=function(t,e,n){var i=this,r=t.hostModel,o=t.getItemModel(e),a=t.getItemLayout(e),s=o.getModel("emphasis"),l=o.get(sk);l=null==l?1:l,n||_h(i),i.useStyle(t.getItemVisual(e,"style")),i.style.lineJoin="round",n?(i.setShape({points:a.points}),i.style.opacity=0,gh(i,{style:{opacity:l}},r,e)):fh(i,{style:{opacity:l},shape:{points:a.points}},r,e),jl(i,o),this._updateLabel(t,e),Yl(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},e.prototype._updateLabel=function(t,e){var n=this,i=this.getTextGuideLine(),r=n.getTextContent(),o=t.hostModel,a=t.getItemModel(e),s=t.getItemLayout(e).label,l=t.getItemVisual(e,"style"),u=l.fill;tc(r,ec(a),{labelFetcher:t.hostModel,labelDataIndex:e,defaultOpacity:l.opacity,defaultText:t.getName(e)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}}),n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:u,outsideFill:u});var h=s.linePoints;i.setShape({points:h}),n.textGuideLineConfig={anchor:h?new De(h[0][0],h[0][1]):null},fh(r,{style:{x:s.x,y:s.y}},o,e),r.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),Tb(n,Cb(a),{stroke:u})},e}(Wu),uk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreLabelLineUpdate=!0,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this._data,o=this.group;i.diff(r).add((function(t){var e=new lk(i,t);i.setItemGraphicEl(t,e),o.add(e)})).update((function(t,e){var n=r.getItemGraphicEl(e);n.updateData(i,t),o.add(n),i.setItemGraphicEl(t,n)})).remove((function(e){xh(r.getItemGraphicEl(e),t,e)})).execute(),this._data=i},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.prototype.dispose=function(){},e.type="funnel",e}(kg),hk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new IM(W(this.getData,this),W(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.getInitialData=function(t,e){return MM(this,{coordDimensions:["value"],encodeDefaulter:H(Jp,this)})},e.prototype._defaultLabelLine=function(t){wo(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.prototype.getDataParams=function(e){var n=this.getData(),i=t.prototype.getDataParams.call(this,e),r=n.mapDimension("value"),o=n.getSum(r);return i.percent=o?+(n.get(r,e)/o*100).toFixed(2):0,i.$vars.push("percent"),i},e.type="series.funnel",e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:60,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:"#212121"}}},e}(mg);function ck(t,e){t.eachSeriesByType("funnel",(function(t){var n=t.getData(),i=n.mapDimension("value"),r=t.get("sort"),o=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e),a=t.get("orient"),s=o.width,l=o.height,u=function(t,e){for(var n=t.mapDimension("value"),i=t.mapArray(n,(function(t){return t})),r=[],o="ascending"===e,a=0,s=t.count();a5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&Mk(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function Mk(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var Ik=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&C(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){E(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];E(B(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(Rp),Tk=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(nb);function Ck(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=Ak(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=Ak(s,[0,a]),r=o=Ak(s,[r,o]),i=0}e[0]=Ak(e[0],n),e[1]=Ak(e[1],n);var l=Dk(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=Ak(e[i],c),u=Dk(e,i),null!=r&&(u.sign!==l.sign||u.spano&&(e[1-i]=e[i]+u.sign*o),e}function Dk(t,e){var n=t[e]-t[1-e];return{span:Math.abs(n),sign:n>0?-1:n<0?1:e?-1:1}}function Ak(t,e){return Math.min(null!=e[1]?e[1]:1/0,Math.max(null!=e[0]?e[0]:-1/0,t))}var kk=E,Lk=Math.min,Pk=Math.max,Ok=Math.floor,Rk=Math.ceil,Nk=Zr,Ek=Math.PI,zk=function(){function t(t,e,n){this.type="parallel",this._axesMap=yt(),this._axesLayout={},this.dimensions=t.dimensions,this._model=t,this._init(t,e,n)}return t.prototype._init=function(t,e,n){var i=t.dimensions,r=t.parallelAxisIndex;kk(i,(function(t,n){var i=r[n],o=e.getComponent("parallelAxis",i),a=this._axesMap.set(t,new Tk(t,m_(o),[0,0],o.get("type"),i)),s="category"===a.type;a.onBand=s&&o.get("boundaryGap"),a.inverse=o.get("inverse"),o.axis=a,a.model=o,a.coordinateSystem=o.coordinateSystem=this}),this)},t.prototype.update=function(t,e){this._updateAxesFromSeries(this._model,t)},t.prototype.containPoint=function(t){var e=this._makeLayoutInfo(),n=e.axisBase,i=e.layoutBase,r=e.pixelDimIndex,o=t[1-r],a=t[r];return o>=n&&o<=n+e.axisLength&&a>=i&&a<=i+e.layoutLength},t.prototype.getModel=function(){return this._model},t.prototype._updateAxesFromSeries=function(t,e){e.eachSeries((function(n){if(t.contains(n,e)){var i=n.getData();kk(this.dimensions,(function(t){var e=this._axesMap.get(t);e.scale.unionExtentFromData(i,i.mapDimension(t)),v_(e.scale,e.model)}),this)}}),this)},t.prototype.resize=function(t,e){this._rect=Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()}),this._layoutAxes()},t.prototype.getRect=function(){return this._rect},t.prototype._makeLayoutInfo=function(){var t,e=this._model,n=this._rect,i=["x","y"],r=["width","height"],o=e.get("layout"),a="horizontal"===o?0:1,s=n[r[a]],l=[0,s],u=this.dimensions.length,h=Vk(e.get("axisExpandWidth"),l),c=Vk(e.get("axisExpandCount")||0,[0,u]),p=e.get("axisExpandable")&&u>3&&u>c&&c>1&&h>0&&s>0,d=e.get("axisExpandWindow");d?(t=Vk(d[1]-d[0],l),d[1]=d[0]+t):(t=Vk(h*(c-1),l),(d=[h*(e.get("axisExpandCenter")||Ok(u/2))-t/2])[1]=d[0]+t);var f=(s-t)/(u-c);f<3&&(f=0);var g=[Ok(Nk(d[0]/h,1))+1,Rk(Nk(d[1]/h,1))-1],y=f/h*d[0];return{layout:o,pixelDimIndex:a,layoutBase:n[i[a]],layoutLength:s,axisBase:n[i[1-a]],axisLength:n[r[1-a]],axisExpandable:p,axisExpandWidth:h,axisCollapseWidth:f,axisExpandWindow:d,axisCount:u,winInnerIndices:g,axisExpandWindow0Pos:y}},t.prototype._layoutAxes=function(){var t=this._rect,e=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),r=i.layout;e.each((function(t){var e=[0,i.axisLength],n=t.inverse?1:0;t.setExtent(e[n],e[1-n])})),kk(n,(function(e,n){var o=(i.axisExpandable?Fk:Bk)(n,i),a={horizontal:{x:o.position,y:i.axisLength},vertical:{x:0,y:o.position}},s={horizontal:Ek/2,vertical:0},l=[a[r].x+t.x,a[r].y+t.y],u=s[r],h=[1,0,0,1,0,0];Se(h,h,u),we(h,h,l),this._axesLayout[e]={position:l,rotation:u,transform:h,axisNameAvailableWidth:o.axisNameAvailableWidth,axisLabelShow:o.axisLabelShow,nameTruncateMaxWidth:o.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}}),this)},t.prototype.getAxis=function(t){return this._axesMap.get(t)},t.prototype.dataToPoint=function(t,e){return this.axisCoordToPoint(this._axesMap.get(e).dataToCoord(t),e)},t.prototype.eachActiveState=function(t,e,n,i){null==n&&(n=0),null==i&&(i=t.count());var r=this._axesMap,o=this.dimensions,a=[],s=[];E(o,(function(e){a.push(t.mapDimension(e)),s.push(r.get(e).model)}));for(var l=this.hasAxisBrushed(),u=n;ur*(1-h[0])?(l="jump",a=s-r*(1-h[2])):(a=s-r*h[1])>=0&&(a=s-r*(1-h[1]))<=0&&(a=0),(a*=e.axisExpandWidth/u)?Ck(a,i,o,"all"):l="none";else{var p=i[1]-i[0];(i=[Pk(0,o[1]*s/p-p/2)])[1]=Lk(o[1],i[0]+p),i[0]=i[1]-p}return{axisExpandWindow:i,behavior:l}},t}();function Vk(t,e){return Lk(Pk(t,e[0]),e[1])}function Bk(t,e){var n=e.layoutLength/(e.axisCount-1);return{position:n*t,axisNameAvailableWidth:n,axisLabelShow:!0}}function Fk(t,e){var n,i,r=e.layoutLength,o=e.axisExpandWidth,a=e.axisCount,s=e.axisCollapseWidth,l=e.winInnerIndices,u=s,h=!1;return t=0;n--)jr(e[n])},e.prototype.getActiveState=function(t){var e=this.activeIntervals;if(!e.length)return"normal";if(null==t||isNaN(+t))return"inactive";if(1===e.length){var n=e[0];if(n[0]<=t&&t<=n[1])return"active"}else for(var i=0,r=e.length;i6}(t)||o){if(a&&!o){"single"===s.brushMode&&sL(t);var l=T(s);l.brushType=ML(l.brushType,a),l.panelId=a===Hk?null:a.panelId,o=t._creatingCover=Qk(t,l),t._covers.push(o)}if(o){var u=CL[ML(t._brushType,a)];o.__brushOption.range=u.getCreatingRange(_L(t,o,t._track)),i&&(tL(t,o),u.updateCommon(t,o)),eL(t,o),r={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&oL(t,e,n)&&sL(t)&&(r={isEnd:i,removeOnClick:!0});return r}function ML(t,e){return"auto"===t?e.defaultBrushType:t}var IL={mousedown:function(t){if(this._dragging)TL(this,t);else if(!t.target||!t.target.draggable){bL(t);var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);this._creatingCover=null,(this._creatingPanel=oL(this,t,e))&&(this._dragging=!0,this._track=[e.slice()])}},mousemove:function(t){var e=t.offsetX,n=t.offsetY,i=this.group.transformCoordToLocal(e,n);if(function(t,e,n){if(t._brushType&&!function(t,e,n){var i=t._zr;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}(t,e.offsetX,e.offsetY)){var i=t._zr,r=t._covers,o=oL(t,e,n);if(!t._dragging)for(var a=0;a=0&&(o[r[a].depth]=new Mc(r[a],this,e));if(i&&n){var s=QA(i,n,this,!0,(function(t,e){t.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getData().getItemLayout(e);if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t})),e.wrapMethod("getItemModel",(function(t,e){var n=t.parentModel,i=n.getGraph().getEdgeByIndex(e).node1.getLayout();if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t}))}));return s.data}},e.prototype.setNodePosition=function(t,e){var n=(this.option.data||this.option.nodes)[t];n.localX=e[0],n.localY=e[1]},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.formatTooltip=function(t,e,n){function i(t){return isNaN(t)||null==t}if("edge"===n){var r=this.getDataParams(t,n),o=r.data,a=r.value;return ng("nameValue",{name:o.source+" -- "+o.target,value:a,noValue:i(a)})}var s=this.getGraph().getNodeByIndex(t).getLayout().value,l=this.getDataParams(t,n).data.name;return ng("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},e.prototype.optionUpdated=function(){},e.prototype.getDataParams=function(e,n){var i=t.prototype.getDataParams.call(this,e,n);if(null==i.value&&"node"===n){var r=this.getGraph().getNodeByIndex(e).getLayout().value;i.value=r}return i},e.type="series.sankey",e.defaultOption={z:2,coordinateSystem:"view",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:"#314656",opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:"#212121"}},animationEasing:"linear",animationDuration:1e3},e}(mg);function HL(t,e){t.eachSeriesByType("sankey",(function(t){var n=t.get("nodeWidth"),i=t.get("nodeGap"),r=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()})}(t,e);t.layoutInfo=r;var o=r.width,a=r.height,s=t.getGraph(),l=s.nodes,u=s.edges;!function(t){E(t,(function(t){var e=QL(t.outEdges,JL),n=QL(t.inEdges,JL),i=t.getValue()||0,r=Math.max(e,n,i);t.setLayout({value:r},!0)}))}(l),function(t,e,n,i,r,o,a,s,l){(function(t,e,n,i,r,o,a){for(var s=[],l=[],u=[],h=[],c=0,p=0;p=0;v&&y.depth>d&&(d=y.depth),g.setLayout({depth:v?y.depth:c},!0),"vertical"===o?g.setLayout({dy:n},!0):g.setLayout({dx:n},!0);for(var m=0;mc-1?d:c-1;a&&"left"!==a&&function(t,e,n,i){if("right"===e){for(var r=[],o=t,a=0;o.length;){for(var s=0;s0;o--)UL(s,l*=.99,a),XL(s,r,n,i,a),tP(s,l,a),XL(s,r,n,i,a)}(t,e,o,r,i,a,s),function(t,e){var n="vertical"===e?"x":"y";E(t,(function(t){t.outEdges.sort((function(t,e){return t.node2.getLayout()[n]-e.node2.getLayout()[n]})),t.inEdges.sort((function(t,e){return t.node1.getLayout()[n]-e.node1.getLayout()[n]}))})),E(t,(function(t){var e=0,n=0;E(t.outEdges,(function(t){t.setLayout({sy:e},!0),e+=t.getLayout().dy})),E(t.inEdges,(function(t){t.setLayout({ty:n},!0),n+=t.getLayout().dy}))}))}(t,s)}(l,u,n,i,o,a,0!==B(l,(function(t){return 0===t.getLayout().value})).length?0:t.get("layoutIterations"),t.get("orient"),t.get("nodeAlign"))}))}function YL(t){var e=t.hostGraph.data.getRawDataItem(t.dataIndex);return null!=e.depth&&e.depth>=0}function XL(t,e,n,i,r){var o="vertical"===r?"x":"y";E(t,(function(t){var a,s,l;t.sort((function(t,e){return t.getLayout()[o]-e.getLayout()[o]}));for(var u=0,h=t.length,c="vertical"===r?"dx":"dy",p=0;p0&&(a=s.getLayout()[o]+l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]+s.getLayout()[c]+e;if((l=u-e-("vertical"===r?i:n))>0){a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0),u=a;for(p=h-2;p>=0;--p)(l=(s=t[p]).getLayout()[o]+s.getLayout()[c]+e-u)>0&&(a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]}}))}function UL(t,e,n){E(t.slice().reverse(),(function(t){E(t,(function(t){if(t.outEdges.length){var i=QL(t.outEdges,ZL,n)/QL(t.outEdges,JL);if(isNaN(i)){var r=t.outEdges.length;i=r?QL(t.outEdges,jL,n)/r:0}if("vertical"===n){var o=t.getLayout().x+(i-$L(t,n))*e;t.setLayout({x:o},!0)}else{var a=t.getLayout().y+(i-$L(t,n))*e;t.setLayout({y:a},!0)}}}))}))}function ZL(t,e){return $L(t.node2,e)*t.getValue()}function jL(t,e){return $L(t.node2,e)}function qL(t,e){return $L(t.node1,e)*t.getValue()}function KL(t,e){return $L(t.node1,e)}function $L(t,e){return"vertical"===e?t.getLayout().x+t.getLayout().dx/2:t.getLayout().y+t.getLayout().dy/2}function JL(t){return t.getValue()}function QL(t,e,n){for(var i=0,r=t.length,o=-1;++oo&&(o=e)})),E(n,(function(e){var n=new _D({type:"color",mappingMethod:"linear",dataExtent:[r,o],visual:t.get("color")}).mapValueToVisual(e.getLayout().value),i=e.getModel().get(["itemStyle","color"]);null!=i?(e.setVisual("color",i),e.setVisual("style",{fill:i})):(e.setVisual("color",n),e.setVisual("style",{fill:n}))}))}i.length&&E(i,(function(t){var e=t.getModel().get("lineStyle");t.setVisual("style",e)}))}))}var nP=function(){function t(){}return t.prototype.getInitialData=function(t,e){var n,i,r=e.getComponent("xAxis",this.get("xAxisIndex")),o=e.getComponent("yAxis",this.get("yAxisIndex")),a=r.get("type"),s=o.get("type");"category"===a?(t.layout="horizontal",n=r.getOrdinalMeta(),i=!0):"category"===s?(t.layout="vertical",n=o.getOrdinalMeta(),i=!0):t.layout=t.layout||"horizontal";var l=["x","y"],u="horizontal"===t.layout?0:1,h=this._baseAxisDim=l[u],c=l[1-u],p=[r,o],d=p[u].get("type"),f=p[1-u].get("type"),g=t.data;if(g&&i){var y=[];E(g,(function(t,e){var n;Y(t)?(n=t.slice(),t.unshift(e)):Y(t.value)?((n=A({},t)).value=n.value.slice(),t.value.unshift(e)):n=t,y.push(n)})),t.data=y}var v=this.defaultValueDimensions,m=[{name:h,type:Gm(d),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:c,type:Gm(f),dimsDef:v.slice()}];return MM(this,{coordDimensions:m,dimensionsCount:v.length+1,encodeDefaulter:H($p,m,this)})},t.prototype.getBaseAxis=function(){var t=this._baseAxisDim;return this.ecModel.getComponent(t+"Axis",this.get(t+"AxisIndex")).axis},t}(),iP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"min",defaultTooltip:!0},{name:"Q1",defaultTooltip:!0},{name:"median",defaultTooltip:!0},{name:"Q3",defaultTooltip:!0},{name:"max",defaultTooltip:!0}],n.visualDrawType="stroke",n}return n(e,t),e.type="series.boxplot",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,boxWidth:[7,50],itemStyle:{color:"#fff",borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2,shadowBlur:5,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0,0,0,0.2)"}},animationDuration:800},e}(mg);R(iP,nP,!0);var rP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this.group,o=this._data;this._data||r.removeAll();var a="horizontal"===t.get("layout")?1:0;i.diff(o).add((function(t){if(i.hasValue(t)){var e=sP(i.getItemLayout(t),i,t,a,!0);i.setItemGraphicEl(t,e),r.add(e)}})).update((function(t,e){var n=o.getItemGraphicEl(e);if(i.hasValue(t)){var s=i.getItemLayout(t);n?(_h(n),lP(s,n,i,t)):n=sP(s,i,t,a),r.add(n),i.setItemGraphicEl(t,n)}else r.remove(n)})).remove((function(t){var e=o.getItemGraphicEl(t);e&&r.remove(e)})).execute(),this._data=i},e.prototype.remove=function(t){var e=this.group,n=this._data;this._data=null,n&&n.eachItemGraphicEl((function(t){t&&e.remove(t)}))},e.type="boxplot",e}(kg),oP=function(){},aP=function(t){function e(e){var n=t.call(this,e)||this;return n.type="boxplotBoxPath",n}return n(e,t),e.prototype.getDefaultShape=function(){return new oP},e.prototype.buildPath=function(t,e){var n=e.points,i=0;for(t.moveTo(n[i][0],n[i][1]),i++;i<4;i++)t.lineTo(n[i][0],n[i][1]);for(t.closePath();ig){var _=[v,x];i.push(_)}}}return{boxData:n,outliers:i}}(e.getRawData(),t.config);return[{dimensions:["ItemName","Low","Q1","Q2","Q3","High"],data:i.boxData},{data:i.outliers}]}};var dP=["color","borderColor"],fP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeClipPath(),this._progressiveEls=null,this._updateDrawMode(t),this._isLargeDraw?this._renderLarge(t):this._renderNormal(t)},e.prototype.incrementalPrepareRender=function(t,e,n){this._clear(),this._updateDrawMode(t)},e.prototype.incrementalRender=function(t,e,n,i){this._progressiveEls=[],this._isLargeDraw?this._incrementalRenderLarge(t,e):this._incrementalRenderNormal(t,e)},e.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},e.prototype._updateDrawMode=function(t){var e=t.pipelineContext.large;null!=this._isLargeDraw&&e===this._isLargeDraw||(this._isLargeDraw=e,this._clear())},e.prototype._renderNormal=function(t){var e=t.getData(),n=this._data,i=this.group,r=e.getLayout("isSimpleBox"),o=t.get("clip",!0),a=t.coordinateSystem,s=a.getArea&&a.getArea();this._data||i.removeAll(),e.diff(n).add((function(n){if(e.hasValue(n)){var a=e.getItemLayout(n);if(o&&mP(s,a))return;var l=vP(a,n,!0);gh(l,{shape:{points:a.ends}},t,n),xP(l,e,n,r),i.add(l),e.setItemGraphicEl(n,l)}})).update((function(a,l){var u=n.getItemGraphicEl(l);if(e.hasValue(a)){var h=e.getItemLayout(a);o&&mP(s,h)?i.remove(u):(u?(fh(u,{shape:{points:h.ends}},t,a),_h(u)):u=vP(h),xP(u,e,a,r),i.add(u),e.setItemGraphicEl(a,u))}else i.remove(u)})).remove((function(t){var e=n.getItemGraphicEl(t);e&&i.remove(e)})).execute(),this._data=e},e.prototype._renderLarge=function(t){this._clear(),SP(t,this.group);var e=t.get("clip",!0)?SS(t.coordinateSystem,!1,t):null;e?this.group.setClipPath(e):this.group.removeClipPath()},e.prototype._incrementalRenderNormal=function(t,e){for(var n,i=e.getData(),r=i.getLayout("isSimpleBox");null!=(n=t.next());){var o=vP(i.getItemLayout(n));xP(o,i,n,r),o.incremental=!0,this.group.add(o),this._progressiveEls.push(o)}},e.prototype._incrementalRenderLarge=function(t,e){SP(e,this.group,this._progressiveEls,!0)},e.prototype.remove=function(t){this._clear()},e.prototype._clear=function(){this.group.removeAll(),this._data=null},e.type="candlestick",e}(kg),gP=function(){},yP=function(t){function e(e){var n=t.call(this,e)||this;return n.type="normalCandlestickBox",n}return n(e,t),e.prototype.getDefaultShape=function(){return new gP},e.prototype.buildPath=function(t,e){var n=e.points;this.__simpleBox?(t.moveTo(n[4][0],n[4][1]),t.lineTo(n[6][0],n[6][1])):(t.moveTo(n[0][0],n[0][1]),t.lineTo(n[1][0],n[1][1]),t.lineTo(n[2][0],n[2][1]),t.lineTo(n[3][0],n[3][1]),t.closePath(),t.moveTo(n[4][0],n[4][1]),t.lineTo(n[5][0],n[5][1]),t.moveTo(n[6][0],n[6][1]),t.lineTo(n[7][0],n[7][1]))},e}(Is);function vP(t,e,n){var i=t.ends;return new yP({shape:{points:n?_P(i,t):i},z2:100})}function mP(t,e){for(var n=!0,i=0;i0?"borderColor":"borderColor0"])||n.get(["itemStyle",t>0?"color":"color0"]);0===t&&(r=n.get(["itemStyle","borderColorDoji"]));var o=n.getModel("itemStyle").getItemStyle(dP);e.useStyle(o),e.style.fill=null,e.style.stroke=r}var IP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return n(e,t),e.prototype.getShadowDim=function(){return"open"},e.prototype.brushSelector=function(t,e,n){var i=e.getItemLayout(t);return i&&n.rect(i.brushRect)},e.type="series.candlestick",e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{scale:!0,itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},e}(mg);function TP(t){t&&Y(t.series)&&E(t.series,(function(t){q(t)&&"k"===t.type&&(t.type="candlestick")}))}R(IP,nP,!0);var CP=["itemStyle","borderColor"],DP=["itemStyle","borderColor0"],AP=["itemStyle","borderColorDoji"],kP=["itemStyle","color"],LP=["itemStyle","color0"],PP={seriesType:"candlestick",plan:Cg(),performRawSeries:!0,reset:function(t,e){function n(t,e){return e.get(t>0?kP:LP)}function i(t,e){return e.get(0===t?AP:t>0?CP:DP)}if(!e.isSeriesFiltered(t))return!t.pipelineContext.large&&{progress:function(t,e){for(var r;null!=(r=t.next());){var o=e.getItemModel(r),a=e.getItemLayout(r).sign,s=o.getItemStyle();s.fill=n(a,o),s.stroke=i(a,o)||s.fill,A(e.ensureUniqueItemVisual(r,"style"),s)}}}}},OP={seriesType:"candlestick",plan:Cg(),reset:function(t){var e=t.coordinateSystem,n=t.getData(),i=function(t,e){var n,i=t.getBaseAxis(),r="category"===i.type?i.getBandWidth():(n=i.getExtent(),Math.abs(n[1]-n[0])/e.count()),o=Ur(rt(t.get("barMaxWidth"),r),r),a=Ur(rt(t.get("barMinWidth"),1),r),s=t.get("barWidth");return null!=s?Ur(s,r):Math.max(Math.min(r/2,o),a)}(t,n),r=["x","y"],o=n.getDimensionIndex(n.mapDimension(r[0])),a=z(n.mapDimensionsAll(r[1]),n.getDimensionIndex,n),s=a[0],l=a[1],u=a[2],h=a[3];if(n.setLayout({candleWidth:i,isSimpleBox:i<=1.3}),!(o<0||a.length<4))return{progress:t.pipelineContext.large?function(n,i){var r,a,c=Ex(4*n.count),p=0,d=[],f=[],g=i.getStore(),y=!!t.get(["itemStyle","borderColorDoji"]);for(;null!=(a=n.next());){var v=g.get(o,a),m=g.get(s,a),x=g.get(l,a),_=g.get(u,a),b=g.get(h,a);isNaN(v)||isNaN(_)||isNaN(b)?(c[p++]=NaN,p+=3):(c[p++]=RP(g,a,m,x,l,y),d[0]=v,d[1]=_,r=e.dataToPoint(d,null,f),c[p++]=r?r[0]:NaN,c[p++]=r?r[1]:NaN,d[1]=b,r=e.dataToPoint(d,null,f),c[p++]=r?r[1]:NaN)}i.setLayout("largePoints",c)}:function(t,n){var r,a=n.getStore();for(;null!=(r=t.next());){var c=a.get(o,r),p=a.get(s,r),d=a.get(l,r),f=a.get(u,r),g=a.get(h,r),y=Math.min(p,d),v=Math.max(p,d),m=M(y,c),x=M(v,c),_=M(f,c),b=M(g,c),w=[];I(w,x,0),I(w,m,1),w.push(C(b),C(x),C(_),C(m));var S=!!n.getItemModel(r).get(["itemStyle","borderColorDoji"]);n.setItemLayout(r,{sign:RP(a,r,p,d,l,S),initBaseline:p>d?x[1]:m[1],ends:w,brushRect:T(f,g,c)})}function M(t,n){var i=[];return i[0]=n,i[1]=t,isNaN(n)||isNaN(t)?[NaN,NaN]:e.dataToPoint(i)}function I(t,e,n){var r=e.slice(),o=e.slice();r[0]=Nh(r[0]+i/2,1,!1),o[0]=Nh(o[0]-i/2,1,!0),n?t.push(r,o):t.push(o,r)}function T(t,e,n){var r=M(t,n),o=M(e,n);return r[0]-=i/2,o[0]-=i/2,{x:r[0],y:r[1],width:i,height:o[1]-r[1]}}function C(t){return t[0]=Nh(t[0],1),t}}}}};function RP(t,e,n,i,r,o){return n>i?-1:n0?t.get(r,e-1)<=i?1:-1:1}function NP(t,e){var n=e.rippleEffectColor||e.color;t.eachChild((function(t){t.attr({z:e.z,zlevel:e.zlevel,style:{stroke:"stroke"===e.brushType?n:null,fill:"fill"===e.brushType?n:null}})}))}var EP=function(t){function e(e,n){var i=t.call(this)||this,r=new oS(e,n),o=new zr;return i.add(r),i.add(o),i.updateData(e,n),i}return n(e,t),e.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},e.prototype.startEffectAnimation=function(t){for(var e=t.symbolType,n=t.color,i=t.rippleNumber,r=this.childAt(1),o=0;o0&&(o=this._getLineLength(i)/l*1e3),o!==this._period||a!==this._loop||s!==this._roundTrip){i.stopAnimation();var h=void 0;h=X(u)?u(n):u,i.__t>0&&(h=-o*i.__t),this._animateSymbol(i,o,h,a,s)}this._period=o,this._loop=a,this._roundTrip=s}},e.prototype._animateSymbol=function(t,e,n,i,r){if(e>0){t.__t=0;var o=this,a=t.animate("",i).when(r?2*e:e,{__t:r?2:1}).delay(n).during((function(){o._updateSymbolPosition(t)}));i||a.done((function(){o.remove(t)})),a.start()}},e.prototype._getLineLength=function(t){return Vt(t.__p1,t.__cp1)+Vt(t.__cp1,t.__p2)},e.prototype._updateAnimationPoints=function(t,e){t.__p1=e[0],t.__p2=e[1],t.__cp1=e[2]||[(e[0][0]+e[1][0])/2,(e[0][1]+e[1][1])/2]},e.prototype.updateData=function(t,e,n){this.childAt(0).updateData(t,e,n),this._updateEffectSymbol(t,e)},e.prototype._updateSymbolPosition=function(t){var e=t.__p1,n=t.__p2,i=t.__cp1,r=t.__t<1?t.__t:2-t.__t,o=[t.x,t.y],a=o.slice(),s=In,l=Tn;o[0]=s(e[0],i[0],n[0],r),o[1]=s(e[1],i[1],n[1],r);var u=t.__t<1?l(e[0],i[0],n[0],r):l(n[0],i[0],e[0],1-r),h=t.__t<1?l(e[1],i[1],n[1],r):l(n[1],i[1],e[1],1-r);t.rotation=-Math.atan2(h,u)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==t.__lastT&&t.__lastT=0&&!(i[o]<=e);o--);o=Math.min(o,r-2)}else{for(o=a;oe);o++);o=Math.min(o-1,r-2)}var s=(e-i[o])/(i[o+1]-i[o]),l=n[o],u=n[o+1];t.x=l[0]*(1-s)+s*u[0],t.y=l[1]*(1-s)+s*u[1];var h=t.__t<1?u[0]-l[0]:l[0]-u[0],c=t.__t<1?u[1]-l[1]:l[1]-u[1];t.rotation=-Math.atan2(c,h)-Math.PI/2,this._lastFrame=o,this._lastFramePercent=e,t.ignore=!1}},e}(BP),WP=function(){this.polyline=!1,this.curveness=0,this.segs=[]},HP=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.getDefaultStyle=function(){return{stroke:"#000",fill:null}},e.prototype.getDefaultShape=function(){return new WP},e.prototype.buildPath=function(t,e){var n,i=e.segs,r=e.curveness;if(e.polyline)for(n=this._off;n0){t.moveTo(i[n++],i[n++]);for(var a=1;a0){var c=(s+u)/2-(l-h)*r,p=(l+h)/2-(u-s)*r;t.quadraticCurveTo(c,p,u,h)}else t.lineTo(u,h)}this.incremental&&(this._off=n,this.notClear=!0)},e.prototype.findDataIndex=function(t,e){var n=this.shape,i=n.segs,r=n.curveness,o=this.style.lineWidth;if(n.polyline)for(var a=0,s=0;s0)for(var u=i[s++],h=i[s++],c=1;c0){if(ls(u,h,(u+p)/2-(h-d)*r,(h+d)/2-(p-u)*r,p,d,o,t,e))return a}else if(as(u,h,p,d,o,t,e))return a;a++}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape.segs,n=1/0,i=1/0,r=-1/0,o=-1/0,a=0;a0&&(o.dataIndex=n+t.__startIndex)}))},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),XP={seriesType:"lines",plan:Cg(),reset:function(t){var e=t.coordinateSystem;if(e){var n=t.get("polyline"),i=t.pipelineContext.large;return{progress:function(r,o){var a=[];if(i){var s=void 0,l=r.end-r.start;if(n){for(var u=0,h=r.start;h0&&(l||s.configLayer(o,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(a/10+.9,1),0)})),r.updateData(i);var u=t.get("clip",!0)&&SS(t.coordinateSystem,!1,t);u?this.group.setClipPath(u):this.group.removeClipPath(),this._lastZlevel=o,this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateLineDraw(i,t).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._lineDraw.incrementalUpdate(t,e.getData()),this._finished=t.end===e.getData().count()},e.prototype.eachRendered=function(t){this._lineDraw&&this._lineDraw.eachRendered(t)},e.prototype.updateTransform=function(t,e,n){var i=t.getData(),r=t.pipelineContext;if(!this._finished||r.large||r.progressiveRender)return{update:!0};var o=XP.reset(t,e,n);o.progress&&o.progress({start:0,end:i.count(),count:i.count()},i),this._lineDraw.updateLayout(),this._clearLayer(n)},e.prototype._updateLineDraw=function(t,e){var n=this._lineDraw,i=this._showEffect(e),r=!!e.get("polyline"),o=e.pipelineContext.large;return n&&i===this._hasEffet&&r===this._isPolyline&&o===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=o?new YP:new RA(r?i?GP:FP:i?BP:OA),this._hasEffet=i,this._isPolyline=r,this._isLargeDraw=o),this.group.add(n.group),n},e.prototype._showEffect=function(t){return!!t.get(["effect","show"])},e.prototype._clearLayer=function(t){var e=t.getZr();"svg"===e.painter.getType()||null==this._lastZlevel||e.painter.getLayer(this._lastZlevel).clear(!0)},e.prototype.remove=function(t,e){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(e)},e.prototype.dispose=function(t,e){this.remove(t,e)},e.type="lines",e}(kg),ZP="undefined"==typeof Uint32Array?Array:Uint32Array,jP="undefined"==typeof Float64Array?Array:Float64Array;function qP(t){var e=t.data;e&&e[0]&&e[0][0]&&e[0][0].coord&&(t.data=z(e,(function(t){var e={coords:[t[0].coord,t[1].coord]};return t[0].name&&(e.fromName=t[0].name),t[1].name&&(e.toName=t[1].name),D([e,t[0],t[1]])})))}var KP=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}return n(e,t),e.prototype.init=function(e){e.data=e.data||[],qP(e);var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count)),t.prototype.init.apply(this,arguments)},e.prototype.mergeOption=function(e){if(qP(e),e.data){var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count))}t.prototype.mergeOption.apply(this,arguments)},e.prototype.appendData=function(t){var e=this._processFlatCoordsArray(t.data);e.flatCoords&&(this._flatCoords?(this._flatCoords=vt(this._flatCoords,e.flatCoords),this._flatCoordsOffset=vt(this._flatCoordsOffset,e.flatCoordsOffset)):(this._flatCoords=e.flatCoords,this._flatCoordsOffset=e.flatCoordsOffset),t.data=new Float32Array(e.count)),this.getRawData().appendData(t.data)},e.prototype._getCoordsFromItemModel=function(t){var e=this.getData().getItemModel(t),n=e.option instanceof Array?e.option:e.getShallow("coords");return n},e.prototype.getLineCoordsCount=function(t){return this._flatCoordsOffset?this._flatCoordsOffset[2*t+1]:this._getCoordsFromItemModel(t).length},e.prototype.getLineCoords=function(t,e){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*t],i=this._flatCoordsOffset[2*t+1],r=0;r ")})},e.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?1e4:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?2e4:this.get("progressiveThreshold"):t},e.prototype.getZLevelKey=function(){var t=this.getModel("effect"),e=t.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:t.get("show")&&e>0?e+"":""},e.type="series.lines",e.dependencies=["grid","polar","geo","calendar"],e.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}},e}(mg);function $P(t){return t instanceof Array||(t=[t,t]),t}var JP={seriesType:"lines",reset:function(t){var e=$P(t.get("symbol")),n=$P(t.get("symbolSize")),i=t.getData();return i.setVisual("fromSymbol",e&&e[0]),i.setVisual("toSymbol",e&&e[1]),i.setVisual("fromSymbolSize",n&&n[0]),i.setVisual("toSymbolSize",n&&n[1]),{dataEach:i.hasItemOption?function(t,e){var n=t.getItemModel(e),i=$P(n.getShallow("symbol",!0)),r=$P(n.getShallow("symbolSize",!0));i[0]&&t.setItemVisual(e,"fromSymbol",i[0]),i[1]&&t.setItemVisual(e,"toSymbol",i[1]),r[0]&&t.setItemVisual(e,"fromSymbolSize",r[0]),r[1]&&t.setItemVisual(e,"toSymbolSize",r[1])}:null}}};var QP=function(){function t(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var t=h.createCanvas();this.canvas=t}return t.prototype.update=function(t,e,n,i,r,o){var a=this._getBrush(),s=this._getGradient(r,"inRange"),l=this._getGradient(r,"outOfRange"),u=this.pointSize+this.blurSize,h=this.canvas,c=h.getContext("2d"),p=t.length;h.width=e,h.height=n;for(var d=0;d0){var I=o(v)?s:l;v>0&&(v=v*S+w),x[_++]=I[M],x[_++]=I[M+1],x[_++]=I[M+2],x[_++]=I[M+3]*v*256}else _+=4}return c.putImageData(m,0,0),h},t.prototype._getBrush=function(){var t=this._brushCanvas||(this._brushCanvas=h.createCanvas()),e=this.pointSize+this.blurSize,n=2*e;t.width=n,t.height=n;var i=t.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor="#000",i.beginPath(),i.arc(-e,e,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),t},t.prototype._getGradient=function(t,e){for(var n=this._gradientPixels,i=n[e]||(n[e]=new Uint8ClampedArray(1024)),r=[0,0,0,0],o=0,a=0;a<256;a++)t[e](a/255,!0,r),i[o++]=r[0],i[o++]=r[1],i[o++]=r[2],i[o++]=r[3];return i},t}();function tO(t){var e=t.dimensions;return"lng"===e[0]&&"lat"===e[1]}var eO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i;e.eachComponent("visualMap",(function(e){e.eachTargetSeries((function(n){n===t&&(i=e)}))})),this._progressiveEls=null,this.group.removeAll();var r=t.coordinateSystem;"cartesian2d"===r.type||"calendar"===r.type?this._renderOnCartesianAndCalendar(t,n,0,t.getData().count()):tO(r)&&this._renderOnGeo(r,t,i,n)},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,e,n,i){var r=e.coordinateSystem;r&&(tO(r)?this.render(e,n,i):(this._progressiveEls=[],this._renderOnCartesianAndCalendar(e,i,t.start,t.end,!0)))},e.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},e.prototype._renderOnCartesianAndCalendar=function(t,e,n,i,r){var o,a,s,l,u=t.coordinateSystem,h=MS(u,"cartesian2d");if(h){var c=u.getAxis("x"),p=u.getAxis("y");0,o=c.getBandWidth()+.5,a=p.getBandWidth()+.5,s=c.scale.getExtent(),l=p.scale.getExtent()}for(var d=this.group,f=t.getData(),g=t.getModel(["emphasis","itemStyle"]).getItemStyle(),y=t.getModel(["blur","itemStyle"]).getItemStyle(),v=t.getModel(["select","itemStyle"]).getItemStyle(),m=t.get(["itemStyle","borderRadius"]),x=ec(t),_=t.getModel("emphasis"),b=_.get("focus"),w=_.get("blurScope"),S=_.get("disabled"),M=h?[f.mapDimension("x"),f.mapDimension("y"),f.mapDimension("value")]:[f.mapDimension("time"),f.mapDimension("value")],I=n;Is[1]||Al[1])continue;var k=u.dataToPoint([D,A]);T=new zs({shape:{x:k[0]-o/2,y:k[1]-a/2,width:o,height:a},style:C})}else{if(isNaN(f.get(M[1],I)))continue;T=new zs({z2:1,shape:u.dataToRect([f.get(M[0],I)]).contentShape,style:C})}if(f.hasItemOption){var L=f.getItemModel(I),P=L.getModel("emphasis");g=P.getModel("itemStyle").getItemStyle(),y=L.getModel(["blur","itemStyle"]).getItemStyle(),v=L.getModel(["select","itemStyle"]).getItemStyle(),m=L.get(["itemStyle","borderRadius"]),b=P.get("focus"),w=P.get("blurScope"),S=P.get("disabled"),x=ec(L)}T.shape.r=m;var O=t.getRawValue(I),R="-";O&&null!=O[2]&&(R=O[2]+""),tc(T,x,{labelFetcher:t,labelDataIndex:I,defaultOpacity:C.opacity,defaultText:R}),T.ensureState("emphasis").style=g,T.ensureState("blur").style=y,T.ensureState("select").style=v,Yl(T,b,w,S),T.incremental=r,r&&(T.states.emphasis.hoverLayer=!0),d.add(T),f.setItemGraphicEl(I,T),this._progressiveEls&&this._progressiveEls.push(T)}},e.prototype._renderOnGeo=function(t,e,n,i){var r=n.targetVisuals.inRange,o=n.targetVisuals.outOfRange,a=e.getData(),s=this._hmLayer||this._hmLayer||new QP;s.blurSize=e.get("blurSize"),s.pointSize=e.get("pointSize"),s.minOpacity=e.get("minOpacity"),s.maxOpacity=e.get("maxOpacity");var l=t.getViewRect().clone(),u=t.getRoamTransform();l.applyTransform(u);var h=Math.max(l.x,0),c=Math.max(l.y,0),p=Math.min(l.width+l.x,i.getWidth()),d=Math.min(l.height+l.y,i.getHeight()),f=p-h,g=d-c,y=[a.mapDimension("lng"),a.mapDimension("lat"),a.mapDimension("value")],v=a.mapArray(y,(function(e,n,i){var r=t.dataToPoint([e,n]);return r[0]-=h,r[1]-=c,r.push(i),r})),m=n.getExtent(),x="visualMap.continuous"===n.type?function(t,e){var n=t[1]-t[0];return e=[(e[0]-t[0])/n,(e[1]-t[0])/n],function(t){return t>=e[0]&&t<=e[1]}}(m,n.option.range):function(t,e,n){var i=t[1]-t[0],r=(e=z(e,(function(e){return{interval:[(e.interval[0]-t[0])/i,(e.interval[1]-t[0])/i]}}))).length,o=0;return function(t){var i;for(i=o;i=0;i--){var a;if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}}return i>=0&&i0?1:-1}(n,o,r,i,c),function(t,e,n,i,r,o,a,s,l,u){var h,c=l.valueDim,p=l.categoryDim,d=Math.abs(n[p.wh]),f=t.getItemVisual(e,"symbolSize");h=Y(f)?f.slice():null==f?["100%","100%"]:[f,f];h[p.index]=Ur(h[p.index],d),h[c.index]=Ur(h[c.index],i?d:Math.abs(o)),u.symbolSize=h;var g=u.symbolScale=[h[0]/s,h[1]/s];g[c.index]*=(l.isHorizontal?-1:1)*a}(t,e,r,o,0,c.boundingLength,c.pxSign,u,i,c),function(t,e,n,i,r){var o=t.get(iO)||0;o&&(oO.attr({scaleX:e[0],scaleY:e[1],rotation:n}),oO.updateTransform(),o/=oO.getLineScale(),o*=e[i.valueDim.index]);r.valueLineWidth=o||0}(n,c.symbolScale,l,i,c);var p=c.symbolSize,d=Yy(n.get("symbolOffset"),p);return function(t,e,n,i,r,o,a,s,l,u,h,c){var p=h.categoryDim,d=h.valueDim,f=c.pxSign,g=Math.max(e[d.index]+s,0),y=g;if(i){var v=Math.abs(l),m=it(t.get("symbolMargin"),"15%")+"",x=!1;m.lastIndexOf("!")===m.length-1&&(x=!0,m=m.slice(0,m.length-1));var _=Ur(m,e[d.index]),b=Math.max(g+2*_,0),w=x?0:2*_,S=co(i),M=S?i:SO((v+w)/b);b=g+2*(_=(v-M*g)/2/(x?M:Math.max(M-1,1))),w=x?0:2*_,S||"fixed"===i||(M=u?SO((Math.abs(u)+w)/b):0),y=M*b-w,c.repeatTimes=M,c.symbolMargin=_}var I=f*(y/2),T=c.pathPosition=[];T[p.index]=n[p.wh]/2,T[d.index]="start"===a?I:"end"===a?l-I:l/2,o&&(T[0]+=o[0],T[1]+=o[1]);var C=c.bundlePosition=[];C[p.index]=n[p.xy],C[d.index]=n[d.xy];var D=c.barRectShape=A({},n);D[d.wh]=f*Math.max(Math.abs(n[d.wh]),Math.abs(T[d.index]+I)),D[p.wh]=n[p.wh];var k=c.clipShape={};k[p.xy]=-n[p.xy],k[p.wh]=h.ecSize[p.wh],k[d.xy]=0,k[d.wh]=n[d.wh]}(n,p,r,o,0,d,s,c.valueLineWidth,c.boundingLength,c.repeatCutLength,i,c),c}function lO(t,e){return t.toGlobalCoord(t.dataToCoord(t.scale.parse(e)))}function uO(t){var e=t.symbolPatternSize,n=Wy(t.symbolType,-e/2,-e/2,e,e);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function hO(t,e,n,i){var r=t.__pictorialBundle,o=n.symbolSize,a=n.valueLineWidth,s=n.pathPosition,l=e.valueDim,u=n.repeatTimes||0,h=0,c=o[e.valueDim.index]+a+2*n.symbolMargin;for(_O(t,(function(t){t.__pictorialAnimationIndex=h,t.__pictorialRepeatTimes=u,h0:i<0)&&(r=u-1-t),e[l.index]=c*(r-u/2+.5)+s[l.index],{x:e[0],y:e[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function cO(t,e,n,i){var r=t.__pictorialBundle,o=t.__pictorialMainPath;o?bO(o,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(o=t.__pictorialMainPath=uO(n),r.add(o),bO(o,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function pO(t,e,n){var i=A({},e.barRectShape),r=t.__pictorialBarRect;r?bO(r,null,{shape:i},e,n):((r=t.__pictorialBarRect=new zs({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,t.add(r))}function dO(t,e,n,i){if(n.symbolClip){var r=t.__pictorialClipPath,o=A({},n.clipShape),a=e.valueDim,s=n.animationModel,l=n.dataIndex;if(r)fh(r,{shape:o},s,l);else{o[a.wh]=0,r=new zs({shape:o}),t.__pictorialBundle.setClipPath(r),t.__pictorialClipPath=r;var u={};u[a.wh]=n.clipShape[a.wh],Kh[i?"updateProps":"initProps"](r,{shape:u},s,l)}}}function fO(t,e){var n=t.getItemModel(e);return n.getAnimationDelayParams=gO,n.isAnimationEnabled=yO,n}function gO(t){return{index:t.__pictorialAnimationIndex,count:t.__pictorialRepeatTimes}}function yO(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function vO(t,e,n,i){var r=new zr,o=new zr;return r.add(o),r.__pictorialBundle=o,o.x=n.bundlePosition[0],o.y=n.bundlePosition[1],n.symbolRepeat?hO(r,e,n):cO(r,0,n),pO(r,n,i),dO(r,e,n,i),r.__pictorialShapeStr=xO(t,n),r.__pictorialSymbolMeta=n,r}function mO(t,e,n,i){var r=i.__pictorialBarRect;r&&r.removeTextContent();var o=[];_O(i,(function(t){o.push(t)})),i.__pictorialMainPath&&o.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),E(o,(function(t){vh(t,{scaleX:0,scaleY:0},n,e,(function(){i.parent&&i.parent.remove(i)}))})),t.setItemGraphicEl(e,null)}function xO(t,e){return[t.getItemVisual(e.dataIndex,"symbol")||"none",!!e.symbolRepeat,!!e.symbolClip].join(":")}function _O(t,e,n){E(t.__pictorialBundle.children(),(function(i){i!==t.__pictorialBarRect&&e.call(n,i)}))}function bO(t,e,n,i,r,o){e&&t.attr(e),i.symbolClip&&!r?n&&t.attr(n):n&&Kh[r?"updateProps":"initProps"](t,n,i.animationModel,i.dataIndex,o)}function wO(t,e,n){var i=n.dataIndex,r=n.itemModel,o=r.getModel("emphasis"),a=o.getModel("itemStyle").getItemStyle(),s=r.getModel(["blur","itemStyle"]).getItemStyle(),l=r.getModel(["select","itemStyle"]).getItemStyle(),u=r.getShallow("cursor"),h=o.get("focus"),c=o.get("blurScope"),p=o.get("scale");_O(t,(function(t){if(t instanceof ks){var e=t.style;t.useStyle(A({image:e.image,x:e.x,y:e.y,width:e.width,height:e.height},n.style))}else t.useStyle(n.style);var i=t.ensureState("emphasis");i.style=a,p&&(i.scaleX=1.1*t.scaleX,i.scaleY=1.1*t.scaleY),t.ensureState("blur").style=s,t.ensureState("select").style=l,u&&(t.cursor=u),t.z2=n.z2}));var d=e.valueDim.posDesc[+(n.boundingLength>0)];tc(t.__pictorialBarRect,ec(r),{labelFetcher:e.seriesModel,labelDataIndex:i,defaultText:iS(e.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:d}),Yl(t,h,c,o.get("disabled"))}function SO(t){var e=Math.round(t);return Math.abs(t-e)<1e-4?e:Math.ceil(t)}var MO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}return n(e,t),e.prototype.getInitialData=function(e){return e.stack=null,t.prototype.getInitialData.apply(this,arguments)},e.type="series.pictorialBar",e.dependencies=["grid"],e.defaultOption=Cc(FS.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:"#212121"}}}),e}(FS);var IO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._layers=[],n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this,o=this.group,a=t.getLayerSeries(),s=i.getLayout("layoutInfo"),l=s.rect,u=s.boundaryGap;function h(t){return t.name}o.x=0,o.y=l.y+u[0];var c=new Vm(this._layersSeries||[],a,h,h),p=[];function d(e,n,s){var l=r._layers;if("remove"!==e){for(var u,h,c=[],d=[],f=a[n].indices,g=0;go&&(o=s),i.push(s)}for(var u=0;uo&&(o=c)}return{y0:r,max:o}}(l),h=u.y0,c=n/u.max,p=o.length,d=o[0].indices.length,f=0;fMath.PI/2?"right":"left"):S&&"center"!==S?"left"===S?(m=r.r0+w,a>Math.PI/2&&(S="right")):"right"===S&&(m=r.r-w,a>Math.PI/2&&(S="left")):(m=o===2*Math.PI&&0===r.r0?0:(r.r+r.r0)/2,S="center"),g.style.align=S,g.style.verticalAlign=f(p,"verticalAlign")||"middle",g.x=m*s+r.cx,g.y=m*l+r.cy;var M=f(p,"rotate"),I=0;"radial"===M?(I=hs(-a))>Math.PI/2&&I<1.5*Math.PI&&(I+=Math.PI):"tangential"===M?(I=Math.PI/2-a)>Math.PI/2?I-=Math.PI:I<-Math.PI/2&&(I+=Math.PI):j(M)&&(I=M*Math.PI/180),g.rotation=hs(I)})),h.dirtyStyle()},e}(zu),kO="sunburstRootToNode",LO="sunburstHighlight";var PO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this;this.seriesModel=t,this.api=n,this.ecModel=e;var o=t.getData(),a=o.tree.root,s=t.getViewRoot(),l=this.group,u=t.get("renderLabelForZeroData"),h=[];s.eachNode((function(t){h.push(t)}));var c=this._oldChildren||[];!function(i,r){if(0===i.length&&0===r.length)return;function s(t){return t.getId()}function h(s,h){!function(i,r){u||!i||i.getValue()||(i=null);if(i!==a&&r!==a)if(r&&r.piece)i?(r.piece.updateData(!1,i,t,e,n),o.setItemGraphicEl(i.dataIndex,r.piece)):function(t){if(!t)return;t.piece&&(l.remove(t.piece),t.piece=null)}(r);else if(i){var s=new AO(i,t,e,n);l.add(s),o.setItemGraphicEl(i.dataIndex,s)}}(null==s?null:i[s],null==h?null:r[h])}new Vm(r,i,s,s).add(h).update(h).remove(H(h,null)).execute()}(h,c),function(i,o){o.depth>0?(r.virtualPiece?r.virtualPiece.updateData(!1,i,t,e,n):(r.virtualPiece=new AO(i,t,e,n),l.add(r.virtualPiece)),o.piece.off("click"),r.virtualPiece.on("click",(function(t){r._rootToNode(o.parentNode)}))):r.virtualPiece&&(l.remove(r.virtualPiece),r.virtualPiece=null)}(a,s),this._initEvents(),this._oldChildren=h},e.prototype._initEvents=function(){var t=this;this.group.off("click"),this.group.on("click",(function(e){var n=!1;t.seriesModel.getViewRoot().eachNode((function(i){if(!n&&i.piece&&i.piece===e.target){var r=i.getModel().get("nodeClick");if("rootToNode"===r)t._rootToNode(i);else if("link"===r){var o=i.getModel(),a=o.get("link");if(a)bp(a,o.get("target",!0)||"_blank")}n=!0}}))}))},e.prototype._rootToNode=function(t){t!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:kO,from:this.uid,seriesId:this.seriesModel.id,targetNode:t})},e.prototype.containPoint=function(t,e){var n=e.getData().getItemLayout(0);if(n){var i=t[0]-n.cx,r=t[1]-n.cy,o=Math.sqrt(i*i+r*r);return o<=n.r&&o>=n.r0}},e.type="sunburst",e}(kg),OO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.ignoreStyleOnData=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};RO(n);var i=this._levelModels=z(t.levels||[],(function(t){return new Mc(t,this,e)}),this),r=UC.createTree(n,this,(function(t){t.wrapMethod("getItemModel",(function(t,e){var n=r.getNodeByDataIndex(e),o=i[n.depth];return o&&(t.parentModel=o),t}))}));return r.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treePathInfo=KC(i,this),n},e.prototype.getLevelModel=function(t){return this._levelModels&&this._levelModels[t.depth]},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){nD(this)},e.type="series.sunburst",e.defaultOption={z:2,center:["50%","50%"],radius:[0,"75%"],clockwise:!0,startAngle:90,minAngle:0,stillShowZeroSum:!0,nodeClick:"rootToNode",renderLabelForZeroData:!1,label:{rotate:"radial",show:!0,opacity:1,align:"center",position:"inside",distance:5,silent:!0},itemStyle:{borderWidth:1,borderColor:"white",borderType:"solid",shadowBlur:0,shadowColor:"rgba(0, 0, 0, 0.2)",shadowOffsetX:0,shadowOffsetY:0,opacity:1},emphasis:{focus:"descendant"},blur:{itemStyle:{opacity:.2},label:{opacity:.1}},animationType:"expansion",animationDuration:1e3,animationDurationUpdate:500,data:[],sort:"desc"},e}(mg);function RO(t){var e=0;E(t.children,(function(t){RO(t);var n=t.value;Y(n)&&(n=n[0]),e+=n}));var n=t.value;Y(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),Y(t.value)?t.value[0]=n:t.value=n}var NO=Math.PI/180;function EO(t,e,n){e.eachSeriesByType(t,(function(t){var e=t.get("center"),i=t.get("radius");Y(i)||(i=[0,i]),Y(e)||(e=[e,e]);var r=n.getWidth(),o=n.getHeight(),a=Math.min(r,o),s=Ur(e[0],r),l=Ur(e[1],o),u=Ur(i[0],a/2),h=Ur(i[1],a/2),c=-t.get("startAngle")*NO,p=t.get("minAngle")*NO,d=t.getData().tree.root,f=t.getViewRoot(),g=f.depth,y=t.get("sort");null!=y&&zO(f,y);var v=0;E(f.children,(function(t){!isNaN(t.getValue())&&v++}));var m=f.getValue(),x=Math.PI/(m||v)*2,_=f.depth>0,b=f.height-(_?-1:1),w=(h-u)/(b||1),S=t.get("clockwise"),M=t.get("stillShowZeroSum"),I=S?1:-1,T=function(e,n){if(e){var i=n;if(e!==d){var r=e.getValue(),o=0===m&&M?x:r*x;o1;)r=r.parentNode;var o=n.getColorFromPalette(r.name||r.dataIndex+"",e);return t.depth>1&&U(o)&&(o=$n(o,(t.depth-1)/(i-1)*.5)),o}(r,t,i.root.height)),A(n.ensureUniqueItemVisual(r.dataIndex,"style"),o)}))}))}var BO={color:"fill",borderColor:"stroke"},FO={symbol:1,symbolSize:1,symbolKeepAspect:1,legendIcon:1,visualMeta:1,liftZ:1,decal:1},GO=Oo(),WO=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){this.currentZLevel=this.get("zlevel",!0),this.currentZ=this.get("z",!0)},e.prototype.getInitialData=function(t,e){return vx(null,this)},e.prototype.getDataParams=function(e,n,i){var r=t.prototype.getDataParams.call(this,e,n);return i&&(r.info=GO(i).info),r},e.type="series.custom",e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,clip:!1},e}(mg);function HO(t,e){return e=e||[0,0],z(["x","y"],(function(n,i){var r=this.getAxis(n),o=e[i],a=t[i]/2;return"category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a))}),this)}function YO(t,e){return e=e||[0,0],z([0,1],(function(n){var i=e[n],r=t[n]/2,o=[],a=[];return o[n]=i-r,a[n]=i+r,o[1-n]=a[1-n]=e[1-n],Math.abs(this.dataToPoint(o)[n]-this.dataToPoint(a)[n])}),this)}function XO(t,e){var n=this.getAxis(),i=e instanceof Array?e[0]:e,r=(t instanceof Array?t[0]:t)/2;return"category"===n.type?n.getBandWidth():Math.abs(n.dataToCoord(i-r)-n.dataToCoord(i+r))}function UO(t,e){return e=e||[0,0],z(["Radius","Angle"],(function(n,i){var r=this["get"+n+"Axis"](),o=e[i],a=t[i]/2,s="category"===r.type?r.getBandWidth():Math.abs(r.dataToCoord(o-a)-r.dataToCoord(o+a));return"Angle"===n&&(s=s*Math.PI/180),s}),this)}function ZO(t,e,n,i){return t&&(t.legacy||!1!==t.legacy&&!n&&!i&&"tspan"!==e&&("text"===e||_t(t,"text")))}function jO(t,e,n){var i,r,o,a=t;if("text"===e)o=a;else{o={},_t(a,"text")&&(o.text=a.text),_t(a,"rich")&&(o.rich=a.rich),_t(a,"textFill")&&(o.fill=a.textFill),_t(a,"textStroke")&&(o.stroke=a.textStroke),_t(a,"fontFamily")&&(o.fontFamily=a.fontFamily),_t(a,"fontSize")&&(o.fontSize=a.fontSize),_t(a,"fontStyle")&&(o.fontStyle=a.fontStyle),_t(a,"fontWeight")&&(o.fontWeight=a.fontWeight),r={type:"text",style:o,silent:!0},i={};var s=_t(a,"textPosition");n?i.position=s?a.textPosition:"inside":s&&(i.position=a.textPosition),_t(a,"textPosition")&&(i.position=a.textPosition),_t(a,"textOffset")&&(i.offset=a.textOffset),_t(a,"textRotation")&&(i.rotation=a.textRotation),_t(a,"textDistance")&&(i.distance=a.textDistance)}return qO(o,t),E(o.rich,(function(t){qO(t,t)})),{textConfig:i,textContent:r}}function qO(t,e){e&&(e.font=e.textFont||e.font,_t(e,"textStrokeWidth")&&(t.lineWidth=e.textStrokeWidth),_t(e,"textAlign")&&(t.align=e.textAlign),_t(e,"textVerticalAlign")&&(t.verticalAlign=e.textVerticalAlign),_t(e,"textLineHeight")&&(t.lineHeight=e.textLineHeight),_t(e,"textWidth")&&(t.width=e.textWidth),_t(e,"textHeight")&&(t.height=e.textHeight),_t(e,"textBackgroundColor")&&(t.backgroundColor=e.textBackgroundColor),_t(e,"textPadding")&&(t.padding=e.textPadding),_t(e,"textBorderColor")&&(t.borderColor=e.textBorderColor),_t(e,"textBorderWidth")&&(t.borderWidth=e.textBorderWidth),_t(e,"textBorderRadius")&&(t.borderRadius=e.textBorderRadius),_t(e,"textBoxShadowColor")&&(t.shadowColor=e.textBoxShadowColor),_t(e,"textBoxShadowBlur")&&(t.shadowBlur=e.textBoxShadowBlur),_t(e,"textBoxShadowOffsetX")&&(t.shadowOffsetX=e.textBoxShadowOffsetX),_t(e,"textBoxShadowOffsetY")&&(t.shadowOffsetY=e.textBoxShadowOffsetY))}function KO(t,e,n){var i=t;i.textPosition=i.textPosition||n.position||"inside",null!=n.offset&&(i.textOffset=n.offset),null!=n.rotation&&(i.textRotation=n.rotation),null!=n.distance&&(i.textDistance=n.distance);var r=i.textPosition.indexOf("inside")>=0,o=t.fill||"#000";$O(i,e);var a=null==i.textFill;return r?a&&(i.textFill=n.insideFill||"#fff",!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=o),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(a&&(i.textFill=t.fill||n.outsideFill||"#000"),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=e.text,i.rich=e.rich,E(e.rich,(function(t){$O(t,t)})),i}function $O(t,e){e&&(_t(e,"fill")&&(t.textFill=e.fill),_t(e,"stroke")&&(t.textStroke=e.fill),_t(e,"lineWidth")&&(t.textStrokeWidth=e.lineWidth),_t(e,"font")&&(t.font=e.font),_t(e,"fontStyle")&&(t.fontStyle=e.fontStyle),_t(e,"fontWeight")&&(t.fontWeight=e.fontWeight),_t(e,"fontSize")&&(t.fontSize=e.fontSize),_t(e,"fontFamily")&&(t.fontFamily=e.fontFamily),_t(e,"align")&&(t.textAlign=e.align),_t(e,"verticalAlign")&&(t.textVerticalAlign=e.verticalAlign),_t(e,"lineHeight")&&(t.textLineHeight=e.lineHeight),_t(e,"width")&&(t.textWidth=e.width),_t(e,"height")&&(t.textHeight=e.height),_t(e,"backgroundColor")&&(t.textBackgroundColor=e.backgroundColor),_t(e,"padding")&&(t.textPadding=e.padding),_t(e,"borderColor")&&(t.textBorderColor=e.borderColor),_t(e,"borderWidth")&&(t.textBorderWidth=e.borderWidth),_t(e,"borderRadius")&&(t.textBorderRadius=e.borderRadius),_t(e,"shadowColor")&&(t.textBoxShadowColor=e.shadowColor),_t(e,"shadowBlur")&&(t.textBoxShadowBlur=e.shadowBlur),_t(e,"shadowOffsetX")&&(t.textBoxShadowOffsetX=e.shadowOffsetX),_t(e,"shadowOffsetY")&&(t.textBoxShadowOffsetY=e.shadowOffsetY),_t(e,"textShadowColor")&&(t.textShadowColor=e.textShadowColor),_t(e,"textShadowBlur")&&(t.textShadowBlur=e.textShadowBlur),_t(e,"textShadowOffsetX")&&(t.textShadowOffsetX=e.textShadowOffsetX),_t(e,"textShadowOffsetY")&&(t.textShadowOffsetY=e.textShadowOffsetY))}var JO={position:["x","y"],scale:["scaleX","scaleY"],origin:["originX","originY"]},QO=G(JO),tR=(V(yr,(function(t,e){return t[e]=1,t}),{}),yr.join(", "),["","style","shape","extra"]),eR=Oo();function nR(t,e,n,i,r){var o=t+"Animation",a=ph(t,i,r)||{},s=eR(e).userDuring;return a.duration>0&&(a.during=s?W(uR,{el:e,userDuring:s}):null,a.setToFinal=!0,a.scope=t),A(a,n[o]),a}function iR(t,e,n,i){var r=(i=i||{}).dataIndex,o=i.isInit,a=i.clearStyle,s=n.isAnimationEnabled(),l=eR(t),u=e.style;l.userDuring=e.during;var h={},c={};if(function(t,e,n){for(var i=0;i=0)){var c=t.getAnimationStyleProps(),p=c?c.style:null;if(p){!r&&(r=i.style={});var d=G(n);for(u=0;u0&&t.animateFrom(p,d)}else!function(t,e,n,i,r){if(r){var o=nR("update",t,e,i,n);o.duration>0&&t.animateFrom(r,o)}}(t,e,r||0,n,h);rR(t,e),u?t.dirty():t.markRedraw()}function rR(t,e){for(var n=eR(t).leaveToProps,i=0;i=0){!o&&(o=i[t]={});var p=G(a);for(h=0;hi[1]&&i.reverse(),{coordSys:{type:"polar",cx:t.cx,cy:t.cy,r:i[1],r0:i[0]},api:{coord:function(i){var r=e.dataToRadius(i[0]),o=n.dataToAngle(i[1]),a=t.coordToPoint([r,o]);return a.push(r,o*Math.PI/180),a},size:W(UO,t)}}},calendar:function(t){var e=t.getRect(),n=t.getRangeInfo();return{coordSys:{type:"calendar",x:e.x,y:e.y,width:e.width,height:e.height,cellWidth:t.getCellWidth(),cellHeight:t.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(e,n){return t.dataToPoint(e,n)}}}}};function CR(t){return t instanceof Is}function DR(t){return t instanceof Sa}var AR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){this._progressiveEls=null;var r=this._data,o=t.getData(),a=this.group,s=RR(t,o,e,n);r||a.removeAll(),o.diff(r).add((function(e){ER(n,null,e,s(e,i),t,a,o)})).remove((function(e){var n=r.getItemGraphicEl(e);n&&oR(n,GO(n).option,t)})).update((function(e,l){var u=r.getItemGraphicEl(l);ER(n,u,e,s(e,i),t,a,o)})).execute();var l=t.get("clip",!0)?SS(t.coordinateSystem,!1,t):null;l?a.setClipPath(l):a.removeClipPath(),this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll(),this._data=null},e.prototype.incrementalRender=function(t,e,n,i,r){var o=e.getData(),a=RR(e,o,n,i),s=this._progressiveEls=[];function l(t){t.isGroup||(t.incremental=!0,t.ensureState("emphasis").hoverLayer=!0)}for(var u=t.start;u=0?e.getStore().get(r,n):void 0}var o=e.get(i.name,n),a=i&&i.ordinalMeta;return a?a.categories[o]:o},styleEmphasis:function(n,i){0;null==i&&(i=s);var r=m(i,vR).getItemStyle(),o=x(i,vR),a=nc(o,null,null,!0,!0);a.text=o.getShallow("show")?ot(t.getFormattedLabel(i,vR),t.getFormattedLabel(i,mR),iS(e,i)):null;var l=ic(o,null,!0);return b(n,r),r=KO(r,a,l),n&&_(r,n),r.legacy=!0,r},visual:function(t,n){if(null==n&&(n=s),_t(BO,t)){var i=e.getItemVisual(n,"style");return i?i[BO[t]]:null}if(_t(FO,t))return e.getItemVisual(n,t)},barLayout:function(t){if("cartesian2d"===o.type){return function(t){var e=[],n=t.axis,i="axis0";if("category"===n.type){for(var r=n.getBandWidth(),o=0;o=c;f--){var g=e.childAt(f);WR(e,g,r)}}(t,c,n,i,r),a>=0?o.replaceAt(c,a):o.add(c),c}function VR(t,e,n){var i,r=GO(t),o=e.type,a=e.shape,s=e.style;return n.isUniversalTransitionEnabled()||null!=o&&o!==r.customGraphicType||"path"===o&&((i=a)&&(_t(i,"pathData")||_t(i,"d")))&&UR(a)!==r.customPathData||"image"===o&&_t(s,"image")&&s.image!==r.customImagePath}function BR(t,e,n){var i=e?FR(t,e):t,r=e?GR(t,i,vR):t.style,o=t.type,a=i?i.textConfig:null,s=t.textContent,l=s?e?FR(s,e):s:null;if(r&&(n.isLegacy||ZO(r,o,!!a,!!l))){n.isLegacy=!0;var u=jO(r,o,!e);!a&&u.textConfig&&(a=u.textConfig),!l&&u.textContent&&(l=u.textContent)}if(!e&&l){var h=l;!h.type&&(h.type="text")}var c=e?n[e]:n.normal;c.cfg=a,c.conOpt=l}function FR(t,e){return e?t?t[e]:null:t}function GR(t,e,n){var i=e&&e.style;return null==i&&n===vR&&t&&(i=t.styleEmphasis),i}function WR(t,e,n){e&&oR(e,GO(t).option,n)}function HR(t,e){var n=t&&t.name;return null!=n?n:"e\0\0"+e}function YR(t,e){var n=this.context,i=null!=t?n.newChildren[t]:null,r=null!=e?n.oldChildren[e]:null;zR(n.api,r,n.dataIndex,i,n.seriesModel,n.group)}function XR(t){var e=this.context,n=e.oldChildren[t];n&&oR(n,GO(n).option,e.seriesModel)}function UR(t){return t&&(t.pathData||t.d)}var ZR=Oo(),jR=T,qR=W,KR=function(){function t(){this._dragging=!1,this.animationThreshold=15}return t.prototype.render=function(t,e,n,i){var r=e.get("value"),o=e.get("status");if(this._axisModel=t,this._axisPointerModel=e,this._api=n,i||this._lastValue!==r||this._lastStatus!==o){this._lastValue=r,this._lastStatus=o;var a=this._group,s=this._handle;if(!o||"hide"===o)return a&&a.hide(),void(s&&s.hide());a&&a.show(),s&&s.show();var l={};this.makeElOption(l,r,t,e,n);var u=l.graphicKey;u!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=u;var h=this._moveAnimation=this.determineAnimation(t,e);if(a){var c=H($R,e,h);this.updatePointerEl(a,l,c),this.updateLabelEl(a,l,c,e)}else a=this._group=new zr,this.createPointerEl(a,l,t,e),this.createLabelEl(a,l,t,e),n.getZr().add(a);eN(a,e,!0),this._renderHandle(r)}},t.prototype.remove=function(t){this.clear(t)},t.prototype.dispose=function(t){this.clear(t)},t.prototype.determineAnimation=function(t,e){var n=e.get("animation"),i=t.axis,r="category"===i.type,o=e.get("snap");if(!o&&!r)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(r&&i.getBandWidth()>a)return!0;if(o){var s=pI(t).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>a}return!1}return!0===n},t.prototype.makeElOption=function(t,e,n,i,r){},t.prototype.createPointerEl=function(t,e,n,i){var r=e.pointer;if(r){var o=ZR(t).pointerEl=new Kh[r.type](jR(e.pointer));t.add(o)}},t.prototype.createLabelEl=function(t,e,n,i){if(e.label){var r=ZR(t).labelEl=new Fs(jR(e.label));t.add(r),QR(r,i)}},t.prototype.updatePointerEl=function(t,e,n){var i=ZR(t).pointerEl;i&&e.pointer&&(i.setStyle(e.pointer.style),n(i,{shape:e.pointer.shape}))},t.prototype.updateLabelEl=function(t,e,n,i){var r=ZR(t).labelEl;r&&(r.setStyle(e.label.style),n(r,{x:e.label.x,y:e.label.y}),QR(r,i))},t.prototype._renderHandle=function(t){if(!this._dragging&&this.updateHandleTransform){var e,n=this._axisPointerModel,i=this._api.getZr(),r=this._handle,o=n.getModel("handle"),a=n.get("status");if(!o.get("show")||!a||"hide"===a)return r&&i.remove(r),void(this._handle=null);this._handle||(e=!0,r=this._handle=Hh(o.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(t){de(t.event)},onmousedown:qR(this._onHandleDragMove,this,0,0),drift:qR(this._onHandleDragMove,this),ondragend:qR(this._onHandleDragEnd,this)}),i.add(r)),eN(r,n,!1),r.setStyle(o.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=o.get("size");Y(s)||(s=[s,s]),r.scaleX=s[0]/2,r.scaleY=s[1]/2,Fg(this,"_doDispatchAxisPointer",o.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,e)}},t.prototype._moveHandleToValue=function(t,e){$R(this._axisPointerModel,!e&&this._moveAnimation,this._handle,tN(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},t.prototype._onHandleDragMove=function(t,e){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(tN(n),[t,e],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(tN(i)),ZR(n).lastProp=null,this._doDispatchAxisPointer()}},t.prototype._doDispatchAxisPointer=function(){if(this._handle){var t=this._payloadInfo,e=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:e.axis.dim,axisIndex:e.componentIndex}]})}},t.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},t.prototype.clear=function(t){this._lastValue=null,this._lastStatus=null;var e=t.getZr(),n=this._group,i=this._handle;e&&n&&(this._lastGraphicKey=null,n&&e.remove(n),i&&e.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),Gg(this,"_doDispatchAxisPointer")},t.prototype.doClear=function(){},t.prototype.buildLabel=function(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}},t}();function $R(t,e,n,i){JR(ZR(n).lastProp,i)||(ZR(n).lastProp=i,e?fh(n,i,t):(n.stopAnimation(),n.attr(i)))}function JR(t,e){if(q(t)&&q(e)){var n=!0;return E(e,(function(e,i){n=n&&JR(t[i],e)})),!!n}return t===e}function QR(t,e){t[e.get(["label","show"])?"show":"hide"]()}function tN(t){return{x:t.x||0,y:t.y||0,rotation:t.rotation||0}}function eN(t,e,n){var i=e.get("z"),r=e.get("zlevel");t&&t.traverse((function(t){"group"!==t.type&&(null!=i&&(t.z=i),null!=r&&(t.zlevel=r),t.silent=n)}))}function nN(t){var e,n=t.get("type"),i=t.getModel(n+"Style");return"line"===n?(e=i.getLineStyle()).fill=null:"shadow"===n&&((e=i.getAreaStyle()).stroke=null),e}function iN(t,e,n,i,r){var o=rN(n.get("value"),e.axis,e.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),a=n.getModel("label"),s=fp(a.get("padding")||0),l=a.getFont(),u=br(o,l),h=r.position,c=u.width+s[1]+s[3],p=u.height+s[0]+s[2],d=r.align;"right"===d&&(h[0]-=c),"center"===d&&(h[0]-=c/2);var f=r.verticalAlign;"bottom"===f&&(h[1]-=p),"middle"===f&&(h[1]-=p/2),function(t,e,n,i){var r=i.getWidth(),o=i.getHeight();t[0]=Math.min(t[0]+e,r)-e,t[1]=Math.min(t[1]+n,o)-n,t[0]=Math.max(t[0],0),t[1]=Math.max(t[1],0)}(h,c,p,i);var g=a.get("backgroundColor");g&&"auto"!==g||(g=e.get(["axisLine","lineStyle","color"])),t.label={x:h[0],y:h[1],style:nc(a,{text:o,font:l,fill:a.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function rN(t,e,n,i,r){t=e.scale.parse(t);var o=e.scale.getLabel({value:t},{precision:r.precision}),a=r.formatter;if(a){var s={value:__(e,{value:t}),axisDimension:e.dim,axisIndex:e.index,seriesData:[]};E(i,(function(t){var e=n.getSeriesByIndex(t.seriesIndex),i=t.dataIndexInside,r=e&&e.getDataParams(i);r&&s.seriesData.push(r)})),U(a)?o=a.replace("{value}",o):X(a)&&(o=a(s))}return o}function oN(t,e,n){var i=[1,0,0,1,0,0];return Se(i,i,n.rotation),we(i,i,n.position),zh([t.dataToCoord(e),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function aN(t,e,n,i,r,o){var a=iI.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get(["label","margin"]),iN(e,i,r,o,{position:oN(i.axis,t,n),align:a.textAlign,verticalAlign:a.textVerticalAlign})}function sN(t,e,n){return{x1:t[n=n||0],y1:t[1-n],x2:e[n],y2:e[1-n]}}function lN(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}}function uN(t,e,n,i,r,o){return{cx:t,cy:e,r0:n,r:i,startAngle:r,endAngle:o,clockwise:!0}}var hN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.grid,s=i.get("type"),l=cN(a,o).getOtherAxis(o).getGlobalExtent(),u=o.toGlobalCoord(o.dataToCoord(e,!0));if(s&&"none"!==s){var h=nN(i),c=pN[s](o,u,l);c.style=h,t.graphicKey=c.type,t.pointer=c}aN(e,t,ZM(a.model,n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=ZM(e.axis.grid.model,e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=oN(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.grid,a=r.getGlobalExtent(!0),s=cN(o,r).getOtherAxis(r).getGlobalExtent(),l="x"===r.dim?0:1,u=[t.x,t.y];u[l]+=e[l],u[l]=Math.min(a[1],u[l]),u[l]=Math.max(a[0],u[l]);var h=(s[1]+s[0])/2,c=[h,h];c[l]=u[l];return{x:u[0],y:u[1],rotation:t.rotation,cursorPoint:c,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},e}(KR);function cN(t,e){var n={};return n[e.dim+"AxisIndex"]=e.index,t.getCartesian(n)}var pN={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:sN([e,n[0]],[e,n[1]],dN(t))}},shadow:function(t,e,n){var i=Math.max(1,t.getBandWidth()),r=n[1]-n[0];return{type:"Rect",shape:lN([e-i/2,n[0]],[i,r],dN(t))}}};function dN(t){return"x"===t.dim?0:1}var fN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="axisPointer",e.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:"#B9BEC9",width:1,type:"dashed"},shadowStyle:{color:"rgba(210,219,238,0.2)"},label:{show:!0,formatter:null,precision:"auto",margin:3,color:"#fff",padding:[5,7,5,7],backgroundColor:"auto",borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:"#333",shadowBlur:3,shadowColor:"#aaa",shadowOffsetX:0,shadowOffsetY:2,throttle:40}},e}(Rp),gN=Oo(),yN=E;function vN(t,e,n){if(!r.node){var i=e.getZr();gN(i).records||(gN(i).records={}),function(t,e){if(gN(t).initialized)return;function n(n,i){t.on(n,(function(n){var r=function(t){var e={showTip:[],hideTip:[]},n=function(i){var r=e[i.type];r?r.push(i):(i.dispatchAction=n,t.dispatchAction(i))};return{dispatchAction:n,pendings:e}}(e);yN(gN(t).records,(function(t){t&&i(t,n,r.dispatchAction)})),function(t,e){var n,i=t.showTip.length,r=t.hideTip.length;i?n=t.showTip[i-1]:r&&(n=t.hideTip[r-1]);n&&(n.dispatchAction=null,e.dispatchAction(n))}(r.pendings,e)}))}gN(t).initialized=!0,n("click",H(xN,"click")),n("mousemove",H(xN,"mousemove")),n("globalout",mN)}(i,e),(gN(i).records[t]||(gN(i).records[t]={})).handler=n}}function mN(t,e,n){t.handler("leave",null,n)}function xN(t,e,n,i){e.handler(t,n,i)}function _N(t,e){if(!r.node){var n=e.getZr();(gN(n).records||{})[t]&&(gN(n).records[t]=null)}}var bN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=e.getComponent("tooltip"),r=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click";vN("axisPointer",n,(function(t,e,n){"none"!==r&&("leave"===t||r.indexOf(t)>=0)&&n({type:"updateAxisPointer",currTrigger:t,x:e&&e.offsetX,y:e&&e.offsetY})}))},e.prototype.remove=function(t,e){_N("axisPointer",e)},e.prototype.dispose=function(t,e){_N("axisPointer",e)},e.type="axisPointer",e}(Tg);function wN(t,e){var n,i=[],r=t.seriesIndex;if(null==r||!(n=e.getSeriesByIndex(r)))return{point:[]};var o=n.getData(),a=Po(o,t);if(null==a||a<0||Y(a))return{point:[]};var s=o.getItemGraphicEl(a),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(a)||[];else if(l&&l.dataToPoint)if(t.isStacked){var u=l.getBaseAxis(),h=l.getOtherAxis(u).dim,c=u.dim,p="x"===h||"radius"===h?1:0,d=o.mapDimension(c),f=[];f[p]=o.get(d,a),f[1-p]=o.get(o.getCalculationInfo("stackResultDimension"),a),i=l.dataToPoint(f)||[]}else i=l.dataToPoint(o.getValues(z(l.dimensions,(function(t){return o.mapDimension(t)})),a))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var SN=Oo();function MN(t,e,n){var i=t.currTrigger,r=[t.x,t.y],o=t,a=t.dispatchAction||W(n.dispatchAction,n),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){AN(r)&&(r=wN({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},e).point);var l=AN(r),u=o.axesInfo,h=s.axesInfo,c="leave"===i||AN(r),p={},d={},f={list:[],map:{}},g={showPointer:H(TN,d),showTooltip:H(CN,f)};E(s.coordSysMap,(function(t,e){var n=l||t.containPoint(r);E(s.coordSysAxesInfo[e],(function(t,e){var i=t.axis,o=function(t,e){for(var n=0;n<(t||[]).length;n++){var i=t[n];if(e.axis.dim===i.axisDim&&e.axis.model.componentIndex===i.axisIndex)return i}}(u,t);if(!c&&n&&(!u||o)){var a=o&&o.value;null!=a||l||(a=i.pointToData(r)),null!=a&&IN(t,a,g,!1,p)}}))}));var y={};return E(h,(function(t,e){var n=t.linkGroup;n&&!d[e]&&E(n.axesInfo,(function(e,i){var r=d[i];if(e!==t&&r){var o=r.value;n.mapper&&(o=t.axis.scale.parse(n.mapper(o,DN(e),DN(t)))),y[t.key]=o}}))})),E(y,(function(t,e){IN(h[e],t,g,!0,p)})),function(t,e,n){var i=n.axesInfo=[];E(e,(function(e,n){var r=e.axisPointerModel.option,o=t[n];o?(!e.useHandle&&(r.status="show"),r.value=o.value,r.seriesDataIndices=(o.payloadBatch||[]).slice()):!e.useHandle&&(r.status="hide"),"show"===r.status&&i.push({axisDim:e.axis.dim,axisIndex:e.axis.model.componentIndex,value:r.value})}))}(d,h,p),function(t,e,n,i){if(AN(e)||!t.list.length)return void i({type:"hideTip"});var r=((t.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:t.list})}(f,r,t,a),function(t,e,n){var i=n.getZr(),r="axisPointerLastHighlights",o=SN(i)[r]||{},a=SN(i)[r]={};E(t,(function(t,e){var n=t.axisPointerModel.option;"show"===n.status&&t.triggerEmphasis&&E(n.seriesDataIndices,(function(t){var e=t.seriesIndex+" | "+t.dataIndex;a[e]=t}))}));var s=[],l=[];E(o,(function(t,e){!a[e]&&l.push(t)})),E(a,(function(t,e){!o[e]&&s.push(t)})),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(h,0,n),p}}function IN(t,e,n,i,r){var o=t.axis;if(!o.scale.isBlank()&&o.containData(e))if(t.involveSeries){var a=function(t,e){var n=e.axis,i=n.dim,r=t,o=[],a=Number.MAX_VALUE,s=-1;return E(e.seriesModels,(function(e,l){var u,h,c=e.getData().mapDimensionsAll(i);if(e.getAxisTooltipData){var p=e.getAxisTooltipData(c,t,n);h=p.dataIndices,u=p.nestestValue}else{if(!(h=e.getData().indicesOfNearest(c[0],t,"category"===n.type?.5:null)).length)return;u=e.getData().get(c[0],h[0])}if(null!=u&&isFinite(u)){var d=t-u,f=Math.abs(d);f<=a&&((f=0&&s<0)&&(a=f,s=d,r=u,o.length=0),E(h,(function(t){o.push({seriesIndex:e.seriesIndex,dataIndexInside:t,dataIndex:e.getData().getRawIndex(t)})})))}})),{payloadBatch:o,snapToValue:r}}(e,t),s=a.payloadBatch,l=a.snapToValue;s[0]&&null==r.seriesIndex&&A(r,s[0]),!i&&t.snap&&o.containData(l)&&null!=l&&(e=l),n.showPointer(t,e,s),n.showTooltip(t,a,l)}else n.showPointer(t,e)}function TN(t,e,n,i){t[e.key]={value:n,payloadBatch:i}}function CN(t,e,n,i){var r=n.payloadBatch,o=e.axis,a=o.model,s=e.axisPointerModel;if(e.triggerTooltip&&r.length){var l=e.coordSys.model,u=fI(l),h=t.map[u];h||(h=t.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},t.list.push(h)),h.dataByAxis.push({axisDim:o.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:r.slice()})}}function DN(t){var e=t.axis.model,n={},i=n.axisDim=t.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=e.componentIndex,n.axisName=n[i+"AxisName"]=e.name,n.axisId=n[i+"AxisId"]=e.id,n}function AN(t){return!t||null==t[0]||isNaN(t[0])||null==t[1]||isNaN(t[1])}function kN(t){yI.registerAxisPointerClass("CartesianAxisPointer",hN),t.registerComponentModel(fN),t.registerComponentView(bN),t.registerPreprocessor((function(t){if(t){(!t.axisPointer||0===t.axisPointer.length)&&(t.axisPointer={});var e=t.axisPointer.link;e&&!Y(e)&&(t.axisPointer.link=[e])}})),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,(function(t,e){t.getComponent("axisPointer").coordSysAxesInfo=uI(t,e)})),t.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},MN)}var LN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis;"angle"===o.dim&&(this.animationThreshold=Math.PI/18);var a=o.polar,s=a.getOtherAxis(o).getExtent(),l=o.dataToCoord(e),u=i.get("type");if(u&&"none"!==u){var h=nN(i),c=PN[u](o,a,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}var p=function(t,e,n,i,r){var o=e.axis,a=o.dataToCoord(t),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,u,h,c=i.getRadiusAxis().getExtent();if("radius"===o.dim){var p=[1,0,0,1,0,0];Se(p,p,s),we(p,p,[i.cx,i.cy]),l=zh([a,-r],p);var d=e.getModel("axisLabel").get("rotate")||0,f=iI.innerTextLayout(s,d*Math.PI/180,-1);u=f.textAlign,h=f.textVerticalAlign}else{var g=c[1];l=i.coordToPoint([g+r,a]);var y=i.cx,v=i.cy;u=Math.abs(l[0]-y)/g<.3?"center":l[0]>y?"left":"right",h=Math.abs(l[1]-v)/g<.3?"middle":l[1]>v?"top":"bottom"}return{position:l,align:u,verticalAlign:h}}(e,n,0,a,i.get(["label","margin"]));iN(t,n,i,r,p)},e}(KR);var PN={line:function(t,e,n,i){return"angle"===t.dim?{type:"Line",shape:sN(e.coordToPoint([i[0],n]),e.coordToPoint([i[1],n]))}:{type:"Circle",shape:{cx:e.cx,cy:e.cy,r:n}}},shadow:function(t,e,n,i){var r=Math.max(1,t.getBandWidth()),o=Math.PI/180;return"angle"===t.dim?{type:"Sector",shape:uN(e.cx,e.cy,i[0],i[1],(-n-r/2)*o,(r/2-n)*o)}:{type:"Sector",shape:uN(e.cx,e.cy,n-r/2,n+r/2,0,2*Math.PI)}}},ON=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.findAxisModel=function(t){var e;return this.ecModel.eachComponent(t,(function(t){t.getCoordSysModel()===this&&(e=t)}),this),e},e.type="polar",e.dependencies=["radiusAxis","angleAxis"],e.defaultOption={z:0,center:["50%","50%"],radius:"80%"},e}(Rp),RN=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",zo).models[0]},e.type="polarAxis",e}(Rp);R(RN,I_);var NN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="angleAxis",e}(RN),EN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="radiusAxis",e}(RN),zN=function(t){function e(e,n){return t.call(this,"radius",e,n)||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e}(nb);zN.prototype.dataToRadius=nb.prototype.dataToCoord,zN.prototype.radiusToData=nb.prototype.coordToData;var VN=Oo(),BN=function(t){function e(e,n){return t.call(this,"angle",e,n||[0,360])||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e.prototype.calculateCategoryInterval=function(){var t=this,e=t.getLabelModel(),n=t.scale,i=n.getExtent(),r=n.count();if(i[1]-i[0]<1)return 0;var o=i[0],a=t.dataToCoord(o+1)-t.dataToCoord(o),s=Math.abs(a),l=br(null==o?"":o+"",e.getFont(),"center","top"),u=Math.max(l.height,7)/s;isNaN(u)&&(u=1/0);var h=Math.max(0,Math.floor(u)),c=VN(t.model),p=c.lastAutoInterval,d=c.lastTickCount;return null!=p&&null!=d&&Math.abs(p-h)<=1&&Math.abs(d-r)<=1&&p>h?h=p:(c.lastTickCount=r,c.lastAutoInterval=h),h},e}(nb);BN.prototype.dataToAngle=nb.prototype.dataToCoord,BN.prototype.angleToData=nb.prototype.coordToData;var FN=["radius","angle"],GN=function(){function t(t){this.dimensions=FN,this.type="polar",this.cx=0,this.cy=0,this._radiusAxis=new zN,this._angleAxis=new BN,this.axisPointerEnabled=!0,this.name=t||"",this._radiusAxis.polar=this._angleAxis.polar=this}return t.prototype.containPoint=function(t){var e=this.pointToCoord(t);return this._radiusAxis.contain(e[0])&&this._angleAxis.contain(e[1])},t.prototype.containData=function(t){return this._radiusAxis.containData(t[0])&&this._angleAxis.containData(t[1])},t.prototype.getAxis=function(t){return this["_"+t+"Axis"]},t.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},t.prototype.getAxesByScale=function(t){var e=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===t&&e.push(n),i.scale.type===t&&e.push(i),e},t.prototype.getAngleAxis=function(){return this._angleAxis},t.prototype.getRadiusAxis=function(){return this._radiusAxis},t.prototype.getOtherAxis=function(t){var e=this._angleAxis;return t===e?this._radiusAxis:e},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},t.prototype.getTooltipAxes=function(t){var e=null!=t&&"auto"!==t?this.getAxis(t):this.getBaseAxis();return{baseAxes:[e],otherAxes:[this.getOtherAxis(e)]}},t.prototype.dataToPoint=function(t,e){return this.coordToPoint([this._radiusAxis.dataToRadius(t[0],e),this._angleAxis.dataToAngle(t[1],e)])},t.prototype.pointToData=function(t,e){var n=this.pointToCoord(t);return[this._radiusAxis.radiusToData(n[0],e),this._angleAxis.angleToData(n[1],e)]},t.prototype.pointToCoord=function(t){var e=t[0]-this.cx,n=t[1]-this.cy,i=this.getAngleAxis(),r=i.getExtent(),o=Math.min(r[0],r[1]),a=Math.max(r[0],r[1]);i.inverse?o=a-360:a=o+360;var s=Math.sqrt(e*e+n*n);e/=s,n/=s;for(var l=Math.atan2(-n,e)/Math.PI*180,u=la;)l+=360*u;return[s,l]},t.prototype.coordToPoint=function(t){var e=t[0],n=t[1]/180*Math.PI;return[Math.cos(n)*e+this.cx,-Math.sin(n)*e+this.cy]},t.prototype.getArea=function(){var t=this.getAngleAxis(),e=this.getRadiusAxis().getExtent().slice();e[0]>e[1]&&e.reverse();var n=t.getExtent(),i=Math.PI/180;return{cx:this.cx,cy:this.cy,r0:e[0],r:e[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:t.inverse,contain:function(t,e){var n=t-this.cx,i=e-this.cy,r=n*n+i*i-1e-4,o=this.r,a=this.r0;return r<=o*o&&r>=a*a}}},t.prototype.convertToPixel=function(t,e,n){return WN(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return WN(e)===this?this.pointToData(n):null},t}();function WN(t){var e=t.seriesModel,n=t.polarModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}function HN(t,e){var n=this,i=n.getAngleAxis(),r=n.getRadiusAxis();if(i.scale.setExtent(1/0,-1/0),r.scale.setExtent(1/0,-1/0),t.eachSeries((function(t){if(t.coordinateSystem===n){var e=t.getData();E(M_(e,"radius"),(function(t){r.scale.unionExtentFromData(e,t)})),E(M_(e,"angle"),(function(t){i.scale.unionExtentFromData(e,t)}))}})),v_(i.scale,i.model),v_(r.scale,r.model),"category"===i.type&&!i.onBand){var o=i.getExtent(),a=360/i.scale.count();i.inverse?o[1]+=a:o[1]-=a,i.setExtent(o[0],o[1])}}function YN(t,e){if(t.type=e.get("type"),t.scale=m_(e),t.onBand=e.get("boundaryGap")&&"category"===t.type,t.inverse=e.get("inverse"),function(t){return"angleAxis"===t.mainType}(e)){t.inverse=t.inverse!==e.get("clockwise");var n=e.get("startAngle");t.setExtent(n,n+(t.inverse?-360:360))}e.axis=t,t.model=e}var XN={dimensions:FN,create:function(t,e){var n=[];return t.eachComponent("polar",(function(t,i){var r=new GN(i+"");r.update=HN;var o=r.getRadiusAxis(),a=r.getAngleAxis(),s=t.findAxisModel("radiusAxis"),l=t.findAxisModel("angleAxis");YN(o,s),YN(a,l),function(t,e,n){var i=e.get("center"),r=n.getWidth(),o=n.getHeight();t.cx=Ur(i[0],r),t.cy=Ur(i[1],o);var a=t.getRadiusAxis(),s=Math.min(r,o)/2,l=e.get("radius");null==l?l=[0,"100%"]:Y(l)||(l=[0,l]);var u=[Ur(l[0],s),Ur(l[1],s)];a.inverse?a.setExtent(u[1],u[0]):a.setExtent(u[0],u[1])}(r,t,e),n.push(r),t.coordinateSystem=r,r.model=t})),t.eachSeries((function(t){if("polar"===t.get("coordinateSystem")){var e=t.getReferringComponents("polar",zo).models[0];0,t.coordinateSystem=e.coordinateSystem}})),n}},UN=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function ZN(t,e,n){e[1]>e[0]&&(e=e.slice().reverse());var i=t.coordToPoint([e[0],n]),r=t.coordToPoint([e[1],n]);return{x1:i[0],y1:i[1],x2:r[0],y2:r[1]}}function jN(t){return t.getRadiusAxis().inverse?0:1}function qN(t){var e=t[0],n=t[t.length-1];e&&n&&Math.abs(Math.abs(e.coord-n.coord)-360)<1e-4&&t.pop()}var KN=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="PolarAxisPointer",n}return n(e,t),e.prototype.render=function(t,e){if(this.group.removeAll(),t.get("show")){var n=t.axis,i=n.polar,r=i.getRadiusAxis().getExtent(),o=n.getTicksCoords(),a=n.getMinorTicksCoords(),s=z(n.getViewLabels(),(function(t){t=T(t);var e=n.scale,i="ordinal"===e.type?e.getRawOrdinalNumber(t.tickValue):t.tickValue;return t.coord=n.dataToCoord(i),t}));qN(s),qN(o),E(UN,(function(e){!t.get([e,"show"])||n.scale.isBlank()&&"axisLine"!==e||$N[e](this.group,t,i,o,a,r,s)}),this)}},e.type="angleAxis",e}(yI),$N={axisLine:function(t,e,n,i,r,o){var a,s=e.getModel(["axisLine","lineStyle"]),l=jN(n),u=l?0:1;(a=0===o[u]?new _u({shape:{cx:n.cx,cy:n.cy,r:o[l]},style:s.getLineStyle(),z2:1,silent:!0}):new Bu({shape:{cx:n.cx,cy:n.cy,r:o[l],r0:o[u]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,t.add(a)},axisTick:function(t,e,n,i,r,o){var a=e.getModel("axisTick"),s=(a.get("inside")?-1:1)*a.get("length"),l=o[jN(n)],u=z(i,(function(t){return new Zu({shape:ZN(n,[l,l+s],t.coord)})}));t.add(Ph(u,{style:k(a.getModel("lineStyle").getLineStyle(),{stroke:e.get(["axisLine","lineStyle","color"])})}))},minorTick:function(t,e,n,i,r,o){if(r.length){for(var a=e.getModel("axisTick"),s=e.getModel("minorTick"),l=(a.get("inside")?-1:1)*s.get("length"),u=o[jN(n)],h=[],c=0;cf?"left":"right",v=Math.abs(d[1]-g)/p<.3?"middle":d[1]>g?"top":"bottom";if(s&&s[c]){var m=s[c];q(m)&&m.textStyle&&(a=new Mc(m.textStyle,l,l.ecModel))}var x=new Fs({silent:iI.isLabelSilent(e),style:nc(a,{x:d[0],y:d[1],fill:a.getTextColor()||e.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:y,verticalAlign:v})});if(t.add(x),h){var _=iI.makeAxisEventDataBase(e);_.targetType="axisLabel",_.value=i.rawLabel,Qs(x).eventData=_}}),this)},splitLine:function(t,e,n,i,r,o){var a=e.getModel("splitLine").getModel("lineStyle"),s=a.get("color"),l=0;s=s instanceof Array?s:[s];for(var u=[],h=0;h=0?"p":"n",T=_;m&&(i[s][M]||(i[s][M]={p:_,n:_}),T=i[s][M][I]);var C=void 0,D=void 0,A=void 0,k=void 0;if("radius"===c.dim){var L=c.dataToCoord(S)-_,P=o.dataToCoord(M);Math.abs(L)=k})}}}))}var oE={startAngle:90,clockwise:!0,splitNumber:12,axisLabel:{rotate:0}},aE={splitNumber:5},sE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="polar",e}(Tg);function lE(t,e){e=e||{};var n=t.coordinateSystem,i=t.axis,r={},o=i.position,a=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],u={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};r.position=["vertical"===a?u.vertical[o]:l[0],"horizontal"===a?u.horizontal[o]:l[3]];r.rotation=Math.PI/2*{horizontal:0,vertical:1}[a];r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,right:1,left:-1}[o],t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),it(e.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var h=e.rotate;return null==h&&(h=t.get(["axisLabel","rotate"])),r.labelRotation="top"===o?-h:h,r.z2=1,r}var uE=["axisLine","axisTickLabel","axisName"],hE=["splitArea","splitLine"],cE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="SingleAxisPointer",n}return n(e,t),e.prototype.render=function(e,n,i,r){var o=this.group;o.removeAll();var a=this._axisGroup;this._axisGroup=new zr;var s=lE(e),l=new iI(e,s);E(uE,l.add,l),o.add(this._axisGroup),o.add(l.getGroup()),E(hE,(function(t){e.get([t,"show"])&&pE[t](this,this.group,this._axisGroup,e)}),this),Fh(a,this._axisGroup,e),t.prototype.render.call(this,e,n,i,r)},e.prototype.remove=function(){xI(this)},e.type="singleAxis",e}(yI),pE={splitLine:function(t,e,n,i){var r=i.axis;if(!r.scale.isBlank()){var o=i.getModel("splitLine"),a=o.getModel("lineStyle"),s=a.get("color");s=s instanceof Array?s:[s];for(var l=a.get("width"),u=i.coordinateSystem.getRect(),h=r.isHorizontal(),c=[],p=0,d=r.getTicksCoords({tickModel:o}),f=[],g=[],y=0;y=e.y&&t[1]<=e.y+e.height:n.contain(n.toLocalCoord(t[1]))&&t[0]>=e.y&&t[0]<=e.y+e.height},t.prototype.pointToData=function(t){var e=this.getAxis();return[e.coordToData(e.toLocalCoord(t["horizontal"===e.orient?0:1]))]},t.prototype.dataToPoint=function(t){var e=this.getAxis(),n=this.getRect(),i=[],r="horizontal"===e.orient?0:1;return t instanceof Array&&(t=t[0]),i[r]=e.toGlobalCoord(e.dataToCoord(+t)),i[1-r]=0===r?n.y+n.height/2:n.x+n.width/2,i},t.prototype.convertToPixel=function(t,e,n){return vE(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return vE(e)===this?this.pointToData(n):null},t}();function vE(t){var e=t.seriesModel,n=t.singleAxisModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}var mE={create:function(t,e){var n=[];return t.eachComponent("singleAxis",(function(i,r){var o=new yE(i,t,e);o.name="single_"+r,o.resize(i,e),i.coordinateSystem=o,n.push(o)})),t.eachSeries((function(t){if("singleAxis"===t.get("coordinateSystem")){var e=t.getReferringComponents("singleAxis",zo).models[0];t.coordinateSystem=e&&e.coordinateSystem}})),n},dimensions:gE},xE=["x","y"],_E=["width","height"],bE=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.coordinateSystem,s=ME(a,1-SE(o)),l=a.dataToPoint(e)[0],u=i.get("type");if(u&&"none"!==u){var h=nN(i),c=wE[u](o,l,s);c.style=h,t.graphicKey=c.type,t.pointer=c}aN(e,t,lE(n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=lE(e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=oN(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.coordinateSystem,a=SE(r),s=ME(o,a),l=[t.x,t.y];l[a]+=e[a],l[a]=Math.min(s[1],l[a]),l[a]=Math.max(s[0],l[a]);var u=ME(o,1-a),h=(u[1]+u[0])/2,c=[h,h];return c[a]=l[a],{x:l[0],y:l[1],rotation:t.rotation,cursorPoint:c,tooltipOption:{verticalAlign:"middle"}}},e}(KR),wE={line:function(t,e,n){return{type:"Line",subPixelOptimize:!0,shape:sN([e,n[0]],[e,n[1]],SE(t))}},shadow:function(t,e,n){var i=t.getBandWidth(),r=n[1]-n[0];return{type:"Rect",shape:lN([e-i/2,n[0]],[i,r],SE(t))}}};function SE(t){return t.isHorizontal()?0:1}function ME(t,e){var n=t.getRect();return[n[xE[e]],n[xE[e]]+n[_E[e]]]}var IE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="single",e}(Tg);var TE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e,n,i){var r=Lp(e);t.prototype.init.apply(this,arguments),CE(e,r)},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),CE(this.option,e)},e.prototype.getCellSize=function(){return this.option.cellSize},e.type="calendar",e.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:"#000",width:1,type:"solid"}},itemStyle:{color:"#fff",borderWidth:1,borderColor:"#ccc"},dayLabel:{show:!0,firstDay:0,position:"start",margin:"50%",color:"#000"},monthLabel:{show:!0,position:"start",margin:5,align:"center",formatter:null,color:"#000"},yearLabel:{show:!0,position:null,margin:30,formatter:null,color:"#ccc",fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},e}(Rp);function CE(t,e){var n,i=t.cellSize;1===(n=Y(i)?i:t.cellSize=[i,i]).length&&(n[1]=n[0]);var r=z([0,1],(function(t){return function(t,e){return null!=t[Mp[e][0]]||null!=t[Mp[e][1]]&&null!=t[Mp[e][2]]}(e,t)&&(n[t]="auto"),null!=n[t]&&"auto"!==n[t]}));kp(t,e,{type:"box",ignoreSize:r})}var DE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=this.group;i.removeAll();var r=t.coordinateSystem,o=r.getRangeInfo(),a=r.getOrient(),s=e.getLocaleModel();this._renderDayRect(t,o,i),this._renderLines(t,o,a,i),this._renderYearText(t,o,a,i),this._renderMonthText(t,s,a,i),this._renderWeekText(t,s,o,a,i)},e.prototype._renderDayRect=function(t,e,n){for(var i=t.coordinateSystem,r=t.getModel("itemStyle").getItemStyle(),o=i.getCellWidth(),a=i.getCellHeight(),s=e.start.time;s<=e.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToRect([s],!1).tl,u=new zs({shape:{x:l[0],y:l[1],width:o,height:a},cursor:"default",style:r});n.add(u)}},e.prototype._renderLines=function(t,e,n,i){var r=this,o=t.coordinateSystem,a=t.getModel(["splitLine","lineStyle"]).getLineStyle(),s=t.get(["splitLine","show"]),l=a.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var u=e.start,h=0;u.time<=e.end.time;h++){p(u.formatedDate),0===h&&(u=o.getDateInfo(e.start.y+"-"+e.start.m));var c=u.date;c.setMonth(c.getMonth()+1),u=o.getDateInfo(c)}function p(e){r._firstDayOfMonth.push(o.getDateInfo(e)),r._firstDayPoints.push(o.dataToRect([e],!1).tl);var l=r._getLinePointsOfOneWeek(t,e,n);r._tlpoints.push(l[0]),r._blpoints.push(l[l.length-1]),s&&r._drawSplitline(l,a,i)}p(o.getNextNDay(e.end.time,1).formatedDate),s&&this._drawSplitline(r._getEdgesPoints(r._tlpoints,l,n),a,i),s&&this._drawSplitline(r._getEdgesPoints(r._blpoints,l,n),a,i)},e.prototype._getEdgesPoints=function(t,e,n){var i=[t[0].slice(),t[t.length-1].slice()],r="horizontal"===n?0:1;return i[0][r]=i[0][r]-e/2,i[1][r]=i[1][r]+e/2,i},e.prototype._drawSplitline=function(t,e,n){var i=new Yu({z2:20,shape:{points:t},style:e});n.add(i)},e.prototype._getLinePointsOfOneWeek=function(t,e,n){for(var i=t.coordinateSystem,r=i.getDateInfo(e),o=[],a=0;a<7;a++){var s=i.getNextNDay(r.time,a),l=i.dataToRect([s.time],!1);o[2*s.day]=l.tl,o[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return o},e.prototype._formatterLabel=function(t,e){return U(t)&&t?(n=t,E(e,(function(t,e){n=n.replace("{"+e+"}",i?re(t):t)})),n):X(t)?t(e):e.nameMap;var n,i},e.prototype._yearTextPositionControl=function(t,e,n,i,r){var o=e[0],a=e[1],s=["center","bottom"];"bottom"===i?(a+=r,s=["center","top"]):"left"===i?o-=r:"right"===i?(o+=r,s=["center","top"]):a-=r;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:o,y:a,style:{align:s[0],verticalAlign:s[1]}}},e.prototype._renderYearText=function(t,e,n,i){var r=t.getModel("yearLabel");if(r.get("show")){var o=r.get("margin"),a=r.get("position");a||(a="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,u=(s[0][1]+s[1][1])/2,h="horizontal"===n?0:1,c={top:[l,s[h][1]],bottom:[l,s[1-h][1]],left:[s[1-h][0],u],right:[s[h][0],u]},p=e.start.y;+e.end.y>+e.start.y&&(p=p+"-"+e.end.y);var d=r.get("formatter"),f={start:e.start.y,end:e.end.y,nameMap:p},g=this._formatterLabel(d,f),y=new Fs({z2:30,style:nc(r,{text:g})});y.attr(this._yearTextPositionControl(y,c[a],n,a,o)),i.add(y)}},e.prototype._monthTextPositionControl=function(t,e,n,i,r){var o="left",a="top",s=t[0],l=t[1];return"horizontal"===n?(l+=r,e&&(o="center"),"start"===i&&(a="bottom")):(s+=r,e&&(a="middle"),"start"===i&&(o="right")),{x:s,y:l,align:o,verticalAlign:a}},e.prototype._renderMonthText=function(t,e,n,i){var r=t.getModel("monthLabel");if(r.get("show")){var o=r.get("nameMap"),a=r.get("margin"),s=r.get("position"),l=r.get("align"),u=[this._tlpoints,this._blpoints];o&&!U(o)||(o&&(e=Nc(o)||e),o=e.get(["time","monthAbbr"])||[]);var h="start"===s?0:1,c="horizontal"===n?0:1;a="start"===s?-a:a;for(var p="center"===l,d=0;d=i.start.time&&n.timea.end.time&&t.reverse(),t},t.prototype._getRangeInfo=function(t){var e,n=[this.getDateInfo(t[0]),this.getDateInfo(t[1])];n[0].time>n[1].time&&(e=!0,n.reverse());var i=Math.floor(n[1].time/AE)-Math.floor(n[0].time/AE)+1,r=new Date(n[0].time),o=r.getDate(),a=n[1].date.getDate();r.setDate(o+i-1);var s=r.getDate();if(s!==a)for(var l=r.getTime()-n[1].time>0?1:-1;(s=r.getDate())!==a&&(r.getTime()-n[1].time)*l>0;)i-=l,r.setDate(s-l);var u=Math.floor((i+n[0].day+6)/7),h=e?1-u:u-1;return e&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:u,nthWeek:h,fweek:n[0].day,lweek:n[1].day}},t.prototype._getDateByWeeksAndDay=function(t,e,n){var i=this._getRangeInfo(n);if(t>i.weeks||0===t&&ei.lweek)return null;var r=7*(t-1)-i.fweek+e,o=new Date(i.start.time);return o.setDate(+i.start.d+r),this.getDateInfo(o)},t.create=function(e,n){var i=[];return e.eachComponent("calendar",(function(r){var o=new t(r,e,n);i.push(o),r.coordinateSystem=o})),e.eachSeries((function(t){"calendar"===t.get("coordinateSystem")&&(t.coordinateSystem=i[t.get("calendarIndex")||0])})),i},t.dimensions=["time","value"],t}();function LE(t){var e=t.calendarModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem:null}function PE(t,e){var n;return E(e,(function(e){null!=t[e]&&"auto"!==t[e]&&(n=!0)})),n}var OE=["transition","enterFrom","leaveTo"],RE=OE.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function NE(t,e,n){if(n&&(!t[n]&&e[n]&&(t[n]={}),t=t[n],e=e[n]),t&&e)for(var i=n?OE:RE,r=0;r=0;l--){var p,d,f;if(f=null!=(d=Ao((p=n[l]).id,null))?r.get(d):null){var g=f.parent,y=(c=VE(g),{}),v=Dp(f,p,g===i?{width:o,height:a}:{width:c.width,height:c.height},null,{hv:p.hv,boundingMode:p.bounding},y);if(!VE(f).isNew&&v){for(var m=p.transition,x={},_=0;_=0)?x[b]=w:f[b]=w}fh(f,x,t,0)}else f.attr(y)}}},e.prototype._clear=function(){var t=this,e=this._elMap;e.each((function(n){WE(n,VE(n).option,e,t._lastGraphicModel)})),this._elMap=yt()},e.prototype.dispose=function(){this._clear()},e.type="graphic",e}(Tg);function FE(t){var e=_t(zE,t)?zE[t]:Dh(t);var n=new e({});return VE(n).type=t,n}function GE(t,e,n,i){var r=FE(n);return e.add(r),i.set(t,r),VE(r).id=t,VE(r).isNew=!0,r}function WE(t,e,n,i){t&&t.parent&&("group"===t.type&&t.traverse((function(t){WE(t,e,n,i)})),oR(t,e,i),n.removeKey(VE(t).id))}function HE(t,e,n,i){t.isGroup||E([["cursor",Sa.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],(function(n){var i=n[0];_t(e,i)?t[i]=rt(e[i],n[1]):null==t[i]&&(t[i]=n[1])})),E(G(e),(function(n){if(0===n.indexOf("on")){var i=e[n];t[n]=X(i)?i:null}})),_t(e,"draggable")&&(t.draggable=e.draggable),null!=e.name&&(t.name=e.name),null!=e.id&&(t.id=e.id)}var YE=["x","y","radius","angle","single"],XE=["cartesian2d","polar","singleAxis"];function UE(t){return t+"Axis"}function ZE(t,e){var n,i=yt(),r=[],o=yt();t.eachComponent({mainType:"dataZoom",query:e},(function(t){o.get(t.uid)||s(t)}));do{n=!1,t.eachComponent("dataZoom",a)}while(n);function a(t){!o.get(t.uid)&&function(t){var e=!1;return t.eachTargetAxis((function(t,n){var r=i.get(t);r&&r[n]&&(e=!0)})),e}(t)&&(s(t),n=!0)}function s(t){o.set(t.uid,!0),r.push(t),t.eachTargetAxis((function(t,e){(i.get(t)||i.set(t,[]))[e]=!0}))}return r}function jE(t){var e=t.ecModel,n={infoList:[],infoMap:yt()};return t.eachTargetAxis((function(t,i){var r=e.getComponent(UE(t),i);if(r){var o=r.getCoordSysModel();if(o){var a=o.uid,s=n.infoMap.get(a);s||(s={model:o,axisModels:[]},n.infoList.push(s),n.infoMap.set(a,s)),s.axisModels.push(r)}}})),n}var qE=function(){function t(){this.indexList=[],this.indexMap=[]}return t.prototype.add=function(t){this.indexMap[t]||(this.indexList.push(t),this.indexMap[t]=!0)},t}(),KE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return n(e,t),e.prototype.init=function(t,e,n){var i=$E(t);this.settledOption=i,this.mergeDefaultAndTheme(t,n),this._doInit(i)},e.prototype.mergeOption=function(t){var e=$E(t);C(this.option,t,!0),C(this.settledOption,e,!0),this._doInit(e)},e.prototype._doInit=function(t){var e=this.option;this._setDefaultThrottle(t),this._updateRangeUse(t);var n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(t,i){"value"===this._rangePropMode[i]&&(e[t[0]]=n[t[0]]=null)}),this),this._resetTarget()},e.prototype._resetTarget=function(){var t=this.get("orient",!0),e=this._targetAxisInfoMap=yt();this._fillSpecifiedTargetAxis(e)?this._orient=t||this._makeAutoOrientByTargetAxis():(this._orient=t||"horizontal",this._fillAutoTargetAxisByOrient(e,this._orient)),this._noTarget=!0,e.each((function(t){t.indexList.length&&(this._noTarget=!1)}),this)},e.prototype._fillSpecifiedTargetAxis=function(t){var e=!1;return E(YE,(function(n){var i=this.getReferringComponents(UE(n),Vo);if(i.specified){e=!0;var r=new qE;E(i.models,(function(t){r.add(t.componentIndex)})),t.set(n,r)}}),this),e},e.prototype._fillAutoTargetAxisByOrient=function(t,e){var n=this.ecModel,i=!0;if(i){var r="vertical"===e?"y":"x";o(n.findComponents({mainType:r+"Axis"}),r)}i&&o(n.findComponents({mainType:"singleAxis",filter:function(t){return t.get("orient",!0)===e}}),"single");function o(e,n){var r=e[0];if(r){var o=new qE;if(o.add(r.componentIndex),t.set(n,o),i=!1,"x"===n||"y"===n){var a=r.getReferringComponents("grid",zo).models[0];a&&E(e,(function(t){r.componentIndex!==t.componentIndex&&a===t.getReferringComponents("grid",zo).models[0]&&o.add(t.componentIndex)}))}}}i&&E(YE,(function(e){if(i){var r=n.findComponents({mainType:UE(e),filter:function(t){return"category"===t.get("type",!0)}});if(r[0]){var o=new qE;o.add(r[0].componentIndex),t.set(e,o),i=!1}}}),this)},e.prototype._makeAutoOrientByTargetAxis=function(){var t;return this.eachTargetAxis((function(e){!t&&(t=e)}),this),"y"===t?"vertical":"horizontal"},e.prototype._setDefaultThrottle=function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var e=this.ecModel.option;this.option.throttle=e.animation&&e.animationDurationUpdate>0?100:20}},e.prototype._updateRangeUse=function(t){var e=this._rangePropMode,n=this.get("rangeMode");E([["start","startValue"],["end","endValue"]],(function(i,r){var o=null!=t[i[0]],a=null!=t[i[1]];o&&!a?e[r]="percent":!o&&a?e[r]="value":n?e[r]=n[r]:o&&(e[r]="percent")}))},e.prototype.noTarget=function(){return this._noTarget},e.prototype.getFirstTargetAxisModel=function(){var t;return this.eachTargetAxis((function(e,n){null==t&&(t=this.ecModel.getComponent(UE(e),n))}),this),t},e.prototype.eachTargetAxis=function(t,e){this._targetAxisInfoMap.each((function(n,i){E(n.indexList,(function(n){t.call(e,i,n)}))}))},e.prototype.getAxisProxy=function(t,e){var n=this.getAxisModel(t,e);if(n)return n.__dzAxisProxy},e.prototype.getAxisModel=function(t,e){var n=this._targetAxisInfoMap.get(t);if(n&&n.indexMap[e])return this.ecModel.getComponent(UE(t),e)},e.prototype.setRawRange=function(t){var e=this.option,n=this.settledOption;E([["start","startValue"],["end","endValue"]],(function(i){null==t[i[0]]&&null==t[i[1]]||(e[i[0]]=n[i[0]]=t[i[0]],e[i[1]]=n[i[1]]=t[i[1]])}),this),this._updateRangeUse(t)},e.prototype.setCalculatedRange=function(t){var e=this.option;E(["start","startValue","end","endValue"],(function(n){e[n]=t[n]}))},e.prototype.getPercentRange=function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getDataPercentWindow()},e.prototype.getValueRange=function(t,e){if(null!=t||null!=e)return this.getAxisProxy(t,e).getDataValueWindow();var n=this.findRepresentativeAxisProxy();return n?n.getDataValueWindow():void 0},e.prototype.findRepresentativeAxisProxy=function(t){if(t)return t.__dzAxisProxy;for(var e,n=this._targetAxisInfoMap.keys(),i=0;i=0}(e)){var n=UE(this._dimName),i=e.getReferringComponents(n,zo).models[0];i&&this._axisIndex===i.componentIndex&&t.push(e)}}),this),t},t.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},t.prototype.getMinMaxSpan=function(){return T(this._minMaxSpan)},t.prototype.calculateDataWindow=function(t){var e,n=this._dataExtent,i=this.getAxisModel().axis.scale,r=this._dataZoomModel.getRangePropMode(),o=[0,100],a=[],s=[];ez(["start","end"],(function(l,u){var h=t[l],c=t[l+"Value"];"percent"===r[u]?(null==h&&(h=o[u]),c=i.parse(Xr(h,o,n))):(e=!0,h=Xr(c=null==c?n[u]:i.parse(c),n,o)),s[u]=null==c||isNaN(c)?n[u]:c,a[u]=null==h||isNaN(h)?o[u]:h})),nz(s),nz(a);var l=this._minMaxSpan;function u(t,e,n,r,o){var a=o?"Span":"ValueSpan";Ck(0,t,n,"all",l["min"+a],l["max"+a]);for(var s=0;s<2;s++)e[s]=Xr(t[s],n,r,!0),o&&(e[s]=i.parse(e[s]))}return e?u(s,a,n,o,!1):u(a,s,o,n,!0),{valueWindow:s,percentWindow:a}},t.prototype.reset=function(t){if(t===this._dataZoomModel){var e=this.getTargetSeriesModels();this._dataExtent=function(t,e,n){var i=[1/0,-1/0];ez(n,(function(t){!function(t,e,n){e&&E(M_(e,n),(function(n){var i=e.getApproximateExtent(n);i[0]t[1]&&(t[1]=i[1])}))}(i,t.getData(),e)}));var r=t.getAxisModel(),o=f_(r.axis.scale,r,i).calculate();return[o.min,o.max]}(this,this._dimName,e),this._updateMinMaxSpan();var n=this.calculateDataWindow(t.settledOption);this._valueWindow=n.valueWindow,this._percentWindow=n.percentWindow,this._setAxisModel()}},t.prototype.filterData=function(t,e){if(t===this._dataZoomModel){var n=this._dimName,i=this.getTargetSeriesModels(),r=t.get("filterMode"),o=this._valueWindow;"none"!==r&&ez(i,(function(t){var e=t.getData(),i=e.mapDimensionsAll(n);if(i.length){if("weakFilter"===r){var a=e.getStore(),s=z(i,(function(t){return e.getDimensionIndex(t)}),e);e.filterSelf((function(t){for(var e,n,r,l=0;lo[1];if(h&&!c&&!p)return!0;h&&(r=!0),c&&(e=!0),p&&(n=!0)}return r&&e&&n}))}else ez(i,(function(n){if("empty"===r)t.setData(e=e.map(n,(function(t){return function(t){return t>=o[0]&&t<=o[1]}(t)?t:NaN})));else{var i={};i[n]=o,e.selectRange(i)}}));ez(i,(function(t){e.setApproximateExtent(o,t)}))}}))}},t.prototype._updateMinMaxSpan=function(){var t=this._minMaxSpan={},e=this._dataZoomModel,n=this._dataExtent;ez(["min","max"],(function(i){var r=e.get(i+"Span"),o=e.get(i+"ValueSpan");null!=o&&(o=this.getAxisModel().axis.scale.parse(o)),null!=o?r=Xr(n[0]+o,n,[0,100],!0):null!=r&&(o=Xr(r,[0,100],n,!0)-n[0]),t[i+"Span"]=r,t[i+"ValueSpan"]=o}),this)},t.prototype._setAxisModel=function(){var t=this.getAxisModel(),e=this._percentWindow,n=this._valueWindow;if(e){var i=$r(n,[0,500]);i=Math.min(i,20);var r=t.axis.scale.rawExtentInfo;0!==e[0]&&r.setDeterminedMinMax("min",+n[0].toFixed(i)),100!==e[1]&&r.setDeterminedMinMax("max",+n[1].toFixed(i)),r.freeze()}},t}();var rz={getTargetSeries:function(t){function e(e){t.eachComponent("dataZoom",(function(n){n.eachTargetAxis((function(i,r){var o=t.getComponent(UE(i),r);e(i,r,o,n)}))}))}e((function(t,e,n,i){n.__dzAxisProxy=null}));var n=[];e((function(e,i,r,o){r.__dzAxisProxy||(r.__dzAxisProxy=new iz(e,i,o,t),n.push(r.__dzAxisProxy))}));var i=yt();return E(n,(function(t){E(t.getTargetSeriesModels(),(function(t){i.set(t.uid,t)}))})),i},overallReset:function(t,e){t.eachComponent("dataZoom",(function(t){t.eachTargetAxis((function(e,n){t.getAxisProxy(e,n).reset(t)})),t.eachTargetAxis((function(n,i){t.getAxisProxy(n,i).filterData(t,e)}))})),t.eachComponent("dataZoom",(function(t){var e=t.findRepresentativeAxisProxy();if(e){var n=e.getDataPercentWindow(),i=e.getDataValueWindow();t.setCalculatedRange({start:n[0],end:n[1],startValue:i[0],endValue:i[1]})}}))}};var oz=!1;function az(t){oz||(oz=!0,t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,rz),function(t){t.registerAction("dataZoom",(function(t,e){E(ZE(e,t),(function(e){e.setRawRange({start:t.start,end:t.end,startValue:t.startValue,endValue:t.endValue})}))}))}(t),t.registerSubTypeDefaulter("dataZoom",(function(){return"slider"})))}function sz(t){t.registerComponentModel(JE),t.registerComponentView(tz),az(t)}var lz=function(){},uz={};function hz(t,e){uz[t]=e}function cz(t){return uz[t]}var pz=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(){t.prototype.optionUpdated.apply(this,arguments);var e=this.ecModel;E(this.option.feature,(function(t,n){var i=cz(n);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(e)),C(t,i.defaultOption))}))},e.type="toolbox",e.layoutMode={type:"box",ignoreSize:!0},e.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemSize:15,itemGap:8,showTitle:!0,iconStyle:{borderColor:"#666",color:"none"},emphasis:{iconStyle:{borderColor:"#3E98C5"}},tooltip:{show:!1,position:"bottom"}},e}(Rp);function dz(t,e){var n=fp(e.get("padding")),i=e.getItemStyle(["color","opacity"]);return i.fill=e.get("backgroundColor"),t=new zs({shape:{x:t.x-n[3],y:t.y-n[0],width:t.width+n[1]+n[3],height:t.height+n[0]+n[2],r:e.get("borderRadius")},style:i,silent:!0,z2:-1})}var fz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this.group;if(r.removeAll(),t.get("show")){var o=+t.get("itemSize"),a="vertical"===t.get("orient"),s=t.get("feature")||{},l=this._features||(this._features={}),u=[];E(s,(function(t,e){u.push(e)})),new Vm(this._featureNames||[],u).add(h).update(h).remove(H(h,null)).execute(),this._featureNames=u,function(t,e,n){var i=e.getBoxLayoutParams(),r=e.get("padding"),o={width:n.getWidth(),height:n.getHeight()},a=Cp(i,o,r);Tp(e.get("orient"),t,e.get("itemGap"),a.width,a.height),Dp(t,i,o,r)}(r,t,n),r.add(dz(r.getBoundingRect(),t)),a||r.eachChild((function(t){var e=t.__title,i=t.ensureState("emphasis"),a=i.textConfig||(i.textConfig={}),s=t.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!X(l)&&e){var u=l.style||(l.style={}),h=br(e,Fs.makeFont(u)),c=t.x+r.x,p=!1;t.y+r.y+o+h.height>n.getHeight()&&(a.position="top",p=!0);var d=p?-5-h.height:o+10;c+h.width/2>n.getWidth()?(a.position=["100%",d],u.align="right"):c-h.width/2<0&&(a.position=[0,d],u.align="left")}}))}function h(h,c){var p,d=u[h],f=u[c],g=s[d],y=new Mc(g,t,t.ecModel);if(i&&null!=i.newTitle&&i.featureName===d&&(g.title=i.newTitle),d&&!f){if(function(t){return 0===t.indexOf("my")}(d))p={onclick:y.option.onclick,featureName:d};else{var v=cz(d);if(!v)return;p=new v}l[d]=p}else if(!(p=l[f]))return;p.uid=Tc("toolbox-feature"),p.model=y,p.ecModel=e,p.api=n;var m=p instanceof lz;d||!f?!y.get("show")||m&&p.unusable?m&&p.remove&&p.remove(e,n):(!function(i,s,l){var u,h,c=i.getModel("iconStyle"),p=i.getModel(["emphasis","iconStyle"]),d=s instanceof lz&&s.getIcons?s.getIcons():i.get("icon"),f=i.get("title")||{};U(d)?(u={})[l]=d:u=d;U(f)?(h={})[l]=f:h=f;var g=i.iconPaths={};E(u,(function(l,u){var d=Hh(l,{},{x:-o/2,y:-o/2,width:o,height:o});d.setStyle(c.getItemStyle()),d.ensureState("emphasis").style=p.getItemStyle();var f=new Fs({style:{text:h[u],align:p.get("textAlign"),borderRadius:p.get("textBorderRadius"),padding:p.get("textPadding"),fill:null},ignore:!0});d.setTextContent(f),Zh({el:d,componentModel:t,itemName:u,formatterParamsExtra:{title:h[u]}}),d.__title=h[u],d.on("mouseover",(function(){var e=p.getItemStyle(),i=a?null==t.get("right")&&"right"!==t.get("left")?"right":"left":null==t.get("bottom")&&"bottom"!==t.get("top")?"bottom":"top";f.setStyle({fill:p.get("textFill")||e.fill||e.stroke||"#000",backgroundColor:p.get("textBackgroundColor")}),d.setTextConfig({position:p.get("textPosition")||i}),f.ignore=!t.get("showTitle"),n.enterEmphasis(this)})).on("mouseout",(function(){"emphasis"!==i.get(["iconStatus",u])&&n.leaveEmphasis(this),f.hide()})),("emphasis"===i.get(["iconStatus",u])?kl:Ll)(d),r.add(d),d.on("click",W(s.onclick,s,e,n,u)),g[u]=d}))}(y,p,d),y.setIconStatus=function(t,e){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[t]=e,i[t]&&("emphasis"===e?kl:Ll)(i[t])},p instanceof lz&&p.render&&p.render(y,e,n,i)):m&&p.dispose&&p.dispose(e,n)}},e.prototype.updateView=function(t,e,n,i){E(this._features,(function(t){t instanceof lz&&t.updateView&&t.updateView(t.model,e,n,i)}))},e.prototype.remove=function(t,e){E(this._features,(function(n){n instanceof lz&&n.remove&&n.remove(t,e)})),this.group.removeAll()},e.prototype.dispose=function(t,e){E(this._features,(function(n){n instanceof lz&&n.dispose&&n.dispose(t,e)}))},e.type="toolbox",e}(Tg);var gz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",o="svg"===e.getZr().painter.getType(),a=o?"svg":n.get("type",!0)||"png",s=e.getConnectedDataURL({type:a,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||"#fff",connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),l=r.browser;if(X(MouseEvent)&&(l.newEdge||!l.ie&&!l.edge)){var u=document.createElement("a");u.download=i+"."+a,u.target="_blank",u.href=s;var h=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});u.dispatchEvent(h)}else if(window.navigator.msSaveOrOpenBlob||o){var c=s.split(","),p=c[0].indexOf("base64")>-1,d=o?decodeURIComponent(c[1]):c[1];p&&(d=window.atob(d));var f=i+"."+a;if(window.navigator.msSaveOrOpenBlob){for(var g=d.length,y=new Uint8Array(g);g--;)y[g]=d.charCodeAt(g);var v=new Blob([y]);window.navigator.msSaveOrOpenBlob(v,f)}else{var m=document.createElement("iframe");document.body.appendChild(m);var x=m.contentWindow,_=x.document;_.open("image/svg+xml","replace"),_.write(d),_.close(),x.focus(),_.execCommand("SaveAs",!0,f),document.body.removeChild(m)}}else{var b=n.get("lang"),w='',S=window.open();S.document.write(w),S.document.title=i}},e.getDefaultOption=function(t){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:"#fff",name:"",excludeComponents:["toolbox"],lang:t.getLocaleModel().get(["toolbox","saveAsImage","lang"])}},e}(lz),yz="__ec_magicType_stack__",vz=[["line","bar"],["stack"]],mz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return E(t.get("type"),(function(t){e[t]&&(n[t]=e[t])})),n},e.getDefaultOption=function(t){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},e.prototype.onclick=function(t,e,n){var i=this.model,r=i.get(["seriesIndex",n]);if(xz[n]){var o,a={series:[]};E(vz,(function(t){P(t,n)>=0&&E(t,(function(t){i.setIconStatus(t,"normal")}))})),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},(function(t){var e=t.subType,r=t.id,o=xz[n](e,r,t,i);o&&(k(o,t.option),a.series.push(o));var s=t.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var u=l.dim+"Axis",h=t.getReferringComponents(u,zo).models[0].componentIndex;a[u]=a[u]||[];for(var c=0;c<=h;c++)a[u][h]=a[u][h]||{};a[u][h].boundaryGap="bar"===n}}}));var s=n;"stack"===n&&(o=C({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),e.dispatchAction({type:"changeMagicType",currentType:s,newOption:a,newTitle:o,featureName:"magicType"})}},e}(lz),xz={line:function(t,e,n,i){if("bar"===t)return C({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(t,e,n,i){if("line"===t)return C({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(t,e,n,i){var r=n.get("stack")===yz;if("line"===t||"bar"===t)return i.setIconStatus("stack",r?"normal":"emphasis"),C({id:e,stack:r?"":yz},i.get(["option","stack"])||{},!0)}};Mm({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},(function(t,e){e.mergeOption(t.newOption)}));var _z=new Array(60).join("-"),bz="\t";function wz(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var Sz=new RegExp("[\t]+","g");function Mz(t,e){var n=t.split(new RegExp("\n*"+_z+"\n*","g")),i={series:[]};return E(n,(function(t,n){if(function(t){if(t.slice(0,t.indexOf("\n")).indexOf(bz)>=0)return!0}(t)){var r=function(t){for(var e=t.split(/\n+/g),n=[],i=z(wz(e.shift()).split(Sz),(function(t){return{name:t,data:[]}})),r=0;r=0)&&t(r,i._targetInfoList)}))}return t.prototype.setOutputRanges=function(t,e){return this.matchOutputRanges(t,e,(function(t,e,n){if((t.coordRanges||(t.coordRanges=[])).push(e),!t.coordRange){t.coordRange=e;var i=Vz[t.brushType](0,n,e);t.__rangeOffset={offset:Fz[t.brushType](i.values,t.range,[1,1]),xyMinMax:i.xyMinMax}}})),t},t.prototype.matchOutputRanges=function(t,e,n){E(t,(function(t){var i=this.findTargetInfo(t,e);i&&!0!==i&&E(i.coordSyses,(function(i){var r=Vz[t.brushType](1,i,t.range,!0);n(t,r.values,i,e)}))}),this)},t.prototype.setInputRanges=function(t,e){E(t,(function(t){var n,i,r,o,a,s=this.findTargetInfo(t,e);if(t.range=t.range||[],s&&!0!==s){t.panelId=s.panelId;var l=Vz[t.brushType](0,s.coordSys,t.coordRange),u=t.__rangeOffset;t.range=u?Fz[t.brushType](l.values,u.offset,(n=l.xyMinMax,i=u.xyMinMax,r=Wz(n),o=Wz(i),a=[r[0]/o[0],r[1]/o[1]],isNaN(a[0])&&(a[0]=1),isNaN(a[1])&&(a[1]=1),a)):l.values}}),this)},t.prototype.makePanelOpts=function(t,e){return z(this._targetInfoList,(function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:e?e(n):null,clipPath:AL(i),isTargetByCursor:LL(i,t,n.coordSysModel),getLinearBrushOtherExtent:kL(i)}}))},t.prototype.controlSeries=function(t,e,n){var i=this.findTargetInfo(t,n);return!0===i||i&&P(i.coordSyses,e.coordinateSystem)>=0},t.prototype.findTargetInfo=function(t,e){for(var n=this._targetInfoList,i=Rz(e,t),r=0;rt[1]&&t.reverse(),t}function Rz(t,e){return No(t,e,{includeMainTypes:Lz})}var Nz={grid:function(t,e){var n=t.xAxisModels,i=t.yAxisModels,r=t.gridModels,o=yt(),a={},s={};(n||i||r)&&(E(n,(function(t){var e=t.axis.grid.model;o.set(e.id,e),a[e.id]=!0})),E(i,(function(t){var e=t.axis.grid.model;o.set(e.id,e),s[e.id]=!0})),E(r,(function(t){o.set(t.id,t),a[t.id]=!0,s[t.id]=!0})),o.each((function(t){var r=t.coordinateSystem,o=[];E(r.getCartesians(),(function(t,e){(P(n,t.getAxis("x").model)>=0||P(i,t.getAxis("y").model)>=0)&&o.push(t)})),e.push({panelId:"grid--"+t.id,gridModel:t,coordSysModel:t,coordSys:o[0],coordSyses:o,getPanelRect:zz.grid,xAxisDeclared:a[t.id],yAxisDeclared:s[t.id]})})))},geo:function(t,e){E(t.geoModels,(function(t){var n=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:n,coordSyses:[n],getPanelRect:zz.geo})}))}},Ez=[function(t,e){var n=t.xAxisModel,i=t.yAxisModel,r=t.gridModel;return!r&&n&&(r=n.axis.grid.model),!r&&i&&(r=i.axis.grid.model),r&&r===e.gridModel},function(t,e){var n=t.geoModel;return n&&n===e.geoModel}],zz={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var t=this.coordSys,e=t.getBoundingRect().clone();return e.applyTransform(Eh(t)),e}},Vz={lineX:H(Bz,0),lineY:H(Bz,1),rect:function(t,e,n,i){var r=t?e.pointToData([n[0][0],n[1][0]],i):e.dataToPoint([n[0][0],n[1][0]],i),o=t?e.pointToData([n[0][1],n[1][1]],i):e.dataToPoint([n[0][1],n[1][1]],i),a=[Oz([r[0],o[0]]),Oz([r[1],o[1]])];return{values:a,xyMinMax:a}},polygon:function(t,e,n,i){var r=[[1/0,-1/0],[1/0,-1/0]];return{values:z(n,(function(n){var o=t?e.pointToData(n,i):e.dataToPoint(n,i);return r[0][0]=Math.min(r[0][0],o[0]),r[1][0]=Math.min(r[1][0],o[1]),r[0][1]=Math.max(r[0][1],o[0]),r[1][1]=Math.max(r[1][1],o[1]),o})),xyMinMax:r}}};function Bz(t,e,n,i){var r=n.getAxis(["x","y"][t]),o=Oz(z([0,1],(function(t){return e?r.coordToData(r.toLocalCoord(i[t]),!0):r.toGlobalCoord(r.dataToCoord(i[t]))}))),a=[];return a[t]=o,a[1-t]=[NaN,NaN],{values:o,xyMinMax:a}}var Fz={lineX:H(Gz,0),lineY:H(Gz,1),rect:function(t,e,n){return[[t[0][0]-n[0]*e[0][0],t[0][1]-n[0]*e[0][1]],[t[1][0]-n[1]*e[1][0],t[1][1]-n[1]*e[1][1]]]},polygon:function(t,e,n){return z(t,(function(t,i){return[t[0]-n[0]*e[i][0],t[1]-n[1]*e[i][1]]}))}};function Gz(t,e,n,i){return[e[0]-i[t]*n[0],e[1]-i[t]*n[1]]}function Wz(t){return t?[t[0][1]-t[0][0],t[1][1]-t[1][0]]:[NaN,NaN]}var Hz,Yz,Xz=E,Uz=_o+"toolbox-dataZoom_",Zz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){this._brushController||(this._brushController=new Jk(n.getZr()),this._brushController.on("brush",W(this._onBrush,this)).mount()),function(t,e,n,i,r){var o=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(o="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=o,t.setIconStatus("zoom",o?"emphasis":"normal");var a=new Pz(qz(t),e,{include:["grid"]}),s=a.makePanelOpts(r,(function(t){return t.xAxisDeclared&&!t.yAxisDeclared?"lineX":!t.xAxisDeclared&&t.yAxisDeclared?"lineY":"rect"}));n._brushController.setPanels(s).enableBrush(!(!o||!s.length)&&{brushType:"auto",brushStyle:t.getModel("brushStyle").getItemStyle()})}(t,e,this,i,n),function(t,e){t.setIconStatus("back",function(t){return Az(t).length}(e)>1?"emphasis":"normal")}(t,e)},e.prototype.onclick=function(t,e,n){jz[n].call(this)},e.prototype.remove=function(t,e){this._brushController&&this._brushController.unmount()},e.prototype.dispose=function(t,e){this._brushController&&this._brushController.dispose()},e.prototype._onBrush=function(t){var e=t.areas;if(t.isEnd&&e.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new Pz(qz(this.model),i,{include:["grid"]}).matchOutputRanges(e,i,(function(t,e,n){if("cartesian2d"===n.type){var i=t.brushType;"rect"===i?(r("x",n,e[0]),r("y",n,e[1])):r({lineX:"x",lineY:"y"}[i],n,e)}})),function(t,e){var n=Az(t);Cz(e,(function(e,i){for(var r=n.length-1;r>=0&&!n[r][i];r--);if(r<0){var o=t.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(o){var a=o.getPercentRange();n[0][i]={dataZoomId:i,start:a[0],end:a[1]}}}})),n.push(e)}(i,n),this._dispatchZoomAction(n)}function r(t,e,r){var o=e.getAxis(t),a=o.model,s=function(t,e,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},(function(n){n.getAxisModel(t,e.componentIndex)&&(i=n)})),i}(t,a,i),l=s.findRepresentativeAxisProxy(a).getMinMaxSpan();null==l.minValueSpan&&null==l.maxValueSpan||(r=Ck(0,r.slice(),o.scale.getExtent(),0,l.minValueSpan,l.maxValueSpan)),s&&(n[s.id]={dataZoomId:s.id,startValue:r[0],endValue:r[1]})}},e.prototype._dispatchZoomAction=function(t){var e=[];Xz(t,(function(t,n){e.push(T(t))})),e.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:e})},e.getDefaultOption=function(t){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:t.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:"rgba(210,219,238,0.2)"}}},e}(lz),jz={zoom:function(){var t=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:t})},back:function(){this._dispatchZoomAction(function(t){var e=Az(t),n=e[e.length-1];e.length>1&&e.pop();var i={};return Cz(n,(function(t,n){for(var r=e.length-1;r>=0;r--)if(t=e[r][n]){i[n]=t;break}})),i}(this.ecModel))}};function qz(t){var e={xAxisIndex:t.get("xAxisIndex",!0),yAxisIndex:t.get("yAxisIndex",!0),xAxisId:t.get("xAxisId",!0),yAxisId:t.get("yAxisId",!0)};return null==e.xAxisIndex&&null==e.xAxisId&&(e.xAxisIndex="all"),null==e.yAxisIndex&&null==e.yAxisId&&(e.yAxisIndex="all"),e}Hz="dataZoom",Yz=function(t){var e=t.getComponent("toolbox",0),n=["feature","dataZoom"];if(e&&null!=e.get(n)){var i=e.getModel(n),r=[],o=No(t,qz(i));return Xz(o.xAxisModels,(function(t){return a(t,"xAxis","xAxisIndex")})),Xz(o.yAxisModels,(function(t){return a(t,"yAxis","yAxisIndex")})),r}function a(t,e,n){var o=t.componentIndex,a={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:Uz+e+o};a[n]=o,r.push(a)}},lt(null==nd.get(Hz)&&Yz),nd.set(Hz,Yz);var Kz=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="tooltip",e.dependencies=["axisPointer"],e.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click",alwaysShowContent:!1,displayMode:"single",renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,enterable:!1,backgroundColor:"#fff",shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:"#999",width:1,type:"dashed",textStyle:{}}},textStyle:{color:"#666",fontSize:14}},e}(Rp);function $z(t){var e=t.get("confine");return null!=e?!!e:"richText"===t.get("renderMode")}function Jz(t){if(r.domSupported)for(var e=document.documentElement.style,n=0,i=t.length;n-1?(u+="top:50%",h+="translateY(-50%) rotate("+(a="left"===s?-225:-45)+"deg)"):(u+="left:50%",h+="translateX(-50%) rotate("+(a="top"===s?225:45)+"deg)");var c=a*Math.PI/180,p=l+r,d=p*Math.abs(Math.cos(c))+p*Math.abs(Math.sin(c)),f=e+" solid "+r+"px;";return'
    '}(n,i,r)),U(t))o.innerHTML=t+a;else if(t){o.innerHTML="",Y(t)||(t=[t]);for(var s=0;s=0?this._tryShow(n,i):"leave"===e&&this._hide(i))}),this))},e.prototype._keepShow=function(){var t=this._tooltipModel,e=this._ecModel,n=this._api,i=t.get("triggerOn");if(null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var r=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout((function(){!n.isDisposed()&&r.manuallyShowTip(t,e,n,{x:r._lastX,y:r._lastY,dataByCoordSys:r._lastDataByCoordSys})}))}},e.prototype.manuallyShowTip=function(t,e,n,i){if(i.from!==this.uid&&!r.node&&n.getDom()){var o=gV(i,n);this._ticket="";var a=i.dataByCoordSys,s=function(t,e,n){var i=Eo(t).queryOptionMap,r=i.keys()[0];if(!r||"series"===r)return;var o=Bo(e,r,i.get(r),{useDefault:!1,enableAll:!1,enableNone:!1}),a=o.models[0];if(!a)return;var s,l=n.getViewOfComponentModel(a);if(l.group.traverse((function(e){var n=Qs(e).tooltipConfig;if(n&&n.name===t.name)return s=e,!0})),s)return{componentMainType:r,componentIndex:a.componentIndex,el:s}}(i,e,n);if(s){var l=s.el.getBoundingRect().clone();l.applyTransform(s.el.transform),this._tryShow({offsetX:l.x+l.width/2,offsetY:l.y+l.height/2,target:s.el,position:i.position,positionDefault:"bottom"},o)}else if(i.tooltip&&null!=i.x&&null!=i.y){var u=pV;u.x=i.x,u.y=i.y,u.update(),Qs(u).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:u},o)}else if(a)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:a,tooltipOption:i.tooltipOption},o);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(t,e,n,i))return;var h=wN(i,e),c=h.point[0],p=h.point[1];null!=c&&null!=p&&this._tryShow({offsetX:c,offsetY:p,target:h.el,position:i.position,positionDefault:"bottom"},o)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},o))}},e.prototype.manuallyHideTip=function(t,e,n,i){var r=this._tooltipContent;this._tooltipModel&&r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,i.from!==this.uid&&this._hide(gV(i,n))},e.prototype._manuallyAxisShowTip=function(t,e,n,i){var r=i.seriesIndex,o=i.dataIndex,a=e.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=o&&null!=a){var s=e.getSeriesByIndex(r);if(s)if("axis"===fV([s.getData().getItemModel(o),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:o,position:i.position}),!0}},e.prototype._tryShow=function(t,e){var n=t.target;if(this._tooltipModel){this._lastX=t.offsetX,this._lastY=t.offsetY;var i=t.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,t);else if(n){var r,o;this._lastDataByCoordSys=null,ky(n,(function(t){return null!=Qs(t).dataIndex?(r=t,!0):null!=Qs(t).tooltipConfig?(o=t,!0):void 0}),!0),r?this._showSeriesItemTooltip(t,r,e):o?this._showComponentItemTooltip(t,o,e):this._hide(e)}else this._lastDataByCoordSys=null,this._hide(e)}},e.prototype._showOrMove=function(t,e){var n=t.get("showDelay");e=W(e,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(e,n):e()},e.prototype._showAxisTooltip=function(t,e){var n=this._ecModel,i=this._tooltipModel,r=[e.offsetX,e.offsetY],o=fV([e.tooltipOption],i),a=this._renderMode,s=[],l=ng("section",{blocks:[],noHeader:!0}),u=[],h=new dg;E(t,(function(t){E(t.dataByAxis,(function(t){var e=n.getComponent(t.axisDim+"Axis",t.axisIndex),r=t.value;if(e&&null!=r){var o=rN(r,e.axis,n,t.seriesDataIndices,t.valueLabelOpt),c=ng("section",{header:o,noHeader:!ut(o),sortBlocks:!0,blocks:[]});l.blocks.push(c),E(t.seriesDataIndices,(function(l){var p=n.getSeriesByIndex(l.seriesIndex),d=l.dataIndexInside,f=p.getDataParams(d);if(!(f.dataIndex<0)){f.axisDim=t.axisDim,f.axisIndex=t.axisIndex,f.axisType=t.axisType,f.axisId=t.axisId,f.axisValue=__(e.axis,{value:r}),f.axisValueLabel=o,f.marker=h.makeTooltipMarker("item",_p(f.color),a);var g=mf(p.formatTooltip(d,!0,null)),y=g.frag;if(y){var v=fV([p],i).get("valueFormatter");c.blocks.push(v?A({valueFormatter:v},y):y)}g.text&&u.push(g.text),s.push(f)}}))}}))})),l.blocks.reverse(),u.reverse();var c=e.position,p=o.get("order"),d=lg(l,h,a,p,n.get("useUTC"),o.get("textStyle"));d&&u.unshift(d);var f="richText"===a?"\n\n":"
    ",g=u.join(f);this._showOrMove(o,(function(){this._updateContentNotChangedOnAxis(t,s)?this._updatePosition(o,c,r[0],r[1],this._tooltipContent,s):this._showTooltipContent(o,g,s,Math.random()+"",r[0],r[1],c,null,h)}))},e.prototype._showSeriesItemTooltip=function(t,e,n){var i=this._ecModel,r=Qs(e),o=r.seriesIndex,a=i.getSeriesByIndex(o),s=r.dataModel||a,l=r.dataIndex,u=r.dataType,h=s.getData(u),c=this._renderMode,p=t.positionDefault,d=fV([h.getItemModel(l),s,a&&(a.coordinateSystem||{}).model],this._tooltipModel,p?{position:p}:null),f=d.get("trigger");if(null==f||"item"===f){var g=s.getDataParams(l,u),y=new dg;g.marker=y.makeTooltipMarker("item",_p(g.color),c);var v=mf(s.formatTooltip(l,!1,u)),m=d.get("order"),x=d.get("valueFormatter"),_=v.frag,b=_?lg(x?A({valueFormatter:x},_):_,y,c,m,i.get("useUTC"),d.get("textStyle")):v.text,w="item_"+s.name+"_"+l;this._showOrMove(d,(function(){this._showTooltipContent(d,b,g,w,t.offsetX,t.offsetY,t.position,t.target,y)})),n({type:"showTip",dataIndexInside:l,dataIndex:h.getRawIndex(l),seriesIndex:o,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,e,n){var i=Qs(e),r=i.tooltipConfig.option||{};if(U(r)){r={content:r,formatter:r}}var o=[r],a=this._ecModel.getComponent(i.componentMainType,i.componentIndex);a&&o.push(a),o.push({formatter:r.content});var s=t.positionDefault,l=fV(o,this._tooltipModel,s?{position:s}:null),u=l.get("content"),h=Math.random()+"",c=new dg;this._showOrMove(l,(function(){var n=T(l.get("formatterParams")||{});this._showTooltipContent(l,u,n,h,t.offsetX,t.offsetY,t.position,e,c)})),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,e,n,i,r,o,a,s,l){if(this._ticket="",t.get("showContent")&&t.get("show")){var u=this._tooltipContent;u.setEnterable(t.get("enterable"));var h=t.get("formatter");a=a||t.get("position");var c=e,p=this._getNearestPoint([r,o],n,t.get("trigger"),t.get("borderColor")).color;if(h)if(U(h)){var d=t.ecModel.get("useUTC"),f=Y(n)?n[0]:n;c=h,f&&f.axisType&&f.axisType.indexOf("time")>=0&&(c=qc(f.axisValue,c,d)),c=mp(c,n,!0)}else if(X(h)){var g=W((function(e,i){e===this._ticket&&(u.setContent(i,l,t,p,a),this._updatePosition(t,a,r,o,u,n,s))}),this);this._ticket=i,c=h(n,i,g)}else c=h;u.setContent(c,l,t,p,a),u.show(t,p),this._updatePosition(t,a,r,o,u,n,s)}},e.prototype._getNearestPoint=function(t,e,n,i){return"axis"===n||Y(e)?{color:i||("html"===this._renderMode?"#fff":"none")}:Y(e)?void 0:{color:i||e.color||e.borderColor}},e.prototype._updatePosition=function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),h=t.get("align"),c=t.get("verticalAlign"),p=a&&a.getBoundingRect().clone();if(a&&p.applyTransform(a.transform),X(e)&&(e=e([n,i],o,r.el,p,{viewSize:[s,l],contentSize:u.slice()})),Y(e))n=Ur(e[0],s),i=Ur(e[1],l);else if(q(e)){var d=e;d.width=u[0],d.height=u[1];var f=Cp(d,{width:s,height:l});n=f.x,i=f.y,h=null,c=null}else if(U(e)&&a){var g=function(t,e,n,i){var r=n[0],o=n[1],a=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,u=e.width,h=e.height;switch(t){case"inside":s=e.x+u/2-r/2,l=e.y+h/2-o/2;break;case"top":s=e.x+u/2-r/2,l=e.y-o-a;break;case"bottom":s=e.x+u/2-r/2,l=e.y+h+a;break;case"left":s=e.x-r-a,l=e.y+h/2-o/2;break;case"right":s=e.x+u+a,l=e.y+h/2-o/2}return[s,l]}(e,p,u,t.get("borderWidth"));n=g[0],i=g[1]}else{g=function(t,e,n,i,r,o,a){var s=n.getSize(),l=s[0],u=s[1];null!=o&&(t+l+o+2>i?t-=l+o:t+=o);null!=a&&(e+u+a>r?e-=u+a:e+=a);return[t,e]}(n,i,r,s,l,h?null:20,c?null:20);n=g[0],i=g[1]}if(h&&(n-=yV(h)?u[0]/2:"right"===h?u[0]:0),c&&(i-=yV(c)?u[1]/2:"bottom"===c?u[1]:0),$z(t)){g=function(t,e,n,i,r){var o=n.getSize(),a=o[0],s=o[1];return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}(n,i,r,s,l);n=g[0],i=g[1]}r.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t,e){var n=this._lastDataByCoordSys,i=this._cbParamsList,r=!!n&&n.length===t.length;return r&&E(n,(function(n,o){var a=n.dataByAxis||[],s=(t[o]||{}).dataByAxis||[];(r=r&&a.length===s.length)&&E(a,(function(t,n){var o=s[n]||{},a=t.seriesDataIndices||[],l=o.seriesDataIndices||[];(r=r&&t.value===o.value&&t.axisType===o.axisType&&t.axisId===o.axisId&&a.length===l.length)&&E(a,(function(t,e){var n=l[e];r=r&&t.seriesIndex===n.seriesIndex&&t.dataIndex===n.dataIndex})),i&&E(t.seriesDataIndices,(function(t){var n=t.seriesIndex,o=e[n],a=i[n];o&&a&&a.data!==o.data&&(r=!1)}))}))})),this._lastDataByCoordSys=t,this._cbParamsList=e,!!r},e.prototype._hide=function(t){this._lastDataByCoordSys=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,e){!r.node&&e.getDom()&&(Gg(this,"_updatePosition"),this._tooltipContent.dispose(),_N("itemTooltip",e))},e.type="tooltip",e}(Tg);function fV(t,e,n){var i,r=e.ecModel;n?(i=new Mc(n,r,r),i=new Mc(e.option,i,r)):i=e;for(var o=t.length-1;o>=0;o--){var a=t[o];a&&(a instanceof Mc&&(a=a.get("tooltip",!0)),U(a)&&(a={formatter:a}),a&&(i=new Mc(a,i,r)))}return i}function gV(t,e){return t.dispatchAction||W(e.dispatchAction,e)}function yV(t){return"center"===t||"middle"===t}var vV=["rect","polygon","keep","clear"];function mV(t,e){var n=bo(t?t.brush:[]);if(n.length){var i=[];E(n,(function(t){var e=t.hasOwnProperty("toolbox")?t.toolbox:[];e instanceof Array&&(i=i.concat(e))}));var r=t&&t.toolbox;Y(r)&&(r=r[0]),r||(r={feature:{}},t.toolbox=[r]);var o=r.feature||(r.feature={}),a=o.brush||(o.brush={}),s=a.type||(a.type=[]);s.push.apply(s,i),function(t){var e={};E(t,(function(t){e[t]=1})),t.length=0,E(e,(function(e,n){t.push(n)}))}(s),e&&!s.length&&s.push.apply(s,vV)}}var xV=E;function _V(t){if(t)for(var e in t)if(t.hasOwnProperty(e))return!0}function bV(t,e,n){var i={};return xV(e,(function(e){var r,o=i[e]=((r=function(){}).prototype.__hidden=r.prototype,new r);xV(t[e],(function(t,i){if(_D.isValidType(i)){var r={type:i,visual:t};n&&n(r,e),o[i]=new _D(r),"opacity"===i&&((r=T(r)).type="colorAlpha",o.__hidden.__alphaForOpacity=new _D(r))}}))})),i}function wV(t,e,n){var i;E(n,(function(t){e.hasOwnProperty(t)&&_V(e[t])&&(i=!0)})),i&&E(n,(function(n){e.hasOwnProperty(n)&&_V(e[n])?t[n]=T(e[n]):delete t[n]}))}var SV={lineX:MV(0),lineY:MV(1),rect:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])},rect:function(t,e,n){return t&&n.boundingRect.intersect(t)}},polygon:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])&&A_(n.range,t[0],t[1])},rect:function(t,e,n){var i=n.range;if(!t||i.length<=1)return!1;var r=t.x,o=t.y,a=t.width,s=t.height,l=i[0];return!!(A_(i,r,o)||A_(i,r+a,o)||A_(i,r,o+s)||A_(i,r+a,o+s)||ze.create(t).contain(l[0],l[1])||Yh(r,o,r+a,o,i)||Yh(r,o,r,o+s,i)||Yh(r+a,o,r+a,o+s,i)||Yh(r,o+s,r+a,o+s,i))||void 0}}};function MV(t){var e=["x","y"],n=["width","height"];return{point:function(e,n,i){if(e){var r=i.range;return IV(e[t],r)}},rect:function(i,r,o){if(i){var a=o.range,s=[i[e[t]],i[e[t]]+i[n[t]]];return s[1]e[0][1]&&(e[0][1]=o[0]),o[1]e[1][1]&&(e[1][1]=o[1])}return e&&RV(e)}};function RV(t){return new ze(t[0][0],t[1][0],t[0][1]-t[0][0],t[1][1]-t[1][0])}var NV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.ecModel=t,this.api=e,this.model,(this._brushController=new Jk(e.getZr())).on("brush",W(this._onBrush,this)).mount()},e.prototype.render=function(t,e,n,i){this.model=t,this._updateController(t,e,n,i)},e.prototype.updateTransform=function(t,e,n,i){AV(e),this._updateController(t,e,n,i)},e.prototype.updateVisual=function(t,e,n,i){this.updateTransform(t,e,n,i)},e.prototype.updateView=function(t,e,n,i){this._updateController(t,e,n,i)},e.prototype._updateController=function(t,e,n,i){(!i||i.$from!==t.id)&&this._brushController.setPanels(t.brushTargetManager.makePanelOpts(n)).enableBrush(t.brushOption).updateCovers(t.areas.slice())},e.prototype.dispose=function(){this._brushController.dispose()},e.prototype._onBrush=function(t){var e=this.model.id,n=this.model.brushTargetManager.setOutputRanges(t.areas,this.ecModel);(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:e,areas:T(n),$from:e}),t.isEnd&&this.api.dispatchAction({type:"brushEnd",brushId:e,areas:T(n),$from:e})},e.type="brush",e}(Tg),EV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.areas=[],n.brushOption={},n}return n(e,t),e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&wV(n,t,["inBrush","outOfBrush"]);var i=n.inBrush=n.inBrush||{};n.outOfBrush=n.outOfBrush||{color:"#ddd"},i.hasOwnProperty("liftZ")||(i.liftZ=5)},e.prototype.setAreas=function(t){t&&(this.areas=z(t,(function(t){return zV(this.option,t)}),this))},e.prototype.setBrushOption=function(t){this.brushOption=zV(this.option,t),this.brushType=this.brushOption.brushType},e.type="brush",e.dependencies=["geo","grid","xAxis","yAxis","parallel","series"],e.defaultOption={seriesIndex:"all",brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:"rgba(210,219,238,0.3)",borderColor:"#D2DBEE"},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4},e}(Rp);function zV(t,e){return C({brushType:t.brushType,brushMode:t.brushMode,transformable:t.transformable,brushStyle:new Mc(t.brushStyle).getItemStyle(),removeOnClick:t.removeOnClick,z:t.z},e,!0)}var VV=["rect","polygon","lineX","lineY","keep","clear"],BV=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n){var i,r,o;e.eachComponent({mainType:"brush"},(function(t){i=t.brushType,r=t.brushOption.brushMode||"single",o=o||!!t.areas.length})),this._brushType=i,this._brushMode=r,E(t.get("type",!0),(function(e){t.setIconStatus(e,("keep"===e?"multiple"===r:"clear"===e?o:e===i)?"emphasis":"normal")}))},e.prototype.updateView=function(t,e,n){this.render(t,e,n)},e.prototype.getIcons=function(){var t=this.model,e=t.get("icon",!0),n={};return E(t.get("type",!0),(function(t){e[t]&&(n[t]=e[t])})),n},e.prototype.onclick=function(t,e,n){var i=this._brushType,r=this._brushMode;"clear"===n?(e.dispatchAction({type:"axisAreaSelect",intervals:[]}),e.dispatchAction({type:"brush",command:"clear",areas:[]})):e.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:"keep"===n?i:i!==n&&n,brushMode:"keep"===n?"multiple"===r?"single":"multiple":r}})},e.getDefaultOption=function(t){return{show:!0,type:VV.slice(),icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:t.getLocaleModel().get(["toolbox","brush","title"])}},e}(lz);var FV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode={type:"box",ignoreSize:!0},n}return n(e,t),e.type="title",e.defaultOption={z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:0,top:0,backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bold",color:"#464646"},subtextStyle:{fontSize:12,color:"#6E7079"}},e}(Rp),GV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){if(this.group.removeAll(),t.get("show")){var i=this.group,r=t.getModel("textStyle"),o=t.getModel("subtextStyle"),a=t.get("textAlign"),s=rt(t.get("textBaseline"),t.get("textVerticalAlign")),l=new Fs({style:nc(r,{text:t.get("text"),fill:r.getTextColor()},{disableBox:!0}),z2:10}),u=l.getBoundingRect(),h=t.get("subtext"),c=new Fs({style:nc(o,{text:h,fill:o.getTextColor(),y:u.height+t.get("itemGap"),verticalAlign:"top"},{disableBox:!0}),z2:10}),p=t.get("link"),d=t.get("sublink"),f=t.get("triggerEvent",!0);l.silent=!p&&!f,c.silent=!d&&!f,p&&l.on("click",(function(){bp(p,"_"+t.get("target"))})),d&&c.on("click",(function(){bp(d,"_"+t.get("subtarget"))})),Qs(l).eventData=Qs(c).eventData=f?{componentType:"title",componentIndex:t.componentIndex}:null,i.add(l),h&&i.add(c);var g=i.getBoundingRect(),y=t.getBoxLayoutParams();y.width=g.width,y.height=g.height;var v=Cp(y,{width:n.getWidth(),height:n.getHeight()},t.get("padding"));a||("middle"===(a=t.get("left")||t.get("right"))&&(a="center"),"right"===a?v.x+=v.width:"center"===a&&(v.x+=v.width/2)),s||("center"===(s=t.get("top")||t.get("bottom"))&&(s="middle"),"bottom"===s?v.y+=v.height:"middle"===s&&(v.y+=v.height/2),s=s||"top"),i.x=v.x,i.y=v.y,i.markRedraw();var m={align:a,verticalAlign:s};l.setStyle(m),c.setStyle(m),g=i.getBoundingRect();var x=v.margin,_=t.getItemStyle(["color","opacity"]);_.fill=t.get("backgroundColor");var b=new zs({shape:{x:g.x-x[3],y:g.y-x[0],width:g.width+x[1]+x[3],height:g.height+x[0]+x[2],r:t.get("borderRadius")},style:_,subPixelOptimize:!0,silent:!0});i.add(b)}},e.type="title",e}(Tg);var WV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode="box",n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),this._initData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this._initData()},e.prototype.setCurrentIndex=function(t){null==t&&(t=this.option.currentIndex);var e=this._data.count();this.option.loop?t=(t%e+e)%e:(t>=e&&(t=e-1),t<0&&(t=0)),this.option.currentIndex=t},e.prototype.getCurrentIndex=function(){return this.option.currentIndex},e.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},e.prototype.setPlayState=function(t){this.option.autoPlay=!!t},e.prototype.getPlayState=function(){return!!this.option.autoPlay},e.prototype._initData=function(){var t,e=this.option,n=e.data||[],i=e.axisType,r=this._names=[];"category"===i?(t=[],E(n,(function(e,n){var i,o=Ao(Mo(e),"");q(e)?(i=T(e)).value=n:i=n,t.push(i),r.push(o)}))):t=n;var o={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new lx([{name:"value",type:o}],this)).initData(t,r)},e.prototype.getData=function(){return this._data},e.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},e.type="timeline",e.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:5,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:"#000"},data:[]},e}(Rp),HV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline.slider",e.defaultOption=Cc(WV.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:"#DAE1F5"},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:"#A4B1D7"},itemStyle:{color:"#A4B1D7",borderWidth:1},checkpointStyle:{symbol:"circle",symbolSize:15,color:"#316bf3",borderColor:"#fff",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:"rgba(0, 0, 0, 0.3)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M31.6,53C17.5,53,6,41.5,6,27.4S17.5,1.8,31.6,1.8C45.7,1.8,57.2,13.3,57.2,27.4S45.7,53,31.6,53z M31.6,3.3 C18.4,3.3,7.5,14.1,7.5,27.4c0,13.3,10.8,24.1,24.1,24.1C44.9,51.5,55.7,40.7,55.7,27.4C55.7,14.1,44.9,3.3,31.6,3.3z M24.9,21.3 c0-2.2,1.6-3.1,3.5-2l10.5,6.1c1.899,1.1,1.899,2.9,0,4l-10.5,6.1c-1.9,1.1-3.5,0.2-3.5-2V21.3z",stopIcon:"path://M30.9,53.2C16.8,53.2,5.3,41.7,5.3,27.6S16.8,2,30.9,2C45,2,56.4,13.5,56.4,27.6S45,53.2,30.9,53.2z M30.9,3.5C17.6,3.5,6.8,14.4,6.8,27.6c0,13.3,10.8,24.1,24.101,24.1C44.2,51.7,55,40.9,55,27.6C54.9,14.4,44.1,3.5,30.9,3.5z M36.9,35.8c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H36c0.5,0,0.9,0.4,0.9,1V35.8z M27.8,35.8 c0,0.601-0.4,1-0.9,1h-1.3c-0.5,0-0.9-0.399-0.9-1V19.5c0-0.6,0.4-1,0.9-1H27c0.5,0,0.9,0.4,0.9,1L27.8,35.8L27.8,35.8z",nextIcon:"M2,18.5A1.52,1.52,0,0,1,.92,18a1.49,1.49,0,0,1,0-2.12L7.81,9.36,1,3.11A1.5,1.5,0,1,1,3,.89l8,7.34a1.48,1.48,0,0,1,.49,1.09,1.51,1.51,0,0,1-.46,1.1L3,18.08A1.5,1.5,0,0,1,2,18.5Z",prevIcon:"M10,.5A1.52,1.52,0,0,1,11.08,1a1.49,1.49,0,0,1,0,2.12L4.19,9.64,11,15.89a1.5,1.5,0,1,1-2,2.22L1,10.77A1.48,1.48,0,0,1,.5,9.68,1.51,1.51,0,0,1,1,8.58L9,.92A1.5,1.5,0,0,1,10,.5Z",prevBtnSize:18,nextBtnSize:18,color:"#A4B1D7",borderColor:"#A4B1D7",borderWidth:1},emphasis:{label:{show:!0,color:"#6f778d"},itemStyle:{color:"#316BF3"},controlStyle:{color:"#316BF3",borderColor:"#316BF3",borderWidth:2}},progress:{lineStyle:{color:"#316BF3"},itemStyle:{color:"#316BF3"},label:{color:"#6f778d"}},data:[]}),e}(WV);R(HV,vf.prototype);var YV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline",e}(Tg),XV=function(t){function e(e,n,i,r){var o=t.call(this,e,n,i)||this;return o.type=r||"value",o}return n(e,t),e.prototype.getLabelModel=function(){return this.model.getModel("label")},e.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},e}(nb),UV=Math.PI,ZV=Oo(),jV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.api=e},e.prototype.render=function(t,e,n){if(this.model=t,this.api=n,this.ecModel=e,this.group.removeAll(),t.get("show",!0)){var i=this._layout(t,n),r=this._createGroup("_mainGroup"),o=this._createGroup("_labelGroup"),a=this._axis=this._createAxis(i,t);t.formatTooltip=function(t){return ng("nameValue",{noName:!0,value:a.scale.getLabel({value:t})})},E(["AxisLine","AxisTick","Control","CurrentPointer"],(function(e){this["_render"+e](i,r,a,t)}),this),this._renderAxisLabel(i,o,a,t),this._position(i,t)}this._doPlayStop(),this._updateTicksStatus()},e.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},e.prototype.dispose=function(){this._clearTimer()},e.prototype._layout=function(t,e){var n,i,r,o,a=t.get(["label","position"]),s=t.get("orient"),l=function(t,e){return Cp(t.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()},t.get("padding"))}(t,e),u={horizontal:"center",vertical:(n=null==a||"auto"===a?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},h={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},c={horizontal:0,vertical:UV/2},p="vertical"===s?l.height:l.width,d=t.getModel("controlStyle"),f=d.get("show",!0),g=f?d.get("itemSize"):0,y=f?d.get("itemGap"):0,v=g+y,m=t.get(["label","rotate"])||0;m=m*UV/180;var x=d.get("position",!0),_=f&&d.get("showPlayBtn",!0),b=f&&d.get("showPrevBtn",!0),w=f&&d.get("showNextBtn",!0),S=0,M=p;"left"===x||"bottom"===x?(_&&(i=[0,0],S+=v),b&&(r=[S,0],S+=v),w&&(o=[M-g,0],M-=v)):(_&&(i=[M-g,0],M-=v),b&&(r=[0,0],S+=v),w&&(o=[M-g,0],M-=v));var I=[S,M];return t.get("inverse")&&I.reverse(),{viewRect:l,mainLength:p,orient:s,rotation:c[s],labelRotation:m,labelPosOpt:n,labelAlign:t.get(["label","align"])||u[s],labelBaseline:t.get(["label","verticalAlign"])||t.get(["label","baseline"])||h[s],playPosition:i,prevBtnPosition:r,nextBtnPosition:o,axisExtent:I,controlSize:g,controlGap:y}},e.prototype._position=function(t,e){var n=this._mainGroup,i=this._labelGroup,r=t.viewRect;if("vertical"===t.orient){var o=[1,0,0,1,0,0],a=r.x,s=r.y+r.height;we(o,o,[-a,-s]),Se(o,o,-UV/2),we(o,o,[a,s]),(r=r.clone()).applyTransform(o)}var l=y(r),u=y(n.getBoundingRect()),h=y(i.getBoundingRect()),c=[n.x,n.y],p=[i.x,i.y];p[0]=c[0]=l[0][0];var d,f=t.labelPosOpt;null==f||U(f)?(v(c,u,l,1,d="+"===f?0:1),v(p,h,l,1,1-d)):(v(c,u,l,1,d=f>=0?0:1),p[1]=c[1]+f);function g(t){t.originX=l[0][0]-t.x,t.originY=l[1][0]-t.y}function y(t){return[[t.x,t.x+t.width],[t.y,t.y+t.height]]}function v(t,e,n,i,r){t[i]+=n[i][r]-e[i][r]}n.setPosition(c),i.setPosition(p),n.rotation=i.rotation=t.rotation,g(n),g(i)},e.prototype._createAxis=function(t,e){var n=e.getData(),i=e.get("axisType"),r=function(t,e){if(e=e||t.get("type"),e)switch(e){case"category":return new Lx({ordinalMeta:t.getCategories(),extent:[1/0,-1/0]});case"time":return new Zx({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC")});default:return new Ox}}(e,i);r.getTicks=function(){return n.mapArray(["value"],(function(t){return{value:t}}))};var o=n.getDataExtent("value");r.setExtent(o[0],o[1]),r.calcNiceTicks();var a=new XV("value",r,t.axisExtent,i);return a.model=e,a},e.prototype._createGroup=function(t){var e=this[t]=new zr;return this.group.add(e),e},e.prototype._renderAxisLine=function(t,e,n,i){var r=n.getExtent();if(i.get(["lineStyle","show"])){var o=new Zu({shape:{x1:r[0],y1:0,x2:r[1],y2:0},style:A({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});e.add(o);var a=this._progressLine=new Zu({shape:{x1:r[0],x2:this._currentPointer?this._currentPointer.x:r[0],y1:0,y2:0},style:k({lineCap:"round",lineWidth:o.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});e.add(a)}},e.prototype._renderAxisTick=function(t,e,n,i){var r=this,o=i.getData(),a=n.scale.getTicks();this._tickSymbols=[],E(a,(function(t){var a=n.dataToCoord(t.value),s=o.getItemModel(t.value),l=s.getModel("itemStyle"),u=s.getModel(["emphasis","itemStyle"]),h=s.getModel(["progress","itemStyle"]),c={x:a,y:0,onclick:W(r._changeTimeline,r,t.value)},p=qV(s,l,e,c);p.ensureState("emphasis").style=u.getItemStyle(),p.ensureState("progress").style=h.getItemStyle(),Hl(p);var d=Qs(p);s.get("tooltip")?(d.dataIndex=t.value,d.dataModel=i):d.dataIndex=d.dataModel=null,r._tickSymbols.push(p)}))},e.prototype._renderAxisLabel=function(t,e,n,i){var r=this;if(n.getLabelModel().get("show")){var o=i.getData(),a=n.getViewLabels();this._tickLabels=[],E(a,(function(i){var a=i.tickValue,s=o.getItemModel(a),l=s.getModel("label"),u=s.getModel(["emphasis","label"]),h=s.getModel(["progress","label"]),c=n.dataToCoord(i.tickValue),p=new Fs({x:c,y:0,rotation:t.labelRotation-t.rotation,onclick:W(r._changeTimeline,r,a),silent:!1,style:nc(l,{text:i.formattedLabel,align:t.labelAlign,verticalAlign:t.labelBaseline})});p.ensureState("emphasis").style=nc(u),p.ensureState("progress").style=nc(h),e.add(p),Hl(p),ZV(p).dataIndex=a,r._tickLabels.push(p)}))}},e.prototype._renderControl=function(t,e,n,i){var r=t.controlSize,o=t.rotation,a=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),u=i.get("inverse",!0);function h(t,n,l,u){if(t){var h=Ir(rt(i.get(["controlStyle",n+"BtnSize"]),r),r),c=function(t,e,n,i){var r=i.style,o=Hh(t.get(["controlStyle",e]),i||{},new ze(n[0],n[1],n[2],n[3]));r&&o.setStyle(r);return o}(i,n+"Icon",[0,-h/2,h,h],{x:t[0],y:t[1],originX:r/2,originY:0,rotation:u?-o:0,rectHover:!0,style:a,onclick:l});c.ensureState("emphasis").style=s,e.add(c),Hl(c)}}h(t.nextBtnPosition,"next",W(this._changeTimeline,this,u?"-":"+")),h(t.prevBtnPosition,"prev",W(this._changeTimeline,this,u?"+":"-")),h(t.playPosition,l?"stop":"play",W(this._handlePlayClick,this,!l),!0)},e.prototype._renderCurrentPointer=function(t,e,n,i){var r=i.getData(),o=i.getCurrentIndex(),a=r.getItemModel(o).getModel("checkpointStyle"),s=this,l={onCreate:function(t){t.draggable=!0,t.drift=W(s._handlePointerDrag,s),t.ondragend=W(s._handlePointerDragend,s),KV(t,s._progressLine,o,n,i,!0)},onUpdate:function(t){KV(t,s._progressLine,o,n,i)}};this._currentPointer=qV(a,a,this._mainGroup,{},this._currentPointer,l)},e.prototype._handlePlayClick=function(t){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:t,from:this.uid})},e.prototype._handlePointerDrag=function(t,e,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},e.prototype._handlePointerDragend=function(t){this._pointerChangeTimeline([t.offsetX,t.offsetY],!0)},e.prototype._pointerChangeTimeline=function(t,e){var n=this._toAxisCoord(t)[0],i=jr(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(a[o]=+a[o].toFixed(c)),[a,h]}var sB={min:H(aB,"min"),max:H(aB,"max"),average:H(aB,"average"),median:H(aB,"median")};function lB(t,e){if(e){var n=t.getData(),i=t.coordinateSystem,r=i&&i.dimensions;if(!function(t){return!isNaN(parseFloat(t.x))&&!isNaN(parseFloat(t.y))}(e)&&!Y(e.coord)&&Y(r)){var o=uB(e,n,i,t);if((e=T(e)).type&&sB[e.type]&&o.baseAxis&&o.valueAxis){var a=P(r,o.baseAxis.dim),s=P(r,o.valueAxis.dim),l=sB[e.type](n,o.baseDataDim,o.valueDataDim,a,s);e.coord=l[0],e.value=l[1]}else e.coord=[null!=e.xAxis?e.xAxis:e.radiusAxis,null!=e.yAxis?e.yAxis:e.angleAxis]}if(null!=e.coord&&Y(r))for(var u=e.coord,h=0;h<2;h++)sB[u[h]]&&(u[h]=pB(n,n.mapDimension(r[h]),u[h]));else e.coord=[];return e}}function uB(t,e,n,i){var r={};return null!=t.valueIndex||null!=t.valueDim?(r.valueDataDim=null!=t.valueIndex?e.getDimension(t.valueIndex):t.valueDim,r.valueAxis=n.getAxis(function(t,e){var n=t.getData().getDimensionInfo(e);return n&&n.coordDim}(i,r.valueDataDim)),r.baseAxis=n.getOtherAxis(r.valueAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim)):(r.baseAxis=i.getBaseAxis(),r.valueAxis=n.getOtherAxis(r.baseAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim),r.valueDataDim=e.mapDimension(r.valueAxis.dim)),r}function hB(t,e){return!(t&&t.containData&&e.coord&&!oB(e))||t.containData(e.coord)}function cB(t,e){return t?function(t,n,i,r){return wf(r<2?t.coord&&t.coord[r]:t.value,e[r])}:function(t,n,i,r){return wf(t.value,e[r])}}function pB(t,e,n){if("average"===n){var i=0,r=0;return t.each(e,(function(t,e){isNaN(t)||(i+=t,r++)})),i/r}return"median"===n?t.getMedian(e):t.getDataExtent(e)["max"===n?1:0]}var dB=Oo(),fB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){this.markerGroupMap=yt()},e.prototype.render=function(t,e,n){var i=this,r=this.markerGroupMap;r.each((function(t){dB(t).keep=!1})),e.eachSeries((function(t){var r=iB.getMarkerModelFromSeries(t,i.type);r&&i.renderSeries(t,r,e,n)})),r.each((function(t){!dB(t).keep&&i.group.remove(t.group)}))},e.prototype.markKeep=function(t){dB(t).keep=!0},e.prototype.toggleBlurSeries=function(t,e){var n=this;E(t,(function(t){var i=iB.getMarkerModelFromSeries(t,n.type);i&&i.getData().eachItemGraphicEl((function(t){t&&(e?Pl(t):Ol(t))}))}))},e.type="marker",e}(Tg);function gB(t,e,n){var i=e.coordinateSystem;t.each((function(r){var o,a=t.getItemModel(r),s=Ur(a.get("x"),n.getWidth()),l=Ur(a.get("y"),n.getHeight());if(isNaN(s)||isNaN(l)){if(e.getMarkerPosition)o=e.getMarkerPosition(t.getValues(t.dimensions,r));else if(i){var u=t.get(i.dimensions[0],r),h=t.get(i.dimensions[1],r);o=i.dataToPoint([u,h])}}else o=[s,l];isNaN(s)||(o[0]=s),isNaN(l)||(o[1]=l),t.setItemLayout(r,o)}))}var yB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=iB.getMarkerModelFromSeries(t,"markPoint");e&&(gB(e.getData(),t,n),this.markerGroupMap.get(t.id).updateLayout())}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new hS),u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new lx(i,n),o=z(n.get("data"),H(lB,e));t&&(o=B(o,H(hB,t)));var a=cB(!!t,i);return r.initData(o,null,a),r}(r,t,e);e.setData(u),gB(e.getData(),t,i),u.each((function(t){var n=u.getItemModel(t),i=n.getShallow("symbol"),r=n.getShallow("symbolSize"),o=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(X(i)||X(r)||X(o)||X(s)){var h=e.getRawValue(t),c=e.getDataParams(t);X(i)&&(i=i(h,c)),X(r)&&(r=r(h,c)),X(o)&&(o=o(h,c)),X(s)&&(s=s(h,c))}var p=n.getModel("itemStyle").getItemStyle(),d=Ty(a,"color");p.fill||(p.fill=d),u.setItemVisual(t,{symbol:i,symbolSize:r,symbolRotate:o,symbolOffset:s,symbolKeepAspect:l,style:p})})),l.updateData(u),this.group.add(l.group),u.eachItemGraphicEl((function(t){t.traverse((function(t){Qs(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markPoint",e}(fB);var vB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markLine",e.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},e}(iB),mB=Oo(),xB=function(t,e,n,i){var r,o=t.getData();if(Y(i))r=i;else{var a=i.type;if("min"===a||"max"===a||"average"===a||"median"===a||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=e.getAxis(null!=i.yAxis?"y":"x"),l=it(i.yAxis,i.xAxis);else{var u=uB(i,o,e,t);s=u.valueAxis,l=pB(o,yx(o,u.valueDataDim),a)}var h="x"===s.dim?0:1,c=1-h,p=T(i),d={coord:[]};p.type=null,p.coord=[],p.coord[c]=-1/0,d.coord[c]=1/0;var f=n.get("precision");f>=0&&j(l)&&(l=+l.toFixed(Math.min(f,20))),p.coord[h]=d.coord[h]=l,r=[p,d,{type:a,valueIndex:i.valueIndex,value:l}]}else r=[]}var g=[lB(t,r[0]),lB(t,r[1]),A({},r[2])];return g[2].type=g[2].type||null,C(g[2],g[0]),C(g[2],g[1]),g};function _B(t){return!isNaN(t)&&!isFinite(t)}function bB(t,e,n,i){var r=1-t,o=i.dimensions[t];return _B(e[r])&&_B(n[r])&&e[t]===n[t]&&i.getAxis(o).containData(e[t])}function wB(t,e){if("cartesian2d"===t.type){var n=e[0].coord,i=e[1].coord;if(n&&i&&(bB(1,n,i,t)||bB(0,n,i,t)))return!0}return hB(t,e[0])&&hB(t,e[1])}function SB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Ur(s.get("x"),r.getWidth()),u=Ur(s.get("y"),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(t.dimensions,e));else{var h=a.dimensions,c=t.get(h[0],e),p=t.get(h[1],e);o=a.dataToPoint([c,p])}if(MS(a,"cartesian2d")){var d=a.getAxis("x"),f=a.getAxis("y");h=a.dimensions;_B(t.get(h[0],e))?o[0]=d.toGlobalCoord(d.getExtent()[n?0:1]):_B(t.get(h[1],e))&&(o[1]=f.toGlobalCoord(f.getExtent()[n?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];t.setItemLayout(e,o)}var MB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=iB.getMarkerModelFromSeries(t,"markLine");if(e){var i=e.getData(),r=mB(e).from,o=mB(e).to;r.each((function(e){SB(r,e,!0,t,n),SB(o,e,!1,t,n)})),i.each((function(t){i.setItemLayout(t,[r.getItemLayout(t),o.getItemLayout(t)])})),this.markerGroupMap.get(t.id).updateLayout()}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new RA);this.group.add(l.group);var u=function(t,e,n){var i;i=t?z(t&&t.dimensions,(function(t){return A(A({},e.getData().getDimensionInfo(e.getData().mapDimension(t))||{}),{name:t,ordinalMeta:null})})):[{name:"value",type:"float"}];var r=new lx(i,n),o=new lx(i,n),a=new lx([],n),s=z(n.get("data"),H(xB,e,t,n));t&&(s=B(s,H(wB,t)));var l=cB(!!t,i);return r.initData(z(s,(function(t){return t[0]})),null,l),o.initData(z(s,(function(t){return t[1]})),null,l),a.initData(z(s,(function(t){return t[2]}))),a.hasItemOption=!0,{from:r,to:o,line:a}}(r,t,e),h=u.from,c=u.to,p=u.line;mB(e).from=h,mB(e).to=c,e.setData(p);var d=e.get("symbol"),f=e.get("symbolSize"),g=e.get("symbolRotate"),y=e.get("symbolOffset");function v(e,n,r){var o=e.getItemModel(n);SB(e,n,r,t,i);var s=o.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=Ty(a,"color")),e.setItemVisual(n,{symbolKeepAspect:o.get("symbolKeepAspect"),symbolOffset:rt(o.get("symbolOffset",!0),y[r?0:1]),symbolRotate:rt(o.get("symbolRotate",!0),g[r?0:1]),symbolSize:rt(o.get("symbolSize"),f[r?0:1]),symbol:rt(o.get("symbol",!0),d[r?0:1]),style:s})}Y(d)||(d=[d,d]),Y(f)||(f=[f,f]),Y(g)||(g=[g,g]),Y(y)||(y=[y,y]),u.from.each((function(t){v(h,t,!0),v(c,t,!1)})),p.each((function(t){var e=p.getItemModel(t).getModel("lineStyle").getLineStyle();p.setItemLayout(t,[h.getItemLayout(t),c.getItemLayout(t)]),null==e.stroke&&(e.stroke=h.getItemVisual(t,"style").fill),p.setItemVisual(t,{fromSymbolKeepAspect:h.getItemVisual(t,"symbolKeepAspect"),fromSymbolOffset:h.getItemVisual(t,"symbolOffset"),fromSymbolRotate:h.getItemVisual(t,"symbolRotate"),fromSymbolSize:h.getItemVisual(t,"symbolSize"),fromSymbol:h.getItemVisual(t,"symbol"),toSymbolKeepAspect:c.getItemVisual(t,"symbolKeepAspect"),toSymbolOffset:c.getItemVisual(t,"symbolOffset"),toSymbolRotate:c.getItemVisual(t,"symbolRotate"),toSymbolSize:c.getItemVisual(t,"symbolSize"),toSymbol:c.getItemVisual(t,"symbol"),style:e})})),l.updateData(p),u.line.eachItemGraphicEl((function(t){Qs(t).dataModel=e,t.traverse((function(t){Qs(t).dataModel=e}))})),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markLine",e}(fB);var IB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markArea",e.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}},e}(iB),TB=Oo(),CB=function(t,e,n,i){var r=i[0],o=i[1];if(r&&o){var a=lB(t,r),s=lB(t,o),l=a.coord,u=s.coord;l[0]=it(l[0],-1/0),l[1]=it(l[1],-1/0),u[0]=it(u[0],1/0),u[1]=it(u[1],1/0);var h=D([{},a,s]);return h.coord=[a.coord,s.coord],h.x0=a.x,h.y0=a.y,h.x1=s.x,h.y1=s.y,h}};function DB(t){return!isNaN(t)&&!isFinite(t)}function AB(t,e,n,i){var r=1-t;return DB(e[r])&&DB(n[r])}function kB(t,e){var n=e.coord[0],i=e.coord[1],r={coord:n,x:e.x0,y:e.y0},o={coord:i,x:e.x1,y:e.y1};return MS(t,"cartesian2d")?!(!n||!i||!AB(1,n,i)&&!AB(0,n,i))||function(t,e,n){return!(t&&t.containZone&&e.coord&&n.coord&&!oB(e)&&!oB(n))||t.containZone(e.coord,n.coord)}(t,r,o):hB(t,r)||hB(t,o)}function LB(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Ur(s.get(n[0]),r.getWidth()),u=Ur(s.get(n[1]),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition){var h=t.getValues(["x0","y0"],e),c=t.getValues(["x1","y1"],e),p=a.clampData(h),d=a.clampData(c),f=[];"x0"===n[0]?f[0]=p[0]>d[0]?c[0]:h[0]:f[0]=p[0]>d[0]?h[0]:c[0],"y0"===n[1]?f[1]=p[1]>d[1]?c[1]:h[1]:f[1]=p[1]>d[1]?h[1]:c[1],o=i.getMarkerPosition(f,n,!0)}else{var g=[m=t.get(n[0],e),x=t.get(n[1],e)];a.clampData&&a.clampData(g,g),o=a.dataToPoint(g,!0)}if(MS(a,"cartesian2d")){var y=a.getAxis("x"),v=a.getAxis("y"),m=t.get(n[0],e),x=t.get(n[1],e);DB(m)?o[0]=y.toGlobalCoord(y.getExtent()["x0"===n[0]?0:1]):DB(x)&&(o[1]=v.toGlobalCoord(v.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];return o}var PB=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]],OB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries((function(t){var e=iB.getMarkerModelFromSeries(t,"markArea");if(e){var i=e.getData();i.each((function(e){var r=z(PB,(function(r){return LB(i,e,r,t,n)}));i.setItemLayout(e,r),i.getItemGraphicEl(e).setShape("points",r)}))}}),this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,{group:new zr});this.group.add(l.group),this.markKeep(l);var u=function(t,e,n){var i,r,o=["x0","y0","x1","y1"];if(t){var a=z(t&&t.dimensions,(function(t){var n=e.getData();return A(A({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})}));r=z(o,(function(t,e){return{name:t,type:a[e%2].type}})),i=new lx(r,n)}else i=new lx(r=[{name:"value",type:"float"}],n);var s=z(n.get("data"),H(CB,e,t,n));t&&(s=B(s,H(kB,t)));var l=t?function(t,e,n,i){return wf(t.coord[Math.floor(i/2)][i%2],r[i])}:function(t,e,n,i){return wf(t.value,r[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(r,t,e);e.setData(u),u.each((function(e){var n=z(PB,(function(n){return LB(u,e,n,t,i)})),o=r.getAxis("x").scale,s=r.getAxis("y").scale,l=o.getExtent(),h=s.getExtent(),c=[o.parse(u.get("x0",e)),o.parse(u.get("x1",e))],p=[s.parse(u.get("y0",e)),s.parse(u.get("y1",e))];jr(c),jr(p);var d=!!(l[0]>c[1]||l[1]p[1]||h[1]=0},e.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",top:0,align:"auto",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",borderRadius:0,borderWidth:0,padding:5,itemGap:10,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:"#ccc",inactiveBorderColor:"#ccc",inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:"#ccc",inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:"#333"},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:"#666",borderWidth:1,borderColor:"#666"},emphasis:{selectorLabel:{show:!0,color:"#eee",backgroundColor:"#666"}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1}},e}(Rp),NB=H,EB=E,zB=zr,VB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._contentGroup=new zB),this.group.add(this._selectorGroup=new zB),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,e,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),t.get("show",!0)){var r=t.get("align"),o=t.get("orient");r&&"auto"!==r||(r="right"===t.get("left")&&"vertical"===o?"right":"left");var a=t.get("selector",!0),s=t.get("selectorPosition",!0);!a||s&&"auto"!==s||(s="horizontal"===o?"end":"start"),this.renderInner(r,t,e,n,a,o,s);var l=t.getBoxLayoutParams(),u={width:n.getWidth(),height:n.getHeight()},h=t.get("padding"),c=Cp(l,u,h),p=this.layoutInner(t,r,c,i,a,s),d=Cp(k({width:p.width,height:p.height},l),u,h);this.group.x=d.x-p.x,this.group.y=d.y-p.y,this.group.markRedraw(),this.group.add(this._backgroundEl=dz(p,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,e,n,i,r,o,a){var s=this.getContentGroup(),l=yt(),u=e.get("selectedMode"),h=[];n.eachRawSeries((function(t){!t.get("legendHoverLink")&&h.push(t.id)})),EB(e.getData(),(function(r,o){var a=r.get("name");if(!this.newlineDisabled&&(""===a||"\n"===a)){var c=new zB;return c.newline=!0,void s.add(c)}var p=n.getSeriesByName(a)[0];if(!l.get(a)){if(p){var d=p.getData(),f=d.getVisual("legendLineStyle")||{},g=d.getVisual("legendIcon"),y=d.getVisual("style");this._createItem(p,a,o,r,e,t,f,y,g,u,i).on("click",NB(BB,a,null,i,h)).on("mouseover",NB(GB,p.name,null,i,h)).on("mouseout",NB(WB,p.name,null,i,h)),l.set(a,!0)}else n.eachRawSeries((function(n){if(!l.get(a)&&n.legendVisualProvider){var s=n.legendVisualProvider;if(!s.containName(a))return;var c=s.indexOfName(a),p=s.getItemVisual(c,"style"),d=s.getItemVisual(c,"legendIcon"),f=qn(p.fill);f&&0===f[3]&&(f[3]=.2,p=A(A({},p),{fill:ri(f,"rgba")})),this._createItem(n,a,o,r,e,t,{},p,d,u,i).on("click",NB(BB,null,a,i,h)).on("mouseover",NB(GB,null,a,i,h)).on("mouseout",NB(WB,null,a,i,h)),l.set(a,!0)}}),this);0}}),this),r&&this._createSelector(r,e,i,o,a)},e.prototype._createSelector=function(t,e,n,i,r){var o=this.getSelectorGroup();EB(t,(function(t){var i=t.type,r=new Fs({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect"})}});o.add(r),tc(r,{normal:e.getModel("selectorLabel"),emphasis:e.getModel(["emphasis","selectorLabel"])},{defaultText:t.title}),Hl(r)}))},e.prototype._createItem=function(t,e,n,i,r,o,a,s,l,u,h){var c=t.visualDrawType,p=r.get("itemWidth"),d=r.get("itemHeight"),f=r.isSelected(e),g=i.get("symbolRotate"),y=i.get("symbolKeepAspect"),v=i.get("icon"),m=function(t,e,n,i,r,o,a){function s(t,e){"auto"===t.lineWidth&&(t.lineWidth=e.lineWidth>0?2:0),EB(t,(function(n,i){"inherit"===t[i]&&(t[i]=e[i])}))}var l=e.getModel("itemStyle"),u=l.getItemStyle(),h=0===t.lastIndexOf("empty",0)?"fill":"stroke",c=l.getShallow("decal");u.decal=c&&"inherit"!==c?gv(c,a):i.decal,"inherit"===u.fill&&(u.fill=i[r]);"inherit"===u.stroke&&(u.stroke=i[h]);"inherit"===u.opacity&&(u.opacity=("fill"===r?i:n).opacity);s(u,i);var p=e.getModel("lineStyle"),d=p.getLineStyle();if(s(d,n),"auto"===u.fill&&(u.fill=i.fill),"auto"===u.stroke&&(u.stroke=i.fill),"auto"===d.stroke&&(d.stroke=i.fill),!o){var f=e.get("inactiveBorderWidth"),g=u[h];u.lineWidth="auto"===f?i.lineWidth>0&&g?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),d.stroke=p.get("inactiveColor"),d.lineWidth=p.get("inactiveWidth")}return{itemStyle:u,lineStyle:d}}(l=v||l||"roundRect",i,a,s,c,f,h),x=new zB,_=i.getModel("textStyle");if(!X(t.getLegendIcon)||v&&"inherit"!==v){var b="inherit"===v&&t.getData().getVisual("symbol")?"inherit"===g?t.getData().getVisual("symbolRotate"):g:0;x.add(function(t){var e=t.icon||"roundRect",n=Wy(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill,t.symbolKeepAspect);n.setStyle(t.itemStyle),n.rotation=(t.iconRotate||0)*Math.PI/180,n.setOrigin([t.itemWidth/2,t.itemHeight/2]),e.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill="#fff",n.style.lineWidth=2);return n}({itemWidth:p,itemHeight:d,icon:l,iconRotate:b,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}))}else x.add(t.getLegendIcon({itemWidth:p,itemHeight:d,icon:l,iconRotate:g,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:y}));var w="left"===o?p+5:-5,S=o,M=r.get("formatter"),I=e;U(M)&&M?I=M.replace("{name}",null!=e?e:""):X(M)&&(I=M(e));var T=f?_.getTextColor():i.get("inactiveColor");x.add(new Fs({style:nc(_,{text:I,x:w,y:d/2,fill:T,align:S,verticalAlign:"middle"},{inheritColor:T})}));var C=new zs({shape:x.getBoundingRect(),invisible:!0}),D=i.getModel("tooltip");return D.get("show")&&Zh({el:C,componentModel:r,itemName:e,itemTooltipOption:D.option}),x.add(C),x.eachChild((function(t){t.silent=!0})),C.silent=!u,this.getContentGroup().add(x),Hl(x),x.__legendDataIndex=n,x},e.prototype.layoutInner=function(t,e,n,i,r,o){var a=this.getContentGroup(),s=this.getSelectorGroup();Tp(t.get("orient"),a,t.get("itemGap"),n.width,n.height);var l=a.getBoundingRect(),u=[-l.x,-l.y];if(s.markRedraw(),a.markRedraw(),r){Tp("horizontal",s,t.get("selectorItemGap",!0));var h=s.getBoundingRect(),c=[-h.x,-h.y],p=t.get("selectorButtonGap",!0),d=t.getOrient().index,f=0===d?"width":"height",g=0===d?"height":"width",y=0===d?"y":"x";"end"===o?c[d]+=l[f]+p:u[d]+=h[f]+p,c[1-d]+=l[g]/2-h[g]/2,s.x=c[0],s.y=c[1],a.x=u[0],a.y=u[1];var v={x:0,y:0};return v[f]=l[f]+p+h[f],v[g]=Math.max(l[g],h[g]),v[y]=Math.min(0,h[y]+c[1-d]),v}return a.x=u[0],a.y=u[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e}(Tg);function BB(t,e,n,i){WB(t,e,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=t?t:e}),GB(t,e,n,i)}function FB(t){for(var e,n=t.getZr().storage.getDisplayList(),i=0,r=n.length;in[r],f=[-c.x,-c.y];e||(f[i]=l[s]);var g=[0,0],y=[-p.x,-p.y],v=rt(t.get("pageButtonGap",!0),t.get("itemGap",!0));d&&("end"===t.get("pageButtonPosition",!0)?y[i]+=n[r]-p[r]:g[i]+=p[r]+v);y[1-i]+=c[o]/2-p[o]/2,l.setPosition(f),u.setPosition(g),h.setPosition(y);var m={x:0,y:0};if(m[r]=d?n[r]:c[r],m[o]=Math.max(c[o],p[o]),m[a]=Math.min(0,p[a]+y[1-i]),u.__rectSize=n[r],d){var x={x:0,y:0};x[r]=Math.max(n[r]-p[r]-v,0),x[o]=m[o],u.setClipPath(new zs({shape:x})),u.__rectSize=x[r]}else h.eachChild((function(t){t.attr({invisible:!0,silent:!0})}));var _=this._getPageInfo(t);return null!=_.pageIndex&&fh(l,{x:_.contentPosition[0],y:_.contentPosition[1]},d?t:null),this._updatePageInfoView(t,_),m},e.prototype._pageGo=function(t,e,n){var i=this._getPageInfo(e)[t];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:e.id})},e.prototype._updatePageInfoView=function(t,e){var n=this._controllerGroup;E(["pagePrev","pageNext"],(function(i){var r=null!=e[i+"DataIndex"],o=n.childOfName(i);o&&(o.setStyle("fill",r?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),o.cursor=r?"pointer":"default")}));var i=n.childOfName("pageText"),r=t.get("pageFormatter"),o=e.pageIndex,a=null!=o?o+1:0,s=e.pageCount;i&&r&&i.setStyle("text",U(r)?r.replace("{current}",null==a?"":a+"").replace("{total}",null==s?"":s+""):r({current:a,total:s}))},e.prototype._getPageInfo=function(t){var e=t.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,r=t.getOrient().index,o=qB[r],a=KB[r],s=this._findTargetItemIndex(e),l=n.children(),u=l[s],h=l.length,c=h?1:0,p={contentPosition:[n.x,n.y],pageCount:c,pageIndex:c-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!u)return p;var d=m(u);p.contentPosition[r]=-d.s;for(var f=s+1,g=d,y=d,v=null;f<=h;++f)(!(v=m(l[f]))&&y.e>g.s+i||v&&!x(v,g.s))&&(g=y.i>g.i?y:v)&&(null==p.pageNextDataIndex&&(p.pageNextDataIndex=g.i),++p.pageCount),y=v;for(f=s-1,g=d,y=d,v=null;f>=-1;--f)(v=m(l[f]))&&x(y,v.s)||!(g.i=e&&t.s<=e+i}},e.prototype._findTargetItemIndex=function(t){return this._showController?(this.getContentGroup().eachChild((function(i,r){var o=i.__legendDataIndex;null==n&&null!=o&&(n=r),o===t&&(e=r)})),null!=e?e:n):0;var e,n},e.type="legend.scroll",e}(VB);function JB(t){Nm(XB),t.registerComponentModel(UB),t.registerComponentView($B),function(t){t.registerAction("legendScroll","legendscroll",(function(t,e){var n=t.scrollDataIndex;null!=n&&e.eachComponent({mainType:"legend",subType:"scroll",query:t},(function(t){t.setScrollDataIndex(n)}))}))}(t)}var QB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.inside",e.defaultOption=Cc(KE.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),e}(KE),tF=Oo();function eF(t,e,n){tF(t).coordSysRecordMap.each((function(t){var i=t.dataZoomInfoMap.get(e.uid);i&&(i.getRange=n)}))}function nF(t,e){if(e){t.removeKey(e.model.uid);var n=e.controller;n&&n.dispose()}}function iF(t,e){t.isDisposed()||t.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:e})}function rF(t,e,n,i){return t.coordinateSystem.containPoint([n,i])}function oF(t){t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,(function(t,e){var n=tF(e),i=n.coordSysRecordMap||(n.coordSysRecordMap=yt());i.each((function(t){t.dataZoomInfoMap=null})),t.eachComponent({mainType:"dataZoom",subType:"inside"},(function(t){E(jE(t).infoList,(function(n){var r=n.model.uid,o=i.get(r)||i.set(r,function(t,e){var n={model:e,containsPoint:H(rF,e),dispatchAction:H(iF,t),dataZoomInfoMap:null,controller:null},i=n.controller=new UI(t.getZr());return E(["pan","zoom","scrollMove"],(function(t){i.on(t,(function(e){var i=[];n.dataZoomInfoMap.each((function(r){if(e.isAvailableBehavior(r.model.option)){var o=(r.getRange||{})[t],a=o&&o(r.dzReferCoordSysInfo,n.model.mainType,n.controller,e);!r.model.get("disabled",!0)&&a&&i.push({dataZoomId:r.model.id,start:a[0],end:a[1]})}})),i.length&&n.dispatchAction(i)}))})),n}(e,n.model));(o.dataZoomInfoMap||(o.dataZoomInfoMap=yt())).set(t.uid,{dzReferCoordSysInfo:n,model:t,getRange:null})}))})),i.each((function(t){var e,n=t.controller,r=t.dataZoomInfoMap;if(r){var o=r.keys()[0];null!=o&&(e=r.get(o))}if(e){var a=function(t){var e,n="type_",i={type_true:2,type_move:1,type_false:0,type_undefined:-1},r=!0;return t.each((function(t){var o=t.model,a=!o.get("disabled",!0)&&(!o.get("zoomLock",!0)||"move");i[n+a]>i[n+e]&&(e=a),r=r&&o.get("preventDefaultMouseMove",!0)})),{controlType:e,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!r}}}(r);n.enable(a.controlType,a.opt),n.setPointerChecker(t.containsPoint),Fg(t,"dispatchAction",e.model.get("throttle",!0),"fixRate")}else nF(i,t)}))}))}var aF=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="dataZoom.inside",e}return n(e,t),e.prototype.render=function(e,n,i){t.prototype.render.apply(this,arguments),e.noTarget()?this._clear():(this.range=e.getPercentRange(),eF(i,e,{pan:W(sF.pan,this),zoom:W(sF.zoom,this),scrollMove:W(sF.scrollMove,this)}))},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){!function(t,e){for(var n=tF(t).coordSysRecordMap,i=n.keys(),r=0;r0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(o[1]-o[0])+o[0],u=Math.max(1/i.scale,0);o[0]=(o[0]-l)*u+l,o[1]=(o[1]-l)*u+l;var h=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return Ck(0,o,[0,100],0,h.minSpan,h.maxSpan),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}},pan:lF((function(t,e,n,i,r,o){var a=uF[i]([o.oldX,o.oldY],[o.newX,o.newY],e,r,n);return a.signal*(t[1]-t[0])*a.pixel/a.pixelLength})),scrollMove:lF((function(t,e,n,i,r,o){return uF[i]([0,0],[o.scrollDelta,o.scrollDelta],e,r,n).signal*(t[1]-t[0])*o.scrollDelta}))};function lF(t){return function(e,n,i,r){var o=this.range,a=o.slice(),s=e.axisModels[0];if(s)return Ck(t(a,s,e,n,i,r),a,[0,100],"all"),this.range=a,o[0]!==a[0]||o[1]!==a[1]?a:void 0}}var uF={grid:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem.getRect();return t=t||[0,0],"x"===o.dim?(a.pixel=e[0]-t[0],a.pixelLength=s.width,a.pixelStart=s.x,a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=s.height,a.pixelStart=s.y,a.signal=o.inverse?-1:1),a},polar:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return t=t?s.pointToCoord(t):[0,0],e=s.pointToCoord(e),"radiusAxis"===n.mainType?(a.pixel=e[0]-t[0],a.pixelLength=l[1]-l[0],a.pixelStart=l[0],a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=u[1]-u[0],a.pixelStart=u[0],a.signal=o.inverse?-1:1),a},singleAxis:function(t,e,n,i,r){var o=n.axis,a=r.model.coordinateSystem.getRect(),s={};return t=t||[0,0],"horizontal"===o.orient?(s.pixel=e[0]-t[0],s.pixelLength=a.width,s.pixelStart=a.x,s.signal=o.inverse?1:-1):(s.pixel=e[1]-t[1],s.pixelLength=a.height,s.pixelStart=a.y,s.signal=o.inverse?-1:1),s}};function hF(t){az(t),t.registerComponentModel(QB),t.registerComponentView(aF),oF(t)}var cF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.slider",e.layoutMode="box",e.defaultOption=Cc(KE.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:"#d2dbee",borderRadius:3,backgroundColor:"rgba(47,69,84,0)",dataBackground:{lineStyle:{color:"#d2dbee",width:.5},areaStyle:{color:"#d2dbee",opacity:.2}},selectedDataBackground:{lineStyle:{color:"#8fb0f7",width:.5},areaStyle:{color:"#8fb0f7",opacity:.2}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:"#fff",borderColor:"#ACB8D1"},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:"#D2DBEE",opacity:.7},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:"#6E7079"},brushSelect:!0,brushStyle:{color:"rgba(135,175,274,0.15)"},emphasis:{handleStyle:{borderColor:"#8FB0F7"},moveHandleStyle:{color:"#8FB0F7"}}}),e}(KE),pF=zs,dF="horizontal",fF="vertical",gF=["line","bar","candlestick","scatter"],yF={easing:"cubicOut",duration:100,delay:0},vF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._displayables={},n}return n(e,t),e.prototype.init=function(t,e){this.api=e,this._onBrush=W(this._onBrush,this),this._onBrushEnd=W(this._onBrushEnd,this)},e.prototype.render=function(e,n,i,r){if(t.prototype.render.apply(this,arguments),Fg(this,"_dispatchZoomAction",e.get("throttle"),"fixRate"),this._orient=e.getOrient(),!1!==e.get("show")){if(e.noTarget())return this._clear(),void this.group.removeAll();r&&"dataZoom"===r.type&&r.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){Gg(this,"_dispatchZoomAction");var t=this.api.getZr();t.off("mousemove",this._onBrush),t.off("mouseup",this._onBrushEnd)},e.prototype._buildView=function(){var t=this.group;t.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var e=this._displayables.sliderGroup=new zr;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(e),this._positionGroup()},e.prototype._resetLocation=function(){var t=this.dataZoomModel,e=this.api,n=t.get("brushSelect")?7:0,i=this._findCoordRect(),r={width:e.getWidth(),height:e.getHeight()},o=this._orient===dF?{right:r.width-i.x-i.width,top:r.height-30-7-n,width:i.width,height:30}:{right:7,top:i.y,width:30,height:i.height},a=Lp(t.option);E(["right","top","width","height"],(function(t){"ph"===a[t]&&(a[t]=o[t])}));var s=Cp(a,r);this._location={x:s.x,y:s.y},this._size=[s.width,s.height],this._orient===fF&&this._size.reverse()},e.prototype._positionGroup=function(){var t=this.group,e=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),r=i&&i.get("inverse"),o=this._displayables.sliderGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;o.attr(n!==dF||r?n===dF&&r?{scaleY:a?1:-1,scaleX:-1}:n!==fF||r?{scaleY:a?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:a?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:a?1:-1,scaleX:1});var s=t.getBoundingRect([o]);t.x=e.x-s.x,t.y=e.y-s.y,t.markRedraw()},e.prototype._getViewExtent=function(){return[0,this._size[0]]},e.prototype._renderBackground=function(){var t=this.dataZoomModel,e=this._size,n=this._displayables.sliderGroup,i=t.get("brushSelect");n.add(new pF({silent:!0,shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:t.get("backgroundColor")},z2:-40}));var r=new pF({shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:"transparent"},z2:0,onclick:W(this._onClickPanel,this)}),o=this.api.getZr();i?(r.on("mousedown",this._onBrushStart,this),r.cursor="crosshair",o.on("mousemove",this._onBrush),o.on("mouseup",this._onBrushEnd)):(o.off("mousemove",this._onBrush),o.off("mouseup",this._onBrushEnd)),n.add(r)},e.prototype._renderDataShadow=function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],t){var e=this._size,n=this._shadowSize||[],i=t.series,r=i.getRawData(),o=i.getShadowDim&&i.getShadowDim(),a=o&&r.getDimensionInfo(o)?i.getShadowDim():t.otherDim;if(null!=a){var s=this._shadowPolygonPts,l=this._shadowPolylinePts;if(r!==this._shadowData||a!==this._shadowDim||e[0]!==n[0]||e[1]!==n[1]){var u=r.getDataExtent(a),h=.3*(u[1]-u[0]);u=[u[0]-h,u[1]+h];var c,p=[0,e[1]],d=[0,e[0]],f=[[e[0],0],[0,0]],g=[],y=d[1]/(r.count()-1),v=0,m=Math.round(r.count()/e[0]);r.each([a],(function(t,e){if(m>0&&e%m)v+=y;else{var n=null==t||isNaN(t)||""===t,i=n?0:Xr(t,u,p,!0);n&&!c&&e?(f.push([f[f.length-1][0],0]),g.push([g[g.length-1][0],0])):!n&&c&&(f.push([v,0]),g.push([v,0])),f.push([v,i]),g.push([v,i]),v+=y,c=n}})),s=this._shadowPolygonPts=f,l=this._shadowPolylinePts=g}this._shadowData=r,this._shadowDim=a,this._shadowSize=[e[0],e[1]];for(var x=this.dataZoomModel,_=0;_<3;_++){var b=w(1===_);this._displayables.sliderGroup.add(b),this._displayables.dataShadowSegs.push(b)}}}function w(t){var e=x.getModel(t?"selectedDataBackground":"dataBackground"),n=new zr,i=new Wu({shape:{points:s},segmentIgnoreThreshold:1,style:e.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),r=new Yu({shape:{points:l},segmentIgnoreThreshold:1,style:e.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(r),n}},e.prototype._prepareDataShadowInfo=function(){var t=this.dataZoomModel,e=t.get("showDataShadow");if(!1!==e){var n,i=this.ecModel;return t.eachTargetAxis((function(r,o){E(t.getAxisProxy(r,o).getTargetSeriesModels(),(function(t){if(!(n||!0!==e&&P(gF,t.get("type"))<0)){var a,s=i.getComponent(UE(r),o).axis,l=function(t){var e={x:"y",y:"x",radius:"angle",angle:"radius"};return e[t]}(r),u=t.coordinateSystem;null!=l&&u.getOtherAxis&&(a=u.getOtherAxis(s).inverse),l=t.getData().mapDimension(l),n={thisAxis:s,series:t,thisDim:r,otherDim:l,otherAxisInverse:a}}}),this)}),this),n}},e.prototype._renderHandle=function(){var t=this.group,e=this._displayables,n=e.handles=[null,null],i=e.handleLabels=[null,null],r=this._displayables.sliderGroup,o=this._size,a=this.dataZoomModel,s=this.api,l=a.get("borderRadius")||0,u=a.get("brushSelect"),h=e.filler=new pF({silent:u,style:{fill:a.get("fillerColor")},textConfig:{position:"inside"}});r.add(h),r.add(new pF({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:o[0],height:o[1],r:l},style:{stroke:a.get("dataBackgroundColor")||a.get("borderColor"),lineWidth:1,fill:"rgba(0,0,0,0)"}})),E([0,1],(function(e){var o=a.get("handleIcon");!By[o]&&o.indexOf("path://")<0&&o.indexOf("image://")<0&&(o="path://"+o);var s=Wy(o,-1,0,2,2,null,!0);s.attr({cursor:mF(this._orient),draggable:!0,drift:W(this._onDragMove,this,e),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1),z2:5});var l=s.getBoundingRect(),u=a.get("handleSize");this._handleHeight=Ur(u,this._size[1]),this._handleWidth=l.width/l.height*this._handleHeight,s.setStyle(a.getModel("handleStyle").getItemStyle()),s.style.strokeNoScale=!0,s.rectHover=!0,s.ensureState("emphasis").style=a.getModel(["emphasis","handleStyle"]).getItemStyle(),Hl(s);var h=a.get("handleColor");null!=h&&(s.style.fill=h),r.add(n[e]=s);var c=a.getModel("textStyle");t.add(i[e]=new Fs({silent:!0,invisible:!0,style:nc(c,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:c.getTextColor(),font:c.getFont()}),z2:10}))}),this);var c=h;if(u){var p=Ur(a.get("moveHandleSize"),o[1]),d=e.moveHandle=new zs({style:a.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:o[1]-.5,height:p}}),f=.8*p,g=e.moveHandleIcon=Wy(a.get("moveHandleIcon"),-f/2,-f/2,f,f,"#fff",!0);g.silent=!0,g.y=o[1]+p/2-.5,d.ensureState("emphasis").style=a.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var y=Math.min(o[1]/2,Math.max(p,10));(c=e.moveZone=new zs({invisible:!0,shape:{y:o[1]-y,height:p+y}})).on("mouseover",(function(){s.enterEmphasis(d)})).on("mouseout",(function(){s.leaveEmphasis(d)})),r.add(d),r.add(g),r.add(c)}c.attr({draggable:!0,cursor:mF(this._orient),drift:W(this._onDragMove,this,"all"),ondragstart:W(this._showDataInfo,this,!0),ondragend:W(this._onDragEnd,this),onmouseover:W(this._showDataInfo,this,!0),onmouseout:W(this._showDataInfo,this,!1)})},e.prototype._resetInterval=function(){var t=this._range=this.dataZoomModel.getPercentRange(),e=this._getViewExtent();this._handleEnds=[Xr(t[0],[0,100],e,!0),Xr(t[1],[0,100],e,!0)]},e.prototype._updateInterval=function(t,e){var n=this.dataZoomModel,i=this._handleEnds,r=this._getViewExtent(),o=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];Ck(e,i,r,n.get("zoomLock")?"all":t,null!=o.minSpan?Xr(o.minSpan,a,r,!0):null,null!=o.maxSpan?Xr(o.maxSpan,a,r,!0):null);var s=this._range,l=this._range=jr([Xr(i[0],r,a,!0),Xr(i[1],r,a,!0)]);return!s||s[0]!==l[0]||s[1]!==l[1]},e.prototype._updateView=function(t){var e=this._displayables,n=this._handleEnds,i=jr(n.slice()),r=this._size;E([0,1],(function(t){var i=e.handles[t],o=this._handleHeight;i.attr({scaleX:o/2,scaleY:o/2,x:n[t]+(t?-1:1),y:r[1]/2-o/2})}),this),e.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:r[1]});var o={x:i[0],width:i[1]-i[0]};e.moveHandle&&(e.moveHandle.setShape(o),e.moveZone.setShape(o),e.moveZone.getBoundingRect(),e.moveHandleIcon&&e.moveHandleIcon.attr("x",o.x+o.width/2));for(var a=e.dataShadowSegs,s=[0,i[0],i[1],r[0]],l=0;le[0]||n[1]<0||n[1]>e[1])){var i=this._handleEnds,r=(i[0]+i[1])/2,o=this._updateInterval("all",n[0]-r);this._updateView(),o&&this._dispatchZoomAction(!1)}},e.prototype._onBrushStart=function(t){var e=t.offsetX,n=t.offsetY;this._brushStart=new De(e,n),this._brushing=!0,this._brushStartTime=+new Date},e.prototype._onBrushEnd=function(t){if(this._brushing){var e=this._displayables.brushRect;if(this._brushing=!1,e){e.attr("ignore",!0);var n=e.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),r=[0,100];this._range=jr([Xr(n.x,i,r,!0),Xr(n.x+n.width,i,r,!0)]),this._handleEnds=[n.x,n.x+n.width],this._updateView(),this._dispatchZoomAction(!1)}}}},e.prototype._onBrush=function(t){this._brushing&&(de(t.event),this._updateBrushRect(t.offsetX,t.offsetY))},e.prototype._updateBrushRect=function(t,e){var n=this._displayables,i=this.dataZoomModel,r=n.brushRect;r||(r=n.brushRect=new pF({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(r)),r.attr("ignore",!1);var o=this._brushStart,a=this._displayables.sliderGroup,s=a.transformCoordToLocal(t,e),l=a.transformCoordToLocal(o.x,o.y),u=this._size;s[0]=Math.max(Math.min(u[0],s[0]),0),r.setShape({x:l[0],y:0,width:s[0]-l[0],height:u[1]})},e.prototype._dispatchZoomAction=function(t){var e=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:t?yF:null,start:e[0],end:e[1]})},e.prototype._findCoordRect=function(){var t,e=jE(this.dataZoomModel).infoList;if(!t&&e.length){var n=e[0].model.coordinateSystem;t=n.getRect&&n.getRect()}if(!t){var i=this.api.getWidth(),r=this.api.getHeight();t={x:.2*i,y:.2*r,width:.6*i,height:.6*r}}return t},e.type="dataZoom.slider",e}(QE);function mF(t){return"vertical"===t?"ns-resize":"ew-resize"}function xF(t){t.registerComponentModel(cF),t.registerComponentView(vF),az(t)}var _F=function(t,e,n){var i=T((bF[t]||{})[e]);return n&&Y(i)?i[i.length-1]:i},bF={color:{active:["#006edd","#e0ffff"],inactive:["rgba(0,0,0,0)"]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},wF=_D.mapVisual,SF=_D.eachVisual,MF=Y,IF=E,TF=jr,CF=Xr,DF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n)},e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&wV(n,t,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},e.prototype.resetVisual=function(t){var e=this.stateList;t=W(t,this),this.controllerVisuals=bV(this.option.controller,e,t),this.targetVisuals=bV(this.option.target,e,t)},e.prototype.getItemSymbol=function(){return null},e.prototype.getTargetSeriesIndices=function(){var t=this.option.seriesIndex,e=[];return null==t||"all"===t?this.ecModel.eachSeries((function(t,n){e.push(n)})):e=bo(t),e},e.prototype.eachTargetSeries=function(t,e){E(this.getTargetSeriesIndices(),(function(n){var i=this.ecModel.getSeriesByIndex(n);i&&t.call(e,i)}),this)},e.prototype.isTargetSeries=function(t){var e=!1;return this.eachTargetSeries((function(n){n===t&&(e=!0)})),e},e.prototype.formatValueText=function(t,e,n){var i,r=this.option,o=r.precision,a=this.dataBound,s=r.formatter;n=n||["<",">"],Y(t)&&(t=t.slice(),i=!0);var l=e?t:i?[u(t[0]),u(t[1])]:u(t);return U(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):X(s)?i?s(t[0],t[1]):s(t):i?t[0]===a[0]?n[0]+" "+l[1]:t[1]===a[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function u(t){return t===a[0]?"min":t===a[1]?"max":(+t).toFixed(Math.min(o,20))}},e.prototype.resetExtent=function(){var t=this.option,e=TF([t.min,t.max]);this._dataExtent=e},e.prototype.getDataDimensionIndex=function(t){var e=this.option.dimension;if(null!=e)return t.getDimensionIndex(e);for(var n=t.dimensions,i=n.length-1;i>=0;i--){var r=n[i],o=t.getDimensionInfo(r);if(!o.isCalculationCoord)return o.storeDimIndex}},e.prototype.getExtent=function(){return this._dataExtent.slice()},e.prototype.completeVisualOption=function(){var t=this.ecModel,e=this.option,n={inRange:e.inRange,outOfRange:e.outOfRange},i=e.target||(e.target={}),r=e.controller||(e.controller={});C(i,n),C(r,n);var o=this.isCategory();function a(n){MF(e.color)&&!n.inRange&&(n.inRange={color:e.color.slice().reverse()}),n.inRange=n.inRange||{color:t.get("gradientColor")}}a.call(this,i),a.call(this,r),function(t,e,n){var i=t[e],r=t[n];i&&!r&&(r=t[n]={},IF(i,(function(t,e){if(_D.isValidType(e)){var n=_F(e,"inactive",o);null!=n&&(r[e]=n,"color"!==e||r.hasOwnProperty("opacity")||r.hasOwnProperty("colorAlpha")||(r.opacity=[0,0]))}})))}.call(this,i,"inRange","outOfRange"),function(t){var e=(t.inRange||{}).symbol||(t.outOfRange||{}).symbol,n=(t.inRange||{}).symbolSize||(t.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),r=this.getItemSymbol()||"roundRect";IF(this.stateList,(function(a){var s=this.itemSize,l=t[a];l||(l=t[a]={color:o?i:[i]}),null==l.symbol&&(l.symbol=e&&T(e)||(o?r:[r])),null==l.symbolSize&&(l.symbolSize=n&&T(n)||(o?s[0]:[s[0],s[0]])),l.symbol=wF(l.symbol,(function(t){return"none"===t?r:t}));var u=l.symbolSize;if(null!=u){var h=-1/0;SF(u,(function(t){t>h&&(h=t)})),l.symbolSize=wF(u,(function(t){return CF(t,[0,h],[0,s[0]],!0)}))}}),this)}.call(this,r)},e.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},e.prototype.isCategory=function(){return!!this.option.categories},e.prototype.setSelected=function(t){},e.prototype.getSelected=function(){return null},e.prototype.getValueState=function(t){return null},e.prototype.getVisualMeta=function(t){return null},e.type="visualMap",e.dependencies=["series"],e.defaultOption={show:!0,z:4,seriesIndex:"all",min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:"rgba(0,0,0,0)",borderColor:"#ccc",contentColor:"#5793f3",inactiveColor:"#aaa",borderWidth:0,padding:5,textGap:10,precision:0,textStyle:{color:"#333"}},e}(Rp),AF=[20,140],kF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual((function(t){t.mappingMethod="linear",t.dataExtent=this.getExtent()})),this._resetRange()},e.prototype.resetItemSize=function(){t.prototype.resetItemSize.apply(this,arguments);var e=this.itemSize;(null==e[0]||isNaN(e[0]))&&(e[0]=AF[0]),(null==e[1]||isNaN(e[1]))&&(e[1]=AF[1])},e.prototype._resetRange=function(){var t=this.getExtent(),e=this.option.range;!e||e.auto?(t.auto=1,this.option.range=t):Y(e)&&(e[0]>e[1]&&e.reverse(),e[0]=Math.max(e[0],t[0]),e[1]=Math.min(e[1],t[1]))},e.prototype.completeVisualOption=function(){t.prototype.completeVisualOption.apply(this,arguments),E(this.stateList,(function(t){var e=this.option.controller[t].symbolSize;e&&e[0]!==e[1]&&(e[0]=e[1]/3)}),this)},e.prototype.setSelected=function(t){this.option.range=t.slice(),this._resetRange()},e.prototype.getSelected=function(){var t=this.getExtent(),e=jr((this.get("range")||[]).slice());return e[0]>t[1]&&(e[0]=t[1]),e[1]>t[1]&&(e[1]=t[1]),e[0]=n[1]||t<=e[1])?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[];return this.eachTargetSeries((function(n){var i=[],r=n.getData();r.each(this.getDataDimensionIndex(r),(function(e,n){t[0]<=e&&e<=t[1]&&i.push(n)}),this),e.push({seriesId:n.id,dataIndex:i})}),this),e},e.prototype.getVisualMeta=function(t){var e=LF(this,"outOfRange",this.getExtent()),n=LF(this,"inRange",this.option.range.slice()),i=[];function r(e,n){i.push({value:e,color:t(e,n)})}for(var o=0,a=0,s=n.length,l=e.length;at[1])break;n.push({color:this.getControllerVisual(o,"color",e),offset:r/100})}return n.push({color:this.getControllerVisual(t[1],"color",e),offset:1}),n},e.prototype._createBarPoints=function(t,e){var n=this.visualMapModel.itemSize;return[[n[0]-e[0],t[0]],[n[0],t[0]],[n[0],t[1]],[n[0]-e[1],t[1]]]},e.prototype._createBarGroup=function(t){var e=this._orient,n=this.visualMapModel.get("inverse");return new zr("horizontal"!==e||n?"horizontal"===e&&n?{scaleX:"bottom"===t?-1:1,rotation:-Math.PI/2}:"vertical"!==e||n?{scaleX:"left"===t?1:-1}:{scaleX:"left"===t?1:-1,scaleY:-1}:{scaleX:"bottom"===t?1:-1,rotation:Math.PI/2})},e.prototype._updateHandle=function(t,e){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,r=n.handleThumbs,o=n.handleLabels,a=i.itemSize,s=i.getExtent();zF([0,1],(function(l){var u=r[l];u.setStyle("fill",e.handlesColor[l]),u.y=t[l];var h=EF(t[l],[0,a[1]],s,!0),c=this.getControllerVisual(h,"symbolSize");u.scaleX=u.scaleY=c/a[0],u.x=a[0]-c/2;var p=zh(n.handleLabelPoints[l],Eh(u,this.group));o[l].setStyle({x:p[0],y:p[1],text:i.formatValueText(this._dataInterval[l]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})}),this)}},e.prototype._showIndicator=function(t,e,n,i){var r=this.visualMapModel,o=r.getExtent(),a=r.itemSize,s=[0,a[1]],l=this._shapes,u=l.indicator;if(u){u.attr("invisible",!1);var h=this.getControllerVisual(t,"color",{convertOpacityToAlpha:!0}),c=this.getControllerVisual(t,"symbolSize"),p=EF(t,o,s,!0),d=a[0]-c/2,f={x:u.x,y:u.y};u.y=p,u.x=d;var g=zh(l.indicatorLabelPoint,Eh(u,this.group)),y=l.indicatorLabel;y.attr("invisible",!1);var v=this._applyTransform("left",l.mainGroup),m="horizontal"===this._orient;y.setStyle({text:(n||"")+r.formatValueText(e),verticalAlign:m?v:"middle",align:m?"center":v});var x={x:d,y:p,style:{fill:h}},_={style:{x:g[0],y:g[1]}};if(r.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var b={duration:100,easing:"cubicInOut",additive:!0};u.x=f.x,u.y=f.y,u.animateTo(x,b),y.animateTo(_,b)}else u.attr(x),y.attr(_);this._firstShowIndicator=!1;var w=this._shapes.handleLabels;if(w)for(var S=0;Sr[1]&&(u[1]=1/0),e&&(u[0]===-1/0?this._showIndicator(l,u[1],"< ",a):u[1]===1/0?this._showIndicator(l,u[0],"> ",a):this._showIndicator(l,l,"≈ ",a));var h=this._hoverLinkDataIndices,c=[];(e||WF(n))&&(c=this._hoverLinkDataIndices=n.findTargetDataIndices(u));var p=function(t,e){var n={},i={};return r(t||[],n),r(e||[],i,n),[o(n),o(i)];function r(t,e,n){for(var i=0,r=t.length;i=0&&(r.dimension=o,i.push(r))}})),t.getData().setVisual("visualMeta",i)}}];function ZF(t,e,n,i){for(var r=e.targetVisuals[i],o=_D.prepareVisualTypes(r),a={color:Ty(t.getData(),"color")},s=0,l=o.length;s0:t.splitNumber>0)&&!t.calculable?"piecewise":"continuous"})),t.registerAction(YF,XF),E(UF,(function(e){t.registerVisual(t.PRIORITY.VISUAL.COMPONENT,e)})),t.registerPreprocessor(qF))}function QF(t){t.registerComponentModel(kF),t.registerComponentView(FF),JF(t)}var tG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._pieceList=[],n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],eG[this._mode].call(this,this._pieceList),this._resetSelected(e,n);var r=this.option.categories;this.resetVisual((function(t,e){"categories"===i?(t.mappingMethod="category",t.categories=T(r)):(t.dataExtent=this.getExtent(),t.mappingMethod="piecewise",t.pieceList=z(this._pieceList,(function(t){return t=T(t),"inRange"!==e&&(t.visual=null),t})))}))},e.prototype.completeVisualOption=function(){var e=this.option,n={},i=_D.listVisualTypes(),r=this.isCategory();function o(t,e,n){return t&&t[e]&&t[e].hasOwnProperty(n)}E(e.pieces,(function(t){E(i,(function(e){t.hasOwnProperty(e)&&(n[e]=1)}))})),E(n,(function(t,n){var i=!1;E(this.stateList,(function(t){i=i||o(e,t,n)||o(e.target,t,n)}),this),!i&&E(this.stateList,(function(t){(e[t]||(e[t]={}))[n]=_F(n,"inRange"===t?"active":"inactive",r)}))}),this),t.prototype.completeVisualOption.apply(this,arguments)},e.prototype._resetSelected=function(t,e){var n=this.option,i=this._pieceList,r=(e?n:t).selected||{};if(n.selected=r,E(i,(function(t,e){var n=this.getSelectedMapKey(t);r.hasOwnProperty(n)||(r[n]=!0)}),this),"single"===n.selectedMode){var o=!1;E(i,(function(t,e){var n=this.getSelectedMapKey(t);r[n]&&(o?r[n]=!1:o=!0)}),this)}},e.prototype.getItemSymbol=function(){return this.get("itemSymbol")},e.prototype.getSelectedMapKey=function(t){return"categories"===this._mode?t.value+"":t.index+""},e.prototype.getPieceList=function(){return this._pieceList},e.prototype._determineMode=function(){var t=this.option;return t.pieces&&t.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},e.prototype.setSelected=function(t){this.option.selected=T(t)},e.prototype.getValueState=function(t){var e=_D.findPieceIndex(t,this._pieceList);return null!=e&&this.option.selected[this.getSelectedMapKey(this._pieceList[e])]?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[],n=this._pieceList;return this.eachTargetSeries((function(i){var r=[],o=i.getData();o.each(this.getDataDimensionIndex(o),(function(e,i){_D.findPieceIndex(e,n)===t&&r.push(i)}),this),e.push({seriesId:i.id,dataIndex:r})}),this),e},e.prototype.getRepresentValue=function(t){var e;if(this.isCategory())e=t.value;else if(null!=t.value)e=t.value;else{var n=t.interval||[];e=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return e},e.prototype.getVisualMeta=function(t){if(!this.isCategory()){var e=[],n=["",""],i=this,r=this._pieceList.slice();if(r.length){var o=r[0].interval[0];o!==-1/0&&r.unshift({interval:[-1/0,o]}),(o=r[r.length-1].interval[1])!==1/0&&r.push({interval:[o,1/0]})}else r.push({interval:[-1/0,1/0]});var a=-1/0;return E(r,(function(t){var e=t.interval;e&&(e[0]>a&&s([a,e[0]],"outOfRange"),s(e.slice()),a=e[1])}),this),{stops:e,outerColors:n}}function s(r,o){var a=i.getRepresentValue({interval:r});o||(o=i.getValueState(a));var s=t(a,o);r[0]===-1/0?n[0]=s:r[1]===1/0?n[1]=s:e.push({value:r[0],color:s},{value:r[1],color:s})}},e.type="visualMap.piecewise",e.defaultOption=Cc(DF.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),e}(DF),eG={splitNumber:function(t){var e=this.option,n=Math.min(e.precision,20),i=this.getExtent(),r=e.splitNumber;r=Math.max(parseInt(r,10),1),e.splitNumber=r;for(var o=(i[1]-i[0])/r;+o.toFixed(n)!==o&&n<5;)n++;e.precision=n,o=+o.toFixed(n),e.minOpen&&t.push({interval:[-1/0,i[0]],close:[0,0]});for(var a=0,s=i[0];a","≥"][e[0]]];t.text=t.text||this.formatValueText(null!=t.value?t.value:t.interval,!1,n)}),this)}};function nG(t,e){var n=t.inverse;("vertical"===t.orient?!n:n)&&e.reverse()}var iG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.doRender=function(){var t=this.group;t.removeAll();var e=this.visualMapModel,n=e.get("textGap"),i=e.textStyleModel,r=i.getFont(),o=i.getTextColor(),a=this._getItemAlign(),s=e.itemSize,l=this._getViewData(),u=l.endsText,h=it(e.get("showLabel",!0),!u);u&&this._renderEndsText(t,u[0],s,h,a),E(l.viewPieceList,(function(i){var l=i.piece,u=new zr;u.onclick=W(this._onItemClick,this,l),this._enableHoverLink(u,i.indexInModelPieceList);var c=e.getRepresentValue(l);if(this._createItemSymbol(u,c,[0,0,s[0],s[1]]),h){var p=this.visualMapModel.getValueState(c);u.add(new Fs({style:{x:"right"===a?-n:s[0]+n,y:s[1]/2,text:l.text,verticalAlign:"middle",align:a,font:r,fill:o,opacity:"outOfRange"===p?.5:1}}))}t.add(u)}),this),u&&this._renderEndsText(t,u[1],s,h,a),Tp(e.get("orient"),t,e.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},e.prototype._enableHoverLink=function(t,e){var n=this;t.on("mouseover",(function(){return i("highlight")})).on("mouseout",(function(){return i("downplay")}));var i=function(t){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:t,batch:NF(i.findTargetDataIndices(e),i)})}},e.prototype._getItemAlign=function(){var t=this.visualMapModel,e=t.option;if("vertical"===e.orient)return RF(t,this.api,t.itemSize);var n=e.align;return n&&"auto"!==n||(n="left"),n},e.prototype._renderEndsText=function(t,e,n,i,r){if(e){var o=new zr,a=this.visualMapModel.textStyleModel;o.add(new Fs({style:nc(a,{x:i?"right"===r?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?r:"center",text:e})})),t.add(o)}},e.prototype._getViewData=function(){var t=this.visualMapModel,e=z(t.getPieceList(),(function(t,e){return{piece:t,indexInModelPieceList:e}})),n=t.get("text"),i=t.get("orient"),r=t.get("inverse");return("horizontal"===i?r:!r)?e.reverse():n&&(n=n.slice().reverse()),{viewPieceList:e,endsText:n}},e.prototype._createItemSymbol=function(t,e,n){t.add(Wy(this.getControllerVisual(e,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(e,"color")))},e.prototype._onItemClick=function(t){var e=this.visualMapModel,n=e.option,i=n.selectedMode;if(i){var r=T(n.selected),o=e.getSelectedMapKey(t);"single"===i||!0===i?(r[o]=!0,E(r,(function(t,e){r[e]=e===o}))):r[o]=!r[o],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:r})}},e.type="visualMap.piecewise",e}(PF);function rG(t){t.registerComponentModel(tG),t.registerComponentView(iG),JF(t)}var oG={label:{enabled:!0},decal:{show:!1}},aG=Oo(),sG={};function lG(t,e){var n=t.getModel("aria");if(n.get("enabled")){var i=T(oG);C(i.label,t.getLocaleModel().get("aria"),!1),C(n.option,i,!1),function(){if(n.getModel("decal").get("show")){var e=yt();t.eachSeries((function(t){if(!t.isColorBySeries()){var n=e.get(t.type);n||(n={},e.set(t.type,n)),aG(t).scope=n}})),t.eachRawSeries((function(e){if(!t.isSeriesFiltered(e))if(X(e.enableAriaDecal))e.enableAriaDecal();else{var n=e.getData();if(e.isColorBySeries()){var i=ud(e.ecModel,e.name,sG,t.getSeriesCount()),r=n.getVisual("decal");n.setVisual("decal",u(r,i))}else{var o=e.getRawData(),a={},s=aG(e).scope;n.each((function(t){var e=n.getRawIndex(t);a[e]=t}));var l=o.count();o.each((function(t){var i=a[t],r=o.getName(t)||t+"",h=ud(e.ecModel,r,s,l),c=n.getItemVisual(i,"decal");n.setItemVisual(i,"decal",u(c,h))}))}}function u(t,e){var n=t?A(A({},e),t):e;return n.dirty=!0,n}}))}}(),function(){var i=t.getLocaleModel().get("aria"),o=n.getModel("label");if(o.option=k(o.option,i),!o.get("enabled"))return;var a=e.getZr().dom;if(o.get("description"))return void a.setAttribute("aria-label",o.get("description"));var s,l=t.getSeriesCount(),u=o.get(["data","maxCount"])||10,h=o.get(["series","maxCount"])||10,c=Math.min(l,h);if(l<1)return;var p=function(){var e=t.get("title");e&&e.length&&(e=e[0]);return e&&e.text}();s=p?r(o.get(["general","withTitle"]),{title:p}):o.get(["general","withoutTitle"]);var d=[];s+=r(l>1?o.get(["series","multiple","prefix"]):o.get(["series","single","prefix"]),{seriesCount:l}),t.eachSeries((function(e,n){if(n1?o.get(["series","multiple",a]):o.get(["series","single",a]),{seriesId:e.seriesIndex,seriesName:e.get("name"),seriesType:(x=e.subType,t.getLocaleModel().get(["series","typeNames"])[x]||"自定义图")});var s=e.getData();if(s.count()>u)i+=r(o.get(["data","partialData"]),{displayCnt:u});else i+=o.get(["data","allData"]);for(var h=o.get(["data","separator","middle"]),p=o.get(["data","separator","end"]),f=[],g=0;g":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},cG=function(){function t(t){if(null==(this._condVal=U(t)?new RegExp(t):et(t)?t:null)){var e="";0,vo(e)}}return t.prototype.evaluate=function(t){var e=typeof t;return U(e)?this._condVal.test(t):!!j(e)&&this._condVal.test(t+"")},t}(),pG=function(){function t(){}return t.prototype.evaluate=function(){return this.value},t}(),dG=function(){function t(){}return t.prototype.evaluate=function(){for(var t=this.children,e=0;e2&&l.push(e),e=[t,n]}function f(t,n,i,r){TG(t,i)&&TG(n,r)||e.push(t,n,i,r,i,r)}function g(t,n,i,r,o,a){var s=Math.abs(n-t),l=4*Math.tan(s/4)/3,u=nM:C2&&l.push(e),l}function DG(t,e,n,i,r,o,a,s,l,u){if(TG(t,n)&&TG(e,i)&&TG(r,a)&&TG(o,s))l.push(a,s);else{var h=2/u,c=h*h,p=a-t,d=s-e,f=Math.sqrt(p*p+d*d);p/=f,d/=f;var g=n-t,y=i-e,v=r-a,m=o-s,x=g*g+y*y,_=v*v+m*m;if(x=0&&_-w*w=0)l.push(a,s);else{var S=[],M=[];wn(t,n,r,a,.5,S),wn(e,i,o,s,.5,M),DG(S[0],M[0],S[1],M[1],S[2],M[2],S[3],M[3],l,u),DG(S[4],M[4],S[5],M[5],S[6],M[6],S[7],M[7],l,u)}}}}function AG(t,e,n){var i=t[e],r=t[1-e],o=Math.abs(i/r),a=Math.ceil(Math.sqrt(o*n)),s=Math.floor(n/a);0===s&&(s=1,a=n);for(var l=[],u=0;u0)for(u=0;uMath.abs(u),c=AG([l,u],h?0:1,e),p=(h?s:u)/c.length,d=0;d1?null:new De(d*l+t,d*u+e)}function OG(t,e,n){var i=new De;De.sub(i,n,e),i.normalize();var r=new De;return De.sub(r,t,e),r.dot(i)}function RG(t,e){var n=t[t.length-1];n&&n[0]===e[0]&&n[1]===e[1]||t.push(e)}function NG(t){var e=t.points,n=[],i=[];Ra(e,n,i);var r=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]),o=r.width,a=r.height,s=r.x,l=r.y,u=new De,h=new De;return o>a?(u.x=h.x=s+o/2,u.y=l,h.y=l+a):(u.y=h.y=l+a/2,u.x=s,h.x=s+o),function(t,e,n){for(var i=t.length,r=[],o=0;or,a=AG([i,r],o?0:1,e),s=o?"width":"height",l=o?"height":"width",u=o?"x":"y",h=o?"y":"x",c=t[s]/a.length,p=0;p0)for(var b=i/n,w=-i/2;w<=i/2;w+=b){var S=Math.sin(w),M=Math.cos(w),I=0;for(x=0;x0;l/=2){var u=0,h=0;(t&l)>0&&(u=1),(e&l)>0&&(h=1),s+=l*l*(3*u^h),0===h&&(1===u&&(t=l-1-t,e=l-1-e),a=t,t=e,e=a)}return s}function JG(t){var e=1/0,n=1/0,i=-1/0,r=-1/0,o=z(t,(function(t){var o=t.getBoundingRect(),a=t.getComputedTransform(),s=o.x+o.width/2+(a?a[4]:0),l=o.y+o.height/2+(a?a[5]:0);return e=Math.min(s,e),n=Math.min(l,n),i=Math.max(s,i),r=Math.max(l,r),[s,l]}));return z(o,(function(o,a){return{cp:o,z:$G(o[0],o[1],e,n,i,r),path:t[a]}})).sort((function(t,e){return t.z-e.z})).map((function(t){return t.path}))}function QG(t){return VG(t.path,t.count)}function tW(t){return Y(t[0])}function eW(t,e){for(var n=[],i=t.length,r=0;r=0;r--)if(!n[r].many.length){var l=n[s].many;if(l.length<=1){if(!s)return n;s=0}o=l.length;var u=Math.ceil(o/2);n[r].many=l.slice(u,o),n[s].many=l.slice(0,u),s++}return n}var nW={clone:function(t){for(var e=[],n=1-Math.pow(1-t.path.style.opacity,1/t.count),i=0;i0){var s,l,u=i.getModel("universalTransition").get("delay"),h=Object.assign({setToFinal:!0},a);tW(t)&&(s=t,l=e),tW(e)&&(s=e,l=t);for(var c=s?s===t:t.length>e.length,p=s?eW(l,s):eW(c?e:t,[c?t:e]),d=0,f=0;f1e4))for(var i=n.getIndices(),r=function(t){for(var e=t.dimensions,n=0;n0&&i.group.traverse((function(t){t instanceof Is&&!t.animators.length&&t.animateFrom({style:{opacity:0}},r)}))}))}function pW(t){var e=t.getModel("universalTransition").get("seriesKey");return e||t.id}function dW(t){return Y(t)?t.sort().join(","):t}function fW(t){if(t.hostModel)return t.hostModel.getModel("universalTransition").get("divideShape")}function gW(t,e){for(var n=0;n=0&&r.push({dataGroupId:e.oldDataGroupIds[n],data:e.oldData[n],divide:fW(e.oldData[n]),dim:t.dimension})})),E(bo(t.to),(function(t){var i=gW(n.updatedSeries,t);if(i>=0){var r=n.updatedSeries[i].getData();o.push({dataGroupId:e.oldDataGroupIds[i],data:r,divide:fW(r),dim:t.dimension})}})),r.length>0&&o.length>0&&cW(r,o,i)}(t,i,n,e)}));else{var o=function(t,e){var n=yt(),i=yt(),r=yt();return E(t.oldSeries,(function(e,n){var o=t.oldDataGroupIds[n],a=t.oldData[n],s=pW(e),l=dW(s);i.set(l,{dataGroupId:o,data:a}),Y(s)&&E(s,(function(t){r.set(t,{key:l,dataGroupId:o,data:a})}))})),E(e.updatedSeries,(function(t){if(t.isUniversalTransitionEnabled()&&t.isAnimationEnabled()){var e=t.get("dataGroupId"),o=t.getData(),a=pW(t),s=dW(a),l=i.get(s);if(l)n.set(s,{oldSeries:[{dataGroupId:l.dataGroupId,divide:fW(l.data),data:l.data}],newSeries:[{dataGroupId:e,divide:fW(o),data:o}]});else if(Y(a)){var u=[];E(a,(function(t){var e=i.get(t);e.data&&u.push({dataGroupId:e.dataGroupId,divide:fW(e.data),data:e.data})})),u.length&&n.set(s,{oldSeries:u,newSeries:[{dataGroupId:e,data:o,divide:fW(o)}]})}else{var h=r.get(a);if(h){var c=n.get(h.key);c||(c={oldSeries:[{dataGroupId:h.dataGroupId,data:h.data,divide:fW(h.data)}],newSeries:[]},n.set(h.key,c)),c.newSeries.push({dataGroupId:e,data:o,divide:fW(o)})}}}})),n}(i,n);E(o.keys(),(function(t){var n=o.get(t);cW(n.oldSeries,n.newSeries,e)}))}E(n.updatedSeries,(function(t){t[vg]&&(t[vg]=!1)}))}for(var a=t.getSeries(),s=i.oldSeries=[],l=i.oldDataGroupIds=[],u=i.oldData=[],h=0;h GitHub仓库Star数量对比
    正在加载数据...

    正在加载数据...

    ================================================ FILE: sa-token-doc/static/swiper/index-swiper.css ================================================ @charset "utf-8"; /* CSS Document */ /* ry盒子 总区域 */ .ry-kuai{ padding-left: 0; padding-right: 0; } /* ry盒子 灰色区域 */ .ry-box{ padding-top: 70px; padding-bottom: 170px; background-color: #eee; position: relative; overflow: hidden; } /* 轮播图容器 */ .ry-box .swiper { width: 100%; height: 100%; } .ry-box .swiper-slide { text-align: center; font-size: 18px; width: 750px; height: 500px; /* cursor: pointer; */ } .ry-box .swiper-slide-tx1{ width: 450px; } .ry-box .swiper-slide img { height: 100%; box-shadow: 0 0 20px #ccc; transition: box-shadow 0.2s; } .ry-box .swiper-slide img:hover{ box-shadow: 0 0 40px #999; } .ry-box .swiper-slide p{ display: inline-block; font-size: 16px; margin-top: 30px; color: #222; } /* 分页器样式 */ .ry-box .swiper-pagination{bottom: -140px;} .ry-box .swiper-pagination .swiper-pagination-bullet{width: 18px; height: 18px; line-height: 18px; color: #FFF; font-size: 12px;} /* 图片放大动画 */ .ry-box .swiper-slide img{ transition: 300ms; transform: scale(0.8); } .ry-box .swiper-slide-active img, .ry-box .swiper-slide-duplicate-active img{ transform: scale(1); } /* 阴影 */ /* .ry-img-yinying{ width: 50%; height: 10px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.8); box-shadow: 0 0 50px #333; margin: auto; } */ ================================================ FILE: sa-token-doc/static/swiper/index-swiper.js ================================================ function initSwiper () { if(window.swiper){ return; } window.swiper = new Swiper(".mySwiper", { // 最大容纳的slide数量,auto=自动 slidesPerView: "auto", // 主角 slide 居中 centeredSlides: true, // 使左右 slide 贴合容器 // centeredSlidesBounds: true, // 循环 loop: true, // 自动播放 autoplay: { // 3秒切换一次 delay: 3000, }, // slide 间距 spaceBetween: 80, // 点击 slide 时,过渡到这个 slide slideToClickedSlide: true, // 切换效果 slide=普通位移、fade=淡入、cube=方块、coverflow=3d流、flip=3d翻转、cards=卡片式、creative=创意性 effect: 'coverflow', // 抓取时,鼠标变小手 grabCursor: true, // 分页器 pagination: { el: ".swiper-pagination", // 点击时切换 slide clickable: true, // 分页器样式,bullets=原点,fraction=分式,progressbar=进度条,custom=自定义 type: "bullets", // 点击小点,切换 slide clickable :true, // 将按钮从小点变成数字 renderBullet: function (index, className) { return '' + (index + 1) + ''; }, }, // 左右切换按钮 navigation: { nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev", }, }); } $(function(){ initSwiper(); }) // 滚动到 swiper 时,再加载 // $(document).scroll(function(){ // // 页面滚动条高度 > ry盒子到顶部距离 + window 视口高度 时,swiper出现 // if($(document).scrollTop() > $('.ry-kuai').offset().top - $(window).height()) { // initSwiper(); // } // }) ================================================ FILE: sa-token-doc/static/vue.css ================================================ @import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;z-index:1}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#42b983);box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:2px;color:#e96900;font-size:.8rem;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section code,.markdown-section pre{background-color:#f8f8f8;font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#f8f8f8;border-radius:2px;color:#525252;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0} ================================================ FILE: sa-token-doc/static/water-change-theme/water-change-theme.css ================================================ /* 水滴样式 */ .water-drop { position: fixed; /* 改为fixed避免触发滚动条 */ width: 20px; height: 28px; border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; transform: rotate(45deg); z-index: 1550; border: 2px solid rgba(0, 0, 0, 0.2); /* 添加轮廓 */ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); /* 添加阴影增强视觉效果 */ } /* 圆形扩散效果 div */ .color-wave { position: fixed; border-radius: 50%; transform: scale(0); z-index: -1; /* 降低z-index确保不遮挡内容 */ pointer-events: none; } /* 将页面主盒子设置为定位,这样就可以让水滴扩散的div 设置 z-index: 保持不覆盖 main-box 里的内容了 */ .main-box{ position: relative; /* z-index: 1; */ } ================================================ FILE: sa-token-doc/static/water-change-theme/water-change-theme.js ================================================ // 绑定修改背景色的按钮事件 $('.theme-box span').click(function() { // 获取主题色 let bgColor = this.style.backgroundColor; // 获取点击位置 const rect = this.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; // 创建水滴元素 createWaterDrop(x - 7, y + 5, bgColor); // setBg(bgColor); localStorage.setItem('bg-color-value', bgColor) }) // 创建水滴动画 function createWaterDrop(x, y, color) { // 创建水滴元素 const waterDrop = document.createElement('div'); waterDrop.className = 'water-drop'; waterDrop.style.backgroundColor = color; waterDrop.style.left = `${x}px`; waterDrop.style.top = `${y}px`; // 添加到文档中 document.body.appendChild(waterDrop); // 获取视口高度 const viewportHeight = window.innerHeight; // 使用GSAP创建水滴下落动画 gsap.to(waterDrop, { top: viewportHeight - 30, // 调整为视口底部内,避免触发滚动条 duration: 1.5, ease: "power2.in", // 加速度下落 onComplete: function() { // 动画完成后移除水滴 document.body.removeChild(waterDrop); // 创建颜色扩散效果 createColorWave(x, viewportHeight, color); } }); } // 创建颜色扩散效果 function createColorWave(x, y, color) { // 创建颜色波元素 const colorWave = document.createElement('div'); colorWave.className = 'color-wave'; colorWave.style.backgroundColor = color; // 计算所需的最小半径(确保能覆盖整个视口) const maxDistance = Math.sqrt( Math.pow(Math.max(x, window.innerWidth - x), 2) + Math.pow(Math.max(y, window.innerHeight - y), 2) ); // 设置颜色波的初始位置和大小 colorWave.style.width = `${maxDistance * 2}px`; colorWave.style.height = `${maxDistance * 2}px`; colorWave.style.left = `${x - maxDistance}px`; colorWave.style.top = `${y - maxDistance}px`; // 确保 colorWave 在所有内容之下 // const contentElements = document.querySelectorAll('nav, main, footer'); // contentElements.forEach(el => { // if (!el.style.zIndex || parseInt(el.style.zIndex) <= 10) { // el.style.zIndex = '20'; // } // }); // 添加到文档中 document.body.appendChild(colorWave); // 使用 GSAP 创建扩散动画 gsap.to(colorWave, { scale: 1, duration: 1.2, ease: "power2.out", onComplete: function() { // 动画完成后更改背景色 // document.body.style.backgroundColor = color; setBg(color) // 延迟移除颜色波 setTimeout(() => { document.body.removeChild(colorWave); }, 500); } }); } // 读取上次记录 let bgColor = localStorage.getItem('bg-color-value'); if (bgColor) { setBg(bgColor); } // 设置背景颜色 function setBg(bgColor) { console.log('---- 背景颜色设定为:', bgColor); // -------- 设置 body 背景 document.body.style.backgroundColor = bgColor; // -------- 设置 header 头背景 // 如果是 16 进制,转 rgba if (bgColor.indexOf('#') == 0) { bgColor = hexToRgba(bgColor, 0.97); } // 如果是 rgb,转 rgba else if (bgColor.match(/\,/g).length == 2) { bgColor = bgColor.replace(')', ' ,0.97)'); } document.querySelector('.doc-header').style.backgroundColor = bgColor; } // 16进制 转 rgba function hexToRgba(str, a) { a = a || 1; var reg = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/ if (!reg.test(str)) { return; } let newStr = (str.toLowerCase()).replace(/\#/g, '') let len = newStr.length; if (len == 3) { let t = '' for (var i = 0; i < len; i++) { t += newStr.slice(i, i + 1).concat(newStr.slice(i, i + 1)) } newStr = t } let arr = []; //将字符串分隔,两个两个的分隔 for (var i = 0; i < 6; i = i + 2) { let s = newStr.slice(i, i + 2) arr.push(parseInt("0x" + s)) } return 'rgb(' + arr.join(",") + ', ' + a + ')'; } ================================================ FILE: sa-token-doc/up/basic-auth.md ================================================ # Http Basic 认证 Http Basic 是 http 协议中最基础的认证方式,其有两个特点: - 简单、易集成。 - 功能支持度低。 在 Sa-Token 中使用 Http Basic 认证非常简单,只需调用几个简单的方法 --- ### 1、启用 Http Basic 认证 首先我们在一个接口中,调用 Http Basic 校验: ``` java @RequestMapping("test3") public SaResult test3() { SaHttpBasicUtil.check("sa:123456"); // ... 其它代码 return SaResult.ok(); } ``` 全局异常处理: ``` java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ``` 然后我们访问这个接口时,浏览器会强制弹出一个表单: sa-basic.png 当我们输入账号密码后 `(sa / 123456)`,才可以继续访问数据: sa-basic-ok.png ### 2、其它启用方式 ``` java // 对当前会话进行 Http Basic 校验,账号密码为 yml 配置的值(例如:sa-token.http-basic=sa:123456) SaHttpBasicUtil.check(); // 对当前会话进行 Http Basic 校验,账号密码为:`sa / 123456` SaHttpBasicUtil.check("sa:123456"); // 以注解方式启用 Http Basic 校验 @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("test3") public SaResult test3() { return SaResult.ok(); } // 在全局拦截器 或 过滤器中启用 Basic 认证 @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() .addInclude("/**").addExclude("/favicon.ico") .setAuth(obj -> { SaRouter.match("/test/**", () -> SaHttpBasicUtil.check("sa:123456")); }); } ``` ### 3、URL 认证 除了访问后再输入账号密码外,我们还可以在 URL 中直接拼接账号密码通过 Basic 认证,例如: ``` url http://sa:123456@127.0.0.1:8081/test/test3 ``` ### 4、Http Digest 认证 Http Digest 认证是 Http Basic 认证的升级版,Http Digest 在提交请求时不会使用明文方式传输认证信息,而是使用一定的规则加密后提交。 不过对于开发者来讲,开启 Http Digest 认证校验的流程与 Http Basic 认证基本是一致的。 ``` java // 测试 Http Digest 认证 浏览器访问: http://localhost:8081/test/testDigest @RequestMapping("testDigest") public SaResult testDigest() { SaHttpDigestUtil.check("sa", "123456"); return SaResult.ok(); } // 使用注解方式开启 Http Digest 认证 @SaCheckHttpDigest("sa:123456") @RequestMapping("testDigest2") public SaResult testDigest() { return SaResult.ok(); } // 对当前会话进行 Http Digest 校验,账号密码为 yml 配置的值(例如:sa-token.http-digest=sa:123456) SaHttpDigestUtil.check(); ``` 与上面的 Http Basic 认证一致,在访问这个路由时,浏览器会强制弹出一个表单,客户端输入正确的账号密码后即可通过校验。 同样的,Http Digest 也支持在浏览器访问接口时直接使用 @ 符号拼接账号密码信息,使客户端直接通过校验。 ``` url http://sa:123456@127.0.0.1:8081/test/testDigest ``` --- 本章代码示例:Sa-Token Http Basic 认证 —— [ HttpBasicController.java ] ================================================ FILE: sa-token-doc/up/disable.md ================================================ # 账号封禁 之前的章节中,我们学习了 踢人下线 和 强制注销 功能,用于清退违规账号。 在部分场景下,我们还需要将其 **账号封禁**,以防止其再次登录。 --- ### 1、账号封禁 对指定账号进行封禁: ``` java // 封禁指定账号 StpUtil.disable(10001, 86400); ``` 参数含义: - 参数1:要封禁的账号id。 - 参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。 注意点:对于正在登录的账号,将其封禁并不会使它立即掉线,如果我们需要它即刻下线,可采用先踢再封禁的策略,例如:
    ``` java // 先踢下线 StpUtil.kickout(10001); // 再封禁账号 StpUtil.disable(10001, 86400); ``` 待到下次登录时,我们先校验一下这个账号是否已被封禁: ``` java // 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` StpUtil.checkDisable(10001); // 通过校验后,再进行登录: StpUtil.login(10001); ``` > [!ATTENTION| label:升级注意:] > 旧版本在 `StpUtil.login()` 时会自动校验账号是否被封禁,v1.31.0 之后将 校验封禁 和 登录 两个动作分离成两个方法,不再自动校验,请注意其中的逻辑更改。 此模块所有方法: ``` java // 封禁指定账号 StpUtil.disable(10001, 86400); // 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) StpUtil.isDisable(10001); // 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` StpUtil.checkDisable(10001); // 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2 StpUtil.getDisableTime(10001); // 解除封禁 StpUtil.untieDisable(10001); ``` ### 2、分类封禁 有的时候,我们并不需要将整个账号禁掉,而是只禁止其访问部分服务。 假设我们在开发一个电商系统,对于违规账号的处罚,我们设定三种分类封禁: - 1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。 - 2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。 - 3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。 相比于封禁账号的一刀切处罚,这里的关键点在于:每一项能力封禁的同时,都不会对其它能力造成影响。 也就是说我们需要一种只对部分服务进行限制的能力,对应到代码层面,就是只禁止部分接口的调用。 ``` java // 封禁指定用户评论能力,期限为 1天 StpUtil.disable(10001, "comment", 86400); ``` 参数释义: - 参数1:要封禁的账号id。 - 参数2:针对这个账号,要封禁的服务标识(可以是任意的自定义字符串)。 - 参数3:要封禁的时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。 分类封禁模块所有可用API: ``` java /* * 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识 */ // 封禁指定用户评论能力,期限为 1天 StpUtil.disable(10001, "comment", 86400); // 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment` StpUtil.checkDisable(10001, "comment"); // 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能 StpUtil.checkDisable(10001, "place-order"); // 现在我们再将其下单能力封禁一下,期限为 7天 StpUtil.disable(10001, "place-order", 86400 * 7); // 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常) StpUtil.checkDisable(10001, "place-order"); // 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码) StpUtil.checkDisable(10001, "open-shop"); ``` 通过以上示例,你应该大致可以理解 `业务封禁 -> 业务校验` 的处理步骤。 有关分类封禁的所有方法: ``` java // 封禁:指定账号的指定服务 StpUtil.disable(10001, "<业务标识>", 86400); // 判断:指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) StpUtil.isDisable(10001, "<业务标识>"); // 校验:指定账号的指定服务 是否已被封禁,如果被封禁则抛出异常 `DisableServiceException` StpUtil.checkDisable(10001, "<业务标识>"); // 获取:指定账号的指定服务 剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁) StpUtil.getDisableTime(10001, "<业务标识>"); // 解封:指定账号的指定服务 StpUtil.untieDisable(10001, "<业务标识>"); ``` ### 3、阶梯封禁 对于多次违规的用户,我们常常采取阶梯处罚的策略,这种 “阶梯” 一般有两种形式: - 处罚时间阶梯:首次违规封禁 1 天,第二次封禁 7 天,第三次封禁 30 天,依次顺延…… - 处罚力度阶梯:首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录,等等…… 基于处罚时间的阶梯,我们只需在封禁时 `StpUtil.disable(10001, 86400)` 传入不同的封禁时间即可,下面我们着重探讨一下基于处罚力度的阶梯形式。 假设我们在开发一个论坛系统,对于违规账号的处罚,我们设定三种力度: - 1、轻度违规:封禁其发帖、评论能力,但允许其点赞、关注等操作。 - 2、中度违规:封禁其发帖、评论、点赞、关注等一切与别人互动的能力,但允许其浏览帖子、浏览评论。 - 3、重度违规:封禁其登录功能,限制一切能力。 解决这种需求的关键在于,我们需要把不同处罚力度,量化成不同的处罚等级,比如上述的 `轻度`、`中度`、`重度` 3 个力度, 我们将其量化为`一级封禁`、`二级封禁`、`三级封禁` 3个等级,数字越大代表封禁力度越高。 然后我们就可以使用阶梯封禁的API,进行鉴权了: ``` java // 阶梯封禁,参数:封禁账号、封禁级别、封禁时间 StpUtil.disableLevel(10001, 3, 10000); // 获取:指定账号封禁的级别 (如果此账号未被封禁则返回 -2) StpUtil.getDisableLevel(10001); // 判断:指定账号是否已被封禁到指定级别,返回 true 或 false StpUtil.isDisableLevel(10001, 3); // 校验:指定账号是否已被封禁到指定级别,如果已达到此级别(例如已被3级封禁,这里校验是否达到2级),则抛出异常 `DisableServiceException` StpUtil.checkDisableLevel(10001, 2); ``` 注意点:`DisableServiceException` 异常代表当前账号未通过封禁校验,可以: - 通过 `e.getLevel()` 获取这个账号实际被封禁的等级。 - 通过 `e.getLimitLevel()` 获取这个账号在校验时要求低于的等级。当 `Level >= LimitLevel` 时,框架就会抛出异常。 如果业务足够复杂,我们还可能将 分类封禁 和 阶梯封禁 组合使用: ``` java // 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间 StpUtil.disableLevel(10001, "comment", 3, 10000); // 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2) StpUtil.getDisableLevel(10001, "comment"); // 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false StpUtil.isDisableLevel(10001, "comment", 3); // 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常 StpUtil.checkDisableLevel(10001, "comment", 2); ``` ### 4、使用注解完成封禁校验 首先我们需要注册 Sa-Token 全局拦截器(可参考 [注解鉴权](/use/at-check) 章节),然后我们就可以使用以下注解校验账号是否封禁 ``` java // 校验当前账号是否被封禁,如果已被封禁会抛出异常,无法进入方法 @SaCheckDisable @PostMapping("send") public SaResult send() { // ... return SaResult.ok(); } // 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 @SaCheckDisable("comment") @PostMapping("send") public SaResult send() { // ... return SaResult.ok(); } // 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值,只要有一个已被封禁,就无法进入方法 @SaCheckDisable({"comment", "place-order", "open-shop"}) @PostMapping("send") public SaResult send() { // ... return SaResult.ok(); } // 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常 @SaCheckDisable(level = 5) @PostMapping("send") public SaResult send() { // ... return SaResult.ok(); } // 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级,如果达到则抛出异常 @SaCheckDisable(value = "comment", level = 5) @PostMapping("send") public SaResult send() { // ... return SaResult.ok(); } ``` ### 5、封禁信息持久化 Sa-Token 默认将封禁信息储存在缓存中,缓存中的数据是“临时性的”、“易丢失的”,而在大多数系统的设计中,需要将封禁数据持久化到数据库中。 要使封禁信息持久化,你只需要在调用 Sa-Token 的封禁 API 后,再继续调用插入数据库的代码即可,形如: ``` java // 在 Sa-Token 框架中封禁指定账号 StpUtil.disable(10001, 86400); // 更改数据库中此人信息 (举例代码) userMapper.disableUser(10001); ``` 这样即可保证封禁数据同步插入到缓存和数据库中,但是还有一个问题,如果我们的程序或缓存中间件重启了,导致缓存数据丢失, 那再调用 `StpUtil.checkDisable(10001)` 代码将没有效果,无法约束到此用户。 比较次的解决方案是在程序启动时,读取数据库中所有封禁信息同步到缓存中去,但是如果封禁记录较多这样将会严重拖慢程序启动时间。 Sa-Token 提供一种方案,可以在你调用 `StpUtil.checkDisable(10001)` 校验封禁时才会触发查询数据库 10001 账号到底有没有被封禁。 你只需要实现 `StpInterface` 的 `isDisabled` 方法即可,例: ``` java @Component public class StpInterfaceImpl implements StpInterface { /** * 返回指定账号 id 是否被封禁 * * @param loginId 账号id * @param service 业务标识符 * @return 描述该账号是否封禁的包装信息对象 */ public SaDisableWrapperInfo isDisabled(Object loginId, String service) { // 查库操作 ... (此处仅做示例代码) return SaDisableWrapperInfo.createDisabled(86400, 1); } } ``` 该方法返回一个 `SaDisableWrapperInfo` 实例对象,用来描述指定账号是否已被封禁,一般有以下几种写法: ``` java // 标准写法:new 对象返回,参数为:是否被封禁、封禁时间(秒)、封禁等级 public SaDisableWrapperInfo isDisabled(Object loginId, String service) { return new SaDisableWrapperInfo(true, 86400, 1); } // 快捷写法:被封禁,解封倒计时86400秒,封禁等级1 public SaDisableWrapperInfo isDisabled(Object loginId, String service) { return SaDisableWrapperInfo.createDisabled(86400, 1); } // 快捷写法:未被封禁 public SaDisableWrapperInfo isDisabled(Object loginId, String service) { return SaDisableWrapperInfo.createNotDisabled(); } // 快捷写法:未被封禁,且将查询结果保存到缓存中,ttl为86400,改时间内不再重复进入 isDisabled 方法 public SaDisableWrapperInfo isDisabled(Object loginId, String service) { return SaDisableWrapperInfo.createNotDisabled(86400); } ``` --- 本章代码示例:Sa-Token 账号禁用 —— [ DisableController.java ] ================================================ FILE: sa-token-doc/up/global-filter.md ================================================ # 全局过滤器 --- ### 组件简述 之前的章节中,我们学习了“根据拦截器实现路由拦截鉴权”,其实在大多数web框架中,使用过滤器可以实现同样的功能,本章我们就利用Sa-Token全局过滤器来实现路由拦截器鉴权。 首先我们先梳理清楚一个问题,既然拦截器已经可以实现路由鉴权,为什么还要用过滤器再实现一遍呢?简而言之: 1. 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。 2. 过滤器可以拦截静态资源,方便我们做一些权限控制。 3. 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制。 但是过滤器也有一些缺点,比如: 1. 由于太过底层,导致无法率先拿到`HandlerMethod`对象,无法据此添加一些额外功能。 2. 由于拦截的太全面了,导致我们需要对很多特殊路由(如`/favicon.ico`)做一些额外处理。 3. 在Spring中,过滤器中抛出的异常无法进入全局`@ExceptionHandler`,我们必须额外编写代码进行异常处理。 Sa-Token同时提供过滤器和拦截器机制,不是为了让谁替代谁,而是为了让大家根据自己的实际业务合理选择,拥有更多的发挥空间。 ### 在 SpringBoot 中注册过滤器 同拦截器一样,为了避免不必要的性能浪费,Sa-Token全局过滤器默认处于关闭状态,若要使用过滤器组件,首先你需要注册它到项目中: ``` java /** * [Sa-Token 权限认证] 配置类 */ @Configuration public class SaTokenConfigure { /** * 注册 [Sa-Token全局过滤器] */ @Bean public SaServletFilter getSaServletFilter() { return new SaServletFilter() // 指定 拦截路由 与 放行路由 .addInclude("/**").addExclude("/favicon.ico") /* 排除掉 /favicon.ico */ // 认证函数: 每次请求执行 .setAuth(obj -> { System.out.println("---------- 进入Sa-Token全局认证 -----------"); // 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin()); // 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */ }) // 异常处理函数:每次认证函数发生异常时执行此函数 .setError(e -> { System.out.println("---------- 进入Sa-Token异常处理 -----------"); return SaResult.error(e.getMessage()); }) // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入) .setBeforeAuth(r -> { // ---------- 设置一些安全响应头 ---------- SaHolder.getResponse() // 服务器名称 .setServer("sa-server") // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 .setHeader("X-Frame-Options", "SAMEORIGIN") // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 .setHeader("X-XSS-Protection", "1; mode=block") // 禁用浏览器内容嗅探 .setHeader("X-Content-Type-Options", "nosniff") ; }) ; } } ``` > [!WARNING| label:注意事项:] > - 在`[认证函数]`里,你可以写和拦截器里一致的代码,进行路由匹配鉴权,参考:[路由拦截鉴权](/use/route-check)。 > - 由于过滤器中抛出的异常不进入全局异常处理,所以你必须提供`[异常处理函数]`来处理`[认证函数]`里抛出的异常。 > - 在`[异常处理函数]`里的返回值,将作为字符串输出到前端,如果需要定制化返回数据,请注意其中的格式转换。 改写 `setError` 函数的响应格式示例: ``` java .setError(e -> { // 设置响应头 SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); // 使用封装的 JSON 工具类转换数据格式 return JSONUtil.toJsonStr( SaResult.error(e.getMessage()) ); }) ``` JSON 工具类可参考:[Hutool-Json](https://hutool.cn/docs/#/json/JSONUtil) ### 自定义过滤器执行顺序 SaServletFilter 默认执行顺序为 `-100`,如果你要自定义过滤器的执行顺序,可以使用 `FilterRegistrationBean` 注册,参考: ``` java /** * 注册 [Sa-Token 全局过滤器] */ @Bean public FilterRegistrationBean getSaServletFilter() { FilterRegistrationBean frBean = new FilterRegistrationBean<>(); frBean.setFilter( new SaServletFilter() .addInclude("/**") .setAuth(obj -> { // .... }) // 等等,其它代码 ... ); frBean.setOrder(-101); // 更改顺序为 -101 return frBean; } ``` 在 SpringBoot 中, Order 值越小,执行时机越靠前。 ### 在 WebFlux 中注册过滤器 `Spring WebFlux`中不提供拦截器机制,因此若你的项目需要路由鉴权功能,过滤器是你唯一的选择,在`Spring WebFlux`注册过滤器的流程与上述流程几乎完全一致, 除了您需要将过滤器名称由`SaServletFilter`更换为`SaReactorFilter`以外,其它所有步骤均可参考以上示例。 ``` java /** * [Sa-Token 权限认证] 配置类 */ @Configuration public class SaTokenConfigure { /** * 注册 [Sa-Token全局过滤器] */ @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 其它代码... ; } } ``` --- 本章代码示例:Sa-Token 全局过滤器 —— [ SaTokenConfigure.java ] ================================================ FILE: sa-token-doc/up/global-listener.md ================================================ # 全局侦听器 --- ### 1、工作原理 Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。 事件触发流程大致如下: sa-token-listener 框架默认内置了侦听器 `SaTokenListenerForLog` 实现:[代码参考](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java) ,功能是控制台 log 打印输出,你可以通过配置`sa-token.is-log=true`开启。 要注册自定义的侦听器也非常简单: 1. 新建类实现 `SaTokenListener` 接口。 2. 将实现类注册到 `SaTokenEventCenter` 事件发布中心。 ### 2、自定义侦听器实现 ##### 2.1、新建实现类: 新建`MySaTokenListener.java`,实现`SaTokenListener`接口,并添加上注解`@Component`,保证此类被`SpringBoot`扫描到: ``` java /** * 自定义侦听器的实现 */ @Component public class MySaTokenListener implements SaTokenListener { /** 每次登录时触发 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { System.out.println("---------- 自定义侦听器实现 doLogin"); } /** 每次注销时触发 */ @Override public void doLogout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doLogout"); } /** 每次被踢下线时触发 */ @Override public void doKickout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doKickout"); } /** 每次被顶下线时触发 */ @Override public void doReplaced(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doReplaced"); } /** 每次被封禁时触发 */ @Override public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { System.out.println("---------- 自定义侦听器实现 doDisable"); } /** 每次被解封时触发 */ @Override public void doUntieDisable(String loginType, Object loginId, String service) { System.out.println("---------- 自定义侦听器实现 doUntieDisable"); } /** 每次二级认证时触发 */ @Override public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { System.out.println("---------- 自定义侦听器实现 doOpenSafe"); } /** 每次退出二级认证时触发 */ @Override public void doCloseSafe(String loginType, String tokenValue, String service) { System.out.println("---------- 自定义侦听器实现 doCloseSafe"); } /** 每次创建Session时触发 */ @Override public void doCreateSession(String id) { System.out.println("---------- 自定义侦听器实现 doCreateSession"); } /** 每次注销Session时触发 */ @Override public void doLogoutSession(String id) { System.out.println("---------- 自定义侦听器实现 doLogoutSession"); } /** 每次Token续期时触发 */ @Override public void doRenewTimeout(String tokenValue, Object loginId, long timeout) { System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); } /** 每次Token续期时触发 */ @Override public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); } } ``` ##### 2.2、将侦听器注册到事件中心: 以上代码由于添加了 `@Component` 注解,会被 SpringBoot 扫描并自动注册到事件中心,此时我们无需手动注册。 如果我们没有添加 `@Component` 注解或者项目属于非 IOC 自动注入环境,则需要我们手动将这个侦听器注册到事件中心: ``` java // 将侦听器注册到事件发布中心 SaTokenEventCenter.registerListener(new MySaTokenListener()); ``` 事件中心的其它一些常用方法: ``` java // 获取已注册的所有侦听器 SaTokenEventCenter.getListenerList(); // 重置侦听器集合 SaTokenEventCenter.setListenerList(listenerList); // 注册一个侦听器 SaTokenEventCenter.registerListener(listener); // 注册一组侦听器 SaTokenEventCenter.registerListenerList(listenerList); // 移除一个侦听器 SaTokenEventCenter.removeListener(listener); // 移除指定类型的所有侦听器 SaTokenEventCenter.removeListener(cls); // 清空所有已注册的侦听器 SaTokenEventCenter.clearListener(); // 判断是否已经注册了指定侦听器 SaTokenEventCenter.hasListener(listener); // 判断是否已经注册了指定类型的侦听器 SaTokenEventCenter.hasListener(cls); ``` ##### 2.3、启动测试: 在 `TestController` 中添加登录测试代码: ``` java // 测试登录接口 @RequestMapping("login") public SaResult login() { System.out.println("登录前"); StpUtil.login(10001); System.out.println("登录后"); return SaResult.ok(); } ``` 启动项目,访问登录接口,观察控制台输出: sa-token-listener-println ### 3、其它注意点 ##### 3.1、你可以通过继承 `SaTokenListenerForSimple` 快速实现一个侦听器: ``` java @Component public class MySaTokenListener extends SaTokenListenerForSimple { /* * SaTokenListenerForSimple 对所有事件提供了空实现,通过继承此类,你只需重写一部分方法即可实现一个可用的侦听器。 */ /** 每次登录时触发 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { System.out.println("---------- 自定义侦听器实现 doLogin"); } } ``` ##### 3.2、使用匿名内部类的方式注册: ``` java // 登录时触发 SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { System.out.println("---------------- doLogin"); } }); ``` ##### 3.3、使用 try-catch 包裹不安全的代码: 如果你认为你的事件处理代码是不安全的(代码可能在运行时抛出异常),则需要使用 `try-catch` 包裹代码,以防因为抛出异常导致 Sa-Token 的整个登录流程被强制中断。 ``` java // 登录时触发 SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { try { // 不安全代码需要写在 try-catch 里 // ...... } catch (Exception e) { e.printStackTrace(); } } }); ``` ##### 3.4、疑问:一个项目可以注册多个侦听器吗? 可以,多个侦听器间彼此独立,互不影响,按照注册顺序依次接受到事件通知。 --- 本章代码示例:Sa-Token 自定义侦听器 —— [ MySaTokenListener.java ] ================================================ FILE: sa-token-doc/up/integ-redis.md ================================================ # Sa-Token 集成 Redis --- Sa-Token 默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如: 1. 重启后数据会丢失。 2. 无法在分布式环境中共享数据。 为此,Sa-Token 提供了扩展接口,你可以轻松将会话数据存储在一些专业的缓存中间件上(比如 Redis), 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。 --- ### 1、Sa-Token 整合 RedisTemplate RedisTemplate 是 SpringBoot 官方推荐的 Redis 客户端,Sa-Token 提供基于 RedisTemplate 的 Redis 整合方案: ``` xml cn.dev33 sa-token-redis-template ${sa.top.version} org.apache.commons commons-pool2 ``` ``` gradle // Sa-Token 整合 RedisTemplate implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}' // 提供 Redis 连接池 implementation 'org.apache.commons:commons-pool2' ``` Redis 的集成有多种方式,缓存的方案也不止 Redis 一种,Sa-Token 为缓存方案提供多种扩展实现。 如果你对 Sa-Token 还不太熟悉,或者只想“省心省事”,我们推荐你直接使用上述的 RedisTemplate 集成方案,而不必进行过多研究。到此为止,你可以跳转到下一章节了。 如果你想对缓存方案再进行一下深入探究,那么你可以参考:[缓存层扩展](/plugin/dao-extend) ### 2、自定义序列化方案 如果你按照上述 RedisTemplate 方案进行集成测试,会发现框架在 Redis 中是以 json 格式存储数据的。可以自定义数据序列化格式吗?当然是可以的。 框架的默认序列化层调用为 `String 序列化` -> `JSON 序列化`。要自定义数据序列化方式你可以从这两方面入手: #### 2.1、自定义 JSON 序列化方案: 先说较为底层的 `JSON 序列化`,如果你引入的是 sa-token-spring-boot-starter 集成包 (含SpringBoot3) ,那么框架将会自动引入 Jackson 框架作为 JSON 序列化方案。 如果你想更换为其它 JSON 解析框架,可以引入相关依赖: ``` xml cn.dev33 sa-token-fastjson ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'` ``` xml cn.dev33 sa-token-fastjson2 ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'` ``` xml cn.dev33 sa-token-snack3 ${sa.top.version} ``` Gradle 参考:`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'` 完整插件列表请参考:[JSON 序列化扩展](/plugin/json-extend) #### 2.2、自定义 String 序列化方案: 或者你想更直接点,不使用 json 序列化方案,也是可以的。你可以直接自定义数据的 String 序列化方案: ``` java // 设置序列化方案: jdk序列化 (base64编码) @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseBase64()); } ``` ``` java // 设置序列化方案: jdk序列化 (16进制编码) @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseHex()); } ``` ``` java // 设置序列化方案: jdk序列化 (ISO-8859-1编码) @PostConstruct public void rewriteComponent() { SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseISO_8859_1()); } ``` 除了以上的几种序列化方案,我们还提供了序列化扩展包,详细可参考:[序列化插件扩展包](/plugin/custom-serializer) ### 3、集成 Redis 请注意: **1. 引入了依赖,我还需要为 Redis 配置连接信息吗?**
    需要!只有项目初始化了正确的 Redis 实例,`Sa-Token`才可以使用 Redis 进行数据持久化,参考以下`yml配置`: ``` yaml spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ``` ``` properties # Redis数据库索引(默认为0) spring.redis.database=1 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) # spring.redis.password= # 连接超时时间 spring.redis.timeout=10s # 连接池最大连接数 spring.redis.lettuce.pool.max-active=200 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.lettuce.pool.max-wait=-1ms # 连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=10 # 连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0 ``` > [!WARNING| label:小提示 ] > 如果你使用的是 SpringBoot3.x 版本,则需要将前缀 `spring.redis` 改为 `spring.data.redis`。 **2. 集成 Redis 后,是我额外手动保存数据,还是框架自动保存?**
    框架自动保存。集成 `Redis` 只需要引入对应的 `pom依赖` 即可,框架所有上层 API 保持不变。 **3. 集成包版本问题**
    Sa-Token-Redis 集成包的版本尽量与 Sa-Token-Starter 集成包的版本一致,否则可能出现兼容性问题。 ### 4、扩展:集成 MongoDB - [集成 MongoDB 参考一](/up/integ-spring-mongod-1) - [集成 MongoDB 参考二](/up/integ-spring-mongod-2) ================================================ FILE: sa-token-doc/up/integ-spring-mongod-1.md ================================================ # Sa-Token 集成 MongoDB --- 此章介绍如何通过扩展 `SaTokenDao` 接口来实现 MongodDB 的集成。 [示例代码:sa-token-mongodb-demo](https://gitee.com/lilihao/sa-token-mongodb-demo) 先决条件: 1. Spring Boot 3 2. Spring Data Mongodb 以下是依赖的引入: --- ``` xml org.springframework.boot spring-boot-starter-data-mongodb ``` ``` gradle // 引入 spring data mongodb implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' ``` 优点:少量改造即可完成集成 MongodDB ### 集成代码: **1. 创建一个类来包装`Sa—Token`的数据** ```java @Document("saTokenMongo") // 你也可以自定义集合名称 public class SaTokenMongoData { @Id private String id; // token @Indexed(unique = true) private String key; // sa-token 的 session private SaSession session; // sa-token 的 token string private String string; //使用 @SuppressWarnings("removal") 的目的是,防止IDEA报错,因为 expireAfterSeconds是不在支持的属性。 @SuppressWarnings("removal") // 给 expireAt 添加 `@Indexed(expireAfterSeconds = 0)` 注解,当过期时MongoDB会自动帮我删除过期的数据 @Indexed(expireAfterSeconds = 0) private LocalDateTime expireAt; // 你也可以使用 Date 类型,对应的在`SaTokenMongoDao`中,需要将LocalDateTime替换成Date // 忽略 getter setter } ``` **2.实现 SaTokenDao** 这个 SaTokenMongoDao 是仿照官方的 redis 集成实现的 ```java package com.xx.xx.security; import cn.dev33.satoken.dao.SaTokenDao; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; @Component public class SaTokenMongoDao implements SaTokenDao { private final MongoTemplate mongoTemplate; public SaTokenMongoDao(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } private Query keyQuery(String key) { return Query.query(Criteria.where("key").is(key)); } /** * 获取 value,如无返空 * * @param key 键名称 * @return value */ @Override public String get(String key) { return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getString).orElse(null); } LocalDateTime getExpireAtFromTimeout(long timeout) { // 当接受到的值是`SaTokenDao.NEVER_EXPIRE`时,说明永不过期,对应的我们需要把 expireAt 设置为null mongodb就不会删除这个记录 return timeout == SaTokenDao.NEVER_EXPIRE ? null : LocalDateTime.now().plusSeconds(timeout); } /** * 写入 value,并设定存活时间(单位: 秒) * * @param key 键名称 * @param value 值 * @param timeout 数据有效期(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ @Override public void set(String key, String value, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 mongoTemplate.upsert( keyQuery(key), Update.update("string", value).set("expireAt", getExpireAtFromTimeout(timeout)), SaTokenMongoData.class ); } /** * 更新 value (过期时间不变) * * @param key 键名称 * @param value 值 */ @Override public void update(String key, String value) { long expire = getTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.set(key, value, expire); } /** * 删除 value * * @param key 键名称 */ @Override public void delete(String key) { mongoTemplate.remove(keyQuery(key), SaTokenMongoData.class); } /** * 获取 value 的剩余存活时间(单位: 秒) * * @param key 指定 key * @return 这个 key 的剩余存活时间 */ @Override public long getTimeout(String key) { LocalDateTime localDateTime = Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getExpireAt).orElse(LocalDateTime.MIN); long seconds = Duration.between(LocalDateTime.now(), localDateTime).getSeconds(); if (seconds < 0) { return 0; } return seconds; } /** * 修改 value 的剩余存活时间(单位: 秒) * * @param key 指定 key * @param timeout 过期时间(单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { // 判断是否想要设置为永久 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); //noinspection StatementWithEmptyBody if (expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } mongoTemplate.upsert( keyQuery(key), Update.update("expireAt", getExpireAtFromTimeout(timeout)), SaTokenMongoData.class ); } /** * 获取 Object,如无返空 * * @param key 键名称 * @return object */ @Override public Object getObject(String key) { return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getSession).orElse(null); } /** * 写入 Object,并设定存活时间 (单位: 秒) * * @param key 键名称 * @param object 值 * @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于等于-2时不存储) */ @Override public void setObject(String key, Object object, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 mongoTemplate.upsert( keyQuery(key), Update.update("session", object).set("expireAt", getExpireAtFromTimeout(timeout)), SaTokenMongoData.class ); } /** * 更新 Object (过期时间不变) * * @param key 键名称 * @param object 值 */ @Override public void updateObject(String key, Object object) { long expire = getObjectTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.setObject(key, object, expire); } /** * 删除 Object * * @param key 键名称 */ @Override public void deleteObject(String key) { delete(key); } /** * 获取 Object 的剩余存活时间 (单位: 秒) * * @param key 指定 key * @return 这个 key 的剩余存活时间 */ @Override public long getObjectTimeout(String key) { return getTimeout(key); } /** * 修改 Object 的剩余存活时间(单位: 秒) * * @param key 指定 key * @param timeout 剩余存活时间 */ @Override public void updateObjectTimeout(String key, long timeout) { // 判断是否想要设置为永久 updateTimeout(key, timeout); } /** * 搜索数据 * * @param prefix 前缀 * @param keyword 关键字 * @param start 开始处索引 * @param size 获取数量 (-1代表从 start 处一直取到末尾) * @param sortType 排序类型(true=正序,false=反序) * @return 查询到的数据集合 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { List criteriaList = new ArrayList<>(); if (StringUtils.hasText(prefix)) { criteriaList.add(Criteria.where("key").regex(Pattern.compile("^" + Pattern.quote(prefix)))); } if (StringUtils.hasText(keyword)) { Pattern keywordPattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE); criteriaList.add(Criteria.where("key").regex(keywordPattern)); } Criteria criteria = new Criteria(); if (!criteriaList.isEmpty()) { criteria.andOperator(criteriaList); } long skip = (long) Math.max(start, 0) * Math.max(size, 1); Query query = Query.query(criteria).skip(skip).limit(size); query.fields().include("key"); return mongoTemplate.find(query, SaTokenMongoData.class).stream().map(SaTokenMongoData::getKey).toList(); } } ``` ================================================ FILE: sa-token-doc/up/integ-spring-mongod-2.md ================================================ # Sa-Token 集成 MongoDB --- 在 Spring Boot 下集成 MongoDB: ``` xml org.springframework.boot spring-boot-starter-data-mongodb ``` ``` gradle // 提供MongoDB依赖 implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' ``` 1. 创建一个 `MySaSession` 并继承 `SaSession` ```java public class MySaSession extends SaSession { public MySaSession(String id) { super(id); } public void setDataMap(Map dataMap) { refreshDataMap(dataMap); } } ``` 原因:由于 `SaSession` 中的 `dataMap` 字段没有 `setter` 方法,当 `spring-data-mongodb` 反序列化 `SaSession` 时会报 `Cannot set property dataMap because no setter, no wither and it's not part of the persistence constructor public cn.dev33.satoken.session.SaSession()` 错误 2. 在 `SpringBoot` 启动方法中重写 `SaStrategy.instance.createSession` 方法,使我们自定义的 `MySaSession` 生效 ```java @SpringBootApplication public class SpringApplication { public static void main(String[] args) { // 重写 SaStrategy.instance.createSession 方法 SaStrategy.instance.createSession = (sessionId) -> { return new MySaSession(sessionId); }; SpringApplication.run(SpringApplication.class, args); } } ``` 3. 实现 SaTokenDao 接口 ```java //定义一个类用于保存SaSession @Document public class SaTokenWrap { private String id; private String value; private Object object; // 这里利用MongoDB的TTL索引,当过期时MongoDB会自动删除过期的数据,同时如果timeout如果为null那么视为永不删除 @Indexed(expireAfterSeconds = 1, background = true) private Date timeout; public boolean live() { return getTimeout() == null || getTimeout().after(new Date()); } } ``` ```java // SaTokenDao 实现 @Component public class SaTokenDaoMongo implements SaTokenDao { private final MongoTemplate mongoTemplate; public SaTokenDaoMongo(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } Optional getByKey(String key) { SaTokenWrap tokenWrap = mongoTemplate.findById(key, SaTokenWrap.class); return Optional.ofNullable(tokenWrap).filter(SaTokenWrap::live); } Date timeoutToDate(long timeout) { return new Date(timeout * 1000 + System.currentTimeMillis()); } void upsertByPath(String key, String path, Object value, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } Update update = Update.update(path, value); if (timeout != SaTokenDao.NEVER_EXPIRE) { update.set("timeout", timeoutToDate(timeout)); } else { update.unset("timeout"); } mongoTemplate.upsert( Query.query(Criteria.where("id").is(key)), update, SaTokenWrap.class ); } void updateByPath(String key, String path, Object value) { mongoTemplate.updateFirst( Query.query(Criteria.where("id").is(key).and("timeout").gte(new Date())), Update.update(path, value), SaTokenWrap.class ); } // ------------------------ String 读写操作 @Override public String get(String key) { return getByKey(key).map(SaTokenWrap::getValue).orElse(null); } @Override public void set(String key, String value, long timeout) { upsertByPath(key, "value", value, timeout); } @Override public void update(String key, String value) { updateByPath(key, "value", value); } @Override public void delete(String key) { mongoTemplate.remove(Query.query(Criteria.where("id").is(key))); } @Override public long getTimeout(String key) { SaTokenWrap tokenWrap = mongoTemplate.findById(key, SaTokenWrap.class); if (tokenWrap == null) { return SaTokenDao.NOT_VALUE_EXPIRE; } if (tokenWrap.getTimeout() == null) { return SaTokenDao.NEVER_EXPIRE; } long expire = tokenWrap.getTimeout().getTime(); long timeout = (expire - System.currentTimeMillis()) / 1000; // 小于零时,视为不存在 if (timeout < 0) { mongoTemplate.remove(Query.query(Criteria.where("id").is(key))); return SaTokenDao.NOT_VALUE_EXPIRE; } return timeout; } @Override public void updateTimeout(String key, long timeout) { Update update = new Update(); if (timeout == SaTokenDao.NEVER_EXPIRE) { update.unset("timeout"); } else { update.set("timeout", timeoutToDate(timeout)); } mongoTemplate.upsert( Query.query(Criteria.where("id").is(key)), update, SaTokenWrap.class ); } // ------------------------ Object 读写操作 @Override public Object getObject(String key) { return getByKey(key).map(SaTokenWrap::getObject).orElse(null); } @Override public void setObject(String key, Object object, long timeout) { upsertByPath(key, "object", object, timeout); } @Override public void updateObject(String key, Object object) { updateByPath(key, "object", object); } @Override public void deleteObject(String key) { delete(key); } @Override public long getObjectTimeout(String key) { return getTimeout(key); } @Override public void updateObjectTimeout(String key, long timeout) { updateTimeout(key, timeout); } // ------------------------ Session 读写操作 // 使用接口默认实现 // --------- 会话管理 @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { List wrapList = mongoTemplate.find( Query.query(Criteria.where("id").regex(prefix + "*" + keyword + "*").and("timeout").gte(new Date())), SaTokenWrap.class ); List list = wrapList.stream().map(SaTokenWrap::getValue).filter(StringUtils::hasText).collect(Collectors.toList()); return SaFoxUtil.searchList(list, start, size, sortType); } } ``` ================================================ FILE: sa-token-doc/up/login-parameter.md ================================================ # 登录参数 ### 1、登录参数 在之前的章节我们提到,通过 `StpUtil.login(xxx)` 可以完成指定账号登录,同时你可以指定第二个参数来扩展登录信息,比如: ``` java // 指定`账号id`和`设备类型`进行登录 StpUtil.login(10001, "PC"); // 设置登录账号 id 为 10001,并指定是否为 “记住我” 模式 StpUtil.login(10001, false); ``` 除了以上内容,第二个参数你还可以指定一个 `SaLoginParameter` 对象,来详细控制登录的多个细节,例如: ``` java StpUtil.login(10001, new SaLoginParameter() .setDeviceType("PC") // 此次登录的客户端设备类型, 一般用于完成 [同端互斥登录] 功能 .setDeviceId("xxxxxxxxx") // 此次登录的客户端设备ID, 登录成功后该设备将标记为可信任设备 .setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) .setTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的有效期, 单位:秒,-1=永久有效 .setActiveTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的最低活跃频率, 单位:秒,-1=不进行活跃检查 .setIsConcurrent(true) // 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) .setIsShare(false) // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token) .setMaxLoginCount(12) // 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) .setMaxTryTimes(12) // 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用) .setExtra("key", "value") // 记录在 Token 上的扩展参数(只在 jwt 模式下生效) .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token .setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头 .setTerminalExtra("key", "value")// 本次登录挂载到 SaTerminalInfo 的自定义扩展数据 .setReplacedRange(SaReplacedRange.CURR_DEVICE_TYPE) // 顶人下线的范围: CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端 .setOverflowLogoutMode(SaLogoutMode.LOGOUT) // 溢出 maxLoginCount 的客户端,将以何种方式注销下线: LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线 .setRightNowCreateTokenSession(true) // 是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) .setupCookieConfig(cookie->{ // 设置 Cookie 配置项 cookie.setDomain("sa-token.cc"); // 设置:作用域 cookie.setPath("/shop"); // 设置:路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。) cookie.setSecure(true); // 设置:是否只在 https 协议下有效 cookie.setHttpOnly(true); // 设置:是否禁止 js 操作 Cookie cookie.setSameSite("Lax"); // 设置:第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) cookie.addExtraAttr("aa", "bb"); // 设置:额外扩展属性 } ); ``` 以上大部分参数在未指定时将使用全局配置作为默认值。 ### 2、注销参数 同样的,在调用注销时,也可以指定一些参数决定注销的细节行为: ``` java // 当前客户端注销 StpUtil.logout(new SaLogoutParameter() // 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话 // 此参数只在调用 StpUtil.logout() 时有效 .setRange(SaLogoutRange.TOKEN) ); // 指定 token 注销 StpUtil.logoutByTokenValue("xxxxxxxxxxxxxxxxxxxxxxx", new SaLogoutParameter() // 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)(默认 false) // 此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效 .setIsKeepFreezeOps(false) // 是否保留此 token 的 Token-Session 对象(默认 false) .setIsKeepTokenSession(true) ); // 指定 loginId 注销 StpUtil.logout(10001, new SaLogoutParameter() .setDeviceType("PC") // 设置注销的设备类型 (如果不指定,则默认注销所有客户端) .setIsKeepTokenSession(true) // 是否保留对应 token 的 Token-Session 对象(默认 false) .setMode(SaLogoutMode.REPLACED) // 设置注销模式:LOGOUT=注销登录、KICKOUT=踢人下线,REPLACED=顶人下线(默认LOGOUT) ); ``` 以上大部分参数在未指定时将使用全局配置作为默认值。 ### 3、遍历登录终端详细操作 如果你的 登录策略 或 注销策略 非常复杂,凭借上述参数无法组合出你的业务场景,你可以手动遍历一个账号的已登录终端信息列表,手动决定某个设备是否下线,例如: ``` java // 测试 @RequestMapping("logout") public SaResult logout() { // 遍历账号 10001 已登录终端列表,进行详细操作 StpUtil.forEachTerminalList(10001, (session, ter) -> { // 根据登录顺序,奇数的保留,偶数的下线 if(ter.getIndex() % 2 == 0) { StpUtil.removeTerminalByLogout(session, ter); // 注销下线方式 移除这个登录客户端 // StpUtil.removeTerminalByKickout(session, ter); // 踢人下线方式 移除这个登录客户端 // StpUtil.removeTerminalByReplaced(session, ter); // 顶人下线方式 移除这个登录客户端 } }); return SaResult.ok(); } ``` ================================================ FILE: sa-token-doc/up/many-account.md ================================================ # 多账号认证 --- ### 1、需求场景 有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 `user表` 和 `admin表`, 在这种场景下,如果两套账号我们都使用 `StpUtil` 类的API进行登录鉴权,那么势必会发生逻辑冲突。 在Sa-Token中,这个问题的模型叫做:多账号体系认证。 要解决这个问题,我们必须有一个合理的机制将这两套账号的授权给区分开,让它们互不干扰才行。 ### 2、演进思路 假如说我们的 user表 和 admin表 都有一个 id=10001 的账号,它们对应的登录代码:`StpUtil.login(10001)` 是一样的, 那么问题来了:在`StpUtil.getLoginId()`获取到的账号id如何区分它是User用户,还是Admin用户? 你可能会想到为他们加一个固定前缀,比如`StpUtil.login("User_" + 10001)`、`StpUtil.login("Admin_" + 10001)`,这样确实是可以解决问题的, 但是同样的:你需要在`StpUtil.getLoginId()`时再裁剪掉相应的前缀才能获取真正的账号id,这样一增一减就让我们的代码变得无比啰嗦。 那么,有没有从框架层面支持的,更优雅的解决方案呢? ### 3、解决方案 前面几篇介绍的api调用,都是经过 StpUtil 类的各种静态方法进行授权认证, 而如果我们深入它的源码,[点此阅览](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java)
    就会发现,此类并没有任何代码逻辑,唯一做的事就是对成员变量`stpLogic`的各个API包装一下进行转发。 这样做有两个好处: - StpLogic 类的所有函数都可以被重写,按需扩展。 - 在构造方法时随意传入一个不同的 `loginType`,就可以再造一套账号登录体系。 ### 4、操作示例 比如说,对于原生`StpUtil`类,我们只做`admin账号`权限认证,而对于`user账号`,我们则: 1. 新建一个新的权限认证类,比如: `StpUserUtil.java`。 2. 将`StpUtil.java`类的全部代码复制粘贴到 `StpUserUtil.java`里。 3. 更改一下其 `LoginType`, 比如: ``` java public class StpUserUtil { /** * 账号体系标识 */ public static final String TYPE = "user"; // 将 LoginType 从`login`改为`user` // 其它代码 ... } ``` 成品样例参考:[码云 StpUserUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java) 4、接下来就可以像调用`StpUtil.java`一样调用 `StpUserUtil.java`了,这两套账号认证的逻辑是完全隔离的。例如: ``` java // 凡是在 StpUtil 上有的方法,都可以在 StpUserUtil 上调用 StpUserUtil.login(10001); // 在当前会话以10001账号进行登录 StpUserUtil.checkLogin(); // 校验当前账号是否以 User 身份进行登录 StpUserUtil.getSession(); // 获取当前 User 账号的 Access-Session 对象 StpUserUtil.checkPermission('xx'); // 校验当前登录的 user 账号是否具有 xx 权限 // ... ``` ### 5、Kit模式 如果你觉得 “复制代码” 的方式繁琐不够优雅,这里还有另一种方案:建立一个 `StpKit.java` 门面类,声明所有的 `StpLogic` 引用: ``` java /** * StpLogic 门面类,管理项目中所有的 StpLogic 账号体系 */ public class StpKit { /** * 默认原生会话对象 */ public static final StpLogic DEFAULT = StpUtil.stpLogic; /** * Admin 会话对象,管理 Admin 表所有账号的登录、权限认证 */ public static final StpLogic ADMIN = new StpLogic("admin"); /** * User 会话对象,管理 User 表所有账号的登录、权限认证 */ public static final StpLogic USER = new StpLogic("user"); /** * XX 会话对象,(项目中有多少套账号表,就声明几个 StpLogic 会话对象) */ public static final StpLogic XXX = new StpLogic("xx"); } ``` 在需要登录、权限认证的地方: ``` java // 在当前会话进行 Admin 账号登录 StpKit.ADMIN.login(10001); // 在当前会话进行 User 账号登录 StpKit.USER.login(10001); // 检测当前会话是否以 Admin 账号登录,并具有 article:add 权限 StpKit.ADMIN.checkPermission("article:add"); // 检测当前会话是否以 User 账号登录,并通过了二级认证 StpKit.USER.checkSafe(); // 获取当前 User 会话的 Session 对象,并进行写值操作 StpKit.USER.getSession().set("name", "zhang"); ``` ### 6、在多账户模式下使用注解鉴权 框架默认的注解鉴权 如`@SaCheckLogin` 只针对原生`StpUtil`进行鉴权。 例如,我们在一个方法上加上`@SaCheckLogin`注解,这个注解只会放行通过`StpUtil.login(id)`进行登录的会话, 而对于通过`StpUserUtil.login(id)`进行登录的会话,则始终不会通过校验。 那么如何告诉`@SaCheckLogin`要鉴别的是哪套账号的登录会话呢?很简单,你只需要指定一下注解的type属性即可: ``` java // 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`,而不是原生`StpUtil` @SaCheckLogin(type = StpUserUtil.TYPE) @RequestMapping("info") public String info() { return "查询用户信息"; } ``` 注:`@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理,亦可根据type属性指定其校验的账号体系,此属性默认为`""`,代表使用原生`StpUtil`账号体系。 ### 7、使用注解合并简化代码 交流群里有同学反应,虽然可以根据 `@SaCheckLogin(type = "user")` 指定账号类型,但几十上百个注解都加上这个的话,还是有些繁琐,代码也不够优雅,有么有更简单的解决方案? 我们期待一种`[注解继承/合并]`的能力,即:自定义一个注解,标注上`@SaCheckLogin(type = "user")`, 然后在方法上标注这个自定义注解,效果等同于标注`@SaCheckLogin(type = "user")`。 很遗憾,JDK默认的注解处理器并没有提供这种`[注解继承/合并]`的能力,不过好在我们可以利用 Spring 的注解处理器,达到同样的目的。 1. 重写Sa-Token默认的注解处理器: ``` java @Configuration public class SaTokenConfigure { @PostConstruct public void rewriteSaStrategy() { // 重写Sa-Token的注解处理器,增加注解合并功能 SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> { return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); }; } } ``` 2. 自定义一个注解: ``` java /** * 登录认证(User版):只有登录之后才能进入该方法 *

    可标注在函数、类上(效果等同于标注在此类的所有方法上) */ @SaCheckLogin(type = "user") @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE}) public @interface SaUserCheckLogin { } ``` 3. 接下来就可以使用我们的自定义注解了: ``` java // 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user") @SaUserCheckLogin @RequestMapping("info") public String info() { return "查询用户信息"; } ``` 注:其它注解 `@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")`同理, 完整示例参考 Gitee 代码: [注解合并](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation)。 > [!TIP| label:自定义注解方案] > 除了注解合并方案,这里还有一份自定义注解方案,参考:[自定义注解](/fun/custom-annotations) ### 8、同端多登陆 假设我们不仅需要在后台同时集成两套账号,我们还需要在一个客户端同时登陆两套账号(业务场景举例:一个APP中可以同时登陆商家账号和用户账号)。 如果我们不做任何特殊处理的话,在客户端会发生`token覆盖`,新登录的 token 会覆盖掉旧登录的 token 从而导致旧登录失效。 具体表现大致为:在一个浏览器登录商家账号后,再登录用户账号,然后商家账号的登录态就会自动失效。 那么如何解决这个问题?很简单,我们只要更改一下 `StpUserUtil` 的 `TokenName` 即可,参考示例如下: ``` java public class StpUserUtil { // 使用匿名子类 重写`stpLogic对象`的一些方法 public static StpLogic stpLogic = new StpLogic("user") { // 重写 StpLogic 类下的 `splicingKeyTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突 @Override public String splicingKeyTokenName() { return super.splicingKeyTokenName() + "-user"; } // 同理你可以按需重写一些其它方法 ... }; // ... } ``` 再次调用 `StpUserUtil.login(10001)` 进行登录授权时,token的名称将不再是 `satoken`,而是我们重写后的 `satoken-user`,这样就不会再客户端发生 token 的相互覆盖了。 ### 9、不同体系不同 SaTokenConfig 配置 如果自定义的 StpUserUtil 需要使用不同 SaTokenConfig 对象, 也很简单,参考示例如下: ``` java @Configuration public class SaTokenConfigure { @PostConstruct public void setSaTokenConfig() { // 设定 StpUtil 使用的 SaTokenConfig 配置参数对象 SaTokenConfig config1 = new SaTokenConfig(); config1.setTokenName("satoken1"); config1.setTimeout(1000); config1.setTokenStyle("random-64"); // 更多设置 ... StpUtil.stpLogic.setConfig(config1); // 设定 StpUserUtil 使用的 SaTokenConfig 配置参数对象 SaTokenConfig config2 = new SaTokenConfig(); config2.setTokenName("satoken2"); config2.setTimeout(2000); config2.setTokenStyle("tik"); // 更多设置 ... StpUserUtil.stpLogic.setConfig(config2); } } ``` ### 10、多账号体系混合鉴权 QQ群中经常有小伙伴提问:在多账号体系下,怎么在 SaInterceptor 拦截器中给一个接口登录鉴权? 其实这个问题,主要是靠你的业务需求来决定,以后台 Admin 账号和前台 User 账号为例: ``` java // 注册 Sa-Token 拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { // 如果这个接口,要求客户端登录了后台 Admin 账号才能访问: SaRouter.match("/art/getInfo").check(r -> StpUtil.checkLogin()); // 如果这个接口,要求客户端登录了前台 User 账号才能访问: SaRouter.match("/art/getInfo").check(r -> StpUserUtil.checkLogin()); // 如果这个接口,要求客户端同时登录 Admin 和 User 账号,才能访问: SaRouter.match("/art/getInfo").check(r -> { StpUtil.checkLogin(); StpUserUtil.checkLogin(); }); // 如果这个接口,要求客户端登录 Admin 和 User 账号任意一个,就能访问: SaRouter.match("/art/getInfo").check(r -> { if(StpUtil.isLogin() == false && StpUserUtil.isLogin() == false) { throw new SaTokenException("请登录后再访问接口"); } }); })).addPathPatterns("/**"); } ``` ### 11、在一个接口里获取是哪个体系的账号正在登录 可以分别用两个体系的 isLogin() 方法去判断,哪个返回 true 就代表正在登录哪个体系 ``` java @RequestMapping("test") public SaResult test2() { String loginType = ""; if(StpUtil.isLogin()) { loginType = StpUtil.getLoginType(); } if(StpUserUtil.isLogin()) { loginType = StpUserUtil.getLoginType(); } System.out.println("当前登录的 loginType:" + loginType); return SaResult.ok(); } ``` 请注意此处可能出现的两种边际情况: - 两个 if 均返回 false:代表客户端在两个账号体系都没有登录。 - 两个 if 均返回 true:代表客户端在两个账号体系都登录了。 ### 12、注意点:运行时不可更改 LoginType 在 Q群 解决问题时,发现有些同学会写出类似下列形式的代码: ``` java StpUtil.login(10001); StpUtil.getStpLogic().setLoginType("user"); StpUtil.getSession().set("name", "zhangsan"); ``` 这是一种错误写法:LoginType 不可在运行时更改,只能在项目启动时指定。一旦项目启动成功后再修改 LoginType ,就会造成线程安全问题和严重的逻辑问题。 --- 本章代码示例:Sa-Token 多账号体系认证 —— [ StpUserUtil.java ] ================================================ FILE: sa-token-doc/up/mock-person.md ================================================ # 模拟他人 --- 以上介绍的 API 都是操作当前账号,对当前账号进行各种鉴权操作,你可能会问,我能不能对别的账号进行一些操作?
    比如:查看账号 10001 有无某个权限码、获取 账号 id=10002 的 `Account-Session`,等等... Sa-Token 在 API 设计时充分考虑了这一点,暴露出多个api进行此类操作: ## 有关操作其它账号的api ``` java // 获取指定账号10001的`tokenValue`值 StpUtil.getTokenValueByLoginId(10001); // 将账号10001的会话注销登录 StpUtil.logout(10001); // 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回 StpUtil.getSessionByLoginId(10001); // 获取账号10001的Session对象, 如果session尚未创建, 则返回null StpUtil.getSessionByLoginId(10001, false); // 获取账号10001是否含有指定角色标识 StpUtil.hasRole(10001, "super-admin"); // 获取账号10001是否含有指定权限码 StpUtil.hasPermission(10001, "user:add"); ``` ## 临时身份切换 有时候,我们需要直接将当前会话的身份切换为其它账号,比如: ``` java // 将当前会话[身份临时切换]为其它账号(本次请求内有效) StpUtil.switchTo(10044); // 此时再调用此方法会返回 10044 (我们临时切换到的账号id) StpUtil.getLoginId(); // 结束 [身份临时切换] StpUtil.endSwitch(); ``` 你还可以:直接在一个代码段里方法内,临时切换身份为指定loginId(此方式无需手动调用`StpUtil.endSwitch()`关闭身份切换) ``` java System.out.println("------- [身份临时切换]调用开始..."); StpUtil.switchTo(10044, () -> { System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch()); // 输出 true System.out.println("获取当前登录账号id: " + StpUtil.getLoginId()); // 输出 10044 }); System.out.println("------- [身份临时切换]调用结束..."); ``` --- 本章代码示例:Sa-Token 身份切换 —— [ SwitchToController.java ] ================================================ FILE: sa-token-doc/up/mutex-login.md ================================================ # 同端互斥登录 如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号。
    同端互斥登录,指的就是:像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。 --- ## 具体API 在 Sa-Token 中如何做到同端互斥登录?
    首先在配置文件中,将 `isConcurrent` 配置为false,然后调用登录等相关接口时声明设备类型即可: #### 指定设备类型登录 ``` java // 指定`账号id`和`设备类型`进行登录 StpUtil.login(10001, "PC"); ``` 调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-4` #### 指定设备类型强制注销 ``` java // 指定`账号id`和`设备类型`进行强制注销 StpUtil.logout(10001, "PC"); ``` 如果第二个参数填写null或不填,代表将这个账号id所有在线端强制注销,被踢出者再次访问系统时会抛出 `NotLoginException` 异常,场景值=`-2` #### 查询当前登录的设备类型 ``` java // 返回当前token的登录设备类型 StpUtil.getLoginDevice(); ``` #### Id 反查 Token ``` java // 获取指定loginId指定设备类型端的tokenValue StpUtil.getTokenValueByLoginId(10001, "APP"); ``` --- 本章代码示例:Sa-Token 同端互斥登录 —— [ MutexLoginController.java ] ================================================ FILE: sa-token-doc/up/not-cookie.md ================================================ # 前后端分离(无Cookie模式) --- ### 何为无 Cookie 模式? 无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 —— **前后端分离模式**。 常规 Web 端鉴权方法,一般由 `Cookie模式` 完成,而 Cookie 有两个特性: 1. 可由后端控制写入。 2. 每次请求自动提交。 这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)
    而在app、小程序等前后端分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊? 见招拆招,其实答案很简单: - 不能后端控制写入了,就前端自己写入。(难点在**后端如何将 Token 传递到前端**) - 每次请求不能自动提交了,那就手动提交。(难点在**前端如何将 Token 传递到后端**,同时**后端将其读取出来**) ### 1、后端将 token 返回到前端 1. 首先调用 `StpUtil.login(id)` 进行登录。 2. 调用 `StpUtil.getTokenInfo()` 返回当前会话的 token 详细参数。 - 此方法返回一个对象,其有两个关键属性:`tokenName`和`tokenValue`(token 的名称和 token 的值)。 - 将此对象传递到前台,让前端人员将这两个值保存到本地。 代码示例: ``` java // 登录接口 @RequestMapping("doLogin") public SaResult doLogin() { // 第1步,先登录上 StpUtil.login(10001); // 第2步,获取 Token 相关参数 SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); // 第3步,返回给前端 return SaResult.data(tokenInfo); } ``` ### 2、前端将 token 提交到后端 1. 无论是app还是小程序,其传递方式都大同小异。 2. 那就是,将 token 塞到请求`header`里 ,格式为:`{tokenName: tokenValue}`。 3. 以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例: **方式1,简单粗暴** ``` js // 1、首先在登录时,将 tokenValue 存储在本地,例如: uni.setStorageSync('tokenValue', tokenValue); // 2、在发起ajax请求的地方,获取这个值,并塞到header里 uni.request({ url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 header: { "content-type": "application/x-www-form-urlencoded", "satoken": uni.getStorageSync('tokenValue') // ⚠️ 关键代码, 注意参数名字是 satoken }, success: (res) => { console.log(res.data); } }); ``` **方式2,更加灵活** ``` js // 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如: uni.setStorageSync('tokenName', tokenName); uni.setStorageSync('tokenValue', tokenValue); // 2、在发起ajax的地方,获取这两个值, 并组织到head里 var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值 var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值 var header = { "content-type": "application/x-www-form-urlencoded" }; if (tokenName != undefined && tokenName != '') { header[tokenName] = tokenValue; } // 3、后续在发起请求时将 header 对象塞到请求头部 uni.request({ url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。 header: header, success: (res) => { console.log(res.data); } }); ``` 4. 只要按照如此方法将`token`值传递到后端,Sa-Token 就能像传统PC端一样自动读取到 token 值,进行鉴权。 5. 你可能会有疑问,难道我每个`ajax`都要写这么一坨?岂不是麻烦死了? - 你当然不能每个 ajax 都写这么一坨,因为这种重复性代码都是要封装在一个函数里统一调用的。 ### 其它解决方案? 如果你对 Cookie 非常了解,那你就会明白,所谓 Cookie ,本质上就是一个特殊的`header`参数而已, 而既然它只是一个 header 参数,我们就能手动模拟实现它,从而完成鉴权操作。 这其实是对`无Cookie模式`的另一种解决方案,有兴趣的同学可以百度了解一下,在此暂不赘述。 --- 本章代码示例:Sa-Token 前后端分离样例 —— [ NotCookieController.java ] ================================================ FILE: sa-token-doc/up/password-secure.md ================================================ # 密码加密 严格来讲,密码加密不属于 [权限认证] 的范畴,但是对于大多数系统来讲,密码加密又是安全认证不可或缺的部分, 所以,应大家要求,`Sa-Token`在 v1.14 版本添加密码加密模块,该模块非常简单,仅仅封装了一些常见的加密算法。 ### 摘要加密 md5、sha1、sha256 ``` java // md5加密 SaSecureUtil.md5("123456"); // sha1加密 SaSecureUtil.sha1("123456"); // sha256加密 SaSecureUtil.sha256("123456"); ``` ### 对称加密 AES加密 ``` java // 定义秘钥和明文 String key = "123456"; String text = "Sa-Token 一个轻量级java权限认证框架"; // 加密 String ciphertext = SaSecureUtil.aesEncrypt(key, text); System.out.println("AES加密后:" + ciphertext); // 解密 String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); System.out.println("AES解密后:" + text2); ``` 附:内部密钥生成策略,方便其他开发语言对接 ```java private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException { KeyGenerator kg = KeyGenerator.getInstance("AES"); //获取SHA1PRNG伪随机数生成器 SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); //将实际密码作为伪随机数生成器的种子 random.setSeed(password.getBytes()); //利用伪随机数生成器生成128位的密钥,能确保解密时生成的密钥的一致性 kg.init(128, random); SecretKey secretKey = kg.generateKey(); return new SecretKeySpec(secretKey.getEncoded(), "AES"); } ``` ### 非对称加密 ~~RSA加密(已过时)~~ ``` java // 定义私钥和公钥 String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg=="; String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB"; // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用公钥加密 String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text); System.out.println("公钥加密后:" + ciphertext); // 使用私钥解密 String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext); System.out.println("私钥解密后:" + text2); ``` 你可能会有疑问,私钥和公钥这么长的一大串,我怎么弄出来,手写吗?当然不是,调用以下方法生成即可 ``` java // 生成一对公钥和私钥,其中Map对象 (private=私钥, public=公钥) System.out.println(SaSecureUtil.rsaGenerateKeyPair()); ``` ### Base64编码与解码 ``` java // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用Base64编码 String base64Text = SaBase64Util.encode(text); System.out.println("Base64编码后:" + base64Text); // 使用Base64解码 String text2 = SaBase64Util.decode(base64Text); System.out.println("Base64解码后:" + text2); ``` ### Base32编码与解码 ``` java // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用Base32编码 String base32Text = SaBase32Util.encode(text); System.out.println("Base32编码后:" + base32Text); // 使用Base32解码 String text2 = SaBase32Util.decode(base32Text); System.out.println("Base32解码后:" + text2); ``` ### TOTP 验证器 ``` java // 1、生成密钥 String secretKey = SaTotpUtil.generateSecretKey(); System.out.println("TOTP 秘钥: " + secretKey); // 2、生成扫码字符串 String qeString = SaTotpUtil.generateGoogleSecretKey("zhangsan", secretKey); System.out.println("扫码字符串: " + qeString); // 3、计算当前 TOTP 码 String code = SaTotpUtil.generateTOTP(secretKey); System.out.println("当前时间戳对应的 TOTP 码: " + code); // 4、验证用户输入 boolean isValid = SaTotpUtil.validateTOTP(secretKey, code, 1); System.out.println("验证结果: " + isValid); ``` 在线 TOTP 管理器推荐: [TOTP 密码生成管理 - 工具哇](https://toolwa.com/totp/) ### BCrypt加密 由它加密的文件可在所有支持的操作系统和处理器上进行转移 它的口令必须是8至56个字符,并将在内部被转化为448位的密钥 > 此类来自于https://github.com/jeremyh/jBCrypt/ ``` java // 使用方法 String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); // 使用checkpw方法检查被加密的字符串是否与原始字符串匹配: BCrypt.checkpw(candidate_password, stored_hash); // gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度: String strong_salt = BCrypt.gensalt(10); String stronger_salt = BCrypt.gensalt(12); ```
    如需更多加密算法,可参考 [Hutool-crypto: 加密](https://hutool.cn/docs/#/crypto/%E6%A6%82%E8%BF%B0) --- 本章代码示例:Sa-Token 密码加密 —— [ SecureController.java ] ================================================ FILE: sa-token-doc/up/remember-me.md ================================================ # [记住我] 模式 --- 如图所示,一般网站的登录界面都会有一个 **`[记住我]`** 按钮,当你勾选它登录后,即使你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码: ../static/login-view.png 那么在Sa-Token中,如何做到 [ 记住我 ] 功能呢? ### 在 Sa-Token 中实现记住我功能 Sa-Token的登录授权,**默认就是`[记住我]`模式**,为了实现`[非记住我]`模式,你需要在登录时如下设置: ``` java // 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录 StpUtil.login(10001, false); ``` 那么,Sa-Token实现`[记住我]`的具体原理是? ### 实现原理 Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是: - 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。 - 持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。 利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式: - 勾选 [记住我] 按钮时:调用`StpUtil.login(10001, true)`,在浏览器写入一个`持久Cookie`储存 Token,此时用户即使重启浏览器 Token 依然有效。 - 不勾选 [记住我] 按钮时:调用`StpUtil.login(10001, false)`,在浏览器写入一个`临时Cookie`储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。 ### 前后端分离模式下如何实现[记住我]? 此时机智的你😏很快发现一个问题,Cookie虽好,却无法在前后端分离环境下使用,那是不是代表上述方案在APP、小程序等环境中无效? 准确的讲,答案是肯定的,任何基于Cookie的认证方案在前后端分离环境下都会失效(原因在于这些客户端默认没有实现Cookie功能),不过好在,这些客户端一般都提供了替代方案, 唯一遗憾的是,此场景中token的生命周期需要我们在前端手动控制: 以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例,我们可以使用如下方式达到同样的效果: ``` js // 使用本地存储保存token,达到 [持久Cookie] 的效果 uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); // 使用globalData保存token,达到 [临时Cookie] 的效果 getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx"; ``` 如果你决定在PC浏览器环境下进行前后端分离模式开发,那么更加简单: ``` js // 使用 localStorage 保存token,达到 [持久Cookie] 的效果 localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); // 使用 sessionStorage 保存token,达到 [临时Cookie] 的效果 sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx"); ``` Remember me, it's too easy! ### 登录时指定 Token 有效期 登录时不仅可以指定是否为`[记住我]`模式,还可以指定一个特定的时间作为 Token 有效时长,如下示例: ``` java // 示例1: // 指定token有效期(单位: 秒),如下所示token七天有效 StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7)); // ----------------------- 示例2:所有参数 // `SaLoginParameter`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如: StpUtil.login(10001, new SaLoginParameter() .setDevice("PC") // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型 .setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) .setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值) .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token .setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头 ); ``` --- 本章代码示例:Sa-Token 记住我登录 —— [ RememberMeController.java ] ================================================ FILE: sa-token-doc/up/safe-auth.md ================================================ # 二级认证 在某些敏感操作下,我们需要对已登录的会话进行二次验证。 比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 **[删除]** 按钮时,还是需要再次输入一遍密码,这么做主要为了两点: 1. 保证操作者是当前账号本人。 2. 增加操作步骤,防止误删除重要数据。 这就是我们本篇要讲的 —— 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。 --- ### 具体API 在`Sa-Token`中进行二级认证非常简单,只需要使用以下API: ``` java // 在当前会话 开启二级认证,时间为120秒 StpUtil.openSafe(120); // 获取:当前会话是否处于二级认证时间内 StpUtil.isSafe(); // 检查当前会话是否已通过二级认证,如未通过则抛出异常 StpUtil.checkSafe(); // 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) StpUtil.getSafeTime(); // 在当前会话 结束二级认证 StpUtil.closeSafe(); ``` ### 一个小示例 一个完整的二级认证业务流程,应该大致如下: ``` java // 删除仓库 @RequestMapping("deleteProject") public SaResult deleteProject(String projectId) { // 第1步,先检查当前会话是否已完成二级认证 if(!StpUtil.isSafe()) { return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口"); } // 第2步,如果已完成二级认证,则开始执行业务逻辑 // ... // 第3步,返回结果 return SaResult.ok("仓库删除成功"); } // 提供密码进行二级认证 @RequestMapping("openSafe") public SaResult openSafe(String password) { // 比对密码(此处只是举例,真实项目时可拿其它参数进行校验) if("123456".equals(password)) { // 比对成功,为当前会话打开二级认证,有效期为120秒 StpUtil.openSafe(120); return SaResult.ok("二级认证成功"); } // 如果密码校验失败,则二级认证也会失败 return SaResult.error("二级认证失败"); } ``` > [!NOTE| label:调用步骤:] > 1. 前端调用 `deleteProject` 接口,尝试删除仓库。 > 2. 后端校验会话尚未完成二级认证,返回: `仓库删除失败,请完成二级认证后再次访问接口`。 > 3. 前端将信息提示给用户,用户输入密码,调用 `openSafe` 接口。 > 4. 后端比对用户输入的密码,完成二级认证,有效期为:120秒。 > 5. 前端在 120 秒内再次调用 `deleteProject` 接口,尝试删除仓库。 > 6. 后端校验会话已完成二级认证,返回:`仓库删除成功`。 ### 指定业务标识进行二级认证 如果项目有多条业务线都需要敏感操作验证,则 `StpUtil.openSafe()` 无法提供细粒度的认证操作, 此时我们可以指定一个业务标识来分辨不同的业务线: ``` java // 在当前会话 开启二级认证,业务标识为client,时间为600秒 StpUtil.openSafe("client", 600); // 获取:当前会话是否已完成指定业务的二级认证 StpUtil.isSafe("client"); // 校验:当前会话是否已完成指定业务的二级认证 ,如未认证则抛出异常 StpUtil.checkSafe("client"); // 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) StpUtil.getSafeTime("client"); // 在当前会话 结束指定业务标识的二级认证 StpUtil.closeSafe("client"); ``` 业务标识可以填写任意字符串,不同业务标识之间的认证互不影响,比如: ``` java // 打开了业务标识为 client 的二级认证 StpUtil.openSafe("client"); // 判断是否处于 shop 的二级认证,会返回 false StpUtil.isSafe("shop"); // 返回 false // 也不会通过校验,会抛出异常 StpUtil.checkSafe("shop"); ``` ### 使用注解进行二级认证 在一个方法上使用 `@SaCheckSafe` 注解,可以在代码进入此方法之前进行一次二级认证校验 ``` java // 二级认证:必须二级认证之后才能进入该方法 @SaCheckSafe @RequestMapping("add") public String add() { return "用户增加"; } // 指定业务类型,进行二级认证校验 @SaCheckSafe("art") @RequestMapping("add2") public String add2() { return "文章增加"; } ``` 详细使用方法可参考:[注解鉴权](/use/at-check),此处不再赘述 --- 本章代码示例:Sa-Token 二级认证 —— [ SafeAuthController.java ] ================================================ FILE: sa-token-doc/up/search-session.md ================================================ # 会话查询 --- ### 1、单账号会话查询 使用 `StpUtil.getTerminalListByLoginId( loginId )` 可获取指定账号已登录终端列表信息,例如: ``` java public static void main(String[] args) { System.out.println("账号 10001 登录设备信息:"); List terminalList = StpUtil.getTerminalListByLoginId(10001); for (SaTerminalInfo ter : terminalList) { System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime()); } } ``` 控制台打印结果: ``` txt 账号 10001 登录设备信息: 登录index=1, 设备type=PC, token=a8fbb46f-e043-459a-a875-0a2874911be8, 登录time=1742354951192 登录index=2, 设备type=APP, token=882b6c9c-bdf9-4e8f-a42b-6e17d2fe0e34, 登录time=1742354960950 登录index=3, 设备type=WEB, token=dacac78c-0983-4819-ab8b-07e7603597fc, 登录time=1742354962848 ``` 一个 `SaTerminalInfo` 对象代表一个终端信息,其有如下字段: ``` java terminal.getIndex(); // 登录会话索引值 (该账号第几个登录的设备) terminal.getDeviceType(); // 所属设备类型,例如:PC、WEB、HD、MOBILE、APP terminal.getTokenValue(); // 此次登录的token值 terminal.getCreateTime(); // 登录时间, 13位时间戳 terminal.getDeviceId(); // 设备id, 设备唯一标识 terminal.getExtra("key"); // 此次登录的额外自定义参数 ``` `Extra` 自定义参数可以在登录时通过如下方式指定: ``` java StpUtil.login(10001, new SaLoginParameter().setTerminalExtra("key", "value")); ``` ### 2、全部会话检索 ``` java // 查询所有已登录的 Token StpUtil.searchTokenValue(String keyword, int start, int size, boolean sortType); // 查询所有 Account-Session 会话 StpUtil.searchSessionId(String keyword, int start, int size, boolean sortType); // 查询所有 Token-Session 会话 StpUtil.searchTokenSessionId(String keyword, int start, int size, boolean sortType); ``` #### 参数详解: - `keyword`: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。 - `start`: 数据开始处索引。 - `size`: 要获取的数据条数 (值为-1代表一直获取到末尾)。 - `sortType`: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。 简单样例: ``` java // 查询 value 包括 1000 的所有 token,结果集从第 0 条开始,返回 10 条 List tokenList = StpUtil.searchTokenValue("1000", 0, 10, true); for (String token : tokenList) { System.out.println(token); } ``` #### 深入:`StpUtil.searchTokenValue` 和 `StpUtil.searchSessionId` 的区别? - StpUtil.searchTokenValue 查询的是登录产生的所有 Token。 - StpUtil.searchSessionId 查询的是所有已登录账号会话id。 举个例子,项目配置如下: ``` yml sa-token: # 允许同一账号在多个设备一起登录 is-concurrent: true # 同一账号每次登录产生不同的token is-share: false ``` 假设此时账号A在 电脑、手机、平板 依次登录(共3次登录),账号B在 电脑、手机 依次登录(共2次登录),那么: - `StpUtil.searchTokenValue` 将返回一共 5 个Token。 - `StpUtil.searchSessionId` 将返回一共 2 个 SessionId。 综上,若要遍历系统所有已登录的会话,代码将大致如下: ``` java // 获取所有已登录的会话id List sessionIdList = StpUtil.searchSessionId("", 0, -1, false); for (String sessionId : sessionIdList) { // 根据会话id,查询对应的 SaSession 对象,此处一个 SaSession 对象即代表一个登录的账号 SaSession session = StpUtil.getSessionBySessionId(sessionId); // 查询这个账号都在哪些设备登录了,依据上面的示例,账号A 的 SaTerminalInfo 数量是 3,账号B 的 SaTerminalInfo 数量是 2 List terminalList = session.terminalListCopy(); System.out.println("会话id:" + sessionId + ",共在 " + terminalList.size() + " 设备登录"); } ```
    #### 注意事项: 由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据: - 单机模式下:百万会话取出10条 Token 平均耗时 `0.255s`。 - Redis模式下:百万会话取出10条 Token 平均耗时 `3.322s`。 请根据业务实际水平合理调用API。 > [!WARNING| label:注意] > 基于活跃 Token 的统计方式会比实际情况略有延迟,如果需要精确统计实时在线用户信息需要采用 WebSocket。 --- 本章代码示例:Sa-Token 会话查询 —— [ SearchSessionController.java ] ================================================ FILE: sa-token-doc/up/token-prefix.md ================================================ # Token 提交前缀 ### 需求场景 在某些系统中,前端提交token时会在前面加个固定的前缀,例如: ``` js { "satoken": "Bearer xxxx-xxxx-xxxx-xxxx" } ``` 此时后端如果不做任何特殊处理,框架将会把`Bearer `视为token的一部分,无法正常读取token信息,导致鉴权失败。 为此,我们需要在yml中添加如下配置: ``` yaml sa-token: # 指定 token 提交时的前缀 token-prefix: Bearer ``` ``` properties # token前缀 sa-token.token-prefix=Bearer ``` 此时 Sa-Token 便可在读取 Token 时裁剪掉 `Bearer`,成功获取`xxxx-xxxx-xxxx-xxxx`。 注:**Token前缀 与 Token值 之间必须有一个空格** ### Cookie 模式自动填充前缀 由于`Cookie`中无法存储空格字符,所以配置 Token 前缀后,Cookie 模式将会失效,无法成功提交带有前缀的 token。 如果需要在这种场景下仍然使用 Cookie 模式验证 token,可以使用 `cookieAutoFillPrefix` 配置项打开 Cookie 模式自动填充前缀: ``` yaml sa-token: # 指定 Cookie 模式下自动填充 token 提交前缀 cookie-auto-fill-prefix: true ``` ``` properties # 指定 Cookie 模式下自动填充 token 提交前缀 sa-token.cookie-auto-fill-prefix=true ``` ================================================ FILE: sa-token-doc/up/token-style.md ================================================ # 自定义 Token 风格 本篇介绍token生成的各种风格,以及自定义token生成策略。 --- ## 内置风格 Sa-Token 默认的 token 生成策略是 uuid 风格,其模样类似于:`623368f0-ae5e-4475-a53f-93e4225f16ae`。
    如果你对这种风格不太感冒,还可以将 token 生成设置为其他风格。 怎么设置呢?只需要在yml配置文件里设置 `sa-token.token-style=风格类型` 即可,其有多种取值: ``` java // 1. token-style=uuid —— uuid风格 (默认风格) "623368f0-ae5e-4475-a53f-93e4225f16ae" // 2. token-style=simple-uuid —— 同上,uuid风格, 只不过去掉了中划线 "6fd4221395024b5f87edd34bc3258ee8" // 3. token-style=random-32 —— 随机32位字符串 "qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W" // 4. token-style=random-64 —— 随机64位字符串 "v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc" // 5. token-style=random-128 —— 随机128位字符串 "nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj" // 6. token-style=tik —— tik风格 "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" ``` ## 自定义 Token 生成策略 如果你觉着以上风格都不是你喜欢的类型,那么你还可以**自定义token生成策略**,来定制化token生成风格。
    怎么做呢?只需要重写 `SaStrategy` 策略类的 `createToken` 算法即可: #### 参考步骤如下: 1、在`SaTokenConfigure`配置类中添加代码: ``` java @Configuration public class SaTokenConfigure { /** * 重写 Sa-Token 框架内部算法策略 */ @PostConstruct public void rewriteSaStrategy() { // 重写 Token 生成策略 SaStrategy.instance.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); // 随机60位长度字符串 }; } } ``` 2、再次调用 `StpUtil.login(10001)`方法进行登录,观察其生成的token样式: ``` java gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH ``` > [!WARNING| label:更改了 token 生成策略但是不生效?] > 把 Redis 中的旧数据清除掉再试试 ================================================ FILE: sa-token-doc/use/at-check.md ================================================ # 注解鉴权 ### 注解鉴权 有同学表示:尽管使用代码鉴权非常方便,但是我仍希望把鉴权逻辑和业务逻辑分离开来,我可以使用注解鉴权吗?当然可以!
    注解鉴权 —— 优雅的将鉴权与业务代码分离! - `@SaCheckLogin`: 登录校验 —— 只有登录之后才能进入该方法。 - `@SaCheckRole("admin")`: 角色校验 —— 必须具有指定角色标识才能进入该方法。 - `@SaCheckPermission("user:add")`: 权限校验 —— 必须具有指定权限才能进入该方法。 - `@SaCheckSafe`: 二级认证校验 —— 必须二级认证之后才能进入该方法。 - `@SaCheckHttpBasic`: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。 - `@SaCheckHttpDigest`: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。 - `@SaCheckDisable("comment")`:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。 - `@SaCheckSign`:API 签名校验 —— 用于跨系统的 API 签名参数校验。 - `@SaIgnore`:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。 Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态。 因此,为了使用注解鉴权,**你必须手动将 Sa-Token 的全局拦截器注册到你项目中**。 ### 1、注册拦截器 以 SpringBoot2 项目为例,新建配置类`SaTokenConfigure.java` ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 拦截器,打开注解式鉴权功能 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器,打开注解式鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } } ``` 保证此类被`springboot`启动类扫描到即可 ### 2、使用注解鉴权 然后我们就可以愉快的使用注解鉴权了: ``` java // 登录校验:只有登录之后才能进入该方法 @SaCheckLogin @RequestMapping("info") public String info() { return "查询用户信息"; } // 角色校验:必须具有指定角色才能进入该方法 @SaCheckRole("super-admin") @RequestMapping("add") public String add() { return "用户增加"; } // 权限校验:必须具有指定权限才能进入该方法 @SaCheckPermission("user-add") @RequestMapping("add") public String add() { return "用户增加"; } // 二级认证校验:必须二级认证之后才能进入该方法 @SaCheckSafe() @RequestMapping("add") public String add() { return "用户增加"; } // Http Basic 校验:只有通过 Http Basic 认证后才能进入该方法 @SaCheckHttpBasic(account = "sa:123456") @RequestMapping("add") public String add() { return "用户增加"; } // Http Digest 校验:只有通过 Http Digest 认证后才能进入该方法 @SaCheckHttpDigest(value = "sa:123456") @RequestMapping("add") public String add() { return "用户增加"; } // 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 @SaCheckDisable("comment") @RequestMapping("send") public String send() { return "查询用户信息"; } ``` 注:以上注解都可以加在类上,代表为这个类所有方法进行鉴权 ### 3、设定校验模式 `@SaCheckRole`与`@SaCheckPermission`注解可设置校验模式,例如: ``` java // 注解式鉴权:只要具有其中一个权限即可通过校验 @RequestMapping("atJurOr") @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) public SaResult atJurOr() { return SaResult.data("用户信息"); } ``` mode有两种取值: - `SaMode.AND`,标注一组权限,会话必须全部具有才可通过校验。 - `SaMode.OR`,标注一组权限,会话只要具有其一即可通过校验。 ### 4、角色权限双重 “or校验” 假设有以下业务场景:一个接口在具有权限 `user.add` 或角色 `admin` 时可以调通。怎么写? ``` java // 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验 @RequestMapping("userAdd") @SaCheckPermission(value = "user.add", orRole = "admin") public SaResult userAdd() { return SaResult.data("用户信息"); } ``` orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法: - 写法一:`orRole = "admin"`,代表需要拥有角色 admin 。 - 写法二:`orRole = {"admin", "manager", "staff"}`,代表具有三个角色其一即可。 - 写法三:`orRole = {"admin, manager, staff"}`,代表必须同时具有三个角色。 ### 5、忽略认证 使用 `@SaIgnore` 可表示一个接口忽略认证: ``` java @SaCheckLogin @RestController public class TestController { // ... 其它方法 // 此接口加上了 @SaIgnore 可以游客访问 @SaIgnore @RequestMapping("getList") public SaResult getList() { // ... return SaResult.ok(); } } ``` 如上代码表示:`TestController` 中的所有方法都需要登录后才可以访问,但是 `getList` 接口可以匿名游客访问。 - @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。 - @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。 - @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权,在下面的 [路由拦截鉴权] 章节中我们会讲到。 ### 6、批量注解鉴权 使用 `@SaCheckOr` 表示批量注解鉴权: ``` java // 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。 @SaCheckOr( login = @SaCheckLogin, role = @SaCheckRole("admin"), permission = @SaCheckPermission("user.add"), safe = @SaCheckSafe("update-password"), httpBasic = @SaCheckHttpBasic(account = "sa:123456"), disable = @SaCheckDisable("submit-orders") ) @RequestMapping("test") public SaResult test() { // ... return SaResult.ok(); } ``` 每一项属性都可以写成数组形式,例如: ``` java // 当前客户端只要有 [ login 账号登录] 或者 [user 账号登录] 其一,就可以通过验证进入方法。 // 注意:`type = "login"` 和 `type = "user"` 是多账号模式章节的扩展属性,此处你可以先略过这个知识点。 @SaCheckOr( login = { @SaCheckLogin(type = "login"), @SaCheckLogin(type = "user") } ) @RequestMapping("test") public SaResult test() { // ... return SaResult.ok(); } ``` 疑问:既然有了 `@SaCheckOr`,为什么没有与之对应的 `@SaCheckAnd` 呢? 因为当你写多个注解时,其天然就是 `and` 校验关系,例如: ``` java // 当你在一个方法上写多个注解鉴权时,其默认就是要满足所有注解规则后,才可以进入方法,只要有一个不满足,就会抛出异常 @SaCheckLogin @SaCheckRole("admin") @SaCheckPermission("user.add") @RequestMapping("test") public SaResult test() { // ... return SaResult.ok(); } ``` 使用 append 字段追加抓取扩展包里的注解,例如: ``` java // 测试:只有通过登录校验,或者提供了正确的 ApiKey,才可以进入方法 @RequestMapping("/test") @SaCheckOr(login = @SaCheckLogin, append = { SaCheckApiKey.class }) @SaCheckApiKey public SaResult test() { // ... return SaResult.ok(); } ``` ### 7、扩展阅读 - 在业务逻辑层使用鉴权注解:[AOP注解鉴权](/plugin/aop-at) - 制作自定义鉴权注解注入到框架:[自定义注解](/fun/custom-annotations) --- 本章代码示例:Sa-Token 注解鉴权 —— [ AtCheckController.java ] 本章小练习:Sa-Token 基础 - 注解鉴权,章节测试 ================================================ FILE: sa-token-doc/use/config.md ================================================ # 框架配置 你可以**零配置启动框架**,但同时你也可以通过一定的参数配置,定制性使用框架,`Sa-Token`支持多种方式配置框架信息 --- ### 1、配置方式 ##### 方式1、在 application.yml 配置 ``` yaml ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称(同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true ``` ``` properties ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## # token 名称(同时也是 cookie 名称) sa-token.token-name=satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 sa-token.timeout=2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 sa-token.active-timeout=-1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) sa-token.is-concurrent=true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) sa-token.is-share=false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) sa-token.token-style=uuid # 是否输出操作日志 sa-token.is-log=true ``` ##### 方式2、通过代码配置 ``` java /** * Sa-Token 配置类 */ @Configuration public class SaTokenConfigure { // Sa-Token 参数配置,参考文档:https://sa-token.cc // 此配置会覆盖 application.yml 中的配置 @Bean @Primary public SaTokenConfig getSaTokenConfigPrimary() { SaTokenConfig config = new SaTokenConfig(); config.setTokenName("satoken"); // token 名称(同时也是 cookie 名称) config.setTimeout(30 * 24 * 60 * 60); // token 有效期(单位:秒),默认30天,-1代表永不过期 config.setActiveTimeout(-1); // token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 config.setIsConcurrent(true); // 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录) config.setIsShare(false); // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token) config.setTokenStyle("uuid"); // token 风格 config.setIsLog(false); // 是否输出操作日志 return config; } } ``` ``` java /** * Sa-Token 配置类 */ @Configuration public class SaTokenConfigure { // Sa-Token 参数配置,参考文档:https://sa-token.cc // 此配置会与 application.yml 中的配置合并 (代码配置优先) @Autowired public void configSaToken(SaTokenConfig config) { config.setTokenName("satoken"); // token 名称(同时也是 cookie 名称) config.setTimeout(30 * 24 * 60 * 60); // token 有效期(单位:秒),默认30天,-1代表永不过期 config.setActiveTimeout(-1); // token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 config.setIsConcurrent(true); // 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录) config.setIsShare(false); // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token) config.setTokenStyle("uuid"); // token 风格 config.setIsLog(false); // 是否输出操作日志 } } ``` 两者的区别在于: - 模式 1 会覆盖 application.yml 中的配置。 - 模式 2 会与 application.yml 中的配置合并(代码配置优先)。 --- ### 2、核心包所有可配置项 #### 2.1、核心模块配置 你不必立刻掌握整个表格,只需要在用到某个功能时再详细查阅它即可 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | tokenName | String | satoken | Token 名称 (同时也是 Cookie 名称、数据持久化前缀) | | timeout | long | 2592000 | Token 有效期(单位:秒),默认30天,-1代表永不过期 [参考:token有效期详解](/fun/token-timeout) | | activeTimeout | long | -1 | Token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结(例如可以设置为1800代表30分钟内无操作就冻结) [参考:token有效期详解](/fun/token-timeout) | | dynamicActiveTimeout | Boolean | false | 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数 | | isConcurrent | Boolean | true | 是否允许同一账号并发登录 (为 true 时允许一起登录,为 false 时新登录挤掉旧登录) | | isShare | Boolean | false | 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token,login 时提供了 Extra 数据后,即使配置了为 true 也不能复用旧 Token,必须创建新 Token) | | replacedLoginExitMode | SaReplacedLoginExitMode | OLD_DEVICE | 在 isConcurrent=false 时,决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线,新设备登录成功, NEW_DEVICE=新设备登录失败,旧设备维持在线) | | replacedRange | SaReplacedRange | CURR_DEVICE_TYPE | 在 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) | | maxLoginCount | int | 12 | 同一账号最大登录数量,-1代表不限 (只有在 `isConcurrent=true`,`isShare=false` 时此配置才有效),[详解](/use/config?id=配置项详解:maxlogincount) | | overflowLogoutMode | SaLogoutMode | LOGOUT | 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) | | maxTryTimes | int | 12 | 在每次创建 Token 时的最高循环次数,用于保证 Token 唯一性(-1=不循环重试,直接使用) | | isReadBody | Boolean | true | 是否尝试从 请求体 里读取 Token | | isReadHeader | Boolean | true | 是否尝试从 header 里读取 Token | | isReadCookie | Boolean | true | 是否尝试从 cookie 里读取 Token,此值为 false 后,`StpUtil.login(id)` 登录时也不会再往前端注入Cookie | | isLastingCookie | Boolean | true | 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在) | | isWriteHeader | Boolean | false | 是否在登录后将 Token 写入到响应头 | | logoutRange | SaLogoutRange | TOKEN | 注销范围 (TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) (此参数只在调用 StpUtil.logout() 时有效) | | isLogoutKeepFreezeOps | Boolean | false | 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API) (此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效) | | isLogoutKeepTokenSession | Boolean | false | 在注销 token 后,是否保留其对应的 Token-Session | | rightNowCreateTokenSession| Boolean | false | 在登录时,是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建) | | tokenStyle | String | uuid | token风格, [参考:自定义Token风格](/up/token-style) | | dataRefreshPeriod | int | 30 | 默认数据持久组件实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 | | tokenSessionCheckLogin | Boolean | true | 获取 `Token-Session` 时是否必须登录 (如果配置为true,会在每次获取 `Token-Session` 时校验是否登录),[详解](/use/config?id=配置项详解:tokenSessionCheckLogin) | | autoRenew | Boolean | true | 是否打开自动续签 (如果此值为true,框架会在每次直接或间接调用 `getLoginId()` 时进行一次过期检查与续签操作),[参考:token有效期详解](/fun/token-timeout) | | tokenPrefix | String | null | token前缀,例如填写 `Bearer` 实际传参 `satoken: Bearer xxxx-xxxx-xxxx-xxxx` [参考:自定义Token前缀](/up/token-prefix) | | cookieAutoFillPrefix | Boolean | false | cookie 模式是否自动填充 token 提交前缀 | | isPrint | Boolean | true | 是否在初始化配置时打印版本字符画 | | isLog | Boolean | false | 是否打印操作日志 | | logLevel | String | trace | 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动 | | logLevelInt | int | 1 | 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动 | | isColorLog | Boolean | null | 是否打印彩色日志,true=打印彩色日志,false=打印黑白日志,null=框架根据运行终端自行判断是否打印彩色日志 | | jwtSecretKey | String | null | jwt秘钥 (只有集成 `sa-token-temp-jwt` 模块时此参数才会生效),[参考:和 jwt 集成](/plugin/jwt-extend) | | sameTokenTimeout | long | 86400 | Same-Token的有效期 (单位: 秒),[参考:内部服务外网隔离](/micro/same-token) | | basic | String | "" | Http Basic 认证的账号和密码 [参考:Http Basic 认证](/up/basic-auth) | | currDomain | String | null | 配置当前项目的网络访问地址 | | checkSameToken | Boolean | false | 是否校验Same-Token(部分rpc插件有效) | | cookie | Object | new SaCookieConfig() | Cookie 配置对象 | | sign | Object | new SaSignConfig() | API 签名配置对象 | #### 2.2、Cookie相关配置: | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | domain | String | null | 作用域(写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景) | | path | String | / | 路径,默认写在域名根路径下 | | secure | Boolean | false | 是否只在 https 协议下有效 | | httpOnly | Boolean | false | 是否禁止 js 操作 Cookie | | sameSite | String | Lax | 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制) | | extraAttrs | String | new LinkedHashMap() | 额外扩展属性 | Cookie 配置示例: ``` yaml # Sa-Token 配置 sa-token: # Cookie 相关配置 cookie: # 基础属性 domain: stp.com path: / secure: false httpOnly: true sameSite: Lax # 额外扩展属性 extraAttrs: # Cookie 优先级 Priority: Medium # Cookie 独立分区 Partitioned: "" # 可以是任意键值对 # abc: def ``` ``` properties # Cookie 相关配置 # ---- 基础属性 sa-token.cookie.domain=stp.com sa-token.cookie.path=/ sa-token.cookie.secure=false sa-token.cookie.httpOnly=true sa-token.cookie.sameSite=Lax # ---- 额外扩展属性 # Cookie 优先级 sa-token.cookie.extraAttrs.Priority=Medium # Cookie 独立分区 sa-token.cookie.extraAttrs.Partitioned="" # 可以是任意键值对 # sa-token.cookie.extraAttrs.abc=def ``` #### 2.3、Sign 参数签名相关配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | secretKey | String | null | API 调用签名秘钥 | | timestampDisparity | long | 900000 | 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 | | digestAlgo | String | md5 | 对 fullStr 的摘要算法 | 示例: ``` yaml # Sa-Token 配置 sa-token: # 参数签名配置 sign: # API 接口调用签名秘钥 secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` ``` properties # API 接口调用签名秘钥 sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` #### 2.4、API Key 相关配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | prefix | String | AK- | API Key 前缀 | | timeout | long | 2592000 | API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) | | isRecordIndex | String | true | 框架是否记录索引信息 | 示例: ``` yaml # Sa-Token 配置 sa-token: # API Key 相关配置 api-key: # API Key 前缀 prefix: AK- # API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) timeout: 2592000 # 框架是否记录索引信息 is-record-index: true ``` ``` properties # API Key 前缀 sa-token.pi-key.prefix=AK- # API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) sa-token.pi-key.timeout=2592000 # 框架是否记录索引信息 sa-token.pi-key.is-record-index=true ``` ### 3、单点登录相关配置 #### 3.1、SSO-Server 端配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | mode | String | | 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) | | ticketTimeout | long | 300 | ticket 有效期 (单位: 秒) | | homeRoute | String | | 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 | | isSlo | Boolean | true | 是否打开单点注销功能 | | autoRenewTimeout | Bolean | false | 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) | | maxRegClient | int | 32 | 在 Access-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 | | isCheckSign | Boolean | true | 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) | | clients | Map | new LinkedHashMap<>(); | 以 Map 格式配置 Client 列表 | | allowAnonClient | Boolean | false | 是否允许匿名 Client 接入。参考: [匿名 client 接入](/sso/anon-client) | | allowUrl | String | | 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用),参考:[SSO整合:配置域名校验](/sso/sso-check-domain) | | secretKey | String | | API 调用签名秘钥 (全局默认 + 匿名 client 使用) | 配置示例: ``` yml # Sa-Token 配置 sa-token: # SSO-Server 配置 sso-server: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 home-route: /home ``` ``` properties # SSO-Server 配置 # Ticket有效期 (单位: 秒),默认五分钟 sa-token.sso-server.ticket-timeout=300 # 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 sa-token.sso-server.home-route=/home ``` #### 3.2、SSO-Client 端配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | mode | String | | 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) | | client | String | "" | 当前 Client 名称标识,用于和 ticket 码的互相锁定 | | serverUrl | String | null | 配置 Server 端主机总地址,拼接在 `authUrl`、`checkTicketUrl`、`userinfoUrl`、`sloUrl` 属性前面,用以简化各种 url 配置,参考:[详解](/sso/sso-questions?id=问:模式三配置一堆-xxx-url-,有办法简化一下吗?) | | authUrl | String | /sso/auth | 配置 Server 端单点登录授权地址 | | signoutUrl | String | /sso/signout | 配置 Server 端单点注销地址 | | pushUrl | String | /sso/pushS | 配置 Server 端的推送消息地址 | | getDataUrl | String | /sso/getData | 配置 Server 端的 拉取数据 地址 | | currSsoLogin | String | null | 配置当前 Client 端的登录地址(为空时自动获取) | | currSsoLogoutCall | String | null | 配置当前 Client 端的单点注销回调URL (为空时自动获取) | | isHttp | Boolean | false | 是否打开模式三(此值为 true 时将使用 http 请求:校验 ticket 值、单点注销、拉取数据getData),参考:[详解](/use/config?id=配置项详解:isHttp) | | isSlo | Boolean | true | 是否打开单点注销功能 | | regLogoutCall | Boolean | false | 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) | | secretKey | String | "" | API 调用签名秘钥 | | isCheckSign | Boolean | true | 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) | 配置示例: ``` yaml # Sa-Token 配置 sa-token: # SSO-相关配置 sso-client: # sso-server 端主机地址 server-url: http://sa-sso-server.com:9000 # 是否打开单点注销功能 is-slo: true ``` ``` properties # sso-server 端主机地址 sa-token.sso-client.server-url=http://sa-sso-server.com:9000 # 是否打开单点注销功能 sa-token.sso-client.is-slo=true ``` #### 3.3、SaSsoClientModel 配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | client | String | "" | 当前 Client 名称标识,用于和 ticket 码的互相锁定 | | allowUrl | String | | 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用),参考:[SSO整合:配置域名校验](/sso/sso-check-domain) | | isPush | Boolean | false | 是否接收推送消息 | | isSlo | Boolean | true | 是否打开单点注销功能 | | secretKey | String | "" | API 调用签名秘钥 | | serverUrl | String | null | 配置 Server 端主机总地址,拼接在 `authUrl`、`checkTicketUrl`、`userinfoUrl`、`sloUrl` 属性前面,用以简化各种 url 配置,参考:[详解](/sso/sso-questions?id=问:模式三配置一堆-xxx-url-,有办法简化一下吗?) | | pushUrl | String | /sso/pushC | 配置此 Client 端的推送消息地址 | 配置示例: ``` yml # Sa-Token 配置 sa-token: # SSO-Server 配置 sso-server: # 应用列表:配置接入的应用信息 clients: # 应用 sso-client1 sso-client1: client: sso-client1 allow-url: "*" secret-key: SSO-C1-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client2 sso-client2: client: sso-client2 allow-url: "*" secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` ``` properties # 应用列表:配置接入的应用信息 # 应用 sso-client1 sa-token.sso-server.clients.sso-client1.client=sso-client1 sa-token.sso-server.clients.sso-client1.allow-url=* sa-token.sso-server.clients.sso-client1.secret-key=SSO-C1-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # 应用 sso-client2 sa-token.sso-server.clients.sso-client2.client=sso-client2 sa-token.sso-server.clients.sso-client2.allow-url=* sa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor ``` ### 4、OAuth2.0相关配置 #### 4.1、OAuth2-Server 相关配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | enableAuthorizationCode | Boolean | true | 是否打开模式:授权码(`Authorization Code`) | | enableImplicit | Boolean | true | 是否打开模式:隐藏式(`Implicit`) | | enablePassword | Boolean | true | 是否打开模式:密码式(`Password`) | | enableClientCredentials | Boolean | true | 是否打开模式:凭证式(`Client Credentials`) | | codeTimeout | long | 300 | Code授权码 保存的时间(单位:秒) 默认五分钟 | | accessTokenTimeout | long | 7200 | 全局默认配置所有应用:`Access-Token` 保存的时间(单位:秒)默认两个小时 | | refreshTokenTimeout | long | 2592000 | 全局默认配置所有应用:`Refresh-Token` 保存的时间(单位:秒) 默认30 天 | | clientTokenTimeout | long | 7200 | 全局默认配置所有应用:`Client-Token` 保存的时间(单位:秒) 默认两个小时 | | maxAccessTokenCount | int | 12 | 全局默认配置所有应用:单个应用单个用户最多同时存在的 Access-Token 数量 | | maxRefreshTokenCount | int | 12 | 全局默认配置所有应用:单个应用单个用户最多同时存在的 Refresh-Token 数量 | | maxClientTokenCount | int | 12 | 全局默认配置所有应用:单个应用最多同时存在的 Client-Token 数量 | | isNewRefresh | Boolean | false | 全局默认配置所有应用:是否在每次 `Refresh-Token` 刷新 `Access-Token` 时,产生一个新的 `Refresh-Token` | | openidDigestPrefix | String | openid_default_digest_prefix | 默认 openid 生成算法中使用的摘要前缀 | | unionidDigestPrefix | String | unionid_default_digest_prefix | 默认 unionid 生成算法中使用的摘要前缀 | | higherScope | String | | 指定高级权限,多个用逗号隔开 | | lowerScope | String | | 指定低级权限,多个用逗号隔开 | | mode4ReturnAccessToken | Boolean | false | 模式4是否返回 AccessToken 字段,用于兼容OAuth2标准协议 | | hideStatusField | Boolean | false | 是否在返回值中隐藏默认的状态字段 (code、msg、data) | | oidc | SaOAuth2OidcConfig | new SaOAuth2OidcConfig() | OIDC 相关配置 | | clients | Map | 配置 SaClientModel 列表信息 | 配置示例: ``` yaml # Sa-Token 配置 sa-token: token-name: sa-token-oauth2-server # OAuth2.0 配置 oauth2-server: enable-authorization-code: true enable-implicit: true enable-password: true enable-client-credentials: true ``` ``` properties # Sa-Token 配置 sa-token.token-name=sa-token-oauth2-server # OAuth2.0 配置 sa-token.oauth2-server.enable-authorization-code=true sa-token.oauth2-server.enable-implicit=true sa-token.oauth2-server.enable-password=true sa-token.oauth2-server.enable-client-credentials=true ``` #### 4.2、OIDC 相关配置 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | iss | String | | iss 值,如不配置则自动计算 | | idTokenTimeout | long | 600 | idToken 有效期(单位秒) 默认十分钟 | ``` yaml # Sa-Token 配置 sa-token: oauth2-server: oidc: iss: xxx idTokenTimeout: 600 ``` ``` properties sa-token.oauth2-server.oidc.iss=xxx sa-token.oauth2-server.oidc.idTokenTimeout=600 ``` #### 4.3、SaClientModel属性定义 | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | clientId | String | null | 应用id,应该全局唯一 | | clientSecret | String | null | 应用秘钥 | | contractScopes | List | [] | 应用签约的所有权限 | | allowRedirectUris | List | [] | 应用允许授权的所有URL(可以使用 `*` 号通配符) | | allowGrantTypes | List | [] | 应用允许的所有 `grant_type` | | subjectId | String | null | 应用主体id | | accessTokenTimeout | long | 取全局配置 (7200) | 此应用`Access-Token` 保存的时间(单位:秒) [默认取全局配置] | | refreshTokenTimeout | long | 取全局配置 (2592000)| 此应用`Refresh-Token` 保存的时间(单位:秒) [默认取全局配置] | | clientTokenTimeout | Boolean | 取全局配置 (7200)| 此应用`Client-Token` 保存的时间(单位:秒) [默认取全局配置] | | maxAccessTokenCount | long | 取全局配置 (12)| 此应用单个用户最多同时存在的 Access-Token 数量 | | maxRefreshTokenCount | long | 取全局配置 (12)| 此应用单个用户最多同时存在的 Refresh-Token 数量 | | maxClientTokenCount | long | 取全局配置 (12)| 此应用最多同时存在的 Client-Token 数量 | | isNewRefresh | Boolean | 取全局配置 | 单独配置此 Client:是否在每次 `Refresh-Token` 刷新 `Access-Token` 时,产生一个新的 Refresh-Token [ 默认取全局配置 ] | | isAutoConfirm | Boolean | false | 是否允许此应用自动确认授权 (高危配置,禁止向不被信任的第三方开启此选项) | ### 5、部分配置项详解 对部分配置项做一下详解 #### 配置项详解:maxLoginCount 配置含义:同一账号最大登录数量。 在配置 `isConcurrent=true`, `isShare=false` 时,Sa-Token 将允许同一账号并发登录,且每次登录都会产生一个新Token, 这些 Token 都会以 `SaTerminalInfo` 的形式记录在其 `Account-Session` 之上,这就造成一个问题: 随着同一账号登录的次数越来越多,SaTerminalInfo 的列表也会越来越大,极端情况下,列表长度可能达到成百上千以上,严重拖慢数据处理速度, 为此 Sa-Token 对这个 SaTerminalInfo 列表的大小设定一个上限值,也就是 `maxLoginCount`,默认值=12。 假设一个账号的登录数量超过 `maxLoginCount` 后,将会主动注销第一个登录的会话(先进先出),以此保证队列中的有效会话数量始终 `<= maxLoginCount` 值。 #### 配置项详解:tokenSessionCheckLogin 配置含义:获取 `Token-Session` 时是否必须登录 (如果配置为true,会在每次获取 `Token-Session` 时校验是否登录)。 在调用 `StpUtil.login(id)` 登录后, - 调用 `StpUtil.getSession()` 可以获取这个会话的 `Account-Session` 对象。 - 调用 `StpUtil.getTokenSession()` 可以获取这个会话 `Token-Session` 对象。 关于两种 Session 有何区别,可以参考这篇:[Session模型详解](/fun/session-model),此处暂不赘述。 从设计上讲,无论会话是否已经登录,只要前端提供了Token,我们就可以找到这个 Token 的专属 `Token-Session` 对象,**这非常灵活但不安全**, 因为前端提交的 Token 可能是任意伪造的。 为了解决这个问题,`StpUtil.getTokenSession()` 方法在获取 `Token-Session` 时,会率先检测一下这个 Token 是否是一个有效Token: - 如果是有效Token,正常返回 `Token-Session` 对象 - 如果是无效Token,则抛出异常。 这样就保证了伪造的 Token 是无法获取 `Token-Session` 对象的。 但是 —— 有的场景下我们又确实需要在登录之前就使用 Token-Session 对象,这时候就把配置项 `tokenSessionCheckLogin` 值改为 `false` 即可。 #### 配置项详解:isHttp 配置含义:是否打开单点登录模式三。 - 此配置项为 false 时,代表使用SSO模式二:使用 Redis 校验 ticket 值、删除 Redis 数据做到单点注销、使用 Redis 同步 Userinfo 数据。 - 此配置项为 true 时,代表使用SSO模式三:使用 Http 请求校验 ticket 值、使用 Http 请求做到单点注销、使用 Http 请求同步 Userinfo 数据。 --- 本章代码示例:Sa-Token 框架配置 —— [ application.yml ] 本章小练习:Sa-Token 基础 - 框架配置,章节测试 ================================================ FILE: sa-token-doc/use/dao-extend.md ================================================ # 持久层扩展 --- Sa-token默认将会话数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:重启后数据会丢失,无法在集群模式下共享数据 为此,Sa-Token将数据持久操作全部抽象到 `SaTokenDao` 接口中,保证大家对框架进行灵活扩展,比如我们可以将会话数据存储在 `Redis`、`Memcached`等专业的缓存中间件中,做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性 除了框架内部对`SaTokenDao`提供的基于内存的默认实现,官方仓库还提供了以下扩展方案:
    ### 1. Sa-Token 整合 Redis (使用jdk默认序列化方式) ``` xml cn.dev33 sa-token-redis ${sa.top.version} ``` 优点:兼容性好,缺点:Session序列化后基本不可读,对开发者来讲等同于乱码 ### 2. Sa-Token 整合 RedisTemplate ``` xml cn.dev33 sa-token-redis-template ${sa.top.version} ``` 优点:Session 序列化后可读性强(默认 JSON 格式),可灵活手动修改
    ### 集成Redis请注意: **1. 无论使用哪种序列化方式,你都必须为项目提供一个Redis实例化方案,例如:** ``` xml org.apache.commons commons-pool2 ``` **2. 引入了依赖,我还需要为Redis配置连接信息吗?**
    需要!只有项目初始化了正确的Redis实例,`Sa-Token`才可以使用Redis进行数据持久化,参考以下`yml配置`: ``` java # 端口 spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) # password: # 连接超时时间(毫秒) timeout: 1000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ``` **3. 集成Redis后,是我额外手动保存数据,还是框架自动保存?**
    框架自动保存。集成`Redis`只需要引入对应的`pom依赖`即可,框架所有上层API保持不变

    更多框架的集成方案正在更新中... (欢迎大家提交pr) ================================================ FILE: sa-token-doc/use/jur-auth.md ================================================ # 权限认证 --- ### 1、设计思路 权限认证的最终目的在于:规定哪些用户可以访问哪些 接口/页面/资源。 例如对于同一个页面: - 管理员账号访问:正常返回数据。 - 普通账号访问:权限不足,拒绝访问 那么框架是如何判断,一个账号是否有权限访问某个接口的呢? 从底层数据的角度来讲,**每个账号都会拥有一组权限码集合,框架要做的就是校验这个集合中是否包含指定的权限码。** - 有,就让你通过。 - 没有?那么禁止访问! 所以现在问题的核心就是两个: 1. 如何定义一个账号所拥有的权限码集合? 2. 本次操作需要校验的权限码是哪个? ### 2、获取当前账号权限码集合 在进行具体的权限校验之前,你需要实现 `StpInterface`接口,告诉框架指定账号拥有的权限码集合是哪些: ``` java /** * 自定义权限加载接口实现类 */ @Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List list = new ArrayList(); list.add("101"); list.add("user.add"); list.add("user.update"); list.add("user.get"); // list.add("user.delete"); list.add("art.*"); return list; } /** * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) */ @Override public List getRoleList(Object loginId, String loginType) { // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ``` **参数解释:** - loginId:账号id,即你在调用 `StpUtil.login(id)` 时写入的`唯一标识`值。 - loginType:账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。 可参考代码:[码云:StpInterfaceImpl.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java) > [!WARNING| label:有同学会产生疑问:我实现了此接口,但是程序启动时好像并没有执行,是不是我写错了?] > 答:不执行是正常现象,程序启动时不会执行这个接口的方法,在每次调用鉴权代码时,才会执行到此。 ### 3、权限校验 然后就可以用以下 api 来鉴权了 ``` java // 获取:当前账号所拥有的权限集合 StpUtil.getPermissionList(); // 判断:当前账号是否含有指定权限, 返回 true 或 false StpUtil.hasPermission("user.add"); // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException StpUtil.checkPermission("user.add"); // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过] StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); // 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] StpUtil.checkPermissionOr("user.add", "user.delete", "user.get"); ``` 扩展:`NotPermissionException` 异常对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常 ### 4、角色校验 在 Sa-Token 中,角色和权限可以分开独立验证 ``` java // 获取:当前账号所拥有的角色集合 StpUtil.getRoleList(); // 判断:当前账号是否拥有指定角色, 返回 true 或 false StpUtil.hasRole("super-admin"); // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException StpUtil.checkRole("super-admin"); // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过] StpUtil.checkRoleAnd("super-admin", "shop-admin"); // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] StpUtil.checkRoleOr("super-admin", "shop-admin"); ``` 扩展:NotRoleException 异常对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常 ### 5、拦截全局异常 有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?**当然不可以!** 你可以创建一个全局异常拦截器,统一返回给前端的格式,参考: ``` java @RestControllerAdvice public class GlobalExceptionHandler { // 全局异常拦截 @ExceptionHandler public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } } ``` 可参考:[码云:GlobalException.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java) ### 6、权限通配符 Sa-Token允许你根据通配符指定**泛权限**,例如当一个账号拥有`art.*`的权限时,`art.add`、`art.delete`、`art.update`都将匹配通过 ``` java // 当拥有 art.* 权限时 StpUtil.hasPermission("art.add"); // true StpUtil.hasPermission("art.update"); // true StpUtil.hasPermission("goods.add"); // false // 当拥有 *.delete 权限时 StpUtil.hasPermission("art.delete"); // true StpUtil.hasPermission("user.delete"); // true StpUtil.hasPermission("user.update"); // false // 当拥有 *.js 权限时 StpUtil.hasPermission("index.js"); // true StpUtil.hasPermission("index.css"); // false StpUtil.hasPermission("index.html"); // false ``` > [!WARNING| label:上帝权限] > 当一个账号拥有 `"*"` 权限时,他可以验证通过任何权限码 (角色认证同理) ### 7、如何把权限精确到按钮级? 权限精确到按钮级的意思就是指:**权限范围可以控制到页面上的每一个按钮是否显示**。 思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断。 如果是前后端一体项目,可以参考:[Thymeleaf 标签方言](/plugin/thymeleaf-extend),如果是前后端分离项目,则: 1. 在登录时,把当前账号拥有的所有权限码一次性返回给前端。 2. 前端将权限码集合保存在`localStorage`或其它全局状态管理对象中。 3. 在需要权限控制的按钮上,使用 js 进行逻辑判断,例如在`Vue`框架中我们可以使用如下写法: ``` js // `arr`是当前用户拥有的权限码数组 // `user.delete`是显示按钮需要拥有的权限码 // `删除按钮`是用户拥有权限码才可以看到的内容。

    ``` 以上写法只为提供一个参考示例,不同框架有不同写法,大家可根据项目技术栈灵活封装进行调用。 > [!ATTENTION| label:前端有了鉴权后端还需要鉴权吗?] > **需要!**
    > 前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全:**无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!** --- 本章代码示例:Sa-Token 权限认证 —— [ JurAuthController.java ] 本章小练习:Sa-Token 基础 - 权限认证,章节测试 ================================================ FILE: sa-token-doc/use/kick.md ================================================ # 踢人下线 所谓踢人下线,核心操作就是找到指定 `loginId` 对应的 `Token`,并设置其失效。 踢下线 --- ### 1、强制注销 ``` java StpUtil.logout(10001); // 强制指定账号注销下线 StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线 StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线 ``` ### 2、踢人下线 ``` java StpUtil.kickout(10001); // 将指定账号踢下线 StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线 StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线 ``` 强制注销 和 踢人下线 的区别在于: - 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。 - 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。 ### 3、顶人下线 “顶人下线” 操作发生在框架登录时顶退旧登录设备,属于框架内部操作,一般情形下你不会调用到此 API: ``` java StpUtil.replaced(10001); // 将指定账号顶下线 StpUtil.replaced(10001, "PC"); // 将指定账号指定端顶下线 StpUtil.replacedByTokenValue("token"); // 将指定 Token 顶下线 ``` --- 本章代码示例:Sa-Token 踢人下线 —— [ KickoutController.java ] 本章小练习:Sa-Token 基础 - 踢人下线,章节测试 ================================================ FILE: sa-token-doc/use/login-auth.md ================================================ # 登录认证 --- ### 1、开始登录 一个完整的登录认证包含哪些步骤?让我们代入用户视角:在打开 网站/APP 后,用户的操作流程大致可以概括为: 1. 打开 网站/APP,进入登录页。 2. 输入 账号+密码 进行登录。 3. 进入首页,进行业务相关操作。 4. 注销登录,关闭 网站/APP。 在整个流程中,Sa-Token 负责哪些部分呢? 下图可以帮助你理解: 如上图所示:**无论用户采用何种登录方式,本质上都是通过提交一定的认证信息,使系统可以定位到 Ta 的唯一标识 —— userId**。 当我们拿到 userId 后,便可以调用框架提供的 API 进行登录: ``` java // 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等 StpUtil.login(Object userId); ``` 只此一句代码,便可以使会话登录成功。实际上,Sa-Token 在背后做了大量的工作,包括但不限于: 1. 检查此账号是否之前已有登录; 2. 为账号生成 Token 凭证与 Session 会话; 3. 记录 Token 活跃时间; 4. 通知全局侦听器,xx 账号登录成功; 5. 检查此账号登录数量是否已达上限; 6. 将 Token 注入到请求上下文; 7. 等等其它工作…… 你暂时不需要完整了解完整过程,你只需要记住关键一点:**Sa-Token 为这个账号创建了一个 token 凭证,且通过 Cookie 上下文返回给了前端**。 所以一般情况下,我们的登录接口代码,会大致类似如下: ``` java // 会话登录接口 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 第一步:比对前端提交的账号名称、密码 if("zhang".equals(name) && "123456".equals(pwd)) { // 第二步:根据账号id,进行登录 StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } ``` 如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:**此处仅仅做了会话登录,但并没有主动向前端返回 token 信息。** 是因为不需要吗?严格来讲是需要的,只不过 `StpUtil.login(id)` 方法利用了 Cookie 自动注入的特性,省略了你手写返回 token 的代码。 > [!TIP| label:Cookie 是什么?] > 如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点: > > - Cookie 可以从后端控制往浏览器中写入 token 值。 > - Cookie 会在前端每次发起请求时自动提交 token 值。 > > 因此,在 Cookie 功能的加持下,我们可以仅靠 `StpUtil.login(id)` 一句代码就完成登录认证。 > > 在浏览器打开 f12 控制台,即可看到被注入的 Cookie 值: > > ### 2、校验是否登录 对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验: - 如果校验通过,则:正常返回数据。 - 如果校验未通过,则:抛出异常,告知其需要先进行登录。 使用以下方法判断当前会话是否已登录: ``` java // 判断当前会话是否已经登录,返回 true=已登录,false=未登录 StpUtil.isLogin(); // 检验当前会话是否已经登录, 如果已登录代码会安全通过,未登录则抛出异常:`NotLoginException` StpUtil.checkLogin(); ``` 例如我们可以在接口内,根据是否登录返回不同的信息: ``` java // 获取我的资料信息 @RequestMapping("myInfo") public String myInfo() { if( StpUtil.isLogin() ) { // ... return "我的资料信息..."; } else { return "未登录,请先登录"; } } ``` 或者在未登录时直接抛出全局异常: ``` java // 获取我的资料信息 @RequestMapping("myInfo") public String myInfo() { StpUtil.checkLogin(); // 如果当前未登录,这句代码会直接抛出异常 `NotLoginException` return "我的资料信息"; } ``` 配合全局异常处理器,统一返回固定格式数据到前端: ``` java @RestControllerAdvice public class GlobalException { @ExceptionHandler(NotLoginException.class) public SaResult handlerException(NotLoginException e) { return SaResult.error(e.getMessage()); } } ``` 异常 `NotLoginException` 代表当前会话暂未登录,可能的原因有很多: - 前端没有提交 token。 - 前端提交的 token 是无效的。 - 前端提交的 token 已经过期。 - …… 可参照此篇:[未登录场景值](/fun/not-login-scene),了解如何获取未登录的场景值。 ### 3、会话查询 如果你想要获取当前登录的是谁: ``` java // 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException` StpUtil.getLoginId(); // 类似查询API还有: StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型 StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型 StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型 // ---------- 以下方法可以指定未登录情形下返回的默认值 ---------- // 获取当前会话账号id, 如果未登录,则返回 null StpUtil.getLoginIdDefaultNull(); // 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型) StpUtil.getLoginId(T defaultValue); ``` ### 4、token 查询 ``` java // 获取当前会话的 token 值 StpUtil.getTokenValue(); // 获取当前`StpLogic`的 token 名称 StpUtil.getTokenName(); // 获取指定 token 对应的账号id,如果未登录,则返回 null StpUtil.getLoginIdByToken(String tokenValue); // 获取当前会话剩余有效期(单位:s,返回-1代表永久有效) StpUtil.getTokenTimeout(); // 获取当前会话的 token 信息参数 StpUtil.getTokenInfo(); ``` 有关`TokenInfo`参数详解,请参考:[TokenInfo参数详解](/fun/token-info) ### 5、会话注销 ``` java // 当前会话注销登录 StpUtil.logout(); ``` ### 6、来个小测试,加深一下理解 新建 `LoginController`,复制或手动敲出以下代码 ``` java /** * 登录测试 */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ``` --- 本章代码示例:Sa-Token 登录认证 —— [ LoginAuthController.java ] 本章小练习:Sa-Token 基础 - 登录认证,章节测试 ================================================ FILE: sa-token-doc/use/route-check.md ================================================ # 路由拦截鉴权 假设我们有如下需求:*项目中所有接口均需要登录校验,只有 “登录接口” 本身对外开放*。 如果给每个接口都手动加上注解鉴权,将会是一件比较麻烦的事情,这时候使用拦截器鉴权模式将大大降低我们的代码量。 如上图所示,拦截器将拦截除登录以外的所以请求,并进行一道前置审核决定是否通过。 --- ### 1、注册 Sa-Token 路由拦截器 以`SpringBoot2.0`为例,新建配置类`SaTokenConfigure.java` ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。 registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())) .addPathPatterns("/**") .excludePathPatterns("/user/doLogin"); } } ``` 以上代码,我们注册了一个基于 `StpUtil.checkLogin()` 的登录校验拦截器,并且排除了`/user/doLogin`接口用来开放登录(除了`/user/doLogin`以外的所有接口都需要登录才能访问)。 > [!WARNING| label:版本升级] > `SaInterceptor` 是新版本提供的拦截器,点此 [查看旧版本代码迁移示例](https://blog.csdn.net/shengzhang_/article/details/126458949)。 ### 2、校验函数详解 自定义认证规则:`new SaInterceptor(handle -> StpUtil.checkLogin())` 是最简单的写法,代表只进行登录校验功能。 我们可以往构造函数塞一个完整的 lambda 函数,来定义详细的校验规则,例如: ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器,定义详细认证规则 registry.addInterceptor(new SaInterceptor(handler -> { // 指定一条 match 规则 SaRouter .match("/**") // 拦截的 path 列表,可以写多个 */ .notMatch("/user/doLogin") // 排除掉的 path 列表,可以写多个 .check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式 // 根据路由划分模块,不同模块不同鉴权 SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); })).addPathPatterns("/**"); } } ``` SaRouter.match() 匹配函数有两个参数: - 参数一:要匹配的path路由。 - 参数二:要执行的校验函数。 在校验函数内不只可以使用 `StpUtil.checkPermission("xxx")` 进行权限校验,你还可以写任意代码,例如: ``` java @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 的拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册路由拦截器,自定义认证规则 registry.addInterceptor(new SaInterceptor(handler -> { // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin()); // 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证 SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin")); // 权限校验 -- 不同模块校验不同权限 SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); // 甚至你可以随意的写一个打印语句 SaRouter.match("/**", r -> System.out.println("----啦啦啦----")); // 连缀写法 SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----")); })).addPathPatterns("/**"); } } ``` ### 3、匹配特征详解 除了上述示例的 path 路由匹配,还可以根据很多其它特征进行匹配,以下是所有可匹配的特征: ``` java // 基础写法样例:匹配一个path,执行一个校验函数 SaRouter.match("/user/**").check(r -> StpUtil.checkLogin()); // 根据 path 路由匹配 ——— 支持写多个path,支持写 restful 风格路由 // 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法 SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ ); // 根据 path 路由排除匹配 // 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法 SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check( /* 要执行的校验函数 */ ); // 根据请求类型匹配 SaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ ); // 根据一个 boolean 条件进行匹配 SaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ ); // 根据一个返回 boolean 结果的lambda表达式匹配 SaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ ); // 多个条件一起使用 // 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头 SaRouter.match(SaHttpMethod.GET).match("/user/**").check( /* 要执行的校验函数 */ ); // 可以无限连缀下去 // 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css SaRouter .match(SaHttpMethod.GET) .match("/admin/**") .match("/**/send/**") .notMatch("/**/*.js") .notMatch("/**/*.css") // .... .check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ ); ``` ### 4、提前退出匹配链 使用 `SaRouter.stop()` 可以提前退出匹配链,例: ``` java registry.addInterceptor(new SaInterceptor(handler -> { SaRouter.match("/**").check(r -> System.out.println("进入1")); SaRouter.match("/**").check(r -> System.out.println("进入2")).stop(); SaRouter.match("/**").check(r -> System.out.println("进入3")); SaRouter.match("/**").check(r -> System.out.println("进入4")); SaRouter.match("/**").check(r -> System.out.println("进入5")); })).addPathPatterns("/**"); ``` 如上示例,代码运行至第2条匹配链时,会在stop函数处提前退出整个匹配函数,从而忽略掉剩余的所有match匹配 除了`stop()`函数,`SaRouter`还提供了 `back()` 函数,用于:停止匹配,结束执行,直接向前端返回结果 ``` java // 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端 SaRouter.match("/user/back").back("要返回到前端的内容"); ``` stop() 与 back() 函数的区别在于: - `SaRouter.stop()` 会停止匹配,进入Controller。 - `SaRouter.back()` 会停止匹配,直接返回结果到前端。 ### 5、使用free打开一个独立的作用域 ``` java // 进入 free 独立作用域 SaRouter.match("/**").free(r -> { SaRouter.match("/a/**").check(/* --- */); SaRouter.match("/b/**").check(/* --- */).stop(); SaRouter.match("/c/**").check(/* --- */); }); // 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 SaRouter.match("/**").check(/* --- */); ``` free() 的作用是:打开一个独立的作用域,使内部的 stop() 不再一次性跳出整个 Auth 函数,而是仅仅跳出当前 free 作用域。 ### 6、使用注解忽略掉路由拦截校验 我们可以使用 `@SaIgnore` 注解,忽略掉路由拦截认证: 1、先配置好了拦截规则: ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handler -> { // 根据路由划分模块,不同模块不同鉴权 SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); // ... })).addPathPatterns("/**"); } ``` 2、然后在 `Controller` 里又添加了忽略校验的注解 ``` java @SaIgnore @RequestMapping("/user/getList") public SaResult getList() { System.out.println("------------ 访问进来方法"); return SaResult.ok(); } ``` 请求将会跳过拦截器的校验,直接进入 Controller 的方法中。 > [!WARNING| label:注意点] > 注解 `@SaIgnore` 的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。 ### 7、关闭注解校验 `SaInterceptor` 只要注册到项目中,默认就会打开注解校验,如果要关闭此能力,需要指定 `isAnnotation` 为 false: ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor( new SaInterceptor(handle -> { SaRouter.match("/**").check(r -> StpUtil.checkLogin()); }).isAnnotation(false) // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了 ).addPathPatterns("/**"); } ``` 你也可以使用 `setBeforeAuth` 注册认证前置函数: ``` java @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { System.out.println(1); }) .setBeforeAuth(handle -> { System.out.println(2); }) ).addPathPatterns("/**"); } ``` 如上代码,先执行 2,再执行注解鉴权,再执行 1,如果 beforeAuth 里包含 `SaRouter.stop()` 将跳过后续的注解鉴权和 auth 认证环节。 --- 本章代码示例:Sa-Token 路由拦截鉴权 —— [ SaTokenConfigure.java ] 本章小练习:Sa-Token 基础 - 路由拦截鉴权,章节测试 ================================================ FILE: sa-token-doc/use/session.md ================================================ # Session会话 --- ### 1、Session是什么? Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能,例如: ``` java // 在登录时缓存 user 对象 StpUtil.getSession().set("user", user); // 然后我们就可以在任意处使用这个 user 对象 SysUser user = (SysUser) StpUtil.getSession().get("user"); ``` 在 Sa-Token 中,Session 分为三种,分别是: - `Account-Session`: 指的是框架为每个 账号id 分配的 Session - `Token-Session`: 指的是框架为每个 token 分配的 Session - `Custom-Session`: 指的是以一个 特定的值 作为SessionId,来分配的 Session > [!TIP| style:callout] > 有关 Account-Session 与 Token-Session 的详细区别,可参考:[Session模型详解](/fun/session-model) ### 2、Account-Session 有关 账号-Session 的 API 如下: ``` java // 获取当前账号 id 的 Account-Session (必须是登录后才能调用) StpUtil.getSession(); // 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回 StpUtil.getSession(true); // 获取账号 id 为 10001 的 Account-Session StpUtil.getSessionByLoginId(10001); // 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回 StpUtil.getSessionByLoginId(10001, true); // 获取 SessionId 为 xxxx-xxxx 的 Account-Session, 在 Session 尚未创建时, 返回 null StpUtil.getSessionBySessionId("xxxx-xxxx"); ``` ### 3、Token-Session 有关 令牌-Session 的 API 如下: ``` java // 获取当前 Token 的 Token-Session 对象 StpUtil.getTokenSession(); // 获取指定 Token 的 Token-Session 对象 StpUtil.getTokenSessionByToken(token); ``` ### 4、Custom-Session 自定义 Session 指的是以一个`特定的值`作为 SessionId 来分配的`Session`, 借助自定义Session,你可以为系统中的任意元素分配相应的session
    例如以商品 id 作为 key 为每个商品分配一个Session,以便于缓存和商品相关的数据,其相关API如下: ``` java // 查询指定key的Session是否存在 SaSessionCustomUtil.isExists("goods-10001"); // 获取指定key的Session,如果没有,则新建并返回 SaSessionCustomUtil.getSessionById("goods-10001"); // 获取指定key的Session,如果没有,第二个参数决定是否新建并返回 SaSessionCustomUtil.getSessionById("goods-10001", false); // 删除指定key的Session SaSessionCustomUtil.deleteSessionById("goods-10001"); ``` ### 5、在 Session 上存取值 以上三种 Session 均为框架设计概念上的区分,实际上在获取它们时,返回的都是 SaSession 对象,你可以使用以下 API 在 SaSession 对象上存取值: ``` java // 写值 session.set("name", "zhang"); // 写值 (只有在此key原本无值的时候才会写入) session.setDefaultValue("name", "zhang"); // 取值 session.get("name"); // 取值 (指定默认值) session.get("name", ""); // 取值 (若无值则执行参数方法, 之后将结果保存到此键名下,并返回此结果 若有值则直接返回, 无需执行参数方法) session.get("name", () -> { return ...; }); // ---------- 数据类型转换: ---------- session.getInt("age"); // 取值 (转int类型) session.getLong("age"); // 取值 (转long类型) session.getString("name"); // 取值 (转String类型) session.getDouble("result"); // 取值 (转double类型) session.getFloat("result"); // 取值 (转float类型) session.getModel("key", Student.class); // 取值 (指定转换类型) session.getModel("key", Student.class, ); // 取值 (指定转换类型, 并指定值为Null时返回的默认值) // 是否含有某个key (返回 true 或 false) session.has("key"); // 删值 session.delete('name'); // 清空所有值 session.clear(); // 获取此 Session 的所有key (返回Set) session.keys(); ``` ### 6、其它操作 ``` java // 返回此 Session 的id session.getId(); // 返回此 Session 的创建时间 (时间戳) session.getCreateTime(); // 返回此 Session 会话上的底层数据对象(如果更新map里的值,请调用session.update()方法避免产生脏数据) session.getDataMap(); // 将这个 Session 从持久库更新一下 session.update(); // 注销此 Session 会话 (从持久库删除此Session) session.logout(); ``` ### 7、避免与 HttpSession 混淆使用 经常有同学会把 `SaSession` 与 `HttpSession` 进行混淆,例如: ``` java @PostMapping("/resetPoints") public void reset(HttpSession session) { // 在 HttpSession 上写入一个值 session.setAttribute("name", 66); // 在 SaSession 进行取值 System.out.println(StpUtil.getSession().get("name")); // 输出null } ``` **要点:** 1. `SaSession` 与 `HttpSession` 没有任何关系,在`HttpSession`上写入的值,在`SaSession`中无法取出 2. `HttpSession`并未被框架接管,在使用Sa-Token时,请在任何情况下均使用`SaSession`,不要使用`HttpSession` ### 8、未登录场景下获取 Token-Session 默认场景下,只有登录后才能通过 `StpUtil.getTokenSession()` 获取 `Token-Session`。 如果想要在未登录场景下获取 Token-Session ,有两种方法: - 方法一:将全局配置项 `tokenSessionCheckLogin` 改为 false,详见:[框架配置](/use/config?id=所有可配置项) - 方法二:使用匿名 Token-Session ``` java // 获取当前 Token 的匿名 Token-Session (可在未登录情况下使用的 Token-Session) StpUtil.getAnonTokenSession(); ``` 注意点:如果前端没有提交 Token ,或者提交的 Token 是一个无效 Token 的话,框架将不会根据此 Token 创建 `Token-Session` 对象, 而是随机一个新的 Token 值来创建 `Token-Session` 对象,此 Token 值可以通过 `StpUtil.getTokenValue()` 获取到。 --- 本章代码示例:Sa-Token Session 会话 —— [ SaSessionController.java ] 本章小练习:Sa-Token 基础 - Session 会话,章节测试 ================================================ FILE: sa-token-plugin/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-parent ${revision} ../pom.xml pom sa-token-plugin sa-token-plugin sa-token plugins sa-token-jackson sa-token-jackson3 sa-token-fastjson sa-token-fastjson2 sa-token-snack3 sa-token-snack4 sa-token-hutool-timed-cache sa-token-caffeine sa-token-thymeleaf sa-token-freemarker sa-token-dubbo sa-token-dubbo3 sa-token-temp-jwt sa-token-jwt sa-token-sso sa-token-oauth2 sa-token-apikey sa-token-sign sa-token-redisson sa-token-redisx sa-token-serializer-features sa-token-forest sa-token-okhttps sa-token-redis-template sa-token-redis-template-jdk-serializer sa-token-redis-jackson sa-token-alone-redis sa-token-alone-redis-by-spring-boot4 sa-token-spring-aop sa-token-spring-el sa-token-grpc sa-token-quick-login sa-token-redisson-spring-boot-starter cn.dev33 sa-token-spring-boot2-dependencies ${revision} pom import ================================================ FILE: sa-token-plugin/sa-token-alone-redis/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-alone-redis sa-token-alone-redis sa-token-alone-redis cn.dev33 sa-token-redis-template true cn.dev33 sa-token-redis-template-jdk-serializer true org.apache.commons commons-pool2 true ================================================ FILE: sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.alone; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate; import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer; import cn.dev33.satoken.exception.SaTokenException; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; import java.util.List; import java.util.stream.Collectors; /** * 为 SaToken 单独设置 Redis 连接信息,使权限缓存与业务缓存分离 * *

    * 使用方式:在引入 sa-token redis 集成相关包的前提下,继续引入当前依赖

    * 注意事项:目前本依赖仅对以下插件有 Redis 分离效果:
    * sa-token-redis-template
    * sa-token-redis-template-jdk-serializer
    *

    * * * @author click33 * @since 1.21.0 */ @Configuration public class SaAloneRedisInject implements EnvironmentAware{ /** * 配置信息的前缀 */ public static final String ALONE_PREFIX = "sa-token.alone-redis"; /** * Sa-Token 持久层接口 */ @Autowired(required = false) public SaTokenDao saTokenDao; /** * 开始注入 */ @Override public void setEnvironment(Environment environment) { try { // 如果 saTokenDao 为空或者为默认实现,则不进行任何操作 if(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) { return; } // ------------------- 开始注入 // 获取cfg对象,解析开发者配置的 sa-token.alone-redis 相关信息 RedisProperties cfg = Binder.get(environment).bind(ALONE_PREFIX, RedisProperties.class).get(); // 1. Redis配置 RedisConfiguration redisAloneConfig; String pattern = environment.getProperty(ALONE_PREFIX + ".pattern", "single"); if (pattern.equals("single")) { // 单体模式 RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); redisConfig.setHostName(cfg.getHost()); redisConfig.setPort(cfg.getPort()); redisConfig.setDatabase(cfg.getDatabase()); redisConfig.setPassword(RedisPassword.of(cfg.getPassword())); redisConfig.setDatabase(cfg.getDatabase()); // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 try { redisConfig.setUsername(cfg.getUsername()); } catch (NoSuchMethodError e){ System.err.println(e.getMessage()); } redisAloneConfig = redisConfig; } else if (pattern.equals("cluster")){ // 普通集群模式 RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(); // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 try { redisClusterConfig.setUsername(cfg.getUsername()); } catch (NoSuchMethodError e){ System.err.println(e.getMessage()); } redisClusterConfig.setPassword(RedisPassword.of(cfg.getPassword())); RedisProperties.Cluster cluster = cfg.getCluster(); List serverList = cluster.getNodes().stream().map(node -> { String[] ipAndPort = node.split(":"); return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); }).collect(Collectors.toList()); redisClusterConfig.setClusterNodes(serverList); redisClusterConfig.setMaxRedirects(cluster.getMaxRedirects()); redisAloneConfig = redisClusterConfig; } else if (pattern.equals("sentinel")) { // 哨兵集群模式 RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); redisSentinelConfiguration.setDatabase(cfg.getDatabase()); // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 try { redisSentinelConfiguration.setUsername(cfg.getUsername()); } catch (NoSuchMethodError e){ System.err.println(e.getMessage()); } redisSentinelConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); RedisProperties.Sentinel sentinel = cfg.getSentinel(); redisSentinelConfiguration.setMaster(sentinel.getMaster()); redisSentinelConfiguration.setSentinelPassword(sentinel.getPassword()); List serverList = sentinel.getNodes().stream().map(node -> { String[] ipAndPort = node.split(":"); return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); }).collect(Collectors.toList()); redisSentinelConfiguration.setSentinels(serverList); redisAloneConfig = redisSentinelConfiguration; } else if (pattern.equals("socket")) { // socket 连接单体 Redis RedisSocketConfiguration redisSocketConfiguration = new RedisSocketConfiguration(); redisSocketConfiguration.setDatabase(cfg.getDatabase()); // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 try { redisSocketConfiguration.setUsername(cfg.getUsername()); } catch (NoSuchMethodError e){ System.err.println(e.getMessage()); } redisSocketConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); String socket = environment.getProperty(ALONE_PREFIX + ".socket", ""); redisSocketConfiguration.setSocket(socket); redisAloneConfig = redisSocketConfiguration; } else if (pattern.equals("aws")) { // AWS ElastiCache // AWS Redis 远程主机地址: String hoseName = "****.***.****.****.cache.amazonaws.com"; String hostName = cfg.getHost(); int port = cfg.getPort(); RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(hostName, port); redisStaticMasterReplicaConfiguration.setDatabase(cfg.getDatabase()); // 低版本没有 username 属性,捕获异常给个提示即可,无需退出程序 try { redisStaticMasterReplicaConfiguration.setUsername(cfg.getUsername()); } catch (NoSuchMethodError e){ System.err.println(e.getMessage()); } redisStaticMasterReplicaConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); redisAloneConfig = redisStaticMasterReplicaConfiguration; } else { // 模式无法识别 throw new SaTokenException("SaToken 无法识别 Alone-Redis 配置的模式: " + pattern); } // 2. 连接池配置 GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); // pool配置 Lettuce lettuce = cfg.getLettuce(); if(lettuce.getPool() != null) { RedisProperties.Pool pool = cfg.getLettuce().getPool(); // 连接池最大连接数 poolConfig.setMaxTotal(pool.getMaxActive()); // 连接池中的最大空闲连接 poolConfig.setMaxIdle(pool.getMaxIdle()); // 连接池中的最小空闲连接 poolConfig.setMinIdle(pool.getMinIdle()); // 连接池最大阻塞等待时间(使用负值表示没有限制) poolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis()); } LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder(); // timeout if(cfg.getTimeout() != null) { builder.commandTimeout(cfg.getTimeout()); } // shutdownTimeout if(lettuce.getShutdownTimeout() != null) { builder.shutdownTimeout(lettuce.getShutdownTimeout()); } // 创建Factory对象 LettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build(); LettuceConnectionFactory factory = new LettuceConnectionFactory(redisAloneConfig, clientConfig); factory.afterPropertiesSet(); // 3. 开始初始化 SaTokenDao ,此处需要依次判断开发者引入的是哪个 redis 库 // 如果开发者引入的是:sa-token-redis-template-jdk-serializer try { Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer"); SaTokenDaoForRedisTemplateUseJdkSerializer dao = (SaTokenDaoForRedisTemplateUseJdkSerializer)saTokenDao; dao.isInit = false; dao.init(factory); return; } catch (ClassNotFoundException ignored) { } // 如果开发者引入的是:sa-token-redis-template try { Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate"); SaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate)saTokenDao; dao.isInit = false; dao.init(factory); return; } catch (ClassNotFoundException ignored) { } // 至此,说明开发者一个 redis 插件也没引入,或者引入的 redis 插件不在 sa-token-alone-redis 的支持范围内 throw new SaTokenException("未引入 sa-token-redis-xxx 相关插件,或引入的插件不在 Alone-Redis 支持范围内"); } catch (Exception e) { e.printStackTrace(); } } /** * 骗过编辑器,增加配置文件代码提示 * @return 配置对象 */ @ConfigurationProperties(prefix = ALONE_PREFIX) public RedisProperties getSaAloneRedisConfig() { return new RedisProperties(); } } ================================================ FILE: sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.dao.alone.SaAloneRedisInject ================================================ FILE: sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.alone.SaAloneRedisInject ================================================ FILE: sa-token-plugin/sa-token-alone-redis-by-spring-boot4/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-alone-redis-by-spring-boot4 sa-token-alone-redis-by-spring-boot4 sa-token-alone-redis for Spring Boot 4 org.springframework.boot spring-boot-starter-data-redis cn.dev33 sa-token-redis-template true cn.dev33 sa-token-redis-template-jdk-serializer true org.apache.commons commons-pool2 true cn.dev33 sa-token-spring-boot4-dependencies ${revision} pom import ================================================ FILE: sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao.alone; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; import cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate; import cn.dev33.satoken.exception.SaTokenException; import jakarta.annotation.PostConstruct; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; import java.util.List; import java.util.stream.Collectors; /** * 为 SaToken 单独设置 Redis 连接信息,使权限缓存与业务缓存分离 (springboot4 版本专用) * *

    * 使用方式:在引入 sa-token redis 集成相关包的前提下,继续引入当前依赖

    * 注意事项:目前本依赖仅对以下插件有 Redis 分离效果:
    * sa-token-redis-template
    * sa-token-redis-template-jdk-serializer
    *

    * * * @author click33 * @since 1.45.0 */ @Configuration public class SaAloneRedisInject { /** * 配置信息的前缀 */ public static final String ALONE_PREFIX = "sa-token.alone-redis"; /** * Sa-Token 持久层接口 */ private final SaTokenDao saTokenDao; private final Environment environment; public SaAloneRedisInject(SaTokenDao saTokenDao, Environment environment) { this.saTokenDao = saTokenDao; this.environment = environment; } /** * 开始注入 */ @PostConstruct public void init() { try { // 如果 saTokenDao 为空或者为默认实现,则不进行任何操作 if(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) { return; } // ------------------- 开始注入 // 获取cfg对象,解析开发者配置的 sa-token.alone-redis 相关信息 DataRedisProperties cfg = Binder.get(environment).bind(ALONE_PREFIX, DataRedisProperties.class).get(); // 1. Redis配置 RedisConfiguration redisAloneConfig; String pattern = environment.getProperty(ALONE_PREFIX + ".pattern", "single"); if (pattern.equals("single")) { // 单体模式 RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); redisConfig.setHostName(cfg.getHost()); redisConfig.setPort(cfg.getPort()); redisConfig.setDatabase(cfg.getDatabase()); redisConfig.setPassword(RedisPassword.of(cfg.getPassword())); redisConfig.setUsername(cfg.getUsername()); redisAloneConfig = redisConfig; } else if (pattern.equals("cluster")){ // 普通集群模式 RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(); redisClusterConfig.setUsername(cfg.getUsername()); redisClusterConfig.setPassword(RedisPassword.of(cfg.getPassword())); DataRedisProperties.Cluster cluster = cfg.getCluster(); if (cluster == null || cluster.getNodes() == null) { throw new SaTokenException("Alone-Redis 集群模式需要配置 cluster.nodes"); } List serverList = cluster.getNodes().stream().map(node -> { String[] ipAndPort = node.split(":"); return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); }).collect(Collectors.toList()); redisClusterConfig.setClusterNodes(serverList); if (cluster.getMaxRedirects() != null) { redisClusterConfig.setMaxRedirects(cluster.getMaxRedirects()); } redisAloneConfig = redisClusterConfig; } else if (pattern.equals("sentinel")) { // 哨兵集群模式 RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); redisSentinelConfiguration.setDatabase(cfg.getDatabase()); redisSentinelConfiguration.setUsername(cfg.getUsername()); redisSentinelConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); DataRedisProperties.Sentinel sentinel = cfg.getSentinel(); if (sentinel == null || sentinel.getNodes() == null) { throw new SaTokenException("Alone-Redis 哨兵模式需要配置 sentinel.nodes"); } redisSentinelConfiguration.setMaster(sentinel.getMaster()); redisSentinelConfiguration.setSentinelPassword(sentinel.getPassword()); List serverList = sentinel.getNodes().stream().map(node -> { String[] ipAndPort = node.split(":"); return new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1])); }).collect(Collectors.toList()); redisSentinelConfiguration.setSentinels(serverList); redisAloneConfig = redisSentinelConfiguration; } else if (pattern.equals("socket")) { // socket 连接单体 Redis RedisSocketConfiguration redisSocketConfiguration = new RedisSocketConfiguration(); redisSocketConfiguration.setDatabase(cfg.getDatabase()); redisSocketConfiguration.setUsername(cfg.getUsername()); redisSocketConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); String socket = environment.getProperty(ALONE_PREFIX + ".socket", ""); redisSocketConfiguration.setSocket(socket); redisAloneConfig = redisSocketConfiguration; } else if (pattern.equals("aws")) { // AWS ElastiCache // AWS Redis 远程主机地址: String hoseName = "****.***.****.****.cache.amazonaws.com"; String hostName = cfg.getHost(); int port = cfg.getPort(); RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(hostName, port); redisStaticMasterReplicaConfiguration.setDatabase(cfg.getDatabase()); redisStaticMasterReplicaConfiguration.setUsername(cfg.getUsername()); redisStaticMasterReplicaConfiguration.setPassword(RedisPassword.of(cfg.getPassword())); redisAloneConfig = redisStaticMasterReplicaConfiguration; } else { // 模式无法识别 throw new SaTokenException("SaToken 无法识别 Alone-Redis 配置的模式: " + pattern); } // 2. 连接池配置 GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); // pool配置 DataRedisProperties.Lettuce lettuce = cfg.getLettuce(); if(lettuce.getPool() != null) { DataRedisProperties.Pool pool = cfg.getLettuce().getPool(); // 连接池最大连接数 poolConfig.setMaxTotal(pool.getMaxActive()); // 连接池中的最大空闲连接 poolConfig.setMaxIdle(pool.getMaxIdle()); // 连接池中的最小空闲连接 poolConfig.setMinIdle(pool.getMinIdle()); // 连接池最大阻塞等待时间(使用负值表示没有限制) poolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis()); } LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder(); // timeout if(cfg.getTimeout() != null) { builder.commandTimeout(cfg.getTimeout()); } // shutdownTimeout builder.shutdownTimeout(lettuce.getShutdownTimeout()); // 创建Factory对象 LettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build(); LettuceConnectionFactory factory = new LettuceConnectionFactory(redisAloneConfig, clientConfig); factory.afterPropertiesSet(); // 3. 开始初始化 SaTokenDao ,此处需要依次判断开发者引入的是哪个 redis 库 // 如果开发者引入的是:sa-token-redis-template-jdk-serializer 或 sa-token-redis-template try { Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer"); SaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate) saTokenDao; dao.isInit = false; dao.init(factory); return; } catch (ClassNotFoundException ignored) { } try { Class.forName("cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate"); SaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate) saTokenDao; dao.isInit = false; dao.init(factory); return; } catch (ClassNotFoundException ignored) { } // 至此,说明开发者一个 redis 插件也没引入,或者引入的 redis 插件不在 sa-token-alone-redis 的支持范围内 throw new SaTokenException("未引入 sa-token-redis-xxx 相关插件,或引入的插件不在 Alone-Redis 支持范围内"); } catch (Exception e) { e.printStackTrace(); } } /** * 骗过编辑器,增加配置文件代码提示 * @return 配置对象 */ @ConfigurationProperties(prefix = ALONE_PREFIX) public DataRedisProperties getSaAloneRedisConfig() { return new DataRedisProperties(); } } ================================================ FILE: sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.dao.alone.SaAloneRedisInject ================================================ FILE: sa-token-plugin/sa-token-apikey/pom.xml ================================================ 4.0.0 sa-token-plugin cn.dev33 ${revision} ../pom.xml jar sa-token-apikey sa-token-apikey sa-token-apikey cn.dev33 sa-token-core ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/SaApiKeyManager.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey; import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoaderDefaultImpl; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; import cn.dev33.satoken.listener.SaTokenEventCenter; /** * 管理 Sa-Token API Key 所有全局组件 * * @author click33 * @since 1.43.0 */ public class SaApiKeyManager { /** * API Key 配置 Bean */ private static volatile SaApiKeyConfig config; public static SaApiKeyConfig getConfig() { if (config == null) { // 初始化默认值 synchronized (SaApiKeyManager.class) { if (config == null) { setConfig(new SaApiKeyConfig()); } } } return config; } public static void setConfig(SaApiKeyConfig config) { SaApiKeyManager.config = config; } /** * ApiKey 数据加载器 */ private volatile static SaApiKeyDataLoader apiKeyDataLoader; public static void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { SaApiKeyManager.apiKeyDataLoader = apiKeyDataLoader; SaTokenEventCenter.doRegisterComponent("SaApiKeyDataLoader", apiKeyDataLoader); } public static SaApiKeyDataLoader getSaApiKeyDataLoader() { if (apiKeyDataLoader == null) { synchronized (SaApiKeyManager.class) { if (apiKeyDataLoader == null) { SaApiKeyManager.apiKeyDataLoader = new SaApiKeyDataLoaderDefaultImpl(); } } } return apiKeyDataLoader; } /** * ApiKey 操作类 */ private volatile static SaApiKeyTemplate apiKeyTemplate; public static void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { SaApiKeyManager.apiKeyTemplate = apiKeyTemplate; SaTokenEventCenter.doRegisterComponent("SaApiKeyTemplate", apiKeyTemplate); } public static SaApiKeyTemplate getSaApiKeyTemplate() { if (apiKeyTemplate == null) { synchronized (SaApiKeyManager.class) { if (apiKeyTemplate == null) { SaApiKeyManager.apiKeyTemplate = new SaApiKeyTemplate(); } } } return apiKeyTemplate; } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/SaCheckApiKey.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.annotation; import cn.dev33.satoken.annotation.SaMode; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * API Key 校验:指定请求中必须包含有效的 ApiKey ,并且包含指定的 scope * *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.42.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckApiKey { /** * 指定 API key 必须包含的权限 [ 数组 ] * * @return / */ String [] scope() default {}; /** * 验证模式:AND | OR,默认AND * * @return / */ SaMode mode() default SaMode.AND; } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/handle/SaCheckApiKeyHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.annotation.handle; import cn.dev33.satoken.annotation.SaMode; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.apikey.annotation.SaCheckApiKey; import cn.dev33.satoken.apikey.template.SaApiKeyUtil; import cn.dev33.satoken.context.SaHolder; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckApiKey 的处理器 * * @author click33 * @since 1.42.0 */ public class SaCheckApiKeyHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckApiKey.class; } @Override public void checkMethod(SaCheckApiKey at, AnnotatedElement element) { _checkMethod(at.scope(), at.mode()); } public static void _checkMethod(String[] scope, SaMode mode) { String apiKey = SaApiKeyUtil.readApiKeyValue(SaHolder.getRequest()); if(mode == SaMode.AND) { SaApiKeyUtil.checkApiKeyScope(apiKey, scope); } else { SaApiKeyUtil.checkApiKeyScopeOr(apiKey, scope); } } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/config/SaApiKeyConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.config; /** * Sa-Token API Key 相关配置 * * @author click33 * @since 1.42.0 */ public class SaApiKeyConfig { /** * API Key 前缀 */ private String prefix = "AK-"; /** * API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) */ private long timeout = 2592000; /** * 框架是否记录索引信息 */ private Boolean isRecordIndex = true; /** * 获取 API Key 前缀 * * @return / */ public String getPrefix() { return this.prefix; } /** * 设置 API Key 前缀 * * @param prefix / * @return 对象自身 */ public SaApiKeyConfig setPrefix(String prefix) { this.prefix = prefix; return this; } /** * 获取 API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) * * @return / */ public long getTimeout() { return this.timeout; } /** * 设置 API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key) * * @param timeout / * @return 对象自身 */ public SaApiKeyConfig setTimeout(long timeout) { this.timeout = timeout; return this; } /** * 获取 框架是否保存索引信息 * * @return / */ public Boolean getIsRecordIndex() { return this.isRecordIndex; } /** * 设置 框架是否保存索引信息 * * @param isRecordIndex / * @return 对象自身 */ public SaApiKeyConfig setIsRecordIndex(Boolean isRecordIndex) { this.isRecordIndex = isRecordIndex; return this; } @Override public String toString() { return "SaApiKeyConfig{" + "prefix='" + prefix + '\'' + ", timeout=" + timeout + ", isRecordIndex=" + isRecordIndex + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/error/SaApiKeyErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.error; /** * 定义 sa-token-apikey 模块所有异常细分状态码 * * @author click33 * @since 1.43.0 */ public interface SaApiKeyErrorCode { /** 无效 API Key */ int CODE_12301 = 12301; /** API Key 已过期 */ int CODE_12302 = 12302; /** API Key 已被禁用 */ int CODE_12303 = 12303; /** API Key 字段自检未通过 */ int CODE_12304 = 12304; /** 未开启索引记录功能却调用了相关 API */ int CODE_12305 = 12305; /** API Key 不具有指定 Scope */ int CODE_12311 = 12311; /** API Key 不属于指定用户 */ int CODE_12312 = 12312; } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.exception; import cn.dev33.satoken.exception.SaTokenException; /** * 一个异常:代表 ApiKey 相关错误 * * @author click33 * @since 1.42.0 */ public class ApiKeyException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 ApiKey 相关错误 * @param cause 根异常原因 */ public ApiKeyException(Throwable cause) { super(cause); } /** * 一个异常:代表 ApiKey 相关错误 * @param message 异常描述 */ public ApiKeyException(String message) { super(message); } /** * 具体引起异常的 ApiKey 值 */ public String apiKey; public String getApiKey() { return apiKey; } public ApiKeyException setApiKey(String apiKey) { this.apiKey = apiKey; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new ApiKeyException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyScopeException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.exception; /** * 一个异常:代表 ApiKey Scope 相关错误 * * @author click33 * @since 1.42.0 */ public class ApiKeyScopeException extends ApiKeyException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 ApiKey Scope 相关错误 * @param cause 根异常原因 */ public ApiKeyScopeException(Throwable cause) { super(cause); } /** * 一个异常:代表 ApiKey Scope 相关错误 * @param message 异常描述 */ public ApiKeyScopeException(String message) { super(message); } /** * 具体引起异常的 ApiKey 值 */ public String apiKey; /** * 具体引起异常的 scope 值 */ public String scope; public String getApiKey() { return apiKey; } public ApiKeyScopeException setApiKey(String apiKey) { this.apiKey = apiKey; return this; } public String getScope() { return scope; } public ApiKeyScopeException setScope(String scope) { this.scope = scope; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new ApiKeyScopeException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoader.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.loader; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.model.ApiKeyModel; /** * ApiKey 数据加载器 * * @author click33 * @since 1.42.0 */ public interface SaApiKeyDataLoader { /** * 获取:框架是否保存索引信息 * * @return / */ default Boolean getIsRecordIndex() { return SaApiKeyManager.getConfig().getIsRecordIndex(); } /** * 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑) * * @param namespace / * @param apiKey / * @return ApiKeyModel */ default ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) { return null; } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoaderDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.loader; /** * ApiKey 数据加载器 默认实现类 * * @author click33 * @since 1.42.0 */ public class SaApiKeyDataLoaderDefaultImpl implements SaApiKeyDataLoader { // be empty of } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/model/ApiKeyModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.model; import cn.dev33.satoken.apikey.error.SaApiKeyErrorCode; import cn.dev33.satoken.apikey.exception.ApiKeyException; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; import java.util.*; /** * Model: API Key * * @author click33 * @since 1.41.0 */ public class ApiKeyModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 名称 */ private String title; /** * 介绍 */ private String intro; /** * ApiKey 值 */ private String apiKey; /** * 账号 id */ private Object loginId; /** * ApiKey 创建时间,13位时间戳 */ private long createTime; /** * ApiKey 到期时间,13位时间戳 (-1=永不过期) */ private long expiresTime; /** * 是否有效 (true=生效, false=禁用) */ private Boolean isValid = true; /** * 授权范围 */ private List scopes = new ArrayList<>(); /** * 扩展数据 */ private Map extraData; /** * 构造函数 */ public ApiKeyModel() { this.createTime = System.currentTimeMillis(); } // method /** * 添加 Scope * @param scope / * @return / */ public ApiKeyModel addScope(String ...scope) { if (this.scopes == null) { this.scopes = new ArrayList<>(); } this.scopes.addAll(Arrays.asList(scope)); return this; } /** * 添加 扩展数据 * @param key / * @param value / * @return / */ public ApiKeyModel addExtra(String key, Object value) { if (this.extraData == null) { this.extraData = new LinkedHashMap<>(); } this.extraData.put(key, value); return this; } /** * 查询扩展数据 */ public Object getExtra(String key) { if (this.extraData == null) { return null; } return this.extraData.get(key); } /** * 删除扩展数据 */ public Object removeExtra(String key) { if (this.extraData == null) { return null; } return this.extraData.remove(key); } /** * 数据自检,判断是否可以保存入库 */ public void checkByCanSaved() { if (SaFoxUtil.isEmpty(this.apiKey)) { throw new ApiKeyException("ApiKey 值不可为空").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); } if (this.loginId == null) { throw new ApiKeyException("无效 ApiKey: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); } if (this.createTime == 0) { throw new ApiKeyException("请指定 createTime 创建时间").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); } if (this.expiresTime == 0) { throw new ApiKeyException("请指定 expiresTime 过期时间").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); } if (this.isValid == null) { throw new ApiKeyException("请指定 isValid 是否生效").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304); } } /** * 获取:此 ApiKey 的剩余有效期(秒), -1=永不过期 * @return / */ public long expiresIn() { if (expiresTime == SaTokenDao.NEVER_EXPIRE) { return SaTokenDao.NEVER_EXPIRE; } long s = (expiresTime - System.currentTimeMillis()) / 1000; return s < 1 ? -2 : s; } /** * 判断:此 ApiKey 是否已超时 * @return / */ public boolean timeExpired() { if (expiresTime == SaTokenDao.NEVER_EXPIRE) { return false; } return System.currentTimeMillis() > expiresTime; } // get and set /** * 获取 名称 * * @return title 名称 */ public String getTitle() { return this.title; } /** * 设置 名称 * * @param title 名称 * @return 对象自身 */ public ApiKeyModel setTitle(String title) { this.title = title; return this; } /** * 获取 介绍 * * @return intro 介绍 */ public String getIntro() { return this.intro; } /** * 设置 介绍 * * @param intro 介绍 * @return 对象自身 */ public ApiKeyModel setIntro(String intro) { this.intro = intro; return this; } /** * 获取 ApiKey 值 * * @return apiKey ApiKey 值 */ public String getApiKey() { return this.apiKey; } /** * 设置 ApiKey 值 * * @param apiKey ApiKey 值 * @return 对象自身 */ public ApiKeyModel setApiKey(String apiKey) { this.apiKey = apiKey; return this; } /** * 获取 账号 id * * @return loginId 账号 id */ public Object getLoginId() { return this.loginId; } /** * 设置 账号 id * * @param loginId 账号 id * @return 对象自身 */ public ApiKeyModel setLoginId(Object loginId) { this.loginId = loginId; return this; } /** * 获取 ApiKey 创建时间,13位时间戳 * * @return createTime ApiKey 创建时间,13位时间戳 */ public long getCreateTime() { return this.createTime; } /** * 设置 ApiKey 创建时间,13位时间戳 * * @param createTime ApiKey 创建时间,13位时间戳 * @return 对象自身 */ public ApiKeyModel setCreateTime(long createTime) { this.createTime = createTime; return this; } /** * 获取 ApiKey 到期时间,13位时间戳 (-1=永不过期) * * @return expiresTime ApiKey 到期时间,13位时间戳 (-1=永不过期) */ public long getExpiresTime() { return this.expiresTime; } /** * 设置 ApiKey 到期时间,13位时间戳 (-1=永不过期) * * @param expiresTime ApiKey 到期时间,13位时间戳 (-1=永不过期) * @return 对象自身 */ public ApiKeyModel setExpiresTime(long expiresTime) { this.expiresTime = expiresTime; return this; } /** * 获取 是否有效 (true=生效 false=禁用) * * @return / */ public Boolean getIsValid() { return this.isValid; } /** * 设置 是否有效 (true=生效 false=禁用) * * @param isValid / * @return 对象自身 */ public ApiKeyModel setIsValid(Boolean isValid) { this.isValid = isValid; return this; } /** * 获取 授权范围 * * @return scopes 授权范围 */ public List getScopes() { return this.scopes; } /** * 设置 授权范围 * * @param scopes 授权范围 * @return 对象自身 */ public ApiKeyModel setScopes(List scopes) { this.scopes = scopes; return this; } /** * 获取 扩展数据 * * @return extraData 扩展数据 */ public Map getExtraData() { return this.extraData; } /** * 设置 扩展数据 * * @param extraData 扩展数据 * @return 对象自身 */ public ApiKeyModel setExtraData(Map extraData) { this.extraData = extraData; return this; } @Override public String toString() { return "ApiKeyModel{" + "title='" + title + ", intro='" + intro + ", apiKey='" + apiKey + ", loginId=" + loginId + ", createTime=" + createTime + ", expiresTime=" + expiresTime + ", isValid=" + isValid + ", scopes=" + scopes + ", extraData=" + extraData + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.template; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.error.SaApiKeyErrorCode; import cn.dev33.satoken.apikey.exception.ApiKeyException; import cn.dev33.satoken.apikey.exception.ApiKeyScopeException; import cn.dev33.satoken.apikey.model.ApiKeyModel; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.raw.SaRawSessionDelegator; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import java.util.ArrayList; import java.util.List; /** * API Key 操作类 * * @author click33 * @since 1.42.0 */ public class SaApiKeyTemplate { /** *默认命名空间 */ public static final String DEFAULT_NAMESPACE = "apikey"; /** * 命名空间 */ public String namespace; /** * Raw Session 读写委托 */ public SaRawSessionDelegator rawSessionDelegator; /** * 在 raw-session 中的保存索引列表使用的 key */ public static final String API_KEY_LIST = "__HD_API_KEY_LIST"; public SaApiKeyTemplate(){ this(DEFAULT_NAMESPACE); } /** * 实例化 * @param namespace 命名空间,用于多实例隔离 */ public SaApiKeyTemplate(String namespace){ if(SaFoxUtil.isEmpty(namespace)) { throw new ApiKeyException("namespace 不能为空"); } this.namespace = namespace; this.rawSessionDelegator = new SaRawSessionDelegator(namespace); } // ------------------- ApiKey /** * 根据 apiKey 从 Cache 获取 ApiKeyModel 信息 * @param apiKey / * @return / */ public ApiKeyModel getApiKeyModelFromCache(String apiKey) { return getSaTokenDao().getObject(splicingApiKeySaveKey(apiKey), ApiKeyModel.class); } /** * 根据 apiKey 从 Database 获取 ApiKeyModel 信息 * @param apiKey / * @return / */ public ApiKeyModel getApiKeyModelFromDatabase(String apiKey) { return SaApiKeyManager.getSaApiKeyDataLoader().getApiKeyModelFromDatabase(namespace, apiKey); } /** * 获取 ApiKeyModel,无效的 ApiKey 会返回 null * @param apiKey / * @return / */ public ApiKeyModel getApiKey(String apiKey) { if(apiKey == null) { return null; } // 先从缓存中获取,缓存中找不到就尝试从数据库获取 ApiKeyModel apiKeyModel = getApiKeyModelFromCache(apiKey); if(apiKeyModel == null) { apiKeyModel = getApiKeyModelFromDatabase(apiKey); saveApiKey(apiKeyModel); } return apiKeyModel; } /** * 校验 ApiKey,成功返回 ApiKeyModel,失败则抛出异常 * @param apiKey / * @return / */ public ApiKeyModel checkApiKey(String apiKey) { ApiKeyModel ak = getApiKey(apiKey); if(ak == null) { throw new ApiKeyException("无效 API Key: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12301); } if(ak.timeExpired()) { throw new ApiKeyException("API Key 已过期: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12302); } if(! ak.getIsValid()) { throw new ApiKeyException("API Key 已被禁用: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12303); } return ak; } /** * 持久化:ApiKeyModel * @param ak / */ public void saveApiKey(ApiKeyModel ak) { if(ak == null) { return; } // 数据自检 ak.checkByCanSaved(); // 保存 ApiKeyModel String saveKey = splicingApiKeySaveKey(ak.getApiKey()); if(ak.timeExpired()) { getSaTokenDao().deleteObject(saveKey); } else { getSaTokenDao().setObject(saveKey, ak, ak.expiresIn()); } // 记录索引 if (getIsRecordIndex()) { // 添加索引 SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId()); ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); if(! apiKeyList.contains(ak.getApiKey())) { apiKeyList.add(ak.getApiKey()); session.set(API_KEY_LIST, apiKeyList); } // 调整 ttl adjustIndex(ak.getLoginId(), session); } } /** * 获取 ApiKey 所代表的 LoginId * @param apiKey ApiKey * @return LoginId */ public Object getLoginIdByApiKey(String apiKey) { return checkApiKey(apiKey).getLoginId(); } /** * 删除 ApiKey * @param apiKey ApiKey */ public void deleteApiKey(String apiKey) { // 删 ApiKeyModel ApiKeyModel ak = getApiKeyModelFromCache(apiKey); if(ak == null) { return; } getSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey)); // 删索引 if(getIsRecordIndex()) { // RawSession 中不存在,提前退出 SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId(), false); if(session == null) { return; } // 索引无记录,提前退出 ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); if(! apiKeyList.contains(apiKey)) { return; } // 如果只有一个 ApiKey,则整个 RawSession 删掉 if (apiKeyList.size() == 1) { rawSessionDelegator.deleteSessionById(ak.getLoginId()); } else { // 否则移除此 ApiKey 并保存 apiKeyList.remove(apiKey); session.set(API_KEY_LIST, apiKeyList); } } } /** * 删除指定 loginId 的所有 ApiKey * @param loginId / */ public void deleteApiKeyByLoginId(Object loginId) { // 先判断是否开启索引 if(! getIsRecordIndex()) { SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 deleteApiKeyByLoginId 操作"); return; } // RawSession 中不存在,提前退出 SaSession session = rawSessionDelegator.getSessionById(loginId, false); if(session == null) { return; } // 先删 ApiKeyModel ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); for (String apiKey : apiKeyList) { getSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey)); } // 再删索引 rawSessionDelegator.deleteSessionById(loginId); } // ------- 创建 /** * 创建一个 ApiKeyModel 对象 * * @return / */ public ApiKeyModel createApiKeyModel() { String apiKey = SaStrategy.instance.generateUniqueToken.execute( "API Key", SaManager.getConfig().getMaxTryTimes(), this::randomApiKeyValue, _apiKey -> getApiKey(_apiKey) == null ); return new ApiKeyModel().setApiKey(apiKey); } /** * 创建一个 ApiKeyModel 对象 * * @return / */ public ApiKeyModel createApiKeyModel(Object loginId) { long timeout = SaApiKeyManager.getConfig().getTimeout(); long expiresTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? SaTokenDao.NEVER_EXPIRE : System.currentTimeMillis() + timeout * 1000; return createApiKeyModel() .setLoginId(loginId) .setIsValid(true) .setExpiresTime(expiresTime) ; } /** * 随机一个 ApiKey 码 * * @return / */ public String randomApiKeyValue() { return SaApiKeyManager.getConfig().getPrefix() + SaFoxUtil.getRandomString(36); } // ------------------- 校验 /** * 判断:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),返回 true 或 false * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public boolean hasApiKeyScope(String apiKey, String... scopes) { try { checkApiKeyScope(apiKey, scopes); return true; } catch (ApiKeyException e) { return false; } } /** * 校验:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),如果不具备则抛出异常 * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public void checkApiKeyScope(String apiKey, String... scopes) { ApiKeyModel ak = checkApiKey(apiKey); if(SaFoxUtil.isEmptyArray(scopes)) { return; } for (String scope : scopes) { if(! ak.getScopes().contains(scope)) { throw new ApiKeyScopeException("该 API Key 不具备 Scope:" + scope) .setApiKey(apiKey) .setScope(scope) .setCode(SaApiKeyErrorCode.CODE_12311); } } } /** * 判断:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),返回 true 或 false * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public boolean hasApiKeyScopeOr(String apiKey, String... scopes) { try { checkApiKeyScopeOr(apiKey, scopes); return true; } catch (ApiKeyException e) { return false; } } /** * 校验:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),如果不具备则抛出异常 * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public void checkApiKeyScopeOr(String apiKey, String... scopes) { ApiKeyModel ak = checkApiKey(apiKey); if(SaFoxUtil.isEmptyArray(scopes)) { return; } for (String scope : scopes) { if(ak.getScopes().contains(scope)) { return; } } throw new ApiKeyScopeException("该 API Key 不具备 Scope:" + scopes[0]) .setApiKey(apiKey) .setScope(scopes[0]) .setCode(SaApiKeyErrorCode.CODE_12311); } /** * 判断:指定 ApiKey 是否属于指定 LoginId,返回 true 或 false * @param apiKey / * @param loginId / */ public boolean isApiKeyLoginId(String apiKey, Object loginId) { try { checkApiKeyLoginId(apiKey, loginId); return true; } catch (ApiKeyException e) { return false; } } /** * 校验:指定 ApiKey 是否属于指定 LoginId,如果不是则抛出异常 * * @param apiKey / * @param loginId / */ public void checkApiKeyLoginId(String apiKey, Object loginId) { ApiKeyModel ak = getApiKey(apiKey); if(ak == null) { throw new ApiKeyException("无效 API Key: " + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12301); } if (SaFoxUtil.notEquals(String.valueOf(ak.getLoginId()), String.valueOf(loginId))) { throw new ApiKeyException("该 API Key 不属于用户: " + loginId) .setApiKey(apiKey) .setCode(SaApiKeyErrorCode.CODE_12312); } } // ------------------- 索引操作 /** * 调整指定 SaSession 的 TTL 值,以保证最小化内存占用 * @param loginId / * @param session 可填写 null,代表使用 loginId 现场查询 */ public void adjustIndex(Object loginId, SaSession session) { // 先判断是否开启索引 if(! getIsRecordIndex()) { SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 adjustIndex 操作"); return; } // 未提供则现场查询 if(session == null) { session = rawSessionDelegator.getSessionById(loginId, false); if(session == null) { return; } } // 重新整理索引列表 ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); ArrayList apiKeyNewList = new ArrayList<>(); ArrayList apiKeyModelList = new ArrayList<>(); for (String apikey : apiKeyList) { ApiKeyModel ak = getApiKeyModelFromCache(apikey); if(ak == null || ak.timeExpired()) { continue; } apiKeyNewList.add(apikey); apiKeyModelList.add(ak); } // 如果队列里已无有效值,则删除该 session if(apiKeyNewList.isEmpty()) { rawSessionDelegator.deleteSessionById(loginId); return; } session.set(API_KEY_LIST, apiKeyNewList); // 调整 SaSession TTL long maxTtl = 0; for (ApiKeyModel ak : apiKeyModelList) { long ttl = ak.expiresIn(); if(ttl == SaTokenDao.NEVER_EXPIRE) { maxTtl = SaTokenDao.NEVER_EXPIRE; break; } if(ttl > maxTtl) { maxTtl = ttl; } } if(maxTtl != 0) { session.updateTimeout(maxTtl); } } /** * 获取指定 loginId 的 ApiKey 列表记录 * @param loginId / * @return / */ public List getApiKeyList(Object loginId) { // 先判断是否开启索引 if(! getIsRecordIndex()) { SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 getApiKeyList 操作"); return new ArrayList<>(); } // 先查 RawSession List apiKeyModelList = new ArrayList<>(); SaSession session = rawSessionDelegator.getSessionById(loginId, false); if(session == null) { return apiKeyModelList; } // 从 RawSession 遍历查询 ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); for (String apikey : apiKeyList) { ApiKeyModel ak = getApiKeyModelFromCache(apikey); if(ak == null || ak.timeExpired()) { continue; } apiKeyModelList.add(ak); } return apiKeyModelList; } // ------------------- 请求查询 /** * 数据读取:从请求对象中读取 ApiKey,获取不到返回 null */ public String readApiKeyValue(SaRequest request) { // 优先从请求参数中获取 String apiKey = request.getParam(namespace); if(SaFoxUtil.isNotEmpty(apiKey)) { return apiKey; } // 然后请求头 apiKey = request.getHeader(namespace); if(SaFoxUtil.isNotEmpty(apiKey)) { return apiKey; } // 最后从 Authorization 中获取 apiKey = SaHttpBasicUtil.getAuthorizationValue(); if(SaFoxUtil.isNotEmpty(apiKey)) { if(apiKey.endsWith(":")) { apiKey = apiKey.substring(0, apiKey.length() - 1); } return apiKey; } return null; } /** * 数据读取:从请求对象中读取 ApiKey,并查询到 ApiKeyModel 信息 */ public ApiKeyModel currentApiKey() { String readApiKeyValue = readApiKeyValue(SaHolder.getRequest()); return checkApiKey(readApiKeyValue); } // ------------------- 拼接key /** * 拼接key:ApiKey 持久化 * @param apiKey ApiKey * @return key */ public String splicingApiKeySaveKey(String apiKey) { return getSaTokenConfig().getTokenName() + ":" + namespace + ":" + apiKey; } // -------- bean 对象代理 /** * 获取使用的 getSaTokenDao 实例 * * @return / */ public SaTokenDao getSaTokenDao() { return SaManager.getSaTokenDao(); } /** * 获取使用的 SaTokenConfig 实例 * * @return / */ public SaTokenConfig getSaTokenConfig() { return SaManager.getConfig(); } /** * 是否保存索引信息 */ public boolean getIsRecordIndex() { return SaApiKeyManager.getSaApiKeyDataLoader().getIsRecordIndex(); } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.apikey.template; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.model.ApiKeyModel; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.session.SaSession; import java.util.List; /** * API Key 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaApiKeyUtil { /** * 获取 ApiKeyModel,无效的 ApiKey 会返回 null * @param apiKey / * @return / */ public static ApiKeyModel getApiKey(String apiKey) { return SaApiKeyManager.getSaApiKeyTemplate().getApiKey(apiKey); } /** * 校验 ApiKey,成功返回 ApiKeyModel,失败则抛出异常 * @param apiKey / * @return / */ public static ApiKeyModel checkApiKey(String apiKey) { return SaApiKeyManager.getSaApiKeyTemplate().checkApiKey(apiKey); } /** * 持久化:ApiKeyModel * @param ak / */ public static void saveApiKey(ApiKeyModel ak) { SaApiKeyManager.getSaApiKeyTemplate().saveApiKey(ak); } /** * 获取 ApiKey 所代表的 LoginId * @param apiKey ApiKey * @return LoginId */ public static Object getLoginIdByApiKey(String apiKey) { return SaApiKeyManager.getSaApiKeyTemplate().getLoginIdByApiKey(apiKey); } /** * 删除 ApiKey * @param apiKey ApiKey */ public static void deleteApiKey(String apiKey) { SaApiKeyManager.getSaApiKeyTemplate().deleteApiKey(apiKey); } /** * 删除指定 loginId 的所有 ApiKey * @param loginId / */ public static void deleteApiKeyByLoginId(Object loginId) { SaApiKeyManager.getSaApiKeyTemplate().deleteApiKeyByLoginId(loginId); } // ------- 创建 /** * 创建一个 ApiKeyModel 对象 * * @return / */ public static ApiKeyModel createApiKeyModel() { return SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel(); } /** * 创建一个 ApiKeyModel 对象 * * @return / */ public static ApiKeyModel createApiKeyModel(Object loginId) { return SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel(loginId); } // ------------------- Scope /** * 判断:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),返回 true 或 false * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public static boolean hasApiKeyScope(String apiKey, String... scopes) { return SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScope(apiKey, scopes); } /** * 校验:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),如果不具备则抛出异常 * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public static void checkApiKeyScope(String apiKey, String... scopes) { SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScope(apiKey, scopes); } /** * 判断:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),返回 true 或 false * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public static boolean hasApiKeyScopeOr(String apiKey, String... scopes) { return SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScopeOr(apiKey, scopes); } /** * 校验:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),如果不具备则抛出异常 * @param apiKey ApiKey * @param scopes 需要校验的权限列表 */ public static void checkApiKeyScopeOr(String apiKey, String... scopes) { SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScopeOr(apiKey, scopes); } /** * 判断:指定 ApiKey 是否属于指定 LoginId,返回 true 或 false * @param apiKey / * @param loginId / */ public static boolean isApiKeyLoginId(String apiKey, Object loginId) { return SaApiKeyManager.getSaApiKeyTemplate().isApiKeyLoginId(apiKey, loginId); } /** * 校验:指定 ApiKey 是否属于指定 LoginId,如果不是则抛出异常 * * @param apiKey / * @param loginId / */ public static void checkApiKeyLoginId(String apiKey, Object loginId) { SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyLoginId(apiKey, loginId); } // ------------------- 请求查询 /** * 数据读取:从请求对象中读取 ApiKey,获取不到返回 null */ public static String readApiKeyValue(SaRequest request) { return SaApiKeyManager.getSaApiKeyTemplate().readApiKeyValue(request); } /** * 数据读取:从请求对象中读取 ApiKey,并查询到 ApiKeyModel 信息 */ public static ApiKeyModel currentApiKey() { return SaApiKeyManager.getSaApiKeyTemplate().currentApiKey(); } // ------------------- 索引操作 /** * 调整指定 SaSession 的 TTL 值,以保证最小化内存占用 * @param loginId / * @param session 可填写 null,代表使用 loginId 现场查询 */ public static void adjustIndex(Object loginId, SaSession session) { SaApiKeyManager.getSaApiKeyTemplate().adjustIndex(loginId, session); } /** * 获取指定 loginId 的 ApiKey 列表记录 * @param loginId / * @return / */ public static List getApiKeyList(Object loginId) { return SaApiKeyManager.getSaApiKeyTemplate().getApiKeyList(loginId); } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForApiKey.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.apikey.annotation.handle.SaCheckApiKeyHandler; import cn.dev33.satoken.strategy.SaAnnotationStrategy; /** * SaToken 插件安装:API Key 组件 * * @author click33 * @since 1.43.0 */ public class SaTokenPluginForApiKey implements SaTokenPlugin { @Override public void install() { // 安装 API Key 鉴权注解 SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckApiKeyHandler()); } } ================================================ FILE: sa-token-plugin/sa-token-apikey/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForApiKey ================================================ FILE: sa-token-plugin/sa-token-caffeine/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-caffeine sa-token-caffeine sa-token integrate Caffeine cn.dev33 sa-token-core com.github.ben-manes.caffeine caffeine ================================================ FILE: sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaMapPackageForCaffeine.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.timedcache.SaMapPackage; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Map 包装类 (Caffeine 版) * * @author click33 * @since 1.41.0 */ public class SaMapPackageForCaffeine implements SaMapPackage { public Cache cache = Caffeine.newBuilder() .expireAfterWrite(Long.MAX_VALUE, TimeUnit.SECONDS) .maximumSize(Integer.MAX_VALUE) .build(); @Override public Object getSource() { return cache; } /** * 读 * * @param key / * @return / */ @Override public V get(String key) { return cache.getIfPresent(key); } /** * 写 * * @param key / * @param value / */ @Override public void put(String key, V value) { cache.put(key, value); } /** * 删 * @param key / */ @Override public void remove(String key) { cache.invalidate(key); } /** * 所有 key */ @Override public Set keySet() { return cache.asMap().keySet(); } } ================================================ FILE: sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForCaffeine.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject; import cn.dev33.satoken.dao.timedcache.SaTimedCache; import cn.dev33.satoken.util.SaFoxUtil; import java.util.List; /** * Sa-Token 持久层实现,基于 SaTimedCache - Caffeine (内存缓存,系统重启后数据丢失) * * @author click33 * @since 1.41.0 */ public class SaTokenDaoForCaffeine implements SaTokenDaoByStringFollowObject, SaTokenDao { public SaTimedCache timedCache = new SaTimedCache( new SaMapPackageForCaffeine<>(), new SaMapPackageForCaffeine<>() ); // ------------------------ Object 读写操作 @Override public Object getObject(String key) { return timedCache.getObject(key); } @Override @SuppressWarnings("unchecked") public T getObject(String key, Class classType){ return (T) getObject(key); } @Override public void setObject(String key, Object object, long timeout) { timedCache.setObject(key, object, timeout); } @Override public void updateObject(String key, Object object) { timedCache.updateObject(key, object); } @Override public void deleteObject(String key) { timedCache.deleteObject(key); } @Override public long getObjectTimeout(String key) { return timedCache.getObjectTimeout(key); } @Override public void updateObjectTimeout(String key, long timeout) { timedCache.updateObjectTimeout(key, timeout); } // --------- 会话管理 @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { return SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType); } // --------- 组件生命周期 /** * 组件被安装时,开始刷新数据线程 */ @Override public void init() { timedCache.initRefreshThread(); } /** * 组件被卸载时,结束定时任务,不再定时清理过期数据 */ @Override public void destroy() { timedCache.endRefreshThread(); } } ================================================ FILE: sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForCaffeine.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDaoForCaffeine; /** * SaToken 插件安装:DAO 扩展 - Caffeine 版 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForCaffeine implements SaTokenPlugin { @Override public void install() { SaManager.setSaTokenDao(new SaTokenDaoForCaffeine()); } } ================================================ FILE: sa-token-plugin/sa-token-caffeine/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForCaffeine ================================================ FILE: sa-token-plugin/sa-token-dubbo/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-dubbo sa-token-dubbo sa-token-dubbo cn.dev33 sa-token-core org.apache.dubbo dubbo true ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboConsumerFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.filter; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.SaTokenContextDefaultImpl; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; /** * Sa-Token 整合 Dubbo Consumer 端(调用端)过滤器 * * @author click33 * @since 1.34.0 */ @Activate(group = {CommonConstants.CONSUMER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) public class SaTokenDubboConsumerFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { // 追加 Same-Token 参数 if(SaManager.getConfig().getCheckSameToken()) { RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); } // 无上下文时只做简单调用,不传递会话 token if( ! SaHolder.getContext().isValid()) { return invoker.invoke(invocation); } // 1、调用前,向下传递会话Token if(SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) { RpcContext.getContext().setAttachment(SaTokenConsts.JUST_CREATED, StpUtil.getTokenValueNotCut()); } // 2、开始调用 Result invoke = invoker.invoke(invocation); // 3、调用后,解析回传的Token值 StpUtil.setTokenValue(invoke.getAttachment(SaTokenConsts.JUST_CREATED_NOT_PREFIX)); // note return invoke; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboContextFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.filter; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.dubbo.util.SaTokenContextDubboUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; /** * Sa-Token 整合 Dubbo 上下文初始化过滤器 * * @author click33 * @since 1.42.0 */ @Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_CONTEXT_FILTER_ORDER) public class SaTokenDubboContextFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) { if(SaHolder.getContext().isValid()) { return invoker.invoke(invocation); } try { SaTokenContextDubboUtil.setContext(RpcContext.getContext()); return invoker.invoke(invocation); } finally { SaTokenContextDubboUtil.clearContext(); } } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboProviderFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.filter; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.Filter; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; /** * Sa-Token 整合 Dubbo Provider端(被调用端)过滤器 * * @author click33 * @since 1.34.0 */ @Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) public class SaTokenDubboProviderFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) { // RPC 调用鉴权 if(SaManager.getConfig().getCheckSameToken()) { String idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN); // dubbo部分协议会将参数变为小写,此处需要额外处理一下,详细参考:https://gitee.com/dromara/sa-token/issues/I4WXQG if(idToken == null) { idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase()); } SaSameUtil.checkToken(idToken); } // 开始调用 return invoker.invoke(invocation); } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaRequestForDubbo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.model; import cn.dev33.satoken.context.model.SaRequest; import org.apache.dubbo.rpc.RpcContext; import java.util.Collection; import java.util.Map; /** * 对 SaRequest 包装类的实现(Dubbo 版) * * @author click33 * @since 1.34.0 */ public class SaRequestForDubbo implements SaRequest { /** * 底层对象 */ protected RpcContext rpcContext; /** * 实例化 * @param rpcContext rpcContext对象 */ public SaRequestForDubbo(RpcContext rpcContext) { this.rpcContext = rpcContext; } /** * 获取底层源对象 */ @Override public Object getSource() { return rpcContext; } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { // 不传播 url 参数 return null; } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return null; } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ return null; } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { // 不传播 header 参数 return null; } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { // 不传播 cookie 参数 return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ // 不传播 cookie 参数 return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ // 不传播 cookie 参数 return null; } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { // 不传播 requestPath return null; } /** * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { // 不传播 url return null; } /** * 返回当前请求的类型 */ @Override public String getMethod() { // 不传播 method return null; } @Override public String getHost() { return null; } /** * 转发请求 */ @Override public Object forward(String path) { // 不传播 forward 动作 return null; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaResponseForDubbo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.model; import cn.dev33.satoken.context.model.SaResponse; import org.apache.dubbo.rpc.RpcContext; /** * 对 SaResponse 包装类的实现(Dubbo 版) * * @author click33 * @since 1.34.0 */ public class SaResponseForDubbo implements SaResponse { /** * 底层Request对象 */ protected RpcContext rpcContext; /** * 实例化 * @param rpcContext rpcContext对象 */ public SaResponseForDubbo(RpcContext rpcContext) { this.rpcContext = rpcContext; } /** * 获取底层源对象 */ @Override public Object getSource() { return rpcContext; } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { // 不回传 status 状态 return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { // 不回传 header响应头 return this; } /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { // 不回传 header响应头 return this; } /** * 重定向 */ @Override public Object redirect(String url) { // 不回传 重定向 动作 return null; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaStorageForDubbo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.model; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.rpc.RpcContext; /** * 对 SaStorage 包装类的实现(Dubbo 版) * * @author click33 * @since 1.34.0 */ public class SaStorageForDubbo implements SaStorage { /** * 底层对象 */ protected RpcContext rpcContext; /** * 实例化 * @param rpcContext rpcContext对象 */ public SaStorageForDubbo(RpcContext rpcContext) { this.rpcContext = rpcContext; } /** * 获取底层源对象 */ @Override public Object getSource() { return rpcContext; } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorageForDubbo set(String key, Object value) { rpcContext.setObjectAttachment(key, value); // 如果是token写入,则回传到Consumer端 if(key.equals(SaTokenConsts.JUST_CREATED_NOT_PREFIX)) { RpcContext.getServerContext().setAttachment(key, value); } return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return rpcContext.getObjectAttachment(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorageForDubbo delete(String key) { rpcContext.removeAttachment(key); return this; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/util/SaTokenContextDubboUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.dubbo.model.SaRequestForDubbo; import cn.dev33.satoken.context.dubbo.model.SaResponseForDubbo; import cn.dev33.satoken.context.dubbo.model.SaStorageForDubbo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import org.apache.dubbo.rpc.RpcContext; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextDubboUtil { /** * 写入当前上下文 * @param rpcContext / */ public static void setContext(RpcContext rpcContext) { SaRequest saRequest = new SaRequestForDubbo(RpcContext.getContext()); SaResponse saResponse = new SaResponseForDubbo(RpcContext.getContext()); SaStorage saStorage = new SaStorageForDubbo(RpcContext.getContext()); SaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage); } /** * 清除当前上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } } ================================================ FILE: sa-token-plugin/sa-token-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter ================================================ saTokenDubboConsumerFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboConsumerFilter saTokenDubboProviderFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboProviderFilter saTokenDubboContextFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboContextFilter ================================================ FILE: sa-token-plugin/sa-token-dubbo3/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml sa-token-dubbo3 sa-token-dubbo3 sa-token-dubbo3 3.2.2 8 8 UTF-8 cn.dev33 sa-token-core org.apache.dubbo dubbo ${dubbo3.version} ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ConsumerFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.filter; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.SaTokenContextDefaultImpl; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; /** * Sa-Token 整合 Dubbo3 Consumer 端(调用端)过滤器 * * @author click33 * @since 1.34.0 */ @Activate(group = {CommonConstants.CONSUMER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) public class SaTokenDubbo3ConsumerFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) { // 追加 Same-Token 参数 if(SaManager.getConfig().getCheckSameToken()) { RpcContext.getServiceContext().setAttachment(SaSameUtil.SAME_TOKEN,SaSameUtil.getToken()); } // 无上下文时只做简单调用,不传递会话 token if( ! SaHolder.getContext().isValid()) { return invoker.invoke(invocation); } // 1. 调用前,向下传递会话Token if(SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) { RpcContext.getServiceContext().setAttachment(SaTokenConsts.JUST_CREATED, StpUtil.getTokenValueNotCut()); } // 2. 开始调用 Result invoke = invoker.invoke(invocation); // 3. 调用后,解析回传的Token值 StpUtil.setTokenValue(invoke.getAttachment(SaTokenConsts.JUST_CREATED_NOT_PREFIX)); // note return invoke; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ContextFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.filter; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.dubbo3.util.SaTokenContextDubbo3Util; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.*; /** * Sa-Token 整合 Dubbo3 上下文初始化过滤器 * * @author click33 * @since 1.42.0 */ @Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_CONTEXT_FILTER_ORDER) public class SaTokenDubbo3ContextFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) { if(SaHolder.getContext().isValid()) { return invoker.invoke(invocation); } try { SaTokenContextDubbo3Util.setContext(RpcContext.getServiceContext()); return invoker.invoke(invocation); } finally { SaTokenContextDubbo3Util.clearContext(); } } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ProviderFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.filter; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.extension.Activate; import org.apache.dubbo.rpc.Filter; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; /** * Sa-Token 整合 Dubbo3 Provider端(被调用端)过滤器 * * @author click33 * @since 1.34.0 */ @Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER) public class SaTokenDubbo3ProviderFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) { // RPC 调用鉴权 if(SaManager.getConfig().getCheckSameToken()) { String idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN); // dubbo部分协议会将参数变为小写,详细参考:https://gitee.com/dromara/sa-token/issues/I4WXQG if(idToken == null) { idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase()); } SaSameUtil.checkToken(idToken); } // 开始调用 return invoker.invoke(invocation); } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaRequestForDubbo3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.model; import cn.dev33.satoken.context.model.SaRequest; import org.apache.dubbo.rpc.RpcContext; import java.util.Collection; import java.util.Map; /** * 对 SaRequest 包装类的实现(Dubbo3 版) * * @author click33 * @since 1.34.0 */ public class SaRequestForDubbo3 implements SaRequest { /** * 底层对象 */ protected RpcContext rpcContext; /** * 实例化 * @param rpcContext rpcContext对象 */ public SaRequestForDubbo3(RpcContext rpcContext) { this.rpcContext = rpcContext; } /** * 获取底层源对象 */ @Override public Object getSource() { return rpcContext; } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { // 不传播 url 参数 return null; } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return null; } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ return null; } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { // 不传播 header 参数 return null; } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { // 不传播 cookie 参数 return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ // 不传播 cookie 参数 return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ // 不传播 cookie 参数 return null; } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { // 不传播 requestPath return null; } /** * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { // 不传播 url return null; } /** * 返回当前请求的类型 */ @Override public String getMethod() { // 不传播 method return null; } @Override public String getHost() { return null; } /** * 转发请求 */ @Override public Object forward(String path) { // 不传播 forward 动作 return null; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaResponseForDubbo3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.model; import cn.dev33.satoken.context.model.SaResponse; import org.apache.dubbo.rpc.RpcContext; /** * 对 SaResponse 包装类的实现(Dubbo3 版) * * @author click33 * @since 1.34.0 */ public class SaResponseForDubbo3 implements SaResponse { /** * 底层Request对象 */ protected RpcContext rpcContext; /** * 实例化 * @param rpcContext rpcContext对象 */ public SaResponseForDubbo3(RpcContext rpcContext) { this.rpcContext = rpcContext; } /** * 获取底层源对象 */ @Override public Object getSource() { return rpcContext; } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { // 不回传 status 状态 return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { // 不回传 header响应头 return this; } /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { // 不回传 header响应头 return this; } /** * 重定向 */ @Override public Object redirect(String url) { // 不回传 重定向 动作 return null; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaStorageForDubbo3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.model; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.util.SaTokenConsts; import org.apache.dubbo.rpc.RpcContext; /** * 对 SaStorage 包装类的实现(Dubbo3 版) * * @author click33 * @since 1.34.0 */ public class SaStorageForDubbo3 implements SaStorage { /** * 底层对象 */ protected RpcContext rpcContext; /** * 实例化 * @param rpcContext rpcContext对象 */ public SaStorageForDubbo3(RpcContext rpcContext) { this.rpcContext = rpcContext; } /** * 获取底层源对象 */ @Override public Object getSource() { return rpcContext; } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorageForDubbo3 set(String key, Object value) { rpcContext.setObjectAttachment(key, value); // 如果是token写入,则回传到Consumer端 if(key.equals(SaTokenConsts.JUST_CREATED_NOT_PREFIX)) { RpcContext.getServerContext().setAttachment(key, value); } return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return rpcContext.getObjectAttachment(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorageForDubbo3 delete(String key) { rpcContext.removeAttachment(key); return this; } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/util/SaTokenContextDubbo3Util.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.dubbo3.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.dubbo3.model.SaRequestForDubbo3; import cn.dev33.satoken.context.dubbo3.model.SaResponseForDubbo3; import cn.dev33.satoken.context.dubbo3.model.SaStorageForDubbo3; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import org.apache.dubbo.rpc.RpcContext; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextDubbo3Util { /** * 写入当前上下文 * @param rpcContext / */ public static void setContext(RpcContext rpcContext) { SaRequest saRequest = new SaRequestForDubbo3(RpcContext.getServiceContext()); SaResponse saResponse = new SaResponseForDubbo3(RpcContext.getServiceContext()); SaStorage saStorage = new SaStorageForDubbo3(RpcContext.getServiceContext()); SaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage); } /** * 清除当前上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } } ================================================ FILE: sa-token-plugin/sa-token-dubbo3/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter ================================================ saTokenDubbo3ConsumerFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ConsumerFilter saTokenDubbo3ProviderFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ProviderFilter saTokenDubbo3ContextFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ContextFilter ================================================ FILE: sa-token-plugin/sa-token-fastjson/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-fastjson sa-token-fastjson sa-token integrate Fastjson cn.dev33 sa-token-core com.alibaba fastjson ================================================ FILE: sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.util.SaFoxUtil; import com.alibaba.fastjson.JSON; /** * JSON 转换器, Fastjson 版实现 * * @author click33 * @since 1.34.0 */ public class SaJsonTemplateForFastjson implements SaJsonTemplate { /** * 序列化:对象 -> json 字符串 */ @Override public String objectToJson(Object obj) { if(SaFoxUtil.isEmpty(obj)) { return null; } return JSON.toJSONString(obj); } /** * 反序列化:json 字符串 → 对象 */ @Override public T jsonToObject(String jsonStr, Class type) { if(SaFoxUtil.isEmpty(jsonStr)) { return null; } return JSON.parseObject(jsonStr, type); } } ================================================ FILE: sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateForFastjson; import cn.dev33.satoken.session.SaSessionForFastjsonCustomized; import cn.dev33.satoken.strategy.SaStrategy; /** * SaToken 插件安装:JSON 转换器 - Fastjson 版 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForFastjson implements SaTokenPlugin { @Override public void install() { // 设置JSON转换器:Fastjson 版 SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson()); // 重写 SaSession 生成策略 SaStrategy.instance.createSession = SaSessionForFastjsonCustomized::new; // 指定 SaSession 类型 SaStrategy.instance.sessionClassType = SaSessionForFastjsonCustomized.class; } } ================================================ FILE: sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/session/SaSessionForFastjsonCustomized.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import cn.dev33.satoken.util.SaFoxUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; /** * Fastjson 定制版 SaSession,重写类型转换API * * @author click33 * @since 1.34.0 */ public class SaSessionForFastjsonCustomized extends SaSession { private static final long serialVersionUID = -7600983549653130681L; /** * 构建一个 SaSession 对象 */ public SaSessionForFastjsonCustomized() { super(); } /** * 构建一个 SaSession 对象 * @param id Session 的 id */ public SaSessionForFastjsonCustomized(String id) { super(id); } /** * 取值 (指定转换类型) * @param 泛型 * @param key key * @param cs 指定转换类型 * @return 值 */ @Override public T getModel(String key, Class cs) { // 如果是想取出为基础类型 Object value = get(key); if(SaFoxUtil.isBasicType(cs)) { return SaFoxUtil.getValueByType(value, cs); } // 为空提前返回 if(valueIsNull(value)) { return null; } // 如果是 JSONObject 类型直接转,否则先转为 String 再转 if(value instanceof JSONObject) { JSONObject jo = (JSONObject) value; return jo.toJavaObject(cs); } else { return JSON.parseObject(value.toString(), cs); } } } ================================================ FILE: sa-token-plugin/sa-token-fastjson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForFastjson ================================================ FILE: sa-token-plugin/sa-token-fastjson2/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-fastjson2 sa-token-fastjson2 sa-token integrate Fastjson2 cn.dev33 sa-token-core com.alibaba.fastjson2 fastjson2 ================================================ FILE: sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson2.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.util.SaFoxUtil; import com.alibaba.fastjson2.JSON; /** * JSON 转换器, Fastjson2 版实现 * * @author click33 * @since 1.34.0 */ public class SaJsonTemplateForFastjson2 implements SaJsonTemplate { /** * 序列化:对象 -> json 字符串 */ @Override public String objectToJson(Object obj) { if(SaFoxUtil.isEmpty(obj)) { return null; } return JSON.toJSONString(obj); } /** * 反序列化:json 字符串 → 对象 */ @Override public T jsonToObject(String jsonStr, Class type) { if(SaFoxUtil.isEmpty(jsonStr)) { return null; } return JSON.parseObject(jsonStr, type); } } ================================================ FILE: sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson2.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateForFastjson2; import cn.dev33.satoken.session.SaSessionForFastjson2Customized; import cn.dev33.satoken.strategy.SaStrategy; /** * SaToken 插件安装:JSON 转换器 - Fastjson2 版 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForFastjson2 implements SaTokenPlugin { @Override public void install() { // 设置 JSON 转换器:Fastjson2 版 SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2()); // 重写 SaSession 生成策略 SaStrategy.instance.createSession = SaSessionForFastjson2Customized::new; // 指定 SaSession 类型 SaStrategy.instance.sessionClassType = SaSessionForFastjson2Customized.class; } } ================================================ FILE: sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/session/SaSessionForFastjson2Customized.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import cn.dev33.satoken.util.SaFoxUtil; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; /** * Fastjson2 定制版 SaSession,重写类型转换API * * @author click33 * @since 1.34.0 */ public class SaSessionForFastjson2Customized extends SaSession { private static final long serialVersionUID = -7600983549653130681L; /** * 构建一个 SaSession 对象 */ public SaSessionForFastjson2Customized() { super(); } /** * 构建一个 SaSession 对象 * @param id Session 的 id */ public SaSessionForFastjson2Customized(String id) { super(id); } /** * 取值 (指定转换类型) * @param 泛型 * @param key key * @param cs 指定转换类型 * @return 值 */ @Override public T getModel(String key, Class cs) { // 如果是想取出为基础类型 Object value = get(key); if(SaFoxUtil.isBasicType(cs)) { return SaFoxUtil.getValueByType(value, cs); } // 为空提前返回 if(valueIsNull(value)) { return null; } // 如果是 JSONObject 类型直接转,否则先转为 String 再转 if(value instanceof JSONObject) { JSONObject jo = (JSONObject) value; return jo.to(cs); } else { return JSON.parseObject(value.toString(), cs); } } } ================================================ FILE: sa-token-plugin/sa-token-fastjson2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForFastjson2 ================================================ FILE: sa-token-plugin/sa-token-forest/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-forest sa-token-forest sa-token integrate Forest cn.dev33 sa-token-core com.dtflys.forest forest-core ================================================ FILE: sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForForest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.http; import cn.dev33.satoken.SaManager; import com.dtflys.forest.Forest; import java.util.Map; /** * Http 请求处理器, Forest 版实现 * * @author click33 * @since 1.43.0 */ public class SaHttpTemplateForForest implements SaHttpTemplate { @Override public String get(String url) { SaManager.log.debug("发起请求,GET:{}", url); String res = Forest.get(url).executeAsString(); SaManager.log.debug("返回结果:{}", res); return res; } @Override public String postByFormData(String url, Map params) { SaManager.log.debug("发起请求,POST:{}\t参数:{}", url, params); String res = Forest.post(url).addBody(params).executeAsString(); SaManager.log.debug("返回结果:{}", res); return res; } } ================================================ FILE: sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForForest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.http.SaHttpTemplateForForest; import com.dtflys.forest.config.ForestConfiguration; /** * SaToken 插件安装:Http 请求处理器 - Forest 版 * * @author click33 * @since 1.43.0 */ public class SaTokenPluginForForest implements SaTokenPlugin { @Override public void install() { // 关闭 Forest 默认日志打印 ForestConfiguration.getDefaultConfiguration().setLogEnabled(false); // 设置 Forest 作为 Http 请求处理器 SaManager.setSaHttpTemplate(new SaHttpTemplateForForest()); } } ================================================ FILE: sa-token-plugin/sa-token-forest/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForForest ================================================ FILE: sa-token-plugin/sa-token-freemarker/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-freemarker sa-token-freemarker sa-token-freemarker cn.dev33 sa-token-core org.freemarker freemarker true ================================================ FILE: sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateDirectiveModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.freemarker.dialect; import freemarker.core.Environment; import freemarker.template.TemplateDirectiveBody; import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; import java.io.IOException; import java.util.Map; import java.util.function.Function; /** * Sa-Token Freemarker 标签模板指令模型 * * @author click33 * @since 1.40.0 */ public class SaTokenTemplateDirectiveModel implements TemplateDirectiveModel { /* * 参考资料: * - https://blog.csdn.net/m0_64210833/article/details/135994864 * - https://blog.csdn.net/qq_35752835/article/details/111321893 */ /** * 使用标签指令模板时,指定值的属性名 */ String attrName; /** * 断言函数,返回 true 时标签内容显示,返回 false 时标签内容不显示 */ Function fun; public SaTokenTemplateDirectiveModel(String attrName, Function fun) { this.attrName = attrName; this.fun = fun; } @Override public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException { // 获取 value Object obj = map.get(attrName); String value = obj == null ? null : obj.toString(); // 使用断言函数判断是否显示标签内容 if(this.fun.apply(value)) { templateDirectiveBody.render(environment.getOut()); } } } ================================================ FILE: sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.freemarker.dialect; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import freemarker.template.SimpleHash; import java.util.List; /** * Sa-Token Freemarker 标签模板模型 * * @author click33 * @since 1.40.0 */ public class SaTokenTemplateModel extends SimpleHash { /** * 默认值属性名 */ public static final String DEFAULT_ATTR_NAME = "value"; /** * 底层使用的 StpLogic */ public StpLogic stpLogic; /** * 使用默认参数注册标签模板模型 */ public SaTokenTemplateModel() { this(DEFAULT_ATTR_NAME, StpUtil.stpLogic); } /** * 构造标签模板模型,使用自定义参数 * * @param stpLogic 使用的 StpLogic 对象 */ public SaTokenTemplateModel(StpLogic stpLogic) { this(DEFAULT_ATTR_NAME, stpLogic); } /** * 构造标签模板模型,使用自定义参数 * * @param attrName 属性名 * @param stpLogic 使用的 StpLogic 对象 */ public SaTokenTemplateModel(String attrName, StpLogic stpLogic) { this.stpLogic = stpLogic; // 登录判断 put("login", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.isLogin())); put("notLogin", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.isLogin())); // 角色判断 put("hasRole", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRole(value))); put("hasRoleAnd", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleAnd(toArray(value)))); put("hasRoleOr", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleOr(toArray(value)))); put("notRole", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value))); put("lackRole", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value))); // 权限判断 put("hasPermission", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermission(value))); put("hasPermissionAnd", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionAnd(toArray(value)))); put("hasPermissionOr", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionOr(toArray(value)))); put("notPermission", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value))); put("lackPermission", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value))); } /** * String 转 Array * @param str 字符串 * @return 数组 */ public String[] toArray(String str) { List list = SaFoxUtil.convertStringToList(str); return list.toArray(new String[0]); } } ================================================ FILE: sa-token-plugin/sa-token-grpc/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-grpc sa-token-grpc sa-token-grpc 8 8 UTF-8 net.devh grpc-spring-boot-starter cn.dev33 sa-token-core ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/constants/GrpcContextConstants.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.constants; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.util.SaTokenConsts; import io.grpc.Metadata; /** * 常量 * * @author lym * @since 1.34.0 */ public class GrpcContextConstants { public static final Metadata.Key SA_SAME_TOKEN = Metadata.Key.of(SaSameUtil.SAME_TOKEN, Metadata.ASCII_STRING_MARSHALLER); public static final Metadata.Key SA_JUST_CREATED_NOT_PREFIX = Metadata.Key.of(SaTokenConsts.JUST_CREATED_NOT_PREFIX, Metadata.ASCII_STRING_MARSHALLER); } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/context/SaTokenGrpcContext.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.context; import io.grpc.*; import java.util.HashMap; import java.util.Map; /** * @author lym * @since 1.34.0 **/ public class SaTokenGrpcContext { /** * grpc请求上下文。请求完成后会由grpc自动清空 * * @see Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler) */ private static final Context.Key> SA_TOKEN_CONTEXT_KEY = Context.key("sa-token-context"); public static Object get(String key) { return SA_TOKEN_CONTEXT_KEY.get().get(key); } public static void set(String key, Object value) { SA_TOKEN_CONTEXT_KEY.get().put(key, value); } public static void removeKey(String key) { SA_TOKEN_CONTEXT_KEY.get().remove(key); } public static Map getContext() { return SA_TOKEN_CONTEXT_KEY.get(); } public static boolean isNotNull() { return SA_TOKEN_CONTEXT_KEY.get() != null; } public static Context create() { return Context.current().withValue(SaTokenGrpcContext.SA_TOKEN_CONTEXT_KEY, new HashMap<>()); } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenContextGrpcServerInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.interceptor; import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; import io.grpc.*; import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; import org.springframework.core.Ordered; /** * 处理请求前,创建上下文 * * @author lym * @since 1.34.0 */ @GrpcGlobalServerInterceptor public class SaTokenContextGrpcServerInterceptor implements ServerInterceptor, Ordered { @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { Context ctx = SaTokenGrpcContext.create(); return Contexts.interceptCall(ctx, call, headers, next); } /** * 必须最先创建上下文,后面的拦截器才能获取到上下文 */ @Override public int getOrder() { return HIGHEST_PRECEDENCE; } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcClientInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.interceptor; import org.springframework.core.Ordered; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaTokenContextDefaultImpl; import cn.dev33.satoken.context.grpc.constants.GrpcContextConstants; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.ForwardingClientCall; import io.grpc.ForwardingClientCallListener; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; /** * 客户端请求的时候,带上token * * @author lym * @since 1.34.0 */ @GrpcGlobalClientInterceptor public class SaTokenGrpcClientInterceptor implements ClientInterceptor, Ordered { @Override public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { // 追加 Same-Token 参数 if (SaManager.getConfig().getCheckSameToken()) { headers.put(GrpcContextConstants.SA_SAME_TOKEN, SaSameUtil.getToken()); } // 调用前,传递会话Token String tokenValue = StpUtil.getTokenValue(); if (SaFoxUtil.isNotEmpty(tokenValue) && SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) { headers.put(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX, tokenValue); } super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { /** * 服务端结束响应后,解析回传的Token值 */ @Override public void onClose(Status status, Metadata responseHeader) { StpUtil.setTokenValue(responseHeader.get(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX)); super.onClose(status, responseHeader); } }, headers); } }; } @Override public int getOrder() { return HIGHEST_PRECEDENCE; } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcServerInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.interceptor; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.grpc.constants.GrpcContextConstants; import cn.dev33.satoken.context.grpc.util.SaTokenContextGrpcUtil; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import io.grpc.*; import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; /** * 鉴权,设置token * * @author lym * @since 1.34.0 **/ @GrpcGlobalServerInterceptor public class SaTokenGrpcServerInterceptor implements ServerInterceptor { @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { try{ // 初始化上下文 SaTokenContextGrpcUtil.setContext(); // RPC 调用鉴权 if (SaManager.getConfig().getCheckSameToken()) { String sameToken = headers.get(GrpcContextConstants.SA_SAME_TOKEN); SaSameUtil.checkToken(sameToken); } String tokenFromClient = headers.get(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX); StpUtil.setTokenValue(tokenFromClient); return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall(call) { /** * 结束响应时,若本服务生成了新token,将其传回客户端 */ @Override public void close(Status status, Metadata responseHeaders) { String justCreateToken = StpUtil.getTokenValue(); if (!SaFoxUtil.equals(justCreateToken, tokenFromClient) && SaFoxUtil.isNotEmpty(justCreateToken)) { responseHeaders.put(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX, justCreateToken); } super.close(status, responseHeaders); } }, headers); }finally { // 清除上下文 SaTokenContextGrpcUtil.clearContext(); } } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaRequestForGrpc.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.model; import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; import cn.dev33.satoken.context.model.SaRequest; import java.util.Collection; import java.util.Map; /** * Request for grpc * * @author lym * @since 1.34.0 */ public class SaRequestForGrpc implements SaRequest { /** * 获取底层源对象 */ @Override public Object getSource() { return SaTokenGrpcContext.getContext(); } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { // 不传播 url 参数 return null; } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return null; } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ return null; } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { // 不传播 header 参数 return null; } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { // 不传播 cookie 参数 return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ // 不传播 cookie 参数 return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ // 不传播 cookie 参数 return null; } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { // 不传播 requestPath return null; } /** * 返回当前请求的url,例:http://xxx.com/test * * @return see note */ public String getUrl() { // 不传播 url return null; } /** * 返回当前请求的类型 */ @Override public String getMethod() { // 不传播 method return null; } @Override public String getHost() { return null; } /** * 转发请求 */ @Override public Object forward(String path) { // 不传播 forward 动作 return null; } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaResponseForGrpc.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.model; import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; import cn.dev33.satoken.context.model.SaResponse; /** * Response for grpc * * @author lym * @since 1.34.0 */ public class SaResponseForGrpc implements SaResponse { /** * 获取底层源对象 */ @Override public Object getSource() { return SaTokenGrpcContext.getContext(); } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { // 不回传 status 状态 return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { // 不回传 header响应头 return this; } /** * 在响应头里添加一个值 * * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { // 不回传 header响应头 return this; } /** * 重定向 */ @Override public Object redirect(String url) { // 不回传 重定向 动作 return null; } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaStorageForGrpc.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.model; import cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext; import cn.dev33.satoken.context.model.SaStorage; /** * Storage for grpc * * @author lym * @since 1.34.0 */ public class SaStorageForGrpc implements SaStorage { /** * 获取底层源对象 */ @Override public Object getSource() { return SaTokenGrpcContext.getContext(); } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorage set(String key, Object value) { SaTokenGrpcContext.set(key, value); return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return SaTokenGrpcContext.get(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorage delete(String key) { SaTokenGrpcContext.removeKey(key); return this; } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/util/SaTokenContextGrpcUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.context.grpc.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.grpc.model.SaRequestForGrpc; import cn.dev33.satoken.context.grpc.model.SaResponseForGrpc; import cn.dev33.satoken.context.grpc.model.SaStorageForGrpc; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextGrpcUtil { /** * 写入当前上下文 */ public static void setContext() { SaRequest saRequest = new SaRequestForGrpc(); SaResponse saResponse = new SaResponseForGrpc(); SaStorage saStorage = new SaStorageForGrpc(); SaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage); } /** * 清除当前上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } } ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcClientInterceptor cn.dev33.satoken.context.grpc.interceptor.SaTokenContextGrpcServerInterceptor cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcServerInterceptor cn.dev33.satoken.context.grpc.SaTokenSecondContextCreatorForGrpc ================================================ FILE: sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcClientInterceptor,\ cn.dev33.satoken.context.grpc.interceptor.SaTokenContextGrpcServerInterceptor,\ cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcServerInterceptor,\ cn.dev33.satoken.context.grpc.SaTokenSecondContextCreatorForGrpc ================================================ FILE: sa-token-plugin/sa-token-hutool-timed-cache/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-hutool-timed-cache sa-token-hutool-timed-cache sa-token integrate hutool-TimedCache cn.dev33 sa-token-core cn.hutool hutool-cache ================================================ FILE: sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForHutoolTimedCache.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject; import cn.dev33.satoken.util.SaFoxUtil; import cn.hutool.cache.CacheUtil; import cn.hutool.cache.impl.CacheObj; import cn.hutool.cache.impl.TimedCache; import java.util.Iterator; import java.util.List; /** * Sa-Token 持久层接口(基于 Hutool-TimedCache,系统重启后数据丢失) * * @author click33 * @since 1.38.0 */ public class SaTokenDaoForHutoolTimedCache implements SaTokenDaoByStringFollowObject { // /** * 底层缓存对象: * 参数填1000,代表默认ttl为1000毫秒,实际上此参数意义不大,因为后续每个值都会单独设置自己的ttl值 */ public TimedCache timedCache = CacheUtil.newTimedCache(1000); // ------------------------ Object 读写操作 @Override public Object getObject(String key) { // 第二个参数代表:是否刷新最后访问时间 // 设置为false,因为我们不需要刷新最后访问时间,只需要取值即可 return timedCache.get(key, false); } @Override public T getObject(String key, Class classType) { return (T) getObject(key); } @Override public void setObject(String key, Object object, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 如果为永不过期 // 在 sa-token 中,-1 代表永不过期 // 在 hutool-TimedCache 中,0 代表永不过期 // 为了适应 hutool-TimedCache 规范,这里将 -1 转换为 0 if(timeout == SaTokenDao.NEVER_EXPIRE) { timedCache.put(key, object, 0); return; } // 正常情况 timedCache.put(key, object, timeout * 1000); } @Override public void updateObject(String key, Object object) { long expire = getObjectTimeout(key); // -2 = 无此键 if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.setObject(key, object, expire); } @Override public void deleteObject(String key) { timedCache.remove(key); } @Override public long getObjectTimeout(String key) { return getKeyTimeout(key); } @Override public void updateObjectTimeout(String key, long timeout) { // $$待优化:对一个不存在的key进行修改timeout操作时,可能会造成一些意外数据,待进一步测试 this.setObject(key, this.getObject(key), timeout); } // ------------------------ Session 读写操作 // 使用接口默认实现 // --------- 会话管理 @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { return SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType); } // --------- 过期时间相关操作 /** * 获取指定 key 的剩余存活时间 (单位:秒) * @param key 指定 key * @return 这个 key 的剩余存活时间,返回-1=永不过期,返回-2=无此键 */ long getKeyTimeout(String key) { final Iterator> values = timedCache.cacheObjIterator(); CacheObj co; while (values.hasNext()) { co = values.next(); if(co.getKey().equals(key)) { long ttl = co.getTtl(); // 在 Hutool-TimedCache 中,ttl=0 (或<0) 代表永不过期,统一返回 Sa-Token 可以理解的 -1 if(ttl <= 0) { return NEVER_EXPIRE; } // 不为 0,那就计算一下剩余有效期 // 单位:毫秒 long timeout = ttl - (System.currentTimeMillis() - co.getLastAccess()); if(timeout < 0) { timeout = 0; } // 转秒返回 return timeout / 1000; } } // 代码至此,说明缓存中没有这个值 return NOT_VALUE_EXPIRE; } // --------- 定时清理过期数据 /** * 组件被安装时,开始刷新数据线程 */ @Override public void init() { // 定时清理间隔 int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); // 配置为<=0代表不启用定时清理 if(dataRefreshPeriod <= 0) { return; } // 启用定时清理(转毫秒) timedCache.schedulePrune(dataRefreshPeriod * 1000L); } /** * 组件被卸载时,结束定时任务,不再定时清理过期数据 */ @Override public void destroy() { timedCache.cancelPruneSchedule(); } } ================================================ FILE: sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForHutoolCache.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDaoForHutoolTimedCache; /** * SaToken 插件安装:DAO 扩展 - Hutool-TimedCache 版 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForHutoolCache implements SaTokenPlugin { @Override public void install() { SaManager.setSaTokenDao(new SaTokenDaoForHutoolTimedCache()); } } ================================================ FILE: sa-token-plugin/sa-token-hutool-timed-cache/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForHutoolCache ================================================ FILE: sa-token-plugin/sa-token-jackson/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-jackson sa-token-jackson sa-token-jackson cn.dev33 sa-token-core com.fasterxml.jackson.core jackson-databind true com.fasterxml.jackson.datatype jackson-datatype-jsr310 true ================================================ FILE: sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.exception.SaJsonConvertException; import cn.dev33.satoken.util.SaFoxUtil; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Map; /** * JSON 转换器, Jackson 版实现 * * @author click33 * @since 1.34.0 */ public class SaJsonTemplateForJackson implements SaJsonTemplate { public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; public static final String DATE_PATTERN = "yyyy-MM-dd"; public static final String TIME_PATTERN = "HH:mm:ss"; public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN); /** * 底层 Mapper 对象 */ public ObjectMapper objectMapper = new ObjectMapper(); public SaJsonTemplateForJackson() { // 1、使 objectMapper 序列化时带上类型信息,以便该 json 字符串可以成功反序列化 // 构建反序列化限制器,此处可以限制只允许指定类型或指定包下的类型才可以反序列化,此处指定所有类型都可以反序列化 PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder() // 允许所有子类型反序列化(即反序列化时遇到的类) .allowIfSubType(Object.class) // 允许所有基类型反序列化(如 Object、自定义抽象类) .allowIfBaseType(Object.class) .build(); // 启用全局默认类型(嵌入类型信息) objectMapper.activateDefaultTyping( ptv, // 对非 final 类嵌入类型信息 ObjectMapper.DefaultTyping.NON_FINAL, // 类型信息以属性形式存在("@class") JsonTypeInfo.As.PROPERTY ); // 2、使空 bean 在序列化时也能记录类型信息,而不是只序列化成 {} objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // 3、配置 [ 忽略未知字段 ] this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 4、配置 [ 时间类型转换 ] JavaTimeModule timeModule = new JavaTimeModule(); // LocalDateTime序列化与反序列化 timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); // LocalDate序列化与反序列化 timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER)); timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER)); // LocalTime序列化与反序列化 timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER)); timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER)); // this.objectMapper.registerModule(timeModule); } /** * 序列化:对象 -> json 字符串 */ @Override public String objectToJson(Object obj) { if(SaFoxUtil.isEmpty(obj)) { return null; } try { if(obj instanceof Map) { return mapObjectMapper.writeValueAsString(obj); } return objectMapper.writeValueAsString(obj); } catch (JsonProcessingException e) { throw new SaJsonConvertException(e); } } /** * 反序列化:json 字符串 → 对象 */ @Override public T jsonToObject(String jsonStr, Class type) { if(SaFoxUtil.isEmpty(jsonStr)) { return null; } try { return objectMapper.readValue(jsonStr, type); } catch (JsonProcessingException e) { throw new SaJsonConvertException(e); } } /* * 由于构造方法中的如下代码: * ObjectMapper.DefaultTyping.NON_FINAL, * 导致 objectMapper 对所有非 final 类型的反序列化均要求提供 @class 信息。 * * 例如: * 一个简单的字符串 {"name": "zhangsan"} 将无法反序列化为 Map 对象,因为这个字符串上没有提供 @class 信息。 * * 尝试诸多方案,均未能解决此问题。 * * 因此,以下代码将为 Map 的反序列化提供一个独立干净的 mapObjectMapper 对象,保证其不受构造方法中关于类型配置的影响。 * */ /** * 处理 Map 的序列化与反序列化 */ public ObjectMapper mapObjectMapper = new ObjectMapper(); /** * 将 json 字符串解析为 Map */ @Override public Map jsonToMap(String jsonStr) { if(SaFoxUtil.isEmpty(jsonStr)) { return null; } try { @SuppressWarnings("unchecked") Map map = mapObjectMapper.readValue(jsonStr, Map.class); return map; } catch (JsonProcessingException e) { throw new SaJsonConvertException(e); } } } ================================================ FILE: sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; import cn.dev33.satoken.json.SaJsonTemplateForJackson; /** * SaToken 插件安装:JSON 转换器 (Jackson 版) * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForJackson implements SaTokenPlugin { @Override public void install() { // 只有在未提供自定义的 json 解析器时才会生效,给于其较弱的优先级 if(SaManager.getSaJsonTemplate().getClass() == SaJsonTemplateDefaultImpl.class){ SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson()); } } } ================================================ FILE: sa-token-plugin/sa-token-jackson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForJackson ================================================ FILE: sa-token-plugin/sa-token-jackson3/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-jackson3 sa-token-jackson3 sa-token-jackson3: Sa-Token 与 Jackson 3 整合 cn.dev33 sa-token-core tools.jackson.core jackson-databind true org.apache.maven.plugins maven-compiler-plugin 17 17 ================================================ FILE: sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.exception.SaJsonConvertException; import cn.dev33.satoken.util.SaFoxUtil; import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.DefaultTyping; import tools.jackson.databind.SerializationFeature; import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import tools.jackson.databind.jsontype.PolymorphicTypeValidator; import tools.jackson.databind.json.JsonMapper; import java.util.Map; /** * JSON 转换器,Jackson 3 版实现 * * @author click33 * @since 1.45.0 */ public class SaJsonTemplateForJackson3 implements SaJsonTemplate { /** * 底层 Mapper 对象(带多态类型信息,用于 Session 等复杂对象序列化) */ public final JsonMapper objectMapper; /** * 处理 Map 的 Mapper(无多态类型配置,用于简单 JSON 解析) */ public final JsonMapper mapObjectMapper; public SaJsonTemplateForJackson3() { // 1、构建反序列化限制器(PolymorphicTypeValidator),限制哪些类型可以被自动多态反序列化 // 这里允许所有 Object 类型和其子类型参与多态反序列化,从而支持复杂对象(如 SaSession)的序列化、反序列化 PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder() .allowIfSubType(Object.class) // 允许 Object.class 的子类型被反序列化 .allowIfBaseType(Object.class) // 允许 Object.class 作为基类型(在启用多态时) .build(); // 构建 Validator 实例 // 2、通过 JsonMapper 的 builder 模式创建 objectMapper(用于带多态类型信息的 JSON 处理,Jackson3 默认不可变需用构建器) this.objectMapper = JsonMapper.builder() // 启用默认多态类型处理,并以 "@class" 作为写入类型信息的属性名(即持久化时包含类型字段),仅对非 final 类型生效 .activateDefaultTypingAsProperty(ptv, DefaultTyping.NON_FINAL, "@class") .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) // 序列化时如果 bean 没有属性,则不抛出异常 .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // 反序列化时如果有未知属性,也不抛出异常(兼容性更好) .build(); // 构建真正的 JsonMapper 实例 // 3、创建 mapObjectMapper,用于简单类型(如 Map)的序列化和反序列化,和 objectMapper 独立,且不启用多态类型 this.mapObjectMapper = JsonMapper.builder() .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) // 序列化时如果 bean 没有属性,则不抛出异常 .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // 反序列化时如果有未知属性,也不抛出异常,避免出错 .build(); // 构建用于处理简单场景的 JsonMapper 实例 } /** * 序列化:对象 -> json 字符串 */ @Override public String objectToJson(Object obj) { if (SaFoxUtil.isEmpty(obj)) { return null; } try { if (obj instanceof Map) { return mapObjectMapper.writeValueAsString(obj); } return objectMapper.writeValueAsString(obj); } catch (JacksonException e) { throw new SaJsonConvertException(e); } } /** * 反序列化:json 字符串 → 对象 */ @Override public T jsonToObject(String jsonStr, Class type) { if (SaFoxUtil.isEmpty(jsonStr)) { return null; } try { return objectMapper.readValue(jsonStr, type); } catch (JacksonException e) { throw new SaJsonConvertException(e); } } /** * 将 json 字符串解析为 Map */ @Override public Map jsonToMap(String jsonStr) { if (SaFoxUtil.isEmpty(jsonStr)) { return null; } try { @SuppressWarnings("unchecked") Map map = mapObjectMapper.readValue(jsonStr, Map.class); return map; } catch (JacksonException e) { throw new SaJsonConvertException(e); } } } ================================================ FILE: sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; import cn.dev33.satoken.json.SaJsonTemplateForJackson3; /** * SaToken 插件安装:JSON 转换器 (Jackson 3 版) * * @author click33 * @since 1.45.0 */ public class SaTokenPluginForJackson3 implements SaTokenPlugin { @Override public void install() { // 只有在未提供自定义的 json 解析器时才会生效,给予其较弱的优先级 if (SaManager.getSaJsonTemplate().getClass() == SaJsonTemplateDefaultImpl.class) { SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3()); } } } ================================================ FILE: sa-token-plugin/sa-token-jackson3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForJackson3 ================================================ FILE: sa-token-plugin/sa-token-jwt/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-jwt sa-token-jwt sa-token-jwt cn.dev33 sa-token-core cn.hutool hutool-jwt ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.jwt.error.SaJwtErrorCode; import cn.dev33.satoken.jwt.exception.SaJwtException; import cn.dev33.satoken.util.SaFoxUtil; import cn.hutool.json.JSONException; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTException; import cn.hutool.jwt.signers.JWTSigner; import cn.hutool.jwt.signers.JWTSignerUtil; import java.util.Map; import java.util.Objects; /** * jwt 操作模板方法封装 * * @author click33 * @since 1.31.0 */ public class SaJwtTemplate { /** * key:账号类型 */ public static final String LOGIN_TYPE = "loginType"; /** * key:账号id */ public static final String LOGIN_ID = "loginId"; /** * key:登录设备类型 */ public static final String DEVICE_TYPE = "deviceType"; /** * key:有效截止期 (时间戳) */ public static final String EFF = "eff"; /** * key:乱数 ( 混入随机字符串,防止每次生成的 token 都是一样的 ) */ public static final String RN_STR = "rnStr"; /** * 当有效期被设为此值时,代表永不过期 */ public static final long NEVER_EXPIRE = SaTokenDao.NEVER_EXPIRE; /** * 表示一个值不存在 */ public static final long NOT_VALUE_EXPIRE = SaTokenDao.NOT_VALUE_EXPIRE; // ------ 创建 /** * 创建 jwt (简单方式) * * @param loginType 登录类型 * @param loginId 账号id * @param extraData 扩展数据 * @param keyt 秘钥 * @return jwt-token */ public String createToken(String loginType, Object loginId, Map extraData, String keyt) { // 构建 JWT jwt = JWT.create() .setPayload(LOGIN_TYPE, loginType) .setPayload(LOGIN_ID, loginId) // 塞入一个随机字符串,防止同账号下每次生成的 token 都一样的 .setPayload(RN_STR, SaFoxUtil.getRandomString(32)) .addPayloads(extraData) ; // 返回 return generateToken(jwt, keyt); } /** * 创建 jwt (全参数方式) * * @param loginType 账号类型 * @param loginId 账号id * @param deviceType 设备类型 * @param timeout token有效期 (单位 秒) * @param extraData 扩展数据 * @param keyt 秘钥 * @return jwt-token */ public String createToken(String loginType, Object loginId, String deviceType, long timeout, Map extraData, String keyt) { // 计算 eff 有效期: // 如果 timeout 指定为 -1,那么 eff 也为 -1,代表永不过期 // 如果 timeout 指定为一个具体的值,那么 eff 为 13 位时间戳,代表此 token 到期的时间 long effTime = timeout; if(timeout != NEVER_EXPIRE) { effTime = timeout * 1000 + System.currentTimeMillis(); } // 创建 JWT jwt = JWT.create() .setPayload(LOGIN_TYPE, loginType) .setPayload(LOGIN_ID, loginId) .setPayload(DEVICE_TYPE, deviceType) .setPayload(EFF, effTime) // 塞入一个随机字符串,防止同账号同一毫秒下每次生成的 token 都一样的 .setPayload(RN_STR, SaFoxUtil.getRandomString(32)) .addPayloads(extraData); // 返回 return generateToken(jwt, keyt); } /** * 为 JWT 对象和 keyt 秘钥,生成 token 字符串 * * @param jwt JWT构建对象 * @param keyt 秘钥 * @return 根据 JWT 对象和 keyt 秘钥,生成的 token 字符串 */ public String generateToken (JWT jwt, String keyt) { return jwt.setSigner(createSigner(keyt)).sign(); } /** * 返回 jwt 使用的签名算法 * * @param keyt 秘钥 * @return / */ public JWTSigner createSigner (String keyt) { return JWTSignerUtil.hs256(keyt.getBytes()); } // ------ 解析 /** * jwt 解析 * * @param token Jwt-Token值 * @param loginType 登录类型 * @param keyt 秘钥 * @param isCheckTimeout 是否校验 timeout 字段 * @return 解析后的jwt 对象 */ public JWT parseToken(String token, String loginType, String keyt, boolean isCheckTimeout) { // 秘钥不可以为空 if(SaFoxUtil.isEmpty(keyt)) { throw new SaJwtException("请配置 jwt 秘钥"); } // 如果token为null if(token == null) { throw new SaJwtException("jwt 字符串不可为空"); } // 解析 JWT jwt; try { jwt = JWT.of(token); } catch (JWTException | JSONException e) { throw new SaJwtException("jwt 解析失败:" + token, e).setCode(SaJwtErrorCode.CODE_30201); } JSONObject payloads = jwt.getPayloads(); // 校验 Token 签名 boolean verify = jwt.setSigner(createSigner(keyt)).verify(); if( ! verify) { throw new SaJwtException("jwt 签名无效:" + token).setCode(SaJwtErrorCode.CODE_30202); } // 校验 loginType if( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) { throw new SaJwtException("jwt loginType 无效:" + token).setCode(SaJwtErrorCode.CODE_30203); } // 校验 Token 有效期 if(isCheckTimeout) { Long effTime = payloads.getLong(EFF, 0L); if(effTime != NEVER_EXPIRE) { if(effTime == null || effTime < System.currentTimeMillis()) { throw new SaJwtException("jwt 已过期:" + token).setCode(SaJwtErrorCode.CODE_30204); } } } // 返回 return jwt; } /** * 获取 jwt 数据载荷 (校验 sign、loginType、timeout) * @param token token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 载荷 */ public JSONObject getPayloads(String token, String loginType, String keyt) { return parseToken(token, loginType, keyt, true).getPayloads(); } /** * 获取 jwt 数据载荷 (校验 sign、loginType,不校验 timeout) * @param token token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 载荷 */ public JSONObject getPayloadsNotCheck(String token, String loginType, String keyt) { return parseToken(token, loginType, keyt, false).getPayloads(); } /** * 获取 jwt 代表的账号id * @param token Token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 值 */ public Object getLoginId(String token, String loginType, String keyt) { return getPayloads(token, loginType, keyt).get(LOGIN_ID); } /** * 获取 jwt 代表的账号id (未登录时返回null) * @param token Token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 值 */ public Object getLoginIdOrNull(String token, String loginType, String keyt) { try { return getPayloads(token, loginType, keyt).get(LOGIN_ID); } catch (SaJwtException e) { return null; } } /** * 获取 jwt 剩余有效期 * @param token JwtToken值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 值 */ public long getTimeout(String token, String loginType, String keyt) { // 如果token为null if(token == null) { return NOT_VALUE_EXPIRE; } // 取出数据 JWT jwt; try { jwt = JWT.of(token); } catch (JWTException e) { // 解析失败 return NOT_VALUE_EXPIRE; } JSONObject payloads = jwt.getPayloads(); // 如果签名无效 boolean verify = jwt.setSigner(createSigner(keyt)).verify(); if( ! verify) { return NOT_VALUE_EXPIRE; } // 如果 loginType 无效 if( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) { return NOT_VALUE_EXPIRE; } // 如果被设置为:永不过期 Long effTime = payloads.get(EFF, Long.class); if(effTime == NEVER_EXPIRE) { return NEVER_EXPIRE; } // 如果已经超时 if(effTime == null || effTime < System.currentTimeMillis()) { return NOT_VALUE_EXPIRE; } // 计算timeout (转化为以秒为单位的有效时间) return (effTime - System.currentTimeMillis()) / 1000; } // -------------- 其它方法 /** * 创建 jwt (Map 参数方式) * * @param map 扩展数据 * @param keyt 秘钥 * @return jwt-token */ public String createToken(Map map, String keyt) { // 创建 JWT jwt = JWT.create().addPayloads(map); // 返回 return generateToken(jwt, keyt); } } ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; import java.util.Map; /** * jwt 操作工具类封装 * * @author click33 * @since 1.27.1 */ public class SaJwtUtil { /** * 底层 saJwtTemplate 对象 */ public static SaJwtTemplate saJwtTemplate = new SaJwtTemplate(); /** * 获取底层 saJwtTemplate 对象 * @return / */ public static SaJwtTemplate getSaJwtTemplate() { return saJwtTemplate; } /** * 设置底层 saJwtTemplate 对象 * @param saJwtTemplate / */ public static void setSaJwtTemplate(SaJwtTemplate saJwtTemplate) { SaJwtUtil.saJwtTemplate = saJwtTemplate; } // 常量 /** * key:账号类型 */ public static final String LOGIN_TYPE = SaJwtTemplate.LOGIN_TYPE; /** * key:账号id */ public static final String LOGIN_ID = SaJwtTemplate.LOGIN_ID; /** * key:登录设备类型 */ public static final String DEVICE_TYPE = SaJwtTemplate.DEVICE_TYPE; /** * key:有效截止期 (时间戳) */ public static final String EFF = SaJwtTemplate.EFF; /** * key:乱数 ( 混入随机字符串,防止每次生成的 token 都是一样的 ) */ public static final String RN_STR = SaJwtTemplate.RN_STR; /** * 当有效期被设为此值时,代表永不过期 */ public static final long NEVER_EXPIRE = SaJwtTemplate.NEVER_EXPIRE; /** * 表示一个值不存在 */ public static final long NOT_VALUE_EXPIRE = SaJwtTemplate.NOT_VALUE_EXPIRE; // ------ 创建 /** * 创建 jwt (简单方式) * @param loginType 登录类型 * @param loginId 账号id * @param extraData 扩展数据 * @param keyt 秘钥 * @return jwt-token */ public static String createToken(String loginType, Object loginId, Map extraData, String keyt) { return saJwtTemplate.createToken(loginType, loginId, extraData, keyt); } /** * 创建 jwt (全参数方式) * @param loginType 账号类型 * @param loginId 账号id * @param deviceType 设备类型 * @param timeout token有效期 (单位 秒) * @param extraData 扩展数据 * @param keyt 秘钥 * @return jwt-token */ public static String createToken(String loginType, Object loginId, String deviceType, long timeout, Map extraData, String keyt) { return saJwtTemplate.createToken(loginType, loginId, deviceType, timeout, extraData, keyt); } /** * 为 JWT 对象和 keyt 秘钥,生成 token 字符串 * @param jwt JWT构建对象 * @param keyt 秘钥 * @return 根据 JWT 对象和 keyt 秘钥,生成的 token 字符串 */ public static String generateToken (JWT jwt, String keyt) { return saJwtTemplate.generateToken(jwt, keyt); } // ------ 解析 /** * jwt 解析 * @param token Jwt-Token值 * @param loginType 登录类型 * @param keyt 秘钥 * @param isCheckTimeout 是否校验 timeout 字段 * @return 解析后的jwt 对象 */ public static JWT parseToken(String token, String loginType, String keyt, boolean isCheckTimeout) { return saJwtTemplate.parseToken(token, loginType, keyt, isCheckTimeout); } /** * 获取 jwt 数据载荷 (校验 sign、loginType、timeout) * @param token token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 载荷 */ public static JSONObject getPayloads(String token, String loginType, String keyt) { return saJwtTemplate.getPayloads(token, loginType, keyt); } /** * 获取 jwt 数据载荷 (校验 sign、loginType,不校验 timeout) * @param token token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 载荷 */ public static JSONObject getPayloadsNotCheck(String token, String loginType, String keyt) { return saJwtTemplate.getPayloadsNotCheck(token, loginType, keyt); } /** * 获取 jwt 代表的账号id * @param token Token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 值 */ public static Object getLoginId(String token, String loginType, String keyt) { return saJwtTemplate.getLoginId(token, loginType, keyt); } /** * 获取 jwt 代表的账号id (未登录时返回null) * @param token Token值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 值 */ public static Object getLoginIdOrNull(String token, String loginType, String keyt) { return saJwtTemplate.getLoginIdOrNull(token, loginType, keyt); } /** * 获取 jwt 剩余有效期 * @param token JwtToken值 * @param loginType 登录类型 * @param keyt 秘钥 * @return 值 */ public static long getTimeout(String token, String loginType, String keyt) { return saJwtTemplate.getTimeout(token, loginType, keyt); } // -------------- 其它方法 /** * 创建 jwt (Map 参数方式) * * @param map 扩展数据 * @param keyt 秘钥 * @return jwt-token */ public static String createToken(Map map, String keyt) { return saJwtTemplate.createToken(map, keyt); } } ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForMixin.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.jwt.error.SaJwtErrorCode; import cn.dev33.satoken.jwt.exception.SaJwtException; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTokenConsts; import java.util.List; import java.util.Map; /** * Sa-Token 整合 jwt -- Mixin 混入模式 * * @author click33 * @since 1.30.0 */ public class StpLogicJwtForMixin extends StpLogic { /** * Sa-Token 整合 jwt -- Mixin 混入 */ public StpLogicJwtForMixin() { super(StpUtil.TYPE); } /** * Sa-Token 整合 jwt -- Mixin 混入 * @param loginType 账号体系标识 */ public StpLogicJwtForMixin(String loginType) { super(loginType); } /** * 获取jwt秘钥 * @return / */ public String jwtSecretKey() { String keyt = getConfigOrGlobal().getJwtSecretKey(); SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); return keyt; } // // ------ 重写方法 // // ------------------- 获取token 相关 ------------------- /** * 创建一个TokenValue */ @Override public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { return SaJwtUtil.createToken(loginType, loginId, deviceType, timeout, extraData, jwtSecretKey()); } /** * 获取当前会话的Token信息 * @return token信息 */ @Override public SaTokenInfo getTokenInfo() { SaTokenInfo info = new SaTokenInfo(); info.tokenName = getTokenName(); info.tokenValue = getTokenValue(); info.isLogin = isLogin(); info.loginId = getLoginIdDefaultNull(); info.loginType = getLoginType(); info.tokenTimeout = getTokenTimeout(); info.sessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; info.tokenSessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; info.tokenActiveTimeout = SaTokenDao.NOT_VALUE_EXPIRE; info.loginDeviceType = getLoginDeviceType(); return info; } // ------------------- 登录相关操作 ------------------- /** * 获取指定Token对应的账号id (不做任何特殊处理) */ @Override public String getLoginIdNotHandle(String tokenValue) { try { Object loginId = SaJwtUtil.getLoginId(tokenValue, loginType, jwtSecretKey()); return String.valueOf(loginId); } catch (SaJwtException e) { // CODE == 30204 时,代表token已过期,此时返回-3,以便外层更精确的显示异常信息 if(e.getCode() == SaJwtErrorCode.CODE_30204) { return NotLoginException.TOKEN_TIMEOUT; } return null; } } /** * 会话注销 */ @Override public void logout() { // ... // 从当前 [storage存储器] 里删除 SaHolder.getStorage().delete(splicingKeyJustCreatedSave()); // 如果打开了Cookie模式,则把cookie清除掉 if(getConfigOrGlobal().getIsReadCookie()){ SaHolder.getResponse().deleteCookie(getTokenName()); } } /** * [work] 注销下线 * * @param tokenValue 指定 token * @param logoutParameter 注销参数 */ public void _logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) { throw new ApiDisabledException(); } /** * [禁用] 会话注销 */ @Override public void _logout(Object loginId, SaLogoutParameter logoutParameter) { throw new ApiDisabledException(); } /** * [禁用] 顶人下线,根据账号id 和 设备类型 */ @Override public void replaced(Object loginId, String deviceType) { throw new ApiDisabledException(); } /** * 获取当前 Token 的扩展信息 */ @Override public Object getExtra(String key) { return getExtra(getTokenValue(), key); } /** * 获取指定 Token 的扩展信息 */ @Override public Object getExtra(String tokenValue, String key) { return SaJwtUtil.getPayloads(tokenValue, loginType, jwtSecretKey()).get(key); } /** * 删除 Token-Id 映射 */ @Override public void deleteTokenToIdMapping(String tokenValue) { // not action } /** * 更改 Token 指向的 账号Id 值 */ @Override public void updateTokenToIdMapping(String tokenValue, Object loginId) { // not action } /** * 存储 Token-Id 映射 */ @Override public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) { // not action } // ------------------- 过期时间相关 ------------------- /** * 获取指定 token 剩余有效时间 (单位: 秒) */ @Override public long getTokenTimeout(String tokenValue) { return SaJwtUtil.getTimeout(tokenValue, loginType, jwtSecretKey()); } // ------------------- Token-Session 相关 ------------------- /** * 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回 * * @param tokenValue token值 * @param isCreate 是否新建 * @return session对象 */ public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) { if(SaFoxUtil.isEmpty(tokenValue)) { throw new SaTokenException("Token-Session 获取失败:token 不能为空"); } long timeout = getTokenTimeout(tokenValue); return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, timeout, session -> { // 这里是该 Token-Session 首次创建时才会被执行的方法: // 设定这个 SaSession 的各种基础信息:类型、账号体系、Token 值 session.setType(SaTokenConsts.SESSION_TYPE__TOKEN); session.setLoginType(getLoginType()); session.setToken(tokenValue); }); } // ------------------- 会话管理 ------------------- /** * [禁用] 根据条件查询Token */ @Override public List searchTokenValue(String keyword, int start, int size, boolean sortType) { throw new ApiDisabledException(); } // ------------------- Bean对象代理 ------------------- /** * 返回当前 StpLogic 是否支持 isShare * @return / */ @Override public boolean isSupportShareToken() { return false; } /** * 返回全局配置对象的 maxTryTimes 属性 * @return / */ @Override public int getConfigOfMaxTryTimes(SaLoginParameter loginParameter) { return -1; } /** * 重写返回:支持 extra 扩展参数 */ @Override public boolean isSupportExtra() { return true; } } ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForSimple.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt; import java.util.Map; import cn.dev33.satoken.jwt.error.SaJwtErrorCode; import cn.dev33.satoken.jwt.exception.SaJwtException; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; /** * Sa-Token 整合 jwt -- Simple 简单模式 * * @author click33 * @since 1.30.0 */ public class StpLogicJwtForSimple extends StpLogic { /** * Sa-Token 整合 jwt -- Simple模式 */ public StpLogicJwtForSimple() { super(StpUtil.TYPE); } /** * Sa-Token 整合 jwt -- Simple模式 * @param loginType 账号体系标识 */ public StpLogicJwtForSimple(String loginType) { super(loginType); } /** * 获取jwt秘钥 * @return / */ public String jwtSecretKey() { String keyt = getConfigOrGlobal().getJwtSecretKey(); SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); return keyt; } // ------ 重写方法 /** * 创建一个TokenValue */ @Override public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { return SaJwtUtil.createToken(loginType, loginId, extraData, jwtSecretKey()); } /** * 获取当前 Token 的扩展信息 */ @Override public Object getExtra(String key) { return getExtra(getTokenValue(), key); } /** * 获取指定 Token 的扩展信息 */ @Override public Object getExtra(String tokenValue, String key) { return SaJwtUtil.getPayloadsNotCheck(tokenValue, loginType, jwtSecretKey()).get(key); } @Override public boolean isSupportShareToken() { // 为确保 jwt-simple 模式的 token Extra 数据生成不受旧token影响,这里必须让 is-share 恒为 false // 即:在使用 jwt-simple 模式后,即使配置了 is-share=true 也不能复用旧 Token,必须每次创建新 Token return false; } /** * 重写返回:支持 extra 扩展参数 */ @Override public boolean isSupportExtra() { return true; } } ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForStateless.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.jwt.error.SaJwtErrorCode; import cn.dev33.satoken.jwt.exception.SaJwtException; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import java.util.Map; /** * Sa-Token 整合 jwt -- Stateless 无状态模式 * * @author click33 * @since 1.30.0 */ public class StpLogicJwtForStateless extends StpLogic { /** * Sa-Token 整合 jwt -- Stateless 无状态 */ public StpLogicJwtForStateless() { super(StpUtil.TYPE); } /** * Sa-Token 整合 jwt -- Stateless 无状态 * @param loginType 账号体系标识 */ public StpLogicJwtForStateless(String loginType) { super(loginType); } /** * 获取jwt秘钥 * @return / */ public String jwtSecretKey() { String keyt = getConfigOrGlobal().getJwtSecretKey(); SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); return keyt; } // // ------ 重写方法 // // ------------------- 获取token 相关 ------------------- /** * 创建一个TokenValue */ @Override public String createTokenValue(Object loginId, String deviceType, long timeout, Map extraData) { return SaJwtUtil.createToken(loginType, loginId, deviceType, timeout, extraData, jwtSecretKey()); } /** * 获取当前会话的Token信息 * @return token信息 */ @Override public SaTokenInfo getTokenInfo() { SaTokenInfo info = new SaTokenInfo(); info.tokenName = getTokenName(); info.tokenValue = getTokenValue(); info.isLogin = isLogin(); info.loginId = getLoginIdDefaultNull(); info.loginType = getLoginType(); info.tokenTimeout = getTokenTimeout(); info.sessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; info.tokenSessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE; info.tokenActiveTimeout = SaTokenDao.NOT_VALUE_EXPIRE; info.loginDeviceType = getLoginDeviceType(); return info; } // ------------------- 登录相关操作 ------------------- /** * 创建指定账号id的登录会话 * @param id 登录id,建议的类型:(long | int | String) * @param loginParameter 此次登录的参数Model * @return 返回会话令牌 */ @Override public String createLoginSession(Object id, SaLoginParameter loginParameter) { // 1、先检查一下,传入的参数是否有效 checkLoginArgs(id, loginParameter); // 3、生成一个token String tokenValue = createTokenValue(id, loginParameter.getDeviceType(), loginParameter.getTimeout(), loginParameter.getExtraData()); // 4、$$ 发布事件:账号xxx 登录成功 SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginParameter); // 5、返回 return tokenValue; } /** * 获取指定Token对应的账号id (不做任何特殊处理) */ @Override public String getLoginIdNotHandle(String tokenValue) { try { Object loginId = SaJwtUtil.getLoginId(tokenValue, loginType, jwtSecretKey()); return String.valueOf(loginId); } catch (SaJwtException e) { // CODE == 30204 时,代表token已过期,此时返回-3,以便外层更精确的显示异常信息 if(e.getCode() == SaJwtErrorCode.CODE_30204) { return NotLoginException.TOKEN_TIMEOUT; } return null; } } /** * 会话注销 */ @Override public void logout() { // 如果连token都没有,那么无需执行任何操作 String tokenValue = getTokenValue(); if(SaFoxUtil.isEmpty(tokenValue)) { return; } // 从当前 [storage存储器] 里删除 SaHolder.getStorage().delete(splicingKeyJustCreatedSave()); // 如果打开了Cookie模式,则把cookie清除掉 if(getConfigOrGlobal().getIsReadCookie()){ SaHolder.getResponse().deleteCookie(getTokenName()); } } /** * 获取当前 Token 的扩展信息 */ @Override public Object getExtra(String key) { return getExtra(getTokenValue(), key); } /** * 获取指定 Token 的扩展信息 */ @Override public Object getExtra(String tokenValue, String key) { return SaJwtUtil.getPayloads(tokenValue, loginType, jwtSecretKey()).get(key); } // ------------------- 过期时间相关 ------------------- /** * 获取指定 token 剩余有效时间 (单位: 秒) */ @Override public long getTokenTimeout(String tokenValue) { return SaJwtUtil.getTimeout(getTokenValue(), loginType, jwtSecretKey()); } // ------------------- id 反查 token 相关操作 ------------------- /** * 返回当前会话的登录设备类型 * @return 当前令牌的登录设备类型 */ @Override public String getLoginDeviceType() { // 如果没有token,直接返回 null String tokenValue = getTokenValue(); if(tokenValue == null) { return null; } // 如果还未登录,直接返回 null if(!isLogin()) { return null; } // 获取 return SaJwtUtil.getPayloadsNotCheck(tokenValue, loginType, jwtSecretKey()).getStr(SaJwtUtil.DEVICE_TYPE); } // ------------------- Bean对象代理 ------------------- /** * [禁用] 返回持久化对象 */ @Override public SaTokenDao getSaTokenDao() { throw new ApiDisabledException(); } /** * 重写返回:支持 extra 扩展参数 */ @Override public boolean isSupportExtra() { return true; } } ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/error/SaJwtErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt.error; /** * 定义 sa-token-jwt 所有异常细分状态码 * * @author click33 * @since 1.33.0 */ public interface SaJwtErrorCode { /** 对 jwt 字符串解析失败 */ int CODE_30201 = 30201; /** 此 jwt 的签名无效 */ int CODE_30202 = 30202; /** 此 jwt 的 loginType 字段不符合预期 */ int CODE_30203 = 30203; /** 此 jwt 已超时 */ int CODE_30204 = 30204; /** 没有配置jwt秘钥 */ int CODE_30205 = 30205; /** 登录时提供的账号id为空 */ int CODE_30206 = 30206; } ================================================ FILE: sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/exception/SaJwtException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jwt.exception; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; /** * 一个异常:代表 jwt 模块相关错误 * * @author click33 * @since 1.33.0 */ public class SaJwtException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129555290130114L; /** * jwt 解析错误 * @param message 异常描述 */ public SaJwtException(String message) { super(message); } /** * jwt 解析错误 * @param message 异常描述 * @param cause 异常对象 */ public SaJwtException(String message, Throwable cause) { super(message, cause); } /** * 写入异常细分状态码 * @param code 异常细分状态码 * @return 对象自身 */ public SaJwtException setCode(int code) { super.setCode(code); return this; } /** * 如果flag==true,则抛出message异常 * @param flag 标记 * @param message 异常信息 */ public static void throwBy(boolean flag, String message) { if(flag) { throw new SaJwtException(message); } } /** * 如果value==null或者isEmpty,则抛出message异常 * @param value 值 * @param message 异常信息 * @param code 异常细分状态码 */ public static void throwByNull(Object value, String message, int code) { if(SaFoxUtil.isEmpty(value)) { throw new SaJwtException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-oauth2 sa-token-oauth2 sa-token realization oauth2.0 cn.dev33 sa-token-core cn.dev33 sa-token-jwt true cn.dev33 sa-token-sign ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverterDefaultImpl; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerateDefaultImpl; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoaderDefaultImpl; import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolverDefaultImpl; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; /** * Sa-Token-OAuth2 模块 总控类 * * @author click33 * @since 1.19.0 */ public class SaOAuth2Manager { /** * OAuth2 配置 Bean */ private static volatile SaOAuth2ServerConfig serverConfig; public static SaOAuth2ServerConfig getServerConfig() { if (serverConfig == null) { // 初始化默认值 synchronized (SaOAuth2Manager.class) { if (serverConfig == null) { setServerConfig(new SaOAuth2ServerConfig()); } } } return serverConfig; } public static void setServerConfig(SaOAuth2ServerConfig serverConfig) { SaOAuth2Manager.serverConfig = serverConfig; } /** * OAuth2 数据加载器 Bean */ private static volatile SaOAuth2DataLoader dataLoader; public static SaOAuth2DataLoader getDataLoader() { if (dataLoader == null) { synchronized (SaOAuth2Manager.class) { if (dataLoader == null) { setDataLoader(new SaOAuth2DataLoaderDefaultImpl()); } } } return dataLoader; } public static void setDataLoader(SaOAuth2DataLoader dataLoader) { SaOAuth2Manager.dataLoader = dataLoader; } /** * OAuth2 数据解析器 Bean */ private static volatile SaOAuth2DataResolver dataResolver; public static SaOAuth2DataResolver getDataResolver() { if (dataResolver == null) { synchronized (SaOAuth2Manager.class) { if (dataResolver == null) { setDataResolver(new SaOAuth2DataResolverDefaultImpl()); } } } return dataResolver; } public static void setDataResolver(SaOAuth2DataResolver dataResolver) { SaOAuth2Manager.dataResolver = dataResolver; } /** * OAuth2 数据格式转换器 Bean */ private static volatile SaOAuth2DataConverter dataConverter; public static SaOAuth2DataConverter getDataConverter() { if (dataConverter == null) { synchronized (SaOAuth2Manager.class) { if (dataConverter == null) { setDataConverter(new SaOAuth2DataConverterDefaultImpl()); } } } return dataConverter; } public static void setDataConverter(SaOAuth2DataConverter dataConverter) { SaOAuth2Manager.dataConverter = dataConverter; } /** * OAuth2 数据构建器 Bean */ private static volatile SaOAuth2DataGenerate dataGenerate; public static SaOAuth2DataGenerate getDataGenerate() { if (dataGenerate == null) { synchronized (SaOAuth2Manager.class) { if (dataGenerate == null) { setDataGenerate(new SaOAuth2DataGenerateDefaultImpl()); } } } return dataGenerate; } public static void setDataGenerate(SaOAuth2DataGenerate dataGenerate) { SaOAuth2Manager.dataGenerate = dataGenerate; } /** * OAuth2 数据持久 Bean */ private static volatile SaOAuth2Dao dao; public static SaOAuth2Dao getDao() { if (dao == null) { synchronized (SaOAuth2Manager.class) { if (dao == null) { setDao(new SaOAuth2Dao()); } } } return dao; } public static void setDao(SaOAuth2Dao dao) { SaOAuth2Manager.dao = dao; } /** * OAuth2 模板方法 Bean */ private static volatile SaOAuth2Template template; public static SaOAuth2Template getTemplate() { if (template == null) { synchronized (SaOAuth2Manager.class) { if (template == null) { setTemplate(new SaOAuth2Template()); } } } return template; } public static void setTemplate(SaOAuth2Template template) { SaOAuth2Manager.template = template; } /** * OAuth2 StpLogic */ private static volatile StpLogic stpLogic; public static StpLogic getStpLogic() { if (stpLogic == null) { synchronized (SaOAuth2Manager.class) { if (stpLogic == null) { setStpLogic(StpUtil.stpLogic); } } } return stpLogic; } public static void setStpLogic(StpLogic stpLogic) { SaOAuth2Manager.stpLogic = stpLogic; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckAccessToken.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Access-Token 校验:指定请求中必须包含有效的 access_token ,并且包含指定的 scope * *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.39.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckAccessToken { /** * 需要校验的 scope [ 数组 ] * * @return / */ String [] scope() default {}; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientIdSecret.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * ClientSecret 校验:指定请求中必须包含有效的 client_id 和 client_secret 信息 * *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.39.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckClientIdSecret { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientToken.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Client-Token 校验:指定请求中必须包含有效的 client_token ,并且包含指定的 scope * *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.39.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckClientToken { /** * 需要校验的 scope [ 数组 ] * * @return / */ String [] scope() default {}; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckAccessTokenHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.annotation.SaCheckAccessToken; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckAccessToken 的处理器 * * @author click33 * @since 1.39.0 */ public class SaCheckAccessTokenHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckAccessToken.class; } @Override public void checkMethod(SaCheckAccessToken at, AnnotatedElement element) { _checkMethod(at.scope()); } public static void _checkMethod(String[] scope) { String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); SaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scope); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientIdSecretHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.oauth2.annotation.SaCheckClientIdSecret; import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckClientSecret 的处理器 * * @author click33 * @since 1.39.0 */ public class SaCheckClientIdSecretHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckClientIdSecret.class; } @Override public void checkMethod(SaCheckClientIdSecret at, AnnotatedElement element) { _checkMethod(); } public static void _checkMethod() { SaOAuth2ServerProcessor.instance.checkCurrClientSecret(); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientTokenHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.annotation.handler; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.annotation.SaCheckClientToken; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckAccessToken 的处理器 * * @author click33 * @since 1.39.0 */ public class SaCheckClientTokenHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckClientToken.class; } @Override public void checkMethod(SaCheckClientToken at, AnnotatedElement element) { _checkMethod(at.scope()); } public static void _checkMethod(String[] scope) { String clientToken = SaOAuth2Manager.getDataResolver().readClientToken(SaHolder.getRequest()); SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scope); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.config; import java.io.Serializable; /** * Sa-Token OAuth2 Server 端 Oidc 配置类 Model * * @author click33 * @since 1.39.0 */ public class SaOAuth2OidcConfig implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** iss 值,如不配置则自动计算 */ public String iss; /** idToken 有效期(单位秒) 默认十分钟 */ public long idTokenTimeout = 60 * 10; /** * 获取 iss 值,如不配置则自动计算 * * @return / */ public String getIss() { return this.iss; } /** * 设置 iss 值,如不配置则自动计算 * * @param iss / * @return 对象自身 */ public SaOAuth2OidcConfig setIss(String iss) { this.iss = iss; return this; } /** * 获取 idToken 有效期(单位秒) 默认十分钟 * * @return / */ public long getIdTokenTimeout() { return this.idTokenTimeout; } /** * 设置 idToken 有效期(单位秒) 默认十分钟 * * @param idTokenTimeout / * @return 对象自身 */ public SaOAuth2OidcConfig setIdTokenTimeout(long idTokenTimeout) { this.idTokenTimeout = idTokenTimeout; return this; } @Override public String toString() { return "SaOAuth2OidcConfig{" + "iss='" + iss + '\'' + ", idTokenTimeout=" + idTokenTimeout + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.config; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; /** * Sa-Token OAuth2 Server 端 配置类 Model * * @author click33 * @since 1.19.0 */ public class SaOAuth2ServerConfig implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** 是否打开模式:授权码(Authorization Code) */ public Boolean enableAuthorizationCode = true; /** 是否打开模式:隐藏式(Implicit) */ public Boolean enableImplicit = true; /** 是否打开模式:密码式(Password) */ public Boolean enablePassword = true; /** 是否打开模式:凭证式(Client Credentials) */ public Boolean enableClientCredentials = true; /** Code授权码 保存的时间(单位:秒) 默认五分钟 */ public long codeTimeout = 60 * 5; /** 全局默认配置所有应用:Access-Token 保存的时间(单位:秒) 默认两个小时 */ public long accessTokenTimeout = 60 * 60 * 2; /** 全局默认配置所有应用:Refresh-Token 保存的时间(单位:秒) 默认30 天 */ public long refreshTokenTimeout = 60 * 60 * 24 * 30; /** 全局默认配置所有应用:Client-Token 保存的时间(单位:秒) 默认两个小时 */ public long clientTokenTimeout = 60 * 60 * 2; /** 全局默认配置所有应用:单个应用单个用户最多同时存在的 Access-Token 数量 */ public int maxAccessTokenCount = 12; /** 全局默认配置所有应用:单个应用单个用户最多同时存在的 Refresh-Token 数量 */ public int maxRefreshTokenCount = 12; /** 全局默认配置所有应用:单个应用最多同时存在的 Client-Token 数量 */ public int maxClientTokenCount = 12; /** 全局默认配置所有应用:是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token */ public Boolean isNewRefresh = false; /** 默认 openid 生成算法中使用的摘要前缀 */ public String openidDigestPrefix = SaOAuth2Consts.OPENID_DEFAULT_DIGEST_PREFIX; /** 默认 unionid 生成算法中使用的摘要前缀 */ public String unionidDigestPrefix = SaOAuth2Consts.UNIONID_DEFAULT_DIGEST_PREFIX; /** 指定高级权限,多个用逗号隔开 */ public String higherScope; /** 指定低级权限,多个用逗号隔开 */ public String lowerScope; /** 模式4是否返回 AccessToken 字段,以使其更符合 OAuth2 RFC 规范 */ public Boolean mode4ReturnAccessToken = false; /** 是否在返回值中隐藏默认的状态字段 (code、msg、data) */ public Boolean hideStatusField = false; /** * oidc 相关配置 */ SaOAuth2OidcConfig oidc = new SaOAuth2OidcConfig(); /** client 列表 */ public Map clients = new LinkedHashMap<>(); // 额外方法 /** * 注册 client * @return / */ public SaOAuth2ServerConfig addClient(SaClientModel client) { if(this.clients == null) { this.clients = new LinkedHashMap<>(); } this.clients.put(client.getClientId(), client); return this; } // get set /** * 是否打开模式:授权码(Authorization Code) * @return enableAuthorizationCode */ public Boolean getEnableAuthorizationCode() { return enableAuthorizationCode; } /** * 设置是否打开模式:授权码(Authorization Code) * @param enableAuthorizationCode 是否开启 * @return 对象自身 */ public SaOAuth2ServerConfig setEnableAuthorizationCode(Boolean enableAuthorizationCode) { this.enableAuthorizationCode = enableAuthorizationCode; return this; } /** * 是否打开模式:隐藏式(Implicit) * @return enableImplicit */ public Boolean getEnableImplicit() { return enableImplicit; } /** * 设置是否打开模式:隐藏式(Implicit) * @param enableImplicit 是否开启 * @return 对象自身 */ public SaOAuth2ServerConfig setEnableImplicit(Boolean enableImplicit) { this.enableImplicit = enableImplicit; return this; } /** * 是否打开模式:密码式(Password) * @return enablePassword */ public Boolean getEnablePassword() { return enablePassword; } /** * 设置是否打开模式:密码式(Password) * @param enablePassword 是否开启 * @return 对象自身 */ public SaOAuth2ServerConfig setEnablePassword(Boolean enablePassword) { this.enablePassword = enablePassword; return this; } /** * 是否打开模式:凭证式(Client Credentials) * @return enableClientCredentials */ public Boolean getEnableClientCredentials() { return enableClientCredentials; } /** * 设置是否打开模式:凭证式(Client Credentials) * @param enableClientCredentials 是否开启 * @return 对象自身 */ public SaOAuth2ServerConfig setEnableClientCredentials(Boolean enableClientCredentials) { this.enableClientCredentials = enableClientCredentials; return this; } /** * 全局默认配置所有应用:是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token * @return isNewRefresh */ public Boolean getIsNewRefresh() { return isNewRefresh; } /** * 全局默认配置所有应用:设置是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token * @param isNewRefresh 是否开启 * @return 对象自身 */ public SaOAuth2ServerConfig setIsNewRefresh(Boolean isNewRefresh) { this.isNewRefresh = isNewRefresh; return this; } /** * Code授权码 保存的时间(单位:秒) 默认五分钟 * @return codeTimeout */ public long getCodeTimeout() { return codeTimeout; } /** * 设置Code授权码保存的时间(单位:秒) * @param codeTimeout 保存时间(秒) * @return 对象自身 */ public SaOAuth2ServerConfig setCodeTimeout(long codeTimeout) { this.codeTimeout = codeTimeout; return this; } /** * 全局默认配置所有应用:Access-Token 保存的时间(单位:秒) 默认两个小时 * @return accessTokenTimeout */ public long getAccessTokenTimeout() { return accessTokenTimeout; } /** * 全局默认配置所有应用:设置Access-Token保存的时间(单位:秒) * @param accessTokenTimeout 保存时间(秒) * @return 对象自身 */ public SaOAuth2ServerConfig setAccessTokenTimeout(long accessTokenTimeout) { this.accessTokenTimeout = accessTokenTimeout; return this; } /** * 全局默认配置所有应用:Refresh-Token 保存的时间(单位:秒) 默认30天 * @return refreshTokenTimeout */ public long getRefreshTokenTimeout() { return refreshTokenTimeout; } /** * 全局默认配置所有应用:设置Refresh-Token保存的时间(单位:秒) * @param refreshTokenTimeout 保存时间(秒) * @return 对象自身 */ public SaOAuth2ServerConfig setRefreshTokenTimeout(long refreshTokenTimeout) { this.refreshTokenTimeout = refreshTokenTimeout; return this; } /** * 全局默认配置所有应用:Client-Token 保存的时间(单位:秒) 默认两个小时 * @return clientTokenTimeout */ public long getClientTokenTimeout() { return clientTokenTimeout; } /** * 全局默认配置所有应用:设置Client-Token保存的时间(单位:秒) * @param clientTokenTimeout 保存时间(秒) * @return 对象自身 */ public SaOAuth2ServerConfig setClientTokenTimeout(long clientTokenTimeout) { this.clientTokenTimeout = clientTokenTimeout; return this; } /** * 全局默认配置所有应用:单个应用单个用户最多同时存在的 Access-Token 数量 * @return maxAccessTokenCount */ public int getMaxAccessTokenCount() { return maxAccessTokenCount; } /** * 设置单个应用单个用户最多同时存在的 Access-Token 数量 * @param maxAccessTokenCount 最大数量 * @return 对象自身 */ public SaOAuth2ServerConfig setMaxAccessTokenCount(int maxAccessTokenCount) { this.maxAccessTokenCount = maxAccessTokenCount; return this; } /** * 全局默认配置所有应用:单个应用单个用户最多同时存在的 Refresh-Token 数量 * @return maxRefreshTokenCount */ public int getMaxRefreshTokenCount() { return maxRefreshTokenCount; } /** * 设置单个应用单个用户最多同时存在的 Refresh-Token 数量 * @param maxRefreshTokenCount 最大数量 * @return 对象自身 */ public SaOAuth2ServerConfig setMaxRefreshTokenCount(int maxRefreshTokenCount) { this.maxRefreshTokenCount = maxRefreshTokenCount; return this; } /** * 全局默认配置所有应用:单个应用最多同时存在的 Client-Token 数量 * @return maxClientTokenCount */ public int getMaxClientTokenCount() { return maxClientTokenCount; } /** * 设置单个应用最多同时存在的 Client-Token 数量 * @param maxClientTokenCount 最大数量 * @return 对象自身 */ public SaOAuth2ServerConfig setMaxClientTokenCount(int maxClientTokenCount) { this.maxClientTokenCount = maxClientTokenCount; return this; } /** * 默认 openid 生成算法中使用的摘要前缀 * @return openidDigestPrefix */ public String getOpenidDigestPrefix() { return openidDigestPrefix; } /** * 设置默认 openid 生成算法中使用的摘要前缀 * @param openidDigestPrefix 摘要前缀 * @return 对象自身 */ public SaOAuth2ServerConfig setOpenidDigestPrefix(String openidDigestPrefix) { this.openidDigestPrefix = openidDigestPrefix; return this; } /** * 默认 unionid 生成算法中使用的摘要前缀 * @return unionidDigestPrefix */ public String getUnionidDigestPrefix() { return unionidDigestPrefix; } /** * 设置默认 unionid 生成算法中使用的摘要前缀 * @param unionidDigestPrefix 摘要前缀 * @return 对象自身 */ public SaOAuth2ServerConfig setUnionidDigestPrefix(String unionidDigestPrefix) { this.unionidDigestPrefix = unionidDigestPrefix; return this; } /** * 指定高级权限,多个用逗号隔开 * @return higherScope */ public String getHigherScope() { return higherScope; } /** * 设置高级权限,多个用逗号隔开 * @param higherScope 权限字符串 * @return 对象自身 */ public SaOAuth2ServerConfig setHigherScope(String higherScope) { this.higherScope = higherScope; return this; } /** * 指定低级权限,多个用逗号隔开 * @return lowerScope */ public String getLowerScope() { return lowerScope; } /** * 设置低级权限,多个用逗号隔开 * @param lowerScope 权限字符串 * @return 对象自身 */ public SaOAuth2ServerConfig setLowerScope(String lowerScope) { this.lowerScope = lowerScope; return this; } /** * 模式4是否返回 AccessToken 字段,以使其更符合 OAuth2 RFC 规范 * @return mode4ReturnAccessToken */ public Boolean getMode4ReturnAccessToken() { return mode4ReturnAccessToken; } /** * 设置模式4是否返回 AccessToken 字段,以使其更符合 OAuth2 RFC 规范 * @param mode4ReturnAccessToken 是否返回 * @return 对象自身 */ public SaOAuth2ServerConfig setMode4ReturnAccessToken(Boolean mode4ReturnAccessToken) { this.mode4ReturnAccessToken = mode4ReturnAccessToken; return this; } /** * 是否在返回值中隐藏默认的状态字段 (code、msg、data) * @return hideStatusField */ public Boolean getHideStatusField() { return hideStatusField; } /** * 设置是否在返回值中隐藏默认的状态字段 (code、msg、data) * @param hideStatusField 是否隐藏 * @return 对象自身 */ public SaOAuth2ServerConfig setHideStatusField(Boolean hideStatusField) { this.hideStatusField = hideStatusField; return this; } /** * 获取oidc相关配置 * @return oidc配置对象 */ public SaOAuth2OidcConfig getOidc() { return oidc; } /** * 设置oidc相关配置 * @param oidc oidc配置对象 * @return 对象自身 */ public SaOAuth2ServerConfig setOidc(SaOAuth2OidcConfig oidc) { this.oidc = oidc; return this; } /** * 获取client列表 * @return client列表 */ public Map getClients() { return clients; } /** * 设置client列表 * @param clients client列表 * @return 对象自身 */ public SaOAuth2ServerConfig setClients(Map clients) { this.clients = clients; return this; } @Override public String toString() { return "SaOAuth2ServerConfig {" + "enableAuthorizationCode=" + enableAuthorizationCode + ", enableImplicit=" + enableImplicit + ", enablePassword=" + enablePassword + ", enableClientCredentials=" + enableClientCredentials + ", isNewRefresh=" + isNewRefresh + ", codeTimeout=" + codeTimeout + ", accessTokenTimeout=" + accessTokenTimeout + ", refreshTokenTimeout=" + refreshTokenTimeout + ", clientTokenTimeout=" + clientTokenTimeout + ", maxAccessTokenCount=" + maxAccessTokenCount + ", maxRefreshTokenCount=" + maxRefreshTokenCount + ", maxClientTokenCount=" + maxClientTokenCount + ", openidDigestPrefix=" + openidDigestPrefix + ", unionidDigestPrefix=" + unionidDigestPrefix + ", higherScope=" + higherScope + ", lowerScope=" + lowerScope + ", mode4ReturnAccessToken=" + mode4ReturnAccessToken + ", hideStatusField=" + hideStatusField + ", oidc=" + oidc + ", clients=" + clients + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/GrantType.java ================================================ package cn.dev33.satoken.oauth2.consts; /** * 所有授权类型 */ public final class GrantType { public static String authorization_code = "authorization_code"; public static String refresh_token = "refresh_token"; public static String password = "password"; public static String client_credentials = "client_credentials"; public static String implicit = "implicit"; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.consts; /** * Sa-Token OAuth2 所有常量 * * @author click33 * @since 1.23.0 */ public class SaOAuth2Consts { /** * 所有API接口 * @author click33 */ public static final class Api { public static String authorize = "/oauth2/authorize"; public static String token = "/oauth2/token"; public static String refresh = "/oauth2/refresh"; public static String revoke = "/oauth2/revoke"; public static String client_token = "/oauth2/client_token"; public static String doLogin = "/oauth2/doLogin"; public static String doConfirm = "/oauth2/doConfirm"; } /** * 所有参数名称 * @author click33 */ public static final class Param { public static String response_type = "response_type"; public static String client_id = "client_id"; public static String client_secret = "client_secret"; public static String redirect_uri = "redirect_uri"; public static String scope = "scope"; public static String state = "state"; public static String code = "code"; public static String token = "token"; public static String access_token = "access_token"; public static String refresh_token = "refresh_token"; public static String client_token = "client_token"; public static String grant_type = "grant_type"; public static String username = "username"; public static String password = "password"; public static String name = "name"; public static String pwd = "pwd"; public static String build_redirect_uri = "build_redirect_uri"; public static String Authorization = "Authorization"; public static String nonce = "nonce"; } /** * 所有返回类型 */ public static final class ResponseType { public static String code = "code"; public static String token = "token"; } /** * 所有 token 类型 */ public static final class TokenType { // 全小写 public static String basic = "basic"; public static String digest = "digest"; public static String bearer = "bearer"; // 首字母大写 public static String Basic = "Basic"; public static String Digest = "Digest"; public static String Bearer = "Bearer"; } /** * 扩展字段 */ public static final class ExtraField { public static String unionid = "unionid"; public static String openid = "openid"; public static String userid = "userid"; public static String id_token = "id_token"; } /** 默认 openid 生成算法中使用的前缀 */ public static final String OPENID_DEFAULT_DIGEST_PREFIX = "openid_default_digest_prefix"; /** 默认 unionid 生成算法中使用的前缀 */ public static final String UNIONID_DEFAULT_DIGEST_PREFIX = "unionid_default_digest_prefix"; /** 表示OK的返回结果 */ public static final String OK = "ok"; /** 表示请求没有得到任何有效处理 {msg: "not handle"} */ public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}"; /** * 最终权限处理器标识符:在所有权限处理器执行之后,执行此 scope 标识符代表的权限处理器 */ public static final String _FINALLY_WORK_SCOPE = "_FINALLY_WORK_SCOPE"; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.dao; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.raw.SaRawSessionDelegator; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTtlMethods; import java.util.ArrayList; import java.util.List; import java.util.Map; import static cn.dev33.satoken.oauth2.template.SaOAuth2Util.checkClientModel; /** * Sa-Token OAuth2 数据持久层 (在 SaTokenDao 之上再封装一层,方便 OAuth2 模块整体的数据读写操作) * * @author click33 * @since 1.39.0 */ public class SaOAuth2Dao implements SaTtlMethods { // ------------------- 索引操作公共代码 /** * Raw Session 读写委托 (存储 Access-Token、Refresh-Token、Client-Token 索引) */ public SaRawSessionDelegator oauth2RSD = new SaRawSessionDelegator("oauth2"); /** * 在 raw-session 中的保存 Access-Token 索引列表使用的 key */ public static final String ACCESS_TOKEN_MAP = "__HD_ACCESS_TOKEN_MAP"; /** * 在 raw-session 中的保存 Refresh-Token 索引列表使用的 key */ public static final String REFRESH_TOKEN_MAP = "__HD_REFRESH_TOKEN_MAP"; /** * 在 raw-session 中的保存 Client-Token 索引列表使用的 key */ public static final String CLIENT_TOKEN_MAP = "__HD_CLIENT_TOKEN_MAP"; /** * 获取:保存 Access-Token 索引时使用的 RawSession * * @param clientId 应用 id * @param loginId 账号 id * @param isCreate 如果尚未创建,是否立即创建 * @return / */ protected SaSession getRawSessionByAccessToken(String clientId, Object loginId, boolean isCreate) { String value = splicingAccessTokenRSDValue(clientId, loginId); return oauth2RSD.getSessionById(value, isCreate); } /** * 获取:保存 Refresh-Token 索引时使用的 RawSession * * @param clientId 应用 id * @param loginId 账号 id * @param isCreate 如果尚未创建,是否立即创建 * @return / */ protected SaSession getRawSessionByRefreshToken(String clientId, Object loginId, boolean isCreate) { String value = splicingRefreshTokenRSDValue(clientId, loginId); return oauth2RSD.getSessionById(value, isCreate); } /** * 获取:保存 Client-Token 索引时使用的 RawSession * * @param clientId 应用 id * @param isCreate 如果尚未创建,是否立即创建 * @return / */ protected SaSession getRawSessionByClientToken(String clientId, boolean isCreate) { String value = splicingClientTokenRSDValue(clientId); return oauth2RSD.getSessionById(value, isCreate); } /** * 在 RawSession 上添加 token 索引,并完整调整索引列表 * * @param session 待操作的 RawSession * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key * @param token 待添加的 token * @param timeout 添加的 token 其过期时间 * @param maxTokenCount 允许的最多 token 数量,超出的将被删除 (-1=不限制) * @param removeFun 执行删除 token 的函数 */ protected void addTokenIndex_AndAdjust(SaSession session, String tokenIndexMapSaveKey, String token, long timeout, int maxTokenCount, SaParamFunction removeFun) { Map tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap); if(! tokenIndexMap.containsKey(token)) { // 添加 tokenIndexMap.put(token, ttlToExpireTime(timeout)); // 剔除过期的 tokenIndexMap = _removeExpiredIndex(tokenIndexMap); // 删掉溢出的 tokenIndexMap = _removeOverflowIndex(tokenIndexMap, maxTokenCount, removeFun); // 保存 session.set(tokenIndexMapSaveKey, tokenIndexMap); // 更新 TTL long maxTtl = getMaxTtlByExpireTime(tokenIndexMap.values()); if(maxTtl != 0) { session.updateTimeout(maxTtl); } } } /** * 在 RawSession 上删除 token 索引,并尝试注销 RawSession * * @param session 待操作的 RawSession * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key * @param token 待删除的 token */ protected void deleteTokenIndex_AndTryLogout(SaSession session, String tokenIndexMapSaveKey, String token) { Map tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap); tokenIndexMap.remove(token); // 如果删除后还有记录,就再次保存 if( ! tokenIndexMap.isEmpty()) { session.set(tokenIndexMapSaveKey, tokenIndexMap); } else { // 没有的话就直接注销此 RawSession session.logout(); } } /** * 剔除已过期的 token 索引 * * @param tokenIndexMap token 索引列表 * @return 调整后的索引列表 */ protected Map _removeExpiredIndex(Map tokenIndexMap) { Map newTokenList = newTokenIndexMap(); for (Map.Entry entry : tokenIndexMap.entrySet()) { long ttl = expireTimeToTtl(entry.getValue()); if(ttl != SaTokenDao.NOT_VALUE_EXPIRE) { newTokenList.put(entry.getKey(), entry.getValue()); } } return newTokenList; } /** * 将 token 索引列表中溢出的部分删除(按照插入顺序先进先出,不考虑每个剩余 token 剩余有效期) * * @param tokenIndexMap token 索引列表(key=token, value=token过期时间)(传入的 Map 必须是有序的) * @param maxTokenCount 允许的最多 token 数量,超出的将被删除 (-1=不限制) * @param removeFun 执行删除 token 的函数 * @return 调整后的索引列表 */ protected Map _removeOverflowIndex(Map tokenIndexMap, int maxTokenCount, SaParamFunction removeFun) { // 如果当前数量未超过限制,直接返回 if (tokenIndexMap.size() <= maxTokenCount || maxTokenCount == SaTokenDao.NEVER_EXPIRE) { return tokenIndexMap; } // 创建新的索引 Map 副本 Map newTokenIndexMap = newTokenIndexMap(); // 溢出数量 int overflowCount = tokenIndexMap.size() - maxTokenCount; // 已删除 Token 数量 int removedCount = 0; // 遍历原 Map 的所有条目 for (Map.Entry entry : tokenIndexMap.entrySet()) { String token = entry.getKey(); if (removedCount < overflowCount) { // 溢出部分:执行删除回调,但不添加到新 Map removeFun.run(token); removedCount++; } else { // 未溢出部分:添加到新 Map 副本 newTokenIndexMap.put(token, entry.getValue()); } } // 返回索引 Map 副本 return newTokenIndexMap; } /** * 从 RawSession 获取 Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 token) * * @param session 待操作的 RawSession * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key * @return / */ protected Map getTokenIndexMap_FromAdjustAfter(SaSession session, String tokenIndexMapSaveKey) { if(session == null) { return newTokenIndexMap(); } // 根据 ttl 值过滤一遍 Map tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap); Map newTokenIndexMap = _removeExpiredIndex(tokenIndexMap); // 如果调整后集合长度归零了,说明 token 已全部过期,直接注销此 RawSession if(newTokenIndexMap.isEmpty()) { session.logout(); return newTokenIndexMap(); } // 没有归零,但是长度变小了,说明有过期的 token,需要重写写入一遍 if(tokenIndexMap.size() > newTokenIndexMap.size()) { session.set(tokenIndexMapSaveKey, newTokenIndexMap); } // 转 List 返回 return newTokenIndexMap; } /** * 从 RawSession 获取 Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 token) * * @param session 待操作的 RawSession * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key * @return / */ protected List getTokenValueList_FromAdjustAfter(SaSession session, String tokenIndexMapSaveKey) { return new ArrayList<>(getTokenIndexMap_FromAdjustAfter(session, tokenIndexMapSaveKey).keySet()); } // ------------------- code 操作 /** * 保存:CodeModel * @param c / */ public void saveCode(CodeModel c) { if(c == null) { return; } getSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getServerConfig().getCodeTimeout()); } /** * 删除:CodeModel * @param code / */ public void deleteCode(String code) { if(code != null) { getSaTokenDao().deleteObject(splicingCodeSaveKey(code)); } } /** * 获取:CodeModel * @param code / * @return / */ public CodeModel getCode(String code) { if(code == null) { return null; } return (CodeModel)getSaTokenDao().getObject(splicingCodeSaveKey(code)); } // ------------------- code 索引 /** * 保存:Code 索引 * @param c / */ public void saveCodeIndex(CodeModel c) { if(c == null) { return; } getSaTokenDao().set(splicingCodeIndexKey(c.clientId, c.loginId), c.code, SaOAuth2Manager.getServerConfig().getCodeTimeout()); } /** * 删除:Code 索引 * @param clientId 应用id * @param loginId 账号id */ public void deleteCodeIndex(String clientId, Object loginId) { getSaTokenDao().delete(splicingCodeIndexKey(clientId, loginId)); } /** * 获取:Code Value * @param clientId 应用id * @param loginId 账号id * @return / */ public String getCodeValue(String clientId, Object loginId) { return getSaTokenDao().get(splicingCodeIndexKey(clientId, loginId)); } // ------------------- Access-Token Model /** * 保存:AccessTokenModel * @param at / */ public void saveAccessToken(AccessTokenModel at) { if(at == null) { return; } getSaTokenDao().setObject(splicingAccessTokenSaveKey(at.accessToken), at, at.getExpiresIn()); } /** * 删除:AccessTokenModel * @param accessToken 值 */ public void deleteAccessToken(String accessToken) { if(accessToken != null) { getSaTokenDao().deleteObject(splicingAccessTokenSaveKey(accessToken)); } } /** * 获取:AccessTokenModel * @param accessToken / * @return / */ public AccessTokenModel getAccessToken(String accessToken) { if(accessToken == null) { return null; } return (AccessTokenModel)getSaTokenDao().getObject(splicingAccessTokenSaveKey(accessToken)); } // ------------------- Access-Token 索引 /** * 保存:Access-Token 索引,并完整调整索引列表 * * @param at / * @param maxAccessTokenCount 允许的最多 Access-Token 数量,超出的将被删除 (-1=不限制) */ public void saveAccessTokenIndex_AndAdjust(AccessTokenModel at, int maxAccessTokenCount) { if(at == null) { return; } SaSession session = getRawSessionByAccessToken(at.clientId, at.loginId, true); addTokenIndex_AndAdjust(session, ACCESS_TOKEN_MAP, at.accessToken, at.getExpiresIn(), maxAccessTokenCount, this::deleteAccessToken); } /** * 删除:Access-Token 在 RawSession 上的单个索引数据 * * @param clientId 应用 id * @param loginId 账号id * @param accessToken 值 */ public void deleteAccessTokenIndex_BySingleData(String clientId, Object loginId, String accessToken) { SaSession session = getRawSessionByAccessToken(clientId, loginId, false); if(session == null) { return; } deleteTokenIndex_AndTryLogout(session, ACCESS_TOKEN_MAP, accessToken); } /** * 删除:Access-Token 索引整体 * @param clientId 应用id * @param loginId 账号id */ public void deleteAccessTokenIndex(String clientId, Object loginId) { oauth2RSD.deleteSessionById(splicingAccessTokenRSDValue(clientId, loginId)); } /** * 获取 Access-Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 AccessToken 索引) * * @param clientId 应用id * @param loginId 账号id * @return / */ public Map getAccessTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) { SaSession session = getRawSessionByAccessToken(clientId, loginId, false); return getTokenIndexMap_FromAdjustAfter(session, ACCESS_TOKEN_MAP); } /** * 获取 Access-Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 AccessToken) * * @param clientId 应用id * @param loginId 账号id * @return / */ public List getAccessTokenValueList_FromAdjustAfter(String clientId, Object loginId) { SaSession session = getRawSessionByAccessToken(clientId, loginId, false); return getTokenValueList_FromAdjustAfter(session, ACCESS_TOKEN_MAP); } // ------------------- Refresh-Token Model /** * 保存:RefreshTokenModel * @param rt . */ public void saveRefreshToken(RefreshTokenModel rt) { if(rt == null) { return; } getSaTokenDao().setObject(splicingRefreshTokenSaveKey(rt.refreshToken), rt, rt.getExpiresIn()); } /** * 删除:RefreshTokenModel * @param refreshToken 值 */ public void deleteRefreshToken(String refreshToken) { if(refreshToken != null) { getSaTokenDao().deleteObject(splicingRefreshTokenSaveKey(refreshToken)); } } /** * 获取:RefreshTokenModel * @param refreshToken / * @return / */ public RefreshTokenModel getRefreshToken(String refreshToken) { if(refreshToken == null) { return null; } return (RefreshTokenModel)getSaTokenDao().getObject(splicingRefreshTokenSaveKey(refreshToken)); } // ------------------- Refresh-Token 索引 /** * 保存:Refresh-Token 索引 * * @param rt / * @param maxRefreshTokenCount 允许的最多 Refresh-Token 数量,超出的将被删除 (-1=不限制) */ public void saveRefreshTokenIndex_AndAdjust(RefreshTokenModel rt, int maxRefreshTokenCount) { if(rt == null) { return; } SaSession session = getRawSessionByRefreshToken(rt.clientId, rt.loginId, true); addTokenIndex_AndAdjust(session, REFRESH_TOKEN_MAP, rt.refreshToken, rt.getExpiresIn(), maxRefreshTokenCount, this::deleteRefreshToken); } /** * 删除:Refresh-Token 在 RawSession 上的单个索引数据 * * @param clientId 应用 id * @param loginId 账号id * @param refreshToken 值 */ public void deleteRefreshTokenIndex_BySingleData(String clientId, Object loginId, String refreshToken) { SaSession session = getRawSessionByRefreshToken(clientId, loginId, false); if(session == null) { return; } deleteTokenIndex_AndTryLogout(session, REFRESH_TOKEN_MAP, refreshToken); } /** * 删除:Refresh-Token 索引整体 * @param clientId 应用id * @param loginId 账号id */ public void deleteRefreshTokenIndex(String clientId, Object loginId) { oauth2RSD.deleteSessionById(splicingRefreshTokenRSDValue(clientId, loginId)); } /** * 获取 Refresh-Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 RefreshToken 索引) * * @param clientId 应用id * @param loginId 账号id * @return / */ public Map getRefreshTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) { SaSession session = getRawSessionByRefreshToken(clientId, loginId, false); return getTokenIndexMap_FromAdjustAfter(session, REFRESH_TOKEN_MAP); } /** * 获取 Refresh-Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 RefreshToken) * * @param clientId 应用id * @param loginId 账号id * @return / */ public List getRefreshTokenValueList_FromAdjustAfter(String clientId, Object loginId) { SaSession session = getRawSessionByRefreshToken(clientId, loginId, false); return getTokenValueList_FromAdjustAfter(session, REFRESH_TOKEN_MAP); } // ------------------- Client-Token Model /** * 保存:ClientTokenModel * @param ct . */ public void saveClientToken(ClientTokenModel ct) { if(ct == null) { return; } getSaTokenDao().setObject(splicingClientTokenSaveKey(ct.clientToken), ct, ct.getExpiresIn()); } /** * 删除:ClientTokenModel * @param clientToken 值 */ public void deleteClientToken(String clientToken) { if(clientToken != null) { getSaTokenDao().deleteObject(splicingClientTokenSaveKey(clientToken)); } } /** * 获取:ClientTokenModel * @param clientToken / * @return / */ public ClientTokenModel getClientToken(String clientToken) { if(clientToken == null) { return null; } return getSaTokenDao().getObject(splicingClientTokenSaveKey(clientToken), ClientTokenModel.class); } // ------------------- Client-Token 索引 /** * 保存:Client-Token 索引 * * @param ct / * @param maxClientTokenCount 允许的最多 Client-Token 数量,超出的将被删除 (-1=不限制) */ public void saveClientTokenIndex_AndAdjust(ClientTokenModel ct, int maxClientTokenCount) { if(ct == null) { return; } SaSession session = getRawSessionByClientToken(ct.clientId, true); addTokenIndex_AndAdjust(session, CLIENT_TOKEN_MAP, ct.clientToken, ct.getExpiresIn(), maxClientTokenCount, this::deleteClientToken); } /** * 删除:Client-Token 在 RawSession 上的单个索引数据 * @param clientId 应用 id * @param clientToken 值 */ public void deleteClientTokenIndex_BySingleData(String clientId, String clientToken) { SaSession session = getRawSessionByClientToken(clientId, false); if(session == null) { return; } deleteTokenIndex_AndTryLogout(session, CLIENT_TOKEN_MAP, clientToken); } /** * 删除:Client-Token 索引整体 * * @param clientId 应用id */ public void deleteClientTokenIndex(String clientId) { oauth2RSD.deleteSessionById(splicingClientTokenRSDValue(clientId)); } /** * 获取 Client-Token 索引列表(获取之前会完整调整索引列表,保证获取的都是有效 ClientToken 索引) * * @param clientId 应用id * @param loginId 账号id * @return / */ public Map getClientTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) { SaSession session = getRawSessionByClientToken(clientId, false); return getTokenIndexMap_FromAdjustAfter(session, CLIENT_TOKEN_MAP); } /** * 获取 Client-Token 列表(获取之前会完整调整索引列表,保证获取的都是有效 ClientToken) * * @param clientId 应用id * @return / */ public List getClientTokenValueList_FromAdjustAfter(String clientId) { SaSession session = getRawSessionByClientToken(clientId, false); return getTokenValueList_FromAdjustAfter(session, CLIENT_TOKEN_MAP); } // ------------------- GrantScope /** * 保存:用户授权记录 * @param clientId 应用id * @param loginId 账号id * @param scopes 权限列表 */ public void saveGrantScope(String clientId, Object loginId, List scopes) { if( ! SaFoxUtil.isEmpty(scopes)) { long ttl = checkClientModel(clientId).getAccessTokenTimeout(); String value = SaOAuth2Manager.getDataConverter().convertScopeListToString(scopes); getSaTokenDao().set(splicingGrantScopeKey(clientId, loginId), value, ttl); } } /** * 删除:用户授权记录 * @param clientId 应用id * @param loginId 账号id */ public void deleteGrantScope(String clientId, Object loginId) { getSaTokenDao().delete(splicingGrantScopeKey(clientId, loginId)); } /** * 获取:用户授权记录 * @param clientId 应用id * @param loginId 账号id * @return 权限 */ public List getGrantScope(String clientId, Object loginId) { String value = getSaTokenDao().get(splicingGrantScopeKey(clientId, loginId)); return SaOAuth2Manager.getDataConverter().convertScopeStringToList(value); } // ------------------- State /** * 保存:state * @param state / */ public void saveState(String state) { if( ! SaFoxUtil.isEmpty(state)) { long ttl = SaOAuth2Manager.getServerConfig().getCodeTimeout(); getSaTokenDao().set(splicingStateSaveKey(state), state, ttl); } } /** * 删除:state记录 * @param state / */ public void deleteState(String state) { getSaTokenDao().delete(splicingStateSaveKey(state)); } /** * 获取:state * @param state / * @return / */ public String getState(String state) { if(SaFoxUtil.isEmpty(state)) { return null; } return getSaTokenDao().get(splicingStateSaveKey(state)); } // ------------------- 其它 /** * 保存:nonce-索引 * @param c / */ public void saveCodeNonceIndex(CodeModel c) { if(c == null || SaFoxUtil.isEmpty(c.nonce)) { return; } getSaTokenDao().set(splicingCodeNonceIndexSaveKey(c.code), c.nonce, SaOAuth2Manager.getServerConfig().getCodeTimeout()); } /** * 获取:nonce * @param code / * @return / */ public String getNonce(String code) { if(SaFoxUtil.isEmpty(code)) { return null; } return getSaTokenDao().get(splicingCodeNonceIndexSaveKey(code)); } // ------------------- 拼接key /** * 拼接 key:Code 保存 * @param code 授权码 * @return key */ public String splicingCodeSaveKey(String code) { return getSaTokenConfig().getTokenName() + ":oauth2:code:" + code; } /** * 拼接 key:Code 索引 * @param clientId 应用id * @param loginId 账号id * @return key */ public String splicingCodeIndexKey(String clientId, Object loginId) { return getSaTokenConfig().getTokenName() + ":oauth2:code-index:" + clientId + ":" + loginId; } /** * 拼接 key:Access-Token 保存 * @param accessToken accessToken * @return key */ public String splicingAccessTokenSaveKey(String accessToken) { return getSaTokenConfig().getTokenName() + ":oauth2:access-token:" + accessToken; } /** * 拼接 key:Access-Token RSD Value * @param clientId 应用id * @param loginId 账号id * @return key */ public String splicingAccessTokenRSDValue(String clientId, Object loginId) { return "access-token:" + clientId + ":" + loginId; } /** * 拼接 key:Refresh-Token 保存 * @param refreshToken refreshToken * @return key */ public String splicingRefreshTokenSaveKey(String refreshToken) { return getSaTokenConfig().getTokenName() + ":oauth2:refresh-token:" + refreshToken; } /** * 拼接 key:Refresh-Token RSD Value * @param clientId 应用id * @param loginId 账号id * @return key */ public String splicingRefreshTokenRSDValue(String clientId, Object loginId) { return "refresh-token:" + clientId + ":" + loginId; } /** * 拼接 key:Client-Token 保存 * @param clientToken clientToken * @return key */ public String splicingClientTokenSaveKey(String clientToken) { return getSaTokenConfig().getTokenName() + ":oauth2:client-token:" + clientToken; } /** * 拼接 key:Client-Token RSD Value * @param clientId 应用id * @return key */ public String splicingClientTokenRSDValue(String clientId) { return "client-token:" + clientId; } /** * 拼接 key:用户授权记录 * @param clientId 应用id * @param loginId 账号id * @return key */ public String splicingGrantScopeKey(String clientId, Object loginId) { return getSaTokenConfig().getTokenName() + ":oauth2:grant-scope:" + clientId + ":" + loginId; } /** * 拼接 key:state 参数保存 * @param state / * @return key */ public String splicingStateSaveKey(String state) { return getSaTokenConfig().getTokenName() + ":oauth2:state:" + state; } /** * 拼接 key:code-nonce 索引 参数保存 * @param code 授权码 * @return key */ public String splicingCodeNonceIndexSaveKey(String code) { return getSaTokenConfig().getTokenName() + ":oauth2:code-nonce-index:" + code; } // -------- bean 对象代理 /** * 获取使用的 getSaTokenDao 实例 * * @return / */ public SaTokenDao getSaTokenDao() { return SaManager.getSaTokenDao(); } /** * 获取使用的 SaTokenConfig 实例 * * @return / */ public SaTokenConfig getSaTokenConfig() { return SaManager.getConfig(); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.convert; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import java.util.List; /** * Sa-Token OAuth2 数据格式转换器 * * @author click33 * @since 1.39.0 */ public interface SaOAuth2DataConverter { /** * 转换 scope 数据格式:String -> List * @param scopeString / * @return / */ List convertScopeStringToList(String scopeString); /** * 转换 scope 数据格式:List -> String * @param scopeList / * @return / */ String convertScopeListToString(List scopeList); /** * 转换 redirect_uri 数据格式:String -> List * @param redirectUris / * @return / */ List convertRedirectUriStringToList(String redirectUris); /** * 根据 RequestAuthModel 构建一个 CodeModel * @param ra RequestAuthModel * @return CodeModel 对象 */ CodeModel convertRequestAuthToCode(RequestAuthModel ra); /** * 根据 RequestAuthModel 构建一个 AccessTokenModel * @param ra RequestAuthModel * @param accessTokenTimeout Access-Token 有效期 (单位:秒) * @return AccessTokenModel 对象 */ AccessTokenModel convertRequestAuthToAccessToken(RequestAuthModel ra, long accessTokenTimeout); /** * 根据 Code 构建一个 Access-Token * @param cm CodeModel对象 * @param accessTokenTimeout Access-Token 有效期 (单位:秒) * @return AccessToken对象 */ AccessTokenModel convertCodeToAccessToken(CodeModel cm, long accessTokenTimeout); /** * 根据 Access-Token 构建一个 Refresh-Token * @param at / * @param refreshTokenTimeout Refresh-Token 有效期 (单位:秒) * @return / */ RefreshTokenModel convertAccessTokenToRefreshToken(AccessTokenModel at, long refreshTokenTimeout); /** * 根据 Refresh-Token 构建一个 Access-Token * @param rt / * @param accessTokenTimeout Access-Token 有效期 (单位:秒) * @return / */ AccessTokenModel convertRefreshTokenToAccessToken(RefreshTokenModel rt, long accessTokenTimeout); /** * 根据 Refresh-Token 构建一个新的 Refresh-Token * @param rt / * @param refreshTokenTimeout Refresh-Token 有效期 (单位:秒) * @return / */ RefreshTokenModel convertRefreshTokenToRefreshToken(RefreshTokenModel rt, long refreshTokenTimeout); /** * 根据 SaClientModel 构建一个 ClientTokenModel * @param clientModel / * @param scopes 权限列表 * @return / */ ClientTokenModel convertSaClientToClientToken(SaClientModel clientModel, List scopes); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverterDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.convert; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTtlMethods; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; /** * Sa-Token OAuth2 数据格式转换器,默认实现类 * * @author click33 * @since 1.39.0 */ public class SaOAuth2DataConverterDefaultImpl implements SaOAuth2DataConverter, SaTtlMethods { /** * 转换 scope 数据格式:String -> List */ @Override public List convertScopeStringToList(String scopeString) { if(SaFoxUtil.isEmpty(scopeString)) { return new ArrayList<>(); } // 兼容以下三种分隔符:空格、逗号、%20、加号 scopeString = scopeString.replace(" ", ","); scopeString = scopeString.replace("%20", ","); scopeString = scopeString.replace("+", ","); return SaFoxUtil.convertStringToList(scopeString); } /** * 转换 scope 数据格式:List -> String */ @Override public String convertScopeListToString(List scopeList) { return SaFoxUtil.convertListToString(scopeList); } /** * 转换 redirect_uri 数据格式:String -> List */ @Override public List convertRedirectUriStringToList(String redirectUris) { if(SaFoxUtil.isEmpty(redirectUris)) { return new ArrayList<>(); } return SaFoxUtil.convertStringToList(redirectUris); } /** * 根据 RequestAuthModel 构建一个 CodeModel * @param ra RequestAuthModel * @return CodeModel 对象 */ @Override public CodeModel convertRequestAuthToCode(RequestAuthModel ra){ String codeValue = SaOAuth2Strategy.instance.createCodeValue.execute(ra.clientId, ra.loginId, ra.scopes); CodeModel cm = new CodeModel(); cm.code = codeValue; cm.clientId = ra.clientId; cm.scopes = ra.scopes; cm.loginId = ra.loginId; cm.redirectUri = ra.redirectUri; cm.nonce = ra.getNonce(); return cm; } /** * 根据 RequestAuthModel 构建一个 AccessTokenModel * @param ra RequestAuthModel * @return AccessTokenModel 对象 */ @Override public AccessTokenModel convertRequestAuthToAccessToken(RequestAuthModel ra, long accessTokenTimeout) { String newAtValue = SaOAuth2Strategy.instance.createAccessToken.execute(ra.clientId, ra.loginId, ra.scopes); AccessTokenModel at = new AccessTokenModel(); at.accessToken = newAtValue; at.clientId = ra.clientId; at.loginId = ra.loginId; at.scopes = ra.scopes; at.tokenType = SaOAuth2Consts.TokenType.Bearer; at.expiresTime = ttlToExpireTime(accessTokenTimeout); at.extraData = new LinkedHashMap<>(); return at; } /** * 根据 Code 构建一个 Access-Token */ @Override public AccessTokenModel convertCodeToAccessToken(CodeModel cm, long accessTokenTimeout) { AccessTokenModel at = new AccessTokenModel(); at.accessToken = SaOAuth2Strategy.instance.createAccessToken.execute(cm.clientId, cm.loginId, cm.scopes); at.clientId = cm.clientId; at.loginId = cm.loginId; at.scopes = cm.scopes; at.tokenType = SaOAuth2Consts.TokenType.Bearer; at.grantType = GrantType.authorization_code; at.expiresTime = ttlToExpireTime(accessTokenTimeout); at.extraData = new LinkedHashMap<>(); return at; } /** * 根据 Access-Token 构建一个 Refresh-Token */ @Override public RefreshTokenModel convertAccessTokenToRefreshToken(AccessTokenModel at, long refreshTokenTimeout) { RefreshTokenModel rt = new RefreshTokenModel(); rt.refreshToken = SaOAuth2Strategy.instance.createRefreshToken.execute(at.clientId, at.loginId, at.scopes); rt.clientId = at.clientId; rt.loginId = at.loginId; rt.scopes = at.scopes; rt.expiresTime = ttlToExpireTime(refreshTokenTimeout); rt.extraData = new LinkedHashMap<>(at.extraData); return rt; } /** * 根据 Refresh-Token 构建一个 Access-Token */ @Override public AccessTokenModel convertRefreshTokenToAccessToken(RefreshTokenModel rt, long accessTokenTimeout) { AccessTokenModel at = new AccessTokenModel(); at.accessToken = SaOAuth2Strategy.instance.createAccessToken.execute(rt.clientId, rt.loginId, rt.scopes); at.refreshToken = rt.refreshToken; at.clientId = rt.clientId; at.loginId = rt.loginId; at.scopes = rt.scopes; at.tokenType = SaOAuth2Consts.TokenType.Bearer; at.grantType = GrantType.refresh_token; at.extraData = new LinkedHashMap<>(rt.extraData); at.expiresTime = ttlToExpireTime(accessTokenTimeout); at.refreshExpiresTime = rt.expiresTime; return at; } /** * 根据 Refresh-Token 构建一个新的 Refresh-Token */ @Override public RefreshTokenModel convertRefreshTokenToRefreshToken(RefreshTokenModel rt, long refreshTokenTimeout) { RefreshTokenModel newRt = new RefreshTokenModel(); newRt.refreshToken = SaOAuth2Strategy.instance.createRefreshToken.execute(rt.clientId, rt.loginId, rt.scopes); newRt.expiresTime = ttlToExpireTime(refreshTokenTimeout); newRt.clientId = rt.clientId; newRt.scopes = rt.scopes; newRt.loginId = rt.loginId; newRt.extraData = new LinkedHashMap<>(rt.extraData); return newRt; } /** * 根据 SaClientModel 构建一个 ClientTokenModel * @param clientModel / * @param scopes 权限列表 * @return / */ @Override public ClientTokenModel convertSaClientToClientToken(SaClientModel clientModel, List scopes) { String clientTokenValue = SaOAuth2Strategy.instance.createClientToken.execute(clientModel.getClientId(), scopes); ClientTokenModel ct = new ClientTokenModel(clientTokenValue, clientModel.getClientId(), scopes); ct.clientToken = clientTokenValue; ct.clientId = clientModel.getClientId(); ct.scopes = scopes; ct.tokenType = SaOAuth2Consts.TokenType.Bearer; ct.expiresTime = ttlToExpireTime(clientModel.getClientTokenTimeout()); ct.grantType = GrantType.client_credentials; ct.extraData = new LinkedHashMap<>(); return ct; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.generate; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import java.util.List; import java.util.function.Consumer; /** * Sa-Token OAuth2 数据构建器,负责相关 Model 数据构建 * * @author click33 * @since 1.39.0 */ public interface SaOAuth2DataGenerate { /** * 构建 Model:Code授权码 * @param ra 请求参数Model * @return 授权码Model */ CodeModel generateCode(RequestAuthModel ra); /** * 构建 Model:Access-Token (根据 code 授权码) * @param code 授权码Model * @return AccessToken Model */ AccessTokenModel generateAccessToken(String code); /** * 刷新 Model:根据 Refresh-Token 生成一个新的 Access-Token * @param refreshToken Refresh-Token值 * @return 新的 Access-Token */ AccessTokenModel refreshAccessToken(String refreshToken); /** * 构建 Model:Access-Token (根据 RequestAuthModel 构建,用于隐藏式 and 密码式) * @param ra 请求参数Model * @param isCreateRt 是否生成对应的Refresh-Token * @param appendWork 对生成的 AccessTokenModel 进行追加操作 * @return Access-Token Model */ AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt, Consumer appendWork); /** * 构建 Model:Client-Token * @param clientId 应用id * @param scopes 授权范围 * @return Client-Token Model */ ClientTokenModel generateClientToken(String clientId, List scopes); /** * 构建 URL:下放Code URL (Authorization Code 授权码) * @param redirectUri 下放地址 * @param code code参数 * @param state state参数 * @return 构建完毕的URL */ String buildRedirectUri(String redirectUri, String code, String state); /** * 构建 URL:下放Access-Token URL (implicit 隐藏式) * @param redirectUri 下放地址 * @param token token * @param state state参数 * @return 构建完毕的URL */ String buildImplicitRedirectUri(String redirectUri, String token, String state); /** * 检查 state 是否被重复使用 * @param state / */ void checkState(String state); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.generate; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2AuthorizationCodeException; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.exception.SaOAuth2RefreshTokenException; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaTtlMethods; import java.util.List; import java.util.function.Consumer; /** * Sa-Token OAuth2 数据构建器,默认实现类 * * @author click33 * @since 1.39.0 */ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate, SaTtlMethods { /** * 构建 Model:Code授权码 * @param ra 请求参数Model * @return 授权码Model */ @Override public CodeModel generateCode(RequestAuthModel ra) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 删除旧 Code dao.deleteCode(dao.getCodeValue(ra.clientId, ra.loginId)); // 生成新 Code CodeModel cm = SaOAuth2Manager.getDataConverter().convertRequestAuthToCode(ra); // 保存新 Code dao.saveCode(cm); dao.saveCodeIndex(cm); // 保存 code -> nonce dao.saveCodeNonceIndex(cm); // 返回 return cm; } /** * 构建 Model:Access-Token (根据 code 授权码) * @param code 授权码 * @return AccessToken Model */ @Override public AccessTokenModel generateAccessToken(String code) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter(); // 1、先校验 CodeModel cm = dao.getCode(code); SaOAuth2AuthorizationCodeException.throwBy(cm == null, "无效 code: " + code, code, SaOAuth2ErrorCode.CODE_30110); // 2、开发者自定义的授权前置检查 SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(cm.loginId, cm.clientId); // 3、生成新 Access-Token SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(cm.clientId); AccessTokenModel at = dataConverter.convertCodeToAccessToken(cm, clientModel.getAccessTokenTimeout()); SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at); // 4、生成新 Refresh-Token RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at, clientModel.getRefreshTokenTimeout()); at.refreshToken = rt.refreshToken; at.refreshExpiresTime = rt.expiresTime; // 5、保存 Access-Token、Refresh-Token dao.saveAccessToken(at); dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount()); dao.saveRefreshToken(rt); dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount()); // 6、删除 Code (一个 code 只可以使用一次) dao.deleteCode(code); dao.deleteCodeIndex(cm.clientId, cm.loginId); // 7、返回 Access-Token return at; } /** * 刷新 Model:根据 Refresh-Token 生成一个新的 Access-Token * @param refreshToken Refresh-Token值 * @return 新的 Access-Token */ @Override public AccessTokenModel refreshAccessToken(String refreshToken) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 1、获取 Refresh-Token 信息 RefreshTokenModel rt = dao.getRefreshToken(refreshToken); SaOAuth2RefreshTokenException.throwBy(rt == null, "无效 refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111); // 2、开发者自定义的授权前置检查 SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(rt.loginId, rt.clientId); // 3、如果配置了 isNewRefresh=true,则生成一个新的 Refresh-Token SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(rt.clientId); if(clientModel.getIsNewRefresh()) { rt = SaOAuth2Manager.getDataConverter().convertRefreshTokenToRefreshToken(rt, clientModel.getRefreshTokenTimeout()); dao.saveRefreshToken(rt); dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount()); } // 4、生成新 Access-Token AccessTokenModel at = SaOAuth2Manager.getDataConverter().convertRefreshTokenToAccessToken(rt, clientModel.getAccessTokenTimeout()); SaOAuth2Strategy.instance.refreshAccessTokenWorkByScope.accept(at); // 5、保存新 Access-Token dao.saveAccessToken(at); dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount()); // 6、返回新 Access-Token return at; } /** * 构建 Model:Access-Token (根据 RequestAuthModel 构建,用于隐藏式 and 密码式) * @param ra 请求参数Model * @param isCreateRt 是否生成对应的Refresh-Token * @param appendWork 对生成的 AccessTokenModel 进行追加操作 * * @return Access-Token Model */ @Override public AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt, Consumer appendWork) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter(); // 1、开发者自定义的授权前置检查 SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); // 2、生成 Access-Token SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(ra.clientId); AccessTokenModel at = dataConverter.convertRequestAuthToAccessToken(ra, clientModel.getAccessTokenTimeout()); if(appendWork != null) { appendWork.accept(at); } SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at); // 3、生成 & 保存 Refresh-Token if(isCreateRt) { RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at, clientModel.getRefreshTokenTimeout()); at.refreshToken = rt.refreshToken; at.refreshExpiresTime = rt.expiresTime; dao.saveRefreshToken(rt); dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount()); } // 4、保存 Access-Token dao.saveAccessToken(at); dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount()); // 5、返回 Access-Token return at; } /** * 构建 Model:Client-Token * @param clientId 应用id * @param scopes 授权范围 * @return Client-Token Model */ @Override public ClientTokenModel generateClientToken(String clientId, List scopes) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 1、如果配置了 Lower-Client-Token 的 ttl ,则需要更新一下 SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(clientId); // 2、生成 Client-Token ClientTokenModel ct = SaOAuth2Manager.getDataConverter().convertSaClientToClientToken(clientModel, scopes); SaOAuth2Strategy.instance.workClientTokenByScope.accept(ct); // 3、保存 Client-Token dao.saveClientToken(ct); dao.saveClientTokenIndex_AndAdjust(ct, clientModel.getMaxClientTokenCount()); // 4、返回 return ct; } /** * 构建 URL:下放Code URL (Authorization Code 授权码) * @param redirectUri 下放地址 * @param code code参数 * @param state state参数 * @return 构建完毕的URL */ @Override public String buildRedirectUri(String redirectUri, String code, String state) { String url = SaFoxUtil.joinParam(redirectUri, SaOAuth2Consts.Param.code, code); if( ! SaFoxUtil.isEmpty(state)) { checkState(state); url = SaFoxUtil.joinParam(url, SaOAuth2Consts.Param.state, state); } return url; } /** * 构建 URL:下放Access-Token URL (implicit 隐藏式) * @param redirectUri 下放地址 * @param token token * @param state state参数 * @return 构建完毕的URL */ @Override public String buildImplicitRedirectUri(String redirectUri, String token, String state) { String url = SaFoxUtil.joinSharpParam(redirectUri, SaOAuth2Consts.Param.token, token); if( ! SaFoxUtil.isEmpty(state)) { checkState(state); url = SaFoxUtil.joinSharpParam(url, SaOAuth2Consts.Param.state, state); } return url; } /** * 检查 state 是否被重复使用 * @param state / */ @Override public void checkState(String state) { String value = SaOAuth2Manager.getDao().getState(state); if(SaFoxUtil.isNotEmpty(value)) { throw new SaOAuth2Exception("多次请求的 state 不可重复: " + state).setCode(SaOAuth2ErrorCode.CODE_30127); } SaOAuth2Manager.getDao().saveState(state); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.loader; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException; import cn.dev33.satoken.secure.SaSecureUtil; import java.util.List; /** * Sa-Token OAuth2 数据加载器 * * @author click33 * @since 1.39.0 */ public interface SaOAuth2DataLoader { /** * 根据 id 获取 Client 信息 * * @param clientId 应用id * @return ClientModel */ default SaClientModel getClientModel(String clientId) { // 默认从内存配置中读取数据 return SaOAuth2Manager.getServerConfig().getClients().get(clientId); } /** * 根据 id 获取 Client 信息,不允许为 null * * @param clientId 应用id * @return ClientModel */ default SaClientModel getClientModelNotNull(String clientId) { SaClientModel clientModel = getClientModel(clientId); if(clientModel == null) { throw new SaOAuth2ClientModelException("无效 client_id: " + clientId) .setClientId(clientId) .setCode(SaOAuth2ErrorCode.CODE_30105); } return clientModel; } /** * 根据 ClientId 和 LoginId 获取 openid * * @param clientId 应用id * @param loginId 账号id * @return 此账号在此Client下的openid */ default String getOpenid(String clientId, Object loginId) { return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getOpenidDigestPrefix() + "_" + clientId + "_" + loginId); } /** * 根据 subjectId 和 loginId 获取 unionid * * @param subjectId 应用主体id * @param loginId 账号id * @return 此账号在此主体 Client 下的 unionid */ default String getUnionid(String subjectId, Object loginId) { return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getUnionidDigestPrefix() + "_" + subjectId + "_" + loginId); } /** * 获取高级权限列表 * @return / */ default List getHigherScopeList() { String higherScope = SaOAuth2Manager.getServerConfig().getHigherScope(); return SaOAuth2Manager.getDataConverter().convertScopeStringToList(higherScope); } /** * 获取低级权限列表 * @return / */ default List getLowerScopeList() { String lowerScope = SaOAuth2Manager.getServerConfig().getLowerScope(); return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoaderDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.loader; /** * Sa-Token OAuth2 数据加载器 默认实现类 * * @author click33 * @since 1.39.0 */ public class SaOAuth2DataLoaderDefaultImpl implements SaOAuth2DataLoader{ // be empty of } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/AccessTokenModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model; import java.io.Serializable; import java.util.List; import java.util.Map; /** * Model: Access-Token * * @author click33 * @since 1.23.0 */ public class AccessTokenModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * Access-Token 值 */ public String accessToken; /** * Refresh-Token 值 */ public String refreshToken; /** * Access-Token 到期时间 */ public long expiresTime; /** * Refresh-Token 到期时间 */ public long refreshExpiresTime; /** * 应用id */ public String clientId; /** * 账号id */ public Object loginId; /** * 授权范围 */ public List scopes; /** * Token 类型 */ public String tokenType; /** * 授权类型 */ public String grantType; /** * 扩展数据 */ public Map extraData; /** * 创建时间,13位时间戳 */ public long createTime; public AccessTokenModel() { this.createTime = System.currentTimeMillis(); } /** * 构建一个 * @param accessToken accessToken * @param clientId 应用id * @param scopes 请求授权范围 * @param loginId 对应的账号id */ public AccessTokenModel(String accessToken, String clientId, Object loginId, List scopes) { this(); this.accessToken = accessToken; this.clientId = clientId; this.loginId = loginId; this.scopes = scopes; } // 额外追加方法 /** * 获取:此 Access-Token 的剩余有效期(秒) * @return / */ public long getExpiresIn() { long s = (expiresTime - System.currentTimeMillis()) / 1000; return s < 1 ? -2 : s; } /** * 获取:此 Refresh-Token 的剩余有效期(秒) * @return / */ public long getRefreshExpiresIn() { long s = (refreshExpiresTime - System.currentTimeMillis()) / 1000; return s < 1 ? -2 : s; } // get set public String getAccessToken() { return accessToken; } public AccessTokenModel setAccessToken(String accessToken) { this.accessToken = accessToken; return this; } public String getRefreshToken() { return refreshToken; } public AccessTokenModel setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; return this; } public long getExpiresTime() { return expiresTime; } public AccessTokenModel setExpiresTime(long expiresTime) { this.expiresTime = expiresTime; return this; } public long getRefreshExpiresTime() { return refreshExpiresTime; } public AccessTokenModel setRefreshExpiresTime(long refreshExpiresTime) { this.refreshExpiresTime = refreshExpiresTime; return this; } public String getClientId() { return clientId; } public AccessTokenModel setClientId(String clientId) { this.clientId = clientId; return this; } public Object getLoginId() { return loginId; } public AccessTokenModel setLoginId(Object loginId) { this.loginId = loginId; return this; } public List getScopes() { return scopes; } public AccessTokenModel setScopes(List scopes) { this.scopes = scopes; return this; } public String getTokenType() { return tokenType; } public AccessTokenModel setTokenType(String tokenType) { this.tokenType = tokenType; return this; } public String getGrantType() { return grantType; } public AccessTokenModel setGrantType(String grantType) { this.grantType = grantType; return this; } public Map getExtraData() { return extraData; } public AccessTokenModel setExtraData(Map extraData) { this.extraData = extraData; return this; } public long getCreateTime() { return createTime; } public AccessTokenModel setCreateTime(long createTime) { this.createTime = createTime; return this; } @Override public String toString() { return "AccessTokenModel{" + "accessToken='" + accessToken + ", refreshToken='" + refreshToken + ", expiresTime=" + expiresTime + ", refreshExpiresTime=" + refreshExpiresTime + ", clientId='" + clientId + ", loginId=" + loginId + ", scopes=" + scopes + ", tokenType='" + tokenType + ", grantType='" + grantType + ", extraData=" + extraData + ", createTime=" + createTime + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/ClientTokenModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model; import java.io.Serializable; import java.util.List; import java.util.Map; /** * Model: Client-Token * * @author click33 * @since 1.23.0 */ public class ClientTokenModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * Client-Token 值 */ public String clientToken; /** * Client-Token 到期时间 */ public long expiresTime; /** * 应用id */ public String clientId; /** * 授权范围 */ public List scopes; /** * Token 类型 */ public String tokenType; /** * 授权类型 */ public String grantType; /** * 扩展数据 */ public Map extraData; /** * 创建时间,13位时间戳 */ public long createTime; public ClientTokenModel(){ this.createTime = System.currentTimeMillis(); } /** * 构建一个 ClientTokenModel * @param clientToken clientToken * @param clientId 应用id * @param scopes 请求授权范围 */ public ClientTokenModel(String clientToken, String clientId, List scopes) { this(); this.clientToken = clientToken; this.clientId = clientId; this.scopes = scopes; } // 额外追加方法 /** * 获取:此 Client-Token 的剩余有效期(秒) * @return / */ public long getExpiresIn() { long s = (expiresTime - System.currentTimeMillis()) / 1000; return s < 1 ? -2 : s; } // get set public String getClientToken() { return clientToken; } public ClientTokenModel setClientToken(String clientToken) { this.clientToken = clientToken; return this; } public long getExpiresTime() { return expiresTime; } public ClientTokenModel setExpiresTime(long expiresTime) { this.expiresTime = expiresTime; return this; } public String getClientId() { return clientId; } public ClientTokenModel setClientId(String clientId) { this.clientId = clientId; return this; } public List getScopes() { return scopes; } public ClientTokenModel setScopes(List scopes) { this.scopes = scopes; return this; } public String getTokenType() { return tokenType; } public ClientTokenModel setTokenType(String tokenType) { this.tokenType = tokenType; return this; } public String getGrantType() { return grantType; } public ClientTokenModel setGrantType(String grantType) { this.grantType = grantType; return this; } public Map getExtraData() { return extraData; } public ClientTokenModel setExtraData(Map extraData) { this.extraData = extraData; return this; } public long getCreateTime() { return createTime; } public ClientTokenModel setCreateTime(long createTime) { this.createTime = createTime; return this; } @Override public String toString() { return "ClientTokenModel{" + "clientToken='" + clientToken + ", expiresTime=" + expiresTime + ", clientId='" + clientId + ", scopes=" + scopes + ", tokenType=" + tokenType + ", grantType=" + grantType + ", extraData=" + extraData + ", createTime=" + createTime + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/CodeModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model; import java.io.Serializable; import java.util.List; /** * Model: 授权码 * * @author click33 * @since 1.23.0 */ public class CodeModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 授权码 */ public String code; /** * 应用id */ public String clientId; /** * 授权范围 */ public List scopes; /** * 对应账号id */ public Object loginId; /** * 重定向的地址 */ public String redirectUri; /** * 随机数 */ public String nonce; /** * 创建时间,13位时间戳 */ public long createTime; /** * 构建一个 */ public CodeModel() { this.createTime = System.currentTimeMillis(); } /** * 构建一个 * @param code 授权码 * @param clientId 应用id * @param scopes 请求授权范围 * @param loginId 对应的账号id * @param redirectUri 重定向地址 * @param nonce 随机数 */ public CodeModel(String code, String clientId, List scopes, Object loginId, String redirectUri, String nonce) { this(); this.code = code; this.clientId = clientId; this.scopes = scopes; this.loginId = loginId; this.redirectUri = redirectUri; this.nonce = nonce; } public String getCode() { return code; } public CodeModel setCode(String code) { this.code = code; return this; } public String getClientId() { return clientId; } public CodeModel setClientId(String clientId) { this.clientId = clientId; return this; } public List getScopes() { return scopes; } public CodeModel setScopes(List scopes) { this.scopes = scopes; return this; } public Object getLoginId() { return loginId; } public CodeModel setLoginId(Object loginId) { this.loginId = loginId; return this; } public String getRedirectUri() { return redirectUri; } public CodeModel setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; return this; } public String getNonce() { return nonce; } public CodeModel setNonce(String nonce) { this.nonce = nonce; return this; } public long getCreateTime() { return createTime; } public CodeModel setCreateTime(long createTime) { this.createTime = createTime; return this; } @Override public String toString() { return "CodeModel{" + "code='" + code + '\'' + ", clientId='" + clientId + '\'' + ", scopes=" + scopes + ", loginId=" + loginId + ", redirectUri='" + redirectUri + '\'' + ", nonce='" + nonce + '\'' + ", createTime=" + createTime + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/RefreshTokenModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model; import java.io.Serializable; import java.util.List; import java.util.Map; /** * Model: Refresh-Token * * @author click33 * @since 1.23.0 */ public class RefreshTokenModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * Refresh-Token 值 */ public String refreshToken; /** * Refresh-Token 到期时间 */ public long expiresTime; /** * 应用id */ public String clientId; /** * 对应账号id */ public Object loginId; /** * 授权范围 */ public List scopes; /** * 扩展数据 */ public Map extraData; /** * 创建时间,13位时间戳 */ public long createTime; public RefreshTokenModel() { this.createTime = System.currentTimeMillis(); } // 额外追加方法 /** * 获取:此 Refresh-Token 的剩余有效期(秒) * @return / */ public long getExpiresIn() { long s = (expiresTime - System.currentTimeMillis()) / 1000; return s < 1 ? -2 : s; } // get set public String getRefreshToken() { return refreshToken; } public RefreshTokenModel setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; return this; } public long getExpiresTime() { return expiresTime; } public RefreshTokenModel setExpiresTime(long expiresTime) { this.expiresTime = expiresTime; return this; } public String getClientId() { return clientId; } public RefreshTokenModel setClientId(String clientId) { this.clientId = clientId; return this; } public List getScopes() { return scopes; } public RefreshTokenModel setScopes(List scopes) { this.scopes = scopes; return this; } public Object getLoginId() { return loginId; } public RefreshTokenModel setLoginId(Object loginId) { this.loginId = loginId; return this; } public Map getExtraData() { return extraData; } public RefreshTokenModel setExtraData(Map extraData) { this.extraData = extraData; return this; } public long getCreateTime() { return createTime; } public RefreshTokenModel setCreateTime(long createTime) { this.createTime = createTime; return this; } @Override public String toString() { return "RefreshTokenModel [" + "refreshToken=" + refreshToken + ", expiresTime=" + expiresTime + ", clientId=" + clientId + ", loginId=" + loginId + ", scopes=" + scopes + ", extraData=" + extraData + ", createTime=" + createTime + "]"; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model.loader; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Client 应用信息 Model * * @author click33 * @since 1.23.0 */ public class SaClientModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 应用id */ public String clientId; /** * 应用秘钥 */ public String clientSecret; /** * 应用签约的所有权限 */ public List contractScopes = new ArrayList<>(); /** * 应用允许授权的所有 redirect_uri */ public List allowRedirectUris = new ArrayList<>(); /** * 应用允许的所有 grant_type */ public List allowGrantTypes = new ArrayList<>(); /** * 主体id */ public String subjectId; /** 此应用 Access-Token 保存的时间(单位秒) [默认取全局配置] */ public long accessTokenTimeout; /** 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] */ public long refreshTokenTimeout; /** 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] */ public long clientTokenTimeout; /** 此应用单个用户最多同时存在的 Access-Token 数量 */ public int maxAccessTokenCount; /** 此应用单个用户最多同时存在的 Refresh-Token 数量 */ public int maxRefreshTokenCount; /** 此应用最多同时存在的 Client-Token 数量 */ public int maxClientTokenCount; /** 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token [默认取全局配置] */ public Boolean isNewRefresh; /** 是否允许此应用自动确认授权(高危配置,禁止向不被信任的第三方开启此选项) */ public Boolean isAutoConfirm = false; public SaClientModel() { SaOAuth2Strategy.instance.setSaClientModelDefaultFields.run(this); } public SaClientModel(String clientId, String clientSecret, List contractScopes, List allowRedirectUris) { this(); this.clientId = clientId; this.clientSecret = clientSecret; this.contractScopes = contractScopes; this.allowRedirectUris = allowRedirectUris; } // 追加方法 /** * @param scopes 添加应用签约的所有权限 * @return 对象自身 */ public SaClientModel addContractScopes(String... scopes) { if(this.contractScopes == null) { this.contractScopes = new ArrayList<>(); } this.contractScopes.addAll(Arrays.asList(scopes)); return this; } /** * @param redirectUris 添加应用允许授权的所有 redirect_uri * @return 对象自身 */ public SaClientModel addAllowRedirectUris(String... redirectUris) { if(this.allowRedirectUris == null) { this.allowRedirectUris = new ArrayList<>(); } this.allowRedirectUris.addAll(Arrays.asList(redirectUris)); return this; } /** * @param grantTypes 应用允许的所有 grant_type * @return 对象自身 */ public SaClientModel addAllowGrantTypes(String... grantTypes) { if(this.allowGrantTypes == null) { this.allowGrantTypes = new ArrayList<>(); } this.allowGrantTypes.addAll(Arrays.asList(grantTypes)); return this; } // get set /** * @return 应用id */ public String getClientId() { return clientId; } /** * @param clientId 应用id * @return 对象自身 */ public SaClientModel setClientId(String clientId) { this.clientId = clientId; return this; } /** * @return 应用秘钥 */ public String getClientSecret() { return clientSecret; } /** * @param clientSecret 应用秘钥 * @return 对象自身 */ public SaClientModel setClientSecret(String clientSecret) { this.clientSecret = clientSecret; return this; } /** * @return 应用签约的所有权限 */ public List getContractScopes() { return contractScopes; } /** * @param contractScopes 应用签约的所有权限 * @return 对象自身 */ public SaClientModel setContractScopes(List contractScopes) { this.contractScopes = contractScopes; return this; } /** * @return 应用允许授权的所有 redirect_uri */ public List getAllowRedirectUris() { return allowRedirectUris; } /** * @param allowRedirectUris 应用允许授权的所有 redirect_uri * @return 对象自身 */ public SaClientModel setAllowRedirectUris(List allowRedirectUris) { this.allowRedirectUris = allowRedirectUris; return this; } /** * @return 应用允许的所有 grant_type */ public List getAllowGrantTypes() { return allowGrantTypes; } /** * 应用允许的所有 grant_type * @param allowGrantTypes / * @return / */ public SaClientModel setAllowGrantTypes(List allowGrantTypes) { this.allowGrantTypes = allowGrantTypes; return this; } /** * 获取 主体id * * @return subjectId 主体id */ public String getSubjectId() { return this.subjectId; } /** * 设置 主体id * * @param subjectId 主体id */ public SaClientModel setSubjectId(String subjectId) { this.subjectId = subjectId; return this; } /** * @return 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token [默认取全局配置] */ public Boolean getIsNewRefresh() { return isNewRefresh; } /** * @param isNewRefresh 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token [默认取全局配置] * @return 对象自身 */ public SaClientModel setIsNewRefresh(Boolean isNewRefresh) { this.isNewRefresh = isNewRefresh; return this; } /** * @return 此应用 Access-Token 保存的时间(单位秒) [默认取全局配置] */ public long getAccessTokenTimeout() { return accessTokenTimeout; } /** * @param accessTokenTimeout 此应用 Access-Token 保存的时间(单位秒) [默认取全局配置] * @return 对象自身 */ public SaClientModel setAccessTokenTimeout(long accessTokenTimeout) { this.accessTokenTimeout = accessTokenTimeout; return this; } /** * @return 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] */ public long getRefreshTokenTimeout() { return refreshTokenTimeout; } /** * @param refreshTokenTimeout 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] * @return 对象自身 */ public SaClientModel setRefreshTokenTimeout(long refreshTokenTimeout) { this.refreshTokenTimeout = refreshTokenTimeout; return this; } /** * @return 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] */ public long getClientTokenTimeout() { return clientTokenTimeout; } /** * @param clientTokenTimeout 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] * @return 对象自身 */ public SaClientModel setClientTokenTimeout(long clientTokenTimeout) { this.clientTokenTimeout = clientTokenTimeout; return this; } /** * 获取 是否允许此应用自动确认授权(高危配置,禁止向不被信任的第三方开启此选项) * * @return / */ public Boolean getIsAutoConfirm() { return this.isAutoConfirm; } /** * 设置 是否允许此应用自动确认授权(高危配置,禁止向不被信任的第三方开启此选项) * * @param isAutoConfirm / * @return 对象自身 */ public SaClientModel setIsAutoConfirm(Boolean isAutoConfirm) { this.isAutoConfirm = isAutoConfirm; return this; } /** * 此应用单个用户最多同时存在的 Access-Token 数量 * @return / */ public int getMaxAccessTokenCount() { return maxAccessTokenCount; } /** * 设置 此应用单个用户最多同时存在的 Access-Token 数量 * @param maxAccessTokenCount / * @return 对象自身 */ public SaClientModel setMaxAccessTokenCount(int maxAccessTokenCount) { this.maxAccessTokenCount = maxAccessTokenCount; return this; } /** * 此应用单个用户最多同时存在的 Refresh-Token 数量 * @return / */ public int getMaxRefreshTokenCount() { return maxRefreshTokenCount; } /** * 此应用单个用户最多同时存在的 Refresh-Token 数量 * @param maxRefreshTokenCount / * @return 对象自身 */ public SaClientModel setMaxRefreshTokenCount(int maxRefreshTokenCount) { this.maxRefreshTokenCount = maxRefreshTokenCount; return this; } /** * 此应用单个用户最多同时存在的 Client-Token 数量 * @return / */ public int getMaxClientTokenCount() { return maxClientTokenCount; } /** * 此应用单个用户最多同时存在的 Client-Token 数量 * @param maxClientTokenCount / * @return 对象自身 */ public SaClientModel setMaxClientTokenCount(int maxClientTokenCount) { this.maxClientTokenCount = maxClientTokenCount; return this; } @Override public String toString() { return "SaClientModel{" + "clientId='" + clientId + '\'' + ", clientSecret='" + clientSecret + '\'' + ", contractScopes=" + contractScopes + ", allowRedirectUris=" + allowRedirectUris + ", allowGrantTypes=" + allowGrantTypes + ", subjectId=" + subjectId + ", isNewRefresh=" + isNewRefresh + ", accessTokenTimeout=" + accessTokenTimeout + ", refreshTokenTimeout=" + refreshTokenTimeout + ", clientTokenTimeout=" + clientTokenTimeout + ", isAutoConfirm=" + isAutoConfirm + ", maxAccessTokenCount=" + maxAccessTokenCount + ", refreshTokenTimeout=" + refreshTokenTimeout + ", maxClientTokenCount=" + maxClientTokenCount + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model.oidc; import java.io.Serializable; import java.util.Map; /** * OIDC IdToken Model * *
    参考: *
    IDToken *
    StandardClaims * * @author click33 * @since 1.23.0 */ public class IdTokenModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 必填:发行者标识符,例如:https://server.example.com */ public String iss; /** * 必填:用户标识符,用户id,例如:10001 */ public Object sub; /** * 必填:客户端标识符,clientId,例如:s6BhdRkqt3 */ public String aud; /** * 必填:令牌到期时间,10位时间戳,例如:1723341795 */ public long exp; /** * 必填:签发此令牌的时间,10位时间戳,例如:1723339995 */ public long iat; /** * 用户认证时间,10位时间戳,例如:1723339988 */ public long authTime; /** * 随机数,客户端提供,防止重放攻击,例如:e9a3f4d9 */ public String nonce; /** * 身份验证上下文类引用 */ public String acr; /** * 身份验证方法参考 */ public String amr; /** * 授权方 - 签发 ID 令牌的一方,如果存在,它必须包含此方的 OAuth 2.0 客户端 ID。 */ public String azp; /** * 扩展数据 */ public Map extraData; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/ClientIdAndSecretModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model.request; import java.io.Serializable; /** * Client 的 id 和 secret * * @author click33 * @since 1.39.0 */ public class ClientIdAndSecretModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 应用id */ public String clientId; /** * 应用秘钥 */ public String clientSecret; public ClientIdAndSecretModel() { } public ClientIdAndSecretModel(String clientId, String clientSecret) { super(); this.clientId = clientId; this.clientSecret = clientSecret; } /** * @return 应用id */ public String getClientId() { return clientId; } /** * @param clientId 应用id * @return 对象自身 */ public ClientIdAndSecretModel setClientId(String clientId) { this.clientId = clientId; return this; } /** * @return 应用秘钥 */ public String getClientSecret() { return clientSecret; } /** * @param clientSecret 应用秘钥 * @return 对象自身 */ public ClientIdAndSecretModel setClientSecret(String clientSecret) { this.clientSecret = clientSecret; return this; } @Override public String toString() { return "ClientIdAndSecretModel{" + "clientId='" + clientId + '\'' + ", clientSecret='" + clientSecret + '\'' + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/RequestAuthModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.model.request; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; import java.util.List; /** * 请求授权参数的 Model * * @author click33 * @since 1.23.0 */ public class RequestAuthModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 应用id */ public String clientId; /** * 授权范围 */ public List scopes; /** * 对应的账号id */ public Object loginId; /** * 待重定向URL */ public String redirectUri; /** * 授权类型, 非必填 */ public String responseType; /** * 状态标识, 可为null */ public String state; /** * 随机数 */ public String nonce; /** * @return clientId */ public String getClientId() { return clientId; } /** * @param clientId 要设置的 clientId * @return 对象自身 */ public RequestAuthModel setClientId(String clientId) { this.clientId = clientId; return this; } /** * @return scopes */ public List getScopes() { return scopes; } /** * @param scopes 要设置的 scopes * @return 对象自身 */ public RequestAuthModel setScopes(List scopes) { this.scopes = scopes; return this; } /** * @return loginId */ public Object getLoginId() { return loginId; } /** * @param loginId 要设置的 loginId * @return 对象自身 */ public RequestAuthModel setLoginId(Object loginId) { this.loginId = loginId; return this; } /** * @return redirectUri */ public String getRedirectUri() { return redirectUri; } /** * @param redirectUri 要设置的 redirectUri * @return 对象自身 */ public RequestAuthModel setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; return this; } /** * @return responseType */ public String getResponseType() { return responseType; } /** * @param responseType 要设置的 responseType * @return 对象自身 */ public RequestAuthModel setResponseType(String responseType) { this.responseType = responseType; return this; } /** * @return state */ public String getState() { return state; } /** * @param state 要设置的 state * @return 对象自身 */ public RequestAuthModel setState(String state) { this.state = state; return this; } /** * @return nonce */ public String getNonce() { return nonce; } /** * @param nonce 要设置的随机数 * @return 对象自身 */ public RequestAuthModel setNonce(String nonce) { this.nonce = nonce; return this; } /** * 数据自检 * @return 对象自身 */ public RequestAuthModel checkModel() { if(SaFoxUtil.isEmpty(clientId)) { throw new SaOAuth2Exception("client_id 不可为空").setCode(SaOAuth2ErrorCode.CODE_30101); } if(SaFoxUtil.isEmpty(scopes)) { throw new SaOAuth2Exception("scope 不可为空").setCode(SaOAuth2ErrorCode.CODE_30102); } if(SaFoxUtil.isEmpty(redirectUri)) { throw new SaOAuth2Exception("redirect_uri 不可为空").setCode(SaOAuth2ErrorCode.CODE_30103); } if(SaFoxUtil.isEmpty(String.valueOf(loginId))) { throw new SaOAuth2Exception("LoginId 不可为空").setCode(SaOAuth2ErrorCode.CODE_30104); } return this; } @Override public String toString() { return "RequestAuthModel{" + "clientId='" + clientId + '\'' + ", scopes=" + scopes + ", loginId=" + loginId + ", redirectUri='" + redirectUri + '\'' + ", responseType='" + responseType + '\'' + ", state='" + state + '\'' + ", nonce='" + nonce + '\'' + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolver.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.resolver; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.util.SaResult; import java.util.Map; /** * Sa-Token OAuth2 数据解析器,负责 Web 交互层面的数据进出: *

    1、从请求中按照指定格式读取数据

    *

    2、构建数据输出格式

    * * @author click33 * @since 1.39.0 */ public interface SaOAuth2DataResolver { /** * 数据读取:从请求对象中读取 ClientId、Secret * * @param request / * @return / */ ClientIdAndSecretModel readClientIdAndSecret(SaRequest request); /** * 数据读取:从请求对象中读取 AccessToken,获取不到返回 null *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token * * @param request / * @return / */ String readAccessToken(SaRequest request); /** * 数据读取:从请求对象中读取 ClientToken,获取不到返回 null *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token * * @param request / * @return / */ String readClientToken(SaRequest request); /** * 数据读取:从请求对象中构建 RequestAuthModel * @param req SaRequest对象 * @param loginId 账号id * @return RequestAuthModel对象 */ RequestAuthModel readRequestAuthModel(SaRequest req, Object loginId); /** * 构建返回值: 获取 token * @param at token信息 * @return / */ Map buildAccessTokenReturnValue(AccessTokenModel at); /** * 构建返回值: RefreshToken 刷新 Access-Token * @param at token信息 * @return / */ default Map buildRefreshTokenReturnValue(AccessTokenModel at) { return buildAccessTokenReturnValue(at); } /** * 构建返回值: 回收 Access-Token * @return / */ default Map buildRevokeTokenReturnValue() { return SaResult.ok(); } /** * 构建返回值: 凭证式 模式认证 获取 token * @param ct token信息 */ Map buildClientTokenReturnValue(ClientTokenModel ct); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolverDefaultImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.data.resolver; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Param; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.TokenType; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.util.LinkedHashMap; import java.util.Map; /** * Sa-Token OAuth2 数据解析器,负责 Web 交互层面的数据进出: *

    1、从请求中按照指定格式读取数据

    *

    2、构建数据输出格式

    * * @author click33 * @since 1.39.0 */ public class SaOAuth2DataResolverDefaultImpl implements SaOAuth2DataResolver { /** * 数据读取:从请求对象中读取 ClientId、Secret,如果获取不到则抛出异常 * * @param request / * @return / */ @Override public ClientIdAndSecretModel readClientIdAndSecret(SaRequest request) { // 优先从请求参数中获取 String clientId = request.getParam(SaOAuth2Consts.Param.client_id); String clientSecret = request.getParam(SaOAuth2Consts.Param.client_secret); // 此处必须 clientId 和 clientSecret 都有值才可以采用,fix pr: https://gitee.com/dromara/sa-token/pulls/346 if(SaFoxUtil.isNotEmpty(clientId) && SaFoxUtil.isNotEmpty(clientSecret)) { return new ClientIdAndSecretModel(clientId, clientSecret); } // 如果请求参数中没有提供 client_id 参数,则尝试从 Authorization 中获取 String authorizationValue = SaHttpBasicUtil.getAuthorizationValue(); if(SaFoxUtil.isNotEmpty(authorizationValue)) { String[] arr = authorizationValue.split(":"); clientId = arr[0]; if(arr.length > 1) { clientSecret = arr[1]; } return new ClientIdAndSecretModel(clientId, clientSecret); } // 如果只提供了 clientId 参数,也为其构建一个 ClientIdAndSecretModel 对象,clientSecret 置空 if(SaFoxUtil.isNotEmpty(clientId)) { return new ClientIdAndSecretModel(clientId, null); } // 如果都没有提供,则抛出异常 throw new SaOAuth2Exception("请提供 client 信息").setCode(SaOAuth2ErrorCode.CODE_30191); } /** * 数据读取:从请求对象中读取 AccessToken,获取不到返回 null,获取不到返回 null *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token */ @Override public String readAccessToken(SaRequest request) { // 优先从请求参数中获取,可以读取到的话直接返回 String accessToken = request.getParam(Param.access_token); if(SaFoxUtil.isNotEmpty(accessToken)) { return accessToken; } // 如果请求参数中没有提供 access_token 参数,则尝试从 Authorization 中获取 String authorizationValue = request.getHeader(Param.Authorization); if(SaFoxUtil.isEmpty(authorizationValue)) { return null; } // 判断前缀,裁剪 String prefix = TokenType.Bearer + " "; if(authorizationValue.startsWith(prefix)) { return authorizationValue.substring(prefix.length()); } // 前缀不符合,返回 null return null; } /** * 数据读取:从请求对象中读取 ClientToken,获取不到返回 null *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token */ @Override public String readClientToken(SaRequest request) { // 优先从请求参数中获取,可以读取到的话直接返回 String clientToken = request.getParam(Param.client_token); if(SaFoxUtil.isNotEmpty(clientToken)) { return clientToken; } // 如果请求参数中没有提供 client_token 参数,则尝试从 Authorization 中获取 String authorizationValue = request.getHeader(Param.Authorization); if(SaFoxUtil.isEmpty(authorizationValue)) { return null; } // 判断前缀,裁剪 String prefix = TokenType.Bearer + " "; if(authorizationValue.startsWith(prefix)) { return authorizationValue.substring(prefix.length()); } // 前缀不符合,返回 null return null; } /** * 数据读取:从请求对象中构建 RequestAuthModel */ @Override public RequestAuthModel readRequestAuthModel(SaRequest req, Object loginId) { RequestAuthModel ra = new RequestAuthModel(); ra.clientId = req.getParamNotNull(Param.client_id); ra.responseType = req.getParamNotNull(Param.response_type); ra.redirectUri = req.getParamNotNull(Param.redirect_uri); ra.state = req.getParam(Param.state); ra.nonce = req.getParam(Param.nonce); ra.scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope)); ra.loginId = loginId; return ra; } /** * 构建返回值: 获取 token */ @Override public Map buildAccessTokenReturnValue(AccessTokenModel at) { Map map = new LinkedHashMap<>(); map.put("token_type", at.tokenType); map.put("access_token", at.accessToken); map.put("refresh_token", at.refreshToken); map.put("expires_in", at.getExpiresIn()); map.put("refresh_expires_in", at.getRefreshExpiresIn()); map.put("client_id", at.clientId); map.put("scope", SaOAuth2Manager.getDataConverter().convertScopeListToString(at.scopes)); map.putAll(at.extraData); SaResult result = SaResult.ok().setMap(map); if(SaOAuth2Manager.getServerConfig().hideStatusField) { result.removeDefaultFields(); } return result; } /** * 构建返回值: 凭证式 模式认证 获取 token */ @Override public Map buildClientTokenReturnValue(ClientTokenModel ct) { Map map = new LinkedHashMap<>(); map.put("token_type", ct.tokenType); map.put("client_token", ct.clientToken); if(SaOAuth2Manager.getServerConfig().getMode4ReturnAccessToken()) { map.put("access_token", ct.clientToken); } map.put("expires_in", ct.getExpiresIn()); map.put("client_id", ct.clientId); map.put("scope", SaOAuth2Manager.getDataConverter().convertScopeListToString(ct.scopes)); map.putAll(ct.extraData); SaResult result = SaResult.ok().setMap(map); if(SaOAuth2Manager.getServerConfig().hideStatusField) { result.removeDefaultFields(); } return result; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.error; /** * 定义 sa-token-oauth2 所有异常细分状态码 * * @author click33 * @since 1.33.0 */ public interface SaOAuth2ErrorCode { /** client_id 不可为空 */ int CODE_30101 = 30101; /** scope 不可为空 */ int CODE_30102 = 30102; /** redirect_uri 不可为空 */ int CODE_30103 = 30103; /** LoginId 不可为空 */ int CODE_30104 = 30104; /** 无效 client_id */ int CODE_30105 = 30105; /** 无效 access_token */ int CODE_30106 = 30106; /** 无效 client_token */ int CODE_30107 = 30107; /** Access-Token 不具备指定的 Scope */ int CODE_30108 = 30108; /** Client-Token 不具备指定的 Scope */ int CODE_30109 = 30109; /** 无效 Code 码 */ int CODE_30110 = 30110; /** 无效 Refresh-Token */ int CODE_30111 = 30111; /** 请求的 Scope 暂未签约 */ int CODE_30112 = 30112; /** 无效 redirect_url */ int CODE_30113 = 30113; /** 非法 redirect_url */ int CODE_30114 = 30114; /** 无效client_secret */ int CODE_30115 = 30115; /** redirect_uri 不一致 */ int CODE_30120 = 30120; /** client_id 不一致 */ int CODE_30122 = 30122; /** 无效 response_type */ int CODE_30125 = 30125; /** 无效 grant_type */ int CODE_30126 = 30126; /** 无效 state */ int CODE_30127 = 30127; /** 暂未开放授权码模式 */ int CODE_30131 = 30131; /** 暂未开放隐藏式模式 */ int CODE_30132 = 30132; /** 暂未开放密码式模式 */ int CODE_30133 = 30133; /** 暂未开放凭证式模式 */ int CODE_30134 = 30134; /** 系统暂未开放的授权模式 */ int CODE_30141 = 30141; /** 应用暂未开放的授权模式 */ int CODE_30142 = 30142; /** 无效的请求 Method */ int CODE_30151 = 30151; /** Password 模式认证失败 */ int CODE_30161 = 30161; /** 其它异常 */ int CODE_30191 = 30191; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 Access-Token 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2AccessTokenException extends SaOAuth2Exception { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 Access-Token 相关错误 * @param cause 根异常原因 */ public SaOAuth2AccessTokenException(Throwable cause) { super(cause); } /** * 一个异常:代表 Access-Token 相关错误 * @param message 异常描述 */ public SaOAuth2AccessTokenException(String message) { super(message); } /** * 具体引起异常的 Access-Token 值 */ public String accessToken; public String getAccessToken() { return accessToken; } public SaOAuth2AccessTokenException setAccessToken(String accessToken) { this.accessToken = accessToken; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2AccessTokenException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 Access-Token Scope 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2AccessTokenScopeException extends SaOAuth2AccessTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 Access-Token Scope 相关错误 * @param cause 根异常原因 */ public SaOAuth2AccessTokenScopeException(Throwable cause) { super(cause); } /** * 一个异常:代表 Access-Token Scope 相关错误 * @param message 异常描述 */ public SaOAuth2AccessTokenScopeException(String message) { super(message); } /** * 具体引起异常的 Access-Token 值 */ public String accessToken; /** * 具体引起异常的 scope 值 */ public String scope; public String getAccessToken() { return accessToken; } public SaOAuth2AccessTokenScopeException setAccessToken(String accessToken) { this.accessToken = accessToken; return this; } public String getScope() { return scope; } public SaOAuth2AccessTokenScopeException setScope(String scope) { this.scope = scope; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2AccessTokenScopeException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AuthorizationCodeException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 Code 授权码相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2AuthorizationCodeException extends SaOAuth2Exception { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 Access-Token 相关错误 * @param cause 根异常原因 */ public SaOAuth2AuthorizationCodeException(Throwable cause) { super(cause); } /** * 一个异常:代表 Access-Token 相关错误 * @param message 异常描述 */ public SaOAuth2AuthorizationCodeException(String message) { super(message); } /** * 具体引起异常的 code 值 */ public String authorizationCode; public String getAuthorizationCode() { return authorizationCode; } public SaOAuth2AuthorizationCodeException setAuthorizationCode(String authorizationCode) { this.authorizationCode = authorizationCode; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param authorizationCode 引入异常的 code 值 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, String authorizationCode, int code) { if(flag) { throw new SaOAuth2AuthorizationCodeException(message).setAuthorizationCode(authorizationCode).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 ClientModel 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2ClientModelException extends SaOAuth2Exception { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 ClientModel 相关错误 * @param cause 根异常原因 */ public SaOAuth2ClientModelException(Throwable cause) { super(cause); } /** * 一个异常:代表 ClientModel 相关错误 * @param message 异常描述 */ public SaOAuth2ClientModelException(String message) { super(message); } /** * 具体引起异常的 ClientId 值 */ public String clientId; public String getClientId() { return clientId; } public SaOAuth2ClientModelException setClientId(String clientId) { this.clientId = clientId; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2ClientModelException(message).setCode(code); } } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param clientId 应用id * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, String clientId, int code) { if(flag) { throw new SaOAuth2ClientModelException(message).setClientId(clientId).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 ClientModel Scope 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2ClientModelScopeException extends SaOAuth2ClientModelException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 ClientModel Scope 相关错误 * @param cause 根异常原因 */ public SaOAuth2ClientModelScopeException(Throwable cause) { super(cause); } /** * 一个异常:代表 ClientModel Scope 相关错误 * @param message 异常描述 */ public SaOAuth2ClientModelScopeException(String message) { super(message); } /** * 具体引起异常的 ClientId 值 */ public String clientId; /** * 具体引起异常的 scope 值 */ public String scope; public String getClientId() { return clientId; } public SaOAuth2ClientModelScopeException setClientId(String clientId) { this.clientId = clientId; return this; } public String getScope() { return scope; } public SaOAuth2ClientModelScopeException setScope(String scope) { this.scope = scope; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2ClientModelScopeException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 Client-Token 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2ClientTokenException extends SaOAuth2Exception { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 Client-Token 相关错误 * @param cause 根异常原因 */ public SaOAuth2ClientTokenException(Throwable cause) { super(cause); } /** * 一个异常:代表 Client-Token 相关错误 * @param message 异常描述 */ public SaOAuth2ClientTokenException(String message) { super(message); } /** * 具体引起异常的 Client-Token 值 */ public String clientToken; public String getClientToken() { return clientToken; } public SaOAuth2ClientTokenException setClientToken(String clientToken) { this.clientToken = clientToken; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2ClientTokenException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 Client-Token Scope 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2ClientTokenScopeException extends SaOAuth2ClientTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 Client-Token Scope 相关错误 * @param cause 根异常原因 */ public SaOAuth2ClientTokenScopeException(Throwable cause) { super(cause); } /** * 一个异常:代表 Client-Token Scope 相关错误 * @param message 异常描述 */ public SaOAuth2ClientTokenScopeException(String message) { super(message); } /** * 具体引起异常的 Client-Token 值 */ public String clientToken; /** * 具体引起异常的 scope 值 */ public String scope; public String getClientToken() { return clientToken; } public SaOAuth2ClientTokenScopeException setClientToken(String clientToken) { this.clientToken = clientToken; return this; } public String getScope() { return scope; } public SaOAuth2ClientTokenScopeException setScope(String scope) { this.scope = scope; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2ClientTokenScopeException(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; import cn.dev33.satoken.exception.SaTokenException; /** * 一个异常:代表 OAuth2 认证流程错误 * * @author click33 * @since 1.33.0 */ public class SaOAuth2Exception extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 OAuth2 认证流程错误 * @param cause 根异常原因 */ public SaOAuth2Exception(Throwable cause) { super(cause); } /** * 一个异常:代表 OAuth2 认证流程错误 * @param message 异常描述 */ public SaOAuth2Exception(String message) { super(message); } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2Exception(message).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.exception; /** * 一个异常:代表 Refresh-Token 相关错误 * * @author click33 * @since 1.39.0 */ public class SaOAuth2RefreshTokenException extends SaOAuth2Exception { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 Refresh-Token 相关错误 * @param cause 根异常原因 */ public SaOAuth2RefreshTokenException(Throwable cause) { super(cause); } /** * 一个异常:代表 Refresh-Token 相关错误 * @param message 异常描述 */ public SaOAuth2RefreshTokenException(String message) { super(message); } /** * 具体引起异常的 Refresh-Token 值 */ public String refreshToken; public String getRefreshToken() { return refreshToken; } public SaOAuth2RefreshTokenException setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; return this; } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, int code) { if(flag) { throw new SaOAuth2RefreshTokenException(message).setCode(code); } } /** * 如果 flag==true,则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param refreshToken refreshToken * @param code 异常细分码 */ public static void throwBy(boolean flag, String message, String refreshToken, int code) { if(flag) { throw new SaOAuth2RefreshTokenException(message).setRefreshToken(refreshToken).setCode(code); } } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2ConfirmViewFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function; import java.util.List; import java.util.function.BiFunction; /** * 函数式接口:OAuth-Server端 确认授权时返回的View * *

    参数:无

    *

    返回:view 视图

    * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2ConfirmViewFunction extends BiFunction, Object> { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2DoLoginHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function; import java.util.function.BiFunction; /** * 函数式接口:登录函数 * *

    参数:name, pwd

    *

    返回:认证返回结果

    * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2DoLoginHandleFunction extends BiFunction { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2NotLoginViewFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function; import java.util.function.Supplier; /** * 函数式接口:OAuth-Server端 未登录时返回的View * *

    参数:clientId, scope

    *

    返回:view 视图

    * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2NotLoginViewFunction extends Supplier { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateAccessTokenValueFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import java.util.List; /** * 函数式接口:创建一个 AccessToken value * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2CreateAccessTokenValueFunction { /** * 创建一个 AccessToken value * @param clientId 应用id * @param loginId 账号id * @param scopes 权限 * @return AccessToken value */ String execute(String clientId, Object loginId, List scopes); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateClientTokenValueFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import java.util.List; /** * 函数式接口:创建一个 ClientToken value * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2CreateClientTokenValueFunction { /** * 创建一个 ClientToken value * @param clientId 应用id * @param scopes 权限 * @return ClientToken value */ String execute(String clientId, List scopes); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateCodeValueFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import java.util.List; /** * 函数式接口:创建一个 code value * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2CreateCodeValueFunction { /** * 创建一个 code value * @param clientId 应用id * @param loginId 账号id * @param scopes 权限 * @return code value */ String execute(String clientId, Object loginId, List scopes); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateRefreshTokenValueFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import java.util.List; /** * 函数式接口:创建一个 RefreshToken value * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2CreateRefreshTokenValueFunction { /** * 创建一个 RefreshToken value * @param clientId 应用id * @param loginId 账号id * @param scopes 权限 * @return RefreshToken value */ String execute(String clientId, Object loginId, List scopes); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2GrantTypeAuthFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import java.util.function.Function; /** * 函数式接口:GrantType 认证 * *

    参数:SaRequest、grant_type

    *

    返回:处理结果

    * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2GrantTypeAuthFunction extends Function { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkAccessTokenFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import java.util.function.Consumer; /** * 函数式接口:AccessTokenModel 加工 * *

    参数:AccessTokenModel

    *

    返回:无

    * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2ScopeWorkAccessTokenFunction extends Consumer { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkClientTokenFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.function.strategy; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import java.util.function.Consumer; /** * 函数式接口:ClientTokenModel 加工 * *

    参数:ClientTokenModel

    *

    返回:无

    * * @author click33 * @since 1.39.0 */ @FunctionalInterface public interface SaOAuth2ScopeWorkClientTokenFunction extends Consumer { } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/AuthorizationCodeGrantTypeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.granttype.handler; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; import java.util.List; /** * authorization_code grant_type 处理器 * * @author click33 * @since 1.39.0 */ public class AuthorizationCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { @Override public String getHandlerGrantType() { return GrantType.authorization_code; } @Override public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); String clientSecret = clientIdAndSecret.clientSecret; String code = req.getParamNotNull(SaOAuth2Consts.Param.code); String redirectUri = req.getParam(SaOAuth2Consts.Param.redirect_uri); // 校验参数 SaOAuth2Manager.getTemplate().checkGainTokenParam(code, clientId, clientSecret, redirectUri); // 构建 Access-Token、返回 return SaOAuth2Manager.getDataGenerate().generateAccessToken(code); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.granttype.handler; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.granttype.handler.model.PasswordAuthResult; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.stp.StpUtil; import java.util.List; /** * password grant_type 处理器 * * @author click33 * @since 1.39.0 */ public class PasswordGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { @Override public String getHandlerGrantType() { return GrantType.password; } @Override public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { // 1、获取请求参数 String username = req.getParamNotNull(SaOAuth2Consts.Param.username); String password = req.getParamNotNull(SaOAuth2Consts.Param.password); // 2、调用API 开始登录,如果没能成功登录,则直接退出 PasswordAuthResult passwordAuthResult = loginByUsernamePassword(username, password); Object loginId = passwordAuthResult.getLoginId(); if(loginId == null) { throw new SaOAuth2Exception("登录失败").setCode(SaOAuth2ErrorCode.CODE_30161); } // 3、构建 ra 对象 RequestAuthModel ra = new RequestAuthModel(); ra.clientId = clientId; ra.loginId = loginId; ra.scopes = scopes; // 4、生成 Access-Token AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = GrantType.password); return at; } /** * 根据 username、password 进行登录,如果登录失败请直接抛出异常或返回 loginId = null * @param username / * @param password / */ public PasswordAuthResult loginByUsernamePassword(String username, String password) { System.err.println("警告信息:当前 password 认证模式,使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle),仅供开发测试"); System.err.println("正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法"); SaOAuth2Strategy.instance.doLoginHandle.apply(username, password); Object loginId = StpUtil.getLoginIdDefaultNull(); return new PasswordAuthResult(loginId); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/RefreshTokenGrantTypeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.granttype.handler; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException; import cn.dev33.satoken.oauth2.exception.SaOAuth2RefreshTokenException; import java.util.List; /** * refresh_token grant_type 处理器 * * @author click33 * @since 1.39.0 */ public class RefreshTokenGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { @Override public String getHandlerGrantType() { return GrantType.refresh_token; } @Override public AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes) { // 获取参数 String refreshToken = req.getParamNotNull(SaOAuth2Consts.Param.refresh_token); // 校验:Refresh-Token 是否存在 RefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken); SaOAuth2RefreshTokenException.throwBy(rt == null, "无效refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111); // 校验:Refresh-Token 代表的 ClientId 与提供的 ClientId 是否一致 SaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), "无效client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); // 获取新 Access-Token AccessTokenModel accessTokenModel = SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken); return accessTokenModel; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/SaOAuth2GrantTypeHandlerInterface.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.granttype.handler; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import java.util.List; /** * 所有 OAuth2 GrantType 处理器的父接口,如果要自定义 GrantType 处理器,必须实现此接口 * * @author click33 * @since 1.39.0 */ public interface SaOAuth2GrantTypeHandlerInterface { /** * 获取所要处理的 GrantType * * @return / */ String getHandlerGrantType(); /** * 获取 AccessTokenModel 对象 * * @param req / * @return / */ AccessTokenModel getAccessToken(SaRequest req, String clientId, List scopes); } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/model/PasswordAuthResult.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.granttype.handler.model; import java.io.Serializable; /** * Model: Password Grant_Type 认证结果 * * @author click33 * @since 1.43.0 */ public class PasswordAuthResult implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 对应账号id */ public Object loginId; /** * 构建一个 */ public PasswordAuthResult() { } /** * 构建一个 * @param loginId 对应的账号id */ public PasswordAuthResult(Object loginId) { this(); this.loginId = loginId; } /** * 获取 对应账号id * @return / */ public Object getLoginId() { return loginId; } /** * 设置 对应账号id * @param loginId 对应账号id * @return 对象自身 */ public PasswordAuthResult setLoginId(Object loginId) { this.loginId = loginId; return this; } @Override public String toString() { return "PasswordAuthResult{" + ", loginId=" + loginId + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.processor; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Api; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Param; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.ResponseType; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.util.SaResult; import java.util.List; /** * Sa-Token OAuth2 请求处理器 * * @author click33 * @since 1.23.0 */ public class SaOAuth2ServerProcessor { /** * 全局默认实例 */ public static SaOAuth2ServerProcessor instance = new SaOAuth2ServerProcessor(); /** * 处理 Server 端请求, 路由分发 * @return 处理结果 */ public Object dister() { // 获取变量 SaRequest req = SaHolder.getRequest(); // ------------------ 路由分发 ------------------ // 模式一:Code授权码 || 模式二:隐藏式 if(req.isPath(Api.authorize)) { return authorize(); } // Code 换 Access-Token || 模式三:密码式 if(req.isPath(Api.token)) { return token(); } // Refresh-Token 刷新 Access-Token if(req.isPath(Api.refresh)) { return refresh(); } // 回收 Access-Token if(req.isPath(Api.revoke)) { return revoke(); } // doLogin 登录接口 if(req.isPath(Api.doLogin)) { return doLogin(); } // doConfirm 确认授权接口 if(req.isPath(Api.doConfirm)) { return doConfirm(); } // 模式四:凭证式 if(req.isPath(Api.client_token)) { return clientToken(); } // 默认返回 return SaOAuth2Consts.NOT_HANDLE; } /** * 模式一:Code授权码 / 模式二:隐藏式 * @return 处理结果 */ public Object authorize() { // 获取变量 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); String responseType = req.getParamNotNull(Param.response_type); // 1、先判断是否开启了指定的授权模式 checkAuthorizeResponseType(responseType, req, cfg); // 2、如果尚未登录, 则先去登录 Object loginId = SaOAuth2Manager.getStpLogic().getLoginIdDefaultNull(); if( loginId == null) { return SaOAuth2Strategy.instance.notLoginView.get(); } // 3、构建请求 Model RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); // 4、开发者自定义的授权前置检查 SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId); // 5、校验:重定向域名是否合法 oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri); // 6、校验:此次申请的Scope,该Client是否已经签约 oauth2Template.checkContractScope(ra.clientId, ra.scopes); // 7、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面 boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes); if(isNeedCarefulConfirm) { SaClientModel cm = oauth2Template.checkClientModel(ra.clientId); if( ! cm.getIsAutoConfirm()) { return SaOAuth2Strategy.instance.confirmView.apply(ra.clientId, ra.scopes); } } // 8、判断授权类型,重定向到不同地址 // 如果是 授权码式,则:开始重定向授权,下放code if(ResponseType.code.equals(ra.responseType)) { CodeModel codeModel = dataGenerate.generateCode(ra); String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); return res.redirect(redirectUri); } // 如果是 隐藏式,则:开始重定向授权,下放 token if(ResponseType.token.equals(ra.responseType)) { AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); return res.redirect(redirectUri); } // 默认返回 throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); } /** * Code 换 Access-Token / 模式三:密码式 / 自定义 grant_type * @return 处理结果 */ public Object token() { AccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(SaHolder.getRequest()); return SaOAuth2Manager.getDataResolver().buildAccessTokenReturnValue(accessTokenModel); } /** * Refresh-Token 刷新 Access-Token * @return 处理结果 */ public Object refresh() { SaRequest req = SaHolder.getRequest(); // 校验 grant_type 必须为 refresh_token String grantType = req.getParamNotNull(Param.grant_type); SaOAuth2Exception.throwBy(!grantType.equals(GrantType.refresh_token), "无效 grant_type:" + grantType, SaOAuth2ErrorCode.CODE_30126); // 刷新 Access-Token AccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(req); return SaOAuth2Manager.getDataResolver().buildRefreshTokenReturnValue(accessTokenModel); } /** * 回收 Access-Token * @return 处理结果 */ public Object revoke() { // 获取变量 SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); SaRequest req = SaHolder.getRequest(); // 获取参数 ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); String clientId = clientIdAndSecret.clientId; String clientSecret = clientIdAndSecret.clientSecret; String accessToken = req.getParamNotNull(Param.access_token); // 如果 Access-Token 不存在,直接返回 if(oauth2Template.getAccessToken(accessToken) == null) { return SaResult.ok("access_token 不存在:" + accessToken); } // 校验参数 oauth2Template.checkAccessTokenParam(clientId, clientSecret, accessToken); // 回收 Access-Token oauth2Template.revokeAccessToken(accessToken); // 返回 return SaOAuth2Manager.getDataResolver().buildRevokeTokenReturnValue(); } /** * doLogin 登录接口 * @return 处理结果 */ public Object doLogin() { SaRequest req = SaHolder.getRequest(); return SaOAuth2Strategy.instance.doLoginHandle.apply(req.getParam(Param.name), req.getParam(Param.pwd)); } /** * doConfirm 确认授权接口 * @return 处理结果 */ public Object doConfirm() { // 获取变量 SaRequest req = SaHolder.getRequest(); String clientId = req.getParamNotNull(Param.client_id); Object loginId = SaOAuth2Manager.getStpLogic().getLoginId(); String scope = req.getParamNotNull(Param.scope); List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(scope); SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); // 此请求只允许 POST 方式 if(!req.isMethod(SaHttpMethod.POST)) { throw new SaOAuth2Exception("无效请求方式:" + req.getMethod()).setCode(SaOAuth2ErrorCode.CODE_30151); } // 确认授权 oauth2Template.saveGrantScope(clientId, loginId, scopes); // 判断所需的返回结果模式 boolean buildRedirectUri = req.isParam(Param.build_redirect_uri, "true"); // -------- 情况1:只返回确认结果即可 if( ! buildRedirectUri ) { oauth2Template.saveGrantScope(clientId, loginId, scopes); return SaResult.ok(); } // -------- 情况2:需要返回最终的 redirect_uri 地址 // 构建请求 Model RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId); // 判断授权类型,构建不同的重定向地址 // 如果是 授权码式,则:开始重定向授权,下放code if(ResponseType.code.equals(ra.responseType)) { CodeModel codeModel = dataGenerate.generateCode(ra); String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state); return SaResult.ok().set(Param.redirect_uri, redirectUri); } // 如果是 隐藏式,则:开始重定向授权,下放 token if(ResponseType.token.equals(ra.responseType)) { AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null); String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state); return SaResult.ok().set(Param.redirect_uri, redirectUri); } // 默认返回 throw new SaOAuth2Exception("无效response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125); } /** * 模式四:凭证式 * @return 处理结果 */ public Object clientToken() { // 获取变量 SaRequest req = SaHolder.getRequest(); SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); String grantType = req.getParamNotNull(Param.grant_type); if(!grantType.equals(GrantType.client_credentials)) { throw new SaOAuth2Exception("无效 grant_type:" + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); } if(!cfg.enableClientCredentials) { throwErrorSystemNotEnableModel(); } if(!currClientModel().getAllowGrantTypes().contains(GrantType.client_credentials)) { throwErrorClientNotEnableModel(); } // 获取参数 ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); String clientId = clientIdAndSecret.clientId; String clientSecret = clientIdAndSecret.clientSecret; List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope)); // 校验 ClientScope oauth2Template.checkContractScope(clientId, scopes); // 校验 ClientSecret oauth2Template.checkClientSecret(clientId, clientSecret); // 生成 ClientTokenModel ct = SaOAuth2Manager.getDataGenerate().generateClientToken(clientId, scopes); // 返回 return SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct); } // ----------- 代码块封装 -------------- /** * 根据当前请求提交的 client_id 参数获取 SaClientModel 对象 * @return / */ public SaClientModel currClientModel() { SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(SaHolder.getRequest()); return oauth2Template.checkClientModel(clientIdAndSecret.clientId); } /** * 校验当前请求中提交的 clientId 和 clientSecret 是否正确,如果正确则返回 SaClientModel 对象 * * @return / */ public SaClientModel checkCurrClientSecret() { SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(SaHolder.getRequest()); return oauth2Template.checkClientSecret(clientIdAndSecret.clientId, clientIdAndSecret.clientSecret); } /** * 校验 authorize 路由的 ResponseType 参数 */ public void checkAuthorizeResponseType(String responseType, SaRequest req, SaOAuth2ServerConfig cfg) { // 模式一:Code授权码 if(responseType.equals(ResponseType.code)) { if(!cfg.enableAuthorizationCode) { throwErrorSystemNotEnableModel(); } if(!currClientModel().getAllowGrantTypes().contains(GrantType.authorization_code)) { throwErrorClientNotEnableModel(); } } // 模式二:隐藏式 else if(responseType.equals(ResponseType.token)) { if(!cfg.enableImplicit) { throwErrorSystemNotEnableModel(); } if(!currClientModel().getAllowGrantTypes().contains(GrantType.implicit)) { throwErrorClientNotEnableModel(); } } // 其它 else { throw new SaOAuth2Exception("无效 response_type: " + responseType).setCode(SaOAuth2ErrorCode.CODE_30125); } } /** * 系统未开放此授权模式时抛出异常 */ public void throwErrorSystemNotEnableModel() { throw new SaOAuth2Exception("系统暂未开放此授权模式").setCode(SaOAuth2ErrorCode.CODE_30141); } /** * 应用未开放此授权模式时抛出异常 */ public void throwErrorClientNotEnableModel() { throw new SaOAuth2Exception("应用暂未开放此授权模式").setCode(SaOAuth2ErrorCode.CODE_30142); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/CommonScope.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.scope; /** * OAuth2 常见 Scope 定义 * * @author click33 * @since 1.39.0 */ public final class CommonScope { private CommonScope() { } /** * 获取 openid */ public static final String OPENID = "openid"; /** * 获取 unionid */ public static final String UNIONID = "unionid"; /** * 获取 userid */ public static final String USERID = "userid"; /** * 获取 id_token */ public static final String OIDC = "oidc"; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.scope.handler; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.jwt.SaJwtUtil; import cn.dev33.satoken.jwt.error.SaJwtErrorCode; import cn.dev33.satoken.jwt.exception.SaJwtException; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel; import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.scope.CommonScope; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.util.SaFoxUtil; import java.net.MalformedURLException; import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; /** * id_token 权限处理器:在 AccessToken 扩展参数中追加 id_token 字段 * * @author click33 * @since 1.39.0 */ public class OidcScopeHandler implements SaOAuth2ScopeHandlerInterface { public String getHandlerScope() { return CommonScope.OIDC; } @Override public void workAccessToken(AccessTokenModel at) { SaRequest req = SaHolder.getRequest(); ClientIdAndSecretModel client = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); // 基础参数 IdTokenModel idToken = new IdTokenModel(); idToken.iss = getIss(); idToken.sub = at.loginId; idToken.aud = client.clientId; idToken.iat = System.currentTimeMillis() / 1000; idToken.exp = idToken.iat + SaOAuth2Manager.getServerConfig().getOidc().getIdTokenTimeout(); idToken.authTime = SaOAuth2Manager.getStpLogic().getSessionByLoginId(at.loginId).getCreateTime() / 1000; idToken.nonce = getNonce(); idToken.acr = null; idToken.amr = null; idToken.azp = client.clientId; // 额外参数 idToken.extraData = new LinkedHashMap<>(); idToken = workExtraData(idToken); // 构建 jwtIdToken String jwtIdToken = generateJwtIdToken(idToken); // 放入 AccessTokenModel at.extraData.put("id_token", jwtIdToken); } @Override public void workClientToken(ClientTokenModel ct) { } @Override public boolean refreshAccessTokenIsWork() { return true; } /** * 获取 iss * @return / */ public String getIss() { // 如果开发者配置了 iss,则使用开发者配置的 iss String cfgIss = SaOAuth2Manager.getServerConfig().getOidc().getIss(); if(SaFoxUtil.isNotEmpty(cfgIss)) { return cfgIss; } // 否则根据请求的 url 计算 iss // 例如请求 url 为: http://localhost:8081/abc/xyz?name=张三 // 则计算的 iss 为: http://localhost:8081 String urlString = SaHolder.getRequest().getUrl(); try { URL url = new URL(urlString); String iss = url.getProtocol() + "://" + url.getHost(); if(url.getPort() != -1) { iss += ":" + url.getPort(); } return iss; } catch (MalformedURLException e) { throw new SaOAuth2Exception(e); } } /** * 获取 nonce * @return / */ public String getNonce() { String nonce = SaHolder.getRequest().getParam(SaOAuth2Consts.Param.nonce); if(SaFoxUtil.isEmpty(nonce)) { // 通过 code 查找nonce // 为了避免其它 handler 可能会用到 nonce, 任由其自然过期,只取用不删除 nonce = SaOAuth2Manager.getDao().getNonce(SaHolder.getRequest().getParam(SaOAuth2Consts.Param.code)); } if(SaFoxUtil.isEmpty(nonce)) { nonce = SaFoxUtil.getRandomString(32); } SaSignManager.getSaSignTemplate().checkNonce(nonce); return nonce; } /** * 加工 IdTokenModel * @return / */ public IdTokenModel workExtraData(IdTokenModel idToken) { // 留给开发者扩展 return idToken; } /** * 将 IdTokenModel 转化为 Map 数据 * @return / */ public Map convertIdTokenToMap(IdTokenModel idToken) { // 基础参数 Map map = new LinkedHashMap<>(); map.put("iss", idToken.iss); map.put("sub", idToken.sub); map.put("aud", idToken.aud); map.put("exp", idToken.exp); map.put("iat", idToken.iat); map.put("auth_time", idToken.authTime); map.put("nonce", idToken.nonce); map.put("acr", idToken.acr); map.put("amr", idToken.amr); map.put("azp", idToken.azp); // 移除 null 值 idToken.extraData.entrySet().removeIf(entry -> entry.getValue() == null); // 扩展参数 map.putAll(idToken.extraData); // 返回 return map; } /** * 生成 jwt 格式的 id_token * @param idToken / * @return / */ public String generateJwtIdToken(IdTokenModel idToken) { Map dataMap = convertIdTokenToMap(idToken); String keyt = SaOAuth2Manager.getStpLogic().getConfigOrGlobal().getJwtSecretKey(); SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); return SaJwtUtil.createToken(dataMap, keyt); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OpenIdScopeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.scope.handler; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.scope.CommonScope; /** * OpenId 权限处理器:在 AccessToken 扩展参数中追加 openid 字段 * * @author click33 * @since 1.39.0 */ public class OpenIdScopeHandler implements SaOAuth2ScopeHandlerInterface { public String getHandlerScope() { return CommonScope.OPENID; } @Override public void workAccessToken(AccessTokenModel at) { at.extraData.put(SaOAuth2Consts.ExtraField.openid, SaOAuth2Manager.getDataLoader().getOpenid(at.clientId, at.loginId)); } @Override public void workClientToken(ClientTokenModel ct) { } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/SaOAuth2ScopeHandlerInterface.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.scope.handler; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; /** * 所有 OAuth2 权限处理器的父接口,如果要自定义 Scope 处理器,必须实现此接口 * * @author click33 * @since 1.39.0 */ public interface SaOAuth2ScopeHandlerInterface { /** * 获取所要处理的权限 * * @return / */ String getHandlerScope(); /** * 当构建的 AccessToken 具有此权限时,所需要执行的方法 * * @param at / */ void workAccessToken(AccessTokenModel at); /** * 当构建的 ClientToken 具有此权限时,所需要执行的方法 * * @param ct / */ void workClientToken(ClientTokenModel ct); /** * 当使用 RefreshToken 刷新 AccessToken 时,是否重新执行 workAccessToken 构建方法 * * @return / */ default boolean refreshAccessTokenIsWork() { return false; } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UnionIdScopeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.scope.handler; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.scope.CommonScope; /** * UnionId Scope 处理器,在返回的 AccessToken 中增加 unionid 字段 * * @author click33 * @since 1.40.0 */ public class UnionIdScopeHandler implements SaOAuth2ScopeHandlerInterface { @Override public String getHandlerScope() { return CommonScope.UNIONID; } @Override public void workAccessToken(AccessTokenModel at) { SaOAuth2DataLoader dataLoader = SaOAuth2Manager.getDataLoader(); SaClientModel cm = dataLoader.getClientModelNotNull(at.clientId); at.extraData.put(SaOAuth2Consts.ExtraField.unionid, dataLoader.getUnionid(cm.subjectId, at.loginId)); } @Override public void workClientToken(ClientTokenModel ct) { } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UserIdScopeHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.scope.handler; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.scope.CommonScope; /** * UserId 权限处理器:在 AccessToken 扩展参数中追加 userid 字段 * * @author click33 * @since 1.39.0 */ public class UserIdScopeHandler implements SaOAuth2ScopeHandlerInterface { public String getHandlerScope() { return CommonScope.USERID; } @Override public void workAccessToken(AccessTokenModel at) { at.extraData.put(SaOAuth2Consts.ExtraField.userid, at.loginId); } @Override public void workClientToken(ClientTokenModel ct) { } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.strategy; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.fun.SaTwoParamFunction; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.consts.GrantType; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.function.SaOAuth2ConfirmViewFunction; import cn.dev33.satoken.oauth2.function.SaOAuth2DoLoginHandleFunction; import cn.dev33.satoken.oauth2.function.SaOAuth2NotLoginViewFunction; import cn.dev33.satoken.oauth2.function.strategy.*; import cn.dev33.satoken.oauth2.granttype.handler.AuthorizationCodeGrantTypeHandler; import cn.dev33.satoken.oauth2.granttype.handler.PasswordGrantTypeHandler; import cn.dev33.satoken.oauth2.granttype.handler.RefreshTokenGrantTypeHandler; import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; import cn.dev33.satoken.oauth2.scope.CommonScope; import cn.dev33.satoken.oauth2.scope.handler.*; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Sa-Token OAuth2 相关策略 * * @author click33 * @since 1.39.0 */ public final class SaOAuth2Strategy { private SaOAuth2Strategy() { registerDefaultScopeHandler(); registerDefaultGrantTypeHandler(); } /** * 全局单例引用 */ public static final SaOAuth2Strategy instance = new SaOAuth2Strategy(); // ------------------ 权限处理器 ------------------ /** * Scope 权限处理器集合 */ public Map scopeHandlerMap = new LinkedHashMap<>(); /** * 注册所有默认的 Scope 权限处理器 */ public void registerDefaultScopeHandler() { scopeHandlerMap.put(CommonScope.OPENID, new OpenIdScopeHandler()); scopeHandlerMap.put(CommonScope.UNIONID, new UnionIdScopeHandler()); scopeHandlerMap.put(CommonScope.USERID, new UserIdScopeHandler()); scopeHandlerMap.put(CommonScope.OIDC, new OidcScopeHandler()); } /** * 注册一个权限处理器 */ public void registerScopeHandler(SaOAuth2ScopeHandlerInterface handler) { scopeHandlerMap.put(handler.getHandlerScope(), handler); SaManager.getLog().info("自定义 SCOPE [{}] (处理器: {})", handler.getHandlerScope(), handler.getClass().getCanonicalName()); } /** * 移除一个权限处理器 */ public void removeScopeHandler(String scope) { scopeHandlerMap.remove(scope); } /** * 根据 scope 信息对一个 AccessTokenModel 进行加工处理 */ public SaOAuth2ScopeWorkAccessTokenFunction workAccessTokenByScope = (at) -> { if(at.scopes != null && !at.scopes.isEmpty()) { for (String scope : at.scopes) { SaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope); if(handler != null) { handler.workAccessToken(at); } } } SaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE); if(finallyWorkScopeHandler != null) { finallyWorkScopeHandler.workAccessToken(at); } }; /** * 当使用 RefreshToken 刷新 AccessToken 时,根据 scope 信息对一个 AccessTokenModel 进行加工处理 */ public SaOAuth2ScopeWorkAccessTokenFunction refreshAccessTokenWorkByScope = (at) -> { if(at.scopes != null && !at.scopes.isEmpty()) { for (String scope : at.scopes) { SaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope); if(handler != null && handler.refreshAccessTokenIsWork()) { handler.workAccessToken(at); } } } SaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE); if(finallyWorkScopeHandler != null && finallyWorkScopeHandler.refreshAccessTokenIsWork()) { finallyWorkScopeHandler.workAccessToken(at); } }; /** * 根据 scope 信息对一个 ClientTokenModel 进行加工处理 */ public SaOAuth2ScopeWorkClientTokenFunction workClientTokenByScope = (ct) -> { if(ct.scopes != null && !ct.scopes.isEmpty()) { for (String scope : ct.scopes) { SaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope); if(handler != null) { handler.workClientToken(ct); } } } SaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE); if(finallyWorkScopeHandler != null) { finallyWorkScopeHandler.workClientToken(ct); } }; // ------------------ grant_type 处理器 ------------------ /** * grant_type 处理器集合 */ public Map grantTypeHandlerMap = new LinkedHashMap<>(); /** * 注册所有默认的 grant_type 处理器 */ public void registerDefaultGrantTypeHandler() { grantTypeHandlerMap.put(GrantType.authorization_code, new AuthorizationCodeGrantTypeHandler()); grantTypeHandlerMap.put(GrantType.password, new PasswordGrantTypeHandler()); grantTypeHandlerMap.put(GrantType.refresh_token, new RefreshTokenGrantTypeHandler()); } /** * 注册一个 grant_type 处理器 */ public void registerGrantTypeHandler(SaOAuth2GrantTypeHandlerInterface handler) { grantTypeHandlerMap.put(handler.getHandlerGrantType(), handler); SaManager.getLog().info("自定义 GRANT_TYPE [{}] (处理器: {})", handler.getHandlerGrantType(), handler.getClass().getCanonicalName()); } /** * 移除一个 grant_type 处理器 */ public void removeGrantTypeHandler(String scope) { grantTypeHandlerMap.remove(scope); } /** * 根据 grantType 构造一个 AccessTokenModel */ public SaOAuth2GrantTypeAuthFunction grantTypeAuth = (req) -> { // 先校验提供的 grant_type 是否有效 String grantType = req.getParamNotNull(SaOAuth2Consts.Param.grant_type); SaOAuth2GrantTypeHandlerInterface grantTypeHandler = grantTypeHandlerMap.get(grantType); if(grantTypeHandler == null) { throw new SaOAuth2Exception("无效 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); } // 针对 authorization_code 与 password 两种特殊 grant_type,需要判断全局是否开启 SaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig(); if(grantType.equals(GrantType.authorization_code) && !config.getEnableAuthorizationCode() ) { throw new SaOAuth2Exception("系统未开放的 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); } if(grantType.equals(GrantType.password) && !config.getEnablePassword() ) { throw new SaOAuth2Exception("系统未开放的 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30126); } // 校验 clientSecret 和 scope ClientIdAndSecretModel clientIdAndSecretModel = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); List scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(SaOAuth2Consts.Param.scope)); SaClientModel clientModel = SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientIdAndSecretModel.getClientId(), clientIdAndSecretModel.getClientSecret(), scopes); // 检测应用是否开启此 grantType if(!clientModel.getAllowGrantTypes().contains(grantType)) { throw new SaOAuth2Exception("应用未开放的 grant_type: " + grantType).setCode(SaOAuth2ErrorCode.CODE_30141); } // 调用 处理器构建 Access-Token return grantTypeHandler.getAccessToken(req, clientIdAndSecretModel.getClientId(), scopes); }; // ------------------ 凭证创建 ------------------ /** * 创建一个 code value */ public SaOAuth2CreateCodeValueFunction createCodeValue = (clientId, loginId, scopes) -> { return SaFoxUtil.getRandomString(60); }; /** * 创建一个 AccessToken value */ public SaOAuth2CreateAccessTokenValueFunction createAccessToken = (clientId, loginId, scopes) -> { return SaFoxUtil.getRandomString(60); }; /** * 创建一个 RefreshToken value */ public SaOAuth2CreateRefreshTokenValueFunction createRefreshToken = (clientId, loginId, scopes) -> { return SaFoxUtil.getRandomString(60); }; /** * 创建一个 ClientToken value */ public SaOAuth2CreateClientTokenValueFunction createClientToken = (clientId, scopes) -> { return SaFoxUtil.getRandomString(60); }; // ------------------ 认证流程回调 ------------------ /** * OAuth-Server端:未登录时返回的View */ public SaOAuth2NotLoginViewFunction notLoginView = () -> "当前会话在 OAuth-Server 认证中心尚未登录"; /** * OAuth-Server端:确认授权时返回的View */ public SaOAuth2ConfirmViewFunction confirmView = (clientId, scopes) -> "本次操作需要用户授权"; /** * OAuth-Server端:登录函数 */ public SaOAuth2DoLoginHandleFunction doLoginHandle = (name, pwd) -> SaResult.error(); /** * OAuth-Server端:用户在授权指定 client 前的检查,如果检查不通过,请直接抛出异常 */ public SaTwoParamFunction userAuthorizeClientCheck = (loginId, clientId) -> { }; // ------------------ 其它 ------------------ /** * 在创建 SaClientModel 时,设置其默认字段 */ public SaParamFunction setSaClientModelDefaultFields = (clientModel) -> { SaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig(); clientModel.accessTokenTimeout = config.getAccessTokenTimeout(); clientModel.refreshTokenTimeout = config.getRefreshTokenTimeout(); clientModel.clientTokenTimeout = config.getClientTokenTimeout(); clientModel.maxAccessTokenCount = config.getMaxAccessTokenCount(); clientModel.maxRefreshTokenCount = config.getMaxRefreshTokenCount(); clientModel.maxClientTokenCount = config.getMaxClientTokenCount(); clientModel.isNewRefresh = config.getIsNewRefresh(); }; } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.template; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.*; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import java.util.List; /** * Sa-Token-OAuth2 模块 代码实现 * * @author click33 * @since 1.23.0 */ public class SaOAuth2Template { // ----------------- SaClientModel 相关 ----------------- /** * 获取 ClientModel,根据 clientId * * @param clientId / * @return / */ public SaClientModel getClientModel(String clientId) { return SaOAuth2Manager.getDataLoader().getClientModel(clientId); } /** * 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 * @param clientId / * @return / */ public SaClientModel checkClientModel(String clientId) { SaClientModel clientModel = getClientModel(clientId); if(clientModel == null) { throw new SaOAuth2ClientModelException("无效 client_id: " + clientId) .setClientId(clientId) .setCode(SaOAuth2ErrorCode.CODE_30105); } return clientModel; } /** * 校验:clientId 与 clientSecret 是否正确,正确返回 SaClientModel,不正确抛出异常 * @param clientId 应用id * @param clientSecret 秘钥 * @return SaClientModel对象 */ public SaClientModel checkClientSecret(String clientId, String clientSecret) { SaClientModel cm = checkClientModel(clientId); if(cm.clientSecret == null || ! cm.clientSecret.equals(clientSecret)) { throw new SaOAuth2ClientModelException("无效 client_secret: " + clientSecret) .setClientId(clientId) .setCode(SaOAuth2ErrorCode.CODE_30115); } return cm; } /** * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes * @param clientId 应用id * @param clientSecret 秘钥 * @param scopes 权限 * @return SaClientModel对象 */ public SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { SaClientModel cm = checkClientSecret(clientId, clientSecret); checkContractScope(cm, scopes); return cm; } /** * 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false * @param clientId 应用id * @param scopes 权限 * @return / */ public boolean isContractScope(String clientId, List scopes) { try { checkContractScope(clientId, scopes); return true; } catch (SaOAuth2ClientModelException e) { return false; } } /** * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 * @param clientId 应用id * @param scopes 权限列表 * @return / */ public SaClientModel checkContractScope(String clientId, List scopes) { return checkContractScope(checkClientModel(clientId), scopes); } /** * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 * @param cm 应用 * @param scopes 权限列表 * @return / */ public SaClientModel checkContractScope(SaClientModel cm, List scopes) { if(SaFoxUtil.isEmptyList(scopes)) { return cm; } for (String scope : scopes) { if(! cm.contractScopes.contains(scope)) { throw new SaOAuth2ClientModelScopeException("该 client 暂未签约 scope: " + scope) .setClientId(cm.clientId) .setScope(scope) .setCode(SaOAuth2ErrorCode.CODE_30112); } } return cm; } // --------- redirect_uri 相关 /** * 校验:该 Client 使用指定 url 作为回调地址,是否合法 * @param clientId 应用id * @param url 指定url */ public void checkRedirectUri(String clientId, String url) { // 1、是否是一个有效的url if( ! SaFoxUtil.isUrl(url)) { throw new SaOAuth2ClientModelException("无效 redirect_url:" + url) .setClientId(clientId) .setCode(SaOAuth2ErrorCode.CODE_30113); } // 2、截取掉?后面的部分 int qIndex = url.indexOf("?"); if(qIndex != -1) { url = url.substring(0, qIndex); } // 3、不允许出现@字符 if(url.contains("@")) { // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect_url 参数绕过 AllowUrl 列表的校验 // // 举个例子 SaClientModel 配置: // allow-url=http://sa-oauth-client.com* // // 开发者原意是为了允许 sa-oauth-client.com 下的所有地址都可以下放 code // // 但是如果攻击者精心构建一个url: // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com@sa-token.cc // // 那么这个url就会绕过 allow-url 的校验,code 被下发到了第三方服务器地址: // http://sa-token.cc/?code=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul // // 造成了 code 参数劫持 // 所以此处需要禁止在 url 中出现 @ 字符 // // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com/@getInfo // // 但是为了安全起见,这么做还是有必要的 throw new SaOAuth2ClientModelException("无效 redirect_url(不允许出现@字符):" + url) .setClientId(clientId) .setCode(SaOAuth2ErrorCode.CODE_30113); } // 4、是否在[允许地址列表]之中 SaClientModel clientModel = checkClientModel(clientId); checkRedirectUriListNormal(clientModel.allowRedirectUris); if( ! SaStrategy.instance.hasElement.apply(clientModel.allowRedirectUris, url)) { throw new SaOAuth2ClientModelException("非法 redirect_url: " + url) .setClientId(clientId) .setCode(SaOAuth2ErrorCode.CODE_30114); } } /** * 校验配置的 allowRedirectUris 是否合规,如果不合规则抛出异常 * @param redirectUriList 待校验的 allow-url 地址列表 */ public void checkRedirectUriListNormal(List redirectUriList){ checkRedirectUriListNormalStaticMethod(redirectUriList); } /** * 校验配置的 allowRedirectUris 是否合规,如果不合规则抛出异常,静态方法内部实现 * @param redirectUriList 待校验的 allow-url 地址列表 */ public static void checkRedirectUriListNormalStaticMethod(List redirectUriList){ for (String url : redirectUriList) { int index = url.indexOf("*"); // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 if(index != -1 && index != url.length() - 1) { // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 // // 举个例子 SaClientModel 配置: // allow-url=http://*.sa-oauth-client.com/ // // 开发者原意是为了允许 sa-oauth-client.com 下的所有子域名都可以下放 ticket // 例如:http://shop.sa-oauth-client.com/ // // 但是如果攻击者精心构建一个url: // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.cc/a.sa-oauth-client.com/ // // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: // http://sa-token.cc/a.sa-oauth-client.com/?code=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx // // 造成了 ticket 参数劫持 // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) // // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: // http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://shop.sa-oauth2-client.com/ // // 但是为了安全起见,这么做还是有必要的 throw new SaOAuth2Exception("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url) .setCode(SaOAuth2ErrorCode.CODE_30114); } } } // --------- 授权相关 /** * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope * @param loginId 账号id * @param clientId 应用id * @param scopes 权限 * @return 是否已经授权 */ public boolean isGrantScope(Object loginId, String clientId, List scopes) { List grantScopeList = SaOAuth2Manager.getDao().getGrantScope(clientId, loginId); return SaFoxUtil.list1ContainList2AllElement(grantScopeList, scopes); } /** * 判断:指定 loginId 在指定 Client 请求指定 Scope 时,是否需要手动确认授权 * @param loginId 账号id * @param clientId 应用id * @param scopes 权限 * @return 是否已经授权 */ public boolean isNeedCarefulConfirm(Object loginId, String clientId, List scopes) { // 如果请求的权限为空,则不需要确认 if(scopes == null || scopes.isEmpty()) { return false; } // 如果包含高级权限,则必须手动确认授权 List higherScopeList = getHigherScopeList(); if(SaFoxUtil.list1ContainList2AnyElement(scopes, higherScopeList)) { return true; } // 如果包含低级权限,则先将低级权限剔除掉 List lowerScopeList = getLowerScopeList(); scopes = SaFoxUtil.list1RemoveByList2(scopes, lowerScopeList); // 如果剔除后的权限为空,则不需要确认 if(scopes.isEmpty()) { return false; } // 根据近期授权记录,判断是否需要确认 return !isGrantScope(loginId, clientId, scopes); } /** * 删除:指定 loginId 针对指定 Client 的授权信息 * @param loginId 账号id * @param clientId 应用id */ public void deleteGrantScope(Object loginId, String clientId) { SaOAuth2Manager.getDao().deleteGrantScope(clientId, loginId); } // --------- 请求数据校验相关 /** * 校验:使用 code 获取 token 时提供的参数校验 * @param code 授权码 * @param clientId 应用id * @param clientSecret 秘钥 * @param redirectUri 重定向地址 * @return CodeModel对象 */ public CodeModel checkGainTokenParam(String code, String clientId, String clientSecret, String redirectUri) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 校验:Code是否存在 CodeModel cm = dao.getCode(code); SaOAuth2AuthorizationCodeException.throwBy(cm == null, "无效 code: " + code, code, SaOAuth2ErrorCode.CODE_30110); // 校验:ClientId是否一致 SaOAuth2ClientModelException.throwBy( ! cm.clientId.equals(clientId), "无效 client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30105); // 校验:Secret是否正确 String dbSecret = checkClientModel(clientId).clientSecret; SaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效 client_secret: " + clientSecret, clientId, SaOAuth2ErrorCode.CODE_30115); // 如果提供了redirectUri,则校验其是否与请求Code时提供的一致 if( ! SaFoxUtil.isEmpty(redirectUri)) { SaOAuth2ClientModelException.throwBy( ! redirectUri.equals(cm.redirectUri), "无效 redirect_uri: " + redirectUri, clientId, SaOAuth2ErrorCode.CODE_30120); } // 返回CodeModel return cm; } /** * 校验:使用 Refresh-Token 刷新 Access-Token 时提供的参数校验 * @param clientId 应用id * @param clientSecret 秘钥 * @param refreshToken Refresh-Token * @return CodeModel对象 */ public RefreshTokenModel checkRefreshTokenParam(String clientId, String clientSecret, String refreshToken) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); // 校验:Refresh-Token是否存在 RefreshTokenModel rt = dao.getRefreshToken(refreshToken); SaOAuth2RefreshTokenException.throwBy(rt == null, "无效 refresh_token: " + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111); // 校验:ClientId 是否一致 SaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), "无效 client_id: " + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); // 校验:Secret 是否正确 String dbSecret = checkClientModel(clientId).clientSecret; SaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), "无效 client_secret: " + clientSecret, clientId, SaOAuth2ErrorCode.CODE_30115); // 返回 Refresh-Token return rt; } /** * 校验:Access-Token、clientId、clientSecret 三者是否匹配成功 * @param clientId 应用id * @param clientSecret 秘钥 * @param accessToken Access-Token * @return SaClientModel对象 */ public AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) { AccessTokenModel at = checkAccessToken(accessToken); SaOAuth2ClientModelException.throwBy( ! at.clientId.equals(clientId), "无效 client_id:" + clientId, clientId, SaOAuth2ErrorCode.CODE_30122); checkClientSecret(clientId, clientSecret); return at; } // ----------------- Code 相关 ----------------- /** * 获取 CodeModel,无效的 code 会返回 null * @param code / * @return / */ public CodeModel getCode(String code) { return SaOAuth2Manager.getDao().getCode(code); } /** * 校验 Code,成功返回 CodeModel,失败则抛出异常 * @param code / * @return / */ public CodeModel checkCode(String code) { CodeModel cm = SaOAuth2Manager.getDao().getCode(code); if(cm == null) { throw new SaOAuth2AuthorizationCodeException("无效 code: " + code) .setAuthorizationCode(code) .setCode(SaOAuth2ErrorCode.CODE_30110); } return cm; } /** * 获取 Code,根据索引: clientId、loginId * @param clientId / * @param loginId / * @return / */ public String getCodeValue(String clientId, Object loginId) { return SaOAuth2Manager.getDao().getCodeValue(clientId, loginId); } // ----------------- Access-Token 相关 ----------------- /** * 获取 AccessTokenModel,无效的 AccessToken 会返回 null * @param accessToken / * @return / */ public AccessTokenModel getAccessToken(String accessToken) { return SaOAuth2Manager.getDao().getAccessToken(accessToken); } /** * 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 * @param accessToken / * @return / */ public AccessTokenModel checkAccessToken(String accessToken) { AccessTokenModel at = SaOAuth2Manager.getDao().getAccessToken(accessToken); if(at == null) { throw new SaOAuth2AccessTokenException("无效 access_token: " + accessToken) .setAccessToken(accessToken) .setCode(SaOAuth2ErrorCode.CODE_30106); } return at; } /** * 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token * * @param clientId / * @param loginId / * @return / */ public List getAccessTokenValueList(String clientId, Object loginId) { return SaOAuth2Manager.getDao().getAccessTokenValueList_FromAdjustAfter(clientId, loginId); } /** * 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false * @param accessToken Access-Token * @param scopes 需要校验的权限列表 */ public boolean hasAccessTokenScope(String accessToken, String... scopes) { try { checkAccessTokenScope(accessToken, scopes); return true; } catch (SaOAuth2AccessTokenException e) { return false; } } /** * 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 * @param accessToken Access-Token * @param scopes 需要校验的权限列表 */ public void checkAccessTokenScope(String accessToken, String... scopes) { AccessTokenModel at = checkAccessToken(accessToken); if(SaFoxUtil.isEmptyArray(scopes)) { return; } for (String scope : scopes) { if(! at.scopes.contains(scope)) { throw new SaOAuth2AccessTokenScopeException("该 access_token 不具备 scope:" + scope) .setAccessToken(accessToken) .setScope(scope) .setCode(SaOAuth2ErrorCode.CODE_30108); } } } /** * 获取 Access-Token 所代表的LoginId * @param accessToken Access-Token * @return LoginId */ public Object getLoginIdByAccessToken(String accessToken) { return checkAccessToken(accessToken).loginId; } /** * 获取 Access-Token 所代表的 clientId * @param accessToken Access-Token * @return LoginId */ public Object getClientIdByAccessToken(String accessToken) { return checkAccessToken(accessToken).clientId; } /** * 回收一个 Access-Token * @param accessToken Access-Token值 */ public void revokeAccessToken(String accessToken) { AccessTokenModel at = getAccessToken(accessToken); if(at == null) { return; } // 删 at、索引 SaOAuth2Dao dao = SaOAuth2Manager.getDao(); dao.deleteAccessToken(accessToken); dao.deleteAccessTokenIndex_BySingleData(at.clientId, at.loginId, accessToken); } /** * 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token * @param clientId / * @param loginId / */ public void revokeAccessTokenByIndex(String clientId, Object loginId) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); List accessTokenList = getAccessTokenValueList(clientId, loginId); if( ! accessTokenList.isEmpty()) { // 删 AT for (String accessToken : accessTokenList) { dao.deleteAccessToken(accessToken); } // 删索引 dao.deleteAccessTokenIndex(clientId, loginId); } } // ----------------- Refresh-Token 相关 ----------------- /** * 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null * @param refreshToken / * @return / */ public RefreshTokenModel getRefreshToken(String refreshToken) { return SaOAuth2Manager.getDao().getRefreshToken(refreshToken); } /** * 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 * @param refreshToken / * @return / */ public RefreshTokenModel checkRefreshToken(String refreshToken) { RefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken); if(rt == null) { throw new SaOAuth2RefreshTokenException("无效 refresh_token: " + refreshToken) .setRefreshToken(refreshToken) .setCode(SaOAuth2ErrorCode.CODE_30111); } return rt; } /** * 获取 Refresh-Token 列表:此应用下 对 某个用户 签发的所有 Refresh-Token * * @param clientId / * @param loginId / * @return / */ public List getRefreshTokenValueList(String clientId, Object loginId) { return SaOAuth2Manager.getDao().getRefreshTokenValueList_FromAdjustAfter(clientId, loginId); } /** * 回收一个 Refresh-Token * * @param refreshToken Refresh-Token 值 */ public void revokeRefreshToken(String refreshToken) { RefreshTokenModel rt = getRefreshToken(refreshToken); if(rt == null) { return; } // 删 rt、索引 SaOAuth2Dao dao = SaOAuth2Manager.getDao(); dao.deleteRefreshToken(refreshToken); dao.deleteRefreshTokenIndex_BySingleData(rt.clientId, rt.loginId, refreshToken); } /** * 回收全部 Refresh-Token:指定应用下 指定用户 的全部 Refresh-Token * * @param clientId / * @param loginId / */ public void revokeRefreshTokenByIndex(String clientId, Object loginId) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); List refreshTokenList = getRefreshTokenValueList(clientId, loginId); if( ! refreshTokenList.isEmpty()) { // 删 RT for (String refreshToken : refreshTokenList) { dao.deleteRefreshToken(refreshToken); } // 删索引 dao.deleteRefreshTokenIndex(clientId, loginId); } } /** * 根据 RefreshToken 刷新出一个 AccessToken * @param refreshToken / * @return / */ public AccessTokenModel refreshAccessToken(String refreshToken) { return SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken); } // ----------------- Client-Token 相关 ----------------- /** * 获取 ClientTokenModel,无效的 ClientToken 会返回 null * @param clientToken / * @return / */ public ClientTokenModel getClientToken(String clientToken) { return SaOAuth2Manager.getDao().getClientToken(clientToken); } /** * 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 * @param clientToken / * @return / */ public ClientTokenModel checkClientToken(String clientToken) { ClientTokenModel ct = getClientToken(clientToken); if(ct == null) { throw new SaOAuth2ClientTokenException("无效 client_token: " + clientToken) .setClientToken(clientToken) .setCode(SaOAuth2ErrorCode.CODE_30107); } return ct; } /** * 获取 Client-Token 列表:此应用下 对 某个用户 签发的所有 Client-token * * @param clientId / * @return / */ public List getClientTokenValueList(String clientId) { return SaOAuth2Manager.getDao().getClientTokenValueList_FromAdjustAfter(clientId); } /** * 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false * @param clientToken Client-Token * @param scopes 需要校验的权限列表 */ public boolean hasClientTokenScope(String clientToken, String... scopes) { try { checkClientTokenScope(clientToken, scopes); return true; } catch (SaOAuth2ClientTokenException e) { return false; } } /** * 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 * @param clientToken Client-Token * @param scopes 需要校验的权限列表 */ public void checkClientTokenScope(String clientToken, String... scopes) { ClientTokenModel ct = checkClientToken(clientToken); if(SaFoxUtil.isEmptyArray(scopes)) { return; } for (String scope : scopes) { if(! ct.scopes.contains(scope)) { throw new SaOAuth2ClientTokenScopeException("该 client_token 不具备 scope:" + scope) .setClientToken(clientToken) .setScope(scope) .setCode(SaOAuth2ErrorCode.CODE_30109); } } } /** * 回收一个 ClientToken * * @param clientToken / */ public void revokeClientToken(String clientToken) { ClientTokenModel ct = getClientToken(clientToken); if(ct == null) { return; } // 删 ct、删索引 SaOAuth2Dao dao = SaOAuth2Manager.getDao(); dao.deleteClientToken(clientToken); dao.deleteClientTokenIndex_BySingleData(ct.clientId, clientToken); } /** * 回收全部 Client-Token:指定应用下的全部 Client-Token * 回收 ClientToken,根据索引: clientId * * @param clientId / */ public void revokeClientTokenByIndex(String clientId) { SaOAuth2Dao dao = SaOAuth2Manager.getDao(); List clientTokenList = getClientTokenValueList(clientId); if( ! clientTokenList.isEmpty()) { // 删 AT for (String clientToken : clientTokenList) { dao.deleteClientToken(clientToken); } // 删索引 dao.deleteClientTokenIndex(clientId); } } // ------------------- 请求查询 /** * 数据读取:从当前请求对象中读取 access_token,并查询到 AccessTokenModel 信息,无效 access_token 抛出异常 *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token */ public AccessTokenModel currentAccessToken() { String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); return checkAccessToken(accessToken); } /** * 数据读取:从当前请求对象中读取 client_token,并查询到 ClientTokenModel 信息,无效 client_token 抛出异常 *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token */ public ClientTokenModel currentClientToken() { String clientToken = SaOAuth2Manager.getDataResolver().readClientToken(SaHolder.getRequest()); return checkClientToken(clientToken); } // ----------------- 包装其它 bean 的方法 ----------------- /** * 持久化:用户授权记录 * @param clientId 应用id * @param loginId 账号id * @param scopes 权限列表 */ public void saveGrantScope(String clientId, Object loginId, List scopes) { SaOAuth2Manager.getDao().saveGrantScope(clientId, loginId, scopes); } /** * 获取高级权限列表 * @return / */ public List getHigherScopeList() { return SaOAuth2Manager.getDataLoader().getHigherScopeList(); } /** * 获取低级权限列表 * @return / */ public List getLowerScopeList() { return SaOAuth2Manager.getDataLoader().getLowerScopeList(); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.oauth2.template; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; import cn.dev33.satoken.oauth2.data.model.CodeModel; import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import java.util.List; /** * Sa-Token OAuth2 模块 工具类 * * @author click33 * @since 1.23.0 */ public class SaOAuth2Util { // ----------------- ClientModel 相关 ----------------- /** * 获取 ClientModel,根据 clientId * * @param clientId / * @return / */ public static SaClientModel getClientModel(String clientId) { return SaOAuth2Manager.getTemplate().getClientModel(clientId); } /** * 校验 clientId 信息并返回 ClientModel,如果找不到对应 Client 信息则抛出异常 * @param clientId / * @return / */ public static SaClientModel checkClientModel(String clientId) { return SaOAuth2Manager.getTemplate().checkClientModel(clientId); } /** * 校验:clientId 与 clientSecret 是否正确 * @param clientId 应用id * @param clientSecret 秘钥 * @return SaClientModel对象 */ public static SaClientModel checkClientSecret(String clientId, String clientSecret) { return SaOAuth2Manager.getTemplate().checkClientSecret(clientId, clientSecret); } /** * 校验:clientId 与 clientSecret 是否正确,并且是否签约了指定 scopes * @param clientId 应用id * @param clientSecret 秘钥 * @param scopes 权限 * @return SaClientModel对象 */ public static SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List scopes) { return SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientId, clientSecret, scopes); } /** * 判断:该 Client 是否签约了指定的 Scope,返回 true 或 false * @param clientId 应用id * @param scopes 权限 * @return / */ public static boolean isContractScope(String clientId, List scopes) { return SaOAuth2Manager.getTemplate().isContractScope(clientId, scopes); } /** * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 * @param clientId 应用id * @param scopes 权限列表 * @return / */ public static SaClientModel checkContractScope(String clientId, List scopes) { return SaOAuth2Manager.getTemplate().checkContractScope(clientId, scopes); } /** * 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常 * @param cm 应用 * @param scopes 权限列表 * @return / */ public static SaClientModel checkContractScope(SaClientModel cm, List scopes) { return SaOAuth2Manager.getTemplate().checkContractScope(cm, scopes); } // --------- redirect_uri 相关 /** * 校验:该 Client 使用指定 url 作为回调地址,是否合法 * @param clientId 应用id * @param url 指定url */ public static void checkRedirectUri(String clientId, String url) { SaOAuth2Manager.getTemplate().checkRedirectUri(clientId, url); } // --------- 授权相关 /** * 判断:指定 loginId 是否对一个 Client 授权给了指定 Scope * @param loginId 账号id * @param clientId 应用id * @param scopes 权限 * @return 是否已经授权 */ public static boolean isGrantScope(Object loginId, String clientId, List scopes) { return SaOAuth2Manager.getTemplate().isGrantScope(loginId, clientId, scopes); } /** * 删除:指定 loginId 针对指定 Client 的授权信息 * @param loginId 账号id * @param clientId 应用id */ public static void deleteGrantScope(Object loginId, String clientId) { SaOAuth2Manager.getTemplate().deleteGrantScope(loginId, clientId); } // ----------------- Code 相关 ----------------- /** * 获取 CodeModel,无效的 code 会返回 null * @param code / * @return / */ public static CodeModel getCode(String code) { return SaOAuth2Manager.getTemplate().getCode(code); } /** * 校验 Code,成功返回 CodeModel,失败则抛出异常 * @param code / * @return / */ public static CodeModel checkCode(String code) { return SaOAuth2Manager.getTemplate().checkCode(code); } /** * 获取 Code,根据索引: clientId、loginId * @param clientId / * @param loginId / * @return / */ public static String getCodeValue(String clientId, Object loginId) { return SaOAuth2Manager.getTemplate().getCodeValue(clientId, loginId); } // ----------------- Access-Token 相关 ----------------- /** * 获取 AccessTokenModel,无效的 AccessToken 会返回 null * @param accessToken / * @return / */ public static AccessTokenModel getAccessToken(String accessToken) { return SaOAuth2Manager.getTemplate().getAccessToken(accessToken); } /** * 校验 Access-Token,成功返回 AccessTokenModel,失败则抛出异常 * @param accessToken / * @return / */ public static AccessTokenModel checkAccessToken(String accessToken) { return SaOAuth2Manager.getTemplate().checkAccessToken(accessToken); } /** * 获取 Access-Token 列表:此应用下 对 某个用户 签发的所有 Access-token * @param clientId / * @param loginId / * @return / */ public static List getAccessTokenValueList(String clientId, Object loginId) { return SaOAuth2Manager.getTemplate().getAccessTokenValueList(clientId, loginId); } /** * 判断:指定 Access-Token 是否具有指定 Scope 列表,返回 true 或 false * @param accessToken Access-Token * @param scopes 需要校验的权限列表 */ public static boolean hasAccessTokenScope(String accessToken, String... scopes) { return SaOAuth2Manager.getTemplate().hasAccessTokenScope(accessToken, scopes); } /** * 校验:指定 Access-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 * @param accessToken Access-Token * @param scopes 需要校验的权限列表 */ public static void checkAccessTokenScope(String accessToken, String... scopes) { SaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scopes); } /** * 获取 Access-Token 所代表的LoginId * @param accessToken Access-Token * @return LoginId */ public static Object getLoginIdByAccessToken(String accessToken) { return SaOAuth2Manager.getTemplate().getLoginIdByAccessToken(accessToken); } /** * 获取 Access-Token 所代表的 clientId * @param accessToken Access-Token * @return LoginId */ public static Object getClientIdByAccessToken(String accessToken) { return SaOAuth2Manager.getTemplate().getClientIdByAccessToken(accessToken); } /** * 回收一个 Access-Token * @param accessToken Access-Token值 */ public static void revokeAccessToken(String accessToken) { SaOAuth2Manager.getTemplate().revokeAccessToken(accessToken); } /** * 回收全部 Access-Token:指定应用下 指定用户 的全部 Access-Token * @param clientId / * @param loginId / */ public static void revokeAccessTokenByIndex(String clientId, Object loginId) { SaOAuth2Manager.getTemplate().revokeAccessTokenByIndex(clientId, loginId); } // ----------------- Refresh-Token 相关 ----------------- /** * 获取 RefreshTokenModel,无效的 RefreshToken 会返回 null * @param refreshToken / * @return / */ public static RefreshTokenModel getRefreshToken(String refreshToken) { return SaOAuth2Manager.getTemplate().getRefreshToken(refreshToken); } /** * 校验 Refresh-Token,成功返回 RefreshTokenModel,失败则抛出异常 * @param refreshToken / * @return / */ public static RefreshTokenModel checkRefreshToken(String refreshToken) { return SaOAuth2Manager.getTemplate().checkRefreshToken(refreshToken); } /** * 获取 Refresh-Token 列表:此应用下 对 某个用户 签发的所有 Refresh-Token * * @param clientId / * @param loginId / * @return / */ public static List getRefreshTokenValueList(String clientId, Object loginId) { return SaOAuth2Manager.getTemplate().getRefreshTokenValueList(clientId, loginId); } /** * 回收一个 Refresh-Token * * @param refreshToken Refresh-Token 值 */ public static void revokeRefreshToken(String refreshToken) { SaOAuth2Manager.getTemplate().revokeRefreshToken(refreshToken); } /** * 回收全部 Refresh-Token:指定应用下 指定用户 的全部 Refresh-Token * @param clientId / * @param loginId / */ public static void revokeRefreshTokenByIndex(String clientId, Object loginId) { SaOAuth2Manager.getTemplate().revokeRefreshTokenByIndex(clientId, loginId); } /** * 根据 RefreshToken 刷新出一个 AccessToken * @param refreshToken / * @return / */ public static AccessTokenModel refreshAccessToken(String refreshToken) { return SaOAuth2Manager.getTemplate().refreshAccessToken(refreshToken); } // ----------------- Client-Token 相关 ----------------- /** * 获取 ClientTokenModel,无效的 ClientToken 会返回 null * @param clientToken / * @return / */ public static ClientTokenModel getClientToken(String clientToken) { return SaOAuth2Manager.getTemplate().getClientToken(clientToken); } /** * 校验 Client-Token,成功返回 ClientTokenModel,失败则抛出异常 * @param clientToken / * @return / */ public static ClientTokenModel checkClientToken(String clientToken) { return SaOAuth2Manager.getTemplate().checkClientToken(clientToken); } /** * 获取 Client-Token 列表:此应用下 对 某个用户 签发的所有 Client-token * * @param clientId / * @return / */ public static List getClientTokenValueList(String clientId) { return SaOAuth2Manager.getTemplate().getClientTokenValueList(clientId); } /** * 判断:指定 Client-Token 是否具有指定 Scope 列表,返回 true 或 false * @param clientToken Client-Token * @param scopes 需要校验的权限列表 */ public static boolean hasClientTokenScope(String clientToken, String... scopes) { return SaOAuth2Manager.getTemplate().hasClientTokenScope(clientToken, scopes); } /** * 校验:指定 Client-Token 是否具有指定 Scope 列表,如果不具备则抛出异常 * @param clientToken Client-Token * @param scopes 需要校验的权限列表 */ public static void checkClientTokenScope(String clientToken, String... scopes) { SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scopes); } /** * 回收一个 ClientToken * * @param clientToken / */ public static void revokeClientToken(String clientToken) { SaOAuth2Manager.getTemplate().revokeClientToken(clientToken); } /** * 回收全部 Client-Token:指定应用下的全部 Client-Token * * @param clientId / */ public static void revokeClientTokenByIndex(String clientId) { SaOAuth2Manager.getTemplate().revokeClientTokenByIndex(clientId); } // ------------------- 请求查询 /** * 数据读取:从当前请求对象中读取 access_token,并查询到 AccessTokenModel 信息,无效 access_token 抛出异常 *
    1、请求参数 access_token,2、请求头 Authorization Bearer access_token */ public static AccessTokenModel currentAccessToken() { return SaOAuth2Manager.getTemplate().currentAccessToken(); } /** * 数据读取:从当前请求对象中读取 client_token,并查询到 ClientTokenModel 信息,无效 client_token 抛出异常 *
    1、请求参数 client_token,2、请求头 Authorization Bearer client_token */ public static ClientTokenModel currentClientToken() { return SaOAuth2Manager.getTemplate().currentClientToken(); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOAuth2.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.oauth2.annotation.handler.SaCheckAccessTokenHandler; import cn.dev33.satoken.oauth2.annotation.handler.SaCheckClientIdSecretHandler; import cn.dev33.satoken.oauth2.annotation.handler.SaCheckClientTokenHandler; import cn.dev33.satoken.strategy.SaAnnotationStrategy; /** * SaToken 插件安装:OAuth2 相关功能 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForOAuth2 implements SaTokenPlugin { @Override public void install() { // 安装 OAuth2 鉴权注解 SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckAccessTokenHandler()); SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckClientTokenHandler()); SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckClientIdSecretHandler()); } } ================================================ FILE: sa-token-plugin/sa-token-oauth2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForOAuth2 ================================================ FILE: sa-token-plugin/sa-token-okhttps/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-okhttps sa-token-okhttps sa-token integrate OkHttps cn.dev33 sa-token-core cn.zhxu okhttps ================================================ FILE: sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForOkHttps.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.http; import cn.dev33.satoken.SaManager; import cn.zhxu.okhttps.OkHttps; import java.util.Map; /** * Http 请求处理器, OkHttps 版实现 * * @author click33 * @since 1.43.0 */ public class SaHttpTemplateForOkHttps implements SaHttpTemplate { @Override public String get(String url) { SaManager.log.debug("发起请求,GET:{}", url); String res = OkHttps.sync(url).get().getBody().toString(); SaManager.log.debug("返回结果:{}", res); return res; } @Override public String postByFormData(String url, Map params) { SaManager.log.debug("发起请求,POST:{}\t参数:{}", url, params); String res = OkHttps.sync(url).addBodyPara(params).post().getBody().toString(); SaManager.log.debug("返回结果:{}", res); return res; } } ================================================ FILE: sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOkHttps.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.http.SaHttpTemplateForOkHttps; /** * SaToken 插件安装:Http 请求处理器 - OkHttps 版 * * @author click33 * @since 1.43.0 */ public class SaTokenPluginForOkHttps implements SaTokenPlugin { @Override public void install() { // 设置 OkHttps 作为 Http 请求处理器 SaManager.setSaHttpTemplate(new SaHttpTemplateForOkHttps()); } } ================================================ FILE: sa-token-plugin/sa-token-okhttps/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForOkHttps ================================================ FILE: sa-token-plugin/sa-token-quick-login/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-quick-login sa-token-quick-login sa-token-quick-login cn.dev33 sa-token-spring-boot-starter true cn.dev33 sa-token-spring-boot3-starter true org.springframework.boot spring-boot-starter-thymeleaf ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.quick; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import cn.dev33.satoken.quick.config.SaQuickConfig; import cn.dev33.satoken.quick.web.SaQuickController; /** * Quick-Bean 注入 * * @author click33 * @since 1.30.0 */ @Configuration @Import({ SaQuickController.class, SaQuickRegister.class}) public class SaQuickInject { /** * 注入 quick-login 配置 * * @param saQuickConfig 配置对象 */ @Autowired(required = false) public void setSaQuickConfig(SaQuickConfig saQuickConfig) { SaQuickManager.setConfig(saQuickConfig); } } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickManager.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.quick; import cn.dev33.satoken.quick.config.SaQuickConfig; import cn.dev33.satoken.util.SaFoxUtil; /** * SaQuickManager,持有 SaQuickConfig 配置对象全局引用 * * @author click33 * @since 1.19.0 */ public class SaQuickManager { /** * 配置文件 Bean */ private static volatile SaQuickConfig config; public static void setConfig(SaQuickConfig config) { SaQuickManager.config = config; // 如果配置了 auto=true,则随机生成账号名密码 if(config.getAuto()) { config.setName(SaFoxUtil.getRandomString(8)); config.setPwd(SaFoxUtil.getRandomString(8)); } } public static SaQuickConfig getConfig() { if (config == null) { synchronized (SaQuickManager.class) { if (config == null) { setConfig(new SaQuickConfig()); } } } return config; } } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.quick; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.httpauth.basic.SaHttpBasicAccount; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.quick.config.SaQuickConfig; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; /** * Quick Login 相关 Bean 注册 * * @author click33 * @since 1.30.0 */ @Configuration public class SaQuickRegister { /** * 使用一个比较短的前缀,尽量提高 cmd 命令台启动时指定参数的便利性 */ public static final String CONFIG_VERSION = "sa"; /** * 注册 Quick-Login 配置 * * @return see note */ @Bean @ConfigurationProperties(prefix = CONFIG_VERSION) public SaQuickConfig getSaQuickConfig() { return new SaQuickConfig(); } /** * 注册 Sa-Token 全局过滤器 * * @return / */ @Bean @Order(SaTokenConsts.ASSEMBLY_ORDER - 1) SaServletFilter getSaServletFilterForQuickLogin() { return new SaServletFilter() // 拦截路由 .addInclude("/**") // 排除掉登录相关接口,不需要鉴权的 .addExclude("/favicon.ico", "/saLogin", "/doLogin", "/sa-res/**") // 认证函数: 每次请求执行 .setAuth(obj -> { SaRouter .match(SaFoxUtil.convertStringToList(SaQuickManager.getConfig().getInclude())) .notMatch(SaFoxUtil.convertStringToList(SaQuickManager.getConfig().getExclude())) .check(r -> { // 如果已关闭认证要求,则直接通过 if (!SaQuickManager.getConfig().getAuth()) { return; } // 如果请求端提供了 Http Basic 认证信息,那么直接使用此认证信息进行登录判断 SaHttpBasicAccount hba = SaHttpBasicUtil.getHttpBasicAccount(); if(hba != null) { SaResult res = SaQuickManager.getConfig().doLoginHandle.apply(hba.getUsername(), hba.getPassword()); if(res.getCode() != SaResult.CODE_SUCCESS) { SaRouter.back(res); } } else { // 未登录时直接转发到 login.html 页面 if (! StpUtil.isLogin()) { SaHolder.getRequest().forward("/saLogin"); SaRouter.back(); } } }); }). // 异常处理函数:每次认证函数发生异常时执行此函数 setError(e -> { return e.getMessage(); }); } } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/config/SaQuickConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.quick.config; import cn.dev33.satoken.quick.function.DoLoginHandleFunction; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; /** * sa-quick 配置类 Model * * @author click33 * @since 1.19.0 */ public class SaQuickConfig { /** 是否开启全局登录校验,如果为 false,则不再拦截请求出现登录页 */ private Boolean auth = true; /** 用户名 */ private String name = "sa"; /** 密码 */ private String pwd = "123456"; /** 是否自动生成一个账号和密码,此配置项为 true 后,name、pwd 字段将失效 */ private Boolean auto = false; /** 登录页面的标题 */ private String title = "Sa-Token 登录"; /** 是否显示底部版权信息 */ private Boolean copr = true; /** 配置拦截的路径,逗号分隔 */ private String include = "/**"; /** 配置拦截的路径,逗号分隔 */ private String exclude = ""; public Boolean getAuth() { return auth; } public void setAuth(Boolean auth) { this.auth = auth; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } public Boolean getAuto() { return auto; } public void setAuto(Boolean auto) { this.auto = auto; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Boolean getCopr() { return copr; } public void setCopr(Boolean copr) { this.copr = copr; } public String getInclude() { return include; } public void setInclude(String include) { this.include = include; } public String getExclude() { return exclude; } public void setExclude(String exclude) { this.exclude = exclude; } /** * 登录处理函数 */ public DoLoginHandleFunction doLoginHandle = (name, pwd) -> { // 参数完整性校验 if(SaFoxUtil.isEmpty(name) || SaFoxUtil.isEmpty(pwd)) { return SaResult.get(500, "请输入账号和密码", null); } // 密码校验:将前端提交的 name、pwd 与配置文件中的配置项进行比对 if(name.equals(this.getName()) && pwd.equals(this.getPwd())) { StpUtil.login(this.getName()); return SaResult.data(StpUtil.getTokenInfo()); } else { return SaResult.error("账号或密码输入错误"); } }; @Override public String toString() { return "SaQuickConfig{" + "auth=" + auth + ", name='" + name + '\'' + ", pwd='" + pwd + '\'' + ", auto=" + auto + ", title='" + title + '\'' + ", copr=" + copr + ", include='" + include + '\'' + ", exclude='" + exclude + '\'' + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/function/DoLoginHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.quick.function; import cn.dev33.satoken.util.SaResult; import java.util.function.BiFunction; /** * 函数式接口:登录处理函数 * *

    参数:账号、密码

    *

    返回:登录结果

    * * @author click33 * @since 1.41.0 */ @FunctionalInterface public interface DoLoginHandleFunction extends BiFunction { } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/web/SaQuickController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.quick.web; import cn.dev33.satoken.quick.SaQuickManager; import cn.dev33.satoken.util.SaResult; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * 登录Controller,处理登录相关请求 * * @author click33 * @since 1.19.0 */ @Controller public class SaQuickController { /** * 进入登录页面 * @param model / * @return / */ @GetMapping("/saLogin") public String saLogin(Model model) { model.addAttribute("cfg", SaQuickManager.getConfig()); return "sa-login.html"; } /** * 登录接口 * @param name 账号 * @param pwd 密码 * @return 是否登录成功 */ @PostMapping("/doLogin") @ResponseBody public SaResult doLogin(@RequestParam("name") String name, @RequestParam("pwd") String pwd) { return SaQuickManager.getConfig().doLoginHandle.apply(name, pwd); } } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.quick.SaQuickInject ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.quick.SaQuickInject ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/layer.js ================================================ /*! layer-v3.1.1 Web弹层组件 MIT License http://layer.layui.com/ By 贤心 */ ;!function(e,t){"use strict";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;n--)if("interactive"===t[n].readyState){e=t[n].src;break}return e||t[i].src}();return e.substring(0,e.lastIndexOf("/")+1)}(),config:{},end:{},minIndex:0,minLeft:[],btn:["确定","取消"],type:["dialog","page","iframe","loading","tips"],getStyle:function(t,i){var n=t.currentStyle?t.currentStyle:e.getComputedStyle(t,null);return n[n.getPropertyValue?"getPropertyValue":"getAttribute"](i)},link:function(t,i,n){if(r.path){var a=document.getElementsByTagName("head")[0],s=document.createElement("link");"string"==typeof i&&(n=i);var l=(n||t).replace(/\.|\//g,""),f="layuicss-"+l,c=0;s.rel="stylesheet",s.href=r.path+t,s.id=f,document.getElementById(f)||a.appendChild(s),"function"==typeof i&&!function u(){return++c>80?e.console&&console.error("layer.css: Invalid"):void(1989===parseInt(o.getStyle(document.getElementById(f),"width"))?i():setTimeout(u,100))}()}}},r={v:"3.1.1",ie:function(){var t=navigator.userAgent.toLowerCase();return!!(e.ActiveXObject||"ActiveXObject"in e)&&((t.match(/msie\s(\d+)/)||[])[1]||"11")}(),index:e.layer&&e.layer.v?1e5:0,path:o.getPath,config:function(e,t){return e=e||{},r.cache=o.config=i.extend({},o.config,e),r.path=o.config.path||r.path,"string"==typeof e.extend&&(e.extend=[e.extend]),o.config.path&&r.ready(),e.extend?(a?layui.addcss("modules/layer/"+e.extend):o.link("theme/"+e.extend),this):this},ready:function(e){var t="layer",i="",n=(a?"modules/layer/":"theme/")+"default/layer.css?v="+r.v+i;return a?layui.addcss(n,e,t):o.link(n,e,t),this},alert:function(e,t,n){var a="function"==typeof t;return a&&(n=t),r.open(i.extend({content:e,yes:n},a?{}:t))},confirm:function(e,t,n,a){var s="function"==typeof t;return s&&(a=n,n=t),r.open(i.extend({content:e,btn:o.btn,yes:n,btn2:a},s?{}:t))},msg:function(e,n,a){var s="function"==typeof n,f=o.config.skin,c=(f?f+" "+f+"-msg":"")||"layui-layer-msg",u=l.anim.length-1;return s&&(a=n),r.open(i.extend({content:e,time:3e3,shade:!1,skin:c,title:!1,closeBtn:!1,btn:!1,resize:!1,end:a},s&&!o.config.skin?{skin:c+" layui-layer-hui",anim:u}:function(){return n=n||{},(n.icon===-1||n.icon===t&&!o.config.skin)&&(n.skin=c+" "+(n.skin||"layui-layer-hui")),n}()))},load:function(e,t){return r.open(i.extend({type:3,icon:e||0,resize:!1,shade:.01},t))},tips:function(e,t,n){return r.open(i.extend({type:4,content:[e,t],closeBtn:!1,time:3e3,shade:!1,resize:!1,fixed:!1,maxWidth:210},n))}},s=function(e){var t=this;t.index=++r.index,t.config=i.extend({},t.config,o.config,e),document.body?t.creat():setTimeout(function(){t.creat()},30)};s.pt=s.prototype;var l=["layui-layer",".layui-layer-title",".layui-layer-main",".layui-layer-dialog","layui-layer-iframe","layui-layer-content","layui-layer-btn","layui-layer-close"];l.anim=["layer-anim-00","layer-anim-01","layer-anim-02","layer-anim-03","layer-anim-04","layer-anim-05","layer-anim-06"],s.pt.config={type:0,shade:.3,fixed:!0,move:l[1],title:"信息",offset:"auto",area:"auto",closeBtn:1,time:0,zIndex:19891014,maxWidth:360,anim:0,isOutAnim:!0,icon:-1,moveType:1,resize:!0,scrollbar:!0,tips:2},s.pt.vessel=function(e,t){var n=this,a=n.index,r=n.config,s=r.zIndex+a,f="object"==typeof r.title,c=r.maxmin&&(1===r.type||2===r.type),u=r.title?'
    '+(f?r.title[0]:r.title)+"
    ":"";return r.zIndex=s,t([r.shade?'
    ':"",'
    '+(e&&2!=r.type?"":u)+'
    '+(0==r.type&&r.icon!==-1?'':"")+(1==r.type&&e?"":r.content||"")+'
    '+function(){var e=c?'':"";return r.closeBtn&&(e+=''),e}()+""+(r.btn?function(){var e="";"string"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t'+r.btn[t]+"";return'
    '+e+"
    "}():"")+(r.resize?'':"")+"
    "],u,i('
    ')),n},s.pt.creat=function(){var e=this,t=e.config,a=e.index,s=t.content,f="object"==typeof s,c=i("body");if(!t.id||!i("#"+t.id)[0]){switch("string"==typeof t.area&&(t.area="auto"===t.area?["",""]:[t.area,""]),t.shift&&(t.anim=t.shift),6==r.ie&&(t.fixed=!1),t.type){case 0:t.btn="btn"in t?t.btn:o.btn[0],r.closeAll("dialog");break;case 2:var s=t.content=f?t.content:[t.content||"http://layer.layui.com","auto"];t.content='';break;case 3:delete t.title,delete t.closeBtn,t.icon===-1&&0===t.icon,r.closeAll("loading");break;case 4:f||(t.content=[t.content,"body"]),t.follow=t.content[1],t.content=t.content[0]+'',delete t.title,t.tips="object"==typeof t.tips?t.tips:[t.tips,!0],t.tipsMore||r.closeAll("tips")}if(e.vessel(f,function(n,r,u){c.append(n[0]),f?function(){2==t.type||4==t.type?function(){i("body").append(n[1])}():function(){s.parents("."+l[0])[0]||(s.data("display",s.css("display")).show().addClass("layui-layer-wrap").wrap(n[1]),i("#"+l[0]+a).find("."+l[5]).before(r))}()}():c.append(n[1]),i(".layui-layer-move")[0]||c.append(o.moveElem=u),e.layero=i("#"+l[0]+a),t.scrollbar||l.html.css("overflow","hidden").attr("layer-full",a)}).auto(a),i("#layui-layer-shade"+e.index).css({"background-color":t.shade[1]||"#000",opacity:t.shade[0]||t.shade}),2==t.type&&6==r.ie&&e.layero.find("iframe").attr("src",s[0]),4==t.type?e.tips():e.offset(),t.fixed&&n.on("resize",function(){e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(a),4==t.type&&e.tips()}),t.time<=0||setTimeout(function(){r.close(e.index)},t.time),e.move().callback(),l.anim[t.anim]){var u="layer-anim "+l.anim[t.anim];e.layero.addClass(u).one("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){i(this).removeClass(u)})}t.isOutAnim&&e.layero.data("isOutAnim",!0)}},s.pt.auto=function(e){var t=this,a=t.config,o=i("#"+l[0]+e);""===a.area[0]&&a.maxWidth>0&&(r.ie&&r.ie<8&&a.btn&&o.width(o.innerWidth()),o.outerWidth()>a.maxWidth&&o.width(a.maxWidth));var s=[o.innerWidth(),o.innerHeight()],f=o.find(l[1]).outerHeight()||0,c=o.find("."+l[6]).outerHeight()||0,u=function(e){e=o.find(e),e.height(s[1]-f-c-2*(0|parseFloat(e.css("padding-top"))))};switch(a.type){case 2:u("iframe");break;default:""===a.area[1]?a.maxHeight>0&&o.outerHeight()>a.maxHeight?(s[1]=a.maxHeight,u("."+l[5])):a.fixed&&s[1]>=n.height()&&(s[1]=n.height(),u("."+l[5])):u("."+l[5])}return t},s.pt.offset=function(){var e=this,t=e.config,i=e.layero,a=[i.outerWidth(),i.outerHeight()],o="object"==typeof t.offset;e.offsetTop=(n.height()-a[1])/2,e.offsetLeft=(n.width()-a[0])/2,o?(e.offsetTop=t.offset[0],e.offsetLeft=t.offset[1]||e.offsetLeft):"auto"!==t.offset&&("t"===t.offset?e.offsetTop=0:"r"===t.offset?e.offsetLeft=n.width()-a[0]:"b"===t.offset?e.offsetTop=n.height()-a[1]:"l"===t.offset?e.offsetLeft=0:"lt"===t.offset?(e.offsetTop=0,e.offsetLeft=0):"lb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=0):"rt"===t.offset?(e.offsetTop=0,e.offsetLeft=n.width()-a[0]):"rb"===t.offset?(e.offsetTop=n.height()-a[1],e.offsetLeft=n.width()-a[0]):e.offsetTop=t.offset),t.fixed||(e.offsetTop=/%$/.test(e.offsetTop)?n.height()*parseFloat(e.offsetTop)/100:parseFloat(e.offsetTop),e.offsetLeft=/%$/.test(e.offsetLeft)?n.width()*parseFloat(e.offsetLeft)/100:parseFloat(e.offsetLeft),e.offsetTop+=n.scrollTop(),e.offsetLeft+=n.scrollLeft()),i.attr("minLeft")&&(e.offsetTop=n.height()-(i.find(l[1]).outerHeight()||0),e.offsetLeft=i.css("left")),i.css({top:e.offsetTop,left:e.offsetLeft})},s.pt.tips=function(){var e=this,t=e.config,a=e.layero,o=[a.outerWidth(),a.outerHeight()],r=i(t.follow);r[0]||(r=i("body"));var s={width:r.outerWidth(),height:r.outerHeight(),top:r.offset().top,left:r.offset().left},f=a.find(".layui-layer-TipsG"),c=t.tips[0];t.tips[1]||f.remove(),s.autoLeft=function(){s.left+o[0]-n.width()>0?(s.tipLeft=s.left+s.width-o[0],f.css({right:12,left:"auto"})):s.tipLeft=s.left},s.where=[function(){s.autoLeft(),s.tipTop=s.top-o[1]-10,f.removeClass("layui-layer-TipsB").addClass("layui-layer-TipsT").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left+s.width+10,s.tipTop=s.top,f.removeClass("layui-layer-TipsL").addClass("layui-layer-TipsR").css("border-bottom-color",t.tips[1])},function(){s.autoLeft(),s.tipTop=s.top+s.height+10,f.removeClass("layui-layer-TipsT").addClass("layui-layer-TipsB").css("border-right-color",t.tips[1])},function(){s.tipLeft=s.left-o[0]-10,s.tipTop=s.top,f.removeClass("layui-layer-TipsR").addClass("layui-layer-TipsL").css("border-bottom-color",t.tips[1])}],s.where[c-1](),1===c?s.top-(n.scrollTop()+o[1]+16)<0&&s.where[2]():2===c?n.width()-(s.left+s.width+o[0]+16)>0||s.where[3]():3===c?s.top-n.scrollTop()+s.height+o[1]+16-n.height()>0&&s.where[0]():4===c&&o[0]+16-s.left>0&&s.where[1](),a.find("."+l[5]).css({"background-color":t.tips[1],"padding-right":t.closeBtn?"30px":""}),a.css({left:s.tipLeft-(t.fixed?n.scrollLeft():0),top:s.tipTop-(t.fixed?n.scrollTop():0)})},s.pt.move=function(){var e=this,t=e.config,a=i(document),s=e.layero,l=s.find(t.move),f=s.find(".layui-layer-resize"),c={};return t.move&&l.css("cursor","move"),l.on("mousedown",function(e){e.preventDefault(),t.move&&(c.moveStart=!0,c.offset=[e.clientX-parseFloat(s.css("left")),e.clientY-parseFloat(s.css("top"))],o.moveElem.css("cursor","move").show())}),f.on("mousedown",function(e){e.preventDefault(),c.resizeStart=!0,c.offset=[e.clientX,e.clientY],c.area=[s.outerWidth(),s.outerHeight()],o.moveElem.css("cursor","se-resize").show()}),a.on("mousemove",function(i){if(c.moveStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1],l="fixed"===s.css("position");if(i.preventDefault(),c.stX=l?0:n.scrollLeft(),c.stY=l?0:n.scrollTop(),!t.moveOut){var f=n.width()-s.outerWidth()+c.stX,u=n.height()-s.outerHeight()+c.stY;af&&(a=f),ou&&(o=u)}s.css({left:a,top:o})}if(t.resize&&c.resizeStart){var a=i.clientX-c.offset[0],o=i.clientY-c.offset[1];i.preventDefault(),r.style(e.index,{width:c.area[0]+a,height:c.area[1]+o}),c.isResize=!0,t.resizing&&t.resizing(s)}}).on("mouseup",function(e){c.moveStart&&(delete c.moveStart,o.moveElem.hide(),t.moveEnd&&t.moveEnd(s)),c.resizeStart&&(delete c.resizeStart,o.moveElem.hide())}),e},s.pt.callback=function(){function e(){var e=a.cancel&&a.cancel(t.index,n);e===!1||r.close(t.index)}var t=this,n=t.layero,a=t.config;t.openLayer(),a.success&&(2==a.type?n.find("iframe").on("load",function(){a.success(n,t.index)}):a.success(n,t.index)),6==r.ie&&t.IE6(n),n.find("."+l[6]).children("a").on("click",function(){var e=i(this).index();if(0===e)a.yes?a.yes(t.index,n):a.btn1?a.btn1(t.index,n):r.close(t.index);else{var o=a["btn"+(e+1)]&&a["btn"+(e+1)](t.index,n);o===!1||r.close(t.index)}}),n.find("."+l[7]).on("click",e),a.shadeClose&&i("#layui-layer-shade"+t.index).on("click",function(){r.close(t.index)}),n.find(".layui-layer-min").on("click",function(){var e=a.min&&a.min(n);e===!1||r.min(t.index,a)}),n.find(".layui-layer-max").on("click",function(){i(this).hasClass("layui-layer-maxmin")?(r.restore(t.index),a.restore&&a.restore(n)):(r.full(t.index,a),setTimeout(function(){a.full&&a.full(n)},100))}),a.end&&(o.end[t.index]=a.end)},o.reselect=function(){i.each(i("select"),function(e,t){var n=i(this);n.parents("."+l[0])[0]||1==n.attr("layer")&&i("."+l[0]).length<1&&n.removeAttr("layer").show(),n=null})},s.pt.IE6=function(e){i("select").each(function(e,t){var n=i(this);n.parents("."+l[0])[0]||"none"===n.css("display")||n.attr({layer:"1"}).hide(),n=null})},s.pt.openLayer=function(){var e=this;r.zIndex=e.config.zIndex,r.setTop=function(e){var t=function(){r.zIndex++,e.css("z-index",r.zIndex+1)};return r.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",t),r.zIndex}},o.record=function(e){var t=[e.width(),e.height(),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:t})},o.rescollbar=function(e){l.html.attr("layer-full")==e&&(l.html[0].style.removeProperty?l.html[0].style.removeProperty("overflow"):l.html[0].style.removeAttribute("overflow"),l.html.removeAttr("layer-full"))},e.layer=r,r.getChildFrame=function(e,t){return t=t||i("."+l[4]).attr("times"),i("#"+l[0]+t).find("iframe").contents().find(e)},r.getFrameIndex=function(e){return i("#"+e).parents("."+l[4]).attr("times")},r.iframeAuto=function(e){if(e){var t=r.getChildFrame("html",e).outerHeight(),n=i("#"+l[0]+e),a=n.find(l[1]).outerHeight()||0,o=n.find("."+l[6]).outerHeight()||0;n.css({height:t+a+o}),n.find("iframe").css({height:t})}},r.iframeSrc=function(e,t){i("#"+l[0]+e).find("iframe").attr("src",t)},r.style=function(e,t,n){var a=i("#"+l[0]+e),r=a.find(".layui-layer-content"),s=a.attr("type"),f=a.find(l[1]).outerHeight()||0,c=a.find("."+l[6]).outerHeight()||0;a.attr("minLeft");s!==o.type[3]&&s!==o.type[4]&&(n||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-f-c<=64&&(t.height=64+f+c)),a.css(t),c=a.find("."+l[6]).outerHeight(),s===o.type[2]?a.find("iframe").css({height:parseFloat(t.height)-f-c}):r.css({height:parseFloat(t.height)-f-c-parseFloat(r.css("padding-top"))-parseFloat(r.css("padding-bottom"))}))},r.min=function(e,t){var a=i("#"+l[0]+e),s=a.find(l[1]).outerHeight()||0,f=a.attr("minLeft")||181*o.minIndex+"px",c=a.css("position");o.record(a),o.minLeft[0]&&(f=o.minLeft[0],o.minLeft.shift()),a.attr("position",c),r.style(e,{width:180,height:s,left:f,top:n.height()-s,position:"fixed",overflow:"hidden"},!0),a.find(".layui-layer-min").hide(),"page"===a.attr("type")&&a.find(l[4]).hide(),o.rescollbar(e),a.attr("minLeft")||o.minIndex++,a.attr("minLeft",f)},r.restore=function(e){var t=i("#"+l[0]+e),n=t.attr("area").split(",");t.attr("type");r.style(e,{width:parseFloat(n[0]),height:parseFloat(n[1]),top:parseFloat(n[2]),left:parseFloat(n[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===t.attr("type")&&t.find(l[4]).show(),o.rescollbar(e)},r.full=function(e){var t,a=i("#"+l[0]+e);o.record(a),l.html.attr("layer-full")||l.html.css("overflow","hidden").attr("layer-full",e),clearTimeout(t),t=setTimeout(function(){var t="fixed"===a.css("position");r.style(e,{top:t?0:n.scrollTop(),left:t?0:n.scrollLeft(),width:n.width(),height:n.height()},!0),a.find(".layui-layer-min").hide()},100)},r.title=function(e,t){var n=i("#"+l[0]+(t||r.index)).find(l[1]);n.html(e)},r.close=function(e){var t=i("#"+l[0]+e),n=t.attr("type"),a="layer-anim-close";if(t[0]){var s="layui-layer-wrap",f=function(){if(n===o.type[1]&&"object"===t.attr("conType")){t.children(":not(."+l[5]+")").remove();for(var a=t.find("."+s),r=0;r<2;r++)a.unwrap();a.css("display",a.data("display")).removeClass(s)}else{if(n===o.type[2])try{var f=i("#"+l[4]+e)[0];f.contentWindow.document.write(""),f.contentWindow.close(),t.find("."+l[5])[0].removeChild(f)}catch(c){}t[0].innerHTML="",t.remove()}"function"==typeof o.end[e]&&o.end[e](),delete o.end[e]};t.data("isOutAnim")&&t.addClass("layer-anim "+a),i("#layui-layer-moves, #layui-layer-shade"+e).remove(),6==r.ie&&o.reselect(),o.rescollbar(e),t.attr("minLeft")&&(o.minIndex--,o.minLeft.push(t.attr("minLeft"))),r.ie&&r.ie<10||!t.data("isOutAnim")?f():setTimeout(function(){f()},200)}},r.closeAll=function(e){i.each(i("."+l[0]),function(){var t=i(this),n=e?t.attr("type")===e:1;n&&r.close(t.attr("times")),n=null})};var f=r.cache||{},c=function(e){return f.skin?" "+f.skin+" "+f.skin+"-"+e:""};r.prompt=function(e,t){var a="";if(e=e||{},"function"==typeof e&&(t=e),e.area){var o=e.area;a='style="width: '+o[0]+"; height: "+o[1]+';"',delete e.area}var s,l=2==e.formType?'":function(){return''}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:["确定","取消"],content:l,skin:"layui-layer-prompt"+c("prompt"),maxWidth:n.width(),success:function(e){s=e.find(".layui-layer-input"),s.focus(),"function"==typeof f&&f(e)},resize:!1,yes:function(i){var n=s.val();""===n?s.focus():n.length>(e.maxlength||500)?r.tips("最多输入"+(e.maxlength||500)+"个字数",s,{tips:1}):t&&t(n,i,s)}},e))},r.tab=function(e){e=e||{};var t=e.tab||{},n="layui-this",a=e.success;return delete e.success,r.open(i.extend({type:1,skin:"layui-layer-tab"+c("tab"),resize:!1,title:function(){var e=t.length,i=1,a="";if(e>0)for(a=''+t[0].title+"";i"+t[i].title+"";return a}(),content:'
      '+function(){var e=t.length,i=1,a="";if(e>0)for(a='
    • '+(t[0].content||"no content")+"
    • ";i'+(t[i].content||"no content")+"";return a}()+"
    ",success:function(t){var o=t.find(".layui-layer-title").children(),r=t.find(".layui-layer-tabmain").children();o.on("mousedown",function(t){t.stopPropagation?t.stopPropagation():t.cancelBubble=!0;var a=i(this),o=a.index();a.addClass(n).siblings().removeClass(n),r.eq(o).show().siblings().hide(),"function"==typeof e.change&&e.change(o)}),"function"==typeof a&&a(t)}},e))},r.photos=function(t,n,a){function o(e,t,i){var n=new Image;return n.src=e,n.complete?t(n):(n.onload=function(){n.onload=null,t(n)},void(n.onerror=function(e){n.onerror=null,i(e)}))}var s={};if(t=t||{},t.photos){var l=t.photos.constructor===Object,f=l?t.photos:{},u=f.data||[],d=f.start||0;s.imgIndex=(0|d)+1,t.img=t.img||"img";var y=t.success;if(delete t.success,l){if(0===u.length)return r.msg("没有图片")}else{var p=i(t.photos),h=function(){u=[],p.find(t.img).each(function(e){var t=i(this);t.attr("layer-index",e),u.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(h(),0===u.length)return;if(n||p.on("click",t.img,function(){var e=i(this),n=e.attr("layer-index");r.photos(i.extend(t,{photos:{start:n,data:u,tab:t.tab},full:t.full}),!0),h()}),!n)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=u.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>u.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){if(!s.end){var t=e.keyCode;e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&r.close(s.index)}},s.tabimg=function(e){if(!(u.length<=1))return f.start=s.imgIndex-1,r.close(s.index),r.photos(t,!0,e)},s.event=function(){s.bigimg.hover(function(){s.imgsee.show()},function(){s.imgsee.hide()}),s.bigimg.find(".layui-layer-imgprev").on("click",function(e){e.preventDefault(),s.imgprev()}),s.bigimg.find(".layui-layer-imgnext").on("click",function(e){e.preventDefault(),s.imgnext()}),i(document).on("keyup",s.keyup)},s.loadi=r.load(1,{shade:!("shade"in t)&&.9,scrollbar:!1}),o(u[d].src,function(n){r.close(s.loadi),s.index=r.open(i.extend({type:1,id:"layui-layer-photos",area:function(){var a=[n.width,n.height],o=[i(e).width()-100,i(e).height()-100];if(!t.full&&(a[0]>o[0]||a[1]>o[1])){var r=[a[0]/o[0],a[1]/o[1]];r[0]>r[1]?(a[0]=a[0]/r[0],a[1]=a[1]/r[0]):r[0]'+(u[d].alt||
    '+(u.length>1?'':"")+'
    '+(u[d].alt||"")+""+s.imgIndex+"/"+u.length+"
    ",success:function(e,i){s.bigimg=e.find(".layui-layer-phimg"),s.imgsee=e.find(".layui-layer-imguide,.layui-layer-imgbar"),s.event(e),t.tab&&t.tab(u[d],e),"function"==typeof y&&y(e)},end:function(){s.end=!0,i(document).off("keyup",s.keyup)}},t))},function(){r.close(s.loadi),r.msg("当前图片地址异常
    是否继续查看下一张?",{time:3e4,btn:["下一张","不看了"],yes:function(){u.length>1&&s.imgnext(!0,!0)}})})}},o.run=function(t){i=t,n=i(e),l.html=i("html"),r.open=function(e){var t=new s(e);return t.index}},e.layui&&layui.define?(r.ready(),layui.define("jquery",function(t){r.path=layui.cache.dir,o.run(layui.$),e.layer=r,t("layer",r)})):"function"==typeof define&&define.amd?define(["jquery"],function(){return o.run(e.jQuery),r}):function(){o.run(e.jQuery),r.ready()}()}(window); ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/mobile/layer.js ================================================ /*! layer mobile-v2.0.0 Web弹层组件 MIT License http://layer.layui.com/mobile By 贤心 */ ;!function(e){"use strict";var t=document,n="querySelectorAll",i="getElementsByClassName",a=function(e){return t[n](e)},s={type:0,shade:!0,shadeClose:!0,fixed:!0,anim:"scale"},l={extend:function(e){var t=JSON.parse(JSON.stringify(s));for(var n in e)t[n]=e[n];return t},timer:{},end:{}};l.touch=function(e,t){e.addEventListener("click",function(e){t.call(this,e)},!1)};var r=0,o=["layui-m-layer"],c=function(e){var t=this;t.config=l.extend(e),t.view()};c.prototype.view=function(){var e=this,n=e.config,s=t.createElement("div");e.id=s.id=o[0]+r,s.setAttribute("class",o[0]+" "+o[0]+(n.type||0)),s.setAttribute("index",r);var l=function(){var e="object"==typeof n.title;return n.title?'

    '+(e?n.title[0]:n.title)+"

    ":""}(),c=function(){"string"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e=''+n.btn[0]+"",2===t&&(e=''+n.btn[1]+""+e),'
    '+e+"
    "):""}();if(n.fixed||(n.top=n.hasOwnProperty("top")?n.top:100,n.style=n.style||"",n.style+=" top:"+(t.body.scrollTop+n.top)+"px"),2===n.type&&(n.content='

    '+(n.content||"")+"

    "),n.skin&&(n.anim="up"),"msg"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?"
    ':"")+'
    "+l+'
    '+n.content+"
    "+c+"
    ",!n.type||2===n.type){var d=t[i](o[0]+n.type),y=d.length;y>=1&&layer.close(d[0].getAttribute("index"))}document.body.appendChild(s);var u=e.elem=a("#"+e.id)[0];n.success&&n.success(u),e.index=r++,e.action(n,u)},c.prototype.action=function(e,t){var n=this;e.time&&(l.timer[n.index]=setTimeout(function(){layer.close(n.index)},1e3*e.time));var a=function(){var t=this.getAttribute("type");0==t?(e.no&&e.no(),layer.close(n.index)):e.yes?e.yes(n.index):layer.close(n.index)};if(e.btn)for(var s=t[i]("layui-m-layerbtn")[0].children,r=s.length,o=0;odiv{line-height:22px;padding-top:7px;margin-bottom:20px;font-size:14px}.layui-m-layerbtn{display:box;display:-moz-box;display:-webkit-box;width:100%;height:50px;line-height:50px;font-size:0;border-top:1px solid #D0D0D0;background-color:#F2F2F2}.layui-m-layerbtn span{display:block;-moz-box-flex:1;box-flex:1;-webkit-box-flex:1;font-size:14px;cursor:pointer}.layui-m-layerbtn span[yes]{color:#40AFFE}.layui-m-layerbtn span[no]{border-right:1px solid #D0D0D0;border-radius:0 0 0 5px}.layui-m-layerbtn span:active{background-color:#F6F6F6}.layui-m-layerend{position:absolute;right:7px;top:10px;width:30px;height:30px;border:0;font-weight:400;background:0 0;cursor:pointer;-webkit-appearance:none;font-size:30px}.layui-m-layerend::after,.layui-m-layerend::before{position:absolute;left:5px;top:15px;content:'';width:18px;height:1px;background-color:#999;transform:rotate(45deg);-webkit-transform:rotate(45deg);border-radius:3px}.layui-m-layerend::after{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}body .layui-m-layer .layui-m-layer-footer{position:fixed;width:95%;max-width:100%;margin:0 auto;left:0;right:0;bottom:10px;background:0 0}.layui-m-layer-footer .layui-m-layercont{padding:20px;border-radius:5px 5px 0 0;background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn{display:block;height:auto;background:0 0;border-top:none}.layui-m-layer-footer .layui-m-layerbtn span{background-color:rgba(255,255,255,.8)}.layui-m-layer-footer .layui-m-layerbtn span[no]{color:#FD482C;border-top:1px solid #c2c2c2;border-radius:0 0 5px 5px}.layui-m-layer-footer .layui-m-layerbtn span[yes]{margin-top:10px;border-radius:5px}body .layui-m-layer .layui-m-layer-msg{width:auto;max-width:90%;margin:0 auto;bottom:-150px;background-color:rgba(0,0,0,.7);color:#fff}.layui-m-layer-msg .layui-m-layercont{padding:10px 20px} ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/theme/default/layer.css ================================================ .layui-layer-imgbar,.layui-layer-imgtit a,.layui-layer-tab .layui-layer-title span,.layui-layer-title{text-overflow:ellipsis;white-space:nowrap}html #layuicss-layer{display:none;position:absolute;width:1989px}.layui-layer,.layui-layer-shade{position:fixed;_position:absolute;pointer-events:auto}.layui-layer-shade{top:0;left:0;width:100%;height:100%;_height:expression(document.body.offsetHeight+"px")}.layui-layer{-webkit-overflow-scrolling:touch;top:150px;left:0;margin:0;padding:0;background-color:#fff;-webkit-background-clip:content;border-radius:2px;box-shadow:1px 1px 50px rgba(0,0,0,.3)}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #B2B2B2;border:1px solid rgba(0,0,0,.1);box-shadow:1px 1px 5px rgba(0,0,0,.2)}.layui-layer-load{background:url(loading-1.gif) center center no-repeat #eee}.layui-layer-ico{background:url(icon.png) no-repeat}.layui-layer-btn a,.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a{display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-move{display:none;position:fixed;*position:absolute;left:0;top:0;width:100%;height:100%;cursor:move;opacity:0;filter:alpha(opacity=0);background-color:#fff;z-index:2147483647}.layui-layer-resize{position:absolute;width:15px;height:15px;right:0;bottom:0;cursor:se-resize}.layer-anim{-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceIn{0%{opacity:0;-webkit-transform:scale(.5);-ms-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-00{-webkit-animation-name:layer-bounceIn;animation-name:layer-bounceIn}@-webkit-keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInDown{0%{opacity:0;-webkit-transform:scale(.1) translateY(-2000px);-ms-transform:scale(.1) translateY(-2000px);transform:scale(.1) translateY(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateY(60px);-ms-transform:scale(.475) translateY(60px);transform:scale(.475) translateY(60px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-01{-webkit-animation-name:layer-zoomInDown;animation-name:layer-zoomInDown}@-webkit-keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layer-fadeInUpBig{0%{opacity:0;-webkit-transform:translateY(2000px);-ms-transform:translateY(2000px);transform:translateY(2000px)}100%{opacity:1;-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}}.layer-anim-02{-webkit-animation-name:layer-fadeInUpBig;animation-name:layer-fadeInUpBig}@-webkit-keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}@keyframes layer-zoomInLeft{0%{opacity:0;-webkit-transform:scale(.1) translateX(-2000px);-ms-transform:scale(.1) translateX(-2000px);transform:scale(.1) translateX(-2000px);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}60%{opacity:1;-webkit-transform:scale(.475) translateX(48px);-ms-transform:scale(.475) translateX(48px);transform:scale(.475) translateX(48px);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}}.layer-anim-03{-webkit-animation-name:layer-zoomInLeft;animation-name:layer-zoomInLeft}@-webkit-keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}@keyframes layer-rollIn{0%{opacity:0;-webkit-transform:translateX(-100%) rotate(-120deg);-ms-transform:translateX(-100%) rotate(-120deg);transform:translateX(-100%) rotate(-120deg)}100%{opacity:1;-webkit-transform:translateX(0) rotate(0);-ms-transform:translateX(0) rotate(0);transform:translateX(0) rotate(0)}}.layer-anim-04{-webkit-animation-name:layer-rollIn;animation-name:layer-rollIn}@keyframes layer-fadeIn{0%{opacity:0}100%{opacity:1}}.layer-anim-05{-webkit-animation-name:layer-fadeIn;animation-name:layer-fadeIn}@-webkit-keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes layer-shake{0%,100%{-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);-ms-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);-ms-transform:translateX(10px);transform:translateX(10px)}}.layer-anim-06{-webkit-animation-name:layer-shake;animation-name:layer-shake}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.layui-layer-title{padding:0 80px 0 20px;height:42px;line-height:42px;border-bottom:1px solid #eee;font-size:14px;color:#333;overflow:hidden;background-color:#F8F8F8;border-radius:2px 2px 0 0}.layui-layer-setwin{position:absolute;right:15px;*right:0;top:15px;font-size:0;line-height:initial}.layui-layer-setwin a{position:relative;width:16px;height:16px;margin-left:10px;font-size:12px;_overflow:hidden}.layui-layer-setwin .layui-layer-min cite{position:absolute;width:14px;height:2px;left:0;top:50%;margin-top:-1px;background-color:#2E2D3C;cursor:pointer;_overflow:hidden}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2D93CA}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{background-position:1px -40px;cursor:pointer}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{position:absolute;right:-28px;top:-28px;width:30px;height:30px;margin-left:0;background-position:-149px -31px;*right:-18px;_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;padding:0 15px 12px;pointer-events:auto;user-select:none;-webkit-user-select:none}.layui-layer-btn a{height:28px;line-height:28px;margin:5px 5px 0;padding:0 15px;border:1px solid #dedede;background-color:#fff;color:#333;border-radius:2px;font-weight:400;cursor:pointer;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{border-color:#1E9FFF;background-color:#1E9FFF;color:#fff}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{position:relative;padding:20px;line-height:24px;word-break:break-all;overflow:hidden;font-size:14px;overflow-x:hidden;overflow-y:auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{position:absolute;top:16px;left:15px;_left:-40px;width:30px;height:30px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{border:6px solid #8D8D8D;border:6px solid rgba(0,0,0,.3);border-radius:5px;box-shadow:none}.layui-layer-msg{min-width:180px;border:1px solid #D3D4D3;box-shadow:none}.layui-layer-hui{min-width:100px;background-color:#000;filter:alpha(opacity=60);background-color:rgba(0,0,0,.6);color:#fff;border:none}.layui-layer-hui .layui-layer-content{padding:12px 25px;text-align:center}.layui-layer-dialog .layui-layer-padding{padding:20px 20px 20px 55px;text-align:left}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-iframe .layui-layer-btn,.layui-layer-page .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{display:block;width:100%}.layui-layer-loading{border-radius:100%;background:0 0;box-shadow:none;border:none}.layui-layer-loading .layui-layer-content{width:60px;height:24px;background:url(loading-0.gif) no-repeat}.layui-layer-loading .layui-layer-loading1{width:37px;height:37px;background:url(loading-1.gif) no-repeat}.layui-layer-ico16,.layui-layer-loading .layui-layer-loading2{width:32px;height:32px;background:url(loading-2.gif) no-repeat}.layui-layer-tips{background:0 0;box-shadow:none;border:none}.layui-layer-tips .layui-layer-content{position:relative;line-height:22px;min-width:12px;padding:8px 15px;font-size:12px;_float:left;border-radius:2px;box-shadow:1px 1px 3px rgba(0,0,0,.2);background-color:#000;color:#fff}.layui-layer-tips .layui-layer-close{right:-2px;top:-1px}.layui-layer-tips i.layui-layer-TipsG{position:absolute;width:0;height:0;border-width:8px;border-color:transparent;border-style:dashed;*overflow:hidden}.layui-layer-tips i.layui-layer-TipsB,.layui-layer-tips i.layui-layer-TipsT{left:5px;border-right-style:solid;border-right-color:#000}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsL,.layui-layer-tips i.layui-layer-TipsR{top:5px;border-bottom-style:solid;border-bottom-color:#000}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{background:#4476A7;color:#fff;border:none}.layui-layer-lan .layui-layer-btn{padding:5px 10px 10px;text-align:right;border-top:1px solid #E9E7E7}.layui-layer-lan .layui-layer-btn a{background:#fff;border-color:#E9E7E7;color:#333}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#C9C5C5}.layui-layer-molv .layui-layer-title{background:#009f95;color:#fff;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95;border-color:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92B8B1}.layui-layer-iconext{background:url(icon-ext.png) no-repeat}.layui-layer-prompt .layui-layer-input{display:block;width:230px;height:36px;margin:0 auto;line-height:30px;padding-left:10px;border:1px solid #e6e6e6;color:#333}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px;padding:6px 10px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px rgba(0,0,0,.4)}.layui-layer-tab .layui-layer-title{padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{position:relative;float:left;min-width:80px;max-width:260px;padding:0 20px;text-align:center;overflow:hidden;cursor:pointer}.layui-layer-tab .layui-layer-title span.layui-this{height:43px;border-left:1px solid #eee;border-right:1px solid #eee;background-color:#fff;z-index:10}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{line-height:24px;clear:both}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.layui-this{display:block}.layui-layer-photos{-webkit-animation-duration:.8s;animation-duration:.8s}.layui-layer-photos .layui-layer-content{overflow:hidden;text-align:center}.layui-layer-photos .layui-layer-phimg img{position:relative;width:100%;display:inline-block;*display:inline;*zoom:1;vertical-align:top}.layui-layer-imgbar,.layui-layer-imguide{display:none}.layui-layer-imgnext,.layui-layer-imgprev{position:absolute;top:50%;width:27px;_width:44px;height:44px;margin-top:-22px;outline:0;blr:expression(this.onFocus=this.blur())}.layui-layer-imgprev{left:10px;background-position:-5px -5px;_background-position:-70px -5px}.layui-layer-imgprev:hover{background-position:-33px -5px;_background-position:-120px -5px}.layui-layer-imgnext{right:10px;_right:8px;background-position:-5px -50px;_background-position:-70px -50px}.layui-layer-imgnext:hover{background-position:-33px -50px;_background-position:-120px -50px}.layui-layer-imgbar{position:absolute;left:0;bottom:0;width:100%;height:32px;line-height:32px;background-color:rgba(0,0,0,.8);background-color:#000\9;filter:Alpha(opacity=80);color:#fff;overflow:hidden;font-size:0}.layui-layer-imgtit *{display:inline-block;*display:inline;*zoom:1;vertical-align:top;font-size:12px}.layui-layer-imgtit a{max-width:65%;overflow:hidden;color:#fff}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@-webkit-keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes layer-bounceOut{100%{opacity:0;-webkit-transform:scale(.7);-ms-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1.05);-ms-transform:scale(1.05);transform:scale(1.05)}0%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}.layer-anim-close{-webkit-animation-name:layer-bounceOut;animation-name:layer-bounceOut;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@media screen and (max-width:1100px){.layui-layer-iframe{overflow-y:auto;-webkit-overflow-scrolling:touch}} ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.css ================================================ *{margin: 0; padding: 0;} body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;} ::-webkit-input-placeholder{color: #ccc;} /* 视图盒子 */ .view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;} /* 背景 EAEFF3 */ .bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);} .bg-2{height: 50%; background-color: #EAEFF3;} /* 渐变背景 */ .bg-1{ background-size: 500%; background-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5); animation: bganimation 30s infinite; } @keyframes bganimation{ 0%{background-position: 0% 50%;} 50%{background-position: 100% 50%;} 100%{background-position: 0% 50%;} } /* 内容盒子 */ .content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;} /* 登录盒子 */ /* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */ .login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;} .login-box{display: flex; align-items: center; text-align: center;} /* 表单 */ .from-box{flex: 1; padding: 20px 50px; background-color: #FFF;} .from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;} .from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;} /* 输入框 */ .from-item{border: 0px #000 solid; margin-bottom: 15px;} .s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;} .s-input{font-size: 12px;} .s-input:focus{border-color: #409eff} /* 登录按钮 */ .s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;} .s-btn:hover{background-color: #50aEFF;} /* 重置按钮 */ .reset-box{text-align: left; font-size: 12px;} .reset-box a{text-decoration: none;} .reset-box a:hover{text-decoration: underline;} /* loading框样式 */ .ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);} .ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;} .ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; } ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.js ================================================ // sa var sa = {}; // 打开loading sa.loading = function(msg) { layer.closeAll(); // 开始前先把所有弹窗关了 return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' }); }; // 隐藏loading sa.hideLoading = function() { layer.closeAll(); }; // ----------------------------------- 登录事件 ----------------------------------- $('.login-btn').click(function(){ sa.loading("正在登录..."); // 开始登录 setTimeout(function() { $.ajax({ url: "doLogin", type: "post", data: { name: $('[name=name]').val(), pwd: $('[name=pwd]').val() }, dataType: 'json', success: function(res){ console.log('返回数据:', res); sa.hideLoading(); if(res.code == 200) { layer.msg('登录成功', {anim: 0, icon: 6 }); setTimeout(function() { location.reload(); }, 800) } else { layer.msg(res.msg, {anim: 6, icon: 2 }); } }, error: function(xhr, type, errorThrown){ sa.hideLoading(); if(xhr.status == 0){ return layer.alert('无法连接到服务器,请检查网络'); } return layer.alert("异常:" + JSON.stringify(xhr)); } }); }, 400); }); // 绑定回车事件 $('[name=name],[name=pwd]').bind('keypress', function(event){ if(event.keyCode == "13") { $('.login-btn').click(); } }); // 输入框获取焦点 $("[name=name]").focus(); // 打印信息 var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/"; console.log(str); ================================================ FILE: sa-token-plugin/sa-token-quick-login/src/main/resources/templates/sa-login.html ================================================ 登录
    This page is provided by Sa-Token
    ================================================ FILE: sa-token-plugin/sa-token-redis-jackson/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-redis-jackson sa-token-redis-jackson sa-token integrate redis (to jackson) cn.dev33 sa-token-jackson cn.dev33 sa-token-redis-template ================================================ FILE: sa-token-plugin/sa-token-redis-template/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-redis-template sa-token-redis-template sa-token integrate RedisTemplate cn.dev33 sa-token-core org.springframework.boot spring-boot-starter-data-redis ================================================ FILE: sa-token-plugin/sa-token-redis-template/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Sa-Token 持久层实现 [ Redis 存储 ] (可用环境: SpringBoot2、SpringBoot3) * * @author click33 * @since 1.34.0 */ public class SaTokenDaoForRedisTemplate implements SaTokenDaoByObjectFollowString, SaTokenDao { public StringRedisTemplate stringRedisTemplate; /** * 标记:当前 redis 连接信息是否已初始化成功 */ public boolean isInit; @Autowired public void init(RedisConnectionFactory connectionFactory) { // 如果已经初始化成功了,就立刻退出,不重复初始化 if(this.isInit) { return; } // 构建StringRedisTemplate StringRedisTemplate stringTemplate = new StringRedisTemplate(); stringTemplate.setConnectionFactory(connectionFactory); stringTemplate.afterPropertiesSet(); this.stringRedisTemplate = stringTemplate; initMore(connectionFactory); // 打上标记,表示已经初始化成功,后续无需再重新初始化 this.isInit = true; } protected void initMore(RedisConnectionFactory connectionFactory) { } /** * 获取Value,如无返空 */ @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } /** * 写入Value,并设定存活时间 (单位: 秒) */ @Override public void set(String key, String value, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if(timeout == SaTokenDao.NEVER_EXPIRE) { stringRedisTemplate.opsForValue().set(key, value); } else { stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } } /** * 修改指定key-value键值对 (过期时间不变) */ @Override public void update(String key, String value) { @SuppressWarnings("all") long expireMs = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS); // -2 = 无此键 if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) { return; } // -1 = 永不过期 if(expireMs == SaTokenDao.NEVER_EXPIRE) { stringRedisTemplate.opsForValue().set(key, value); } else { stringRedisTemplate.opsForValue().set(key, value, expireMs, TimeUnit.MILLISECONDS); } } /** * 删除Value */ @Override public void delete(String key) { stringRedisTemplate.delete(key); } /** * 获取Value的剩余存活时间 (单位: 秒) */ @Override public long getTimeout(String key) { return stringRedisTemplate.getExpire(key); } /** * 修改Value的剩余存活时间 (单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { // 判断是否想要设置为永久 if(timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if(expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } /** * 搜索数据 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Set keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*"); List list = new ArrayList<>(keys); return SaFoxUtil.searchList(list, start, size, sortType); } } ================================================ FILE: sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate ================================================ FILE: sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate ================================================ FILE: sa-token-plugin/sa-token-redis-template-jdk-serializer/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-redis-template-jdk-serializer sa-token-redis-template-jdk-serializer sa-token integrate RedisTemplate (jdk-serializer) cn.dev33 sa-token-core org.springframework.boot spring-boot-starter-data-redis ================================================ FILE: sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Sa-Token 持久层实现 [ RedisTemplate 存储 ] (可用环境: SpringBoot2、SpringBoot3) *
    copy by: sa-token-redis-template 插件 * * @author click33 * @since 1.34.0 */ public class SaTokenDaoForRedisTemplate implements SaTokenDaoByObjectFollowString, SaTokenDao { public StringRedisTemplate stringRedisTemplate; /** * 标记:当前 redis 连接信息是否已初始化成功 */ public boolean isInit; @Autowired public void init(RedisConnectionFactory connectionFactory) { // 如果已经初始化成功了,就立刻退出,不重复初始化 if(this.isInit) { return; } // 构建StringRedisTemplate StringRedisTemplate stringTemplate = new StringRedisTemplate(); stringTemplate.setConnectionFactory(connectionFactory); stringTemplate.afterPropertiesSet(); this.stringRedisTemplate = stringTemplate; initMore(connectionFactory); // 打上标记,表示已经初始化成功,后续无需再重新初始化 this.isInit = true; } protected void initMore(RedisConnectionFactory connectionFactory) { } /** * 获取Value,如无返空 */ @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } /** * 写入Value,并设定存活时间 (单位: 秒) */ @Override public void set(String key, String value, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if(timeout == SaTokenDao.NEVER_EXPIRE) { stringRedisTemplate.opsForValue().set(key, value); } else { stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } } /** * 修改指定key-value键值对 (过期时间不变) */ @Override public void update(String key, String value) { @SuppressWarnings("all") long expireMs = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS); // -2 = 无此键 if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) { return; } // -1 = 永不过期 if(expireMs == SaTokenDao.NEVER_EXPIRE) { stringRedisTemplate.opsForValue().set(key, value); } else { stringRedisTemplate.opsForValue().set(key, value, expireMs, TimeUnit.MILLISECONDS); } } /** * 删除Value */ @Override public void delete(String key) { stringRedisTemplate.delete(key); } /** * 获取Value的剩余存活时间 (单位: 秒) */ @Override public long getTimeout(String key) { return stringRedisTemplate.getExpire(key); } /** * 修改Value的剩余存活时间 (单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { // 判断是否想要设置为永久 if(timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if(expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } /** * 搜索数据 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Set keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*"); List list = new ArrayList<>(keys); return SaFoxUtil.searchList(list, start, size, sortType); } } ================================================ FILE: sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplateUseJdkSerializer.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.concurrent.TimeUnit; /** * Sa-Token 持久层实现 [ RedisTemplate 存储、JDK默认序列化 ] (可用环境: SpringBoot2、SpringBoot3) * * @author click33 * @since 1.34.0 */ public class SaTokenDaoForRedisTemplateUseJdkSerializer extends SaTokenDaoForRedisTemplate implements SaTokenDao { /** * Object 读写专用 */ public RedisTemplate objectRedisTemplate; @Override protected void initMore(RedisConnectionFactory connectionFactory) { // 指定相应的序列化方案 StringRedisSerializer keySerializer = new StringRedisSerializer(); JdkSerializationRedisSerializer valueSerializer = new JdkSerializationRedisSerializer(); // 构建RedisTemplate RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); this.objectRedisTemplate = template; } /** * 获取Object,如无返空 */ @Override public Object getObject(String key) { return objectRedisTemplate.opsForValue().get(key); } /** * 获取 Object (指定反序列化类型),如无返空 * * @param key 键名称 * @return object */ @SuppressWarnings("unchecked") @Override public T getObject(String key, Class classType) { return (T) objectRedisTemplate.opsForValue().get(key); } /** * 写入Object,并设定存活时间 (单位: 秒) */ @Override public void setObject(String key, Object object, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if(timeout == SaTokenDao.NEVER_EXPIRE) { objectRedisTemplate.opsForValue().set(key, object); } else { objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS); } } /** * 更新Object (过期时间不变) */ @Override public void updateObject(String key, Object object) { @SuppressWarnings("all") long expireMs = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS); // -2 = 无此键 if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) { return; } // -1 = 永不过期 if(expireMs == SaTokenDao.NEVER_EXPIRE) { objectRedisTemplate.opsForValue().set(key, object); } else { objectRedisTemplate.opsForValue().set(key, object, expireMs, TimeUnit.MILLISECONDS); } } /** * 删除Object */ @Override public void deleteObject(String key) { objectRedisTemplate.delete(key); } /** * 获取Object的剩余存活时间 (单位: 秒) */ @Override public long getObjectTimeout(String key) { return objectRedisTemplate.getExpire(key); } /** * 修改Object的剩余存活时间 (单位: 秒) */ @Override public void updateObjectTimeout(String key, long timeout) { // 判断是否想要设置为永久 if(timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getObjectTimeout(key); if(expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.setObject(key, this.getObject(key), timeout); } return; } objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } } ================================================ FILE: sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer ================================================ FILE: sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer ================================================ FILE: sa-token-plugin/sa-token-redisson/README.md ================================================ ## sa-token-redisson 此扩展,不与生态绑定。可用于不同的生态(SpringBoot,Solon,JFinal等)。 ### 1、例 solon 集成 添加关键依赖 ```xml cn.dev33 sa-token-redisson ${sa-token.version} org.noear redisson-solon-plugin ${solon.version} ``` 添加 dao 配置 ```yaml sa-token-dao: config: | singleServerConfig: password: "123456" address: "redis://localhost:6379" database: 0 ``` 开始组装 ```java @Configuration public class SaTokenConfigure { /** * 构造 RedissonClient * */ @Bean public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) { return supplier.get(); } /** * 构建 SaTokenDao * */ @Bean public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) { return new SaTokenDaoForRedisson(redissonClient); } } ``` ### 2、例 springboot 集成 添加关键依赖 ```xml cn.dev33 sa-token-redisson ${sa-token.version} org.redisson redisson-spring-boot-starter ${redisson.version} ``` 添加 dao 配置 ```yaml spring.redis: redisson: file: classpath:redisson.yml ``` 开始组装 ```java @Configuration public class SaTokenConfigure { /** * 构建 SaTokenDao * */ @Bean public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) { return new SaTokenDaoForRedisson(redissonClient); } } ``` ================================================ FILE: sa-token-plugin/sa-token-redisson/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-redisson sa-token-redisson sa-token integrate Redisson cn.dev33 sa-token-core org.redisson redisson ================================================ FILE: sa-token-plugin/sa-token-redisson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisson.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; import cn.dev33.satoken.util.SaFoxUtil; import org.redisson.api.RBatch; import org.redisson.api.RBucket; import org.redisson.api.RBucketAsync; import org.redisson.api.RedissonClient; import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Sa-Token 持久层实现 [ Redisson客户端、Redis存储 ] * * @author 疯狂的狮子Li * @author noear * @since 1.34.0 */ public class SaTokenDaoForRedisson implements SaTokenDaoByObjectFollowString, SaTokenDao { /** * redisson 客户端 */ public final RedissonClient redissonClient; public SaTokenDaoForRedisson(RedissonClient redissonClient) { this.redissonClient = redissonClient; } /** * 获取Value,如无返空 */ @Override public String get(String key) { RBucket rBucket = redissonClient.getBucket(key); return rBucket.get(); } /** * 写入Value,并设定存活时间 (单位: 秒) */ @Override public void set(String key, String value, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if(timeout == SaTokenDao.NEVER_EXPIRE) { RBucket bucket = redissonClient.getBucket(key); bucket.set(value); } else { RBatch batch = redissonClient.createBatch(); RBucketAsync bucket = batch.getBucket(key); bucket.setAsync(value); bucket.expireAsync(Duration.ofSeconds(timeout)); batch.execute(); } } /** * 修修改指定key-value键值对 (过期时间不变) */ @Override public void update(String key, String value) { long expire = getTimeout(key); // -2 = 无此键 if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.set(key, value, expire); } /** * 删除Value */ @Override public void delete(String key) { redissonClient.getBucket(key).delete(); } /** * 获取Value的剩余存活时间 (单位: 秒) */ @Override public long getTimeout(String key) { RBucket rBucket = redissonClient.getBucket(key); long timeout = rBucket.remainTimeToLive(); return timeout < 0 ? timeout : timeout / 1000; } /** * 修改Value的剩余存活时间 (单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { // 判断是否想要设置为永久 if(timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if(expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } RBucket rBucket = redissonClient.getBucket(key); rBucket.expire(Duration.ofSeconds(timeout)); } /** * 搜索数据 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Stream stream = redissonClient.getKeys().getKeysStreamByPattern(prefix + "*" + keyword + "*"); List list = stream.collect(Collectors.toList()); return SaFoxUtil.searchList(list, start, size, sortType); } } ================================================ FILE: sa-token-plugin/sa-token-redisson-spring-boot-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-redisson-spring-boot-starter sa-token-redisson-spring-boot-starter sa-token integrate redisson (to jackson) cn.dev33 sa-token-core cn.dev33 sa-token-redisson org.redisson redisson-spring-boot-starter ================================================ FILE: sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenDaoForRedissonBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisson; import org.redisson.api.RedissonClient; import org.springframework.context.annotation.Bean; /** * 注册 SaTokenDaoForRedisson Bean * * @author click33 * @since 1.34.0 */ public class SaTokenDaoForRedissonBeanRegister { @Bean public SaTokenDao getSaTokenDaoForRedisson(RedissonClient redissonClient) { return new SaTokenDaoForRedisson(redissonClient); } } ================================================ FILE: sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.spring.SaTokenDaoForRedissonBeanRegister ================================================ FILE: sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.spring.SaTokenDaoForRedissonBeanRegister ================================================ FILE: sa-token-plugin/sa-token-redisx/README.md ================================================ sa-token-redisx 是中立的扩展。可任何应用开发框架下使用(springboot, solon, jfinal 等..) ### 使用示例 #### 1.配置 ```yaml sa-token: #名字可以随意取 redis: server: "localhost:6379" password: 123456 db: 1 # serializer: "org.noear.redisx.utils.SerializerJson" #指定自定义序列化实现(默认为 SerializerDefault) ``` #### 2.代码 **注入风格** ```java @Configuration public class Config { @Bean public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) { return saTokenDao; } } ``` **手动风格** ```java SaTokenDaoOfRedis saTokenDao = new SaTokenDaoOfRedis(props); SaManager.setSaTokenDao(saTokenDao); ``` ================================================ FILE: sa-token-plugin/sa-token-redisx/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-redisx sa-token-redisx sa-token integrate redis cn.dev33 sa-token-core org.noear redisx org.noear solon-test test ================================================ FILE: sa-token-plugin/sa-token-redisx/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisx.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.dao; import cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString; import cn.dev33.satoken.util.SaFoxUtil; import org.noear.redisx.RedisClient; import org.noear.redisx.plus.RedisBucket; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Set; /** * SaTokenDao 的 redis 适配(基于json序列化,不能完全精准还原所有类型) * * @author noear * @since 1.34.0 * @since 1.41.0 */ public class SaTokenDaoForRedisx implements SaTokenDaoByObjectFollowString, SaTokenDao { private final RedisBucket redisBucket; public SaTokenDaoForRedisx(Properties props) { this(new RedisClient(props)); } public SaTokenDaoForRedisx(RedisClient redisClient) { redisBucket = redisClient.getBucket(); } /** * 获取Value,如无返空 */ @Override public String get(String key) { return redisBucket.get(key); } /** * 写入Value,并设定存活时间 (单位: 秒) */ @Override public void set(String key, String value, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if (timeout == SaTokenDao.NEVER_EXPIRE) { redisBucket.store(key, value); } else { redisBucket.store(key, value, (int) timeout); } } /** * 修改指定key-value键值对 (过期时间不变) */ @Override public void update(String key, String value) { long expire = getTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.set(key, value, expire); } /** * 删除Value */ @Override public void delete(String key) { redisBucket.remove(key); } /** * 获取Value的剩余存活时间 (单位: 秒) */ @Override public long getTimeout(String key) { return redisBucket.ttl(key); } /** * 修改Value的剩余存活时间 (单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { // 判断是否想要设置为永久 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if (expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } redisBucket.delay(key, (int) timeout); } /** * 搜索数据 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Set keys = redisBucket.keys(prefix + "*" + keyword + "*"); List list = new ArrayList<>(keys); return SaFoxUtil.searchList(list, start, size, sortType); } } ================================================ FILE: sa-token-plugin/sa-token-redisx/src/test/java/demo/App.java ================================================ package demo; import org.noear.solon.Solon; /** * @author noear 2022/3/30 created */ public class App { public static void main(String[] args) { Solon.start(App.class, args); } } ================================================ FILE: sa-token-plugin/sa-token-redisx/src/test/java/demo/Config.java ================================================ package demo; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisx; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * @author noear 2022/3/30 created */ @Configuration public class Config { @Bean public void saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoForRedisx saTokenDao) { //手动操作,可适用于任何框架 SaManager.setSaTokenDao(saTokenDao); } @Bean public SaTokenDao saTokenDaoInit2(@Inject("${sa-token.redis}") SaTokenDaoForRedisx saTokenDao) { //Solon 项目,可用此案 return saTokenDao; } } ================================================ FILE: sa-token-plugin/sa-token-redisx/src/test/resources/app.yml ================================================ sa-token: #名字可以随意取 redis: server: "localhost:6379" password: 123456 db: 1 maxTotal: 200 ================================================ FILE: sa-token-plugin/sa-token-serializer-features/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-serializer-features sa-token-serializer-features sa-token-serializer-features cn.dev33 sa-token-core ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSerializerFeatures.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; /** * SaToken 插件安装:自定义序列化器 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForSerializerFeatures implements SaTokenPlugin { @Override public void install() { // 默认不注册,需要开发者手动注册去选择 // SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan()); } } ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseCustomCharacters.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer; import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdk; import java.util.HashMap; import java.util.Map; /** * 序列化器,base64 算法,采用自定义字符集 * * @author click33 * @since 1.41.0 */ public class SaSerializerForBase64UseCustomCharacters implements SaSerializerTemplateForJdk { // 自定义字符集,需确保包含64个中文字符 public String CUSTOM_CHARS; // 填充符,确保不在字符集中 public char PAD_CHAR; public SaSerializerForBase64UseCustomCharacters(String customChars, char padChar) { if (customChars.length() != 64) { throw new IllegalArgumentException("自定义字符集长度必须为64"); } if (customChars.indexOf(padChar) != -1) { throw new IllegalArgumentException("填充符不能在自定义字符集中"); } this.CUSTOM_CHARS = customChars; this.PAD_CHAR = padChar; } @Override public String bytesToString(byte[] data) { StringBuilder encoded = new StringBuilder(); int length = data.length; int i = 0; // 处理完整的3字节组 while (i < length - 2) { int byte1 = data[i++] & 0xFF; int byte2 = data[i++] & 0xFF; int byte3 = data[i++] & 0xFF; int combined = (byte1 << 16) | (byte2 << 8) | byte3; encoded.append(CUSTOM_CHARS.charAt((combined >> 18) & 0x3F)); encoded.append(CUSTOM_CHARS.charAt((combined >> 12) & 0x3F)); encoded.append(CUSTOM_CHARS.charAt((combined >> 6) & 0x3F)); encoded.append(CUSTOM_CHARS.charAt(combined & 0x3F)); } // 处理剩余字节(0、1或2个) int remaining = length - i; if (remaining > 0) { int byte1 = data[i++] & 0xFF; int byte2 = remaining > 1 ? data[i++] & 0xFF : 0; int combined = (byte1 << 16) | (byte2 << 8); encoded.append(CUSTOM_CHARS.charAt((combined >> 18) & 0x3F)); encoded.append(CUSTOM_CHARS.charAt((combined >> 12) & 0x3F)); if (remaining == 1) { encoded.append(PAD_CHAR).append(PAD_CHAR); } else { encoded.append(CUSTOM_CHARS.charAt((combined >> 6) & 0x3F)); encoded.append(PAD_CHAR); } } return encoded.toString(); } @Override public byte[] stringToBytes(String encodedStr) { if (CUSTOM_CHARS.length() != 64) { throw new IllegalStateException("自定义字符集长度必须为64"); } Map charMap = new HashMap<>(); for (int i = 0; i < CUSTOM_CHARS.length(); i++) { charMap.put(CUSTOM_CHARS.charAt(i), i); } int length = encodedStr.length(); if (length % 4 != 0) { throw new IllegalArgumentException("编码字符串长度无效"); } // 计算填充符数量 int paddingCount = 0; for (int i = length - 1; i >= 0 && encodedStr.charAt(i) == PAD_CHAR; i--) { paddingCount++; } int numGroups = length / 4; byte[] decoded = new byte[numGroups * 3 - paddingCount]; int decodedIndex = 0; for (int group = 0; group < numGroups; group++) { int[] indices = new int[4]; for (int j = 0; j < 4; j++) { char c = encodedStr.charAt(group * 4 + j); if (c == PAD_CHAR) { indices[j] = 0; // 填充符处理为0,后续根据paddingCount调整 } else { Integer index = charMap.get(c); if (index == null) { throw new IllegalArgumentException("无效字符: " + c); } indices[j] = index; } } int combined = (indices[0] << 18) | (indices[1] << 12) | (indices[2] << 6) | indices[3]; for (int k = 0; k < 3; k++) { if (decodedIndex < decoded.length) { decoded[decodedIndex++] = (byte) ((combined >> (16 - 8 * k)) & 0xFF); } } } return decoded; } } ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseEmoji.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer; import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdk; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 序列化器,base64 算法,采用 64 个 Emoji 小黄脸作为元字符集,无填充字符 * * @author click33 * @since 1.41.0 */ public class SaSerializerForBase64UseEmoji implements SaSerializerTemplateForJdk { private final List EMOJI_TABLE = new ArrayList<>(); // 编码表 private final Map EMOJI_MAP = new HashMap<>(); // 解码表 public SaSerializerForBase64UseEmoji() { // 初始化编码表(64个Emoji,U+1F600 到 U+1F63F) for (int i = 0; i < 64; i++) { int codePoint = 0x1F600 + i; String emoji = new String(Character.toChars(codePoint)); EMOJI_TABLE.add(emoji); EMOJI_MAP.put(emoji, i); } } @Override public String bytesToString(byte[] data) { StringBuilder binaryStr = new StringBuilder(); for (byte b : data) { binaryStr.append(String.format("%8s", Integer.toBinaryString(b & 0xFF)) .replace(' ', '0')); } // 补零到6的倍数 int bitLength = binaryStr.length(); int paddingBits = (6 - (bitLength % 6)) % 6; for (int i = 0; i < paddingBits; i++) { binaryStr.append('0'); } // 转换为索引 List indices = new ArrayList<>(); for (int i = 0; i < binaryStr.length(); i += 6) { String chunk = binaryStr.substring(i, Math.min(i + 6, binaryStr.length())); indices.add(Integer.parseInt(chunk, 2)); } // 拼接Emoji StringBuilder result = new StringBuilder(); for (int index : indices) { result.append(EMOJI_TABLE.get(index)); } return result.toString(); } @Override public byte[] stringToBytes(String encoded) { List indices = new ArrayList<>(); // 提取索引(每个Emoji占2个char) for (int i = 0; i < encoded.length(); ) { if (i + 1 >= encoded.length()) break; String emoji = encoded.substring(i, i + 2); i += 2; Integer index = EMOJI_MAP.get(emoji); if (index == null) { throw new IllegalArgumentException("非法Emoji: " + emoji); } indices.add(index); } // 转换为二进制字符串 StringBuilder binaryStr = new StringBuilder(); for (int index : indices) { binaryStr.append(String.format("%6s", Integer.toBinaryString(index)) .replace(' ', '0')); } // 转换为字节数组(自动处理末尾补零) List bytes = new ArrayList<>(); for (int i = 0; i < binaryStr.length(); i += 8) { int endIndex = Math.min(i + 8, binaryStr.length()); String byteStr = binaryStr.substring(i, endIndex); if (byteStr.length() < 8) break; // 忽略末尾不足8位的部分 bytes.add((byte) Integer.parseInt(byteStr, 2)); } byte[] result = new byte[bytes.size()]; for (int i = 0; i < bytes.size(); i++) { result[i] = bytes.get(i); } return result; } } ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UsePeriodicTable.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer; /** * 序列化器,base64 算法,采用 元素周期表 前六十四位作为元字符集 * * @author click33 * @since 1.41.0 */ public class SaSerializerForBase64UsePeriodicTable extends SaSerializerForBase64UseCustomCharacters { public SaSerializerForBase64UsePeriodicTable() { super( // 自定义字符集,需确保包含64个不重复的字符 "氢氦锂铍硼碳氮氧" + "氟氖钠镁铝硅磷硫" + "氯氩钾钙钪钛钒铬" + "锰铁钴镍铜锌镓锗" + "砷硒溴氪铷锶钇锆" + "铌钼锝钌铑钯银镉" + "铟锡锑碲碘氙铯钡" + "镧铈镨钕钷钐铕钆" , // 填充符,确保不在字符集中 '鿫' ); } } ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseSpecialSymbols.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer; /** * 序列化器,base64 算法,采用64个特殊符号作为元字符集 * * @author click33 * @since 1.41.0 */ public class SaSerializerForBase64UseSpecialSymbols extends SaSerializerForBase64UseCustomCharacters { public SaSerializerForBase64UseSpecialSymbols() { super( // 自定义字符集,需确保包含64个不重复的字符 "▲▼●◆■★▶◀" + "♠♥♦♣▁▂▃▄" + "▅▆▇█▏▎▍▌" + "▋▊▉▬〓◤◥◣" + "◢♩♪♫♬§〼↖" + "↑↗←→↙↓↘☴" + "☲☷☳☱☶☵☰◐" + "◑☀☼▪•‥…∷" , // 填充符,确保不在字符集中 '※' ); } } ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseTianGan.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.serializer; /** * 序列化器,base64 算法,采用 十大天干、十二地支 等64个中文字符作为元字符集 * * @author click33 * @since 1.41.0 */ public class SaSerializerForBase64UseTianGan extends SaSerializerForBase64UseCustomCharacters { public SaSerializerForBase64UseTianGan() { super( // 自定义字符集,需确保包含64个不重复的字符 "甲乙丙丁戊己庚辛" + "壬癸子丑寅卯辰巳" + "午未申酉戌亥乾坤" + "震巽坎离艮兑金木" + "水火土天地日月山" + "石田风雷电霜雾露" + "东南西北中信谷岚" + "宇宙羽泰铭安鹤纤" , // 填充符,确保不在字符集中 '口' ); } } ================================================ FILE: sa-token-plugin/sa-token-serializer-features/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForSerializerFeatures ================================================ FILE: sa-token-plugin/sa-token-sign/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-sign sa-token-sign sa-token Sign cn.dev33 sa-token-core ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSign.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.sign.annotation.handle.SaCheckSignHandler; import cn.dev33.satoken.strategy.SaAnnotationStrategy; /** * SaToken 插件安装:API 参数签名 组件 * * @author click33 * @since 1.43.0 */ public class SaTokenPluginForSign implements SaTokenPlugin { @Override public void install() { // 安装 API 参数签名 鉴权注解 SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckSignHandler()); } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/SaSignManager.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.template.SaSignTemplate; import java.util.LinkedHashMap; import java.util.Map; /** * 管理 Sa-Token API 参数签名 所有全局组件 * * @author click33 * @since 1.43.0 */ public class SaSignManager { /** * API 参数签名 配置 Bean */ private static volatile SaSignConfig config; public static SaSignConfig getConfig() { if (config == null) { // 初始化默认值 synchronized (SaSignManager.class) { if (config == null) { setConfig(new SaSignConfig()); } } } return config; } public static void setConfig(SaSignConfig config) { SaSignManager.config = config; } /** * API 签名配置 多实例 配置 Bean */ private static volatile Map signMany; public static Map getSignMany() { if (signMany == null) { // 初始化默认值 synchronized (SaSignManager.class) { if (signMany == null) { setSignMany(new LinkedHashMap<>()); } } } return signMany; } public static void setSignMany(Map signMany) { SaSignManager.signMany = signMany; } /** * API 参数签名 */ private volatile static SaSignTemplate saSignTemplate; public static void setSaSignTemplate(SaSignTemplate saSignTemplate) { SaSignManager.saSignTemplate = saSignTemplate; SaTokenEventCenter.doRegisterComponent("SaSignTemplate", saSignTemplate); } public static SaSignTemplate getSaSignTemplate() { if (saSignTemplate == null) { synchronized (SaManager.class) { if (saSignTemplate == null) { SaSignManager.saSignTemplate = new SaSignTemplate(); } } } return saSignTemplate; } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/SaCheckSign.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * API 参数签名校验:必须具有正确的参数签名才可以通过校验 * *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.41.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface SaCheckSign { /** * 多实例下的 appid 值,用于区分不同的实例,如不填写则代表使用全局默认实例
    * 允许以 #{} 的形式指定为请求参数,如:#{appid} * * @return / */ String appid() default ""; /** * 指定参与签名的参数有哪些,如果不填写则默认为全部参数 * * @return / */ String [] verifyParams() default {}; } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/handle/SaCheckSignHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.annotation.handle; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.sign.annotation.SaCheckSign; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.sign.template.SaSignMany; import java.lang.reflect.AnnotatedElement; /** * 注解 SaCheckSign 的处理器 * * @author click33 * @since 1.41.0 */ public class SaCheckSignHandler implements SaAnnotationHandlerInterface { @Override public Class getHandlerAnnotationClass() { return SaCheckSign.class; } @Override public void checkMethod(SaCheckSign at, AnnotatedElement element) { _checkMethod(at.appid(), at.verifyParams()); } public static void _checkMethod(String appid, String[] verifyParams) { SaRequest req = SaHolder.getRequest(); // 如果 appid 为 #{} 格式,则从请求参数中获取 if(appid.startsWith("#{") && appid.endsWith("}")) { String reqParamName = appid.substring(2, appid.length() - 1); appid = req.getParam(reqParamName); } SaSignMany.getSignTemplate(appid).checkRequest(req, verifyParams); } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.config; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.fun.SaParamRetFunction; import cn.dev33.satoken.secure.SaSecureUtil; /** * Sa-Token API 接口签名/验签 相关配置类 * * @author click33 * @since 1.34.0 */ public class SaSignConfig { /** * API 调用签名秘钥 */ private String secretKey; /** * 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 * *

    比如此处你配置了60秒,当一个请求从 client 发起后,如果 server 端60秒内没有处理,60秒后再想处理就无法校验通过了。

    *

    timestamp + nonce 有效防止重放攻击。

    */ private long timestampDisparity = 1000 * 60 * 15; /** * 对 fullStr 的摘要算法 */ private String digestAlgo = "md5"; public SaSignConfig() { } /** * 构造函数 * @param secretKey 秘钥 */ public SaSignConfig(String secretKey) { this.secretKey = secretKey; } // -------------- 扩展方法 /** * 计算保存 nonce 时应该使用的 ttl,单位:秒 * @return / */ public long getSaveNonceExpire() { // 如果 timestampDisparity >= 0,则 nonceTtl 的值等于 timestampDisparity 的值,单位转秒 if(timestampDisparity >= 0) { return timestampDisparity / 1000; } // 否则,nonceTtl 的值为 24 小时 else { return 60 * 60 * 24; } } /** * 复制对象 * @return / */ public SaSignConfig copy() { SaSignConfig obj = new SaSignConfig(); obj.secretKey = this.secretKey; obj.timestampDisparity = this.timestampDisparity; obj.digestAlgo = this.digestAlgo; obj.digestMethod = this.digestMethod; return obj; } // -------------- 策略函数 /** * 对 fullStr 的摘要算法函数 */ public SaParamRetFunction digestMethod = (fullStr) -> { // md5 if(digestAlgo.equalsIgnoreCase("md5")) { return SaSecureUtil.md5(fullStr); } // sha1 if(digestAlgo.equalsIgnoreCase("sha1")) { return SaSecureUtil.sha1(fullStr); } // sha256 if(digestAlgo.equalsIgnoreCase("sha256")) { return SaSecureUtil.sha256(fullStr); } // sha384 if(digestAlgo.equalsIgnoreCase("sha384")) { return SaSecureUtil.sha384(fullStr); } // sha512 if(digestAlgo.equalsIgnoreCase("sha512")) { return SaSecureUtil.sha512(fullStr); } // 未知 throw new SaTokenException("不支持的摘要算法:" + digestAlgo + ",你可以自定义摘要算法函数实现"); }; /** * 设置: 对 fullStr 的摘要算法函数 * * @param digestMethod / * @return 对象自身 */ public SaSignConfig setDigestMethod(SaParamRetFunction digestMethod) { this.digestMethod = digestMethod; return this; } // -------------- get/set /** * 获取 API 调用签名秘钥 * * @return / */ public String getSecretKey() { return this.secretKey; } /** * 设置 API 调用签名秘钥 * * @param secretKey / * @return 对象自身 */ public SaSignConfig setSecretKey(String secretKey) { this.secretKey = secretKey; return this; } /** * 获取 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 * *

    比如此处你配置了60秒,当一个请求从 client 发起后,如果 server 端60秒内没有处理,60秒后再想处理就无法校验通过了。

    *

    timestamp + nonce 有效防止重放攻击。

    * * @return / */ public long getTimestampDisparity() { return this.timestampDisparity; } /** * 设置 接口调用时的时间戳允许的差距(单位:ms),-1 代表不校验差距,默认15分钟 * *

    比如此处你配置了60秒,当一个请求从 client 发起后,如果 server 端60秒内没有处理,60秒后再想处理就无法校验通过了。

    *

    timestamp + nonce 有效防止重放攻击。

    * * @param timestampDisparity / * @return 对象自身 */ public SaSignConfig setTimestampDisparity(long timestampDisparity) { this.timestampDisparity = timestampDisparity; return this; } /** * 获取 对 fullStr 的摘要算法 * * @return digestAlgo 对 fullStr 的摘要算法 */ public String getDigestAlgo() { return this.digestAlgo; } /** * 设置 对 fullStr 的摘要算法 * @param digestAlgo / * @return / */ public SaSignConfig setDigestAlgo(String digestAlgo) { this.digestAlgo = digestAlgo; return this; } @Override public String toString() { return "SaSignConfig [" + "secretKey=" + secretKey + ", timestampDisparity=" + timestampDisparity + "]"; } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignManyConfigWrapper.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.config; import java.util.LinkedHashMap; import java.util.Map; /** * SaSignManyConfig 配置包装类,以更方便框架完成属性注入操作 * * @author click33 * @since 1.43.0 */ public class SaSignManyConfigWrapper { public Map signMany = new LinkedHashMap<>(); /** * 获取 * * @return signMany */ public Map getSignMany() { return this.signMany; } /** * 设置 * * @param signMany */ public void setSignMany(Map signMany) { this.signMany = signMany; } @Override public String toString() { return "SaSignManyConfigWrapper{" + "signMany=" + signMany + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/error/SaSignErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.error; /** * 定义 sa-token-sign 模块所有异常细分状态码 * * @author click33 * @since 1.43.0 */ public interface SaSignErrorCode { /** 参与参数签名的秘钥不可为空 */ int CODE_12201 = 12201; /** 给定的签名无效 */ int CODE_12202 = 12202; /** timestamp 超出允许的范围 */ int CODE_12203 = 12203; /** 未找到对应 appid 的 SaSignConfig */ int CODE_12211 = 12211; } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/exception/SaSignException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.exception; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; /** * 一个异常:代表 API 参数签名校验失败 * * @author click33 * @since 1.34.0 */ public class SaSignException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130144L; /** * 一个异常:代表 API 参数签名校验失败 * @param message 异常描述 */ public SaSignException(String message) { super(message); } /** * 断言 flag 不为 true,否则抛出 message 异常 * @param flag 表达式 * @param message 异常信息 */ public static void notTrue(boolean flag, String message) { // notTrue if(flag) { throw new SaSignException(message); } } /** * 断言 value 不为空,否则抛出 message 异常 * @param value 值 * @param message 异常信息 */ public static void notEmpty(Object value, String message) { if(SaFoxUtil.isEmpty(value)) { throw new SaSignException(message); } } // ------------------- 已过期 ------------------- /** * 如果flag==true,则抛出message异常 *

    已过期:请使用 notTrue 代替,用法不变

    * * @param flag 标记 * @param message 异常信息 */ @Deprecated public static void throwBy(boolean flag, String message) { if(flag) { throw new SaSignException(message); } } /** * 如果 value isEmpty,则抛出 message 异常 *

    已过期:请使用 notEmpty 代替,用法不变

    * * @param value 值 * @param message 异常信息 */ @Deprecated public static void throwByNull(Object value, String message) { if(SaFoxUtil.isEmpty(value)) { throw new SaSignException(message); } } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignMany.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.template; import cn.dev33.satoken.fun.SaParamRetFunction; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.error.SaSignErrorCode; import cn.dev33.satoken.sign.exception.SaSignException; import cn.dev33.satoken.util.SaFoxUtil; /** * API 参数签名算法 - 多实例总控类 * * @author click33 * @since 1.41.0 */ public class SaSignMany { /** * 根据 appid 获取 SaSignConfig,允许自定义 */ public static SaParamRetFunction findSaSignConfigMethod = (appid) -> { return SaSignManager.getSignMany().get(appid); }; /** * 获取 SaSignTemplate,根据 appid * @param appid / * @return / */ public static SaSignTemplate getSignTemplate(String appid) { // appid 为空,返回全局默认 SaSignTemplate if(SaFoxUtil.isEmpty(appid)){ return SaSignManager.getSaSignTemplate(); } // 获取 SaSignConfig SaSignConfig config = findSaSignConfigMethod.run(appid); if(config == null){ throw new SaSignException("未找到签名配置,appid=" + appid).setCode(SaSignErrorCode.CODE_12211); } // 创建 SaSignTemplate 并返回 return new SaSignTemplate(config); } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.template; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.sign.error.SaSignErrorCode; import cn.dev33.satoken.sign.exception.SaSignException; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.util.SaFoxUtil; import java.util.Map; import java.util.TreeMap; import static cn.dev33.satoken.SaManager.log; /** * API 参数签名算法,在跨系统接口调用时防参数篡改、防重放攻击。 * *

    * 以 SSO 数据拉取为例,流程大致如下: *
    1. 以 md5( loginId={账号id}8nonce={随机字符串}8timestamp={13位时间戳}8key={secretkey秘钥} ) 生成签名 sign。 *
    2. 将 sign 作为参数,拼接到请求地址后面,如:http://xxx.com?loginId=100018nonce=xxx8timestamp=xxx8sign=xxx。 *
    3. 服务端接收到请求后,以同样的算法生成一次 sign 。 *
    4. 对比两次 sign 是否一致,一致则通过,否则拒绝 。 *

    * * @author click33 * @since 1.30.0 */ public class SaSignTemplate { public SaSignTemplate() { } /** * 构造函数 * @param signConfig 签名参数配置对象 */ public SaSignTemplate(SaSignConfig signConfig) { this.signConfig = signConfig; } // ----------- 签名配置 SaSignConfig signConfig; /** * 获取:API 签名配置 * @return / */ public SaSignConfig getSignConfig() { return signConfig; } /** * 获取:API 签名配置: * 1. 如果用户自定义了 signConfig ,则使用用户自定义的。 * 2. 否则使用全局默认配置。 * * @return / */ public SaSignConfig getSignConfigOrGlobal() { // 如果用户自定义了 signConfig ,则使用用户自定义的 if(signConfig != null) { return signConfig; } // 否则使用全局默认配置 return SaSignManager.getConfig(); } /** * 获取:API 签名配置的秘钥 * @return / */ public String getSecretKey() { return getSignConfigOrGlobal().getSecretKey(); } /** * 设置:API 签名配置 * @param signConfig / */ public SaSignTemplate setSignConfig(SaSignConfig signConfig) { this.signConfig = signConfig; return this; } // ----------- 自定义使用的参数名称 (不声明final,允许开发者自定义修改) public static String key = "key"; public static String timestamp = "timestamp"; public static String nonce = "nonce"; public static String sign = "sign"; // ----------- 拼接参数 /** * 将所有参数连接成一个字符串(不排序),形如:b=28a=18c=3 * @param paramsMap 参数列表 * @return 拼接出的参数字符串 */ public String joinParams(Map paramsMap) { // 按照 k1=v1&k2=v2&k3=v3 排列 StringBuilder sb = new StringBuilder(); for (String key : paramsMap.keySet()) { Object value = paramsMap.get(key); if( ! SaFoxUtil.isEmpty(value) ) { sb.append(key).append("=").append(value).append("&"); } } // 删除最后一位 & if(sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } // . return sb.toString(); } /** * 将所有参数按照字典顺序连接成一个字符串,形如:a=18b=28c=3 * @param paramsMap 参数列表 * @return 拼接出的参数字符串 */ public String joinParamsDictSort(Map paramsMap) { // 保证字段按照字典顺序排列 if( ! (paramsMap instanceof TreeMap) ) { paramsMap = new TreeMap<>(paramsMap); } // 拼接 return joinParams(paramsMap); } // ----------- 创建签名 /** * 创建签名:md5(paramsStr + keyStr) * @param paramsMap 参数列表 * @return 签名 */ public String createSign(Map paramsMap) { String secretKey = getSecretKey(); SaSignException.notEmpty(secretKey, "参与参数签名的秘钥不可为空", SaSignErrorCode.CODE_12201); // 如果调用者不小心传入了 sign 参数,则此处需要将 sign 参数排除在外 if(paramsMap.containsKey(sign)) { // 为了保证不影响原有的 paramsMap,此处需要再复制一份 paramsMap = new TreeMap<>(paramsMap); paramsMap.remove(sign); } // 计算签名 String paramsStr = joinParamsDictSort(paramsMap); String fullStr = paramsStr + "&" + key + "=" + secretKey; String signStr = digestFullStr(fullStr); // 输入日志,方便调试 log.debug("fullStr:{}", fullStr); log.debug("signStr:{}", signStr); // 返回 return signStr; } /** * 使用摘要算法创建签名 * @param fullStr 待摘要的字符串 * @return 签名 */ public String digestFullStr(String fullStr) { return getSignConfigOrGlobal().digestMethod.run(fullStr); } /** * 给 paramsMap 追加 timestamp、nonce、sign 三个参数 * @param paramsMap 参数列表 * @return 加工后的参数列表 */ public Map addSignParams(Map paramsMap) { paramsMap.put(timestamp, String.valueOf(System.currentTimeMillis())); paramsMap.put(nonce, SaFoxUtil.getRandomString(32)); paramsMap.put(sign, createSign(paramsMap)); return paramsMap; } /** * 给 paramsMap 追加 timestamp、nonce、sign 三个参数,并转换为参数字符串,形如: * data=xxx8nonce=xxx8timestamp=xxx8sign=xxx * @param paramsMap 参数列表 * @return 加工后的参数列表 转化为的参数字符串 */ public String addSignParamsAndJoin(Map paramsMap) { // 追加参数 paramsMap = addSignParams(paramsMap); // 拼接参数 return joinParams(paramsMap); } // ----------- 校验签名 /** * 判断:指定时间戳与当前时间戳的差距是否在允许的范围内 * @param timestamp 待校验的时间戳 * @return 是否在允许的范围内 */ public boolean isValidTimestamp(long timestamp) { long allowDisparity = getSignConfigOrGlobal().getTimestampDisparity(); long disparity = Math.abs(System.currentTimeMillis() - timestamp); return allowDisparity == -1 || disparity <= allowDisparity; } /** * 校验:指定时间戳与当前时间戳的差距是否在允许的范围内,如果超出则抛出异常 * @param timestamp 待校验的时间戳 */ public void checkTimestamp(long timestamp) { if( ! isValidTimestamp(timestamp) ) { throw new SaSignException("timestamp 超出允许的范围:" + timestamp).setCode(SaSignErrorCode.CODE_12203); } } /** * 判断:随机字符串 nonce 是否有效。 * 注意:同一 nonce 可以被多次判断有效,不会被缓存 * @param nonce 待判断的随机字符串 * @return 是否有效 */ public boolean isValidNonce(String nonce) { // 为空代表无效 if(SaFoxUtil.isEmpty(nonce)) { return false; } // 校验此 nonce 是否已被使用过 String key = splicingNonceSaveKey(nonce); return SaManager.getSaTokenDao().get(key) == null; } /** * 校验:随机字符串 nonce 是否有效,如果无效则抛出异常。 * 注意:同一 nonce 只可以被校验通过一次,校验后将保存在缓存中,再次校验将无法通过 * @param nonce 待校验的随机字符串 */ public void checkNonce(String nonce) { // 为空代表无效 if(SaFoxUtil.isEmpty(nonce)) { throw new SaSignException("nonce 为空,无效"); } // 校验此 nonce 是否已被使用过 String key = splicingNonceSaveKey(nonce); if(SaManager.getSaTokenDao().get(key) != null) { throw new SaSignException("此 nonce 已被使用过,不可重复使用:" + nonce); } // 校验通过后,将此 nonce 保存在缓存中,保证下次校验无法通过 SaManager.getSaTokenDao().set(key, nonce, getSignConfigOrGlobal().getSaveNonceExpire() * 2 + 2); } /** * 判断:给定的参数 生成的签名是否为有效签名 * @param paramsMap 参数列表 * @param sign 待验证的签名 * @return 签名是否有效 */ public boolean isValidSign(Map paramsMap, String sign) { String theSign = createSign(paramsMap); return theSign.equals(sign); } /** * 校验:给定的参数 生成的签名是否为有效签名,如果签名无效则抛出异常 * @param paramsMap 参数列表 * @param sign 待验证的签名 */ public void checkSign(Map paramsMap, String sign) { if( ! isValidSign(paramsMap, sign) ) { throw new SaSignException("无效签名:" + sign).setCode(SaSignErrorCode.CODE_12202); } } /** * 判断:参数列表中的 nonce、timestamp、sign 是否均为合法的 * @param paramMap 待校验的请求参数集合 * @return 是否合法 */ @SuppressWarnings("all") public boolean isValidParamMap(Map paramMap) { // 获取必须的三个参数 String timestampValue = paramMap.get(timestamp); String nonceValue = paramMap.get(nonce); String signValue = paramMap.get(sign); // 参数非空校验 // 配置isCheckNonce=false时,可以不传 nonce if(SaFoxUtil.isEmpty(timestampValue) || SaFoxUtil.isEmpty(signValue)) { return false; } // 三个值的校验必须全部通过 return isValidTimestamp(Long.parseLong(timestampValue)) && isValidNonce(nonceValue) && isValidSign(paramMap, signValue); } /** * 校验:参数列表中的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 * @param paramMap 待校验的请求参数集合 */ public void checkParamMap(Map paramMap) { // 获取必须的三个参数 String timestampValue = paramMap.get(timestamp); String nonceValue = paramMap.get(nonce); String signValue = paramMap.get(sign); // 参数非空校验 SaSignException.notEmpty(timestampValue, "缺少 timestamp 字段"); SaSignException.notEmpty(nonceValue, "缺少 nonce 字段"); SaSignException.notEmpty(signValue, "缺少 sign 字段"); // 依次校验三个参数 checkTimestamp(Long.parseLong(timestampValue)); checkNonce(nonceValue); checkSign(paramMap, signValue); // 通过 √ } // ----------- Web 请求相关 封装 /** * 判断:一个请求中的 nonce、timestamp、sign 是否均为合法的 * @param request 待校验的请求对象 * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 * @return 是否合法 */ public boolean isValidRequest(SaRequest request, String... paramNames) { if(paramNames.length == 0) { return isValidParamMap(request.getParamMap()); } else { return isValidParamMap(takeRequestParam(request, paramNames)); } } /** * 校验:一个请求的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 * @param request 待校验的请求对象 * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 */ public void checkRequest(SaRequest request, String... paramNames) { if (paramNames.length == 0) { checkParamMap(request.getParamMap()); } else { checkParamMap(takeRequestParam(request, paramNames)); } } /** * 从请求中提取指定的参数 * @param request 请求对象 * @param paramNames 指定的参数名称,不可为空,如果传入空数组则代表只拿 timestamp、nonce、sign 三个参数 * @return 提取出的参数 */ protected Map takeRequestParam(SaRequest request, String [] paramNames) { Map paramMap = new TreeMap<>(); // 此三个参数是必须获取的 paramMap.put(timestamp, request.getParam(timestamp)); paramMap.put(nonce, request.getParam(nonce)); paramMap.put(sign, request.getParam(sign)); // 获取指定的参数 for (String paramName : paramNames) { paramMap.put(paramName, request.getParam(paramName)); } // 返回 return paramMap; } // ------------------- 返回相应key ------------------- /** * 拼接key:存储 nonce 时使用的 key * @param nonce nonce 值 * @return key */ public String splicingNonceSaveKey(String nonce) { return SaManager.getConfig().getTokenName() + ":sign:nonce:" + nonce; } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sign.template; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.sign.SaSignManager; import java.util.Map; /** * API 参数签名算法 - 工具类 * * @author click33 * @since 1.34.0 */ public class SaSignUtil { // ----------- 拼接参数 /** * 将所有参数连接成一个字符串(不排序),形如:b=28a=18c=3 * @param paramsMap 参数列表 * @return 拼接出的参数字符串 */ public static String joinParams(Map paramsMap) { return SaSignManager.getSaSignTemplate().joinParams(paramsMap); } /** * 将所有参数按照字典顺序连接成一个字符串,形如:a=18b=28c=3 * @param paramsMap 参数列表 * @return 拼接出的参数字符串 */ public static String joinParamsDictSort(Map paramsMap) { return SaSignManager.getSaSignTemplate().joinParamsDictSort(paramsMap); } // ----------- 创建签名 /** * 创建签名:md5(paramsStr + keyStr) * @param paramsMap 参数列表 * @return 签名 */ public static String createSign(Map paramsMap) { return SaSignManager.getSaSignTemplate().createSign(paramsMap); } /** * 给 paramsMap 追加 timestamp、nonce、sign 三个参数 * @param paramsMap 参数列表 * @return 加工后的参数列表 */ public static Map addSignParams(Map paramsMap) { return SaSignManager.getSaSignTemplate().addSignParams(paramsMap); } /** * 给 paramsMap 追加 timestamp、nonce、sign 三个参数,并转换为参数字符串,形如: * data=xxx8nonce=xxx8timestamp=xxx8sign=xxx * @param paramsMap 参数列表 * @return 加工后的参数列表 转化为的参数字符串 */ public static String addSignParamsAndJoin(Map paramsMap) { return SaSignManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap); } // ----------- 校验签名 /** * 判断:指定时间戳与当前时间戳的差距是否在允许的范围内 * @param timestamp 待校验的时间戳 * @return 是否在允许的范围内 */ public static boolean isValidTimestamp(long timestamp) { return SaSignManager.getSaSignTemplate().isValidTimestamp(timestamp); } /** * 校验:指定时间戳与当前时间戳的差距是否在允许的范围内,如果超出则抛出异常 * @param timestamp 待校验的时间戳 */ public static void checkTimestamp(long timestamp) { SaSignManager.getSaSignTemplate().checkTimestamp(timestamp); } /** * 判断:随机字符串 nonce 是否有效。 * 注意:同一 nonce 可以被多次判断有效,不会被缓存 * @param nonce 待判断的随机字符串 * @return 是否有效 */ public static boolean isValidNonce(String nonce) { return SaSignManager.getSaSignTemplate().isValidNonce(nonce); } /** * 校验:随机字符串 nonce 是否有效,如果无效则抛出异常。 * 注意:同一 nonce 只可以被校验通过一次,校验后将保存在缓存中,再次校验将无法通过 * @param nonce 待校验的随机字符串 */ public static void checkNonce(String nonce) { SaSignManager.getSaSignTemplate().checkNonce(nonce); } /** * 判断:给定的参数 生成的签名是否为有效签名 * @param paramsMap 参数列表 * @param sign 待验证的签名 * @return 签名是否有效 */ public static boolean isValidSign(Map paramsMap, String sign) { return SaSignManager.getSaSignTemplate().isValidSign(paramsMap, sign); } /** * 校验:给定的参数 生成的签名是否为有效签名,如果签名无效则抛出异常 * @param paramsMap 参数列表 * @param sign 待验证的签名 */ public static void checkSign(Map paramsMap, String sign) { SaSignManager.getSaSignTemplate().checkSign(paramsMap, sign); } /** * 判断:参数列表中的 nonce、timestamp、sign 是否均为合法的 * @param paramMap 待校验的请求参数集合 * @return 是否合法 */ public static boolean isValidParamMap(Map paramMap) { return SaSignManager.getSaSignTemplate().isValidParamMap(paramMap); } /** * 校验:参数列表中的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 * @param paramMap 待校验的请求参数集合 */ public static void checkParamMap(Map paramMap) { SaSignManager.getSaSignTemplate().checkParamMap(paramMap); } // ----------- Web 请求相关 封装 /** * 判断:一个请求中的 nonce、timestamp、sign 是否均为合法的 * @param request 待校验的请求对象 * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 * @return 是否合法 */ public static boolean isValidRequest(SaRequest request, String... paramNames) { return SaSignManager.getSaSignTemplate().isValidRequest(request, paramNames); } /** * 校验:一个请求的 nonce、timestamp、sign 是否均为合法的,如果不合法,则抛出对应的异常 * @param request 待校验的请求对象 * @param paramNames 指定参与签名的参数有哪些,如果不填写则默认为全部参数 */ public static void checkRequest(SaRequest request, String... paramNames) { SaSignManager.getSaSignTemplate().checkRequest(request, paramNames); } } ================================================ FILE: sa-token-plugin/sa-token-sign/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForSign ================================================ FILE: sa-token-plugin/sa-token-snack3/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-snack3 sa-token-snack3 sa-token integrate Snack3 cn.dev33 sa-token-core org.noear snack3 ================================================ FILE: sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.util.SaFoxUtil; import org.noear.snack.ONode; import org.noear.snack.core.Feature; /** * JSON 转换器, Snack3 版实现 * * @author click33 * @author noear * @since 1.41.0 */ public class SaJsonTemplateForSnack3 implements SaJsonTemplate { /** * 序列化:对象 -> json 字符串 */ @Override public String objectToJson(Object obj) { if (SaFoxUtil.isEmpty(obj)) { return null; } return ONode.loadObj(obj, Feature.WriteClassName, Feature.NotWriteRootClassName).toJson(); } /** * 反序列化:json 字符串 → 对象 */ @Override public T jsonToObject(String jsonStr, Class type) { if (SaFoxUtil.isEmpty(jsonStr)) { return null; } return ONode.deserialize(jsonStr, type); } } ================================================ FILE: sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack3.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateForSnack3; import cn.dev33.satoken.session.SaSessionForSnack3Customized; import cn.dev33.satoken.strategy.SaStrategy; /** * SaToken 插件安装:JSON 转换器 - Snack3 版 * * @author click33 * @author noear * @since 1.41.0 */ public class SaTokenPluginForSnack3 implements SaTokenPlugin { @Override public void install() { // 设置JSON转换器:Snack3 版 SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack3()); // 重写 SaSession 生成策略 SaStrategy.instance.createSession = SaSessionForSnack3Customized::new; // 指定 SaSession 类型 SaStrategy.instance.sessionClassType = SaSessionForSnack3Customized.class; } } ================================================ FILE: sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/session/SaSessionForSnack3Customized.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import cn.dev33.satoken.util.SaFoxUtil; import org.noear.snack.ONode; /** * Fastjson 定制版 SaSession,重写类型转换API * * @author click33 * @author noear * @since 1.34.0 */ public class SaSessionForSnack3Customized extends SaSession { private static final long serialVersionUID = -7600983549653130681L; /** * 构建一个 SaSession 对象 */ public SaSessionForSnack3Customized() { super(); } /** * 构建一个 SaSession 对象 * * @param id Session 的 id */ public SaSessionForSnack3Customized(String id) { super(id); } /** * 取值 (指定转换类型) * * @param 泛型 * @param key key * @param cs 指定转换类型 * @return 值 */ @Override public T getModel(String key, Class cs) { // 如果是想取出为基础类型 Object value = get(key); if (SaFoxUtil.isBasicType(cs)) { return SaFoxUtil.getValueByType(value, cs); } // 为空提前返回 if (valueIsNull(value)) { return null; } // 如果是 JSONObject 类型直接转,否则先转为 String 再转 if (value instanceof ONode) { ONode jo = (ONode) value; return jo.toObject(cs); } else if (value instanceof String) { return ONode.deserialize((String) value, cs); } else { //有可能是 Map return ONode.load(value).toObject(cs); } } } ================================================ FILE: sa-token-plugin/sa-token-snack3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForSnack3 ================================================ FILE: sa-token-plugin/sa-token-snack4/pom.xml ================================================ sa-token-plugin cn.dev33 ${revision} ../pom.xml 4.0.0 sa-token-snack4 sa-token-snack4 sa-token integrate Snack4 cn.dev33 sa-token-core org.noear snack4 ================================================ FILE: sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack4.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.json; import cn.dev33.satoken.util.SaFoxUtil; import org.noear.snack4.ONode; import org.noear.snack4.Feature; import org.noear.snack4.Options; /** * JSON 转换器, Snack3 版实现 * * @author click33 * @author noear * @since 1.41.0 */ public class SaJsonTemplateForSnack4 implements SaJsonTemplate { private final Options options = Options.of(Feature.Write_ClassName, Feature.Write_NotRootClassName, Feature.Read_AutoType); /** * 序列化:对象 -> json 字符串 */ @Override public String objectToJson(Object obj) { if (SaFoxUtil.isEmpty(obj)) { return null; } return ONode.ofBean(obj, options).toJson(); } /** * 反序列化:json 字符串 → 对象 */ @Override public T jsonToObject(String jsonStr, Class type) { if (SaFoxUtil.isEmpty(jsonStr)) { return null; } return ONode.deserialize(jsonStr, type, options); } } ================================================ FILE: sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack4.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateForSnack4; import cn.dev33.satoken.session.SaSessionForSnack4Customized; import cn.dev33.satoken.strategy.SaStrategy; /** * SaToken 插件安装:JSON 转换器 - Snack3 版 * * @author click33 * @author noear * @since 1.41.0 */ public class SaTokenPluginForSnack4 implements SaTokenPlugin { @Override public void install() { // 设置JSON转换器:Snack3 版 SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack4()); // 重写 SaSession 生成策略 SaStrategy.instance.createSession = SaSessionForSnack4Customized::new; // 指定 SaSession 类型 SaStrategy.instance.sessionClassType = SaSessionForSnack4Customized.class; } } ================================================ FILE: sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/session/SaSessionForSnack4Customized.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.session; import cn.dev33.satoken.util.SaFoxUtil; import org.noear.snack4.ONode; /** * Fastjson 定制版 SaSession,重写类型转换API * * @author click33 * @author noear * @since 1.34.0 */ public class SaSessionForSnack4Customized extends SaSession { private static final long serialVersionUID = -7600983549653130681L; /** * 构建一个 SaSession 对象 */ public SaSessionForSnack4Customized() { super(); } /** * 构建一个 SaSession 对象 * * @param id Session 的 id */ public SaSessionForSnack4Customized(String id) { super(id); } /** * 取值 (指定转换类型) * * @param 泛型 * @param key key * @param cs 指定转换类型 * @return 值 */ @Override public T getModel(String key, Class cs) { // 如果是想取出为基础类型 Object value = get(key); if (SaFoxUtil.isBasicType(cs)) { return SaFoxUtil.getValueByType(value, cs); } // 为空提前返回 if (valueIsNull(value)) { return null; } // 如果是 JSONObject 类型直接转,否则先转为 String 再转 if (value instanceof ONode) { ONode jo = (ONode) value; return jo.toBean(cs); } else if (value instanceof String) { return ONode.deserialize((String) value, cs); } else { //有可能是 Map return ONode.ofBean(value).toBean(cs); } } } ================================================ FILE: sa-token-plugin/sa-token-snack4/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForSnack4 ================================================ FILE: sa-token-plugin/sa-token-spring-aop/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-spring-aop sa-token-spring-aop sa-token authentication by spring-aop cn.dev33 sa-token-core org.springframework.boot spring-boot-starter-aop ================================================ FILE: sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAopPointcutAdvisorBeanRegister.java ================================================ package cn.dev33.satoken.aop; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.List; /** * Sa-Token AOP 环绕切入 Bean 注册 *

    * 参考资料:
    * https://www.jb51.net/program/297714rev.htm
    * https://www.bilibili.com/video/BV1WZ421W7Qx
    * https://blog.csdn.net/Tomwildboar/article/details/139199801
    *

    * * @author click33 * @since 2024/8/3 */ @Configuration public class SaAopPointcutAdvisorBeanRegister { /** * Advisor 静态全局引用 */ public static SaAroundAnnotationPointcutAdvisor saAroundAnnoAdvisor; @Bean public SaAroundAnnotationPointcutAdvisor saAroundAnnotationHandlePointcutAdvisor (List> handlerList) { SaAroundAnnotationPointcutAdvisor advisor = new SaAroundAnnotationPointcutAdvisor(); // 定义切入规则 AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); String expression = calcExpression(handlerList); pointcut.setExpression(expression); advisor.setPointcut(pointcut); // 定义执行的方法 advisor.setAdvice(new SaAroundAnnotationMethodInterceptor()); // 保存全局引用 SaAopPointcutAdvisorBeanRegister.saAroundAnnoAdvisor = advisor; return advisor; } /** * 计算切入表达式 * @param appendHandlerList 追加的 SaAnnotationAbstractHandler 处理器 * @return / */ public static String calcExpression(List> appendHandlerList) { // 框架内置的 List> list = new ArrayList<>(SaAnnotationStrategy.instance.annotationHandlerMap.keySet()); // 额外追加的 if(appendHandlerList != null) { for (SaAnnotationHandlerInterface handler : appendHandlerList) { Class cls = handler.getHandlerAnnotationClass(); if(!list.contains(cls)) { list.add(handler.getHandlerAnnotationClass()); } } } // 计算 return calcClassListExpression(list); } /** * 计算 class 列表的切入表达式, * 最终样例形如:
             public static final String POINTCUT_SIGN =
             "@within(cn.dev33.satoken.annotation.SaCheckLogin) || @annotation(cn.dev33.satoken.annotation.SaCheckLogin) || "
             + "@within(cn.dev33.satoken.annotation.SaCheckRole) || @annotation(cn.dev33.satoken.annotation.SaCheckRole) || "
             + "@within(cn.dev33.satoken.annotation.SaCheckPermission) || @annotation(cn.dev33.satoken.annotation.SaCheckPermission)";
         
    * @param list / * @return / */ public static String calcClassListExpression(List> list) { String pointcutExpression = ""; for (Class cls : list) { if(!pointcutExpression.isEmpty()) { pointcutExpression += " || "; } pointcutExpression += "@within(" + cls.getName() + ") || @annotation(" + cls.getName() + ")"; } return pointcutExpression; } } ================================================ FILE: sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationMethodInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.aop; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import java.lang.reflect.Method; /** * Sa-Token 注解方法拦截器 AOP环绕切入 * * @author click33 * @since 1.39.0 */ public class SaAroundAnnotationMethodInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { // 注解鉴权 try{ Method method = invocation.getMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } catch (StopMatchException ignored) { } // 执行原有防范 return invocation.proceed(); } } ================================================ FILE: sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationPointcutAdvisor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.aop; import org.springframework.aop.support.DefaultPointcutAdvisor; /** * Sa-Token 注解方法 Advisor AOP环绕切入 * * @author click33 * @since 1.39.0 */ public class SaAroundAnnotationPointcutAdvisor extends DefaultPointcutAdvisor { } ================================================ FILE: sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.aop.SaAopPointcutAdvisorBeanRegister ================================================ FILE: sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.aop.SaAopPointcutAdvisorBeanRegister ================================================ FILE: sa-token-plugin/sa-token-spring-el/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-spring-el sa-token-spring-el sa-token authentication by spring-el cn.dev33 sa-token-core org.springframework.boot spring-boot-starter-aop ================================================ FILE: sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/annotation/SaCheckEL.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 注解鉴权:根据 EL 表达式执行鉴权 * *

    可标注在方法、类上(效果等同于标注在此类的所有方法上) * * @author click33 * @since 1.40.0 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface SaCheckEL { /** * 需要执行的 EL 表达式 * * @return / */ String value() default ""; } ================================================ FILE: sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELAspect.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.aop; import cn.dev33.satoken.annotation.SaCheckEL; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ObjectUtils; import java.lang.reflect.Method; /** * Sa-Token 注解鉴权 EL 表达式 AOP 切入 (用于处理 @SaCheckEL 注解) * * @author click33 * @since 1.40.0 */ @Aspect public class SaCheckELAspect implements BeanFactoryAware { /** * 表达式解析器 (用于解析 EL 表达式) */ private final ExpressionParser parser = new SpelExpressionParser(); /** * 参数名发现器 (用于获取方法参数名) */ private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer(); /** * Spring Bean 工厂 (用于解析 Spring 容器中的 Bean 对象) */ private BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } /** * 前置通知 (所有被 SaCheckEL 注解修饰的方法或类) * * @param joinPoint / */ @Before("@within(cn.dev33.satoken.annotation.SaCheckEL) || @annotation(cn.dev33.satoken.annotation.SaCheckEL)") public void atBefore(JoinPoint joinPoint) { // 获取方法签名与参数列表 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Object[] args = joinPoint.getArgs(); // 如果标注了 @SaIgnore 注解,则跳过,代表不进行校验 if(SaAnnotationStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) { return; } // 1、根数据对象构建 // 构建校验上下文根数据对象 SaCheckELRootMap rootMap = new SaCheckELRootMap(method, extractArgs(method, args), joinPoint.getTarget() ); // 添加 this 指针指向注解函数所在类,使之可以在表达式中通过 this.xx 访问类的属性和方法 (与Target一致,此处只是为了更加语义化) rootMap.put(SaCheckELRootMap.KEY_THIS, joinPoint.getTarget()); // 添加全局默认的 StpLogic 对象,使之可以在表达式中通过 stp.checkLogin() 方式调用校验方法 rootMap.put(SaCheckELRootMap.KEY_STP, StpUtil.getStpLogic()); // 添加 JoinPoint 对象,使开发者在扩展时可以根据 JoinPoint 对象获取更多信息 rootMap.put(SaCheckELRootMap.KEY_JOIN_POINT, joinPoint); // 执行开发者自定义的增强策略 SaAnnotationStrategy.instance.checkELRootMapExtendFunction.accept(rootMap); // 2、表达式解析方案构建 // 创建表达式解析上下文 MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(rootMap, method, args, pnd); // 添加属性访问器,使之可以解析 Map 对象的属性作为根上下文 context.addPropertyAccessor(new MapAccessor()); // 设置 Bean 解析器,使之可以在表达式中引用 Spring 容器管理的所有 Bean 对象 context.setBeanResolver(new BeanFactoryResolver(beanFactory)); // 3、开始校验 // 先校验 Method 所属 Class 上的注解表达式 SaCheckEL ofClass = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method.getDeclaringClass(), SaCheckEL.class); if (ofClass != null) { parser.parseExpression(ofClass.value()).getValue(context); } // 再校验 Method 上的注解表达式 SaCheckEL ofMethod = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method, SaCheckEL.class); if (ofMethod != null) { parser.parseExpression(ofMethod.value()).getValue(context); } } /** * 如果是可变长参数,则展开并返回,否则原样返回 * * @param method / * @param args / * @return / */ private Object[] extractArgs(Method method, Object[] args) { if (!method.isVarArgs()) { return args; } else { Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]); Object[] combinedArgs = new Object[args.length - 1 + varArgs.length]; System.arraycopy(args, 0, combinedArgs, 0, args.length - 1); System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length); return combinedArgs; } } } ================================================ FILE: sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELRootMap.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.aop; import cn.dev33.satoken.error.SaErrorCode; import cn.dev33.satoken.exception.SaTokenException; import java.lang.reflect.Method; import java.util.HashMap; /** * Sa-Token 注解鉴权 EL 表达式解析器的根数据对象 * * @author click33 * @since 1.40.0 */ public class SaCheckELRootMap extends HashMap { /** * KEY标记:被切入的函数 */ public static final String KEY_METHOD = "method"; /** * KEY标记:被切入的函数参数 */ public static final String KEY_ARGS = "args"; /** * KEY标记:被切入的目标对象 */ public static final String KEY_TARGET = "target"; /** * KEY标记:注解所在类对象引用 */ public static final String KEY_THIS = "this"; /** * KEY标记:全局默认 StpLogic 对象 */ public static final String KEY_STP = "stp"; /** * KEY标记:本次切入的 JoinPoint 对象 */ public static final String KEY_JOIN_POINT = "joinPoint"; public SaCheckELRootMap(Method method, Object[] args, Object target) { this.put(KEY_METHOD, method); this.put(KEY_ARGS, args); this.put(KEY_TARGET, target); } /** * 获取 被切入的函数 * * @return method 被切入的函数 */ public Method getMethod() { return (Method) this.get(KEY_METHOD); } /** * 获取 被切入的函数参数 * * @return args 被切入的函数参数 */ public Object[] getArgs() { return (Object[]) this.get(KEY_ARGS); } /** * 获取 被切入的目标对象 * * @return target 被切入的目标对象 */ public Object getTarget() { return this.get(KEY_TARGET); } /** * 获取 注解所在类对象引用 * * @return this 注解所在类对象引用 */ public Object getThis() { return this.get(KEY_THIS); } /** * 获取本次切入的 JoinPoint 对象 */ public Object getJoinPoint() { return this.get(KEY_JOIN_POINT); } /** * 断言函数, 表达式执行结果为true才能通过 * * @param flag 执行结果 */ public void NEED(boolean flag) { NEED(flag, SaErrorCode.CODE_UNDEFINED, "未通过 EL 表达式校验"); } /** * 断言函数, 表达式执行结果为true才能通过,并在未通过时抛出 SaTokenException 异常,异常描述信息为 errorMessage * * @param flag 执行结果 */ public void NEED(boolean flag, String errorMessage) { NEED(flag, SaErrorCode.CODE_UNDEFINED, errorMessage); } /** * 断言函数, 表达式执行结果为true才能通过,并在未通过时抛出 SaTokenException 异常,异常码为 errorCode,异常描述信息为 errorMessage * * @param flag 执行结果 */ public void NEED(boolean flag, int errorCode, String errorMessage) { if(!flag) { throw new SaTokenException(errorCode, errorMessage); } } } ================================================ FILE: sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.aop.SaCheckELAspect ================================================ FILE: sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.aop.SaCheckELAspect ================================================ FILE: sa-token-plugin/sa-token-spring-el/src/main/resources/spel-extension.json ================================================ { "cn.dev33.satoken.annotation.SaCheckEL@value": { "method": { "parameters": true, "parametersPrefix": [ "p", "a" ] }, "fields": { "root": "cn.dev33.satoken.aop.SaCheckELRootMap", "stp": "cn.dev33.satoken.stp.StpLogic" } } } ================================================ FILE: sa-token-plugin/sa-token-sso/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-sso sa-token-sso sa-token realization sso cn.dev33 sa-token-core cn.dev33 sa-token-sign ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/SaSsoManager.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; /** * Sa-Token-SSO 模块 总控类 * * @author click33 * @since 1.30.0 */ public class SaSsoManager { /** * Sso Server 端 配置 Bean */ private volatile static SaSsoServerConfig serverConfig; public static SaSsoServerConfig getServerConfig() { if (serverConfig == null) { synchronized (SaSsoManager.class) { if (serverConfig == null) { setServerConfig(new SaSsoServerConfig()); } } } return serverConfig; } public static void setServerConfig(SaSsoServerConfig serverConfig) { SaSsoManager.serverConfig = serverConfig; // 如果配置了 is-check-sign=false,则打印一条警告日志 if ( ! serverConfig.getIsCheckSign()) { printNoCheckSignWarningByStartup(); } } /** * Sso Client 端 配置 Bean */ private volatile static SaSsoClientConfig clientConfig; public static SaSsoClientConfig getClientConfig() { if (clientConfig == null) { synchronized (SaSsoManager.class) { if (clientConfig == null) { setClientConfig(new SaSsoClientConfig()); } } } return clientConfig; } public static void setClientConfig(SaSsoClientConfig clientConfig) { SaSsoManager.clientConfig = clientConfig; // 如果配置了 is-check-sign=false,则打印一条警告日志 if ( ! clientConfig.getIsCheckSign()) { printNoCheckSignWarningByStartup(); } } // 在启动时检测到 sa-token.sso-[server/client].is-check-sign=false 时,输出警告信息 public static void printNoCheckSignWarningByStartup() { System.err.println("-----------------------------------------------------------------------------"); System.err.println("警告信息:"); System.err.println("当前配置项 sa-token.sso-[server/client].is-check-sign=false 代表跳过 SSO 参数签名校验"); System.err.println("此模式仅为方便本地调试使用,生产环境下请务必配置为 true (配置项默认为true)"); System.err.println("-----------------------------------------------------------------------------"); } // 在运行时检测到 sa-token.sso-[server/client].is-check-sign=false 时,输出警告信息 public static void printNoCheckSignWarningByRuntime() { System.err.println("警告信息:当前配置项 sa-token.sso-[server/client].is-check-sign=false 已跳过参数签名校验," + "此模式仅为方便本地调试使用,生产环境下请务必配置为 true (配置项默认为true)"); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.config; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; /** * Sa-Token SSO Client 端 配置类 * * @author click33 * @since 1.30.0 */ public class SaSsoClientConfig implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) */ public String mode = ""; /** * 当前 Client 标识(非必填,不填时代表当前应用是一个匿名应用) */ public String client; /** * 配置 SSO Server 端主机总地址 */ public String serverUrl; /** * 单独配置 Server 端:单点登录授权地址 */ public String authUrl = "/sso/auth"; /** * 单独配置 Server 端:单点注销地址 */ public String signoutUrl = "/sso/signout"; /** * 单独配置 Server 端:推送消息地址 */ public String pushUrl = "/sso/pushS"; /** * 单独配置 Server 端:查询数据 getData 地址 */ public String getDataUrl = "/sso/getData"; /** * 配置当前 Client 端的登录地址(为空时自动获取) */ public String currSsoLogin; /** * 配置当前 Client 端的单点注销回调URL (为空时自动获取) */ public String currSsoLogoutCall; /** * 是否打开模式三(此值为 true 时将使用 http 请求校验 ticket 值) */ public Boolean isHttp = false; /** * 是否打开单点注销功能 (为 true 时,开放 /sso/logout 接口,以及接收单点注销回调消息推送) */ public Boolean isSlo = true; /** * 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) */ public Boolean regLogoutCall = false; /** * API 调用签名秘钥 */ public String secretKey; /** * 是否校验参数签名(为 false 时暂时关闭参数签名校验,此为方便本地调试用的一个配置项,生产环境请务必为true) */ public Boolean isCheckSign = true; // 额外添加的一些函数 /** * @return 获取拼接 url:Server 端单点登录授权地址 */ public String splicingAuthUrl() { return SaFoxUtil.spliceTwoUrl(getServerUrl(), getAuthUrl()); } /** * @return 获取拼接 url:Server 端查询数据 getData 地址 */ public String splicingGetDataUrl() { return SaFoxUtil.spliceTwoUrl(getServerUrl(), getGetDataUrl()); } /** * @return 获取拼接 url:Server 端单点注销地址 */ public String splicingSignoutUrl() { return SaFoxUtil.spliceTwoUrl(getServerUrl(), getSignoutUrl()); } /** * @return 获取拼接 url:单独配置 Server 端推送消息地址 */ public String splicingPushUrl() { return SaFoxUtil.spliceTwoUrl(getServerUrl(), getPushUrl()); } // get set /** * 获取 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) * * @return / */ public String getMode() { return this.mode; } /** * 设置 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) * * @param mode / */ public void setMode(String mode) { this.mode = mode; } /** * @return 是否打开单点注销功能 (为 true 时,开放 /sso/logout 接口,以及接收单点注销回调消息推送) */ public Boolean getIsSlo() { return isSlo; } /** * @param isSlo 是否打开单点注销功能 (为 true 时,开放 /sso/logout 接口,以及接收单点注销回调消息推送) * @return 对象自身 */ public SaSsoClientConfig setIsSlo(Boolean isSlo) { this.isSlo = isSlo; return this; } /** * @return isHttp 是否打开模式三(此值为 true 时将使用 http 请求校验 ticket 值) */ public Boolean getIsHttp() { return isHttp; } /** * @param isHttp 是否打开模式三(此值为 true 时将使用 http 请求校验 ticket 值) * @return 对象自身 */ public SaSsoClientConfig setIsHttp(Boolean isHttp) { this.isHttp = isHttp; return this; } /** * 当前 Client 标识(非必填,不填时代表当前应用是一个匿名应用) * * @return / */ public String getClient() { return client; } /** * 当前 Client 标识(非必填,不填时代表当前应用是一个匿名应用) * * @param client / */ public SaSsoClientConfig setClient(String client) { this.client = client; return this; } /** * @return 单独配置 Server 端:单点登录授权地址 */ public String getAuthUrl() { return authUrl; } /** * @param authUrl 单独配置 Server 端:单点登录授权地址 * @return 对象自身 */ public SaSsoClientConfig setAuthUrl(String authUrl) { this.authUrl = authUrl; return this; } /** * @return 单独配置 Server 端:查询数据 getData 地址 */ public String getGetDataUrl() { return getDataUrl; } /** * @param getDataUrl 单独配置 Server 端:查询数据 getData 地址 * @return 对象自身 */ public SaSsoClientConfig setGetDataUrl(String getDataUrl) { this.getDataUrl = getDataUrl; return this; } /** * @return 单独配置 Server 端:单点注销地址 */ public String getSignoutUrl() { return signoutUrl; } /** * @param signoutUrl 单独配置 Server 端:单点注销地址 * @return 对象自身 */ public SaSsoClientConfig setSignoutUrl(String signoutUrl) { this.signoutUrl = signoutUrl; return this; } /** * 获取 单独配置 Server 端:推送消息地址 * * @return / */ public String getPushUrl() { return this.pushUrl; } /** * 设置 单独配置 Server 端:推送消息地址 * * @param pushUrl / * @return 对象自身 */ public SaSsoClientConfig setPushUrl(String pushUrl) { this.pushUrl = pushUrl; return this; } /** * @return 配置当前 Client 端的登录地址(为空时自动获取) */ public String getCurrSsoLogin() { return currSsoLogin; } /** * @param currSsoLogin 配置当前 Client 端的登录地址(为空时自动获取) * @return 对象自身 */ public SaSsoClientConfig setCurrSsoLogin(String currSsoLogin) { this.currSsoLogin = currSsoLogin; return this; } /** * @return 配置当前 Client 端的单点注销回调URL (为空时自动获取) */ public String getCurrSsoLogoutCall() { return currSsoLogoutCall; } /** * @param currSsoLogoutCall 配置当前 Client 端的单点注销回调URL (为空时自动获取) * @return 对象自身 */ public SaSsoClientConfig setCurrSsoLogoutCall(String currSsoLogoutCall) { this.currSsoLogoutCall = currSsoLogoutCall; return this; } /** * 配置 SSO Server 端主机总地址 * * @return / */ public String getServerUrl() { return serverUrl; } /** * 配置 SSO Server 端主机总地址 * * @param serverUrl / * @return 对象自身 */ public SaSsoClientConfig setServerUrl(String serverUrl) { this.serverUrl = serverUrl; return this; } /** * 获取 API 调用签名秘钥 * * @return / */ public String getSecretKey() { return this.secretKey; } /** * 设置 API 调用签名秘钥 * * @param secretKey / * @return 对象自身 */ public SaSsoClientConfig setSecretKey(String secretKey) { this.secretKey = secretKey; return this; } /** * 获取 是否校验参数签名(为 false 时暂时关闭参数签名校验,此为方便本地调试用的一个配置项,生产环境请务必为true) * * @return isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) */ public Boolean getIsCheckSign() { return this.isCheckSign; } /** * 设置 是否校验参数签名(为 false 时暂时关闭参数签名校验,此为方便本地调试用的一个配置项,生产环境请务必为true) * * @param isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) */ public SaSsoClientConfig setIsCheckSign(Boolean isCheckSign) { this.isCheckSign = isCheckSign; return this; } /** * 获取 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) * * @return / */ public Boolean getRegLogoutCall() { return this.regLogoutCall; } /** * 设置 是否注册单点登录注销回调 (为 true 时,登录时附带单点登录回调地址,并且开放 /sso/logoutCall 地址) * * @param regLogoutCall / * @return / */ public SaSsoClientConfig setRegLogoutCall(Boolean regLogoutCall) { this.regLogoutCall = regLogoutCall; return this; } @Override public String toString() { return "SaSsoClientConfig [" + "mode=" + mode + ", client=" + client + ", serverUrl=" + serverUrl + ", authUrl=" + authUrl + ", signoutUrl=" + signoutUrl + ", pushUrl=" + pushUrl + ", getDataUrl=" + getDataUrl + ", currSsoLogin=" + currSsoLogin + ", currSsoLogoutCall=" + currSsoLogoutCall + ", isHttp=" + isHttp + ", isSlo=" + isSlo + ", regLogoutCall=" + regLogoutCall + ", secretKey=" + secretKey + ", isCheckSign=" + isCheckSign + "]"; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.config; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; import java.util.List; /** * Sa-Token SSO 客户端信息配置 (在 Server 端配置允许接入的 Client 信息) * * @author click33 * @since 1.43.0 */ public class SaSsoClientModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * Client 名称标识 */ public String client; /** * 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket ) */ public String allowUrl = ""; /** * 是否接收推送消息 */ public Boolean isPush = false; /** * 是否打开单点注销功能 */ public Boolean isSlo = true; /** * API 调用签名秘钥 */ public String secretKey; /** * 此 Client 端主机总地址 */ public String serverUrl; /** * 此 Client 端推送消息的地址 (如不配置,默认根据 serverUrl + '/sso/pushC' 进行拼接) */ public String pushUrl = "/sso/pushC"; // 额外添加的一些函数 /** * 以数组形式写入允许的授权回调地址 * @param url 所有集合 * @return 对象自身 */ public SaSsoClientModel setAllow(String ...url) { this.setAllowUrl(SaFoxUtil.arrayJoin(url)); return this; } /** * 获取拼接 url:此 Client 端推送消息的地址 * * @return / */ public String splicingPushUrl() { String _pushUrl = SaFoxUtil.spliceTwoUrl(getServerUrl(), getPushUrl()); if ( ! SaFoxUtil.isUrl(_pushUrl)) { throw new SaSsoException("应用 [" + getClient() + "] 推送地址无效:" + _pushUrl).setCode(SaSsoErrorCode.CODE_30023); } return _pushUrl; } // get set /** * @return Client 名称标识 */ public String getClient() { return client; } /** * @param client Client 名称标识 */ public SaSsoClientModel setClient(String client) { this.client = client; return this; } /** * @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket ) */ public String getAllowUrl() { return allowUrl; } /** * @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket ) * @return 对象自身 */ public SaSsoClientModel setAllowUrl(String allowUrl) { // 提前校验一下配置的 allowUrl 是否合法,让开发者尽早发现错误 if(SaFoxUtil.isNotEmpty(allowUrl)) { List allowUrlList = SaFoxUtil.convertStringToList(allowUrl); SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList); } this.allowUrl = allowUrl; return this; } /** * @return isHttp 是否打开模式三 */ public Boolean getIsPush() { return isPush; } /** * @param isPush 是否打开模式三 * @return 对象自身 */ public SaSsoClientModel setIsPush(Boolean isPush) { this.isPush = isPush; return this; } /** * @return 是否打开单点注销功能 */ public Boolean getIsSlo() { return isSlo; } /** * @param isSlo 是否打开单点注销功能 * @return 对象自身 */ public SaSsoClientModel setIsSlo(Boolean isSlo) { this.isSlo = isSlo; return this; } /** * 获取 API 调用签名秘钥 * * @return / */ public String getSecretKey() { return this.secretKey; } /** * 设置 API 调用签名秘钥 * * @param secretKey / * @return 对象自身 */ public SaSsoClientModel setSecretKey(String secretKey) { this.secretKey = secretKey; return this; } /** * 获取 此 Client 端主机总地址 * * @return serverUrl 此 Client 端主机总地址 */ public String getServerUrl() { return this.serverUrl; } /** * 设置 此 Client 端主机总地址 * * @param serverUrl 此 Client 端主机总地址 * @return 对象自身 */ public SaSsoClientModel setServerUrl(String serverUrl) { this.serverUrl = serverUrl; return this; } /** * 获取 此 Client 端推送消息的地址 (如不配置,默认根据 serverUrl + '/sso/pushC' 进行拼接) * * @return / */ public String getPushUrl() { return this.pushUrl; } /** * 设置 此 Client 端推送消息的地址 (如不配置,默认根据 serverUrl + '/sso/pushC' 进行拼接) * * @param pushUrl 此 Client 端推送消息的地址 * @return 对象自身 */ public SaSsoClientModel setPushUrl(String pushUrl) { this.pushUrl = pushUrl; return this; } @Override public String toString() { return "SaSsoClientModel [" + "client=" + client + ", allowUrl=" + allowUrl + ", isSlo=" + isSlo + ", isPush=" + isPush + ", secretKey=" + secretKey + ", serverUrl=" + serverUrl + ", pushUrl=" + pushUrl + "]"; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.config; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Sa-Token SSO Server 端 配置类 * * @author click33 * @since 1.38.0 */ public class SaSsoServerConfig implements Serializable { private static final long serialVersionUID = -6541180061782004705L; // ----------------- Server端相关配置 /** * 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) */ public String mode = ""; /** * ticket 有效期 (单位: 秒) */ public long ticketTimeout = 60 * 5; /** * 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 */ public String homeRoute; /** * 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息) */ public Boolean isSlo = true; /** * 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) */ public Boolean autoRenewTimeout = false; /** * 在 Account-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 */ public int maxRegClient = 32; /** * 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) */ public Boolean isCheckSign = true; /** * Client 信息配置列表 */ public Map clients = new LinkedHashMap<>(); // 匿名 Client 相关配置 /** * 是否允许匿名 Client 接入 */ public Boolean allowAnonClient = false; /** * 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) */ public String allowUrl = ""; /** * API 调用签名秘钥 (全局默认 + 匿名 client 使用) */ public String secretKey; // 额外方法 /** * 以数组形式写入允许的授权回调地址 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) * @param url 所有集合 * @return 对象自身 */ public SaSsoServerConfig setAllow(String ...url) { this.setAllowUrl(SaFoxUtil.arrayJoin(url)); return this; } /** * 添加一个应用 * @param client / * @return 对象自身 */ public SaSsoServerConfig addClient(SaSsoClientModel client) { this.clients.put(client.getClient(), client); return this; } // get set /** * 获取 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) * * @return / */ public String getMode() { return this.mode; } /** * 设置 指定当前系统集成 SSO 时使用的模式(约定型配置项,不对代码逻辑产生任何影响) * * @param mode / */ public void setMode(String mode) { this.mode = mode; } /** * @return ticket 有效期 (单位: 秒) */ public long getTicketTimeout() { return ticketTimeout; } /** * @param ticketTimeout ticket 有效期 (单位: 秒) * @return 对象自身 */ public SaSsoServerConfig setTicketTimeout(long ticketTimeout) { this.ticketTimeout = ticketTimeout; return this; } /** * @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) */ public String getAllowUrl() { return allowUrl; } /** * @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用) * @return 对象自身 */ public SaSsoServerConfig setAllowUrl(String allowUrl) { // 提前校验一下配置的 allowUrl 是否合法,让开发者尽早发现错误 if(SaFoxUtil.isNotEmpty(allowUrl)) { List allowUrlList = SaFoxUtil.convertStringToList(allowUrl); SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList); } this.allowUrl = allowUrl; return this; } /** * @return 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 */ public String getHomeRoute() { return homeRoute; } /** * @param homeRoute 主页路由:在 /sso/auth 登录页不指定 redirect 参数时,默认跳转的地址 * @return 对象自身 */ public SaSsoServerConfig setHomeRoute(String homeRoute) { this.homeRoute = homeRoute; return this; } /** * @return 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息) */ public Boolean getIsSlo() { return isSlo; } /** * @param isSlo 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息) * @return 对象自身 */ public SaSsoServerConfig setIsSlo(Boolean isSlo) { this.isSlo = isSlo; return this; } /** * @return 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) */ public Boolean getAutoRenewTimeout() { return autoRenewTimeout; } /** * @param autoRenewTimeout 是否在每次下发 ticket 时,自动续期 token 的有效期(根据全局 timeout 值) * @return 对象自身 */ public SaSsoServerConfig setAutoRenewTimeout(Boolean autoRenewTimeout) { this.autoRenewTimeout = autoRenewTimeout; return this; } /** * @return maxLoginClient 在 Account-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 */ public int getMaxRegClient() { return maxRegClient; } /** * @param maxRegClient 在 Account-Session 上记录 Client 信息的最高数量(-1=无限),超过此值将进行自动清退处理,先进先出 * @return 对象自身 */ public SaSsoServerConfig setMaxRegClient(int maxRegClient) { this.maxRegClient = maxRegClient; return this; } /** * 获取 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) * * @return isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) */ public Boolean getIsCheckSign() { return this.isCheckSign; } /** * 设置 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) * * @param isCheckSign 是否校验参数签名(方便本地调试用的一个配置项,生产环境请务必为true) */ public SaSsoServerConfig setIsCheckSign(Boolean isCheckSign) { this.isCheckSign = isCheckSign; return this; } /** * 获取 是否允许匿名 Client 接入 * * @return / */ public Boolean getAllowAnonClient() { return this.allowAnonClient; } /** * 设置 是否允许匿名 Client 接入 * * @param allowAnonClient / */ public SaSsoServerConfig setAllowAnonClient(Boolean allowAnonClient) { this.allowAnonClient = allowAnonClient; return this; } /** * 获取 API 调用签名秘钥 (全局默认 + 匿名 client 使用) * * @return / */ public String getSecretKey() { return this.secretKey; } /** * 设置 API 调用签名秘钥 (全局默认 + 匿名 client 使用) * * @param secretKey / * @return 对象自身 */ public SaSsoServerConfig setSecretKey(String secretKey) { this.secretKey = secretKey; return this; } /** * 获取 Client 信息配置列表 * * @return clients Client 信息配置列表 */ public Map getClients() { return this.clients; } /** * 设置 Client 信息配置列表 * * @param clients Client 信息配置列表 * @return 对象自身 */ public SaSsoServerConfig setClients(Map clients) { this.clients = clients; return this; } @Override public String toString() { return "SaSsoServerConfig [" + "mode=" + mode + ", ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", homeRoute=" + homeRoute + ", isSlo=" + isSlo + ", autoRenewTimeout=" + autoRenewTimeout + ", maxRegClient=" + maxRegClient + ", isCheckSign=" + isCheckSign + ", allowAnonClient=" + allowAnonClient + ", secretKey=" + secretKey + ", clients=" + clients + "]"; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.error; /** * 定义 sa-token-sso 所有异常细分状态码 * * @author click33 * @since 1.33.0 */ public interface SaSsoErrorCode { /** redirect 重定向 url 是一个无效地址 */ int CODE_30001 = 30001; /** redirect 重定向 url 不在 allowUrl 允许的范围内 */ int CODE_30002 = 30002; /** 接口调用方提供的 secretkey 秘钥无效 */ int CODE_30003 = 30003; /** 提供的 ticket 是无效的 */ int CODE_30004 = 30004; /** 在模式三下,sso-client 调用 sso-server 端 校验ticket接口 时,得到的响应是校验失败 */ int CODE_30005 = 30005; /** 在模式三下,sso-client 调用 sso-server 端 单点注销接口 时,得到的响应是注销失败 */ int CODE_30006 = 30006; /** http 请求调用 提供的 timestamp 与当前时间的差距超出允许的范围 */ int CODE_30007 = 30007; /** http 请求调用 提供的 sign 无效 */ int CODE_30008 = 30008; /** 本地系统没有配置 secretkey 字段 */ int CODE_30009 = 30009; /** 本地系统没有配置 http 请求处理器 */ int CODE_30010 = 30010; /** 该 ticket 不属于当前 client */ int CODE_30011 = 30011; /** 当前缺少配置 server-url 地址 */ int CODE_30012 = 30012; /** 提供的 client 参数值无效 */ int CODE_30013 = 30013; /** 在 /sso/auth 既没有指定 redirect 参数,也没有配置 homeRoute 路由 */ int CODE_30014 = 30014; /** 无效的 allow-url 配置 */ int CODE_30015 = 30015; /** 未能找到指定类型的消息处理器 */ int CODE_30021 = 30021; /** 消息类型不能为空 */ int CODE_30022 = 30022; /** 无效的消息推送地址 */ int CODE_30023 = 30023; /** SSO 消息里缺少指定的参数 */ int CODE_30024 = 30024; } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/exception/SaSsoException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.exception; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; /** * 一个异常:代表 SSO 认证流程错误 * * @author click33 * @since 1.30.0 */ public class SaSsoException extends SaTokenException { /** * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; /** * 一个异常:代表 SSO 认证流程错误 * @param message 异常描述 */ public SaSsoException(String message) { super(message); } /** * 一个异常:代表 SSO 认证流程错误 * @param code 异常细分状态码 * @param message 异常描述 */ public SaSsoException(int code, String message) { super(code, message); } /** * 写入异常细分状态码 * @param code 异常细分状态码 * @return 对象自身 */ public SaSsoException setCode(int code) { super.setCode(code); return this; } /** * 断言 flag 不为 true,否则抛出 message 异常 * @param flag 标记 * @param message 异常信息 * @param code 异常细分状态码 */ public static void notTrue(boolean flag, String message, int code) { if(flag) { throw new SaSsoException(message).setCode(code); } } /** * 断言 value 不为空,否则抛出 message 异常 * @param value 值 * @param message 异常信息 * @param code 异常细分状态码 */ public static void notEmpty(Object value, String message, int code) { if(SaFoxUtil.isEmpty(value)) { throw new SaSsoException(message).setCode(code); } } /** * 如果flag==true,则抛出message异常 * @param flag 标记 * @param message 异常信息 */ @Deprecated public static void throwBy(boolean flag, String message) { if(flag) { throw new SaSsoException(message); } } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/CheckTicketAppendDataFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.function; import cn.dev33.satoken.util.SaResult; import java.util.function.BiFunction; /** * 函数式接口:sso-server 端:在校验 ticket 后,给 sso-client 端追加返回信息的函数 * *

    参数:loginId, SaResult 响应参数对象

    *

    返回:SaResult 响应参数对象

    * * @author click33 * @since 1.38.0 */ @FunctionalInterface public interface CheckTicketAppendDataFunction extends BiFunction { } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/DoLoginHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.function; import java.util.function.BiFunction; /** * 函数式接口:sso-server 端:登录处理函数 * *

    参数:账号、密码

    *

    返回:登录结果

    * * @author click33 * @since 1.38.0 */ @FunctionalInterface public interface DoLoginHandleFunction extends BiFunction { } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/NotLoginViewFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.function; import java.util.function.Supplier; /** * 函数式接口:sso-server 端:未登录时返回的 View * *

    参数:无

    *

    返回:未登录时的 View 视图

    * * @author click33 * @since 1.38.0 */ @FunctionalInterface public interface NotLoginViewFunction extends Supplier { } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SaSsoMessageHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.function; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.template.SaSsoTemplate; /** * 函数式接口:处理 SSO 消息的函数式接口 * *

    参数:ssoTemplate 模板对象, 要处理的 message 消息

    *

    返回:任意值

    * * @author click33 * @since 1.38.0 */ @FunctionalInterface public interface SaSsoMessageHandleFunction { Object execute(SaSsoTemplate ssoTemplate, SaSsoMessage message); } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SendRequestFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.function; import java.util.function.Function; /** * 函数式接口:发送 Http 请求的处理函数 * *

    参数:要请求的url

    *

    返回:请求结果

    * * @author click33 * @since 1.38.0 */ @FunctionalInterface public interface SendRequestFunction extends Function { } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/TicketResultHandleFunction.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.function; import cn.dev33.satoken.sso.model.SaCheckTicketResult; /** * 函数式接口:sso-client 端:自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) * *

    参数:loginId, back

    *

    返回:返回给前端的值

    * * @author click33 * @since 1.38.0 */ @FunctionalInterface public interface TicketResultHandleFunction { Object run(SaCheckTicketResult ctr, String back); } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessage.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message; import cn.dev33.satoken.application.SaSetValueInterface; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.util.SaFoxUtil; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; /** * SSO 消息 Model * * @author click33 * @since 1.43.0 */ public class SaSsoMessage extends LinkedHashMap implements SaSetValueInterface, Serializable { /** * */ private static final long serialVersionUID = 1L; /** * KEY:TYPE */ public static final String MSG_TYPE = "msgType"; public SaSsoMessage() { } /** * 构造函数 * @param type 消息类型 */ public SaSsoMessage(String type) { setType(type); } /** * 构造函数 * @param map 消息参数 */ public SaSsoMessage(Map map) { this.putAll(map); } /** * 获取消息类型 * @return / */ public String getType() { return getString(MSG_TYPE); } /** * 设置消息类型 * @param type / * @return / */ public SaSsoMessage setType(String type) { return set(MSG_TYPE, type); } /** * 校验消息类型 */ public void checkType() { if(SaFoxUtil.isEmpty(getString(MSG_TYPE))) { throw new SaSsoException("消息类型不可为空").setCode(SaSsoErrorCode.CODE_30022); } } // ----------- @Override public Object get(String key) { return super.get(key); } @Override public SaSsoMessage set(String key, Object value) { super.put(key, value); return this; } @Override public SaSsoMessage delete(String key) { super.remove(key); return this; } // ----------- /** * 获取一个值 (此值必须存在,否则抛出异常 ) * @param key 键 * @return 参数值 */ public Object getValueNotNull(String key) { Object value = get(key); if(SaFoxUtil.isEmpty(value)) { throw new SaSsoException("缺少参数:" + key).setCode(SaSsoErrorCode.CODE_30024); } return value; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessageHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.sso.function.SaSsoMessageHandleFunction; import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; import cn.dev33.satoken.sso.message.handle.SaSsoMessageSimpleHandle; import cn.dev33.satoken.sso.template.SaSsoTemplate; import java.util.LinkedHashMap; import java.util.Map; /** * SSO 消息处理器 - 持有器 * * @author click33 * @since 1.43.0 */ public class SaSsoMessageHolder { /** * 所有已注册的消息处理器 */ public final Map messageHandleMap = new LinkedHashMap<>(); /** * 判断是否具有指定类型的消息处理器 * * @param type 消息类型 * @return / */ public boolean hasHandle(String type) { return messageHandleMap.containsKey(type); } /** * 删除指定类型的消息处理器 * * @param type 消息类型 */ public SaSsoMessageHolder removeHandle(String type) { messageHandleMap.remove(type); return this; } /** * 添加指定类型的消息处理器 * * @param handle / * @return 对象自身 */ public SaSsoMessageHolder addHandle(SaSsoMessageHandle handle) { messageHandleMap.put(handle.getHandlerType(), handle); return this; } /** * 添加指定类型的简单消息处理器 * * @param type 要处理的消息类型 * @param handle 要执行的方法 * @return 对象自身 */ public SaSsoMessageHolder addHandle(String type, SaSsoMessageHandleFunction handle) { messageHandleMap.put(type, new SaSsoMessageSimpleHandle(type, handle)); return this; } /** * 获取指定类型的消息处理器 * * @param type / */ public SaSsoMessageHandle getHandle(String type) { return messageHandleMap.get(type); } /** * 处理指定消息 * * @param ssoTemplate / * @param message / * @return 处理结果 */ public Object handleMessage(SaSsoTemplate ssoTemplate, SaSsoMessage message) { SaSsoMessageHandle handle = messageHandleMap.get(message.getType()); if(handle == null) { throw new SaSsoException("未能找到消息处理器: " + message.getType()).setCode(SaSsoErrorCode.CODE_30021); } return handle.handle(ssoTemplate, message); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageHandle.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message.handle; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.template.SaSsoTemplate; /** * SSO 消息处理器 - 父接口 * * @author click33 * @since 1.43.0 */ public interface SaSsoMessageHandle { /** * 获取所要处理的消息类型 * * @return / */ String getHandlerType(); /** * 具体要执行的处理方法 * * @param ssoTemplate / * @param message / * @return / */ Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message); } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageSimpleHandle.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message.handle; import cn.dev33.satoken.sso.function.SaSsoMessageHandleFunction; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.template.SaSsoTemplate; /** * SSO 消息处理器 - 简单实现,方便 lambda 表达式编程 * * @author click33 * @since 1.43.0 */ public class SaSsoMessageSimpleHandle implements SaSsoMessageHandle{ public String type; public SaSsoMessageHandleFunction handle; /** * SSO 消息处理器 - 简单实现,方便 lambda 表达式编程 * @param type 要处理的消息类型 * @param handle 要执行的方法 */ public SaSsoMessageSimpleHandle(String type, SaSsoMessageHandleFunction handle) { this.type = type; this.handle = handle; } /** * 获取所要处理的消息类型 * * @return / */ @Override public String getHandlerType() { return type; } /** * 具体要执行的处理方法 * * @param ssoTemplate / * @param message / * @return / */ @Override public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message){ return handle.execute(ssoTemplate, message); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/client/SaSsoMessageLogoutCallHandle.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message.handle.client; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoTemplate; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaResult; /** * SSO 消息处理器 - sso-client 端:处理 单点注销回调 的请求 * * @author click33 * @since 1.43.0 */ public class SaSsoMessageLogoutCallHandle implements SaSsoMessageHandle { /** * 获取所要处理的消息类型 * * @return / */ public String getHandlerType() { return SaSsoConsts.MESSAGE_LOGOUT_CALL; } /** * 执行方法 * * @param ssoTemplate / * @param message / * @return / */ public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) { // 1、获取对象 SaSsoClientTemplate ssoClientTemplate = (SaSsoClientTemplate) ssoTemplate; StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); ParamName paramName = ssoClientTemplate.paramName; // 2、判断当前应用是否开启单点注销功能 if( ! ssoClientTemplate.getClientConfig().getIsSlo()) { return SaResult.error("当前 sso-client 端未开启单点注销功能"); } // 3、获取参数 Object loginId = message.getValueNotNull(paramName.loginId); loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(loginId); String deviceId = message.getString(paramName.deviceId); // 4、注销当前应用端会话 stpLogic.logout(loginId, new SaLogoutParameter() .setDeviceId(deviceId) ); // 5、响应 return SaResult.ok("单点注销回调成功"); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageCheckTicketHandle.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message.handle.server; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; import cn.dev33.satoken.sso.model.TicketModel; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.sso.template.SaSsoTemplate; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.util.SaResult; /** * SSO 消息处理器 - sso-server 端:处理校验 ticket 的请求 * * @author click33 * @since 1.43.0 */ public class SaSsoMessageCheckTicketHandle implements SaSsoMessageHandle { /** * 获取所要处理的消息类型 * * @return / */ public String getHandlerType() { return SaSsoConsts.MESSAGE_CHECK_TICKET; } /** * 执行方法 * * @param ssoTemplate / * @param message / * @return / */ public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) { // 1、获取对象 SaSsoServerTemplate ssoServerTemplate = (SaSsoServerTemplate) ssoTemplate; ParamName paramName = ssoServerTemplate.paramName; StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal(); String client = message.getString(paramName.client); String ticket = message.getValueNotNull(paramName.ticket).toString(); String sloCallback = message.getString(paramName.ssoLogoutCall); // 2、校验ticket,获取 loginId TicketModel ticketModel = ssoServerTemplate.checkTicketParamAndDelete(ticket, client); Object loginId = ticketModel.getLoginId(); // 3、注册此客户端的登录信息 ssoServerTemplate.registerSloCallbackUrl(loginId, client, sloCallback); // 4、给 client 端响应结果 SaResult result = SaResult.ok(); result.setData(loginId); // 兼容历史版本 result.set(paramName.loginId, loginId); result.set(paramName.tokenValue, ticketModel.getTokenValue()); result.set(paramName.deviceId, stpLogic.getLoginDeviceIdByToken(ticketModel.getTokenValue())); result.set(paramName.remainTokenTimeout, stpLogic.getTokenTimeout(ticketModel.getTokenValue())); result.set(paramName.remainSessionTimeout, stpLogic.getSessionTimeoutByLoginId(loginId)); result = ssoServerTemplate.strategy.checkTicketAppendData.apply(loginId, result); return result; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageSignoutHandle.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.message.handle.server; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.sso.template.SaSsoTemplate; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaResult; /** * SSO 消息处理器 - sso-server 端:处理 单点注销 的请求 * * @author click33 * @since 1.43.0 */ public class SaSsoMessageSignoutHandle implements SaSsoMessageHandle { /** * 获取所要处理的消息类型 * * @return / */ public String getHandlerType() { return SaSsoConsts.MESSAGE_SIGNOUT; } /** * 执行方法 * * @param ssoTemplate / * @param message / * @return / */ public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) { // 1、获取对象 SaSsoServerTemplate ssoServerTemplate = (SaSsoServerTemplate) ssoTemplate; ParamName paramName = ssoServerTemplate.paramName; // 2、判断当前是否开启了全局单点注销功能 if( ! ssoServerTemplate.getServerConfig().getIsSlo()) { return SaResult.error("当前 sso-server 端未开启单点注销功能"); } // 3、获取参数 String client = message.getString(paramName.client); Object loginId = message.get(paramName.loginId); String deviceId = message.getString(paramName.deviceId); // 4、单点注销 SaLogoutParameter logoutParameter = ssoServerTemplate.getStpLogicOrGlobal().createSaLogoutParameter().setDeviceId(deviceId); ssoServerTemplate.ssoLogout(loginId, logoutParameter, client); // 5、响应 return SaResult.ok(); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaCheckTicketResult.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.model; import cn.dev33.satoken.util.SaResult; import java.io.Serializable; /** * 校验 ticket 返回 loginId 等结果的参数封装 * * @author click33 * @since 1.38.0 */ public class SaCheckTicketResult implements Serializable { private static final long serialVersionUID = 1406115065849845073L; /** 账号id */ public Object loginId; /** 在 sso-server 端的 token 值 */ public String tokenValue; /** 登录设备 id */ public String deviceId; /** 此账号 token 剩余有效期 */ public Long remainTokenTimeout; /** 此账号会话剩余有效期 */ public Long remainSessionTimeout; /** 此账号在认证中心的 loginId */ public Object centerId; /** 从 sso-server 返回的原生所有参数 */ public SaResult result; @Override public String toString() { return "SaCheckTicketResult{" + "loginId=" + loginId + ", tokenValue='" + tokenValue + '\'' + ", deviceId='" + deviceId + '\'' + ", remainTokenTimeout=" + remainTokenTimeout + ", remainSessionTimeout=" + remainSessionTimeout + ", centerId=" + centerId + ", result=" + result + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientInfo.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.model; import cn.dev33.satoken.sso.util.SaSsoConsts; import java.io.Serializable; /** * Sa-Token SSO 应用信息(注册在 SaSession 上的已登录应用信息列表) * * @author click33 * @since 1.38.0 */ public class SaSsoClientInfo implements Serializable { private static final long serialVersionUID = 1406115065849845073L; /* * 只能记录模式三登录的 client 信息,模式一和模式二的信息即使记录上,也无法完成单点注销操作,遂不记录 * 所以:mode、tokenValue 字段,仅留作扩展,暂时无用 */ /** * 此 client 登录模式(1=模式一,2=模式二,3=模式三) */ public int mode; /** * 客户端标识 */ public String client; /** * 单点注销回调 url */ public String sloCallbackUrl; /** * 此 client 注册信息的时间,13位时间戳 */ public long regTime; /** * 此账号有记录以来为第几次登录,默认从0开始递增 */ public int index; public SaSsoClientInfo() { } /** * 模式三构建 */ public SaSsoClientInfo(String client, String sloCallbackUrl, int index) { this.mode = SaSsoConsts.SSO_MODE_3; this.client = client; this.sloCallbackUrl = sloCallbackUrl; this.regTime = System.currentTimeMillis(); this.index = index; } // get set /** * 获取 此 client 登录模式(1=模式一,2=模式二,3=模式三) * * @return mode 此 client 登录模式(1=模式一,2=模式二,3=模式三) */ public int getMode() { return this.mode; } /** * 设置 此 client 登录模式(1=模式一,2=模式二,3=模式三) * * @param mode 此 client 登录模式(1=模式一,2=模式二,3=模式三) * @return / */ public SaSsoClientInfo setMode(int mode) { this.mode = mode; return this; } /** * 获取 客户端标识 * * @return client 客户端标识 */ public String getClient() { return this.client; } /** * 设置 客户端标识 * * @param client 客户端标识 * @return / */ public SaSsoClientInfo setClient(String client) { this.client = client; return this; } /** * 获取 单点注销回调url * * @return ssoLogoutCall 单点注销回调url */ public String getSloCallbackUrl() { return this.sloCallbackUrl; } /** * 设置 单点注销回调url * * @param sloCallbackUrl 单点注销回调url * @return / */ public SaSsoClientInfo setSloCallbackUrl(String sloCallbackUrl) { this.sloCallbackUrl = sloCallbackUrl; return this; } /** * 获取 此 client 注册信息的时间,13位时间戳 * * @return regTime 此 client 注册信息的时间,13位时间戳 */ public long getRegTime() { return this.regTime; } /** * 设置 此 client 注册信息的时间,13位时间戳 * * @param regTime 此 client 注册信息的时间,13位时间戳 * @return / */ public SaSsoClientInfo setRegTime(long regTime) { this.regTime = regTime; return this; } /** * 获取 此账号有记录以来为第几次登录,默认从0开始递增 * * @return regTime 此账号有记录以来为第几次登录,默认从0开始递增 */ public long getIndex() { return this.index; } /** * 设置 此账号有记录以来为第几次登录,默认从0开始递增 * * @param index 此账号有记录以来为第几次登录,默认从0开始递增 * @return / */ public SaSsoClientInfo setIndex(int index) { this.index = index; return this; } @Override public String toString() { return "SaSsoClientModel{" + "mode=" + mode + ", client='" + client + '\'' + ", sloCallbackUrl='" + sloCallbackUrl + '\'' + ", regTime=" + regTime + ", index=" + index + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.model; /** * Sa-Token SSO Model * * @author click33 * @since 1.38.0 */ @Deprecated public class SaSsoClientModel extends SaSsoClientInfo { } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/TicketModel.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.model; import java.io.Serializable; /** * Model: Ticket 码 * * @author click33 * @since 1.43.0 */ public class TicketModel implements Serializable { private static final long serialVersionUID = -6541180061782004705L; /** * ticket 码 */ public String ticket; /** * 应用标识 */ public String client; /** * 对应 loginId */ public Object loginId; /** * 会话 token */ public String tokenValue; /** * 创建时间,13位时间戳 */ public long createTime; /** * 构建一个 */ public TicketModel() { this.createTime = System.currentTimeMillis(); } /** * 构建一个 * @param ticket 授权码 * @param client 应用id * @param loginId 对应的账号id * @param tokenValue 会话 token */ public TicketModel(String ticket, String client, Object loginId, String tokenValue) { this(); this.ticket = ticket; this.client = client; this.loginId = loginId; this.tokenValue = tokenValue; } // get set /** * 获取 ticket 码 * * @return / */ public String getTicket() { return this.ticket; } /** * 设置 ticket 码 * * @param ticket / * @return 对象自身 */ public TicketModel setTicket(String ticket) { this.ticket = ticket; return this; } /** * 获取 应用标识 * * @return / */ public String getClient() { return this.client; } /** * 设置 应用标识 * * @param client / * @return 对象自身 */ public TicketModel setClient(String client) { this.client = client; return this; } /** * 获取 对应 loginId * * @return / */ public Object getLoginId() { return this.loginId; } /** * 设置 对应 loginId * * @param loginId / * @return 对象自身 */ public TicketModel setLoginId(Object loginId) { this.loginId = loginId; return this; } /** * 获取 会话 token * * @return tokenValue 会话 token */ public String getTokenValue() { return this.tokenValue; } /** * 设置 会话 token * * @param tokenValue 会话 token * @return 对象自身 */ public TicketModel setTokenValue(String tokenValue) { this.tokenValue = tokenValue; return this; } /** * 获取 创建时间,13位时间戳 * * @return / */ public long getCreateTime() { return this.createTime; } /** * 设置 创建时间,13位时间戳 * * @param createTime / * @return 对象自身 */ public TicketModel setCreateTime(long createTime) { this.createTime = createTime; return this; } @Override public String toString() { return "TicketModel{" + "ticket='" + ticket + '\'' + ", client='" + client + '\'' + ", loginId=" + loginId + ", tokenValue=" + tokenValue + ", createTime=" + createTime + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.name; /** * SSO 模块所有 API 路由名称定义 * * @author click33 * @since 1.32.0 */ public class ApiName { /** SSO-Server端:授权地址 */ public String ssoAuth = "/sso/auth"; /** SSO-Server端:RestAPI 登录接口 */ public String ssoDoLogin = "/sso/doLogin"; /** SSO-Server端:校验ticket 获取账号id */ public String ssoCheckTicket = "/sso/checkTicket"; /** SSO-Server端:接收推送消息 */ public String ssoPushS = "/sso/pushS"; /** SSO-Server端:获取userinfo */ public String ssoUserinfo = "/sso/userinfo"; /** SSO-Server端:单点注销地址 */ public String ssoSignout = "/sso/signout"; /** SSO-Client端:登录地址 */ public String ssoLogin = "/sso/login"; /** SSO-Client端:单点注销地址 */ public String ssoLogout = "/sso/logout"; /** SSO-Client端:判断当前是否登录地址 */ public String ssoIsLogin = "/sso/isLogin"; /** SSO-Client端:单点注销的回调 */ public String ssoLogoutCall = "/sso/logoutCall"; /** SSO-Client端:接收推送消息 */ public String ssoPushC = "/sso/pushC"; /** * 批量修改 path,新增固定前缀 * @param prefix 示例值:/sso-user、/sso-admin * @return 对象自身 */ public ApiName addPrefix(String prefix) { this.ssoAuth = prefix + this.ssoAuth; this.ssoDoLogin = prefix + this.ssoDoLogin; this.ssoCheckTicket = prefix + this.ssoCheckTicket; this.ssoPushS = prefix + this.ssoPushS; this.ssoUserinfo = prefix + this.ssoUserinfo; this.ssoSignout = prefix + this.ssoSignout; this.ssoLogin = prefix + this.ssoLogin; this.ssoLogout = prefix + this.ssoLogout; this.ssoIsLogin = prefix + this.ssoIsLogin; this.ssoPushC = prefix + this.ssoPushC; this.ssoLogoutCall = prefix + this.ssoLogoutCall; return this; } /** * 批量修改 path,替换掉 /sso 固定前缀 * @param prefix 示例值:/sso-user、/sso-admin * @return 对象自身 */ public ApiName replacePrefix(String prefix) { String oldPrefix = "/sso"; this.ssoAuth = this.ssoAuth.replaceFirst(oldPrefix, prefix); this.ssoDoLogin = this.ssoDoLogin.replaceFirst(oldPrefix, prefix); this.ssoCheckTicket = this.ssoCheckTicket.replaceFirst(oldPrefix, prefix); this.ssoPushS = this.ssoPushS.replaceFirst(oldPrefix, prefix); this.ssoUserinfo = this.ssoUserinfo.replaceFirst(oldPrefix, prefix); this.ssoSignout = this.ssoSignout.replaceFirst(oldPrefix, prefix); this.ssoLogin = this.ssoLogin.replaceFirst(oldPrefix, prefix); this.ssoLogout = this.ssoLogout.replaceFirst(oldPrefix, prefix); this.ssoIsLogin = this.ssoIsLogin.replaceFirst(oldPrefix, prefix); this.ssoPushC = this.ssoPushC.replaceFirst(oldPrefix, prefix); this.ssoLogoutCall = this.ssoLogoutCall.replaceFirst(oldPrefix, prefix); return this; } @Override public String toString() { return "ApiName{" + "ssoAuth='" + ssoAuth + '\'' + ", ssoDoLogin='" + ssoDoLogin + '\'' + ", ssoCheckTicket='" + ssoCheckTicket + '\'' + ", ssoPushS='" + ssoPushS + '\'' + ", ssoUserinfo='" + ssoUserinfo + '\'' + ", ssoSignout='" + ssoSignout + '\'' + ", ssoIsLogin='" + ssoIsLogin + '\'' + ", ssoLogin='" + ssoLogin + '\'' + ", ssoLogout='" + ssoLogout + '\'' + ", ssoLogoutCall='" + ssoLogoutCall + '\'' + ", ssoPushC='" + ssoPushC + '\'' + '}'; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ParamName.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.name; /** * SSO 模块所有参数名称定义 * * @author click33 * @since 1.32.0 */ public class ParamName { /** redirect 参数名称 */ public String redirect = "redirect"; /** ticket 参数名称 */ public String ticket = "ticket"; /** back 参数名称 */ public String back = "back"; /** mode 参数名称 */ public String mode = "mode"; /** 账号 id */ public String loginId = "loginId"; /** client 应用标识 */ public String client = "client"; /** token 名称 */ public String tokenName = "tokenName"; /** token 值 */ public String tokenValue = "tokenValue"; /** 设备 id */ public String deviceId = "deviceId"; /** 接口参数签名秘钥 */ public String secretkey = "secretkey"; /** Client 端单点注销时 - 回调 URL 参数名称 */ public String ssoLogoutCall = "ssoLogoutCall"; /** 是否为超过 maxRegClient 触发的自动注销 */ public String autoLogout = "autoLogout"; public String name = "name"; public String pwd = "pwd"; public String timestamp = "timestamp"; public String nonce = "nonce"; public String sign = "sign"; /** Session 剩余有效期 参数名称 */ public String remainSessionTimeout = "remainSessionTimeout"; /** token 剩余有效期 参数名称 */ public String remainTokenTimeout = "remainTokenTimeout"; /** 是否单设备 id 注销 */ public String singleDeviceIdLogout = "singleDeviceIdLogout"; } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoClientProcessor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.processor; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.model.SaCheckTicketResult; import cn.dev33.satoken.sso.model.TicketModel; import cn.dev33.satoken.sso.name.ApiName; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.util.Map; /** * SSO 请求处理器 (Client端) * * @author click33 * @since 1.38.0 */ public class SaSsoClientProcessor { /** * 全局默认实例 */ public static SaSsoClientProcessor instance = new SaSsoClientProcessor(); /** * 底层 SaSsoClientTemplate 对象 */ public SaSsoClientTemplate ssoClientTemplate = new SaSsoClientTemplate(); // ----------- SSO-Client 端路由分发 ----------- /** * 分发 Client 端所有请求 * @return 处理结果 */ public Object dister() { ApiName apiName = ssoClientTemplate.apiName; // 获取对象 SaRequest req = SaHolder.getRequest(); SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); // ------------------ 路由分发 ------------------ // sso-client:登录地址 if(req.isPath(apiName.ssoLogin)) { return ssoLogin(); } // sso-client:单点注销 if(req.isPath(apiName.ssoLogout)) { return ssoLogout(); } // sso-client:接收消息推送 if(req.isPath(apiName.ssoPushC)) { return ssoPushC(); } // sso-client:单点注销的回调 if(req.isPath(apiName.ssoLogoutCall) && cfg.getRegLogoutCall()) { return ssoLogoutCall(); } // 默认返回 return SaSsoConsts.NOT_HANDLE; } /** * SSO-Client端:登录地址 * @return 处理结果 */ public Object ssoLogin() { // 获取对象 SaRequest req = SaHolder.getRequest(); ParamName paramName = ssoClientTemplate.paramName; String ticket = req.getParam(paramName.ticket); /* * 此时有两种情况: * 情况1:ticket 无值,说明此请求是 sso-client 端访问,需要重定向至 sso-server 认证中心 * 情况2:ticket 有值,说明此请求从 sso-server 认证中心重定向而来,需要根据 ticket 进行登录 */ if(ticket == null) { return _goServerAuth(); } else { return _loginByTicket(); } } /** * SSO-Client端:单点注销 * @return 处理结果 */ public Object ssoLogout() { // 获取对象 SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); // 无论登录时选择的是模式二还是模式三 // 在注销时都应该按照模式三的方法,通过 http 请求调用 sso-server 的单点注销接口来做到全端下线 // 如果按照模式二的方法注销,则会导致按照模式三登录的应用无法参与到单点注销环路中来 if(cfg.getIsSlo()) { return _ssoLogoutByMode3(); } // 默认返回 return SaSsoConsts.NOT_HANDLE; } /** * SSO-Client端:接收推送消息 * * @return 处理结果 */ public Object ssoPushC() { SaSsoClientConfig ssoClientConfig = ssoClientTemplate.getClientConfig(); // 1、校验签名 Map paramMap = SaHolder.getRequest().getParamMap(); if(ssoClientConfig.getIsCheckSign()) { ssoClientTemplate.getSignTemplate().checkParamMap(paramMap); } else { SaSsoManager.printNoCheckSignWarningByRuntime(); } // 2、处理消息 SaSsoMessage message = new SaSsoMessage(paramMap); return ssoClientTemplate.handleMessage(message); } /** * SSO-Client端:单点注销的回调 [模式三] * @return 处理结果 */ public Object ssoLogoutCall() { // 获取对象 SaRequest req = SaHolder.getRequest(); StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); ParamName paramName = ssoClientTemplate.paramName; SaSsoClientConfig ssoConfig = ssoClientTemplate.getClientConfig(); // 获取参数 Object loginId = req.getParamNotNull(paramName.loginId); loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(loginId); String deviceId = req.getParam(paramName.deviceId); // 校验参数签名 if(ssoConfig.getIsCheckSign()) { ssoClientTemplate.getSignTemplate().checkRequest(req); } else { SaSsoManager.printNoCheckSignWarningByRuntime(); } // 注销当前应用端会话 SaLogoutParameter logoutParameter = ssoClientTemplate.getStpLogicOrGlobal().createSaLogoutParameter(); stpLogic.logout(loginId, logoutParameter.setDeviceId(deviceId)); // 响应 return SaResult.ok("单点注销回调成功"); } // 次级方法 /** * 跳转去 sso-server 认证中心 * @return / */ public Object _goServerAuth() { // 获取对象 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); ParamName paramName = ssoClientTemplate.paramName; // 获取参数 String back = req.getParam(paramName.back, "/"); // 如果当前 sso-client 端已经登录,则无需访问 SSO 认证中心,可以直接返回 if(stpLogic.isLogin()) { return res.redirect(back); } // 获取当前项目的 sso 登录中转页地址,形如:http://sso-client.com/sso/login // 全局配置了就是用全局的,否则使用当前请求的地址 String currSsoLoginUrl = cfg.getCurrSsoLogin(); if(SaFoxUtil.isEmpty(currSsoLoginUrl)) { currSsoLoginUrl = SaHolder.getRequest().getUrl(); } // 构建最终授权地址 url,形如:http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com String serverAuthUrl = ssoClientTemplate.buildServerAuthUrl(currSsoLoginUrl, back); return res.redirect(serverAuthUrl); } /** * 根据认证中心回传的 ticket 进行登录 * @return / */ public Object _loginByTicket() { // 获取对象 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); ParamName paramName = ssoClientTemplate.paramName; ApiName apiName = ssoClientTemplate.apiName; // 获取参数 String back = req.getParam(paramName.back, "/"); String ticket = req.getParam(paramName.ticket); // 1、校验 ticket,获取 loginId 等数据 SaCheckTicketResult ctr = checkTicket(ticket, apiName.ssoLogin); // 2、如果开发者自定义了 ticket 结果值处理函数,则使用自定义的函数 if(ssoClientTemplate.strategy.ticketResultHandle != null) { return ssoClientTemplate.strategy.ticketResultHandle.run(ctr, back); } // 3、登录并重定向至back地址 stpLogic.login(ctr.loginId, new SaLoginParameter() .setTimeout(ctr.remainTokenTimeout) .setDeviceId(ctr.deviceId) ); return res.redirect(back); } /** * SSO-Client端:单点注销 [模式三] * @return 处理结果 */ public Object _ssoLogoutByMode3() { // 获取对象 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); boolean singleDeviceIdLogout = req.isParam(ssoClientTemplate.paramName.singleDeviceIdLogout, "true"); // 如果未登录,则无需注销 if( ! stpLogic.isLogin()) { return _ssoLogoutBack(req, res); } // 向 sso-server 认证中心推送消息:单点注销 SaLogoutParameter logoutParameter = stpLogic.createSaLogoutParameter(); if(singleDeviceIdLogout) { logoutParameter.setDeviceId(stpLogic.getLoginDeviceId()); } Object loginId = stpLogic.getLoginId(); Object centerId = ssoClientTemplate.strategy.convertLoginIdToCenterId.run(loginId); SaSsoMessage message = ssoClientTemplate.buildSignoutMessage(centerId, logoutParameter); SaResult result = ssoClientTemplate.pushMessageAsSaResult(message); // 如果 sso-server 响应的状态码非200,代表业务失败,将回应的 msg 字段作为异常抛出 if(result.getCode() == null || SaResult.CODE_SUCCESS != result.getCode()) { throw new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30006); } // 极端场景下,sso-server 中心的单点注销可能并不会通知到当前 client 端,所以这里需要再补一刀 if(stpLogic.isLogin()) { stpLogic.logout(loginId, logoutParameter); } return _ssoLogoutBack(req, res); } /** * 封装:校验ticket,取出loginId,如果 ticket 无效则抛出异常 (适用于模式二或模式三) * * @param ticket ticket码 * @return SaCheckTicketResult */ public SaCheckTicketResult checkTicket(String ticket) { return checkTicket(ticket, null); } /** * 封装:校验ticket,取出loginId,如果 ticket 无效则抛出异常 (适用于模式二或模式三) * * @param ticket ticket码 * @param currUri 当前路由的uri,用于计算单点注销回调地址 (如果是使用模式二,可以填写null) * @return SaCheckTicketResult */ public SaCheckTicketResult checkTicket(String ticket, String currUri) { SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); // 两种模式: // isHttp=true:模式三,使用 http 请求从认证中心校验ticket // isHttp=false:模式二,直连 redis 中校验 ticket if(cfg.getIsHttp()) { return _checkTicketByHttp(ticket, currUri); } else { return _checkTicketByRedis(ticket); } } /** * 校验 ticket,http 请求方式 * @param ticket / * @param currUri / * @return / */ public SaCheckTicketResult _checkTicketByHttp(String ticket, String currUri) { SaSsoClientConfig cfg = ssoClientTemplate.getClientConfig(); ApiName apiName = ssoClientTemplate.apiName; ParamName paramName = ssoClientTemplate.paramName; // 计算当前 sso-client 的单点注销回调地址 String ssoLogoutCall = null; if(cfg.getRegLogoutCall()) { // 如果配置了回调地址,就使用配置的值: if(SaFoxUtil.isNotEmpty(cfg.getCurrSsoLogoutCall())) { ssoLogoutCall = cfg.getCurrSsoLogoutCall(); } // 如果提供了当前 uri,则根据此值来计算: else if(SaFoxUtil.isNotEmpty(currUri)) { ssoLogoutCall = SaHolder.getRequest().getUrl().replace(currUri, apiName.ssoLogoutCall); } // 否则视为不注册单点注销回调地址 else { } } // 发起请求 SaSsoMessage message = ssoClientTemplate.buildCheckTicketMessage(ticket, ssoLogoutCall); SaResult result = ssoClientTemplate.pushMessageAsSaResult(message); // 如果 sso-server 响应的状态码非200,代表业务失败,将回应的 msg 字段作为异常抛出 if(result.getCode() == null || result.getCode() != SaResult.CODE_SUCCESS) { throw new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30005); } // 构建返回结果 SaCheckTicketResult ctr = new SaCheckTicketResult(); ctr.loginId = result.get(paramName.loginId); ctr.tokenValue = result.get(paramName.tokenValue, String.class); ctr.deviceId = result.get(paramName.deviceId, String.class); ctr.remainTokenTimeout = result.get(paramName.remainTokenTimeout, Long.class); ctr.remainSessionTimeout = result.get(paramName.remainSessionTimeout, Long.class); ctr.result = result; // 转换 loginId 和 centerId ctr.centerId = ctr.loginId; ctr.loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(ctr.centerId); return ctr; } /** * 校验 ticket,直连 redis 方式 * @param ticket / * @return / */ public SaCheckTicketResult _checkTicketByRedis(String ticket) { // 直连 redis 校验 ticket // 注意此处调用了 SaSsoServerProcessor 处理器里的方法, // 这意味着如果你的 sso-server 端重写了 SaSsoServerProcessor 里的部分方法, // 而在当前 sso-client 没有按照相应格式重写 SaSsoClientProcessor 里的方法, // 可能会导致调用失败(注意是可能,而非一定,主要取决于你是否改变了数据读写格式), // 解决方案为:在当前 sso-client 端也按照 sso-server 端的格式重写 SaSsoClientProcessor 里的方法 StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal(); TicketModel ticketModel = SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, ssoClientTemplate.getClient()); SaCheckTicketResult ctr = new SaCheckTicketResult(); ctr.loginId = ticketModel.getLoginId(); ctr.tokenValue = ticketModel.getTokenValue(); ctr.deviceId = stpLogic.getLoginDeviceIdByToken(ticketModel.getTokenValue()); ctr.remainTokenTimeout = stpLogic.getTokenTimeout(ticketModel.getTokenValue()); ctr.remainSessionTimeout = stpLogic.getSessionTimeoutByLoginId(ticketModel.getLoginId()); ctr.result = null; // 转换 loginId 和 centerId ctr.centerId = ctr.loginId; ctr.loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(ctr.centerId); return ctr; } /** * 封装:单点注销成功后返回结果 * @param req SaRequest对象 * @param res SaResponse对象 * @return 返回结果 */ public Object _ssoLogoutBack(SaRequest req, SaResponse res) { return SaSsoProcessorHelper.ssoLogoutBack(req, res, ssoClientTemplate.paramName); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoProcessorHelper.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.processor; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; /** * SSO 请求处理器,辅助方法 * * @author click33 * @since 1.38.0 */ public class SaSsoProcessorHelper { /** * 封装:单点注销成功后返回结果 * @param req SaRequest对象 * @param res SaResponse对象 * @return 返回结果 */ public static Object ssoLogoutBack(SaRequest req, SaResponse res, ParamName paramName) { /* * 三种情况: * 1. 有back参数,值为SELF -> 回退一级并刷新 * 2. 有back参数,值为url -> 跳转到此url地址 * 3. 无back参数 -> 返回json数据 */ String back = req.getParam(paramName.back); if(SaFoxUtil.isNotEmpty(back)) { if(back.equals(SaSsoConsts.SELF)) { res.setHeader("Content-Type", "text/html; charset=utf-8"); return ""; } return res.redirect(back); } else { return SaResult.ok("单点注销成功"); } } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoServerProcessor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.processor; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.name.ApiName; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import cn.dev33.satoken.util.SaSugar; import java.util.Map; /** * SSO 请求处理器 (Server端) * * @author click33 * @since 1.38.0 */ public class SaSsoServerProcessor { /** * 全局默认实例 */ public static SaSsoServerProcessor instance = new SaSsoServerProcessor(); /** * 底层 SaSsoServerTemplate 对象 */ public SaSsoServerTemplate ssoServerTemplate = new SaSsoServerTemplate(); // ----------- SSO-Server 端路由分发 ----------- /** * 分发 Server 端所有请求 * * @return 处理结果 */ public Object dister() { // 获取对象 SaRequest req = SaHolder.getRequest(); ApiName apiName = ssoServerTemplate.apiName; // ------------------ 路由分发 ------------------ // sso-server:授权地址 if(req.isPath(apiName.ssoAuth)) { return ssoAuth(); } // sso-server:RestAPI 登录接口 if(req.isPath(apiName.ssoDoLogin)) { return ssoDoLogin(); } // sso-server:单点注销 if(req.isPath(apiName.ssoSignout)) { return ssoSignout(); } // sso-server:接收推送消息 if(req.isPath(apiName.ssoPushS)) { return ssoPushS(); } // 默认返回 return SaSsoConsts.NOT_HANDLE; } /** * SSO-Server端:授权地址 * @return 处理结果 */ public Object ssoAuth() { // 获取对象 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); SaSsoServerConfig cfg = ssoServerTemplate.getServerConfig(); StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal(); ParamName paramName = ssoServerTemplate.paramName; // 两种情况: // 情况1:在 SSO 认证中心尚未登录,需要显示登录视图,去登录 // 情况2:在 SSO 认证中心已经登录,需要重定向回 Client 端 // 情况1,显示登录视图 if( ! stpLogic.isLogin()) { return ssoServerTemplate.strategy.notLoginView.get(); } // 情况2,开始跳转 String mode = req.getParam(paramName.mode, SaSsoConsts.MODE_TICKET); String redirect = req.getParam(paramName.redirect); String client = req.getParam(paramName.client); // 构建最终重定向地址 String redirectUrl = SaSugar.get(() -> { // 若 redirect 参数为空,说明用户并不是从 client 重定向来的,而是直接访问的 http://sso-server.com/sso/auth 地址 // 此时需要跳转到配置的 homeRoute 路由上, // 若 homeRoute 也为空,则没有明确的跳转地址了,需要抛出异常 if(SaFoxUtil.isEmpty(redirect)) { if(SaFoxUtil.isEmpty(cfg.getHomeRoute())) { throw new SaSsoException("未指定 redirect 参数,也未配置 homeRoute 路由,无法完成重定向操作").setCode(SaSsoErrorCode.CODE_30014); } return cfg.getHomeRoute(); } // 方式1:直接重定向回Client端 (mode=simple,一般是模式一) if(mode.equals(SaSsoConsts.MODE_SIMPLE)) { ssoServerTemplate.checkRedirectUrl(client, redirect); return redirect; } else { // 方式2:带着 ticket 参数重定向回Client端 (mode=ticket,一般是模式二、三) // 构建并跳转 String _redirectUrl = ssoServerTemplate.buildRedirectUrl(client, redirect, stpLogic.getLoginId(), stpLogic.getTokenValue()); // 构建成功,说明 redirect 地址合法,此时需要更新一下当前 token 有效期 if(cfg.getAutoRenewTimeout()) { stpLogic.renewTimeout(stpLogic.getConfigOrGlobal().getTimeout()); } return _redirectUrl; } }); // 跳转 ssoServerTemplate.strategy.jumpToRedirectUrlNotice.run(redirectUrl); return res.redirect(redirectUrl); } /** * SSO-Server端:RestAPI 登录接口 * @return 处理结果 */ public Object ssoDoLogin() { // 获取参数 SaRequest req = SaHolder.getRequest(); ParamName paramName = ssoServerTemplate.paramName; String name = req.getParam(paramName.name); String pwd = req.getParam(paramName.pwd); // 处理 return ssoServerTemplate.strategy.doLoginHandle.apply(name, pwd); } /** * SSO-Server端:单点注销 * @return 处理结果 */ public Object ssoSignout() { // 获取对象 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal(); Object loginId = stpLogic.getLoginIdDefaultNull(); boolean singleDeviceIdLogout = req.isParam(ssoServerTemplate.paramName.singleDeviceIdLogout, "true"); // 单点注销 if(SaFoxUtil.isNotEmpty(loginId)) { SaLogoutParameter logoutParameter = stpLogic.createSaLogoutParameter(); if(singleDeviceIdLogout) { logoutParameter.setDeviceId(stpLogic.getLoginDeviceId()); } ssoServerTemplate.ssoLogout(loginId, logoutParameter, null); } // 完成 return _ssoLogoutBack(req, res); } /** * SSO-Server端:接收推送消息 * * @return 处理结果 */ public Object ssoPushS() { ParamName paramName = ssoServerTemplate.paramName; SaSsoServerConfig ssoServerConfig = ssoServerTemplate.getServerConfig(); // 1、获取参数 SaRequest req = SaHolder.getRequest(); String client = req.getParam(paramName.client); // 2、校验提供的client是否为非法字符 if(SaSsoConsts.CLIENT_WILDCARD.equals(client)) { return SaResult.error("无效 client 标识:" + client); } // 3、校验参数签名 Map paramMap = req.getParamMap(); if(ssoServerConfig.getIsCheckSign()) { ssoServerTemplate.getSignTemplate(client).checkParamMap(paramMap); } else { SaSsoManager.printNoCheckSignWarningByRuntime(); } // 4、处理消息 SaSsoMessage message = new SaSsoMessage(paramMap); if( ! ssoServerTemplate.messageHolder.hasHandle(message.getType())) { return SaResult.error("未能找到消息处理器:" + message.getType()); } return ssoServerTemplate.handleMessage(message); } /** * 封装:单点注销成功后返回结果 * @param req SaRequest对象 * @param res SaResponse对象 * @return 返回结果 */ public Object _ssoLogoutBack(SaRequest req, SaResponse res) { return SaSsoProcessorHelper.ssoLogoutBack(req, res, ssoServerTemplate.paramName); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoClientStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.strategy; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaParamRetFunction; import cn.dev33.satoken.sso.function.SendRequestFunction; import cn.dev33.satoken.sso.function.TicketResultHandleFunction; import cn.dev33.satoken.util.SaResult; import java.util.Map; /** * Sa-Token SSO Client 相关策略 * * @author click33 * @since 1.43.0 */ public class SaSsoClientStrategy { /** * 发送 Http 请求的处理函数 */ public SendRequestFunction sendRequest = url -> { return SaManager.getSaHttpTemplate().get(url); }; /** * 自定义校验 ticket 返回值的处理逻辑 (每次从认证中心获取校验 ticket 的结果后调用) *

    参数:loginId, back *

    返回值:返回给前端的值 */ public TicketResultHandleFunction ticketResultHandle = null; /** * 转换:认证中心 centerId > 本地 loginId * *

    参数:认证中心 centerId *

    返回值:本地 loginId */ public SaParamRetFunction convertCenterIdToLoginId = (centerId) -> { return centerId; }; /** * 转换:本地 loginId > 认证中心 centerId * *

    参数:本地 loginId *

    返回值:认证中心 centerId */ public SaParamRetFunction convertLoginIdToCenterId = (loginId) -> { return loginId; }; /** * 发送 Http 请求,并将响应结果转换为 SaResult * * @param url 请求地址 * @return 返回的结果 */ public SaResult requestAsSaResult(String url) { String body = sendRequest.apply(url); Map map = SaManager.getSaJsonTemplate().jsonToMap(body); return new SaResult(map); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoServerStrategy.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.strategy; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.sso.function.CheckTicketAppendDataFunction; import cn.dev33.satoken.sso.function.DoLoginHandleFunction; import cn.dev33.satoken.sso.function.NotLoginViewFunction; import cn.dev33.satoken.sso.function.SendRequestFunction; import cn.dev33.satoken.util.SaResult; import java.util.Map; /** * Sa-Token SSO Server 相关策略 * * @author click33 * @since 1.43.0 */ public class SaSsoServerStrategy { /** * 发送 Http 请求的处理函数 */ public SendRequestFunction sendRequest = url -> { return SaManager.getSaHttpTemplate().get(url); }; /** * 使用异步模式执行一个任务 */ public SaParamFunction asyncRun = fun -> { new Thread(() -> { fun.run(); }).start(); }; /** * 未登录时返回的 View */ public NotLoginViewFunction notLoginView = () -> { return "当前会话在 SSO-Server 认证中心尚未登录(当前未配置登录视图)"; }; /** * SSO-Server端:登录函数 */ public DoLoginHandleFunction doLoginHandle = (name, pwd) -> { return SaResult.error(); }; /** * SSO-Server端:在授权重定向之前的通知 */ public SaParamFunction jumpToRedirectUrlNotice = (redirectUrl) -> { }; /** * SSO-Server端:在校验 ticket 后,给 sso-client 端追加返回信息的函数 */ public CheckTicketAppendDataFunction checkTicketAppendData = (loginId, result) -> { return result; }; /** * 发送 Http 请求,并将响应结果转换为 SaResult * * @param url 请求地址 * @return 返回的结果 */ public SaResult requestAsSaResult(String url) { String body = sendRequest.apply(url); Map map = SaManager.getSaJsonTemplate().jsonToMap(body); return new SaResult(map); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.template; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.template.SaSignTemplate; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.message.handle.client.SaSsoMessageLogoutCallHandle; import cn.dev33.satoken.sso.strategy.SaSsoClientStrategy; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.util.Map; /** * SSO 模板方法类 (Client端) * * @author click33 * @since 1.38.0 */ public class SaSsoClientTemplate extends SaSsoTemplate { /** * Client 相关策略 */ public SaSsoClientStrategy strategy = new SaSsoClientStrategy(); public SaSsoClientTemplate() { super.messageHolder.addHandle(new SaSsoMessageLogoutCallHandle()); } // ------------------- getData 相关 ------------------- /** * 根据配置的 getData 地址,查询数据 * * @param paramMap 查询参数 * @return 查询结果 */ public Object getData(Map paramMap) { String getDataUrl = getClientConfig().splicingGetDataUrl(); return getData(getDataUrl, paramMap); } /** * 根据自定义 path 地址,查询数据 (此方法需要配置 sa-token.sso.server-url 地址) * * @param path 自定义 path * @param paramMap 查询参数 * @return 查询结果 */ public Object getData(String path, Map paramMap) { String url = buildCustomPathUrl(path, paramMap); return strategy.sendRequest.apply(url); } /** * 构建URL:Server端 getData 地址,带签名等参数 * @param paramMap 查询参数 * @return / */ public String buildGetDataUrl(Map paramMap) { String getDataUrl = getClientConfig().getGetDataUrl(); return buildCustomPathUrl(getDataUrl, paramMap); } /** * 构建URL:Server 端自定义 path 地址,带签名等参数 (此方法需要配置 sa-token.sso.server-url 地址) * @param paramMap 请求参数 * @return / */ public String buildCustomPathUrl(String path, Map paramMap) { SaSsoClientConfig ssoConfig = getClientConfig(); // 构建 url // 如果 path 不是以 http 开头,那么就拼接上 serverUrl String url = path; if( ! url.startsWith("http") ) { String serverUrl = ssoConfig.getServerUrl(); SaSsoException.notEmpty(serverUrl, "请先配置 sa-token.sso-client.server-url 地址", SaSsoErrorCode.CODE_30012); url = SaFoxUtil.spliceTwoUrl(serverUrl, path); } // 构建参数字符串 paramMap.put(paramName.client, getClient()); String signParamsStr = getSignTemplate().addSignParamsAndJoin(paramMap); // 拼接 return SaFoxUtil.joinParam(url, signParamsStr); } // ---------------------- 构建交互地址 ---------------------- /** * 构建URL:Server端 单点登录授权地址, *
    形如:http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com * @param clientLoginUrl Client端登录地址 * @param back 回调路径 * @return [SSO-Server端-认证地址 ] */ public String buildServerAuthUrl(String clientLoginUrl, String back) { SaSsoClientConfig ssoConfig = getClientConfig(); // 服务端认证地址 String serverUrl = ssoConfig.splicingAuthUrl(); // 拼接客户端标识 String client = getClient(); if(SaFoxUtil.isNotEmpty(client)) { serverUrl = SaFoxUtil.joinParam(serverUrl, paramName.client, client); } // 对back地址编码 back = (back == null ? "" : back); back = SaFoxUtil.encodeUrl(back); // 开始拼接 sso 统一认证地址,形如:serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com /* * 部分 Servlet 版本 request.getRequestURL() 返回的 url 带有 query 参数,形如:http://domain.com?id=1, * 如果不加判断会造成最终生成的 serverAuthUrl 带有双 back 参数 ,这个 if 判断正是为了解决此问题 */ if( ! clientLoginUrl.contains(paramName.back + "=") ) { clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, paramName.back, back); } // 返回 return SaFoxUtil.joinParam(serverUrl, paramName.redirect, clientLoginUrl); } // ------------------- 消息推送 ------------------- /** * 向 sso-server 推送消息 * * @param message / * @return / */ public String pushMessage(SaSsoMessage message) { SaSsoClientConfig ssoConfig = getClientConfig(); // 拼接 push-url 地址 String pushUrl = ssoConfig.splicingPushUrl(); SaSsoException.notTrue(! SaFoxUtil.isUrl(pushUrl), "无效 push-url 地址:" + pushUrl, SaSsoErrorCode.CODE_30023); // 组织参数 message.set(paramName.client, getClient()); message.checkType(); String paramsStr = getSignTemplate().addSignParamsAndJoin(message); // 发起请求 String finalUrl = SaFoxUtil.joinParam(pushUrl, paramsStr); return strategy.sendRequest.apply(finalUrl); } /** * 向 sso-server 推送消息,并将返回值转为 SaResult * * @param message / * @return / */ public SaResult pushMessageAsSaResult(SaSsoMessage message) { String res = pushMessage(message); Map map = SaManager.getSaJsonTemplate().jsonToMap(res); return new SaResult(map); } /** * 构建消息:校验 ticket * * @param ticket ticket码 * @param ssoLogoutCallUrl 单点注销时的回调URL * @return 构建完毕的URL */ public SaSsoMessage buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl) { SaSsoClientConfig ssoConfig = getClientConfig(); SaSsoMessage message = new SaSsoMessage(); message.setType(SaSsoConsts.MESSAGE_CHECK_TICKET); message.set(paramName.client, getClient()); message.set(paramName.ticket, ticket); message.set(paramName.ssoLogoutCall, ssoLogoutCallUrl); return message; } /** * 构建消息:单点注销 * * @param loginId 要注销的账号 id * @param logoutParameter 单点注销 * @return 单点注销URL */ public SaSsoMessage buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter) { SaSsoMessage message = new SaSsoMessage(); message.setType(SaSsoConsts.MESSAGE_SIGNOUT); message.set(paramName.client, getClient()); message.set(paramName.loginId, loginId); message.set(paramName.deviceId, logoutParameter.getDeviceId()); return message; } // ------------------- Bean 对象获取 ------------------- /** * 获取底层使用的SsoClient配置对象 * @return / */ public SaSsoClientConfig getClientConfig() { return SaSsoManager.getClientConfig(); } /** * 获取当前项目 client 标识 * @return / */ public String getClient() { return getClientConfig().getClient(); } /** * 获取底层使用的 API 签名对象 * * @return / */ public SaSignTemplate getSignTemplate() { SaSignConfig signConfig = SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().copy(); // 使用 secretKey 的优先级:SSO 模块全局配置 > sign 模块默认配置 String secretKey = getClientConfig().getSecretKey(); if(SaFoxUtil.isEmpty(secretKey)) { secretKey = signConfig.getSecretKey(); } signConfig.setSecretKey(secretKey); return new SaSignTemplate(signConfig); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.template; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaResult; import java.util.Map; /** * SSO 模板方法类 (Client端) * * @author click33 * @since 1.38.0 */ public class SaSsoClientUtil { private SaSsoClientUtil() { } /** * 返回底层使用的 SaSsoClientTemplate 对象 * @return / */ public static SaSsoClientTemplate getSsoTemplate() { return SaSsoClientProcessor.instance.ssoClientTemplate; } // ------------------- getData 相关 ------------------- /** * 根据配置的 getData 地址,查询数据 * * @param paramMap 查询参数 * @return 查询结果 */ public static Object getData(Map paramMap) { return SaSsoClientProcessor.instance.ssoClientTemplate.getData(paramMap); } /** * 根据自定义 path 地址,查询数据 (此方法需要配置 sa-token.sso.server-url 地址) * * @param path 自定义 path * @param paramMap 查询参数 * @return 查询结果 */ public static Object getData(String path, Map paramMap) { return SaSsoClientProcessor.instance.ssoClientTemplate.getData(path, paramMap); } // ---------------------- 构建交互地址 ---------------------- /** * 构建URL:Server端 单点登录授权地址, *
    形如:http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com * @param clientLoginUrl Client端登录地址 * @param back 回调路径 * @return [SSO-Server端-认证地址 ] */ public static String buildServerAuthUrl(String clientLoginUrl, String back) { return SaSsoClientProcessor.instance.ssoClientTemplate.buildServerAuthUrl(clientLoginUrl, back); } // ------------------- 消息推送 ------------------- /** * 向 sso-server 推送消息 * * @param message / * @return / */ public static String pushMessage(SaSsoMessage message) { return SaSsoClientProcessor.instance.ssoClientTemplate.pushMessage(message); } /** * 向 sso-server 推送消息,并将返回值转为 SaResult * * @param message / * @return / */ public static SaResult pushMessageAsSaResult(SaSsoMessage message) { return SaSsoClientProcessor.instance.ssoClientTemplate.pushMessageAsSaResult(message); } /** * 构建消息:校验 ticket * * @param ticket ticket码 * @param ssoLogoutCallUrl 单点注销时的回调URL * @return 构建完毕的URL */ public static SaSsoMessage buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl) { return SaSsoClientProcessor.instance.ssoClientTemplate.buildCheckTicketMessage(ticket, ssoLogoutCallUrl); } /** * 构建消息:单点注销 * * @param loginId 要注销的账号 id * @param logoutParameter 单点注销 * @return 单点注销URL */ public static SaSsoMessage buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter) { return SaSsoClientProcessor.instance.ssoClientTemplate.buildSignoutMessage(loginId, logoutParameter); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.template; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.template.SaSignTemplate; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientModel; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.error.SaSsoErrorCode; import cn.dev33.satoken.sso.exception.SaSsoException; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.message.handle.server.SaSsoMessageCheckTicketHandle; import cn.dev33.satoken.sso.message.handle.server.SaSsoMessageSignoutHandle; import cn.dev33.satoken.sso.model.SaSsoClientInfo; import cn.dev33.satoken.sso.model.TicketModel; import cn.dev33.satoken.sso.strategy.SaSsoServerStrategy; import cn.dev33.satoken.sso.util.SaSsoConsts; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; import java.util.*; /** * SSO 模板方法类 (Server端) * * @author click33 * @since 1.38.0 */ public class SaSsoServerTemplate extends SaSsoTemplate { /** * Server 相关策略 */ public SaSsoServerStrategy strategy = new SaSsoServerStrategy(); public SaSsoServerTemplate() { super.messageHolder.addHandle(new SaSsoMessageCheckTicketHandle()); super.messageHolder.addHandle(new SaSsoMessageSignoutHandle()); } // ---------------------- Ticket 操作 ---------------------- // 增删改 /** * 保存 Ticket * @param ticketModel / */ public void saveTicket(TicketModel ticketModel) { long ticketTimeout = getServerConfig().getTicketTimeout(); SaManager.getSaTokenDao().setObject(splicingTicketModelSaveKey(ticketModel.getTicket()), ticketModel, ticketTimeout); } /** * 删除 Ticket * @param ticket Ticket码 */ public void deleteTicket(String ticket) { if(ticket == null) { return; } SaManager.getSaTokenDao().deleteObject(splicingTicketModelSaveKey(ticket)); } /** * 根据参数创建一个 ticket 码 * * @param client 客户端标识 * @param loginId 账号 id * @param tokenValue 会话 Token * @return Ticket码 */ public TicketModel createTicket(String client, Object loginId, String tokenValue) { TicketModel ticketModel = new TicketModel(); ticketModel.setTicket(randomTicket(loginId)); ticketModel.setClient(client); ticketModel.setLoginId(loginId); ticketModel.setTokenValue(tokenValue); return ticketModel; } /** * 根据参数创建一个 ticket 码,并保存 * * @param client 客户端标识 * @param loginId 账号 id * @param tokenValue 会话 Token * @return Ticket码 */ public String createTicketAndSave(String client, Object loginId, String tokenValue) { // 创建 TicketModel ticketModel = createTicket(client, loginId, tokenValue); // 保存 saveTicket(ticketModel); saveTicketIndex(client, loginId, ticketModel.getTicket()); // 返回 return ticketModel.getTicket(); } /** * 随机一个 Ticket 码 * @param loginId 账号id * @return Ticket 码 */ public String randomTicket(Object loginId) { return SaFoxUtil.getRandomString(64); } // 查 /** * 查询 ticket ,如果 ticket 无效则返回 null * * @param ticket Ticket码 * @return 账号id */ public TicketModel getTicket(String ticket) { if(SaFoxUtil.isEmpty(ticket)) { return null; } return SaManager.getSaTokenDao().getObject(splicingTicketModelSaveKey(ticket), TicketModel.class); } /** * 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null * @param ticket Ticket码 * @return 账号id */ public Object getLoginId(String ticket) { TicketModel ticketModel = getTicket(ticket); if(ticketModel == null) { return null; } return ticketModel.getLoginId(); } /** * 查询 ticket 指向的 loginId,并转换为指定类型 * @param 要转换的类型 * @param ticket Ticket码 * @param cs 要转换的类型 * @return 账号id */ public T getLoginId(String ticket, Class cs) { return SaFoxUtil.getValueByType(getLoginId(ticket), cs); } // 校验 /** * 校验 Ticket,无效 ticket 会抛出异常 * * @param ticket Ticket码 * @return / */ public TicketModel checkTicket(String ticket) { TicketModel ticketModel = getTicket(ticket); if(ticketModel == null) { throw new SaSsoException("无效 ticket : " + ticket).setCode(SaSsoErrorCode.CODE_30004); } return ticketModel; } /** * 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 * @param ticket Ticket码 * @return 账号id */ public TicketModel checkTicketParamAndDelete(String ticket) { return checkTicketParamAndDelete(ticket, SaSsoConsts.CLIENT_WILDCARD); } /** * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 * * @param ticket Ticket码 * @param client client 标识 * @return / */ public TicketModel checkTicketParamAndDelete(String ticket, String client) { TicketModel ticketModel = checkTicket(ticket); // 校验 client 参数是否正确,即:创建 ticket 的 client 和当前校验 ticket 的 client 是否一致 String ticketClient = ticketModel.getClient(); if(SaSsoConsts.CLIENT_WILDCARD.equals(client)) { // 如果提供的是通配符,直接越过 client 校验 } else if (SaFoxUtil.isEmpty(client) && SaFoxUtil.isEmpty(ticketClient)) { // 如果提供的和期望的两者均为空,则通过校验 } else { // 开始详细比对 if(SaFoxUtil.notEquals(client, ticketClient)) { throw new SaSsoException("该 ticket 不属于 client=" + client + ", ticket 值: " + ticket).setCode(SaSsoErrorCode.CODE_30011); } } // 删除 ticket 信息,使其只有一次性有效 deleteTicket(ticket); deleteTicketIndex(client, ticketModel.getLoginId()); // return ticketModel; } // ticket 索引 /** * 保存 Ticket 索引 (id 反查 ticket) * * @param client 应用端 * @param ticket ticket码 * @param loginId 账号id */ public void saveTicketIndex(String client, Object loginId, String ticket) { long ticketTimeout = getServerConfig().getTicketTimeout(); SaManager.getSaTokenDao().set(splicingTicketIndexKey(client, loginId), String.valueOf(ticket), ticketTimeout); } /** * 删除 Ticket 索引 * * @param client 应用标识 * @param loginId 账号id */ public void deleteTicketIndex(String client, Object loginId) { if(loginId == null) { return; } SaManager.getSaTokenDao().delete(splicingTicketIndexKey(client, loginId)); } /** * 查询 指定 client、loginId 其所属的 ticket 值 * * @param client 应用 * @param loginId 账号id * @return Ticket值 */ public String getTicketValue(String client, Object loginId) { if(loginId == null) { return null; } return SaManager.getSaTokenDao().get(splicingTicketIndexKey(client, loginId)); } // ---------------------- Client 信息获取 ---------------------- /** * 获取所有 Client * * @return / */ public List getClients() { return new ArrayList<>(getServerConfig().getClients().values()); } /** * 获取应用信息,无效 client 返回 null * * @param client / * @return / */ public SaSsoClientModel getClient(String client) { return getServerConfig().getClients().get(client); } /** * 获取应用信息,无效 client 则抛出异常 * * @param client / * @return / */ public SaSsoClientModel getClientNotNull(String client) { if(SaFoxUtil.isEmpty(client)) { if(getConfigOfAllowAnonClient()) { return getAnonClient(); } else { throw new SaSsoException("client 标识不可为空"); } } else { SaSsoClientModel scm = getClient(client); if(scm == null) { throw new SaSsoException("未能获取应用信息,client=" + client).setCode(SaSsoErrorCode.CODE_30013); } return scm; } } /** * 获取配置项:是否允许匿名 client 接入 * * @return / */ public boolean getConfigOfAllowAnonClient() { return getServerConfig().getAllowAnonClient(); } /** * 获取匿名 client 配置信息 * * @return / */ public SaSsoClientModel getAnonClient() { SaSsoServerConfig serverConfig = getServerConfig(); SaSsoClientModel scm = new SaSsoClientModel(); scm.setAllowUrl(serverConfig.getAllowUrl()); scm.setIsSlo(serverConfig.getIsSlo()); scm.setSecretKey(serverConfig.getSecretKey()); if(SaFoxUtil.isEmpty(scm.getSecretKey())) { scm.setSecretKey(SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().getSecretKey()); } return scm; } /** * 获取所有需要接收消息推送的 Client * * @return / */ public List getNeedPushClients() { List list = new ArrayList<>(); List clients = getClients(); for(SaSsoClientModel scm : clients) { if (scm.getIsPush()) { list.add(scm); } } return list; } // ------------------- 重定向 URL 构建与校验 ------------------- /** * 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 * * @param client 客户端标识 * @param redirect sso-client 端的重定向地址 * @param loginId 账号 id * @param tokenValue 会话 token * @return / */ public String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) { // 校验 重定向地址 是否合法 checkRedirectUrl(client, redirect); // 删掉 旧Ticket deleteTicket(getTicketValue(client, loginId)); // 创建 新Ticket String ticket = createTicketAndSave(client, loginId, tokenValue); // 构建 授权重定向地址 (Server端 根据此地址向 Client端 下放 Ticket) return SaFoxUtil.joinParam(encodeBackParam(redirect), paramName.ticket, ticket); } /** * 对 url 中的 back 参数进行 URL 编码, 解决超链接重定向后参数丢失的 bug * * @param url url * @return 编码过后的url */ public String encodeBackParam(String url) { // 获取back参数所在位置 int index = url.indexOf("?" + paramName.back + "="); if(index == -1) { index = url.indexOf("&" + paramName.back + "="); if(index == -1) { return url; } } // 开始编码 int length = paramName.back.length() + 2; String back = url.substring(index + length); back = SaFoxUtil.encodeUrl(back); // 放回url中 url = url.substring(0, index + length) + back; return url; } /** * 校验重定向 url 合法性 * * @param client 应用标识 * @param url 下放ticket的url地址 */ public void checkRedirectUrl(String client, String url) { // 1、是否是一个有效的url if( ! SaFoxUtil.isUrl(url) ) { throw new SaSsoException("无效redirect:" + url).setCode(SaSsoErrorCode.CODE_30001); } // 2、截取掉?后面的部分 int qIndex = url.indexOf("?"); if(qIndex != -1) { url = url.substring(0, qIndex); } // 3、不允许出现@字符 if(url.contains("@")) { // 为什么不允许出现 @ 字符呢,因为这有可能导致 redirect 参数绕过 AllowUrl 列表的校验 // // 举个例子 配置文件: // sa-token.sso-server.allow-url=http://sa-sso-client1.com* // // 开发者原意是为了允许 sa-sso-client1.com 下的所有地址都可以下放ticket // // 但是如果攻击者精心构建一个url: // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com@sa-token.cc // // 那么这个url就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: // http://sa-token.cc/?ticket=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul // // 造成了ticket 参数劫持 // 所以此处需要禁止在 url 中出现 @ 字符 // // 这么一刀切的做法,可能会导致一些特殊的正常url也无法通过校验,例如: // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com:9003/@getInfo // // 但是为了安全起见,这么做还是有必要的 throw new SaSsoException("无效redirect(不允许出现@字符):" + url).setCode(SaSsoErrorCode.CODE_30001); } // 4、判断是否在 [ 允许的地址列表 ] 之中 String allowUrlString = getClientNotNull(client).getAllowUrl(); List allowUrlList = Arrays.asList(allowUrlString.replaceAll(" ", "").split(",")); checkAllowUrlList(allowUrlList); if( ! SaStrategy.instance.hasElement.apply(allowUrlList, url) ) { throw new SaSsoException("非法redirect:" + url).setCode(SaSsoErrorCode.CODE_30002); } // 校验通过 √ } /** * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 * @param allowUrlList 待校验的 allow-url 地址列表 */ public void checkAllowUrlList(List allowUrlList){ checkAllowUrlListStaticMethod(allowUrlList); } /** * 校验配置的 AllowUrl 是否合规,如果不合规则抛出异常 * @param allowUrlList 待校验的 allow-url 地址列表 */ public static void checkAllowUrlListStaticMethod(List allowUrlList){ for (String url : allowUrlList) { int index = url.indexOf("*"); // 如果配置了 * 字符,则必须出现在最后一位,否则属于无效配置项 if(index != -1 && index != url.length() - 1) { // 为什么不允许 * 字符出现在中间位置呢,因为这有可能导致 redirect 参数绕过 allow-url 列表的校验 // // 举个例子 配置文件: // sa-token.sso-server.allow-url=http://*.sa-sso-client1.com // // 开发者原意是为了允许 sa-sso-client1.com 下的所有子域名都可以下放ticket // 例如:http://shop.sa-sso-client1.com // // 但是如果攻击者精心构建一个url: // http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-token.cc/a.sa-sso-client1.com/sso/login // // 那么这个 url 就会绕过 allow-url 的校验,ticket 被下发到了第三方服务器地址: // http://sa-token.cc/a.sa-sso-client1.com/sso/login?ticket=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx // // 造成了 ticket 参数劫持 // 所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符(出现在末尾是没有问题的) // // 这么一刀切的做法,可能会导致正常场景下的子域名url也无法通过校验,例如: // http://sa-sso-server.com:9000/sso/auth?redirect=http://shop.sa-sso-client1.com/sso/login // // 但是为了安全起见,这么做还是有必要的 throw new SaSsoException("无效的 allow-url 配置(*通配符只允许出现在最后一位):" + url).setCode(SaSsoErrorCode.CODE_30015); } } } // ------------------- 单点注销 ------------------- /** * 为指定账号 id 注册应用接入信息(模式三) * * @param loginId 账号id * @param client 指定客户端标识,可为null * @param sloCallbackUrl 单点注销时的回调URL */ public void registerSloCallbackUrl(Object loginId, String client, String sloCallbackUrl) { // 如果提供的参数是空值,则直接返回,不进行任何操作 if(SaFoxUtil.isEmpty(loginId)) { return; } SaSession session = getStpLogicOrGlobal().getSessionByLoginId(loginId); // 取出原来的 List scmList = session.get(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, ArrayList::new); // 将 新登录client 加入到集合中 SaSsoClientInfo scm = new SaSsoClientInfo(client, sloCallbackUrl, calcNextIndex(scmList)); scmList.add(scm); // 如果登录的client数量超过了限制,则从最早的一个登录开始清退 int maxRegClient = getServerConfig().maxRegClient; if(maxRegClient != -1) { for (;;) { if(scmList.size() > maxRegClient) { SaSsoClientInfo removeScm = scmList.remove(0); strategy.asyncRun.run(() -> { notifyClientLogout(loginId, null, removeScm, true, true); }); } else { break; } } } // 存入持久库 session.set(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, scmList); } /** * 计算下一个 index 值 * @param scmList / * @return / */ public int calcNextIndex(List scmList) { // 如果目前还没有任何登录记录,则直接返回0 if(scmList == null || scmList.isEmpty()) { return 0; } // 获取目前最大的index值 int maxIndex = scmList.get(scmList.size() - 1).index; // 如果已经是 int 最大值了,则直接返回0 if(maxIndex == Integer.MAX_VALUE) { return 0; } // 否则返回最大值+1 maxIndex++; return maxIndex; } /** * 指定账号单点注销 * * @param loginId 指定账号 */ public void ssoLogout(Object loginId) { ssoLogout(loginId, getStpLogicOrGlobal().createSaLogoutParameter(), null); } /** * 指定账号单点注销 * * @param loginId 指定账号 * @param logoutParameter 注销参数 * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 */ public void ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) { // 1、消息推送:单点注销 pushToAllClientByLogoutCall(loginId, logoutParameter, ignoreClient); // 2、SaSession 挂载的 Client 端注销会话 SaSession session = getStpLogicOrGlobal().getSessionByLoginId(loginId, false); if(session == null) { return; } List scmList = session.get(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, ArrayList::new); scmList.forEach(scm -> { strategy.asyncRun.run(() -> { notifyClientLogout(loginId, logoutParameter.getDeviceId(), scm, false, false); }); }); // 3、Server 端本身注销 getStpLogicOrGlobal().logout(loginId, logoutParameter); } /** * 通知指定账号的指定客户端注销 * * @param loginId 指定账号 * @param deviceId 指定设备 id * @param scm 客户端信息对象 * @param autoLogout 是否为超过 maxRegClient 的自动注销 * @param isPushWork 如果该 client 没有注册注销回调地址,是否使用 push 消息的方式进行注销回调通知 * * @return / */ public String notifyClientLogout(Object loginId, String deviceId, SaSsoClientInfo scm, boolean autoLogout, boolean isPushWork) { // 如果给个null值,不进行任何操作 if(scm == null || scm.mode != SaSsoConsts.SSO_MODE_3) { return null; } // 如果此 Client 并没有注册 单点注销 回调地址 String sloCallUrl = scm.getSloCallbackUrl(); if(SaFoxUtil.isEmpty(sloCallUrl)) { if(isPushWork && SaFoxUtil.isNotEmpty(scm.getClient())) { SaSsoClientModel client = getClient(scm.getClient()); return pushToClientByLogoutCall(client, loginId, true, getStpLogicOrGlobal().createSaLogoutParameter()); } return null; } // 参数 Map paramsMap = new TreeMap<>(); paramsMap.put(paramName.client, scm.getClient()); paramsMap.put(paramName.loginId, loginId); paramsMap.put(paramName.deviceId, deviceId); paramsMap.put(paramName.autoLogout, autoLogout); String signParamsStr = getSignTemplate(scm.getClient()).addSignParamsAndJoin(paramsMap); // 拼接 String finalUrl = SaFoxUtil.joinParam(sloCallUrl, signParamsStr); // 发起请求 return strategy.sendRequest.apply(finalUrl); } // ------------------- 消息推送 ------------------- /** * 向指定 Client 推送消息 * @param clientModel / * @param message / * @return / */ public String pushMessage(SaSsoClientModel clientModel, SaSsoMessage message) { message.checkType(); String noticeUrl = clientModel.splicingPushUrl(); String paramsStr = getSignTemplate(clientModel.getClient()).addSignParamsAndJoin(message); String finalUrl = SaFoxUtil.joinParam(noticeUrl, paramsStr); return strategy.sendRequest.apply(finalUrl); } /** * 向指定 client 推送消息,并将返回值转为 SaResult * * @param clientModel / * @param message / * @return / */ public SaResult pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message) { String res = pushMessage(clientModel, message); Map map = SaManager.getSaJsonTemplate().jsonToMap(res); return new SaResult(map); } /** * 向指定 Client 推送消息 * @param client / * @param message / * @return / */ public String pushMessage(String client, SaSsoMessage message) { return pushMessage(getClientNotNull(client), message); } /** * 向指定 client 推送消息,并将返回值转为 SaResult * * @param client / * @param message / * @return / */ public SaResult pushMessageAsSaResult(String client, SaSsoMessage message) { String res = pushMessage(client, message); Map map = SaManager.getSaJsonTemplate().jsonToMap(res); return new SaResult(map); } /** * 向所有 Client 推送消息 * * @param message / */ public void pushToAllClient(SaSsoMessage message) { pushToAllClient(message, null); } /** * 向所有 Client 推送消息,并忽略掉某个 client * * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 * @param message / */ public void pushToAllClient(SaSsoMessage message, String ignoreClient) { List needPushClients = getNeedPushClients(); for (SaSsoClientModel client : needPushClients) { if(SaFoxUtil.isNotEmpty(ignoreClient) && ignoreClient.equals(client.getClient())) { continue; } strategy.asyncRun.run(() -> pushMessage(client, message)); } } /** * 向所有 Client 推送消息:单点注销回调 * * @param loginId / * @param logoutParameter 注销参数 * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 */ public void pushToAllClientByLogoutCall(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) { List npClients = getNeedPushClients(); for (SaSsoClientModel client : npClients) { if(SaFoxUtil.isNotEmpty(ignoreClient) && ignoreClient.equals(client.getClient())) { continue; } if(client.getIsSlo()) { strategy.asyncRun.run(() -> { pushToClientByLogoutCall(client, loginId, false, logoutParameter); }); } } } /** * 向指定 Client 推送消息:单点注销回调 * * @param client 应用 * @param loginId / * @param autoLogout 是否为超过 maxRegClient 的自动注销 * @param logoutParameter 注销参数 * @return / */ public String pushToClientByLogoutCall(SaSsoClientModel client, Object loginId, boolean autoLogout, SaLogoutParameter logoutParameter) { SaSsoMessage message = new SaSsoMessage(); message.setType(SaSsoConsts.MESSAGE_LOGOUT_CALL); message.set(paramName.loginId, loginId); message.set(paramName.autoLogout, autoLogout); message.set(paramName.deviceId, logoutParameter.getDeviceId()); return pushMessage(client, message); } // ------------------- Bean 获取 ------------------- /** * 获取底层使用的SsoServer配置对象 * @return / */ public SaSsoServerConfig getServerConfig() { return SaSsoManager.getServerConfig(); } /** * 获取底层使用的 API 签名对象 * @param client 指定客户端标识,填 null 代表获取默认的 * @return / */ public SaSignTemplate getSignTemplate(String client) { SaSignConfig signConfig = SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().copy(); SaSsoClientModel clientModel = getClientNotNull(client); // 使用 secretKey 的优先级:client 单独配置 > SSO 模块全局配置 > sign 模块默认配置 String secretKey = clientModel.getSecretKey(); if (SaFoxUtil.isEmpty(secretKey) && SaFoxUtil.isNotEmpty(client)) { secretKey = getServerConfig().getSecretKey(); } if(SaFoxUtil.isEmpty(secretKey)) { secretKey = signConfig.getSecretKey(); } signConfig.setSecretKey(secretKey); return new SaSignTemplate(signConfig); } // ------------------- 返回相应key ------------------- /** * 拼接key:TicketModel * @param ticket ticket值 * @return key */ public String splicingTicketModelSaveKey(String ticket) { return getStpLogicOrGlobal().getConfigOrGlobal().getTokenName() + ":ticket:" + ticket; } /** * 拼接key:Ticket 索引 * * @param client 应用标识 * @param id 账号id * @return key */ public String splicingTicketIndexKey(String client, Object id) { if(SaFoxUtil.isEmpty(client) || SaSsoConsts.CLIENT_WILDCARD.equals(client)) { client = SaSsoConsts.CLIENT_ANON; } return getStpLogicOrGlobal().getConfigOrGlobal().getTokenName() + ":ticket-index:" + client + ":" + id; } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.template; import cn.dev33.satoken.sso.config.SaSsoClientModel; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.model.TicketModel; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.stp.parameter.SaLogoutParameter; import cn.dev33.satoken.util.SaResult; import java.util.List; /** * SSO 工具类 (Server端) * * @author click33 * @since 1.43.0 */ public class SaSsoServerUtil { private SaSsoServerUtil() { } /** * 返回底层使用的 SaSsoServerTemplate 对象 * @return / */ public static SaSsoServerTemplate getSsoTemplate() { return SaSsoServerProcessor.instance.ssoServerTemplate; } // ---------------------- Ticket 操作 ---------------------- // 增删改 /** * 删除 Ticket * @param ticket Ticket码 */ public static void deleteTicket(String ticket) { SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicket(ticket); } /** * 根据参数创建一个 ticket 码,并保存 * * @param client 客户端标识 * @param loginId 账号 id * @param tokenValue 会话 Token * @return Ticket码 */ public static String createTicketAndSave(String client, Object loginId, String tokenValue) { return SaSsoServerProcessor.instance.ssoServerTemplate.createTicketAndSave(client, loginId, tokenValue); } // 查 /** * 查询 ticket ,如果 ticket 无效则返回 null * * @param ticket Ticket码 * @return 账号id */ public static TicketModel getTicket(String ticket) { return SaSsoServerProcessor.instance.ssoServerTemplate.getTicket(ticket); } /** * 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null * @param ticket Ticket码 * @return 账号id */ public static Object getLoginId(String ticket) { return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket); } /** * 查询 ticket 指向的 loginId,并转换为指定类型 * @param 要转换的类型 * @param ticket Ticket码 * @param cs 要转换的类型 * @return 账号id */ public static T getLoginId(String ticket, Class cs) { return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket, cs); } // 校验 /** * 校验 Ticket,无效 ticket 会抛出异常 * * @param ticket Ticket码 * @return / */ public static TicketModel checkTicket(String ticket) { return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicket(ticket); } /** * 校验 Ticket 码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 * @param ticket Ticket码 * @return 账号id */ public static TicketModel checkTicketParamAndDelete(String ticket) { return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket); } /** * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 * * @param ticket Ticket码 * @param client client 标识 * @return / */ public static TicketModel checkTicketParamAndDelete(String ticket, String client) { return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, client); } // ticket 索引 /** * 查询 指定 client、loginId 其所属的 ticket 值 * * @param client 应用 * @param loginId 账号id * @return Ticket值 */ public static String getTicketValue(String client, Object loginId) { return SaSsoServerProcessor.instance.ssoServerTemplate.getTicketValue(client, loginId); } // ---------------------- Client 信息获取 ---------------------- /** * 获取所有 Client * * @return / */ public static List getClients() { return SaSsoServerProcessor.instance.ssoServerTemplate.getClients(); } /** * 获取应用信息,无效 client 返回 null * * @param client / * @return / */ public static SaSsoClientModel getClient(String client) { return SaSsoServerProcessor.instance.ssoServerTemplate.getClient(client); } /** * 获取应用信息,无效 client 则抛出异常 * * @param client / * @return / */ public static SaSsoClientModel getClientNotNull(String client) { return SaSsoServerProcessor.instance.ssoServerTemplate.getClientNotNull(client); } /** * 获取匿名 client 信息 * * @return / */ public static SaSsoClientModel getAnonClient() { return SaSsoServerProcessor.instance.ssoServerTemplate.getAnonClient(); } /** * 获取所有需要接收消息推送的 Client * * @return / */ public static List getNeedPushClients() { return SaSsoServerProcessor.instance.ssoServerTemplate.getNeedPushClients(); } // ------------------- 重定向 URL 构建与校验 ------------------- /** * 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 * * @param client 客户端标识 * @param redirect sso-client 端的重定向地址 * @param loginId 账号 id * @param tokenValue 会话 token * @return / */ public static String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) { return SaSsoServerProcessor.instance.ssoServerTemplate.buildRedirectUrl(client, redirect, loginId, tokenValue); } /** * 校验重定向 url 合法性 * * @param client 应用标识 * @param url 下放ticket的url地址 */ public static void checkRedirectUrl(String client, String url) { SaSsoServerProcessor.instance.ssoServerTemplate.checkRedirectUrl(client, url); } // ------------------- 单点注销 ------------------- /** * 指定账号单点注销 * * @param loginId 指定账号 */ public static void ssoLogout(Object loginId) { SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId); } /** * 指定账号单点注销 * * @param loginId 指定账号 * @param logoutParameter 注销参数 * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 */ public static void ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) { SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId, logoutParameter, ignoreClient); } // ------------------- 消息推送 ------------------- /** * 向指定 Client 推送消息 * @param clientModel / * @param message / * @return / */ public static String pushMessage(SaSsoClientModel clientModel, SaSsoMessage message) { return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessage(clientModel, message); } /** * 向指定 client 推送消息,并将返回值转为 SaResult * * @param clientModel / * @param message / * @return / */ public static SaResult pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message) { return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessageAsSaResult(clientModel, message); } /** * 向指定 Client 推送消息 * @param client / * @param message / * @return / */ public static String pushMessage(String client, SaSsoMessage message) { return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessage(client, message); } /** * 向指定 client 推送消息,并将返回值转为 SaResult * * @param client / * @param message / * @return / */ public static SaResult pushMessageAsSaResult(String client, SaSsoMessage message) { return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessageAsSaResult(client, message); } /** * 向所有 Client 推送消息 * * @param message / */ public static void pushToAllClient(SaSsoMessage message) { SaSsoServerProcessor.instance.ssoServerTemplate.pushToAllClient(message); } /** * 向所有 Client 推送消息,并忽略掉某个 client * * @param ignoreClient 要被忽略掉的 client,填 null 代表不忽略 * @param message / */ public static void pushToAllClient(SaSsoMessage message, String ignoreClient) { SaSsoServerProcessor.instance.ssoServerTemplate.pushToAllClient(message, ignoreClient); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.template; import cn.dev33.satoken.sso.message.SaSsoMessage; import cn.dev33.satoken.sso.message.SaSsoMessageHolder; import cn.dev33.satoken.sso.name.ApiName; import cn.dev33.satoken.sso.name.ParamName; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; /** * SSO 模板方法类 (公共端) * * @author click33 * @since 1.30.0 */ public class SaSsoTemplate { // ---------------------- 全局配置 ---------------------- /** * 所有 API 名称 */ public ApiName apiName = new ApiName(); /** * 所有参数名称 */ public ParamName paramName = new ParamName(); /** * @param paramName 替换 paramName 对象 * @return 对象自身 */ public SaSsoTemplate setParamName(ParamName paramName) { this.paramName = paramName; return this; } /** * @param apiName 替换 apiName 对象 * @return 对象自身 */ public SaSsoTemplate setApiName(ApiName apiName) { this.apiName = apiName; return this; } /** * 底层使用的 StpLogic 对象 */ StpLogic stpLogic; /** * 写入底层使用的会话对象 * * @param stpLogic / * @return / */ public SaSsoTemplate setStpLogic(StpLogic stpLogic) { this.stpLogic = stpLogic; return this; } /** * 获取底层使用的会话对象 * @return / */ public StpLogic getStpLogic() { return this.stpLogic; } /** * 获取底层使用的会话对象,如果没有配置则返回全局默认 StpLogic * @return / */ public StpLogic getStpLogicOrGlobal() { StpLogic stpLogic = getStpLogic(); if (stpLogic == null) { return StpUtil.stpLogic; } return stpLogic; } // ----------- 消息处理 /** * SSO 消息处理器 - 持有器 */ public SaSsoMessageHolder messageHolder = new SaSsoMessageHolder(); /** * 处理指定消息 * * @param message / */ public Object handleMessage(SaSsoMessage message) { return messageHolder.handleMessage(this, message); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.template; import cn.dev33.satoken.sso.model.TicketModel; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import java.util.Map; /** * Sa-Token-SSO 单点登录模块 工具类 * *

    请更换为 SaSsoServerUtil 或 SaSsoClientUtil

    * * @author click33 * @since 1.30.0 */ @Deprecated public class SaSsoUtil { // ---------------------- Ticket 操作 ---------------------- /** * 根据参数创建一个 ticket 码 * * @param client 客户端标识 * @param loginId 账号 id * @param deviceId 设备 id * @return Ticket码 */ public static String createTicket(String client, Object loginId, String deviceId) { return SaSsoServerProcessor.instance.ssoServerTemplate.createTicketAndSave(client, loginId, deviceId); } /** * 删除 Ticket * @param ticket Ticket码 */ public static void deleteTicket(String ticket) { SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicket(ticket); } /** * 删除 Ticket索引 * @param client 应用 id * @param loginId 账号id */ public static void deleteTicketIndex(String client, Object loginId) { SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicketIndex(client, loginId); } /** * 根据 Ticket码 获取账号id,如果Ticket码无效则返回null * @param ticket Ticket码 * @return 账号id */ public static Object getLoginId(String ticket) { return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket); } /** * 根据 Ticket码 获取账号id,并转换为指定类型 * @param 要转换的类型 * @param ticket Ticket码 * @param cs 要转换的类型 * @return 账号id */ public static T getLoginId(String ticket, Class cs) { return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket, cs); } /** * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 * @param ticket Ticket码 * @return 账号id */ public static TicketModel checkTicket(String ticket) { return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket); } /** * 校验ticket码,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除 * @param ticket Ticket码 * @param client client 标识 * @return 账号id */ public static TicketModel checkTicket(String ticket, String client) { return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, client); } /** * 校验重定向url合法性 * * @param client 应用标识 * @param url 下放ticket的url地址 */ public static void checkRedirectUrl(String client, String url) { SaSsoServerProcessor.instance.ssoServerTemplate.checkRedirectUrl(client, url); } // ------------------- SSO 模式三 ------------------- /** * 为指定账号id注册单点注销回调URL * @param loginId 账号id * @param client 指定客户端标识,可为null * @param sloCallbackUrl 单点注销时的回调URL */ public static void registerSloCallbackUrl(Object loginId, String client, String sloCallbackUrl) { SaSsoServerProcessor.instance.ssoServerTemplate.registerSloCallbackUrl(loginId, client, sloCallbackUrl); } /** * 指定账号单点注销 (以Server方发起) * @param loginId 指定账号 */ public static void ssoLogout(Object loginId) { SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId); } /** * 获取:查询数据 * @param paramMap 查询参数 * @return 查询结果 */ public static Object getData(Map paramMap) { return SaSsoClientProcessor.instance.ssoClientTemplate.getData(paramMap); } /** * 根据自定义 path 查询数据 (此方法需要配置 sa-token.sso.server-url 地址) * @param path 自定义 path * @param paramMap 查询参数 * @return 查询结果 */ public static Object getData(String path, Map paramMap) { return SaSsoClientProcessor.instance.ssoClientTemplate.getData(path, paramMap); } // ---------------------- 构建URL ---------------------- /** * 构建URL:Server端 单点登录地址 * @param clientLoginUrl Client端登录地址 * @param back 回调路径 * @return [SSO-Server端-认证地址 ] */ public static String buildServerAuthUrl(String clientLoginUrl, String back) { return SaSsoClientProcessor.instance.ssoClientTemplate.buildServerAuthUrl(clientLoginUrl, back); } /** * 构建 URL:sso-server 端向 sso-client 下放 ticket 的地址 * * @param client 客户端标识 * @param redirect sso-client 端的重定向地址 * @param loginId 账号 id * @param tokenValue 会话 token * @return / */ public static String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) { return SaSsoServerProcessor.instance.ssoServerTemplate.buildRedirectUrl(client, redirect, loginId, tokenValue); } /** * 构建URL:Server端 getData 地址,带签名等参数 * @param paramMap 查询参数 * @return / */ public static String buildGetDataUrl(Map paramMap) { return SaSsoClientProcessor.instance.ssoClientTemplate.buildGetDataUrl(paramMap); } /** * 构建URL:Server 端自定义 path 地址,带签名等参数 (此方法需要配置 sa-token.sso.server-url 地址) * @param paramMap 请求参数 * @return / */ public static String buildCustomPathUrl(String path, Map paramMap) { return SaSsoClientProcessor.instance.ssoClientTemplate.buildCustomPathUrl(path, paramMap); } } ================================================ FILE: sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/util/SaSsoConsts.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.sso.util; /** * Sa-Token-SSO模块相关常量 * * @author click33 * @since 1.30.0 */ public class SaSsoConsts { /** Client端单点注销回调URL的Set集合,存储在Session中使用的key */ @Deprecated public static final String SLO_CALLBACK_SET_KEY = "SLO_CALLBACK_SET_KEY_"; /** Client 端 Model 信息的 List 集合,存储在 SaSession 中使用的key */ public static final String SSO_CLIENT_MODEL_LIST_KEY_ = "SSO_CLIENT_MODEL_LIST_KEY_"; /** 表示OK的返回结果 */ public static final String OK = "ok"; /** 表示自己 */ public static final String SELF = "self"; /** 表示简单模式(SSO模式一) */ public static final String MODE_SIMPLE = "simple"; /** 表示ticket模式(SSO模式二和模式三) */ public static final String MODE_TICKET = "ticket"; /** 表示请求没有得到任何有效处理 {msg: "not handle"} */ public static final String NOT_HANDLE = "{\"msg\": \"not handle\"}"; /** client 身份,* 代表通配,可以解析出所有 client 的 ticket */ public static final String CLIENT_WILDCARD = "*"; /** client 身份,代表匿名 client */ public static final String CLIENT_ANON = "anon"; /** SSO 模式1 */ public static final int SSO_MODE_1 = 1; /** SSO 模式2 */ public static final int SSO_MODE_2 = 2; /** SSO 模式3 */ public static final int SSO_MODE_3 = 3; /** 消息类型:校验 ticket */ public static final String MESSAGE_CHECK_TICKET = "checkTicket"; /** 消息类型:单点注销 */ public static final String MESSAGE_SIGNOUT = "signout"; /** 消息类型:单点注销回调 */ public static final String MESSAGE_LOGOUT_CALL = "logoutCall"; } ================================================ FILE: sa-token-plugin/sa-token-temp-jwt/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-temp-jwt sa-token-temp-jwt sa-token-temp-jwt cn.dev33 sa-token-core io.jsonwebtoken jjwt javax.xml.bind jaxb-api 2.3.1 ================================================ FILE: sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.temp.jwt.SaTempTemplateForJwt; /** * SaToken 插件安装:临时 token 生成器 - Jwt 版 * * @author click33 * @since 1.41.0 */ public class SaTokenPluginForTempForJwt implements SaTokenPlugin { @Override public void install() { SaManager.setSaTempTemplate(new SaTempTemplateForJwt()); } } ================================================ FILE: sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.temp.jwt; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; /** * jwt 相关操作工具类,封装一下 * * @author click33 * @since 1.20.0 */ public class SaJwtUtil { /** * key: value 前缀 */ public static final String KEY_VALUE = "value_"; /** * key: 有效期 (时间戳) */ public static final String KEY_EFF = "eff"; /** 当有效期被设为此值时,代表永不过期 */ public static final long NEVER_EXPIRE = SaTokenDao.NEVER_EXPIRE; /** * 根据指定值创建 jwt-token * * @param value 要保存的值 * @param timeout token有效期 (单位 秒) * @param keyt 秘钥 * @return jwt-token */ public static String createToken(Object value, long timeout, String keyt) { // 计算eff有效期: // 如果 timeout 指定为 -1,那么 eff 也为 -1,代表永不过期 // 如果 timeout 指定为一个具体的值,那么 eff 为 13 位时间戳,代表此数据到期的时间 long eff = timeout; if(timeout != NEVER_EXPIRE) { eff = timeout * 1000 + System.currentTimeMillis(); } // 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用 SecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes()); JwtBuilder builder = Jwts.builder() .header().add("typ", "JWT").and() .claim(KEY_VALUE, value) .claim(KEY_EFF, eff) .signWith(key); // 生成jwt-token return builder.compact(); } /** * 从一个 jwt-token 解析出载荷 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return Claims对象 */ public static Claims parseToken(String jwtToken, String keyt) { // 解析出载荷 SecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes()); return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(jwtToken).getPayload(); } /** * 从一个 jwt-token 解析出载荷, 并取出数据 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return 值 */ public static Object getValue(String jwtToken, String keyt) { // 取出数据 Claims claims = parseToken(jwtToken, keyt); // 验证是否超时 Long eff = claims.get(KEY_EFF, Long.class); if(eff == null || (eff < System.currentTimeMillis() && eff != NEVER_EXPIRE)) { throw new SaTokenException("token 已超时,无法解析:" + jwtToken).setCode(SaTempJwtErrorCode.CODE_30303); } // 获取数据 return claims.get(KEY_VALUE); } /** * 从一个 jwt-token 解析出载荷, 并取出其剩余有效期 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return 值 */ public static long getTimeout(String jwtToken, String keyt) { // 取出数据 Claims claims = parseToken(jwtToken, keyt); // 验证是否超时 Long eff = claims.get(KEY_EFF, Long.class); // 永不过期 if(eff == NEVER_EXPIRE) { return NEVER_EXPIRE; } // 已经超时 if(eff < System.currentTimeMillis()) { return SaTokenDao.NOT_VALUE_EXPIRE; } // 计算timeout return (eff - System.currentTimeMillis()) / 1000; } } ================================================ FILE: sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.temp.jwt; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.temp.SaTempTemplate; import cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode; import cn.dev33.satoken.util.SaFoxUtil; import java.util.List; /** * Sa-Token 临时令牌验证模块接口 JWT实现类,提供以 JWT 为逻辑内核的临时 token 验证功能 * * @author click33 * @since 1.20.0 */ public class SaTempTemplateForJwt extends SaTempTemplate { /** * 根据value创建一个token */ @Override public String createToken(Object value, long timeout, boolean isRecordIndex) { return SaJwtUtil.createToken(value, timeout, getJwtSecretKey()); } /** * 解析token获取value */ @Override public Object parseToken(String token) { return SaJwtUtil.getValue(token, getJwtSecretKey()); } /** * 返回指定token的剩余有效期,单位:秒 */ @Override public long getTimeout(String token) { return SaJwtUtil.getTimeout(token, getJwtSecretKey()); } /** * 删除一个token */ @Override public void deleteToken(String token) { throw new ApiDisabledException("jwt cannot delete token").setCode(SaTempJwtErrorCode.CODE_30302); } /** * 获取指定 value 的 temp-token 列表记录 * @param value / * @return / */ public List getTempTokenList(Object value) { throw new ApiDisabledException("jwt cannot get token list").setCode(SaTempJwtErrorCode.CODE_30304); } /** * 获取jwt秘钥 * @return jwt秘钥 */ @Override public String getJwtSecretKey() { String jwtSecretKey = SaManager.getConfig().getJwtSecretKey(); if(SaFoxUtil.isEmpty(jwtSecretKey)) { throw new SaTokenException("请配置:jwtSecretKey").setCode(SaTempJwtErrorCode.CODE_30301); } return jwtSecretKey; } } ================================================ FILE: sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.temp.jwt.error; /** * 定义 sa-token-temp-jwt 所有异常细分状态码 * * @author click33 * @since 1.33.0 */ public interface SaTempJwtErrorCode { /** jwt 模式没有提供秘钥 */ int CODE_30301 = 30301; /** jwt 模式不可以删除 Token */ int CODE_30302 = 30302; /** Token已超时 */ int CODE_30303 = 30303; /** jwt 模式不可以查询旧 Token 列表 */ int CODE_30304 = 30304; } ================================================ FILE: sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin ================================================ cn.dev33.satoken.plugin.SaTokenPluginForTempForJwt ================================================ FILE: sa-token-plugin/sa-token-thymeleaf/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-plugin ${revision} ../pom.xml jar sa-token-thymeleaf sa-token-thymeleaf sa-token-thymeleaf cn.dev33 sa-token-core org.thymeleaf thymeleaf true org.springframework.boot spring-boot-configuration-processor 2.5.14 true ================================================ FILE: sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/Sa-Token-Dialect.xml ================================================ ================================================ FILE: sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenDialect.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.thymeleaf.dialect; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.thymeleaf.dialect.AbstractProcessorDialect; import org.thymeleaf.processor.IProcessor; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; import org.thymeleaf.standard.processor.StandardXmlNsTagProcessor; import org.thymeleaf.templatemode.TemplateMode; /** * Sa-Token 集成 Thymeleaf 标签方言 * * @author click33 * @since 1.27.0 */ public class SaTokenDialect extends AbstractProcessorDialect { /** * 底层使用的 StpLogic */ public StpLogic stpLogic; /** * 使用默认参数注册方言 */ public SaTokenDialect() { this("sa", 1000, StpUtil.stpLogic); } /** * 构造方言对象,使用自定义参数 * * @param name 方言名称 * @param precedence 优先级 * @param stpLogic 使用的 StpLogic 对象 */ public SaTokenDialect(String name, int precedence, StpLogic stpLogic) { // 名称、前缀、优先级 super(name, name, precedence); this.stpLogic = stpLogic; } /** * 返回所有方言处理器 */ @Override public Set getProcessors(String prefix) { return new HashSet<>(Arrays.asList( // 登录判断 new SaTokenTagProcessor(prefix, "login", value -> stpLogic.isLogin()), new SaTokenTagProcessor(prefix, "notLogin", value -> ! stpLogic.isLogin()), // 角色判断 new SaTokenTagProcessor(prefix, "hasRole", value -> stpLogic.hasRole(value)), new SaTokenTagProcessor(prefix, "hasRoleAnd", value -> stpLogic.hasRoleAnd(toArray(value))), new SaTokenTagProcessor(prefix, "hasRoleOr", value -> stpLogic.hasRoleOr(toArray(value))), new SaTokenTagProcessor(prefix, "notRole", value -> ! stpLogic.hasRole(value)), new SaTokenTagProcessor(prefix, "lackRole", value -> ! stpLogic.hasRole(value)), // 权限判断 new SaTokenTagProcessor(prefix, "hasPermission", value -> stpLogic.hasPermission(value)), new SaTokenTagProcessor(prefix, "hasPermissionAnd", value -> stpLogic.hasPermissionAnd(toArray(value))), new SaTokenTagProcessor(prefix, "hasPermissionOr", value -> stpLogic.hasPermissionOr(toArray(value))), new SaTokenTagProcessor(prefix, "notPermission", value -> ! stpLogic.hasPermission(value)), new SaTokenTagProcessor(prefix, "lackPermission", value -> ! stpLogic.hasPermission(value)), // 移除标签命名空间 new StandardXmlNsTagProcessor(TemplateMode.HTML,prefix) )); } /** * String 转 Array * @param str 字符串 * @return 数组 */ public String[] toArray(String str) { List list = SaFoxUtil.convertStringToList(str); return list.toArray(new String[0]); } } ================================================ FILE: sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenTagProcessor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.thymeleaf.dialect; import java.util.function.Function; import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.engine.AttributeName; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; import org.thymeleaf.processor.element.IElementTagStructureHandler; import org.thymeleaf.templatemode.TemplateMode; /** * 封装 Sa-Token 标签方言处理器 * * @author click33 * @since 1.27.0 */ public class SaTokenTagProcessor extends AbstractAttributeTagProcessor { Function fun; public SaTokenTagProcessor(final String dialectPrefix, String attrName, Function fun) { super( TemplateMode.HTML, // This processor will apply only to HTML mode dialectPrefix, // Prefix to be applied to name for matching null, // No tag name: match any tag name false, // No prefix to be applied to tag name attrName, // Name of the attribute that will be matched true, // Apply dialect prefix to attribute name 10000, // Precedence (inside dialect's own precedence) true); // Remove the matched attribute afterwards this.fun = fun; } @Override protected void doProcess( final ITemplateContext context, final IProcessableElementTag tag, final AttributeName attributeName, final String attributeValue, final IElementTagStructureHandler structureHandler) { // 执行表达式返回值为false,则删除这个标签 if( ! this.fun.apply(attributeValue)) { structureHandler.removeElement(); } } } ================================================ FILE: sa-token-special-dependencies/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-parent ${revision} ../pom.xml pom sa-token-special-dependencies sa-token-special-dependencies Sa-Token Special Dependencies sa-token-spring-boot2-dependencies sa-token-spring-boot3-dependencies sa-token-spring-boot4-dependencies ================================================ FILE: sa-token-special-dependencies/sa-token-spring-boot2-dependencies/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-special-dependencies ${revision} ../pom.xml pom sa-token-spring-boot2-dependencies sa-token-spring-boot2-dependencies Sa-Token SpringBoot2 Dependencies 2.7.18 5.3.39 3.7.4 org.springframework.boot spring-boot-starter ${springboot2.version} org.springframework.boot spring-boot-starter-web ${springboot2.version} org.springframework.boot spring-boot-configuration-processor ${springboot2.version} org.springframework spring-web ${springboot2-spring.version} org.springframework spring-webmvc ${springboot2-spring.version} io.projectreactor reactor-core ${reactor-core.version} org.springframework.boot spring-boot-starter-data-redis ${springboot2.version} org.springframework.boot spring-boot-starter-thymeleaf ${springboot2.version} org.springframework.boot spring-boot-starter-aop ${springboot2.version} org.springframework.boot spring-boot-starter-actuator ${springboot2.version} org.springframework.boot spring-boot-starter-test ${springboot2.version} ================================================ FILE: sa-token-special-dependencies/sa-token-spring-boot3-dependencies/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-special-dependencies ${revision} ../pom.xml pom sa-token-spring-boot3-dependencies sa-token-spring-boot3-dependencies Sa-Token SpringBoot3 Dependencies 3.5.11 6.2.16 org.springframework.boot spring-boot-starter ${springboot3.version} org.springframework.boot spring-boot-starter-web ${springboot3.version} org.springframework spring-web ${springboot3-spring.version} org.springframework.boot spring-boot-configuration-processor ${springboot3.version} ================================================ FILE: sa-token-special-dependencies/sa-token-spring-boot4-dependencies/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-special-dependencies ${revision} ../pom.xml pom sa-token-spring-boot4-dependencies sa-token-spring-boot4-dependencies Sa-Token SpringBoot4 Dependencies 4.0.3 7.0.3 org.springframework.boot spring-boot-starter ${springboot4.version} org.springframework.boot spring-boot-starter-webmvc ${springboot4.version} org.springframework spring-web ${springboot4-spring.version} org.springframework.boot spring-boot-starter-webflux ${springboot4.version} org.springframework.boot spring-boot-configuration-processor ${springboot4.version} org.springframework.boot spring-boot-starter-data-redis ${springboot4.version} org.apache.commons commons-pool2 2.12.1 true ================================================ FILE: sa-token-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-parent ${revision} ../pom.xml pom sa-token-starter sa-token-starter sa-token starters sa-token-servlet sa-token-jakarta-servlet sa-token-spring-boot-webmvc-reactor-v2v3v4-common sa-token-spring-boot-reactor-v2v3v4-common sa-token-spring-boot-starter sa-token-spring-boot-webmvc-v3v4-common sa-token-spring-boot3-starter sa-token-spring-boot4-starter sa-token-reactor-spring-boot-starter sa-token-reactor-spring-boot3-starter sa-token-reactor-spring-boot4-starter sa-token-solon-plugin sa-token-jboot-plugin sa-token-jfinal-plugin sa-token-loveqq-boot-starter ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-jakarta-servlet sa-token-jakarta-servlet sa-token authentication by Jakarta Servlet API cn.dev33 sa-token-core jakarta.servlet jakarta.servlet-api ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.error; /** * 定义 sa-token-servlet 所有异常细分状态码 * * @author click33 * @since 1.34.0 */ public interface SaServletErrorCode { /** 转发失败 */ int CODE_20001 = 20001; /** 重定向失败 */ int CODE_20002 = 20002; } ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.servlet.error.SaServletErrorCode; import cn.dev33.satoken.util.SaFoxUtil; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; /** * 对 SaRequest 包装类的实现(Jakarta-Servlet 版) * * @author click33 * @since 1.34.0 */ public class SaRequestForServlet implements SaRequest { /** * 底层Request对象 */ protected HttpServletRequest request; /** * 实例化 * @param request request对象 */ public SaRequestForServlet(HttpServletRequest request) { this.request = request; } /** * 获取底层源对象 */ @Override public Object getSource() { return request; } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { return request.getParameter(name); } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return Collections.list(request.getParameterNames()); } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ // 获取所有参数 Map parameterMap = request.getParameterMap(); Map map = new LinkedHashMap<>(parameterMap.size()); for (String key : parameterMap.keySet()) { String[] values = parameterMap.get(key); map.put(key, values[0]); } return map; } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { return request.getHeader(name); } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { return getCookieLastValue(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie != null && name.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ String value = null; Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie != null && name.equals(cookie.getName())) { value = cookie.getValue(); } } } return value; } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { return ApplicationInfo.cutPathPrefix(request.getRequestURI()); } /** * 返回当前请求的url,例:http://xxx.com/test * @return see note */ @Override public String getUrl() { String currDomain = SaManager.getConfig().getCurrDomain(); if( ! SaFoxUtil.isEmpty(currDomain)) { return currDomain + this.getRequestPath(); } return request.getRequestURL().toString(); } /** * 返回当前请求的类型 */ @Override public String getMethod() { return request.getMethod(); } /** * 查询请求 host */ @Override public String getHost() { return request.getServerName(); } /** * 转发请求 */ @Override public Object forward(String path) { try { HttpServletResponse response = (HttpServletResponse)SaManager.getSaTokenContext().getResponse().getSource(); request.getRequestDispatcher(path).forward(request, response); return null; } catch (ServletException | IOException e) { throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20001); } } } ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.servlet.error.SaServletErrorCode; import jakarta.servlet.http.HttpServletResponse; /** * 对 SaResponse 包装类的实现(Jakarta-Servlet 版) * * @author click33 * @since 1.34.0 */ public class SaResponseForServlet implements SaResponse { /** * 底层Request对象 */ protected HttpServletResponse response; /** * 实例化 * @param response response对象 */ public SaResponseForServlet(HttpServletResponse response) { this.response = response; } /** * 获取底层源对象 */ @Override public Object getSource() { return response; } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { response.setStatus(sc); return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { response.setHeader(name, value); return this; } /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ @Override public SaResponse addHeader(String name, String value) { response.addHeader(name, value); return this; } /** * 重定向 */ @Override public Object redirect(String url) { try { response.sendRedirect(url); } catch (Exception e) { throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20002); } return null; } } ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.context.model.SaStorage; import jakarta.servlet.http.HttpServletRequest; /** * 对 SaStorage 包装类的实现(Jakarta-Servlet 版) * * @author click33 * @since 1.34.0 */ public class SaStorageForServlet implements SaStorage { /** * 底层Request对象 */ protected HttpServletRequest request; /** * 实例化 * @param request request对象 */ public SaStorageForServlet(HttpServletRequest request) { this.request = request; } /** * 获取底层源对象 */ @Override public Object getSource() { return request; } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorageForServlet set(String key, Object value) { request.setAttribute(key, value); return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return request.getAttribute(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorageForServlet delete(String key) { request.removeAttribute(key); return this; } } ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token对接 Jakarta-Servlet API 容器所需要的实现类接口包 */ package cn.dev33.satoken.servlet; ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaJakartaServletOperateUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.util; import cn.dev33.satoken.util.SaTokenConsts; import jakarta.servlet.ServletResponse; import java.io.IOException; /** * Jakarta Servlet 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaJakartaServletOperateUtil { /** * 写入结果到输出流 * @param response / * @param result / */ public static void writeResult(ServletResponse response, String result) throws IOException { // 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } response.getWriter().print(result); response.getWriter().flush(); } } ================================================ FILE: sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextJakartaServletUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.model.SaStorageForServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextJakartaServletUtil { /** * 写入当前上下文 * @param request / * @param response / */ public static void setContext(HttpServletRequest request, HttpServletResponse response) { SaRequest req = new SaRequestForServlet(request); SaResponse res = new SaResponseForServlet(response); SaStorage stg = new SaStorageForServlet(request); SaManager.getSaTokenContext().setContext(req, res, stg); } /** * 写入上下文对象, 并在执行函数后将其清除 * @param request / * @param response / * @param fun / */ public static void setContext(HttpServletRequest request, HttpServletResponse response, SaFunction fun) { try { setContext(request, response); fun.run(); } finally { clearContext(); } } /** * 写入上下文对象, 并在执行函数后将其清除 * * @param request / * @param response / * @param fun / * @return / * @param / */ public static T setContext(HttpServletRequest request, HttpServletResponse response, SaRetGenericFunction fun) { try { setContext(request, response); return fun.run(); } finally { clearContext(); } } /** * 清除当前上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } /** * 获取当前 ModelBox * @return / */ public static SaTokenContextModelBox getModelBox() { return SaManager.getSaTokenContext().getModelBox(); } /** * 获取当前 Request * @return / */ public static HttpServletRequest getRequest() { return (HttpServletRequest) getModelBox().getRequest().getSource(); } /** * 获取当前 Response * @return / */ public static HttpServletResponse getResponse() { return (HttpServletResponse) getModelBox().getResponse().getSource(); } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/pom.xml ================================================ sa-token-starter cn.dev33 ${revision} ../pom.xml 4.0.0 jar sa-token-jboot-plugin sa-token-jboot-plugin jboot integrate sa-token 8 8 3.8.0 io.jboot jboot provided cn.dev33 sa-token-core cn.dev33 sa-token-servlet redis.clients jedis ${jedis.version} provided org.apache.maven.plugins maven-compiler-plugin 3.6.1 1.8 1.8 UTF-8 -parameters ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PathAnalyzer { private static final Map cached = new LinkedHashMap<>(); private final Pattern pattern; public static PathAnalyzer get(String expr) { PathAnalyzer pa = cached.get(expr); if (pa == null) { synchronized(expr.intern()) { pa = cached.get(expr); if (pa == null) { pa = new PathAnalyzer(expr); cached.put(expr, pa); } } } return pa; } private PathAnalyzer(String expr) { this.pattern = Pattern.compile(exprCompile(expr), Pattern.CASE_INSENSITIVE); } public Matcher matcher(String uri) { return this.pattern.matcher(uri); } public boolean matches(String uri) { return this.pattern.matcher(uri).find(); } private static String exprCompile(String expr) { String p = expr.replace(".", "\\."); p = p.replace("$", "\\$"); p = p.replace("**", ".[]"); p = p.replace("*", "[^/]*"); if (p.contains("{")) { if (p.indexOf("_}") > 0) { p = p.replaceAll("\\{[^\\}]+?\\_\\}", "(.+?)"); } p = p.replaceAll("\\{[^\\}]+?\\}", "([^/]+?)"); } if (!p.startsWith("/")) { p = "/" + p; } p = p.replace(".[]", ".*"); return "^" + p + "$"; } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; /** * 注解式鉴权 - 拦截器 */ public class SaAnnotationInterceptor implements Interceptor { @Override public void intercept(Invocation invocation) { SaAnnotationStrategy.instance.checkMethodAnnotation.accept((invocation.getMethod())); invocation.invoke(); } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import com.jfinal.log.Log; import io.jboot.components.serializer.JbootSerializer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class SaJdkSerializer implements JbootSerializer { private static final Log LOG = Log.getLog(SaJdkSerializer.class); @Override public byte[] serialize(Object value) { if (value == null) { return null; } ObjectOutputStream objectOut = null; try { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024); objectOut = new ObjectOutputStream(bytesOut); objectOut.writeObject(value); objectOut.flush(); return bytesOut.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); } finally { if(objectOut != null) try {objectOut.close();} catch (Exception e) { LOG.error(e.getMessage(), e);} } } @Override public Object deserialize(byte[] bytes) { if (bytes == null || bytes.length == 0) { return null; } ObjectInputStream objectInput = null; try { ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes); objectInput = new ObjectInputStream(bytesInput); return objectInput.readObject(); } catch (Exception e) { throw new RuntimeException(e); } finally { if (objectInput != null) try {objectInput.close();} catch (Exception e) {LOG.error(e.getMessage(), e);} } } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisCache.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.components.cache.JbootCache; import io.jboot.components.cache.JbootCacheConfig; import io.jboot.core.spi.JbootSpi; import io.jboot.exception.JbootIllegalConfigException; import io.jboot.support.redis.JbootRedisConfig; import io.jboot.support.redis.RedisScanResult; import io.jboot.utils.StrUtil; import redis.clients.jedis.*; import redis.clients.jedis.exceptions.JedisConnectionException; import java.util.ArrayList; import java.util.List; /** * sa 缓存处理 */ @JbootSpi("sacache") @SuppressWarnings({"unchecked", "rawtypes"}) public class SaRedisCache implements JbootCache { protected JbootRedisConfig config; protected JedisPool jedisPool; private final ThreadLocal CACHE_NAME_PREFIX_TL = new ThreadLocal<>(); public SaRedisCache(JbootRedisConfig config) { this.config = config; String host = config.getHost(); Integer port = config.getPort(); Integer timeout = config.getTimeout(); String password = config.getPassword(); Integer database = config.getDatabase(); String clientName = config.getClientName(); if (host.contains(":")) { port = Integer.valueOf(host.split(":")[1]); } JedisPoolConfig poolConfig = new JedisPoolConfig(); if (StrUtil.isNotBlank(config.getTestWhileIdle())) { poolConfig.setTestWhileIdle(config.getTestWhileIdle()); } if (StrUtil.isNotBlank(config.getTestOnBorrow())) { poolConfig.setTestOnBorrow(config.getTestOnBorrow()); } if (StrUtil.isNotBlank(config.getTestOnCreate())) { poolConfig.setTestOnCreate(config.getTestOnCreate()); } if (StrUtil.isNotBlank(config.getTestOnReturn())) { poolConfig.setTestOnReturn(config.getTestOnReturn()); } if (StrUtil.isNotBlank(config.getMinEvictableIdleTimeMillis())) { poolConfig.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis()); } if (StrUtil.isNotBlank(config.getTimeBetweenEvictionRunsMillis())) { poolConfig.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis()); } if (StrUtil.isNotBlank(config.getNumTestsPerEvictionRun())) { poolConfig.setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun()); } if (StrUtil.isNotBlank(config.getMaxTotal())) { poolConfig.setMaxTotal(config.getMaxTotal()); } if (StrUtil.isNotBlank(config.getMaxIdle())) { poolConfig.setMaxIdle(config.getMaxIdle()); } if (StrUtil.isNotBlank(config.getMinIdle())) { poolConfig.setMinIdle(config.getMinIdle()); } if (StrUtil.isNotBlank(config.getMaxWaitMillis())) { poolConfig.setMaxWaitMillis(config.getMaxWaitMillis()); } this.jedisPool = new JedisPool(poolConfig, host, port, timeout, timeout, password, database, clientName); } public SaRedisCache(JedisPool jedisPool) { this.jedisPool = jedisPool; } @Override public JbootCache setCurrentCacheNamePrefix(String cacheNamePrefix) { if (StrUtil.isNotBlank(cacheNamePrefix)) { CACHE_NAME_PREFIX_TL.set(cacheNamePrefix); } else { CACHE_NAME_PREFIX_TL.remove(); } return this; } @Override public void removeCurrentCacheNamePrefix() { CACHE_NAME_PREFIX_TL.remove(); } @Override public JbootCacheConfig getConfig() { return null; } @Override public T get(String cacheName, Object key) { Jedis jedis = getJedis(); try { return (T) (jedis.get(key.toString())); } finally { returnResource(jedis); } } @Override public void put(String cacheName, Object key, Object value) { Jedis jedis = getJedis(); try { jedis.set(key.toString(), value.toString()); } finally { returnResource(jedis); } } @Override public void put(String cacheName, Object key, Object value, int liveSeconds) { Jedis jedis = getJedis(); try { jedis.setex(key.toString(), Long.parseLong(liveSeconds + ""), value.toString()); } finally { returnResource(jedis); } } @Override public void remove(String cacheName, Object key) { Jedis jedis = getJedis(); try { jedis.del(key.toString()); } finally { returnResource(jedis); } } @Override public void removeAll(String cacheName) { } @Override public T get(String cacheName, Object key, IDataLoader dataLoader) { return null; } @Override public T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds) { return null; } @Override public Integer getTtl(String cacheName, Object key) { Jedis jedis = getJedis(); try { return jedis.ttl(key.toString()).intValue(); } finally { returnResource(jedis); } } @Override public void setTtl(String cacheName, Object key, int seconds) { Jedis jedis = getJedis(); try { jedis.expire(key.toString(), Long.parseLong(seconds + "")); } finally { returnResource(jedis); } } @Override public void refresh(String cacheName, Object key) { } @Override public void refresh(String cacheName) { } @Override public List getNames() { return null; } @Override public List getKeys(String cacheName) { List keys = new ArrayList<>(); String cursor = "0"; int scanCount = 1000; boolean continueState = true; do { RedisScanResult redisScanResult = this.scan("*", cursor, scanCount); List scanKeys = redisScanResult.getResults(); cursor = redisScanResult.getCursor(); if (scanKeys != null && scanKeys.size() > 0) { for (String key : scanKeys) { keys.add(key.substring(3)); } } if (redisScanResult.isCompleteIteration()) { continueState = false; } } while (continueState); return keys; } public Jedis getJedis() { try { return jedisPool.getResource(); } catch (JedisConnectionException e) { throw new JbootIllegalConfigException("can not connect to redis host " + config.getHost() + ":" + config.getPort() + " ," + " cause : " + e, e); } } public void returnResource(Jedis jedis) { if (jedis != null) { jedis.close(); } } public RedisScanResult scan(String pattern, String cursor, int scanCount) { ScanParams params = new ScanParams(); params.match(pattern).count(scanCount); try (Jedis jedis = getJedis()) { ScanResult scanResult = jedis.scan(cursor, params); return new RedisScanResult<>(scanResult.getCursor(), scanResult.getResult()); } } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenCacheDao.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.util.SaFoxUtil; import io.jboot.Jboot; import io.jboot.components.serializer.JbootSerializer; import io.jboot.exception.JbootIllegalConfigException; import io.jboot.support.redis.JbootRedisConfig; import io.jboot.utils.ConfigUtil; import redis.clients.jedis.Jedis; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * 使用Jboot的缓存方法存取Token数据 */ @SuppressWarnings({"unchecked", "rawtypes"}) public class SaTokenCacheDao implements SaTokenDaoBySessionFollowObject { protected SaRedisCache saRedisCache; protected JbootSerializer serializer; private final Map saRedisMap = new ConcurrentHashMap(); /** * 使用默认redis配置 */ public SaTokenCacheDao() { JbootRedisConfig config = Jboot.config(JbootRedisConfig.class); this.saRedisCache = new SaRedisCache(config); this.serializer = new SaJdkSerializer(); } /** * 调用的Cache名称 * * @param cacheName 使用的缓存配置名,默认为 default */ public SaTokenCacheDao(String cacheName) { SaRedisCache saCache = this.saRedisMap.get(cacheName); if (saCache == null) { synchronized (this) { saCache = this.saRedisMap.get(cacheName); if (saCache == null) { Map configModels = ConfigUtil.getConfigModels(JbootRedisConfig.class); if (!configModels.containsKey(cacheName)) { throw new JbootIllegalConfigException("Please config \"jboot.redis." + cacheName + ".host\" in your jboot.properties."); } JbootRedisConfig jbootRedisConfig = configModels.get(cacheName); saCache = new SaRedisCache(jbootRedisConfig); this.saRedisMap.put(cacheName, saCache); } } } this.saRedisCache = saCache; this.serializer = new SaJdkSerializer(); } @Override public String get(String key) { Jedis jedis = saRedisCache.getJedis(); try { return jedis.get(key); } finally { saRedisCache.returnResource(jedis); } } @Override public void set(String key, String value, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } Jedis jedis = saRedisCache.getJedis(); try { if (timeout == SaTokenDao.NEVER_EXPIRE) { jedis.set(key, value); } else { jedis.setex(key, timeout, value); } } finally { saRedisCache.returnResource(jedis); } } @Override public void update(String key, String value) { long expire = getTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.set(key, value, expire); } @Override public void delete(String key) { Jedis jedis = saRedisCache.getJedis(); try { jedis.del(key); } finally { saRedisCache.returnResource(jedis); } } @Override public long getTimeout(String key) { Jedis jedis = saRedisCache.getJedis(); try { return jedis.ttl(key); } finally { saRedisCache.returnResource(jedis); } } @Override public void updateTimeout(String key, long timeout) { //判断是否想要设置为永久 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if (expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } Jedis jedis = saRedisCache.getJedis(); try { jedis.expire(key, timeout); } finally { saRedisCache.returnResource(jedis); } } @Override public Object getObject(String key) { Jedis jedis = saRedisCache.getJedis(); try { return valueFromBytes(jedis.get(keyToBytes(key))); } finally { saRedisCache.returnResource(jedis); } } @Override public T getObject(String key, Class classType) { return (T) getObject(key); } @Override public void setObject(String key, Object object, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } Jedis jedis = saRedisCache.getJedis(); try { if (timeout == SaTokenDao.NEVER_EXPIRE) { jedis.set(keyToBytes(key), valueToBytes(object)); } else { jedis.setex(keyToBytes(key), timeout, valueToBytes(object)); } } finally { saRedisCache.returnResource(jedis); } } @Override public void updateObject(String key, Object object) { long expire = getObjectTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.setObject(key, object, expire); } @Override public void deleteObject(String key) { Jedis jedis = saRedisCache.getJedis(); try { jedis.del(keyToBytes(key)); } finally { saRedisCache.returnResource(jedis); } } @Override public long getObjectTimeout(String key) { Jedis jedis = saRedisCache.getJedis(); try { return jedis.ttl(keyToBytes(key)); } finally { saRedisCache.returnResource(jedis); } } @Override public void updateObjectTimeout(String key, long timeout) { //判断是否想要设置为永久 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getObjectTimeout(key); if (expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.setObject(key, this.getObject(key), timeout); } return; } Jedis jedis = saRedisCache.getJedis(); try { jedis.expire(keyToBytes(key), timeout); } finally { saRedisCache.returnResource(jedis); } } @Override public SaSession getSession(String sessionId) { return SaTokenDaoBySessionFollowObject.super.getSession(sessionId); } @Override public void setSession(SaSession session, long timeout) { SaTokenDaoBySessionFollowObject.super.setSession(session, timeout); } @Override public void updateSession(SaSession session) { SaTokenDaoBySessionFollowObject.super.updateSession(session); } @Override public void deleteSession(String sessionId) { SaTokenDaoBySessionFollowObject.super.deleteSession(sessionId); } @Override public long getSessionTimeout(String sessionId) { return SaTokenDaoBySessionFollowObject.super.getSessionTimeout(sessionId); } @Override public void updateSessionTimeout(String sessionId, long timeout) { SaTokenDaoBySessionFollowObject.super.updateSessionTimeout(sessionId, timeout); } @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Jedis jedis = saRedisCache.getJedis(); try { Set keys = jedis.keys(prefix + "*" + keyword + "*"); List list = new ArrayList<>(keys); return SaFoxUtil.searchList(list, start, size, sortType); } finally { saRedisCache.returnResource(jedis); } } protected byte[] keyToBytes(Object key) { return key.toString().getBytes(); } protected byte[] valueToBytes(Object value) { return serializer.serialize(value); } protected Object valueFromBytes(byte[] bytes) { if (bytes == null || bytes.length == 0) { return null; } return serializer.deserialize(bytes); } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import cn.dev33.satoken.context.SaTokenContextForReadOnly; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.model.SaStorageForServlet; import cn.dev33.satoken.strategy.SaStrategy; import io.jboot.web.controller.JbootControllerContext; /** * Sa-Token 上线文处理器 [Jboot 版本实现] */ public class SaTokenContextForJboot implements SaTokenContextForReadOnly { public SaTokenContextForJboot() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = (pattern, path) -> { return PathAnalyzer.get(pattern).matches(path); }; } /** * 获取当前请求的Request对象 */ @Override public SaRequest getRequest() { return new SaRequestForServlet(JbootControllerContext.get().getRequest()); } /** * 获取当前请求的Response对象 */ @Override public SaResponse getResponse() { return new SaResponseForServlet(JbootControllerContext.get().getResponse()); } /** * 获取当前请求的 [存储器] 对象 */ @Override public SaStorage getStorage() { return new SaStorageForServlet(JbootControllerContext.get().getRequest()); } @Override public boolean isValid() { return JbootControllerContext.get() != null; } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import cn.dev33.satoken.filter.SaFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class SaTokenPathFilter implements SaFilter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaTokenPathFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaTokenPathFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaTokenPathFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaTokenPathFilter setExcludeList(List pathList) { excludeList = pathList; return this; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> {}; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e); }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; @Override public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaTokenPathFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } /*@Override public void doFilter(Controller ctx, FilterChain chain) throws Throwable { try { // 执行全局过滤器 beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> { auth.run(null); }); } catch (StopMatchException e) { } catch (Throwable e) { // 1. 获取异常处理策略结果 String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); // 2. 写入输出流 ctx.renderText(result); return; } // 执行 chain.doFilter(ctx); }*/ } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot.test; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.StpUtil; import io.jboot.Jboot; import io.jboot.app.JbootApplication; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; @RequestMapping("/") public class AppRun extends JbootController { public static void main(String[] args) { JbootApplication.run(args); } public void index() { renderText("index"); } public void doLogin() { StpUtil.login(10001); //赋值角色 renderText("登录成功"); } public void getLoginInfo() { System.out.println("是否登录:" + StpUtil.isLogin()); System.out.println("登录信息" + StpUtil.getTokenInfo()); renderJson(StpUtil.getTokenInfo()); } @SaCheckRole("super-admin") public void add() { renderText("超级管理员方法!"); } @SuppressWarnings("unused") public void token(String token) { Object t = Jboot.getRedis().get("xxxxx"); //默认redis库 SaSession saSession = StpUtil.getSessionByLoginId(StpUtil.getLoginIdByToken(token), false); //satoken redis库 renderJson(saSession); } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaCookieConfig; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.jboot.SaAnnotationInterceptor; import cn.dev33.satoken.jboot.SaTokenCacheDao; import cn.dev33.satoken.jboot.SaTokenContextForJboot; import cn.dev33.satoken.util.SaTokenConsts; import com.jfinal.config.Constants; import com.jfinal.config.Interceptors; import com.jfinal.config.Routes; import com.jfinal.template.Engine; import io.jboot.aop.jfinal.JfinalHandlers; import io.jboot.aop.jfinal.JfinalPlugins; import io.jboot.core.listener.JbootAppListener; public class AtteStartListener implements JbootAppListener { public void onInit() { SaTokenContext saTokenContext = new SaTokenContextForJboot(); SaManager.setSaTokenContext(saTokenContext); SaManager.setStpInterface(new StpInterfaceImpl()); SaTokenConfig saTokenConfig = new SaTokenConfig(); saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID); saTokenConfig.setTimeout(60*60*4); //登录有效时间4小时 saTokenConfig.setActiveTimeout(30*60); //半小时无操作就冻结 token saTokenConfig.setIsShare(false); saTokenConfig.setTokenName("token"); //更换satoken的名称 saTokenConfig.setCookie(new SaCookieConfig().setHttpOnly(true)); //开启cookies的httponly属性 SaManager.setConfig(saTokenConfig); } @Override public void onConstantConfig(Constants constants) { } @Override public void onRouteConfig(Routes routes) { } @Override public void onEngineConfig(Engine engine) { } @Override public void onPluginConfig(JfinalPlugins plugins) { } @Override public void onInterceptorConfig(Interceptors interceptors) { //开启注解方式权限验证 interceptors.add(new SaAnnotationInterceptor()); } @Override public void onHandlerConfig(JfinalHandlers handlers) { } @Override public void onStartBefore() { } @Override public void onStart() { SaManager.setSaTokenDao(new SaTokenCacheDao("sa")); } @Override public void onStartFinish() { } @Override public void onStop() { } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jboot.test; import cn.dev33.satoken.stp.StpInterface; import io.jboot.aop.annotation.Bean; import java.util.ArrayList; import java.util.List; @Bean public class StpInterfaceImpl implements StpInterface { @Override public List getPermissionList(Object o, String s) { return null; } @Override public List getRoleList(Object o, String s) { List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-starter/sa-token-jboot-plugin/src/test/resources/jboot.properties ================================================ undertow.devMode=true undertow.port=9980 undertow.host=0.0.0.0 #other redis config jboot.cache.type=redis jboot.redis.host=127.0.0.1 jboot.redis.port=6379 jboot.redis.password=123456 jboot.redis.database=3 #satoken redis config jboot.cache.sa.type=sacache jboot.redis.sa.host=127.0.0.1 jboot.redis.sa.port=6379 jboot.redis.sa.password=123456 jboot.redis.sa.database=1 ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/pom.xml ================================================ sa-token-starter cn.dev33 ${revision} ../pom.xml 4.0.0 jar sa-token-jfinal-plugin sa-token-jfinal-plugin jfinal integrate sa-token 8 8 org.slf4j slf4j-api 1.7.24 com.jfinal jfinal-undertow 2.8 com.jfinal jfinal provided cn.dev33 sa-token-core cn.dev33 sa-token-servlet org.apache.commons commons-pool2 test redis.clients jedis 3.7.0 slf4j-api org.slf4j de.ruedigermoeller fst 2.29 test org.apache.maven.plugins maven-compiler-plugin 3.6.1 1.8 1.8 UTF-8 -parameters ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PathAnalyzer { private static final Map cached = new LinkedHashMap<>(); private final Pattern pattern; public static PathAnalyzer get(String expr) { PathAnalyzer pa = cached.get(expr); if (pa == null) { synchronized(expr.intern()) { pa = cached.get(expr); if (pa == null) { pa = new PathAnalyzer(expr); cached.put(expr, pa); } } } return pa; } private PathAnalyzer(String expr) { this.pattern = Pattern.compile(exprCompile(expr), Pattern.CASE_INSENSITIVE); } public Matcher matcher(String uri) { return this.pattern.matcher(uri); } public boolean matches(String uri) { return this.pattern.matcher(uri).find(); } private static String exprCompile(String expr) { String p = expr.replace(".", "\\."); p = p.replace("$", "\\$"); p = p.replace("**", ".[]"); p = p.replace("*", "[^/]*"); if (p.contains("{")) { if (p.indexOf("_}") > 0) { p = p.replaceAll("\\{[^\\}]+?\\_\\}", "(.+?)"); } p = p.replaceAll("\\{[^\\}]+?\\}", "([^/]+?)"); } if (!p.startsWith("/")) { p = "/" + p; } p = p.replace(".[]", ".*"); return "^" + p + "$"; } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; /** * 注解式鉴权 - 拦截器 */ public class SaAnnotationInterceptor implements Interceptor { @Override public void intercept(Invocation invocation) { SaAnnotationStrategy.instance.checkMethodAnnotation.accept((invocation.getMethod())); invocation.invoke(); } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import com.jfinal.core.Controller; public class SaControllerContext { private static ThreadLocal controllers = new ThreadLocal<>(); public static void hold(Controller controller) { controllers.set(controller); } public static Controller get() { return controllers.get(); } public static void release() { controllers.remove(); } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import com.jfinal.kit.LogKit; import com.jfinal.plugin.redis.serializer.ISerializer; import com.jfinal.plugin.redis.serializer.JdkSerializer; import redis.clients.jedis.util.SafeEncoder; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class SaJdkSerializer implements ISerializer { public static final ISerializer me = new JdkSerializer(); public byte[] keyToBytes(String key) { return SafeEncoder.encode(key); } public String keyFromBytes(byte[] bytes) { return SafeEncoder.encode(bytes); } public byte[] fieldToBytes(Object field) { return valueToBytes(field); } public Object fieldFromBytes(byte[] bytes) { return valueFromBytes(bytes); } public byte[] valueToBytes(Object value) { ObjectOutputStream objectOut = null; try { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024); objectOut = new ObjectOutputStream(bytesOut); objectOut.writeObject(value); objectOut.flush(); return bytesOut.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); } finally { if(objectOut != null) try {objectOut.close();} catch (Exception e) { LogKit.error(e.getMessage(), e);} } } public Object valueFromBytes(byte[] bytes) { if(bytes == null || bytes.length == 0) return null; ObjectInputStream objectInput = null; try { ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes); objectInput = new ObjectInputStream(bytesInput); return objectInput.readObject(); } catch (Exception e) { throw new RuntimeException(e); } finally { if (objectInput != null) try {objectInput.close();} catch (Exception e) {LogKit.error(e.getMessage(), e);} } } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import com.jfinal.aop.Invocation; import com.jfinal.config.Constants; import com.jfinal.core.*; import com.jfinal.kit.ReflectKit; import com.jfinal.log.Log; import com.jfinal.render.Render; import com.jfinal.render.RenderException; import com.jfinal.render.RenderManager; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SaTokenActionHandler extends ActionHandler { protected boolean devMode; protected ActionMapping actionMapping; protected ControllerFactory controllerFactory; protected ActionReporter actionReporter; protected static final RenderManager renderManager = RenderManager.me(); private static final Log log = Log.getLog(ActionHandler.class); protected void init(ActionMapping actionMapping, Constants constants) { this.actionMapping = actionMapping; this.devMode = constants.getDevMode(); this.controllerFactory = constants.getControllerFactory(); this.actionReporter = constants.getActionReporter(); } /** * 子类覆盖 getAction 方法可以定制路由功能 */ protected Action getAction(String target, String[] urlPara) { return actionMapping.getAction(target, urlPara); } @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { if (target.indexOf('.') != -1) { return ; } isHandled[0] = true; String[] urlPara = {null}; Action action = getAction(target, urlPara); if (action == null) { if (log.isWarnEnabled()) { String qs = request.getQueryString(); log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs)); } renderManager.getRenderFactory().getErrorRender(404).setContext(request, response).render(); return ; } Controller controller = null; try { // Controller controller = action.getControllerClass().newInstance(); controller = controllerFactory.getController(action.getControllerClass()); CPI._init_(controller, action, request, response, urlPara[0]); // if (resolveJson && controller.isJsonRequest()) { // // 注入 JsonRequest 包装对象接管 request // controller.setHttpServletRequest(jsonRequestFactory.apply(controller.getRawData(), controller.getRequest())); // } //加入SaToken上下文处理 SaControllerContext.hold(controller); if (devMode) { if (actionReporter.isReportAfterInvocation(request)) { new Invocation(action, controller).invoke(); actionReporter.report(target, controller, action); } else { actionReporter.report(target, controller, action); new Invocation(action, controller).invoke(); } } else { new Invocation(action, controller).invoke(); } Render render = controller.getRender(); if (render instanceof ForwardActionRender) { String actionUrl = ((ForwardActionRender)render).getActionUrl(); if (target.equals(actionUrl)) { throw new RuntimeException("The forward action url is the same as before."); } else { handle(actionUrl, request, response, isHandled); } return ; } if (render == null) { render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName()); } render.setContext(request, response, action.getViewPath()).render(); } catch (RenderException e) { if (log.isErrorEnabled()) { String qs = request.getQueryString(); log.error(qs == null ? target : target + "?" + qs, e); } } catch (ActionException e) { handleActionException(target, request, response, action, e); } catch (Exception e) { if (log.isErrorEnabled()) { String qs = request.getQueryString(); String targetInfo = (qs == null ? target : target + "?" + qs); String sign = ReflectKit.getMethodSignature(action.getMethod()); log.error(sign + " : " + targetInfo, e); } renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render(); } finally { SaControllerContext.release(); controllerFactory.recycle(controller); } } /** * 抽取出该方法是为了缩短 handle 方法中的代码量,确保获得 JIT 优化, * 方法长度超过 8000 个字节码时,将不会被 JIT 编译成二进制码 *

    * 通过开启 java 的 -XX:+PrintCompilation 启动参数得知,handle(...) * 方法(73 行代码)已被 JIT 优化,优化后的字节码长度为 593 个字节,相当于 * 每行代码产生 8.123 个字节 */ private void handleActionException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ActionException e) { int errorCode = e.getErrorCode(); String msg = null; if (errorCode == 404) { msg = "404 Not Found: "; } else if (errorCode == 400) { msg = "400 Bad Request: "; } else if (errorCode == 401) { msg = "401 Unauthorized: "; } else if (errorCode == 403) { msg = "403 Forbidden: "; } if (msg != null) { if (log.isWarnEnabled()) { String qs = request.getQueryString(); msg = msg + (qs == null ? target : target + "?" + qs); if (e.getMessage() != null) { msg = msg + "\n" + e.getMessage(); } log.warn(msg); } } else { if (log.isErrorEnabled()) { String qs = request.getQueryString(); log.error(errorCode + " Error: " + (qs == null ? target : target + "?" + qs), e); } } e.getErrorRender().setContext(request, response, action.getViewPath()).render(); } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import cn.dev33.satoken.context.SaTokenContextForReadOnly; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.model.SaStorageForServlet; import cn.dev33.satoken.strategy.SaStrategy; /** * Sa-Token 上线文处理器 [Jfinal 版本实现] */ public class SaTokenContextForJfinal implements SaTokenContextForReadOnly { public SaTokenContextForJfinal() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = (pattern, path) -> { return PathAnalyzer.get(pattern).matches(path); }; } /** * 获取当前请求的Request对象 */ @Override public SaRequest getRequest() { return new SaRequestForServlet(SaControllerContext.get().getRequest()); } /** * 获取当前请求的Response对象 */ @Override public SaResponse getResponse() { return new SaResponseForServlet(SaControllerContext.get().getResponse()); } /** * 获取当前请求的 [存储器] 对象 */ @Override public SaStorage getStorage() { return new SaStorageForServlet(SaControllerContext.get().getRequest()); } @Override public boolean isValid() { return SaControllerContext.get() != null; } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject; import cn.dev33.satoken.util.SaFoxUtil; import com.jfinal.plugin.redis.Cache; import com.jfinal.plugin.redis.Redis; import com.jfinal.plugin.redis.serializer.ISerializer; import redis.clients.jedis.Jedis; import java.util.ArrayList; import java.util.List; import java.util.Set; public class SaTokenDaoRedis implements SaTokenDaoBySessionFollowObject { protected Cache redis; protected ISerializer serializer; /** * 标记:是否已初始化成功 */ public boolean isInit; public SaTokenDaoRedis(String confName) { redis = Redis.use(confName); serializer = new SaJdkSerializer(); } /** * 获取Value,如无返空 */ @Override public String get(String key) { Jedis jedis = getJedis(); try { return jedis.get(key); } finally { close(jedis); } } /** * 写入Value,并设定存活时间 (单位: 秒) */ @Override public void set(String key, String value, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } Jedis jedis = getJedis(); try { if (timeout == SaTokenDao.NEVER_EXPIRE) { jedis.set(key, value); } else { jedis.setex(key, timeout, value); } } finally { close(jedis); } } /** * 修改指定key-value键值对 (过期时间不变) */ @Override public void update(String key, String value) { long expire = getTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.set(key, value, expire); } /** * 删除Value */ @Override public void delete(String key) { Jedis jedis = getJedis(); try { jedis.del(key); } finally { close(jedis); } } /** * 获取Value的剩余存活时间 (单位: 秒) */ @Override public long getTimeout(String key) { Jedis jedis = getJedis(); try { return jedis.ttl(key); } finally { close(jedis); } } /** * 修改Value的剩余存活时间 (单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { //判断是否想要设置为永久 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if (expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } Jedis jedis = getJedis(); try { jedis.expire(key, timeout); } finally { close(jedis); } } /** * 获取Object,如无返空 */ @Override public Object getObject(String key) { Jedis jedis = getJedis(); try { return valueFromBytes(jedis.get(keyToBytes(key))); } finally { close(jedis); } } @Override public T getObject(String key, Class classType) { return (T) getObject(key); } /** * 写入Object,并设定存活时间 (单位: 秒) */ @Override public void setObject(String key, Object object, long timeout) { if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } Jedis jedis = getJedis(); try { if (timeout == SaTokenDao.NEVER_EXPIRE) { jedis.set(keyToBytes(key), valueToBytes(object)); } else { jedis.setex(keyToBytes(key), timeout, valueToBytes(object)); } } finally { close(jedis); } } /** * 更新Object (过期时间不变) */ @Override public void updateObject(String key, Object object) { long expire = getObjectTimeout(key); // -2 = 无此键 if (expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.setObject(key, object, expire); } /** * 删除Object */ @Override public void deleteObject(String key) { Jedis jedis = getJedis(); try { jedis.del(keyToBytes(key)); } finally { close(jedis); } } @Override public long getObjectTimeout(String key) { Jedis jedis = getJedis(); try { return jedis.ttl(keyToBytes(key)); } finally { close(jedis); } } /** * 修改Object的剩余存活时间 (单位: 秒) */ @Override public void updateObjectTimeout(String key, long timeout) { //判断是否想要设置为永久 if (timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getObjectTimeout(key); if (expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.setObject(key, this.getObject(key), timeout); } return; } Jedis jedis = getJedis(); try { jedis.expire(keyToBytes(key), timeout); } finally { close(jedis); } } /** * 搜索数据 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Set keys = redis.keys(prefix + "*" + keyword + "*"); List list = new ArrayList<>(keys); return SaFoxUtil.searchList(list, start, size, sortType); } public Jedis getJedis() { return redis.getJedis(); } public void close(Jedis jedis) { if (jedis != null) jedis.close(); } protected byte[] keyToBytes(Object key) { return key.toString().getBytes(); } protected byte[] valueToBytes(Object value) { return serializer.valueToBytes(value); } protected Object valueFromBytes(byte[] bytes) { return serializer.valueFromBytes(bytes); } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class SaTokenPathFilter implements SaFilter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaTokenPathFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaTokenPathFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaTokenPathFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaTokenPathFilter setExcludeList(List pathList) { excludeList = pathList; return this; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> {}; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e); }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; @Override public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaTokenPathFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } /*@Override public void doFilter(Controller ctx, FilterChain chain) throws Throwable { try { // 执行全局过滤器 beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> { auth.run(null); }); } catch (StopMatchException e) { } catch (Throwable e) { // 1. 获取异常处理策略结果 String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e)); // 2. 写入输出流 ctx.renderText(result); return; } // 执行 chain.doFilter(ctx); }*/ } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal.test; import cn.dev33.satoken.annotation.SaCheckRole; import cn.dev33.satoken.stp.StpUtil; import com.jfinal.core.Controller; import com.jfinal.core.Path; import com.jfinal.server.undertow.UndertowServer; @Path("/") public class AppRun extends Controller { public static void main(String[] args) { UndertowServer.create(Config.class) .addHotSwapClassPrefix("cn.dev33.satoken.jfinal.") .start(); } public void index(){ renderText("index"); } public void doLogin(){ StpUtil.logout(); StpUtil.login(10002); //赋值角色 renderText("登录成功"); } public void getLoginInfo(){ System.out.println("是否登录:"+StpUtil.isLogin()); System.out.println("登录信息"+StpUtil.getTokenInfo()); renderJson(StpUtil.getTokenInfo()); } @SaCheckRole("super-admin") public void add(){ renderText("超级管理员方法!"); } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaCookieConfig; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.jfinal.*; import cn.dev33.satoken.util.SaTokenConsts; import com.jfinal.config.*; import com.jfinal.plugin.redis.RedisPlugin; import com.jfinal.plugin.redis.serializer.ISerializer; import com.jfinal.template.Engine; public class Config extends JFinalConfig { public Config(){ //注册权限验证功能,由saToken处理请求上下文 SaTokenContext saTokenContext = new SaTokenContextForJfinal(); SaManager.setSaTokenContext(saTokenContext); //加载权限角色设置数据接口 SaManager.setStpInterface(new StpInterfaceImpl()); //设置token生成类型 SaTokenConfig saTokenConfig = new SaTokenConfig(); saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID); saTokenConfig.setTimeout(60*60*4); //登录有效时间4小时 saTokenConfig.setActiveTimeout(30*60); //半小时无操作就冻结 token saTokenConfig.setIsShare(false); saTokenConfig.setTokenName("token"); //更改satoken的cookies名称 SaCookieConfig saCookieConfig = new SaCookieConfig(); saCookieConfig.setHttpOnly(true); //开启cookies 的httponly属性 saTokenConfig.setCookie(saCookieConfig); SaManager.setConfig(saTokenConfig); } @Override public void configConstant(Constants constants) { } @Override public void configRoute(Routes routes) { //路由扫描 routes.scan("cn.dev33.satoken.jfinal"); } @Override public void configEngine(Engine engine) { } @Override public void configPlugin(Plugins plugins) { //添加redis扩展 plugins.add(createRedisPlugin("satoken",1, SaJdkSerializer.me)); } @Override public void configInterceptor(Interceptors interceptors) { //开启注解方式权限验证 interceptors.add(new SaAnnotationInterceptor()); } @Override public void configHandler(Handlers handlers) { //将上下文交给satoken处理 handlers.setActionHandler(new SaTokenActionHandler()); } /** * 创建Redis插件 * @param name 名称 * @param dbIndex 使用的库ID * @param serializer 自定义序列化方法 * @return */ private RedisPlugin createRedisPlugin(String name, Integer dbIndex, ISerializer serializer) { RedisPlugin redisPlugin = new RedisPlugin(name, "redis-host", 6379, 3000,"pwd",dbIndex); redisPlugin.setSerializer(serializer); return redisPlugin; } @Override public void onStart(){ //增加redis缓存,需要先配置redis地址 SaManager.setSaTokenDao(new SaTokenDaoRedis("satoken")); } } ================================================ FILE: sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.jfinal.test; import cn.dev33.satoken.stp.StpInterface; import java.util.ArrayList; import java.util.List; public class StpInterfaceImpl implements StpInterface { @Override public List getPermissionList(Object o, String s) { return null; } @Override public List getRoleList(Object o, String s) { List list = new ArrayList(); list.add("admin"); list.add("super-admin"); return list; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml sa-token-loveqq-boot-starter sa-token-loveqq-boot-starter jar loveqq-framework integrate sa-token 1.8 com.kfyty loveqq-core provided com.kfyty loveqq-mvc-core provided com.kfyty loveqq-boot-starter-redisson cn.dev33 sa-token-jackson com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-jsr310 cn.dev33 sa-token-redisson org.redisson redisson jakarta.servlet jakarta.servlet-api true cn.dev33 sa-token-sso true cn.dev33 sa-token-oauth2 true cn.dev33 sa-token-apikey true cn.dev33 sa-token-sign true ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.http.SaHttpTemplate; import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate; import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; import cn.dev33.satoken.json.SaJsonTemplate; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.listener.SaTokenListener; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.loveqq.boot.support.SaPathMatcherHolder; import cn.dev33.satoken.plugin.SaTokenPlugin; import cn.dev33.satoken.plugin.SaTokenPluginHolder; import cn.dev33.satoken.same.SaSameTemplate; import cn.dev33.satoken.secure.totp.SaTotpTemplate; import cn.dev33.satoken.serializer.SaSerializerTemplate; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; import cn.dev33.satoken.temp.SaTempTemplate; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.support.PatternMatcher; import java.util.List; /** * 注入 Sa-Token 所需要的 Bean * * @author click33 * @since 1.34.0 */ @Component public class SaBeanInject { /** * 组件注入 *

    为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化

    * * @param log log 对象 * @param saTokenConfig 配置对象 */ public SaBeanInject(@Autowired(required = false) SaLog log, @Autowired(required = false) SaTokenConfig saTokenConfig, @Autowired(required = false) SaTokenPluginHolder pluginHolder) { if (log != null) { SaManager.setLog(log); } if (saTokenConfig != null) { SaManager.setConfig(saTokenConfig); } // 初始化 Sa-Token SPI 插件 if (pluginHolder == null) { pluginHolder = SaTokenPluginHolder.instance; } pluginHolder.init(); SaTokenPluginHolder.instance = pluginHolder; } /** * 注入持久化Bean * * @param saTokenDao SaTokenDao对象 */ @Autowired(required = false) public void setSaTokenDao(SaTokenDao saTokenDao) { SaManager.setSaTokenDao(saTokenDao); } /** * 注入权限认证Bean * * @param stpInterface StpInterface对象 */ @Autowired(required = false) public void setStpInterface(StpInterface stpInterface) { SaManager.setStpInterface(stpInterface); } /** * 注入上下文Bean * * @param saTokenContext SaTokenContext对象 */ @Autowired(required = false) public void setSaTokenContext(SaTokenContext saTokenContext) { SaManager.setSaTokenContext(saTokenContext); } /** * 注入侦听器Bean * * @param listenerList 侦听器集合 */ @Autowired(required = false) public void setSaTokenListener(List listenerList) { SaTokenEventCenter.registerListenerList(listenerList); } /** * 注入自定义注解处理器 * * @param handlerList 自定义注解处理器集合 */ @Autowired(required = false) public void setSaAnnotationHandler(List> handlerList) { for (SaAnnotationHandlerInterface handler : handlerList) { SaAnnotationStrategy.instance.registerAnnotationHandler(handler); } } /** * 注入临时令牌验证模块 Bean * * @param saTempTemplate / */ @Autowired(required = false) public void setSaTempTemplate(SaTempTemplate saTempTemplate) { SaManager.setSaTempTemplate(saTempTemplate); } /** * 注入 Same-Token 模块 Bean * * @param saSameTemplate saSameTemplate对象 */ @Autowired(required = false) public void setSaIdTemplate(SaSameTemplate saSameTemplate) { SaManager.setSaSameTemplate(saSameTemplate); } /** * 注入 Sa-Token Http Basic 认证模块 * * @param saBasicTemplate saBasicTemplate对象 */ @Autowired(required = false) public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) { SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate; } /** * 注入 Sa-Token Http Digest 认证模块 * * @param saHttpDigestTemplate saHttpDigestTemplate 对象 */ @Autowired(required = false) public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) { SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate; } /** * 注入自定义的 JSON 转换器 Bean * * @param saJsonTemplate JSON 转换器 */ @Autowired(required = false) public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { SaManager.setSaJsonTemplate(saJsonTemplate); } /** * 注入自定义的 Http 转换器 Bean * * @param saHttpTemplate / */ @Autowired(required = false) public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { SaManager.setSaHttpTemplate(saHttpTemplate); } /** * 注入自定义的序列化器 Bean * * @param saSerializerTemplate 序列化器 */ @Autowired(required = false) public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { SaManager.setSaSerializerTemplate(saSerializerTemplate); } /** * 注入自定义的 TOTP 算法 Bean * * @param totpTemplate TOTP 算法类 */ @Autowired(required = false) public void setSaTotpTemplate(SaTotpTemplate totpTemplate) { SaManager.setSaTotpTemplate(totpTemplate); } /** * 注入自定义的 StpLogic * * @param stpLogic / */ @Autowired(required = false) public void setStpLogic(StpLogic stpLogic) { StpUtil.setStpLogic(stpLogic); } /** * 利用自动注入特性,获取Spring框架内部使用的路由匹配器 * * @param pathMatcher 要设置的 pathMatcher */ @Autowired(required = false) public void setPathMatcher(PatternMatcher pathMatcher) { SaPathMatcherHolder.setPathMatcher(pathMatcher); } /** * 注入自定义防火墙校验 hook 集合 * * @param hooks / */ @Autowired(required = false) public void setSaFirewallCheckHooks(List hooks) { for (SaFirewallCheckHook hook : hooks) { SaFirewallStrategy.instance.registerHook(hook); } } /** * 注入CORS 策略处理函数 * * @param corsHandle / */ @Autowired(required = false) public void setCorsHandle(SaCorsHandleFunction corsHandle) { SaStrategy.instance.corsHandle = corsHandle; } /** * 注入自定义插件集合 * * @param plugins / */ @Autowired(required = false) public void setSaTokenPluginList(List plugins) { for (SaTokenPlugin plugin : plugins) { SaTokenPluginHolder.instance.installPlugin(plugin); } } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoForRedisson; import cn.dev33.satoken.loveqq.boot.context.path.ApplicationContextPathLoading; import cn.dev33.satoken.loveqq.boot.filter.SaFirewallCheckFilter; import cn.dev33.satoken.loveqq.boot.filter.SaTokenContextFilter; import cn.dev33.satoken.loveqq.boot.filter.SaTokenCorsFilter; import cn.dev33.satoken.loveqq.boot.support.SaPathMatcherHolder; import cn.dev33.satoken.strategy.SaStrategy; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Import; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnBean; import org.redisson.api.RedissonClient; /** * 注册Sa-Token所需要的Bean *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 * * @author click33 */ @Component @Import(config = { SaFirewallCheckFilter.class, SaTokenContextFilter.class, SaTokenCorsFilter.class }) public class SaBeanRegister { public SaBeanRegister() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = SaPathMatcherHolder::match; } /** * 获取配置Bean * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token") public SaTokenConfig getSaTokenConfig() { return new SaTokenConfig(); } /** * redis dao 集成 * * @return {@link SaTokenDao} */ @Bean @ConditionalOnBean(RedissonClient.class) public SaTokenDao saTokenDao(RedissonClient redisson) { return new SaTokenDaoForRedisson(redisson); } /** * 应用上下文路径加载器 * * @return / */ @Bean public ApplicationContextPathLoading getApplicationContextPathLoading() { return new ApplicationContextPathLoading(); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.apiKey; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; /** * 注入 Sa-Token API Key 所需要的 Bean * * @author click33 * @since 1.43.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.apikey.SaApiKeyManager") public class SaApiKeyBeanInject { /** * 注入 API Key 配置对象 * * @param saApiKeyConfig 配置对象 */ @Autowired(required = false) public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) { SaApiKeyManager.setConfig(saApiKeyConfig); } /** * 注入自定义的 API Key 模版方法 Bean * * @param apiKeyTemplate / */ @Autowired(required = false) public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate); } /** * 注入自定义的 API Key 数据加载器 Bean * * @param apiKeyDataLoader / */ @Autowired(required = false) public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.apiKey; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; /** * 注册 Sa-Token API Key 所需要的 Bean * * @author click33 * @since 1.43.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.apikey.SaApiKeyManager") public class SaApiKeyBeanRegister { /** * 获取 API Key 配置对象 * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token.api-key") public SaApiKeyConfig getSaApiKeyConfig() { return new SaApiKeyConfig(); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/SaReactorHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.context; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; import reactor.core.publisher.Mono; /** * Reactor 上下文操作(异步),持有当前请求的 ServerWebExchange 全局引用 * * @author click33 * @since 1.19.0 */ public class SaReactorHolder { public static final String REQUEST_CONTEXT_ATTRIBUTE = "com.kfyty.loveqq.framework.web.mvc.reactor.request.support.RequestContextHolder.REQUEST_CONTEXT_ATTRIBUTE"; public static final String RESPONSE_CONTEXT_ATTRIBUTE = "com.kfyty.loveqq.framework.web.mvc.reactor.request.support.ResponseContextHolder.REQUEST_CONTEXT_ATTRIBUTE"; /** * 获取 Mono < ServerRequest > * * @return / */ public static Mono getRequest() { return Mono.deferContextual(Mono::just).map(e -> e.get(REQUEST_CONTEXT_ATTRIBUTE)); } /** * 获取 Mono < ServerResponse > * * @return / */ public static Mono getResponse() { return Mono.deferContextual(Mono::just).map(e -> e.get(RESPONSE_CONTEXT_ATTRIBUTE)); } /** * 将 ServerRequest/ServerResponse 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 * * @return / */ public static Mono sync(SaRetGenericFunction fun) { return Mono.deferContextual(ctx -> { SaTokenContextModelBox prev = SaTokenContextUtil.setContext(ctx.get(REQUEST_CONTEXT_ATTRIBUTE), ctx.get(RESPONSE_CONTEXT_ATTRIBUTE)); try { return Mono.just(fun.run()); } finally { SaTokenContextUtil.clearContext(prev); } }); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/path/ApplicationContextPathLoading.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.context.path; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.util.SaFoxUtil; import com.kfyty.loveqq.framework.core.autoconfig.CommandLineRunner; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Value; /** * 应用上下文路径加载器 * * @author click33 * @since 1.37.0 */ public class ApplicationContextPathLoading implements CommandLineRunner { @Value("${k.mvc.tomcat.contextPath:}") private String contextPath; @Override public void run(String... args) throws Exception { String routePrefix = ""; if (SaFoxUtil.isNotEmpty(contextPath)) { if (!contextPath.startsWith("/")) { contextPath = "/" + contextPath; } if (contextPath.endsWith("/")) { contextPath = contextPath.substring(0, contextPath.length() - 1); } routePrefix += contextPath; } if (SaFoxUtil.isNotEmpty(routePrefix) && !routePrefix.equals("/")) { ApplicationInfo.routePrefix = routePrefix; } } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaFirewallCheckFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.filter; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.loveqq.boot.model.LoveqqSaRequest; import cn.dev33.satoken.loveqq.boot.model.LoveqqSaResponse; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.util.SaTokenConsts; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; import com.kfyty.loveqq.framework.web.core.filter.Filter; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; /** * 防火墙校验过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) * * @author click33 * @since 1.37.0 */ @Component @Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) public class SaFirewallCheckFilter implements Filter { @Override public Continue doFilter(ServerRequest request, ServerResponse response) { LoveqqSaRequest saRequest = new LoveqqSaRequest(request); LoveqqSaResponse saResponse = new LoveqqSaResponse(response); SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); try { SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); } catch (StopMatchException ignored) { // ignored } catch (BackResultException e) { SaTokenOperateUtil.writeResult(response, e.getMessage()); return Continue.FALSE; } catch (FirewallCheckException e) { if (SaFirewallStrategy.instance.checkFailHandle == null) { SaTokenOperateUtil.writeResult(response, e.getMessage()); } else { SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); } return Continue.FALSE; } finally { SaTokenContextUtil.clearContext(prev); } // 更多异常则不处理,交由 Web 框架处理 // 向内执行 return Continue.TRUE; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaRequestFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.filter; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaTokenConsts; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; import com.kfyty.loveqq.framework.web.core.filter.Filter; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 全局鉴权过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) *

    * 默认优先级为 -100,尽量保证在其它过滤器之前执行 *

    * * @author click33 * @since 1.19.0 */ @Order(SaTokenConsts.ASSEMBLY_ORDER) public class SaRequestFilter implements SaFilter, Filter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaRequestFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaRequestFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaRequestFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaRequestFilter setExcludeList(List pathList) { excludeList = pathList; return this; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> { }; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e); }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> { }; @Override public SaRequestFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaRequestFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaRequestFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } // ------------------------ doFilter @Override public Continue doFilter(ServerRequest request, ServerResponse response) { SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); try { beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null)); } catch (StopMatchException ignored) { // ignored } catch (BackResultException e) { SaTokenOperateUtil.writeResult(response, e.getMessage()); return Continue.FALSE; } catch (Throwable e) { SaTokenOperateUtil.writeResult(response, String.valueOf(error.run(e))); return Continue.FALSE; } finally { SaTokenContextUtil.clearContext(prev); } // 执行 return Continue.TRUE; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenContextFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.filter; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.util.SaTokenConsts; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; import com.kfyty.loveqq.framework.web.core.filter.Filter; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; /** * SaTokenContext 上下文初始化过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) * * @author click33 * @since 1.42.0 */ @Component @Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) public class SaTokenContextFilter implements Filter { @Override public Continue doFilter(ServerRequest request, ServerResponse response) { SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); return Continue.ofTrue(() -> SaTokenContextUtil.clearContext(prev)); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenCorsFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.filter; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaTokenConsts; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Order; import com.kfyty.loveqq.framework.web.core.filter.Filter; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; /** * CORS 跨域策略过滤器 (基于 loveqq-framework 统一 Filter,可以统一 servlet 和 reactor 配置) * loveqq-framework 也有跨域过滤器,切勿同时配置 * * @author click33 * @see com.kfyty.loveqq.framework.web.core.cors.CorsFilter * @since 1.42.0 */ @Component @Order(SaTokenConsts.CORS_FILTER_ORDER) public class SaTokenCorsFilter implements Filter { @Override public Continue doFilter(ServerRequest request, ServerResponse response) { SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); try { SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); } catch (StopMatchException ignored) { // ignored } catch (BackResultException e) { SaTokenOperateUtil.writeResult(response, e.getMessage()); return Continue.FALSE; } finally { SaTokenContextUtil.clearContext(prev); } return Continue.TRUE; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/interceptor/SaInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.interceptor; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil; import cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; import com.kfyty.loveqq.framework.web.core.interceptor.HandlerInterceptor; import com.kfyty.loveqq.framework.web.core.route.HandlerMethodRoute; import com.kfyty.loveqq.framework.web.core.route.Route; import java.lang.reflect.Method; /** * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力 * * @author click33 * @since 1.31.0 */ public class SaInterceptor implements HandlerInterceptor { /** * 是否打开注解鉴权,配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 */ public boolean isAnnotation = true; /** * 认证前置函数:在注解鉴权之前执行 *

    参数:路由处理函数指针 */ public SaParamFunction beforeAuth = handler -> { }; /** * 认证函数:每次请求执行 *

    参数:路由处理函数指针 */ public SaParamFunction auth = handler -> { }; /** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 */ public SaInterceptor() { } /** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 * * @param auth 认证函数,每次请求执行 */ public SaInterceptor(SaParamFunction auth) { this.auth = auth; } /** * 设置是否打开注解鉴权:配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 * * @param isAnnotation / * @return 对象自身 */ public SaInterceptor isAnnotation(boolean isAnnotation) { this.isAnnotation = isAnnotation; return this; } /** * 写入 [ 认证前置函数 ]: 在注解鉴权之前执行 * * @param beforeAuth / * @return 对象自身 */ public SaInterceptor setBeforeAuth(SaParamFunction beforeAuth) { this.beforeAuth = beforeAuth; return this; } /** * 写入 [ 认证函数 ]: 每次请求执行 * * @param auth / * @return 对象自身 */ public SaInterceptor setAuth(SaParamFunction auth) { this.auth = auth; return this; } // ----------------- 验证方法 ----------------- /** * 每次请求之前触发的方法 */ @Override public boolean preHandle(ServerRequest request, ServerResponse response, Route handler) { SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response); try { // 前置函数:在注解鉴权之前执行 beforeAuth.run(handler); // 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权 if (isAnnotation && handler instanceof HandlerMethodRoute) { Method method = ((HandlerMethodRoute) handler).getMappedMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } // Auth 路由拦截鉴权校验 auth.run(handler); } catch (StopMatchException e) { // StopMatchException 异常代表:停止匹配,进入Controller } catch (BackResultException e) { SaTokenOperateUtil.writeResult(response, e.getMessage()); return false; } finally { SaTokenContextUtil.clearContext(prev); } // 通过验证 return true; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaRequest.java ================================================ package cn.dev33.satoken.loveqq.boot.model; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.util.SaFoxUtil; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; import java.net.HttpCookie; import java.util.Collection; import java.util.Map; /** * 对 SaRequest 包装类的实现 * * @author kfyty725 */ public class LoveqqSaRequest implements SaRequest { /** * loveqq-framework 包装请求 */ private final ServerRequest request; public LoveqqSaRequest(ServerRequest request) { this.request = request; } @Override public Object getSource() { return request; } @Override public String getParam(String name) { return request.getParameter(name); } @Override public Collection getParamNames() { return request.getParameterNames(); } @Override public Map getParamMap() { return request.getParameterMap(); } @Override public String getHeader(String name) { return request.getHeader(name); } @Override public String getCookieValue(String name) { HttpCookie cookie = request.getCookie(name); return cookie == null ? null : cookie.getValue(); } @Override public String getCookieFirstValue(String name) { HttpCookie[] cookies = request.getCookies(); if (cookies != null) { for (HttpCookie cookie : cookies) { if (cookie != null && name.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } @Override public String getCookieLastValue(String name) { String value = null; HttpCookie[] cookies = request.getCookies(); if (cookies != null) { for (HttpCookie cookie : cookies) { if (cookie != null && name.equals(cookie.getName())) { value = cookie.getValue(); } } } return value; } @Override public String getRequestPath() { return ApplicationInfo.cutPathPrefix(request.getRequestURI()); } @Override public String getUrl() { String currDomain = SaManager.getConfig().getCurrDomain(); if (!SaFoxUtil.isEmpty(currDomain)) { return currDomain + this.getRequestPath(); } return request.getRequestURL(); } @Override public String getMethod() { return request.getMethod(); } @Override public String getHost() { return request.getHost(); } @Override public Object forward(String path) { ServerResponse response = (ServerResponse) SaManager.getSaTokenContext().getResponse().getSource(); return response.sendRedirect(path); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaResponse.java ================================================ package cn.dev33.satoken.loveqq.boot.model; import cn.dev33.satoken.context.model.SaResponse; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; /** * 对 SaResponse 包装类的实现 * * @author kfyty725 */ public class LoveqqSaResponse implements SaResponse { /** * loveqq-framework 包装响应 */ private final ServerResponse response; public LoveqqSaResponse(ServerResponse response) { this.response = response; } @Override public Object getSource() { return response; } @Override public SaResponse setStatus(int sc) { response.setStatus(sc); return this; } @Override public SaResponse setHeader(String name, String value) { response.setHeader(name, value); return this; } @Override public SaResponse addHeader(String name, String value) { response.addHeader(name, value); return this; } @Override public Object redirect(String url) { return response.sendRedirect(url); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaStorage.java ================================================ package cn.dev33.satoken.loveqq.boot.model; import cn.dev33.satoken.context.model.SaStorage; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; /** * 对 SaStorage 包装类的实现 * * @author kfyty725 */ public class LoveqqSaStorage implements SaStorage { /** * loveqq-framework 包装请求 */ private final ServerRequest request; public LoveqqSaStorage(ServerRequest request) { this.request = request; } @Override public Object getSource() { return request; } @Override public Object get(String key) { return request.getAttribute(key); } @Override public SaStorage set(String key, Object value) { request.setAttribute(key, value); return this; } @Override public SaStorage delete(String key) { request.removeAttribute(key); return this; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.oauth2; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; import java.util.List; // 小提示:如果你在 idea 中运行源码时出现异常:java: 程序包cn.dev33.satoken.oauth2不存在。 // 在项目根目录进入 cmd,执行 mvn package 即可解决 /** * 注入 Sa-Token-OAuth2 所需要的组件 * * @author click33 * @since 1.34.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.oauth2.SaOAuth2Manager") public class SaOAuth2BeanInject { /** * 注入 OAuth2 配置对象 * * @param saOAuth2Config 配置对象 */ @Autowired(required = false) public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { SaOAuth2Manager.setServerConfig(saOAuth2Config); } /** * 注入 OAuth2 模板代码类 * * @param saOAuth2Template 模板代码类 */ @Autowired(required = false) public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) { SaOAuth2Manager.setTemplate(saOAuth2Template); } /** * 注入 OAuth2 请求处理器 * * @param serverProcessor 请求处理器 */ @Autowired(required = false) public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) { SaOAuth2ServerProcessor.instance = serverProcessor; } /** * 注入 OAuth2 数据加载器 * * @param dataLoader / */ @Autowired(required = false) public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) { SaOAuth2Manager.setDataLoader(dataLoader); } /** * 注入 OAuth2 数据解析器 Bean * * @param dataResolver / */ @Autowired(required = false) public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) { SaOAuth2Manager.setDataResolver(dataResolver); } /** * 注入 OAuth2 数据格式转换器 Bean * * @param dataConverter / */ @Autowired(required = false) public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) { SaOAuth2Manager.setDataConverter(dataConverter); } /** * 注入 OAuth2 数据构建器 Bean * * @param dataGenerate / */ @Autowired(required = false) public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) { SaOAuth2Manager.setDataGenerate(dataGenerate); } /** * 注入 OAuth2 数据持久 Bean * * @param dao / */ @Autowired(required = false) public void setSaOAuth2Dao(SaOAuth2Dao dao) { SaOAuth2Manager.setDao(dao); } /** * 注入自定义 scope 处理器 * * @param handlerList 自定义 scope 处理器集合 */ @Autowired(required = false) public void setSaOAuth2ScopeHandler(List handlerList) { for (SaOAuth2ScopeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerScopeHandler(handler); } } /** * 注入自定义 grant_type 处理器 * * @param handlerList 自定义 grant_type 处理器集合 */ @Autowired(required = false) public void setSaOAuth2GrantTypeHandlerInterface(List handlerList) { for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); } } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.oauth2; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; /** * 注册 Sa-Token-OAuth2 所需要的Bean * * @author click33 * @since 1.34.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.oauth2.SaOAuth2Manager") public class SaOAuth2BeanRegister { /** * 获取 OAuth2 配置 Bean * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token.oauth2-server") public SaOAuth2ServerConfig getSaOAuth2Config() { return new SaOAuth2ServerConfig(); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token 集成 loveqq-framework 的各个组件 */ package cn.dev33.satoken.loveqq.boot; ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.sign; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; import cn.dev33.satoken.sign.template.SaSignTemplate; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; /** * 注入 Sa-Token API 参数签名 所需要的 Bean * * @author click33 * @since 1.43.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.sign.SaSignManager") public class SaSignBeanInject { /** * 注入 API 参数签名配置对象 * * @param saSignConfig 配置对象 */ @Autowired(required = false) public void setSignConfig(SaSignConfig saSignConfig) { SaSignManager.setConfig(saSignConfig); } /** * 注入 API 参数签名配置对象 * * @param signManyConfigWrapper 配置对象 */ @Autowired(required = false) public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) { SaSignManager.setSignMany(signManyConfigWrapper.getSignMany()); } /** * 注入自定义的 参数签名 模版方法 Bean * * @param saSignTemplate 参数签名 Bean */ @Autowired(required = false) public void setSaSignTemplate(SaSignTemplate saSignTemplate) { SaSignManager.setSaSignTemplate(saSignTemplate); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.sign; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; /** * 注册 Sa-Token API 参数签名所需要的 Bean * * @author click33 * @since 1.43.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.sign.SaSignManager") public class SaSignBeanRegister { /** * 获取 API 参数签名配置对象 * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token.sign") public SaSignConfig getSaSignConfig() { return new SaSignConfig(); } /** * 获取 API 参数签名 Many 配置对象 * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token") public SaSignManyConfigWrapper getSaSignManyConfigWrapper() { return new SaSignManyConfigWrapper(); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.sso; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; /** * 注入 Sa-Token SSO 所需要的 Bean * * @author click33 * @since 1.34.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.sso.SaSsoManager") public class SaSsoBeanInject { /** * 注入 Sa-Token SSO Server 端 配置类 * * @param serverConfig 配置对象 */ @Autowired(required = false) public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) { SaSsoManager.setServerConfig(serverConfig); } /** * 注入 Sa-Token SSO Client 端 配置类 * * @param clientConfig 配置对象 */ @Autowired(required = false) public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) { SaSsoManager.setClientConfig(clientConfig); } /** * 注入 SSO 模板代码类 (Server 端) * * @param ssoServerTemplate / */ @Autowired(required = false) public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) { SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate; } /** * 注入 SSO 模板代码类 (Client 端) * * @param ssoClientTemplate / */ @Autowired(required = false) public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) { SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.sso; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean; import com.kfyty.loveqq.framework.core.autoconfig.annotation.Component; import com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass; import com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnMissingBean; /** * 注册 Sa-Token SSO 所需要的 Bean * * @author click33 * @since 1.34.0 */ @Component @ConditionalOnClass("cn.dev33.satoken.sso.SaSsoManager") public class SaSsoBeanRegister { /** * 获取 SSO Server 端 配置对象 * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token.sso-server") public SaSsoServerConfig getSaSsoServerConfig() { return new SaSsoServerConfig(); } /** * 获取 SSO Client 端 配置对象 * * @return 配置对象 */ @Bean @ConfigurationProperties("sa-token.sso-client") public SaSsoClientConfig getSaSsoClientConfig() { return new SaSsoClientConfig(); } /** * 获取 SSO Server 端 SaSsoServerTemplate * * @return / */ @Bean @ConditionalOnMissingBean(SaSsoServerTemplate.class) public SaSsoServerTemplate getSaSsoServerTemplate() { return SaSsoServerProcessor.instance.ssoServerTemplate; } /** * 获取 SSO Client 端 SaSsoClientTemplate * * @return / */ @Bean @ConditionalOnMissingBean(SaSsoClientTemplate.class) public SaSsoClientTemplate getSaSsoClientTemplate() { return SaSsoClientProcessor.instance.ssoClientTemplate; } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/support/SaPathMatcherHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.support; import com.kfyty.loveqq.framework.core.support.AntPathMatcher; import com.kfyty.loveqq.framework.core.support.PatternMatcher; /** * 路由匹配工具类:持有 PathMatcher 全局引用,方便快捷的调用 PathMatcher 相关方法 * * @author click33 * @since 1.34.0 */ public class SaPathMatcherHolder { private SaPathMatcherHolder() { } /** * 路由匹配器 */ public static PatternMatcher pathMatcher; /** * 获取路由匹配器 * * @return 路由匹配器 */ public static PatternMatcher getPathMatcher() { if (pathMatcher == null) { pathMatcher = new AntPathMatcher(); } return pathMatcher; } /** * 写入路由匹配器 * * @param pathMatcher 路由匹配器 */ public static void setPathMatcher(PatternMatcher pathMatcher) { SaPathMatcherHolder.pathMatcher = pathMatcher; } /** * 判断:指定路由匹配符是否可以匹配成功指定路径 * * @param pattern 路由匹配符 * @param path 要匹配的路径 * @return 是否匹配成功 */ public static boolean match(String pattern, String path) { return getPathMatcher().matches(pattern, path); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenContextUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.utils; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.loveqq.boot.model.LoveqqSaRequest; import cn.dev33.satoken.loveqq.boot.model.LoveqqSaResponse; import cn.dev33.satoken.loveqq.boot.model.LoveqqSaStorage; import com.kfyty.loveqq.framework.web.core.http.ServerRequest; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextUtil { /** * 写入当前上下文 * 并返回当前的上下文,以支持 loveqq-framework 的 servlet/reactor 的统一配置 * * @param request / * @param response / */ public static SaTokenContextModelBox setContext(ServerRequest request, ServerResponse response) { SaTokenContextModelBox prev = SaTokenContextForThreadLocalStaff.getModelBoxOrNull(); SaRequest req = new LoveqqSaRequest(request); SaResponse res = new LoveqqSaResponse(response); SaStorage stg = new LoveqqSaStorage(request); SaManager.getSaTokenContext().setContext(req, res, stg); return prev; } /** * 写入上下文对象, 并在执行函数后将其清除 * * @param request / * @param response / * @param fun / */ public static void setContext(ServerRequest request, ServerResponse response, SaFunction fun) { SaTokenContextModelBox prev = setContext(request, response); try { fun.run(); } finally { clearContext(prev); } } /** * 写入上下文对象, 并在执行函数后将其清除 * * @param request / * @param response / * @param fun / * @param / * @return / */ public static T setContext(ServerRequest request, ServerResponse response, SaRetGenericFunction fun) { SaTokenContextModelBox prev = setContext(request, response); try { return fun.run(); } finally { clearContext(prev); } } /** * 清除当前上下文 * 并恢复之前的上下文,以支持 loveqq-framework 的 servlet/reactor 的统一配置 */ public static void clearContext(SaTokenContextModelBox prev) { if (prev == null) { SaManager.getSaTokenContext().clearContext(); } else { SaManager.getSaTokenContext().setContext(prev.getRequest(), prev.getResponse(), prev.getStorage()); } } /** * 获取当前 ModelBox * * @return / */ public static SaTokenContextModelBox getModelBox() { return SaManager.getSaTokenContext().getModelBox(); } /** * 获取当前 Request * * @return / */ public static ServerRequest getRequest() { return (ServerRequest) getModelBox().getRequest().getSource(); } /** * 获取当前 Response * * @return / */ public static ServerResponse getResponse() { return (ServerResponse) getModelBox().getResponse().getSource(); } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenOperateUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.loveqq.boot.utils; import cn.dev33.satoken.util.SaTokenConsts; import com.kfyty.loveqq.framework.core.exception.ResolvableException; import com.kfyty.loveqq.framework.web.core.http.ServerResponse; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; /** * {@link ServerResponse} 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenOperateUtil { /** * 写入结果到输出流 * * @param response / * @param result / */ public static void writeResult(ServerResponse response, String result) { // 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if (response.getContentType() == null) { response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } try (OutputStream out = response.getOutputStream()) { out.write(result.getBytes(StandardCharsets.UTF_8)); out.flush(); } catch (IOException e) { throw new ResolvableException(e); } } } ================================================ FILE: sa-token-starter/sa-token-loveqq-boot-starter/src/main/resources/META-INF/k.factories ================================================ com.kfyty.loveqq.framework.core.autoconfig.annotation.EnableAutoConfiguration=\ cn.dev33.satoken.loveqq.boot.SaBeanRegister,\ cn.dev33.satoken.loveqq.boot.SaBeanInject,\ cn.dev33.satoken.loveqq.boot.apiKey.SaApiKeyBeanRegister,\ cn.dev33.satoken.loveqq.boot.apiKey.SaApiKeyBeanInject,\ cn.dev33.satoken.loveqq.boot.oauth2.SaOAuth2BeanRegister,\ cn.dev33.satoken.loveqq.boot.oauth2.SaOAuth2BeanInject,\ cn.dev33.satoken.loveqq.boot.sign.SaSignBeanRegister,\ cn.dev33.satoken.loveqq.boot.sign.SaSignBeanInject,\ cn.dev33.satoken.loveqq.boot.sso.SaSsoBeanRegister,\ cn.dev33.satoken.loveqq.boot.sso.SaSsoBeanInject ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-reactor-spring-boot-starter sa-token-reactor-spring-boot-starter springboot reactor integrate sa-token org.springframework.boot spring-boot-starter true cn.dev33 sa-token-spring-boot-reactor-v2v3v4-common cn.dev33 sa-token-jackson cn.dev33 sa-token-spring-boot2-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token 集成 Reactor 响应式编程 (SpringBoot 2.x) */ package cn.dev33.satoken.reactor; ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SpringBootVersionCompatibilityChecker.java ================================================ package cn.dev33.satoken.reactor.spring; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.boot.SpringBootVersion; /** * SpringBoot 版本与 Sa-Token 版本兼容检查器,当开发者错误的在 SpringBoot3/4.x 项目中引入当前集成包时,将在控制台做出提醒并阻断项目启动 * * @author Uncarbon * @since 1.38.0 */ public class SpringBootVersionCompatibilityChecker { public SpringBootVersionCompatibilityChecker() { String version = SpringBootVersion.getVersion(); if (SaFoxUtil.isEmpty(version) || version.startsWith("1.") || version.startsWith("2.")) { return; } String str = "当前 SpringBoot 版本(" + version + ")与 Sa-Token 依赖不兼容," + "请将依赖 sa-token-reactor-spring-boot-starter 修改为:sa-token-reactor-spring-boot3/4-starter"; System.err.println(str); throw new SaTokenException(str); } } ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.reactor.spring.SpringBootVersionCompatibilityChecker ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.reactor.spring.SaTokenContextRegister ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot3-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-reactor-spring-boot3-starter sa-token-reactor-spring-boot3-starter springboot3 reactor integrate sa-token cn.dev33 sa-token-spring-boot-reactor-v2v3v4-common cn.dev33 sa-token-jackson cn.dev33 sa-token-spring-boot3-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor; /** * 占位符 * * @author click33 * @since 1.45.0 */ public class Placeholder { } ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.reactor.spring.SaTokenContextRegister ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot4-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-reactor-spring-boot4-starter sa-token-reactor-spring-boot4-starter springboot4 reactor integrate sa-token cn.dev33 sa-token-spring-boot-reactor-v2v3v4-common cn.dev33 sa-token-jackson3 cn.dev33 sa-token-spring-boot4-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor; /** * 占位符 * * @author click33 * @since 1.45.0 */ public class Placeholder { } ================================================ FILE: sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.reactor.spring.SaTokenContextRegister ================================================ FILE: sa-token-starter/sa-token-servlet/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-servlet sa-token-servlet sa-token authentication by Servlet API cn.dev33 sa-token-core javax.servlet javax.servlet-api true ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.error; /** * 定义 sa-token-servlet 所有异常细分状态码 * * @author click33 * @since 1.33.0 */ public interface SaServletErrorCode { /** 转发失败 */ int CODE_20001 = 20001; /** 重定向失败 */ int CODE_20002 = 20002; } ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.servlet.error.SaServletErrorCode; import cn.dev33.satoken.util.SaFoxUtil; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; /** * 对 SaRequest 包装类的实现(Servlet 版) * * @author click33 * @since 1.19.0 */ public class SaRequestForServlet implements SaRequest { /** * 底层Request对象 */ protected HttpServletRequest request; /** * 实例化 * @param request request对象 */ public SaRequestForServlet(HttpServletRequest request) { this.request = request; } /** * 获取底层源对象 */ @Override public Object getSource() { return request; } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { return request.getParameter(name); } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return Collections.list(request.getParameterNames()); } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ // 获取所有参数 Map parameterMap = request.getParameterMap(); Map map = new LinkedHashMap<>(parameterMap.size()); for (String key : parameterMap.keySet()) { String[] values = parameterMap.get(key); map.put(key, values[0]); } return map; } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { return request.getHeader(name); } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { return getCookieLastValue(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie != null && name.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ String value = null; Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie != null && name.equals(cookie.getName())) { value = cookie.getValue(); } } } return value; } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { return ApplicationInfo.cutPathPrefix(request.getRequestURI()); } /** * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { String currDomain = SaManager.getConfig().getCurrDomain(); if( ! SaFoxUtil.isEmpty(currDomain)) { return currDomain + this.getRequestPath(); } return request.getRequestURL().toString(); } /** * 返回当前请求的类型 */ @Override public String getMethod() { return request.getMethod(); } /** * 查询请求 host */ @Override public String getHost() { return request.getServerName(); } /** * 转发请求 */ @Override public Object forward(String path) { try { HttpServletResponse response = (HttpServletResponse)SaManager.getSaTokenContext().getResponse().getSource(); request.getRequestDispatcher(path).forward(request, response); return null; } catch (ServletException | IOException e) { throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20001); } } } ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.servlet.error.SaServletErrorCode; import javax.servlet.http.HttpServletResponse; /** * 对 SaResponse 包装类的实现(Servlet 版) * * @author click33 * @since 1.19.0 */ public class SaResponseForServlet implements SaResponse { /** * 底层Request对象 */ protected HttpServletResponse response; /** * 实例化 * @param response response对象 */ public SaResponseForServlet(HttpServletResponse response) { this.response = response; } /** * 获取底层源对象 */ @Override public Object getSource() { return response; } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { response.setStatus(sc); return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { response.setHeader(name, value); return this; } /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { response.addHeader(name, value); return this; } /** * 重定向 */ @Override public Object redirect(String url) { try { response.sendRedirect(url); } catch (Exception e) { throw new SaTokenException(e).setCode(SaServletErrorCode.CODE_20002); } return null; } } ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.context.model.SaStorage; import javax.servlet.http.HttpServletRequest; /** * 对 SaStorage 包装类的实现(Servlet 版) * * @author click33 * @since 1.19.0 */ public class SaStorageForServlet implements SaStorage { /** * 底层Request对象 */ protected HttpServletRequest request; /** * 实例化 * @param request request对象 */ public SaStorageForServlet(HttpServletRequest request) { this.request = request; } /** * 获取底层源对象 */ @Override public Object getSource() { return request; } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorageForServlet set(String key, Object value) { request.setAttribute(key, value); return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return request.getAttribute(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorageForServlet delete(String key) { request.removeAttribute(key); return this; } } ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token对接 Servlet API 容器所需要的实现类接口包 */ package cn.dev33.satoken.servlet; ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaServletOperateUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.util; import cn.dev33.satoken.util.SaTokenConsts; import javax.servlet.ServletResponse; import java.io.IOException; /** * Servlet 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaServletOperateUtil { /** * 写入结果到输出流 * @param response / * @param result / */ public static void writeResult(ServletResponse response, String result) throws IOException { // 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } response.getWriter().print(result); response.getWriter().flush(); } } ================================================ FILE: sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextServletUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.servlet.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.model.SaStorageForServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextServletUtil { /** * 写入当前上下文 * @param request / * @param response / */ public static void setContext(HttpServletRequest request, HttpServletResponse response) { SaRequest req = new SaRequestForServlet(request); SaResponse res = new SaResponseForServlet(response); SaStorage stg = new SaStorageForServlet(request); SaManager.getSaTokenContext().setContext(req, res, stg); } /** * 写入上下文对象, 并在执行函数后将其清除 * @param request / * @param response / * @param fun / */ public static void setContext(HttpServletRequest request, HttpServletResponse response, SaFunction fun) { try { setContext(request, response); fun.run(); } finally { clearContext(); } } /** * 写入上下文对象, 并在执行函数后将其清除 * * @param request / * @param response / * @param fun / * @return / * @param / */ public static T setContext(HttpServletRequest request, HttpServletResponse response, SaRetGenericFunction fun) { try { setContext(request, response); return fun.run(); } finally { clearContext(); } } /** * 清除当前上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } /** * 获取当前 ModelBox * @return / */ public static SaTokenContextModelBox getModelBox() { return SaManager.getSaTokenContext().getModelBox(); } /** * 获取当前 Request * @return / */ public static HttpServletRequest getRequest() { return (HttpServletRequest) getModelBox().getRequest().getSource(); } /** * 获取当前 Response * @return / */ public static HttpServletResponse getResponse() { return (HttpServletResponse) getModelBox().getResponse().getSource(); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-solon-plugin sa-token-solon-plugin solon integrate sa-token org.noear solon provided cn.dev33 sa-token-core cn.dev33 sa-token-oauth2 true cn.dev33 sa-token-sso true cn.dev33 sa-token-apikey true cn.dev33 sa-token-sign true org.noear redisx provided org.noear snack3 provided org.redisson redisson provided com.fasterxml.jackson.core jackson-databind provided com.fasterxml.jackson.datatype jackson-datatype-jsr310 provided ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.http.SaHttpTemplate; import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate; import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; import cn.dev33.satoken.json.SaJsonTemplate; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.listener.SaTokenListener; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.plugin.SaTokenPlugin; import cn.dev33.satoken.plugin.SaTokenPluginHolder; import cn.dev33.satoken.same.SaSameTemplate; import cn.dev33.satoken.secure.totp.SaTotpTemplate; import cn.dev33.satoken.serializer.SaSerializerTemplate; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; import cn.dev33.satoken.temp.SaTempTemplate; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; import java.util.List; /** * 注入 Sa-Token 所需要的 Bean * * @author click33 * @since 1.34.0 */ @Configuration public class SaBeanInject { /** * 组件注入 *

    为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化

    * * @param log log 对象 * @param saTokenConfig 配置对象 */ public SaBeanInject( @Inject(required = false) SaLog log, @Inject(required = false) SaTokenConfig saTokenConfig, @Inject(required = false) SaTokenPluginHolder pluginHolder ) { if (log != null) { SaManager.setLog(log); } if (saTokenConfig != null) { SaManager.setConfig(saTokenConfig); } // 初始化 Sa-Token SPI 插件 if (pluginHolder == null) { pluginHolder = SaTokenPluginHolder.instance; } pluginHolder.init(); SaTokenPluginHolder.instance = pluginHolder; } /** * 注入持久化Bean * * @param saTokenDao SaTokenDao对象 */ @Condition(onBean = SaTokenDao.class) @Bean public void setSaTokenDao(SaTokenDao saTokenDao) { SaManager.setSaTokenDao(saTokenDao); } /** * 注入权限认证Bean * * @param stpInterface StpInterface对象 */ @Condition(onBean = StpInterface.class) @Bean public void setStpInterface(StpInterface stpInterface) { SaManager.setStpInterface(stpInterface); } /** * 注入上下文Bean * * @param saTokenContext SaTokenContext对象 */ @Condition(onBean = SaTokenContext.class) @Bean public void setSaTokenContext(SaTokenContext saTokenContext) { SaManager.setSaTokenContext(saTokenContext); } /** * 注入侦听器Bean * * @param listenerList 侦听器集合 */ @Bean public void setSaTokenListener(List listenerList) { SaTokenEventCenter.registerListenerList(listenerList); } /** * 注入自定义注解处理器 * * @param handlerList 自定义注解处理器集合 */ @Bean public void setSaAnnotationHandler(List> handlerList) { for (SaAnnotationHandlerInterface handler : handlerList) { SaAnnotationStrategy.instance.registerAnnotationHandler(handler); } } /** * 注入临时令牌验证模块 Bean * * @param saTempTemplate / */ @Condition(onBean = SaTempTemplate.class) @Bean public void setSaTempTemplate(SaTempTemplate saTempTemplate) { SaManager.setSaTempTemplate(saTempTemplate); } /** * 注入 Same-Token 模块 Bean * * @param saSameTemplate saSameTemplate对象 */ @Condition(onBean = SaSameTemplate.class) @Bean public void setSaIdTemplate(SaSameTemplate saSameTemplate) { SaManager.setSaSameTemplate(saSameTemplate); } /** * 注入 Sa-Token Http Basic 认证模块 * * @param saBasicTemplate saBasicTemplate对象 */ @Condition(onBean = SaHttpBasicTemplate.class) @Bean public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) { SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate; } /** * 注入 Sa-Token Http Digest 认证模块 * * @param saHttpDigestTemplate saHttpDigestTemplate 对象 */ @Condition(onBean = SaHttpDigestTemplate.class) @Bean public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) { SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate; } /** * 注入自定义的 JSON 转换器 Bean * * @param saJsonTemplate JSON 转换器 */ @Condition(onBean = SaJsonTemplate.class) @Bean public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { SaManager.setSaJsonTemplate(saJsonTemplate); } /** * 注入自定义的 Http 转换器 Bean * * @param saHttpTemplate Http 转换器 */ @Condition(onBean = SaHttpTemplate.class) @Bean public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { SaManager.setSaHttpTemplate(saHttpTemplate); } /** * 注入自定义的序列化器 Bean * * @param saSerializerTemplate 序列化器 */ @Condition(onBean = SaSerializerTemplate.class) @Bean public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { SaManager.setSaSerializerTemplate(saSerializerTemplate); } /** * 注入自定义的 TOTP 算法 Bean * * @param totpTemplate TOTP 算法类 */ @Condition(onBean = SaTotpTemplate.class) @Bean public void setSaTotpTemplate(SaTotpTemplate totpTemplate) { SaManager.setSaTotpTemplate(totpTemplate); } /** * 注入自定义的 StpLogic * * @param stpLogic / */ @Condition(onBean = StpLogic.class) @Bean public void setStpLogic(StpLogic stpLogic) { StpUtil.setStpLogic(stpLogic); } /** * 注入自定义防火墙校验 hook 集合 * * @param hooks / */ @Bean public void setSaFirewallCheckHooks(List hooks) { for (SaFirewallCheckHook hook : hooks) { SaFirewallStrategy.instance.registerHook(hook); } } /** * 注入CORS 策略处理函数 * * @param corsHandle / */ @Condition(onBean = SaCorsHandleFunction.class) @Bean public void setCorsHandle(SaCorsHandleFunction corsHandle) { SaStrategy.instance.corsHandle = corsHandle; } /** * 注入自定义插件集合 * * @param plugins / */ @Bean public void setSaTokenPluginList(List plugins) { for (SaTokenPlugin plugin : plugins) { SaTokenPluginHolder.instance.installPlugin(plugin); } } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.solon.integration.SaFirewallCheckFilterForSolon; import cn.dev33.satoken.solon.integration.SaTokenContextFilterForSolon; import cn.dev33.satoken.solon.integration.SaTokenCorsFilterForSolon; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaTokenConsts; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.util.PathAnalyzer; /** * 注册Sa-Token所需要的Bean *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 * @author click33 * */ @Configuration public class SaBeanRegister { public SaBeanRegister() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = (pattern, path) -> { return PathAnalyzer.get(pattern).matches(path); }; } /** * 获取配置Bean * * @return 配置对象 */ @Bean public SaTokenConfig getSaTokenConfig(@Inject(value = "${sa-token}", required = false) SaTokenConfig config) { if (config == null) { return new SaTokenConfig(); } else { return config; } } /** * 上下文过滤器 * * @return / */ @Bean(index = SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) public Filter saTokenContextFilterForSolon() { return new SaTokenContextFilterForSolon(); } /** * CORS 跨域策略过滤器 * * @return / */ @Bean(index = SaTokenConsts.CORS_FILTER_ORDER) public Filter saTokenCorsFilterForSolon() { return new SaTokenCorsFilterForSolon(); } /** * 防火墙过滤器 * * @return / */ @Bean(index = SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) public Filter saFirewallCheckFilterForSolon() { return new SaFirewallCheckFilterForSolon(); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaSolonPlugin.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon; import cn.dev33.satoken.solon.apikey.SaApiKeyBeanInject; import cn.dev33.satoken.solon.apikey.SaApiKeyBeanRegister; import cn.dev33.satoken.solon.oauth2.SaOAuth2BeanInject; import cn.dev33.satoken.solon.oauth2.SaOAuth2BeanRegister; import cn.dev33.satoken.solon.sign.SaSignBeanInject; import cn.dev33.satoken.solon.sign.SaSignBeanRegister; import cn.dev33.satoken.solon.sso.SaSsoBeanInject; import cn.dev33.satoken.solon.sso.SaSsoBeanRegister; import org.noear.solon.core.AppContext; import org.noear.solon.core.Plugin; /** * @author noear * @since 1.4 */ public class SaSolonPlugin implements Plugin { @Override public void start(AppContext context) { // sa-token context.beanMake(SaBeanRegister.class); context.beanMake(SaBeanInject.class); // sa-sso context.beanMake(SaSsoBeanRegister.class); context.beanMake(SaSsoBeanInject.class); // sa-oauth2 context.beanMake(SaOAuth2BeanRegister.class); context.beanMake(SaOAuth2BeanInject.class); // sa-apikey context.beanMake(SaApiKeyBeanRegister.class); context.beanMake(SaApiKeyBeanInject.class); // sa-sign context.beanMake(SaSignBeanRegister.class); context.beanMake(SaSignBeanInject.class); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.apikey; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; /** * 注入 Sa-Token API Key 所需要的 Bean * * @author click33 * @since 1.43.0 */ @Condition(onClass=SaApiKeyManager.class) @Configuration public class SaApiKeyBeanInject { /** * 注入 API Key 配置对象 * * @param saApiKeyConfig 配置对象 */ @Bean @Condition(onBean = SaApiKeyConfig.class) public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) { SaApiKeyManager.setConfig(saApiKeyConfig); } /** * 注入自定义的 API Key 模版方法 Bean * * @param apiKeyTemplate / */ @Bean @Condition(onBean = SaApiKeyTemplate.class) public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate); } /** * 注入自定义的 API Key 数据加载器 Bean * * @param apiKeyDataLoader / */ @Bean @Condition(onBean = SaApiKeyDataLoader.class) public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.apikey; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * 注册 Sa-Token API Key 所需要的 Bean * * @author click33 * @since 1.43.0 */ @Configuration @Condition(onClass= SaApiKeyManager.class) public class SaApiKeyBeanRegister { /** * 获取 API Key 配置对象 * @return 配置对象 */ @Bean public SaApiKeyConfig getSaApiKeyConfig(@Inject(value = "${sa-token.api-key}", required = false) SaApiKeyConfig saApiKeyConfig) { if (saApiKeyConfig == null) { return new SaApiKeyConfig(); } else { return saApiKeyConfig; } } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/error/SaSolonErrorCode.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.error; /** * 定义 sa-token-solon-plugin 所有异常细分状态码 * * @author click33 * @since 2022-10-30 */ public interface SaSolonErrorCode { /** 默认的拦截器异常处理函数 */ int CODE_20301 = 20301; /** 默认的 Filter 异常处理函数 */ int CODE_20302 = 20302; } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaFirewallCheckFilterForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.integration; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.solon.model.SaRequestForSolon; import cn.dev33.satoken.solon.model.SaResponseForSolon; import cn.dev33.satoken.solon.util.SaSolonOperateUtil; import cn.dev33.satoken.strategy.SaFirewallStrategy; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 防火墙校验过滤器 (基于 Solon) * * @author noear * @since 1.41.0 */ public class SaFirewallCheckFilterForSolon implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { SaRequestForSolon saRequest = new SaRequestForSolon(); SaResponseForSolon saResponse = new SaResponseForSolon(); try { SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaSolonOperateUtil.writeResult(ctx, e.getMessage()); return; } catch (FirewallCheckException e) { if(SaFirewallStrategy.instance.checkFailHandle == null) { SaSolonOperateUtil.writeResult(ctx, e.getMessage()); } else { SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); } return; } // 更多异常则不处理,交由 Web 框架处理 chain.doFilter(ctx); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenContextFilterForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.integration; import cn.dev33.satoken.solon.util.SaTokenContextSolonUtil; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * 上下文初始化过滤器 (基于 Solon) * * @author noear * @since 1.42.0 */ public class SaTokenContextFilterForSolon implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { SaTokenContextSolonUtil.setContext(ctx); chain.doFilter(ctx); } finally { SaTokenContextSolonUtil.clearContext(); } } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenCorsFilterForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.integration; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.solon.util.SaSolonOperateUtil; import cn.dev33.satoken.strategy.SaStrategy; import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Filter; import org.noear.solon.core.handle.FilterChain; /** * CORS 跨域策略过滤器 (基于 Solon) * * @author click33 * @since 1.42.0 */ public class SaTokenCorsFilterForSolon implements Filter { @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaSolonOperateUtil.writeResult(ctx, e.getMessage()); return; } chain.doFilter(ctx); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.integration; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.solon.util.SaSolonOperateUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import org.noear.solon.Solon; import org.noear.solon.core.handle.*; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * sa-token 基于路由的过滤式鉴权(增加了注解的处理);使用优先级要低些 *

    * 对静态文件有处理效果 *

    * order: -100 (SaTokenInterceptor 和 SaTokenFilter 二选一;不要同时用) * * @author noear * @since 1.10 */ public class SaTokenFilter implements SaFilter, Filter { //之所以改名,为了跟 SaTokenInterceptor 形成一对 /** * 是否打开注解鉴权 */ public boolean isAnnotation = true; // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaTokenFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaTokenFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaTokenFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaTokenFilter setExcludeList(List pathList) { excludeList = pathList; return this; } /** * 获取 [拦截路由] 集合 * * @return see note */ public List getIncludeList() { return includeList; } /** * 获取 [放行路由] 集合 * * @return see note */ public List getExcludeList() { return excludeList; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> { }; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { if (e instanceof SaTokenException) { throw (SaTokenException) e; } else { throw new SaTokenException(e); } }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> { }; @Override public SaTokenFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaTokenFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaTokenFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } @Override public void doFilter(Context ctx, FilterChain chain) throws Throwable { try { //查找当前主处理(在网关内用时,可直接获取缓存) Handler mainHandler = ctx.mainHandler(); if (mainHandler == null) { mainHandler = Solon.app().router().matchMain(ctx); } if (mainHandler instanceof Gateway) { //支持网关处理 Gateway gateway = (Gateway) mainHandler; mainHandler = gateway.find(ctx); } Action action = (mainHandler instanceof Action ? (Action) mainHandler : null); //1.执行前置处理(主要是一些跨域之类的) if(beforeAuth != null) { beforeAuth.run(mainHandler); } //先路径过滤下(包括了静态文件) Handler finalMainHandler = mainHandler; SaRouter.match(includeList).notMatch(excludeList).check(r -> { //2.执行注解处理 if(authAnno(action)) { //3.执行规则处理(如果没有被 @SaIgnore 忽略) auth.run(finalMainHandler); } }); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaSolonOperateUtil.writeResult(ctx, e.getMessage()); return; } catch (SaTokenException e) { SaSolonOperateUtil.writeResult(ctx, error.run(e)); return; } chain.doFilter(ctx); } private boolean authAnno(Action action) { //2.验证注解处理 if (isAnnotation && action != null) { // 注解校验 try{ Method method = action.method().getMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } catch (StopMatchException ignored) { return false; } } return true; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.integration; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.solon.util.SaSolonOperateUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import org.noear.solon.core.handle.*; import org.noear.solon.core.route.RouterInterceptor; import org.noear.solon.core.route.RouterInterceptorChain; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * sa-token 基于路由的过滤式鉴权(增加了注解的处理);使用优先级要低些 *

    * 对静态文件无处理效果 *

    * order: -100 (SaTokenInterceptor 和 SaTokenFilter 二选一;不要同时用) * * @author noear * @since 1.12 */ public class SaTokenInterceptor implements SaFilter, RouterInterceptor { /** * 是否打开注解鉴权 */ public boolean isAnnotation = true; // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ protected List includeList = new ArrayList<>(); /** * 放行路由 */ protected List excludeList = new ArrayList<>(); /** * 添加 [拦截路由] * * @param paths 路由 * @return 对象自身 */ @Override public SaTokenInterceptor addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } /** * 添加 [放行路由] * * @param paths 路由 * @return 对象自身 */ @Override public SaTokenInterceptor addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } /** * 写入 [拦截路由] 集合 * * @param pathList 路由集合 * @return 对象自身 */ @Override public SaTokenInterceptor setIncludeList(List pathList) { includeList = pathList; return this; } /** * 写入 [放行路由] 集合 * * @param pathList 路由集合 * @return 对象自身 */ @Override public SaTokenInterceptor setExcludeList(List pathList) { excludeList = pathList; return this; } /** * 获取 [拦截路由] 集合 * * @return see note */ public List getIncludeList() { return includeList; } /** * 获取 [放行路由] 集合 * * @return see note */ public List getExcludeList() { return excludeList; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ protected SaFilterAuthStrategy auth = r -> { }; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ protected SaFilterErrorStrategy error = e -> { if (e instanceof SaTokenException) { throw (SaTokenException) e; } else { throw new SaTokenException(e); } }; /** * 前置函数:在每次[认证函数]之前执行 */ protected SaFilterAuthStrategy beforeAuth = r -> { }; /** * 写入[认证函数]: 每次请求执行 * * @param auth see note * @return 对象自身 */ @Override public SaTokenInterceptor setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } /** * 写入[异常处理函数]:每次[认证函数]发生异常时执行此函数 * * @param error see note * @return 对象自身 */ @Override public SaTokenInterceptor setError(SaFilterErrorStrategy error) { this.error = error; return this; } /** * 写入[前置函数]:在每次[认证函数]之前执行 * * @param beforeAuth see note * @return 对象自身 */ @Override public SaTokenInterceptor setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } @Override public void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain) throws Throwable { try { if (mainHandler instanceof Gateway) { //支持网关处理 Gateway gateway = (Gateway) mainHandler; mainHandler = gateway.find(ctx); } Action action = (mainHandler instanceof Action ? (Action) mainHandler : null); //1.执行前置处理(主要是一些跨域之类的) if(beforeAuth != null) { beforeAuth.run(mainHandler); } //先路径过滤下(不包括静态文件) Handler finalMainHandler = mainHandler; SaRouter.match(includeList).notMatch(excludeList).check(r -> { //2.执行注解处理 if(authAnno(action)) { //3.执行规则处理(如果没有被 @SaIgnore 忽略) auth.run(finalMainHandler); } }); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaSolonOperateUtil.writeResult(ctx, e.getMessage()); return; } catch (SaTokenException e) { SaSolonOperateUtil.writeResult(ctx, error.run(e)); return; } chain.doIntercept(ctx, mainHandler); } private boolean authAnno(Action action) { //2.验证注解处理 if (isAnnotation && action != null) { // 注解校验 try{ Method method = action.method().getMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } catch (StopMatchException ignored) { return false; } } return true; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaContextForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.model; import cn.dev33.satoken.context.SaTokenContextForReadOnly; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import org.noear.solon.core.handle.Context; /** *

    此为低版本(<1.42.0) 的上下文处理方案,基于 Solon 内部封装 Context.current() 读写上下文,仅做留档,如无必要请勿使用

    * * @author noear * @since 1.4 */ public class SaContextForSolon implements SaTokenContextForReadOnly { /** * 获取当前请求的Request对象 */ @Override public SaRequest getRequest() { return new SaRequestForSolon(); } /** * 获取当前请求的Response对象 */ @Override public SaResponse getResponse() { return new SaResponseForSolon(); } /** * 获取当前请求的 [存储器] 对象 */ @Override public SaStorage getStorage() { return new SaStorageForSolon(); } /** * 此上下文是否有效 * @return / */ public boolean isValid() { return Context.current() != null; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaRequestForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.model; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.util.SaFoxUtil; import org.noear.solon.core.handle.Context; import java.util.Collection; import java.util.Map; /** * @author noear * @since 1.4 */ public class SaRequestForSolon implements SaRequest { protected Context ctx; public SaRequestForSolon() { this(Context.current()); } public SaRequestForSolon(Context ctx) { this.ctx = ctx; } @Override public Object getSource() { return ctx; } @Override public String getParam(String s) { return ctx.param(s); } @Override public Collection getParamNames() { return ctx.paramNames(); } /** * 获取 [请求体] 里提交的所有参数 * * @return 参数列表 */ @Override public Map getParamMap() { return ctx.paramMap().toValueMap(); } @Override public String getHeader(String s) { return ctx.header(s); } @Override public String getCookieValue(String name) { return getCookieLastValue(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name) { return ctx.cookie(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name) { return ctx.cookieMap().holder(name).getLastValue(); } @Override public String getRequestPath() { return ctx.pathNew(); } @Override public String getUrl() { String currDomain = SaManager.getConfig().getCurrDomain(); if (!SaFoxUtil.isEmpty(currDomain)) { return currDomain + this.getRequestPath(); } return ctx.url(); } @Override public String getMethod() { return ctx.method(); } @Override public String getHost() { return ctx.uri().getHost(); } @Override public Object forward(String path) { ctx.forward(path); return null; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaResponseForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.model; import cn.dev33.satoken.context.model.SaResponse; import org.noear.solon.core.handle.Context; /** * @author noear * @since 1.4 */ public class SaResponseForSolon implements SaResponse { protected Context ctx; public SaResponseForSolon() { this(Context.current()); } public SaResponseForSolon(Context ctx) { this.ctx = ctx; } @Override public Object getSource() { return ctx; } @Override public SaResponse setStatus(int sc) { ctx.status(sc); return this; } @Override public SaResponse setHeader(String name, String value) { ctx.headerSet(name, value); return this; } /** * 在响应头里添加一个值 * * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { ctx.headerAdd(name, value); return this; } @Override public Object redirect(String url) { ctx.redirect(url); return null; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaStorageForSolon.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.model; import cn.dev33.satoken.context.model.SaStorage; import org.noear.solon.core.handle.Context; /** * @author noear * @since 1.4 */ public class SaStorageForSolon implements SaStorage { protected Context ctx; public SaStorageForSolon() { this(Context.current()); } public SaStorageForSolon(Context ctx) { this.ctx = ctx; } @Override public Object getSource() { return ctx; } @Override public SaStorageForSolon set(String key, Object value) { ctx.attrSet(key, value); return this; } @Override public Object get(String key) { return ctx.attr(key); } @Override public SaStorageForSolon delete(String key) { ctx.attrMap().remove(key); return this; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.oauth2; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; import java.util.List; // 小提示:如果你在 idea 中运行源码时出现异常:java: 程序包cn.dev33.satoken.oauth2不存在。 // 在项目根目录进入 cmd,执行 mvn package 即可解决 /** * 注入 Sa-Token-OAuth2 所需要的组件 * * @author click33 * @since 1.34.0 */ @Condition(onClass=SaOAuth2Manager.class) @Configuration public class SaOAuth2BeanInject { /** * 注入 OAuth2 配置对象 * * @param saOAuth2Config 配置对象 */ @Condition(onBean = SaOAuth2ServerConfig.class) @Bean public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { SaOAuth2Manager.setServerConfig(saOAuth2Config); } /** * 注入 OAuth2 模板代码类 * * @param saOAuth2Template 模板代码类 */ @Condition(onBean = SaOAuth2Template.class) @Bean public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) { SaOAuth2Manager.setTemplate(saOAuth2Template); } /** * 注入 OAuth2 请求处理器 * * @param serverProcessor 请求处理器 */ @Condition(onBean = SaOAuth2ServerProcessor.class) @Bean public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) { SaOAuth2ServerProcessor.instance = serverProcessor; } /** * 注入 OAuth2 数据加载器 * * @param dataLoader / */ @Condition(onBean = SaOAuth2DataLoader.class) @Bean public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) { SaOAuth2Manager.setDataLoader(dataLoader); } /** * 注入 OAuth2 数据解析器 Bean * * @param dataResolver / */ @Condition(onBean = SaOAuth2DataResolver.class) @Bean public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) { SaOAuth2Manager.setDataResolver(dataResolver); } /** * 注入 OAuth2 数据格式转换器 Bean * * @param dataConverter / */ @Condition(onBean = SaOAuth2DataConverter.class) @Bean public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) { SaOAuth2Manager.setDataConverter(dataConverter); } /** * 注入 OAuth2 数据构建器 Bean * * @param dataGenerate / */ @Condition(onBean = SaOAuth2DataGenerate.class) @Bean public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) { SaOAuth2Manager.setDataGenerate(dataGenerate); } /** * 注入 OAuth2 数据持久 Bean * * @param dao / */ @Condition(onBean = SaOAuth2Dao.class) @Bean public void setSaOAuth2Dao(SaOAuth2Dao dao) { SaOAuth2Manager.setDao(dao); } /** * 注入自定义 scope 处理器 * * @param handlerList 自定义 scope 处理器集合 */ @Bean public void setSaOAuth2ScopeHandler(List handlerList) { for (SaOAuth2ScopeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerScopeHandler(handler); } } /** * 注入自定义 grant_type 处理器 * * @param handlerList 自定义 grant_type 处理器集合 */ @Bean public void setSaOAuth2GrantTypeHandlerInterface(List handlerList) { for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); } } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.oauth2; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * 注册 Sa-Token-OAuth2 所需要的Bean * * @author click33 * @since 1.34.0 */ @Condition(onClass=SaOAuth2Manager.class) @Configuration public class SaOAuth2BeanRegister { /** * 获取 OAuth2 配置 Bean * * @return 配置对象 */ @Bean public SaOAuth2ServerConfig getSaOAuth2Config(@Inject(value = "${sa-token.oauth2-server}", required = false) SaOAuth2ServerConfig serverConfig) { if (serverConfig == null) { return new SaOAuth2ServerConfig(); } else { return serverConfig; } } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/package-info.java ================================================ /** * sa-token 集成 solon 的各个组件 */ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon; ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.sign; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; import cn.dev33.satoken.sign.template.SaSignTemplate; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; /** * 注入 Sa-Token API 参数签名 所需要的 Bean * * @author click33 * @since 1.43.0 */ @Configuration @Condition(onClass= SaSignManager.class) public class SaSignBeanInject { /** * 注入 API 参数签名配置对象 * * @param saSignConfig 配置对象 */ @Bean @Condition(onBean = SaSignConfig.class) public void setSignConfig(SaSignConfig saSignConfig) { SaSignManager.setConfig(saSignConfig); } /** * 注入 API 参数签名配置对象 * * @param signManyConfigWrapper 配置对象 */ @Bean @Condition(onBean = SaSignManyConfigWrapper.class) public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) { SaSignManager.setSignMany(signManyConfigWrapper.getSignMany()); } /** * 注入自定义的 参数签名 模版方法 Bean * * @param saSignTemplate 参数签名 Bean */ @Bean @Condition(onBean = SaSignTemplate.class) public void setSaSignTemplate(SaSignTemplate saSignTemplate) { SaSignManager.setSaSignTemplate(saSignTemplate); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.sign; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * 注册 Sa-Token API 参数签名所需要的 Bean * * @author click33 * @since 1.43.0 */ @Configuration @Condition(onClass= SaSignManager.class) public class SaSignBeanRegister { /** * 获取 API 参数签名配置对象 * @return 配置对象 */ @Bean public SaSignConfig getSaSignConfig(@Inject(value = "${sa-token.sign}", required = false) SaSignConfig saSignConfig) { if (saSignConfig == null) { return new SaSignConfig(); } else { return saSignConfig; } } /** * 获取 API 参数签名 Many 配置对象 * @return 配置对象 */ @Bean public SaSignManyConfigWrapper getSaSignManyConfigWrapper(@Inject(value = "${sa-token}", required = false) SaSignManyConfigWrapper signManyConfigWrapper) { if (signManyConfigWrapper == null) { return new SaSignManyConfigWrapper(); } else { return signManyConfigWrapper; } } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.sso; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; /** * 注入 Sa-Token SSO 所需要的 Bean * * @author click33 * @since 1.34.0 */ @Condition(onClass=SaSsoManager.class) @Configuration public class SaSsoBeanInject { /** * 注入 Sa-Token SSO Server 端 配置类 * * @param serverConfig 配置对象 */ @Condition(onBean = SaSsoServerConfig.class) @Bean public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) { SaSsoManager.setServerConfig(serverConfig); } /** * 注入 Sa-Token SSO Client 端 配置类 * * @param clientConfig 配置对象 */ @Condition(onBean = SaSsoClientConfig.class) @Bean public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) { SaSsoManager.setClientConfig(clientConfig); } /** * 注入 SSO 模板代码类 (Server 端) * * @param ssoServerTemplate / */ @Condition(onBean = SaSsoServerTemplate.class) @Bean public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) { SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate; } /** * 注入 SSO 模板代码类 (Client 端) * * @param ssoClientTemplate / */ @Condition(onBean = SaSsoClientTemplate.class) @Bean public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) { SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.sso; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; import org.noear.solon.annotation.Inject; /** * 注册 Sa-Token SSO 所需要的 Bean * * @author click33 * @since 1.34.0 */ @Condition(onClass=SaSsoManager.class) @Configuration public class SaSsoBeanRegister { /** * 获取 SSO Server 端 配置对象 * * @return 配置对象 */ @Bean public SaSsoServerConfig getSaSsoServerConfig(@Inject(value = "${sa-token.sso-server}", required = false) SaSsoServerConfig serverConfig) { if (serverConfig == null) { return new SaSsoServerConfig(); } else { return serverConfig; } } /** * 获取 SSO Client 端 配置对象 * * @return 配置对象 */ @Bean public SaSsoClientConfig getSaSsoClientConfig(@Inject(value = "${sa-token.sso-client}", required = false) SaSsoClientConfig clientConfig) { if (clientConfig == null) { return new SaSsoClientConfig(); } else { return clientConfig; } } /** * 获取 SSO Server 端 SaSsoServerTemplate * * @return / */ @Bean @Condition(onMissingBean = SaSsoServerTemplate.class) public SaSsoServerTemplate getSaSsoServerTemplate() { return SaSsoServerProcessor.instance.ssoServerTemplate; } /** * 获取 SSO Client 端 SaSsoClientTemplate * * @return / */ @Bean @Condition(onMissingBean = SaSsoClientTemplate.class) public SaSsoClientTemplate getSaSsoClientTemplate() { return SaSsoClientProcessor.instance.ssoClientTemplate; } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaSolonOperateUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.util; import org.noear.solon.core.handle.Context; /** * Solon 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaSolonOperateUtil { /** * 写入结果到输出流 * @param ctx / * @param result / */ public static void writeResult(Context ctx, Object result) throws Throwable { if (result != null) { ctx.render(result); } ctx.setHandled(true); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaTokenContextSolonUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.solon.util; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.fun.SaFunction; import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.solon.model.SaRequestForSolon; import cn.dev33.satoken.solon.model.SaResponseForSolon; import cn.dev33.satoken.solon.model.SaStorageForSolon; import org.noear.solon.core.handle.Context; /** * SaTokenContext 上下文读写工具类 * * @author click33 * @since 1.42.0 */ public class SaTokenContextSolonUtil { /** * 写入当前上下文 */ public static void setContext(Context ctx) { SaRequest req = new SaRequestForSolon(ctx); SaResponse res = new SaResponseForSolon(ctx); SaStorage stg = new SaStorageForSolon(ctx); SaManager.getSaTokenContext().setContext(req, res, stg); } /** * 写入上下文对象, 并在执行函数后将其清除 * @param ctx / * @param fun / */ public static void setContext(Context ctx, SaFunction fun) { try { setContext(ctx); fun.run(); } finally { clearContext(); } } /** * 写入上下文对象, 并在执行函数后将其清除 * * @param ctx / * @param fun / * @return / * @param / */ public static T setContext(Context ctx, SaRetGenericFunction fun) { try { setContext(ctx); return fun.run(); } finally { clearContext(); } } /** * 清除当前上下文 */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } /** * 获取当前 ModelBox * @return / */ public static SaTokenContextModelBox getModelBox() { return SaManager.getSaTokenContext().getModelBox(); } /** * 获取当前 Context * @return / */ public static Context getContext() { return (Context) getModelBox().getStorage().getSource(); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/main/resources/META-INF/solon/cn.dev33.satoken.solon.properties ================================================ solon.plugin=cn.dev33.satoken.solon.SaSolonPlugin ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/test/java/demo/App.java ================================================ package demo; import org.noear.solon.Solon; /** * @author noear 2022/3/30 created */ public class App { public static void main(String[] args) { Solon.start(App.class, args); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/test/java/demo/Config.java ================================================ //package demo; // //import org.noear.solon.annotation.Bean; //import org.noear.solon.annotation.Configuration; //import org.noear.solon.core.handle.Filter; // //import cn.dev33.satoken.router.SaRouter; //import cn.dev33.satoken.solon.integration.SaTokenPathFilter; //import cn.dev33.satoken.stp.StpUtil; // ///** // * @author noear 2022/3/30 created // */ //@Configuration //public class Config { // // @Bean // public Filter saTokenFilter() { // return new SaTokenPathFilter() // // 指定 [拦截路由] 与 [放行路由] // .addInclude("/**").addExclude("/favicon.ico") // // // 认证函数: 每次请求执行 // .setAuth(s -> { // SaRouter.match("/**", StpUtil::checkLogin); // // // 根据路由划分模块,不同模块不同鉴权 // SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); // SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); // SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); // SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); // }) // // // 异常处理函数:每次认证函数发生异常时执行此函数 // .setError(e -> { // System.out.println("---------- sa全局异常 "); // System.out.println(e.getMessage()); // StpUtil.login(123); // return e.getMessage(); // }); // } //} ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/App.java ================================================ package demo2; import org.noear.solon.Solon; /** * @author noear 2022/3/30 created */ public class App { public static void main(String[] args) { Solon.start(App.class, args); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/Config.java ================================================ package demo2; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import org.noear.solon.Solon; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Configuration; /** * @author noear 2022/7/11 created */ @Configuration public class Config { // @Bean // public void saTokenPathInterceptor() { // Solon.app().before(new SaTokenPathInterceptor() // // 指定 [拦截路由] 与 [放行路由] // .addInclude("/**").addExclude("/favicon.ico") // // // 认证函数: 每次请求执行 // .setAuth(s -> { // SaRouter.match("/**", StpUtil::checkLogin); // // // 根据路由划分模块,不同模块不同鉴权 // SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); // SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); // SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); // SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); // }) // // // 异常处理函数:每次认证函数发生异常时执行此函数 // .setError(e -> { // System.out.println("---------- sa全局异常 "); // System.out.println(e.getMessage()); // StpUtil.login(123); // return e.getMessage(); // }) // ); // } @Bean public void saTokenPathInterceptor2() { Solon.app().routerInterceptor((ctx, mainHandler, chain) -> { SaRouter.match("/**", StpUtil::checkLogin); // 根据路由划分模块,不同模块不同鉴权 SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); chain.doIntercept(ctx, mainHandler); }); } } ================================================ FILE: sa-token-starter/sa-token-solon-plugin/src/test/resources/app.yml ================================================ # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true sa-token-dao: #名字可以随意取 redis: server: "localhost:6379" password: 123456 db: 1 maxTotal: 200 ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-spring-boot-reactor-v2v3v4-common sa-token-spring-boot-reactor-v2v3v4-common sa-token springboot reactor v2/v3/v4 common cn.dev33 sa-token-core org.springframework.boot spring-boot-starter true org.springframework spring-web true io.projectreactor reactor-core true org.springframework.boot spring-boot-configuration-processor true cn.dev33 sa-token-spring-boot-webmvc-reactor-v2v3v4-common cn.dev33 sa-token-spring-boot2-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.context; import cn.dev33.satoken.fun.SaRetGenericFunction; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import reactor.util.context.Context; import reactor.util.context.ContextView; /** * Reactor 上下文操作(异步),持有当前请求的 ServerWebExchange 全局引用 * * @author click33 * @since 1.19.0 */ public class SaReactorHolder { /** * ServerWebExchange key */ public static final String EXCHANGE_KEY = "SA_REACTOR_EXCHANGE_KEY"; /** * WebFilterChain key */ public static final String CHAIN_KEY = "SA_REACTOR__CHAIN_KEY"; /** * 在流式上下文写入 ServerWebExchange * @param ctx 必填 * @param exchange 必填 * @param chain 非必填 * @return / */ public static Context setContext(Context ctx, ServerWebExchange exchange, WebFilterChain chain) { return ctx .put(EXCHANGE_KEY, exchange) .put(CHAIN_KEY, chain); } /** * 在流式上下文获取 ServerWebExchange * @param ctx / * @return / */ public static ServerWebExchange getExchange(ContextView ctx) { return ctx.get(EXCHANGE_KEY); } /** * 在流式上下文获取 WebFilterChain * @param ctx / * @return / */ public static WebFilterChain getChain(ContextView ctx) { return ctx.get(CHAIN_KEY); } /** * 获取 Mono < ServerWebExchange > * @return / */ public static Mono getMonoExchange() { return Mono.deferContextual(ctx -> Mono.just(getExchange(ctx))); } /** * 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 * * @return / */ public static Mono sync(SaRetGenericFunction fun) { return Mono.deferContextual(ctx -> { try { SaReactorSyncHolder.setContext(ctx.get(EXCHANGE_KEY)); return Mono.just(fun.run()); } finally { SaReactorSyncHolder.clearContext(); } }); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.context; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.fun.SaRetGenericFunction; import cn.dev33.satoken.reactor.model.SaRequestForReactor; import cn.dev33.satoken.reactor.model.SaResponseForReactor; import cn.dev33.satoken.reactor.model.SaStorageForReactor; import org.springframework.web.server.ServerWebExchange; /** * Reactor上下文操作(同步),持有当前请求的 ServerWebExchange 全局引用 * * @author click33 * @since 1.19.0 */ public class SaReactorSyncHolder { /** * 在同步上下文写入 ServerWebExchange * @param exchange / */ public static void setContext(ServerWebExchange exchange) { SaRequest request = new SaRequestForReactor(exchange.getRequest()); SaResponse response = new SaResponseForReactor(exchange.getResponse()); SaStorage storage = new SaStorageForReactor(exchange); SaManager.getSaTokenContext().setContext(request, response, storage); } /** * 在同步上下文清除 ServerWebExchange */ public static void clearContext() { SaManager.getSaTokenContext().clearContext(); } /** * 在同步上下文获取 ServerWebExchange * @return / */ public static ServerWebExchange getExchange() { SaTokenContextModelBox box = SaManager.getSaTokenContext().getModelBox(); return (ServerWebExchange)box.getStorage().getSource(); } /** * 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文 * @param exchange / * @param fun / */ public static R setContext(ServerWebExchange exchange, SaRetGenericFunction fun) { try { setContext(exchange); return fun.run(); } finally { clearContext(); } } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.filter; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.reactor.model.SaRequestForReactor; import cn.dev33.satoken.reactor.model.SaResponseForReactor; import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * 防火墙校验过滤器 (Reactor版) * * @author click33 * @since 1.37.0 */ @Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) public class SaFirewallCheckFilterForReactor implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { SaRequestForReactor saRequest = new SaRequestForReactor(exchange.getRequest()); SaResponseForReactor saResponse = new SaResponseForReactor(exchange.getResponse()); try { SaReactorSyncHolder.setContext(exchange); SaFirewallStrategy.instance.check.execute(saRequest, saResponse, exchange); } catch (StopMatchException ignored) {} catch (BackResultException e) { return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); } // FirewallCheckException 异常则交由异常处理策略处理 catch (FirewallCheckException e) { if(SaFirewallStrategy.instance.checkFailHandle == null) { return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); } else { SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); return Mono.empty(); } } finally { SaReactorSyncHolder.clearContext(); } // 更多异常则不处理,交由 Web 框架处理 // 向下执行 return chain.filter(exchange); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.filter; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Reactor 全局鉴权过滤器 *

    * 默认优先级为 -100,尽量保证在其它过滤器之前执行 *

    * * @author click33 * @since 1.34.0 */ @Order(SaTokenConsts.ASSEMBLY_ORDER) public class SaReactorFilter implements SaFilter, WebFilter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaReactorFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaReactorFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaReactorFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaReactorFilter setExcludeList(List pathList) { excludeList = pathList; return this; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> {}; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e); }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; @Override public SaReactorFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaReactorFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaReactorFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } // ------------------------ filter @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { // ---------- 全局认证处理 try { SaReactorSyncHolder.setContext(exchange); beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null)); } catch (StopMatchException ignored) {} catch (BackResultException e) { return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); } catch (Throwable e) { return SaReactorOperateUtil.writeResult(exchange, String.valueOf(error.run(e))); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } } /* * 三种 Filter : * WebFilter: Spring WebFlux 的过滤器,用于拦截 Web 请求 * GlobalFilter: Spring Cloud Gateway 的全局过滤器,用于拦截 Gateway 请求 * GatewayFilter: Spring Cloud Gateway 的局部过滤器,用于拦截 Gateway 请求 */ ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.filter; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * SaTokenContext 上下文初始化过滤器 (基于 Reactor) * * @author click33 * @since 1.42.0 */ @Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) public class SaTokenContextFilterForReactor implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange) .contextWrite(ctx -> SaReactorHolder.setContext(ctx, exchange, chain)) .doFinally(r -> { // 在流式上下文中保存的数据会随着流式操作的结束而销毁,所以此处无需手动清除数据 }); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.filter; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.reactor.util.SaReactorOperateUtil; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; /** * CORS 跨域策略过滤器 (基于 Reactor) * * @author click33 * @since 1.42.0 */ @Order(SaTokenConsts.CORS_FILTER_ORDER) public class SaTokenCorsFilterForReactor implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { try { SaReactorSyncHolder.setContext(exchange); SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); } catch (StopMatchException ignored) {} catch (BackResultException e) { return SaReactorOperateUtil.writeResult(exchange, e.getMessage()); } finally { SaReactorSyncHolder.clearContext(); } return chain.filter(exchange); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.model; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.http.HttpCookie; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; import java.util.Collection; import java.util.Map; /** * 对 SaRequest 包装类的实现(Reactor 响应式编程版) * * @author click33 * @since 1.34.0 */ public class SaRequestForReactor implements SaRequest { /** * 底层Request对象 */ protected ServerHttpRequest request; /** * 实例化 * @param request request对象 */ public SaRequestForReactor(ServerHttpRequest request) { this.request = request; } /** * 获取底层源对象 */ @Override public Object getSource() { return request; } /** * 在 [请求体] 里获取一个值 */ @Override public String getParam(String name) { return request.getQueryParams().getFirst(name); } /** * 获取 [请求体] 里提交的所有参数名称 * @return 参数名称列表 */ @Override public Collection getParamNames(){ return request.getQueryParams().keySet(); } /** * 获取 [请求体] 里提交的所有参数 * @return 参数列表 */ @Override public Map getParamMap(){ return request.getQueryParams().toSingleValueMap(); } /** * 在 [请求头] 里获取一个值 */ @Override public String getHeader(String name) { return request.getHeaders().getFirst(name); } /** * 在 [Cookie作用域] 里获取一个值 */ @Override public String getCookieValue(String name) { return getCookieLastValue(name); } /** * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的) */ @Override public String getCookieFirstValue(String name){ HttpCookie cookie = request.getCookies().getFirst(name); if(cookie == null) { return null; } return cookie.getValue(); } /** * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的) * @param name 键 * @return 值 */ @Override public String getCookieLastValue(String name){ String value = null; String cookieStr = getHeader("Cookie"); if(SaFoxUtil.isNotEmpty(cookieStr)) { String[] cookieItems = cookieStr.split(";"); for (String item : cookieItems) { String[] kv = item.split("="); if (kv.length == 2) { if (kv[0].trim().equals(name)) { value = kv[1].trim(); } } } } return value; // 此种写法无法获取到最后一个 Cookie,WebFlux 底层代码应该是有bug,前端提交多个同名Cookie时只能解析出第一个来 // List cookies = request.getCookies().get(name); // if(cookies.isEmpty()) { // return null; // } // return cookies.get(cookies.size() - 1).getValue(); } /** * 返回当前请求path (不包括上下文名称) */ @Override public String getRequestPath() { return ApplicationInfo.cutPathPrefix(request.getPath().toString()); } /** * 返回当前请求的url,例:http://xxx.com/test * @return see note */ public String getUrl() { String currDomain = SaManager.getConfig().getCurrDomain(); if( ! SaFoxUtil.isEmpty(currDomain)) { return currDomain + this.getRequestPath(); } return request.getURI().toString(); } /** * 返回当前请求的类型 */ @Override public String getMethod() { return request.getMethod().name(); } /** * 查询请求 host */ @Override public String getHost() { return request.getURI().getHost(); } /** * 转发请求 */ @Override public Object forward(String path) { ServerWebExchange exchange = SaReactorSyncHolder.getExchange(); WebFilterChain chain = exchange.getAttribute(SaReactorHolder.CHAIN_KEY); ServerHttpRequest newRequest = request.mutate().path(path).build(); ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); return chain.filter(newExchange); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.model; import java.net.URI; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import cn.dev33.satoken.context.model.SaResponse; /** * 对 SaResponse 包装类的实现(Reactor 响应式编程版) * * @author click33 * @since 1.34.0 */ public class SaResponseForReactor implements SaResponse { /** * 底层Response对象 */ protected ServerHttpResponse response; /** * 实例化 * @param response response对象 */ public SaResponseForReactor(ServerHttpResponse response) { this.response = response; } /** * 获取底层源对象 */ @Override public Object getSource() { return response; } /** * 设置响应状态码 */ @Override public SaResponse setStatus(int sc) { response.setStatusCode(HttpStatus.valueOf(sc)); return this; } /** * 在响应头里写入一个值 */ @Override public SaResponse setHeader(String name, String value) { response.getHeaders().set(name, value); return this; } /** * 在响应头里添加一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse addHeader(String name, String value) { response.getHeaders().add(name, value); return this; } /** * 重定向 */ @Override public Object redirect(String url) { response.setStatusCode(HttpStatus.FOUND); response.getHeaders().setLocation(URI.create(url)); return null; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.model; import org.springframework.web.server.ServerWebExchange; import cn.dev33.satoken.context.model.SaStorage; /** * 对 SaStorage 包装类的实现(Reactor 响应式编程版) * * @author click33 * @since 1.34.0 */ public class SaStorageForReactor implements SaStorage { /** * 底层 ServerWebExchange 对象 */ protected ServerWebExchange exchange; /** * 实例化 * @param exchange exchange对象 */ public SaStorageForReactor(ServerWebExchange exchange) { this.exchange = exchange; } /** * 获取底层源对象 */ @Override public Object getSource() { return exchange; } /** * 在 [Request作用域] 里写入一个值 */ @Override public SaStorageForReactor set(String key, Object value) { exchange.getAttributes().put(key, value); return this; } /** * 在 [Request作用域] 里获取一个值 */ @Override public Object get(String key) { return exchange.getAttributes().get(key); } /** * 在 [Request作用域] 里删除一个值 */ @Override public SaStorageForReactor delete(String key) { exchange.getAttributes().remove(key); return this; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token 集成 Reactor 响应式编程的各个组件 */ package cn.dev33.satoken.reactor; ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.spring; import cn.dev33.satoken.context.SaTokenContextForThreadLocal; /** *

    此为低版本(<1.42.0) 的上下文处理方案,仅做留档,如无必要请勿使用

    * * Sa-Token 上下文处理器 [ Spring Reactor 版本实现 ] ,基于 SaTokenContextForThreadLocal 定制 * * @author click33 * @since 1.33.0 */ public class SaTokenContextForSpringReactor extends SaTokenContextForThreadLocal { } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.spring; import cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor; import cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor; import cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor; import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; import cn.dev33.satoken.strategy.SaStrategy; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token 所需要的 Bean * * @author click33 * @since 1.34.0 */ public class SaTokenContextRegister { public SaTokenContextRegister() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = (pattern, path) -> { return SaPathPatternParserUtil.match(pattern, path); }; } /** * 上下文过滤器 * * @return / */ @Bean public SaTokenContextFilterForReactor saTokenContextFilterForServlet() { return new SaTokenContextFilterForReactor(); } /** * CORS 跨域策略过滤器 * * @return / */ @Bean public SaTokenCorsFilterForReactor saTokenCorsFilterForReactor() { return new SaTokenCorsFilterForReactor(); } /** * 防火墙过滤器 * * @return / */ @Bean public SaFirewallCheckFilterForReactor saFirewallCheckFilterForReactor() { return new SaFirewallCheckFilterForReactor(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.reactor.util; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * Reactor 操作工具类 * * @author click33 * @since 1.42.0 */ public class SaReactorOperateUtil { /** * 写入结果到输出流 * @param exchange / * @param result / * @return / */ public static Mono writeResult(ServerWebExchange exchange, String result) { // 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) { exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes()))); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-spring-boot-starter sa-token-spring-boot-starter springboot integrate sa-token org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true cn.dev33 sa-token-servlet cn.dev33 sa-token-spring-boot-webmvc-reactor-v2v3v4-common cn.dev33 sa-token-jackson cn.dev33 sa-token-spring-boot2-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.util.SaServletOperateUtil; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 防火墙校验过滤器 (基于 Servlet) * * @author click33 * @since 1.37.0 */ @Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) public class SaFirewallCheckFilterForServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; SaRequestForServlet saRequest = new SaRequestForServlet(req); SaResponseForServlet saResponse = new SaResponseForServlet(res); try { SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaServletOperateUtil.writeResult(response, e.getMessage()); return; } catch (FirewallCheckException e) { if(SaFirewallStrategy.instance.checkFailHandle == null) { SaServletOperateUtil.writeResult(response, e.getMessage()); } else { SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); } return; } // 更多异常则不处理,交由 Web 框架处理 // 向内执行 chain.doFilter(request, response); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.servlet.util.SaServletOperateUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import javax.servlet.*; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 全局鉴权过滤器 (基于 Servlet) *

    * 默认优先级为 -100,尽量保证在其它过滤器之前执行 *

    * * @author click33 * @since 1.19.0 */ @Order(SaTokenConsts.ASSEMBLY_ORDER) public class SaServletFilter implements SaFilter, Filter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaServletFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaServletFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaServletFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaServletFilter setExcludeList(List pathList) { excludeList = pathList; return this; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> {}; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e); }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; @Override public SaServletFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaServletFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } // ------------------------ doFilter @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 执行全局过滤器 beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> { auth.run(null); }); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaServletOperateUtil.writeResult(response, e.getMessage()); return; } catch (Throwable e) { SaServletOperateUtil.writeResult(response, String.valueOf(error.run(e))); return; } // 执行 chain.doFilter(request, response); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * SaTokenContext 上下文初始化过滤器 (基于 Servlet) * * @author click33 * @since 1.42.0 */ @Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) public class SaTokenContextFilterForServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { SaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response); chain.doFilter(request, response); } finally { SaTokenContextServletUtil.clearContext(); } } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.servlet.util.SaServletOperateUtil; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaTokenConsts; import org.springframework.core.annotation.Order; import javax.servlet.*; import java.io.IOException; /** * CORS 跨域策略过滤器 (基于 Servlet) * * @author click33 * @since 1.42.0 */ @Order(SaTokenConsts.CORS_FILTER_ORDER) public class SaTokenCorsFilterForServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaServletOperateUtil.writeResult(response, e.getMessage()); return; } chain.doFilter(request, response); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.interceptor; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力 * * @author click33 * @since 1.31.0 */ public class SaInterceptor implements HandlerInterceptor { /** * 是否打开注解鉴权,配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 */ public boolean isAnnotation = true; /** * 认证前置函数:在注解鉴权之前执行 *

    参数:路由处理函数指针 */ public SaParamFunction beforeAuth = handler -> {}; /** * 认证函数:每次请求执行 *

    参数:路由处理函数指针 */ public SaParamFunction auth = handler -> {}; /** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 */ public SaInterceptor() { } /** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 * @param auth 认证函数,每次请求执行 */ public SaInterceptor(SaParamFunction auth) { this.auth = auth; } /** * 设置是否打开注解鉴权:配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 * @param isAnnotation / * @return 对象自身 */ public SaInterceptor isAnnotation(boolean isAnnotation) { this.isAnnotation = isAnnotation; return this; } /** * 写入 [ 认证前置函数 ]: 在注解鉴权之前执行 * @param beforeAuth / * @return 对象自身 */ public SaInterceptor setBeforeAuth(SaParamFunction beforeAuth) { this.beforeAuth = beforeAuth; return this; } /** * 写入 [ 认证函数 ]: 每次请求执行 * @param auth / * @return 对象自身 */ public SaInterceptor setAuth(SaParamFunction auth) { this.auth = auth; return this; } // ----------------- 验证方法 ----------------- /** * 每次请求之前触发的方法 */ @Override @SuppressWarnings("all") public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // 前置函数:在注解鉴权之前执行 beforeAuth.run(handler); // 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权 if(isAnnotation && handler instanceof HandlerMethod) { Method method = ((HandlerMethod) handler).getMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } // Auth 路由拦截鉴权校验 auth.run(handler); } catch (StopMatchException e) { // StopMatchException 异常代表:停止匹配,进入Controller } catch (BackResultException e) { // BackResultException 异常代表:停止匹配,向前端输出结果 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 back 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { response.setContentType("text/plain; charset=utf-8"); } response.getWriter().print(e.getMessage()); return false; } // 通过验证 return true; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token 集成 SpringBoot 的各个组件 */ package cn.dev33.satoken; ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.context.SaTokenContextForReadOnly; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.model.SaStorageForServlet; /** *

    此为低版本(<1.42.0) 的上下文处理方案,基于 Spring 内部工具类 RequestContextHolder 读写上下文,仅做留档,如无必要请勿使用

    * * Sa-Token 上下文处理器 [ SpringMVC版本实现 ]。在 SpringMVC、SpringBoot 中使用 Sa-Token 时,必须注入此实现类,否则会出现上下文无效异常 * * @author click33 * @since 1.19.0 */ public class SaTokenContextForSpring implements SaTokenContextForReadOnly { /** * 获取当前请求的 Request 包装对象 */ @Override public SaRequest getRequest() { return new SaRequestForServlet(SpringMVCUtil.getRequest()); } /** * 获取当前请求的 Response 包装对象 */ @Override public SaResponse getResponse() { return new SaResponseForServlet(SpringMVCUtil.getResponse()); } /** * 获取当前请求的 Storage 包装对象 */ @Override public SaStorage getStorage() { return new SaStorageForServlet(SpringMVCUtil.getRequest()); } /** * 判断:在本次请求中,此上下文是否可用。 */ @Override public boolean isValid() { return SpringMVCUtil.isWeb(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.filter.SaFirewallCheckFilterForServlet; import cn.dev33.satoken.filter.SaTokenContextFilterForServlet; import cn.dev33.satoken.filter.SaTokenCorsFilterForServlet; import cn.dev33.satoken.spring.pathmatch.SaPatternsRequestConditionHolder; import cn.dev33.satoken.strategy.SaStrategy; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token 框架所需要的 Bean * * @author click33 * @since 1.34.0 */ public class SaTokenContextRegister { public SaTokenContextRegister() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = (pattern, path) -> { return SaPatternsRequestConditionHolder.match(pattern, path); }; } /** * 上下文过滤器 * * @return / */ @Bean public SaTokenContextFilterForServlet saTokenContextFilterForServlet() { return new SaTokenContextFilterForServlet(); } /** * CORS 跨域策略过滤器 * * @return / */ @Bean public SaTokenCorsFilterForServlet saTokenCorsFilterForServlet() { return new SaTokenCorsFilterForServlet(); } /** * 防火墙过滤器 * * @return / */ @Bean public SaFirewallCheckFilterForServlet saFirewallCheckFilterForServlet() { return new SaFirewallCheckFilterForServlet(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringBootVersionCompatibilityChecker.java ================================================ package cn.dev33.satoken.spring; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.boot.SpringBootVersion; /** * SpringBoot 版本与 Sa-Token 版本兼容检查器,当开发者错误的在 SpringBoot3/4.x 项目中引入当前集成包时,将在控制台做出提醒并阻断项目启动 * * @author Uncarbon * @since 1.38.0 */ public class SpringBootVersionCompatibilityChecker { public SpringBootVersionCompatibilityChecker() { String version = SpringBootVersion.getVersion(); if (SaFoxUtil.isEmpty(version) || version.startsWith("1.") || version.startsWith("2.")) { return; } String str = "当前 SpringBoot 版本(" + version + ")与 Sa-Token 依赖不兼容," + "请将依赖 sa-token-spring-boot-starter 修改为:sa-token-spring-boot3/4-starter"; System.err.println(str); throw new SaTokenException(str); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.exception.NotWebContextException; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * SpringMVC 相关操作工具类,快速获取当前会话的 HttpServletRequest、HttpServletResponse 对象 * * @author click33 * @since 1.19.0 */ public class SpringMVCUtil { private SpringMVCUtil() { } /** * 获取当前会话的 request 对象 * @return request */ public static HttpServletRequest getRequest() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { throw new NotWebContextException("非 web 上下文无法获取 HttpServletRequest"); } return servletRequestAttributes.getRequest(); } /** * 获取当前会话的 response 对象 * @return response */ public static HttpServletResponse getResponse() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { throw new NotWebContextException("非 web 上下文无法获取 HttpServletResponse"); } return servletRequestAttributes.getResponse(); } /** * 判断当前是否处于 Web 上下文中 * @return / */ public static boolean isWeb() { return RequestContextHolder.getRequestAttributes() != null; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.spring.SpringBootVersionCompatibilityChecker ================================================ FILE: sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.spring.SaTokenContextRegister ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-spring-boot-webmvc-reactor-v2v3v4-common sa-token-spring-boot-webmvc-reactor-v2v3v4-common sa-token springboot webmvc/reactor v2/v3/v4 common org.springframework.boot spring-boot-starter true org.springframework spring-webmvc true org.springframework.boot spring-boot-configuration-processor true cn.dev33 sa-token-sso true cn.dev33 sa-token-oauth2 true cn.dev33 sa-token-apikey true cn.dev33 sa-token-sign true cn.dev33 sa-token-spring-boot2-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token 集成 SpringBoot 的各个组件 */ package cn.dev33.satoken; ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction; import cn.dev33.satoken.http.SaHttpTemplate; import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate; import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; import cn.dev33.satoken.json.SaJsonTemplate; import cn.dev33.satoken.listener.SaTokenEventCenter; import cn.dev33.satoken.listener.SaTokenListener; import cn.dev33.satoken.log.SaLog; import cn.dev33.satoken.plugin.SaTokenPlugin; import cn.dev33.satoken.plugin.SaTokenPluginHolder; import cn.dev33.satoken.same.SaSameTemplate; import cn.dev33.satoken.secure.totp.SaTotpTemplate; import cn.dev33.satoken.serializer.SaSerializerTemplate; import cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; import cn.dev33.satoken.temp.SaTempTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.util.PathMatcher; import java.util.List; /** * 注入 Sa-Token 所需要的 Bean * * @author click33 * @since 1.34.0 */ public class SaBeanInject { /** * 组件注入 *

    为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化

    * * @param log log 对象 * @param saTokenConfig 配置对象 */ public SaBeanInject( @Autowired(required = false) SaLog log, @Autowired(required = false) SaTokenConfig saTokenConfig, @Autowired(required = false) SaTokenPluginHolder pluginHolder ){ if(log != null) { SaManager.setLog(log); } if(saTokenConfig != null) { SaManager.setConfig(saTokenConfig); } // 初始化 Sa-Token SPI 插件 if (pluginHolder == null) { pluginHolder = SaTokenPluginHolder.instance; } pluginHolder.init(); SaTokenPluginHolder.instance = pluginHolder; } /** * 注入持久化Bean * * @param saTokenDao SaTokenDao对象 */ @Autowired(required = false) public void setSaTokenDao(SaTokenDao saTokenDao) { SaManager.setSaTokenDao(saTokenDao); } /** * 注入权限认证Bean * * @param stpInterface StpInterface对象 */ @Autowired(required = false) public void setStpInterface(StpInterface stpInterface) { SaManager.setStpInterface(stpInterface); } /** * 注入上下文Bean * * @param saTokenContext SaTokenContext对象 */ @Autowired(required = false) public void setSaTokenContext(SaTokenContext saTokenContext) { SaManager.setSaTokenContext(saTokenContext); } /** * 注入侦听器Bean * * @param listenerList 侦听器集合 */ @Autowired(required = false) public void setSaTokenListener(List listenerList) { SaTokenEventCenter.registerListenerList(listenerList); } /** * 注入自定义注解处理器 * * @param handlerList 自定义注解处理器集合 */ @Autowired(required = false) public void setSaAnnotationHandler(List> handlerList) { for (SaAnnotationHandlerInterface handler : handlerList) { SaAnnotationStrategy.instance.registerAnnotationHandler(handler); } } /** * 注入临时令牌验证模块 Bean * * @param saTempTemplate / */ @Autowired(required = false) public void setSaTempTemplate(SaTempTemplate saTempTemplate) { SaManager.setSaTempTemplate(saTempTemplate); } /** * 注入 Same-Token 模块 Bean * * @param saSameTemplate saSameTemplate对象 */ @Autowired(required = false) public void setSaIdTemplate(SaSameTemplate saSameTemplate) { SaManager.setSaSameTemplate(saSameTemplate); } /** * 注入 Sa-Token Http Basic 认证模块 * * @param saBasicTemplate saBasicTemplate对象 */ @Autowired(required = false) public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) { SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate; } /** * 注入 Sa-Token Http Digest 认证模块 * * @param saHttpDigestTemplate saHttpDigestTemplate 对象 */ @Autowired(required = false) public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) { SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate; } /** * 注入自定义的 JSON 转换器 Bean * * @param saJsonTemplate JSON 转换器 */ @Autowired(required = false) public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) { SaManager.setSaJsonTemplate(saJsonTemplate); } /** * 注入自定义的 Http 转换器 Bean * * @param saHttpTemplate / */ @Autowired(required = false) public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) { SaManager.setSaHttpTemplate(saHttpTemplate); } /** * 注入自定义的序列化器 Bean * * @param saSerializerTemplate 序列化器 */ @Autowired(required = false) public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) { SaManager.setSaSerializerTemplate(saSerializerTemplate); } /** * 注入自定义的 TOTP 算法 Bean * * @param totpTemplate TOTP 算法类 */ @Autowired(required = false) public void setSaTotpTemplate(SaTotpTemplate totpTemplate) { SaManager.setSaTotpTemplate(totpTemplate); } /** * 注入自定义的 StpLogic * @param stpLogic / */ @Autowired(required = false) public void setStpLogic(StpLogic stpLogic) { StpUtil.setStpLogic(stpLogic); } /** * 利用自动注入特性,获取Spring框架内部使用的路由匹配器 * * @param pathMatcher 要设置的 pathMatcher */ @Autowired(required = false) @Qualifier("mvcPathMatcher") public void setPathMatcher(PathMatcher pathMatcher) { SaPathMatcherHolder.setPathMatcher(pathMatcher); } /** * 注入自定义防火墙校验 hook 集合 * * @param hooks / */ @Autowired(required = false) public void setSaFirewallCheckHooks(List hooks) { for (SaFirewallCheckHook hook : hooks) { SaFirewallStrategy.instance.registerHook(hook); } } /** * 注入CORS 策略处理函数 * * @param corsHandle / */ @Autowired(required = false) public void setCorsHandle(SaCorsHandleFunction corsHandle) { SaStrategy.instance.corsHandle = corsHandle; } /** * 注入自定义插件集合 * * @param plugins / */ @Autowired(required = false) public void setSaTokenPluginList(List plugins) { for (SaTokenPlugin plugin : plugins) { SaTokenPluginHolder.instance.installPlugin(plugin); } } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.spring.context.path.ApplicationContextPathLoading; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; /** * 注册Sa-Token所需要的Bean *

    Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖 * @author click33 * */ public class SaBeanRegister { /** * 获取配置Bean * * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token") public SaTokenConfig getSaTokenConfig() { return new SaTokenConfig(); } /** * 应用上下文路径加载器 * @return / */ @Bean public ApplicationContextPathLoading getApplicationContextPathLoading() { return new ApplicationContextPathLoading(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.apikey; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader; import cn.dev33.satoken.apikey.template.SaApiKeyTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; /** * 注入 Sa-Token API Key 所需要的 Bean * * @author click33 * @since 1.43.0 */ @ConditionalOnClass(SaApiKeyManager.class) public class SaApiKeyBeanInject { /** * 注入 API Key 配置对象 * * @param saApiKeyConfig 配置对象 */ @Autowired(required = false) public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) { SaApiKeyManager.setConfig(saApiKeyConfig); } /** * 注入自定义的 API Key 模版方法 Bean * * @param apiKeyTemplate / */ @Autowired(required = false) public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) { SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate); } /** * 注入自定义的 API Key 数据加载器 Bean * * @param apiKeyDataLoader / */ @Autowired(required = false) public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) { SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.apikey; import cn.dev33.satoken.apikey.SaApiKeyManager; import cn.dev33.satoken.apikey.config.SaApiKeyConfig; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token API Key 所需要的 Bean * * @author click33 * @since 1.43.0 */ @ConditionalOnClass(SaApiKeyManager.class) public class SaApiKeyBeanRegister { /** * 获取 API Key 配置对象 * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token.api-key") public SaApiKeyConfig getSaApiKeyConfig() { return new SaApiKeyConfig(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * sa-token-apikey 模块自动化配置(只有引入了 sa-token-apikey 模块后,此包下的代码才会开始工作) */ package cn.dev33.satoken.spring.apikey; ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/context/path/ApplicationContextPathLoading.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.context.path; import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.util.SaFoxUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; /** * 应用上下文路径加载器 * * @author click33 * @since 1.37.0 */ public class ApplicationContextPathLoading implements ApplicationRunner { @Value("${server.servlet.context-path:}") String contextPath; @Value("${spring.mvc.servlet.path:}") String servletPath; @Override public void run(ApplicationArguments args) throws Exception { String routePrefix = ""; if(SaFoxUtil.isNotEmpty(contextPath)) { if(! contextPath.startsWith("/")){ contextPath = "/" + contextPath; } if (contextPath.endsWith("/")) { contextPath = contextPath.substring(0, contextPath.length() - 1); } routePrefix += contextPath; } if(SaFoxUtil.isNotEmpty(servletPath)) { if(! servletPath.startsWith("/")){ servletPath = "/" + servletPath; } if (servletPath.endsWith("/")) { servletPath = servletPath.substring(0, servletPath.length() - 1); } routePrefix += servletPath; } if(SaFoxUtil.isNotEmpty(routePrefix) && ! routePrefix.equals("/") ){ ApplicationInfo.routePrefix = routePrefix; } } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.oauth2; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate; import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader; import cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver; import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface; import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor; import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import java.util.List; // 小提示:如果你在 idea 中运行源码时出现异常:java: 程序包cn.dev33.satoken.oauth2不存在。 // 在项目根目录进入 cmd,执行 mvn package 即可解决 /** * 注入 Sa-Token-OAuth2 所需要的组件 * * @author click33 * @since 1.34.0 */ @ConditionalOnClass(SaOAuth2Manager.class) public class SaOAuth2BeanInject { /** * 注入 OAuth2 配置对象 * * @param saOAuth2Config 配置对象 */ @Autowired(required = false) public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { SaOAuth2Manager.setServerConfig(saOAuth2Config); } /** * 注入 OAuth2 模板代码类 * * @param saOAuth2Template 模板代码类 */ @Autowired(required = false) public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) { SaOAuth2Manager.setTemplate(saOAuth2Template); } /** * 注入 OAuth2 请求处理器 * * @param serverProcessor 请求处理器 */ @Autowired(required = false) public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) { SaOAuth2ServerProcessor.instance = serverProcessor; } /** * 注入 OAuth2 数据加载器 * * @param dataLoader / */ @Autowired(required = false) public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) { SaOAuth2Manager.setDataLoader(dataLoader); } /** * 注入 OAuth2 数据解析器 Bean * * @param dataResolver / */ @Autowired(required = false) public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) { SaOAuth2Manager.setDataResolver(dataResolver); } /** * 注入 OAuth2 数据格式转换器 Bean * * @param dataConverter / */ @Autowired(required = false) public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) { SaOAuth2Manager.setDataConverter(dataConverter); } /** * 注入 OAuth2 数据构建器 Bean * * @param dataGenerate / */ @Autowired(required = false) public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) { SaOAuth2Manager.setDataGenerate(dataGenerate); } /** * 注入 OAuth2 数据持久 Bean * * @param dao / */ @Autowired(required = false) public void setSaOAuth2Dao(SaOAuth2Dao dao) { SaOAuth2Manager.setDao(dao); } /** * 注入自定义 scope 处理器 * * @param handlerList 自定义 scope 处理器集合 */ @Autowired(required = false) public void setSaOAuth2ScopeHandler(List handlerList) { for (SaOAuth2ScopeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerScopeHandler(handler); } } /** * 注入自定义 grant_type 处理器 * * @param handlerList 自定义 grant_type 处理器集合 */ @Autowired(required = false) public void setSaOAuth2GrantTypeHandlerInterface(List handlerList) { for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); } } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.oauth2; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token-OAuth2 所需要的Bean * * @author click33 * @since 1.34.0 */ @ConditionalOnClass(SaOAuth2Manager.class) public class SaOAuth2BeanRegister { /** * 获取 OAuth2 配置 Bean * * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token.oauth2-server") public SaOAuth2ServerConfig getSaOAuth2Config() { return new SaOAuth2ServerConfig(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token-OAuth2 模块自动化配置(只有引入了Sa-Token-OAuth2模块后,此包下的代码才会开始工作) */ package cn.dev33.satoken.spring.oauth2; ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathMatcherHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.pathmatch; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; /** * 路由匹配工具类:持有 PathMatcher 全局引用,方便快捷的调用 PathMatcher 相关方法 * * @author click33 * @since 1.34.0 */ public class SaPathMatcherHolder { private SaPathMatcherHolder() { } /** * 路由匹配器 */ public static PathMatcher pathMatcher; /** * 获取路由匹配器 * @return 路由匹配器 */ public static PathMatcher getPathMatcher() { if(pathMatcher == null) { pathMatcher = new AntPathMatcher(); } return pathMatcher; } /** * 写入路由匹配器 * @param pathMatcher 路由匹配器 */ public static void setPathMatcher(PathMatcher pathMatcher) { SaPathMatcherHolder.pathMatcher = pathMatcher; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathPatternParserUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.pathmatch; import org.springframework.http.server.PathContainer; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; /** * 路由匹配工具类:使用 PathPatternParser 模式匹配 * * @author click33 * @since 1.35.1 */ public class SaPathPatternParserUtil { private SaPathPatternParserUtil() { } /** * 判断:指定路由匹配符是否可以匹配成功指定路径 * @param pattern 路由匹配符 * @param path 要匹配的路径 * @return 是否匹配成功 */ public static boolean match(String pattern, String path) { PathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern); PathContainer pathContainer = PathContainer.parsePath(path); return pathPattern.matches(pathContainer); } /* 表现: springboot 2.x SpringMVC match("/test/test", "/test/test/") // true springboot 2.x WebFlux match("/test/test", "/test/test/") // true springboot 3.x SpringMVC match("/test/test", "/test/test/") // false springboot 3.x WebFlux match("/test/test", "/test/test/") // false */ } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPatternsRequestConditionHolder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.pathmatch; import cn.dev33.satoken.exception.SaTokenException; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * 路由匹配工具类 * * @author click33 * @since 1.35.1 */ public class SaPatternsRequestConditionHolder { private SaPatternsRequestConditionHolder() { } public static PatternsRequestCondition patternsRequestCondition; public static Method matcherMethod; static { try { patternsRequestCondition = new PatternsRequestCondition(); matcherMethod = PatternsRequestCondition.class.getDeclaredMethod("getMatchingPattern", String.class, String.class); matcherMethod.setAccessible(true); } catch (NoSuchMethodException e) { throw new SaTokenException("路由匹配器初始化失败", e); } } /** * 判断:指定路由匹配符是否可以匹配成功指定路径 * @param pattern 路由匹配符 * @param lookupPath 要匹配的路径 * @return 是否匹配成功 */ public static boolean match(String pattern, String lookupPath) { try { return matcherMethod.invoke(patternsRequestCondition, pattern, lookupPath) != null; } catch (IllegalAccessException | InvocationTargetException e) { throw new SaTokenException("路由匹配器调用失败", e); } } /* 性能测试: 100万次 new 对象方式,耗时:3.685s 最慢 反射调方法方式,耗时:1.311s 中等 原始方式,耗时:0.445s 最快,但有bug */ } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.sign; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; import cn.dev33.satoken.sign.template.SaSignTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; /** * 注入 Sa-Token API 参数签名 所需要的 Bean * * @author click33 * @since 1.43.0 */ @ConditionalOnClass(SaSignManager.class) public class SaSignBeanInject { /** * 注入 API 参数签名配置对象 * * @param saSignConfig 配置对象 */ @Autowired(required = false) public void setSignConfig(SaSignConfig saSignConfig) { SaSignManager.setConfig(saSignConfig); } /** * 注入 API 参数签名配置对象 * * @param signManyConfigWrapper 配置对象 */ @Autowired(required = false) public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) { SaSignManager.setSignMany(signManyConfigWrapper.getSignMany()); } /** * 注入自定义的 参数签名 模版方法 Bean * * @param saSignTemplate 参数签名 Bean */ @Autowired(required = false) public void setSaSignTemplate(SaSignTemplate saSignTemplate) { SaSignManager.setSaSignTemplate(saSignTemplate); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.sign; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.sign.config.SaSignManyConfigWrapper; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token API 参数签名所需要的 Bean * * @author click33 * @since 1.43.0 */ @ConditionalOnClass(SaSignManager.class) public class SaSignBeanRegister { /** * 获取 API 参数签名配置对象 * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token.sign") public SaSignConfig getSaSignConfig() { return new SaSignConfig(); } /** * 获取 API 参数签名 Many 配置对象 * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token") public SaSignManyConfigWrapper getSaSignManyConfigWrapper() { return new SaSignManyConfigWrapper(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * sa-token-sign 模块自动化配置(只有引入了 sa-token-sign 模块后,此包下的代码才会开始工作) */ package cn.dev33.satoken.spring.sign; ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanInject.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.sso; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; /** * 注入 Sa-Token SSO 所需要的 Bean * * @author click33 * @since 1.34.0 */ @ConditionalOnClass(SaSsoManager.class) public class SaSsoBeanInject { /** * 注入 Sa-Token SSO Server 端 配置类 * * @param serverConfig 配置对象 */ @Autowired(required = false) public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) { SaSsoManager.setServerConfig(serverConfig); } /** * 注入 Sa-Token SSO Client 端 配置类 * * @param clientConfig 配置对象 */ @Autowired(required = false) public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) { SaSsoManager.setClientConfig(clientConfig); } /** * 注入 SSO 模板代码类 (Server 端) * * @param ssoServerTemplate / */ @Autowired(required = false) public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) { SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate; } /** * 注入 SSO 模板代码类 (Client 端) * * @param ssoClientTemplate / */ @Autowired(required = false) public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) { SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring.sso; import cn.dev33.satoken.sso.SaSsoManager; import cn.dev33.satoken.sso.config.SaSsoClientConfig; import cn.dev33.satoken.sso.config.SaSsoServerConfig; import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; import cn.dev33.satoken.sso.processor.SaSsoServerProcessor; import cn.dev33.satoken.sso.template.SaSsoClientTemplate; import cn.dev33.satoken.sso.template.SaSsoServerTemplate; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token SSO 所需要的 Bean * * @author click33 * @since 1.34.0 */ @ConditionalOnClass(SaSsoManager.class) public class SaSsoBeanRegister { /** * 获取 SSO Server 端 配置对象 * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token.sso-server") public SaSsoServerConfig getSaSsoServerConfig() { return new SaSsoServerConfig(); } /** * 获取 SSO Client 端 配置对象 * @return 配置对象 */ @Bean @ConfigurationProperties(prefix = "sa-token.sso-client") public SaSsoClientConfig getSaSsoClientConfig() { return new SaSsoClientConfig(); } /** * 获取 SSO Server 端 SaSsoServerTemplate * * @return / */ @Bean @ConditionalOnMissingBean(SaSsoServerTemplate.class) public SaSsoServerTemplate getSaSsoServerTemplate() { return SaSsoServerProcessor.instance.ssoServerTemplate; } /** * 获取 SSO Client 端 SaSsoClientTemplate * * @return / */ @Bean @ConditionalOnMissingBean(SaSsoClientTemplate.class) public SaSsoClientTemplate getSaSsoClientTemplate() { return SaSsoClientProcessor.instance.ssoClientTemplate; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token-SSO 模块自动化配置(只有引入了 sa-token-sso 模块后,此包下的代码才会开始工作) */ package cn.dev33.satoken.spring.sso; ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.spring.SaBeanRegister cn.dev33.satoken.spring.SaBeanInject cn.dev33.satoken.spring.sso.SaSsoBeanRegister cn.dev33.satoken.spring.sso.SaSsoBeanInject cn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister cn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject cn.dev33.satoken.spring.apikey.SaApiKeyBeanRegister cn.dev33.satoken.spring.apikey.SaApiKeyBeanInject cn.dev33.satoken.spring.sign.SaSignBeanRegister cn.dev33.satoken.spring.sign.SaSignBeanInject ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.dev33.satoken.spring.SaBeanRegister,\ cn.dev33.satoken.spring.SaBeanInject,\ cn.dev33.satoken.spring.sso.SaSsoBeanRegister,\ cn.dev33.satoken.spring.sso.SaSsoBeanInject,\ cn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister,\ cn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject,\ cn.dev33.satoken.spring.apikey.SaApiKeyBeanRegister,\ cn.dev33.satoken.spring.apikey.SaApiKeyBeanInject,\ cn.dev33.satoken.spring.sign.SaSignBeanRegister,\ cn.dev33.satoken.spring.sign.SaSignBeanInject ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-spring-boot-webmvc-v3v4-common sa-token-spring-boot-webmvc-v3v4-common sa-token springboot webmvc v3v4 common org.springframework.boot spring-boot-starter-web true org.springframework.boot spring-boot-configuration-processor true cn.dev33 sa-token-jakarta-servlet cn.dev33 sa-token-spring-boot-webmvc-reactor-v2v3v4-common cn.dev33 sa-token-spring-boot3-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForJakartaServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.FirewallCheckException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.util.SaTokenConsts; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.annotation.Order; import java.io.IOException; /** * 防火墙校验过滤器 (基于 Jakarta-Servlet) * * @author click33 * @since 1.37.0 */ @Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER) public class SaFirewallCheckFilterForJakartaServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; SaRequestForServlet saRequest = new SaRequestForServlet(req); SaResponseForServlet saResponse = new SaResponseForServlet(res); try { SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); return; } catch (FirewallCheckException e) { if(SaFirewallStrategy.instance.checkFailHandle == null) { SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); } else { SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null); } return; } // 更多异常则不处理,交由 Web 框架处理 // 向内执行 chain.doFilter(request, response); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil; import cn.dev33.satoken.util.SaTokenConsts; import jakarta.servlet.*; import org.springframework.core.annotation.Order; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 全局鉴权过滤器 (基于 Jakarta-Servlet) *

    * 默认优先级为 -100,尽量保证在其它过滤器之前执行 *

    * * @author click33 * @since 1.34.0 */ @Order(SaTokenConsts.ASSEMBLY_ORDER) public class SaServletFilter implements SaFilter, Filter { // ------------------------ 设置此过滤器 拦截 & 放行 的路由 /** * 拦截路由 */ public List includeList = new ArrayList<>(); /** * 放行路由 */ public List excludeList = new ArrayList<>(); @Override public SaServletFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; } @Override public SaServletFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; } @Override public SaServletFilter setIncludeList(List pathList) { includeList = pathList; return this; } @Override public SaServletFilter setExcludeList(List pathList) { excludeList = pathList; return this; } // ------------------------ 钩子函数 /** * 认证函数:每次请求执行 */ public SaFilterAuthStrategy auth = r -> {}; /** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e); }; /** * 前置函数:在每次[认证函数]之前执行 * 注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth */ public SaFilterAuthStrategy beforeAuth = r -> {}; @Override public SaServletFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; } @Override public SaServletFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; } @Override public SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; } // ------------------------ doFilter @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 执行全局过滤器 beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> { auth.run(null); }); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); return; } catch (Throwable e) { SaJakartaServletOperateUtil.writeResult(response, String.valueOf(error.run(e))); return; } // 执行 chain.doFilter(request, response); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForJakartaServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil; import cn.dev33.satoken.util.SaTokenConsts; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.annotation.Order; import java.io.IOException; /** * SaTokenContext 上下文初始化过滤器 (基于 Jakarta-Servlet) * * @author click33 * @since 1.42.0 */ @Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER) public class SaTokenContextFilterForJakartaServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { SaTokenContextJakartaServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response); chain.doFilter(request, response); } finally { SaTokenContextJakartaServletUtil.clearContext(); } } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForJakartaServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.filter; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaTokenContextModelBox; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaTokenConsts; import jakarta.servlet.*; import org.springframework.core.annotation.Order; import java.io.IOException; /** * CORS 跨域策略过滤器 (基于 Jakarta-Servlet) * * @author click33 * @since 1.42.0 */ @Order(SaTokenConsts.CORS_FILTER_ORDER) public class SaTokenCorsFilterForJakartaServlet implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { SaTokenContextModelBox box = SaHolder.getContext().getModelBox(); SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage()); } catch (StopMatchException ignored) {} catch (BackResultException e) { SaJakartaServletOperateUtil.writeResult(response, e.getMessage()); return; } chain.doFilter(request, response); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.interceptor; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.fun.SaParamFunction; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import java.lang.reflect.Method; /** * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力 * * @author click33 * @since 1.34.0 */ public class SaInterceptor implements HandlerInterceptor { /** * 是否打开注解鉴权 */ public boolean isAnnotation = true; /** * 认证函数:每次请求执行 *

    参数:路由处理函数指针 */ public SaParamFunction auth = handler -> {}; /** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 */ public SaInterceptor() { } /** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 * @param auth 认证函数,每次请求执行 */ public SaInterceptor(SaParamFunction auth) { this.auth = auth; } /** * 设置是否打开注解鉴权 * @param isAnnotation / * @return 对象自身 */ public SaInterceptor isAnnotation(boolean isAnnotation) { this.isAnnotation = isAnnotation; return this; } /** * 写入[认证函数]: 每次请求执行 * @param auth / * @return 对象自身 */ public SaInterceptor setAuth(SaParamFunction auth) { this.auth = auth; return this; } // ----------------- 验证方法 ----------------- /** * 每次请求之前触发的方法 */ @Override @SuppressWarnings("all") public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权 if(isAnnotation && handler instanceof HandlerMethod) { Method method = ((HandlerMethod) handler).getMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } // Auth 校验 auth.run(handler); } catch (StopMatchException e) { // StopMatchException 异常代表:停止匹配,进入Controller } catch (BackResultException e) { // BackResultException 异常代表:停止匹配,向前端输出结果 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 back 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { response.setContentType("text/plain; charset=utf-8"); } response.getWriter().print(e.getMessage()); return false; } // 通过验证 return true; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/package-info.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Sa-Token 集成 SpringBoot3 的各个组件 */ package cn.dev33.satoken; ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpringInJakartaServlet.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.context.SaTokenContextForReadOnly; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.context.model.SaResponse; import cn.dev33.satoken.context.model.SaStorage; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.servlet.model.SaResponseForServlet; import cn.dev33.satoken.servlet.model.SaStorageForServlet; /** *

    此为低版本(<1.42.0) 的上下文处理方案,基于 Spring 内部工具类 RequestContextHolder 读写上下文,仅做留档,如无必要请勿使用

    * * Sa-Token 上下文处理器 [ SpringBoot3 Jakarta Servlet 版 ],在 SpringBoot3 中使用 Sa-Token 时,必须注入此实现类,否则会出现上下文无效异常 * * @author click33 * @since 1.34.0 */ public class SaTokenContextForSpringInJakartaServlet implements SaTokenContextForReadOnly { /** * 获取当前请求的 Request 包装对象 */ @Override public SaRequest getRequest() { return new SaRequestForServlet(SpringMVCUtil.getRequest()); } /** * 获取当前请求的 Response 包装对象 */ @Override public SaResponse getResponse() { return new SaResponseForServlet(SpringMVCUtil.getResponse()); } /** * 获取当前请求的 Storage 包装对象 */ @Override public SaStorage getStorage() { return new SaStorageForServlet(SpringMVCUtil.getRequest()); } /** * 判断:在本次请求中,此上下文是否可用。 */ @Override public boolean isValid() { return SpringMVCUtil.isWeb(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.filter.SaFirewallCheckFilterForJakartaServlet; import cn.dev33.satoken.filter.SaTokenContextFilterForJakartaServlet; import cn.dev33.satoken.filter.SaTokenCorsFilterForJakartaServlet; import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; import cn.dev33.satoken.strategy.SaStrategy; import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token 框架所需要的 Bean * * @author click33 * @since 1.34.0 */ public class SaTokenContextRegister { public SaTokenContextRegister() { // 重写路由匹配算法 SaStrategy.instance.routeMatcher = (pattern, path) -> { return SaPathPatternParserUtil.match(pattern, path); }; } /** * 上下文过滤器 * * @return / */ @Bean public SaTokenContextFilterForJakartaServlet saTokenContextFilterForServlet() { return new SaTokenContextFilterForJakartaServlet(); } /** * CORS 跨域策略过滤器 * * @return / */ @Bean public SaTokenCorsFilterForJakartaServlet saTokenCorsFilterForJakartaServlet() { return new SaTokenCorsFilterForJakartaServlet(); } /** * 防火墙过滤器 * * @return / */ @Bean public SaFirewallCheckFilterForJakartaServlet saFirewallCheckFilterForJakartaServlet() { return new SaFirewallCheckFilterForJakartaServlet(); } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.spring; import cn.dev33.satoken.exception.NotWebContextException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; /** * SpringMVC 相关操作工具类,快速获取当前会话的 HttpServletRequest、HttpServletResponse 对象 * * @author click33 * @since 1.34.0 */ public class SpringMVCUtil { private SpringMVCUtil() { } /** * 获取当前会话的 request * @return request */ public static HttpServletRequest getRequest() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { throw new NotWebContextException("非 web 上下文无法获取 HttpServletRequest"); } return servletRequestAttributes.getRequest(); } /** * 获取当前会话的 response * @return response */ public static HttpServletResponse getResponse() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { throw new NotWebContextException("非 web 上下文无法获取 HttpServletRequest"); } return servletRequestAttributes.getResponse(); } /** * 判断当前是否处于 Web 上下文中 * @return request */ public static boolean isWeb() { return RequestContextHolder.getRequestAttributes() != null; } } ================================================ FILE: sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ cn.dev33.satoken.spring.SaTokenContextRegister ================================================ FILE: sa-token-starter/sa-token-spring-boot3-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-spring-boot3-starter sa-token-spring-boot3-starter springboot3 integrate sa-token cn.dev33 sa-token-spring-boot-webmvc-v3v4-common cn.dev33 sa-token-jackson cn.dev33 sa-token-spring-boot3-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/Placeholder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken; /** * 占位符 * * @author click33 * @since 1.45.0 */ public class Placeholder { } ================================================ FILE: sa-token-starter/sa-token-spring-boot4-starter/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-starter ${revision} ../pom.xml jar sa-token-spring-boot4-starter sa-token-spring-boot4-starter springboot4 integrate sa-token cn.dev33 sa-token-spring-boot-webmvc-v3v4-common cn.dev33 sa-token-jackson3 cn.dev33 sa-token-spring-boot4-dependencies ${revision} pom import ================================================ FILE: sa-token-starter/sa-token-spring-boot4-starter/src/main/java/cn/dev33/satoken/Placeholder.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken; /** * 占位符 * * @author click33 * @since 1.45.0 */ public class Placeholder { } ================================================ FILE: sa-token-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-parent ${revision} ../pom.xml pom sa-token-test sa-token-test sa-token-test sa-token-easy-test sa-token-springboot-test sa-token-jwt-test sa-token-temp-jwt-test sa-token-json-test sa-token-jackson3-test sa-token-serializer-test org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-configuration-processor true cn.dev33 sa-token-spring-boot2-dependencies ${revision} pom import org.springframework.boot spring-boot-starter-test 2.7.18 org.jacoco jacoco-maven-plugin 0.8.6 prepare-agent prepare-agent report-aggregate test report-aggregate org.apache.maven.plugins maven-surefire-plugin 2.22.2 ${argLine} -Xms256m -Xmx2048m -Dfile.encoding=utf-8 1 random ================================================ FILE: sa-token-test/sa-token-easy-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-easy-test sa-token-easy-test sa-token-easy-test cn.dev33 sa-token-jackson3 ${revision} tools.jackson.core jackson-databind 3.0.0 com.fasterxml.jackson.datatype jackson-datatype-jsr310 ================================================ FILE: sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/SaJsonTemplateTest.java ================================================ package com.pj.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.NotImplException; import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; import cn.dev33.satoken.json.SaJsonTemplateForJackson3; import com.pj.test.model.SysUser; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; /** * Sa-Token json 序列化模块测试 * * @author click33 * */ public class SaJsonTemplateTest { // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ SaJsonTemplateTest star ..."); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ SaJsonTemplateTest end ... \n"); } // 测试:DefaultImpl @Test public void testDefaultImpl() { SaManager.setSaJsonTemplate(new SaJsonTemplateDefaultImpl()); Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateDefaultImpl.class); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().objectToJson(user) ); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx", SysUser.class) ); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx") ); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToMap("xxx") ); } // 测试:Jackson3 @Test public void testJackson3() { SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3()); Assertions.assertEquals(SaJsonTemplateForJackson3.class, SaManager.getSaJsonTemplate().getClass()); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); // 与 json2 不同点:Jackson 3 默认按字母序排列属性 Assertions.assertEquals("{\"@class\":\"com.pj.test.model.SysUser\",\"age\":18,\"id\":10001,\"name\":\"张三\",\"role\":null}", objectJson); // test Json -> Object SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson); Assertions.assertEquals(user3.toString(), user.toString()); // more testNull(); testMap(); } // 测试 Map 的转换 private void testMap() { // test Map -> Json Map map = new HashMap<>(); map.put("id", 10001); map.put("name", "张三"); map.put("age", 18); String mapJson = SaManager.getSaJsonTemplate().objectToJson(map); Assertions.assertEquals("{\"name\":\"张三\",\"id\":10001,\"age\":18}", mapJson); // test Json -> Map Map map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson); Assertions.assertEquals(map2.toString(), map.toString()); } // 测试 Null 值 private void testNull() { Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null)); } } ================================================ FILE: sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysRole.java ================================================ package com.pj.test.model; /** * Role 实体类 * * @author click33 * @since 2022-10-15 */ public class SysRole { // // public SysRole() { // } // // public SysRole(long id, String name) { // super(); // this.id = id; // this.name = name; // } // // // /** // * 角色id // */ // private long id; // // /** // * 角色名称 // */ // private String name; // // /** // * @return id // */ // public long getId() { // return id; // } // // /** // * @param id 要设置的 id // */ // public void setId(long id) { // this.id = id; // } // // /** // * @return name // */ // public String getName() { // return name; // } // // /** // * @param name 要设置的 name // */ // public void setName(String name) { // this.name = name; // } // // @Override // public String toString() { // return "SysRole [id=" + id + ", name=" + name + "]"; // } // } ================================================ FILE: sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysUser.java ================================================ package com.pj.test.model; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser { public SysUser() { } public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * 用户角色 */ private SysRole role; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } public SysRole getRole() { return role; } public SysUser setRole(SysRole role) { this.role = role; return this; } @Override public String toString() { return "SysUser{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", role=" + role + '}'; } } ================================================ FILE: sa-token-test/sa-token-jackson3-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-jackson3-test sa-token-jackson3-test sa-token-jackson3-test cn.dev33 sa-token-jackson3 ${revision} tools.jackson.core jackson-databind 3.0.0 com.fasterxml.jackson.datatype jackson-datatype-jsr310 ================================================ FILE: sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/SaJsonTemplateForJackson3Test.java ================================================ package com.pj.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.json.SaJsonTemplateForJackson3; import com.pj.test.model.SysUser; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; /** * Sa-Token-jackson3 序列化模块测试 * *
     * 为什么单独写一个模块来测试 Jackson 3 ?
     *
     * 在同一个项目里同时引入 jackson 2 和 jackson 3 后,
     * 执行:
     *      SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson);
     * 会报错:
     *      java.lang.NoSuchFieldError: POJO
     * 	        at tools.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:399)
     * 	        at tools.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:361)
     * 	        at tools.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:265)
     * 	        at tools.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
     * 	        at tools.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:158)
     * 	        at tools.jackson.databind.DeserializationContext.findNonContextualValueDeserializer(DeserializationContext.java:733)
     * 	        at tools.jackson.databind.deser.jdk.UntypedObjectDeserializer._findCustomDeser(UntypedObjectDeserializer.java:179)
     * 	        at tools.jackson.databind.deser.jdk.UntypedObjectDeserializer.resolve(UntypedObjectDeserializer.java:152)
     *
     * 暂未找到解决方案,所以只能单独写一个测试类来测试 Jackson 3 的功能了。
     *
     * 
    * @author click33 * */ public class SaJsonTemplateForJackson3Test { // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ SaJsonTemplateForJackson3 Test star ..."); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ SaJsonTemplateForJackson3 Test end ... \n"); } // 测试:Jackson3 @Test public void testJackson3() { SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3()); Assertions.assertEquals(SaJsonTemplateForJackson3.class, SaManager.getSaJsonTemplate().getClass()); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); // 与 json2 不同点:Jackson 3 默认按字母序排列属性 Assertions.assertEquals("{\"@class\":\"com.pj.test.model.SysUser\",\"age\":18,\"id\":10001,\"name\":\"张三\",\"role\":null}", objectJson); // test Json -> Object SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson); Assertions.assertEquals(user3.toString(), user.toString()); // more testNull(); testMap(); } // 测试 Map 的转换 private void testMap() { // test Map -> Json Map map = new HashMap<>(); map.put("id", 10001); map.put("name", "张三"); map.put("age", 18); String mapJson = SaManager.getSaJsonTemplate().objectToJson(map); Assertions.assertEquals("{\"name\":\"张三\",\"id\":10001,\"age\":18}", mapJson); // test Json -> Map Map map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson); Assertions.assertEquals(map2.toString(), map.toString()); } // 测试 Null 值 private void testNull() { Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null)); } } ================================================ FILE: sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysRole.java ================================================ package com.pj.test.model; /** * Role 实体类 * * @author click33 * @since 2022-10-15 */ public class SysRole { // // public SysRole() { // } // // public SysRole(long id, String name) { // super(); // this.id = id; // this.name = name; // } // // // /** // * 角色id // */ // private long id; // // /** // * 角色名称 // */ // private String name; // // /** // * @return id // */ // public long getId() { // return id; // } // // /** // * @param id 要设置的 id // */ // public void setId(long id) { // this.id = id; // } // // /** // * @return name // */ // public String getName() { // return name; // } // // /** // * @param name 要设置的 name // */ // public void setName(String name) { // this.name = name; // } // // @Override // public String toString() { // return "SysRole [id=" + id + ", name=" + name + "]"; // } // } ================================================ FILE: sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysUser.java ================================================ package com.pj.test.model; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser { public SysUser() { } public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * 用户角色 */ private SysRole role; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } public SysRole getRole() { return role; } public SysUser setRole(SysRole role) { this.role = role; return this; } @Override public String toString() { return "SysUser{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", role=" + role + '}'; } } ================================================ FILE: sa-token-test/sa-token-json-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-json-test sa-token-json-test sa-token-json-test cn.dev33 sa-token-jackson cn.dev33 sa-token-fastjson cn.dev33 sa-token-fastjson2 cn.dev33 sa-token-snack3 com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-jsr310 ================================================ FILE: sa-token-test/sa-token-json-test/src/test/java/com/pj/test/SaJsonTemplateTest.java ================================================ package com.pj.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.NotImplException; import cn.dev33.satoken.json.*; import com.pj.test.model.SysUser; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; /** * Sa-Token json 序列化模块测试 * * @author click33 * */ public class SaJsonTemplateTest { // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ SaJsonTemplateTest star ..."); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ SaJsonTemplateTest end ... \n"); } // 测试:DefaultImpl @Test public void testDefaultImpl() { SaManager.setSaJsonTemplate(new SaJsonTemplateDefaultImpl()); Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateDefaultImpl.class); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().objectToJson(user) ); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx", SysUser.class) ); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject("xxx") ); Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToMap("xxx") ); } // 测试:Jackson @Test public void testJackson() { SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson()); Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForJackson.class); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); Assertions.assertEquals("{\"@class\":\"com.pj.test.model.SysUser\",\"id\":10001,\"name\":\"张三\",\"age\":18,\"role\":null}", objectJson); // test Json -> Object SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson); Assertions.assertEquals(user3.toString(), user.toString()); // more testNull(); testMap(); } // 测试:Fastjson @Test public void testFastjson() { SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson()); Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForFastjson.class); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); Assertions.assertEquals("{\"age\":18,\"id\":10001,\"name\":\"张三\"}", objectJson); // test Json -> Object SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); testMap(); } // 测试:Fastjson2 @Test public void testFastjson2() { SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2()); Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForFastjson2.class); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); Assertions.assertEquals("{\"age\":18,\"id\":10001,\"name\":\"张三\"}", objectJson); // test Json -> Object SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); testMap(); } // 测试:Snack3 @Test public void testSnack3() { SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack3()); Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForSnack3.class); // test Object -> Json SysUser user = new SysUser(10001, "张三", 18); String objectJson = SaManager.getSaJsonTemplate().objectToJson(user); Assertions.assertEquals("{\"id\":10001,\"name\":\"张三\",\"age\":18}", objectJson); // test Json -> Object SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); testMap(); } // 测试 Map 的转换 private void testMap() { // test Map -> Json Map map = new HashMap<>(); map.put("id", 10001); map.put("name", "张三"); map.put("age", 18); String mapJson = SaManager.getSaJsonTemplate().objectToJson(map); Assertions.assertEquals("{\"name\":\"张三\",\"id\":10001,\"age\":18}", mapJson); // test Json -> Map Map map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson); Assertions.assertEquals(map2.toString(), map.toString()); } // 测试 Null 值 private void testNull() { Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null)); Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null)); } } ================================================ FILE: sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysRole.java ================================================ package com.pj.test.model; /** * Role 实体类 * * @author click33 * @since 2022-10-15 */ public class SysRole { // // public SysRole() { // } // // public SysRole(long id, String name) { // super(); // this.id = id; // this.name = name; // } // // // /** // * 角色id // */ // private long id; // // /** // * 角色名称 // */ // private String name; // // /** // * @return id // */ // public long getId() { // return id; // } // // /** // * @param id 要设置的 id // */ // public void setId(long id) { // this.id = id; // } // // /** // * @return name // */ // public String getName() { // return name; // } // // /** // * @param name 要设置的 name // */ // public void setName(String name) { // this.name = name; // } // // @Override // public String toString() { // return "SysRole [id=" + id + ", name=" + name + "]"; // } // } ================================================ FILE: sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysUser.java ================================================ package com.pj.test.model; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser { public SysUser() { } public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * 用户角色 */ private SysRole role; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } public SysRole getRole() { return role; } public SysUser setRole(SysRole role) { this.role = role; return this; } @Override public String toString() { return "SysUser{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", role=" + role + '}'; } } ================================================ FILE: sa-token-test/sa-token-jwt-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-jwt-test sa-token-jwt-test sa-token-jwt-test cn.dev33 sa-token-spring-boot-starter cn.dev33 sa-token-jwt ================================================ FILE: sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForMixinTest.java ================================================ package com.pj.test; import java.util.List; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.jwt.SaJwtUtil; import cn.dev33.satoken.jwt.StpLogicJwtForMixin; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.SaLoginConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaTokenConsts; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; /** * Sa-Token 整合 jwt:mixin 模式 测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) public class JwtForMixinTest { // 持久化Bean @Autowired(required = false) SaTokenDao dao = SaManager.getSaTokenDao(); // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ JwtForMixinTest star ..."); StpUtil.setStpLogic(new StpLogicJwtForMixin()); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ JwtForMixinTest end ... \n"); } @BeforeEach public void beforeEach() { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); } @AfterEach public void afterEach() { SaTokenContextServletUtil.clearContext(); } // 测试:登录 @Test public void doLogin() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); // API 验证 Assertions.assertTrue(StpUtil.isLogin()); Assertions.assertNotNull(token); // token不为null Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 // token 验证 JWT jwt = JWT.of(token); JSONObject payloads = jwt.getPayloads(); Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_ID), "10001"); // 账号 Assertions.assertEquals(payloads.getStr(SaJwtUtil.DEVICE_TYPE), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_TYPE), StpUtil.TYPE); // 账号类型 // db数据 验证 // token不存在 Assertions.assertNull(dao.get("satoken:login:token:" + token)); // Session 存在 SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNotNull(session); Assertions.assertEquals(session.getId(), "satoken:login:session:" + 10001); Assertions.assertTrue(session.getTerminalList().size() >= 1); } // 测试:注销 @Test public void logout() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); Assertions.assertEquals(JWT.of(token).getPayloads().getStr("loginId"), "10001"); // 注销 StpUtil.logout(); // token 应该被清除 Assertions.assertNull(StpUtil.getTokenValue()); Assertions.assertFalse(StpUtil.isLogin()); } // 测试:Session会话 @Test public void testSession() { StpUtil.login(10001); // API 应该可以获取 Session Assertions.assertNotNull(StpUtil.getSession(false)); // db中应该存在 Session SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNotNull(session); // 存取值 session.set("name", "zhang"); session.set("age", "18"); Assertions.assertEquals(session.get("name"), "zhang"); Assertions.assertEquals(session.getInt("age"), 18); Assertions.assertEquals((int)session.getModel("age", int.class), 18); Assertions.assertEquals((int)session.get("age", 20), 18); Assertions.assertEquals((int)session.get("name2", 20), 20); Assertions.assertEquals((int)session.get("name2", () -> 30), 30); session.clear(); Assertions.assertEquals(session.get("name"), null); } // 测试:权限认证 @Test public void testCheckPermission() { StpUtil.login(10001); // 权限认证 Assertions.assertTrue(StpUtil.hasPermission("user-add")); Assertions.assertTrue(StpUtil.hasPermission("user-list")); Assertions.assertTrue(StpUtil.hasPermission("user")); Assertions.assertTrue(StpUtil.hasPermission("art-add")); Assertions.assertFalse(StpUtil.hasPermission("get-user")); // and Assertions.assertTrue(StpUtil.hasPermissionAnd("art-add", "art-get")); Assertions.assertFalse(StpUtil.hasPermissionAnd("art-add", "comment-add")); // or Assertions.assertTrue(StpUtil.hasPermissionOr("art-add", "comment-add")); Assertions.assertFalse(StpUtil.hasPermissionOr("comment-add", "comment-delete")); } // 测试:角色认证 @Test public void testCheckRole() { StpUtil.login(10001); // 角色认证 Assertions.assertTrue(StpUtil.hasRole("admin")); Assertions.assertFalse(StpUtil.hasRole("teacher")); // and Assertions.assertTrue(StpUtil.hasRoleAnd("admin", "super-admin")); Assertions.assertFalse(StpUtil.hasRoleAnd("admin", "ceo")); // or Assertions.assertTrue(StpUtil.hasRoleOr("admin", "ceo")); Assertions.assertFalse(StpUtil.hasRoleOr("ceo", "cto")); } // 测试:根据token强制注销 @Test public void testLogoutByToken() { Assertions.assertThrows(ApiDisabledException.class, () -> { // 先登录上 StpUtil.login(10001); Assertions.assertTrue(StpUtil.isLogin()); String token = StpUtil.getTokenValue(); // 根据token注销 StpUtil.logoutByTokenValue(token); }); } // 测试:根据账号id强制注销 @Test public void testLogoutByLoginId() { Assertions.assertThrows(ApiDisabledException.class, () -> { // 先登录上 StpUtil.login(10001); Assertions.assertTrue(StpUtil.isLogin()); // 根据账号id注销 StpUtil.logout(10001); }); } // 测试Token-Session @Test public void testTokenSession() { // 先登录上 StpUtil.login(10001); String token = StpUtil.getTokenValue(); // 刚开始不存在 Assertions.assertNull(StpUtil.stpLogic.getTokenSession(false)); SaSession session = dao.getSession("satoken:login:token-session:" + token); Assertions.assertNull(session); // 调用一次就存在了 StpUtil.getTokenSession(); Assertions.assertNotNull(StpUtil.stpLogic.getTokenSession(false)); SaSession session2 = dao.getSession("satoken:login:token-session:" + token); Assertions.assertNotNull(session2); } // 测试:账号封禁 @Test public void testDisable() { Assertions.assertThrows(DisableServiceException.class, () -> { // 封号 StpUtil.disable(10007, 200); Assertions.assertTrue(StpUtil.isDisable(10007)); Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); // 解封 StpUtil.untieDisable(10007); Assertions.assertFalse(StpUtil.isDisable(10007)); Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), null); // 封号后校验 (会抛出 DisableLoginException 异常) StpUtil.disable(10007, 200); StpUtil.checkDisable(10007); StpUtil.login(10007); }); } // 测试:身份切换 @Test public void testSwitch() { // 登录 StpUtil.login(10001); Assertions.assertFalse(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // 开始身份切换 StpUtil.switchTo(10044); Assertions.assertTrue(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10044); // 结束切换 StpUtil.endSwitch(); Assertions.assertFalse(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); } // 测试:会话管理 @Test public void testSearchTokenValue() { Assertions.assertThrows(ApiDisabledException.class, () -> { // 登录 StpUtil.login(10001); StpUtil.login(10002); StpUtil.login(10003); StpUtil.login(10004); StpUtil.login(10005); // 查询 List list = StpUtil.searchTokenValue("", 0, 10, true); Assertions.assertTrue(list.size() >= 5); }); } // 测试:getExtra @Test public void getExtra() { // 登录 StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan")); String tokenValue = StpUtil.getTokenValue(); // 可以取到 Assertions.assertEquals(StpUtil.getExtra("name"), "zhangsan"); Assertions.assertEquals(StpUtil.getExtra(tokenValue, "name"), "zhangsan"); // 取不到 Assertions.assertEquals(StpUtil.getExtra("name2"), null); } } ================================================ FILE: sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForSimpleTest.java ================================================ package com.pj.test; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import org.junit.jupiter.api.*; import org.springframework.boot.test.context.SpringBootTest; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.jwt.StpLogicJwtForSimple; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.SaLoginConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaTokenConsts; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; /** * Sa-Token 整合 jwt:Simple 模式 测试 * * @author click33 * */ //@RunWith(SpringRunner.class) @SpringBootTest(classes = StartUpApplication.class) public class JwtForSimpleTest { // 持久化Bean static SaTokenDao dao; // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ JwtForSimpleTest star ..."); dao = SaManager.getSaTokenDao(); StpUtil.setStpLogic(new StpLogicJwtForSimple()); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ JwtForSimpleTest end ... \n"); } @BeforeEach public void beforeEach() { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); } @AfterEach public void afterEach() { SaTokenContextServletUtil.clearContext(); } // 测试:登录 @Test public void doLogin() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); // API 验证 Assertions.assertTrue(StpUtil.isLogin()); Assertions.assertNotNull(token); // token不为null Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 // token 验证 JWT jwt = JWT.of(token); JSONObject payloads = jwt.getPayloads(); Assertions.assertEquals(payloads.getStr("loginId"), "10001"); // db数据 验证 // token存在 Assertions.assertEquals(dao.get("satoken:login:token:" + token), "10001"); // Session 存在 SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNotNull(session); Assertions.assertEquals(session.getId(), "satoken:login:session:" + 10001); Assertions.assertTrue(session.getTerminalList().size() >= 1); } // 测试:getExtra @Test public void getExtra() { // 登录 StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan")); String tokenValue = StpUtil.getTokenValue(); // 可以取到 Assertions.assertEquals(StpUtil.getExtra("name"), "zhangsan"); Assertions.assertEquals(StpUtil.getExtra(tokenValue, "name"), "zhangsan"); // 取不到 Assertions.assertEquals(StpUtil.getExtra("name2"), null); } } ================================================ FILE: sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForStatelessTest.java ================================================ package com.pj.test; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.jwt.SaJwtUtil; import cn.dev33.satoken.jwt.StpLogicJwtForStateless; import cn.dev33.satoken.stp.SaLoginConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaTokenConsts; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; /** * Sa-Token 整合 jwt:stateless 模式 测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) public class JwtForStatelessTest { // 持久化Bean @Autowired(required = false) SaTokenDao dao = SaManager.getSaTokenDao(); // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ JwtForStatelessTest star ..."); StpUtil.setStpLogic(new StpLogicJwtForStateless()); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ JwtForStatelessTest end ... \n"); } @BeforeEach public void beforeEach() { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); } @AfterEach public void afterEach() { SaTokenContextServletUtil.clearContext(); } // 测试:登录 @Test public void doLogin() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); // API 验证 Assertions.assertTrue(StpUtil.isLogin()); Assertions.assertNotNull(token); // token不为null Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 // token 验证 JWT jwt = JWT.of(token); JSONObject payloads = jwt.getPayloads(); Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_ID), "10001"); // 账号 Assertions.assertEquals(payloads.getStr(SaJwtUtil.DEVICE_TYPE), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 Assertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_TYPE), StpUtil.TYPE); // 账号类型 // 时间 Assertions.assertTrue(StpUtil.getTokenTimeout() <= SaManager.getConfig().getTimeout()); Assertions.assertTrue(StpUtil.getTokenTimeout() > SaManager.getConfig().getTimeout() - 10000); try { // 尝试获取Session会抛出异常 StpUtil.getSession(); Assertions.assertTrue(false); } catch (Exception e) { } } // 测试:注销 @Test public void logout() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); Assertions.assertEquals(JWT.of(token).getPayloads().getStr("loginId"), "10001"); // 注销 StpUtil.logout(); // token 应该被清除 Assertions.assertNull(StpUtil.getTokenValue()); Assertions.assertFalse(StpUtil.isLogin()); } // 测试:Session会话 @Test public void testSession() { Assertions.assertThrows(ApiDisabledException.class, () -> { StpUtil.login(10001); // 会抛异常 StpUtil.getSession(); }); } // 测试:权限认证 @Test public void testCheckPermission() { StpUtil.login(10001); // 权限认证 Assertions.assertTrue(StpUtil.hasPermission("user-add")); Assertions.assertTrue(StpUtil.hasPermission("user-list")); Assertions.assertTrue(StpUtil.hasPermission("user")); Assertions.assertTrue(StpUtil.hasPermission("art-add")); Assertions.assertFalse(StpUtil.hasPermission("get-user")); // and Assertions.assertTrue(StpUtil.hasPermissionAnd("art-add", "art-get")); Assertions.assertFalse(StpUtil.hasPermissionAnd("art-add", "comment-add")); // or Assertions.assertTrue(StpUtil.hasPermissionOr("art-add", "comment-add")); Assertions.assertFalse(StpUtil.hasPermissionOr("comment-add", "comment-delete")); } // 测试:角色认证 @Test public void testCheckRole() { StpUtil.login(10001); // 角色认证 Assertions.assertTrue(StpUtil.hasRole("admin")); Assertions.assertFalse(StpUtil.hasRole("teacher")); // and Assertions.assertTrue(StpUtil.hasRoleAnd("admin", "super-admin")); Assertions.assertFalse(StpUtil.hasRoleAnd("admin", "ceo")); // or Assertions.assertTrue(StpUtil.hasRoleOr("admin", "ceo")); Assertions.assertFalse(StpUtil.hasRoleOr("ceo", "cto")); } // 测试:根据token强制注销 @Test public void testLogoutByToken() { Assertions.assertThrows(ApiDisabledException.class, () -> { // 先登录上 StpUtil.login(10001); Assertions.assertTrue(StpUtil.isLogin()); String token = StpUtil.getTokenValue(); // 根据token注销 StpUtil.logoutByTokenValue(token); }); } // 测试:根据账号id强制注销 @Test public void testLogoutByLoginId() { Assertions.assertThrows(ApiDisabledException.class, () -> { // 先登录上 StpUtil.login(10001); Assertions.assertTrue(StpUtil.isLogin()); // 根据账号id注销 StpUtil.logout(10001); }); } // 测试:getExtra @Test public void getExtra() { // 登录 StpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan")); String tokenValue = StpUtil.getTokenValue(); // 可以取到 Assertions.assertEquals(StpUtil.getExtra("name"), "zhangsan"); Assertions.assertEquals(StpUtil.getExtra(tokenValue, "name"), "zhangsan"); // 取不到 Assertions.assertEquals(StpUtil.getExtra("name2"), null); } } ================================================ FILE: sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/StartUpApplication.java ================================================ package com.pj.test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动类 * @author Auster * */ @SpringBootApplication public class StartUpApplication { public static void main(String[] args) { SpringApplication.run(StartUpApplication.class, args); } } ================================================ FILE: sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/satoken/StpInterfaceImpl.java ================================================ package com.pj.test.satoken; import java.util.Arrays; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; /** * 自定义权限验证接口扩展 * * @author Auster * */ @Component public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { return Arrays.asList("user*", "art-add", "art-delete", "art-update", "art-get"); } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { return Arrays.asList("admin", "super-admin"); } } ================================================ FILE: sa-token-test/sa-token-jwt-test/src/test/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ================================================ FILE: sa-token-test/sa-token-serializer-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-serializer-test sa-token-serializer-test sa-token-serializer-test cn.dev33 sa-token-serializer-features ================================================ FILE: sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/SaSerializerTemplateTest.java ================================================ package com.pj.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.serializer.SaSerializerForBase64UseEmoji; import cn.dev33.satoken.serializer.SaSerializerForBase64UsePeriodicTable; import cn.dev33.satoken.serializer.SaSerializerForBase64UseSpecialSymbols; import cn.dev33.satoken.serializer.SaSerializerForBase64UseTianGan; import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseBase64; import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseHex; import cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseISO_8859_1; import com.pj.test.model.SysUser; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; /** * Sa-Token Serializer 序列化模块测试 * * @author click33 * */ public class SaSerializerTemplateTest { // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ SaSerializerTemplateTest star ..."); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ SaSerializerTemplateTest end ... \n"); } // 测试:SaSerializerTemplateForJdkUseBase64 @Test public void testSaSerializerTemplateForJdkUseBase64() { SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseBase64()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseBase64.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); Assertions.assertEquals("rO0ABXNyABljb20ucGoudGVzdC5tb2RlbC5TeXNVc2Vy0MeZoPBtVUwCAARJAANhZ2VKAAJpZEwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztMAARyb2xldAAbTGNvbS9wai90ZXN0L21vZGVsL1N5c1JvbGU7eHAAAAASAAAAAAAAJxF0AAblvKDkuIlw", objectString); // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试:SaSerializerTemplateForJdkUseHex @Test public void testSaSerializerTemplateForJdkUseHex() { SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseHex()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseHex.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); Assertions.assertEquals("ACED000573720019636F6D2E706A2E746573742E6D6F64656C2E53797355736572D0C799A0F06D554C0200044900036167654A000269644C00046E616D657400124C6A6176612F6C616E672F537472696E673B4C0004726F6C6574001B4C636F6D2F706A2F746573742F6D6F64656C2F537973526F6C653B7870000000120000000000002711740006E5BCA0E4B88970", objectString); // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试:SaSerializerTemplateForJdkUseISO_8859_1 @Test public void testSaSerializerTemplateForJdkUseISO_8859_1() { SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseISO_8859_1()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseISO_8859_1.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); // Assertions.assertEquals("xxxx", objectString); // 太过奇形怪状,无法直接断言 // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试:SaSerializerForBase64UseTianGan @Test public void testSaSerializerForBase64UseTianGan() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseTianGan.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); Assertions.assertEquals("雷辰中甲乙坤卯西甲乙日天离谷中雾艮庚石雾兑庚亥北兑丙宙霜离谷未日离丙宙酉金坤卯亥艮谷亥西中寅金巽石巳乙霜亥戌东丙甲甲未癸甲甲卯火巽谷亥子甲甲癸田巽戊东甲乙庚宙火离乾亥中甲乙癸寅坎月己谷震申安电震乾宙山丑信卯中艮月日雾巽北霜寅甲甲未西离谷南日兑甲甲离酉庚卯露离申安东坎土安中巽坤卯中丑谷信露巽庚亥电丑信卯宙艮信癸露离庚戌泰金辛甲甲甲甲甲申甲甲甲甲甲甲甲甲癸南己中甲甲离日露子丁地雾壬日东", objectString); // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试:SaSerializerForBase64UsePeriodicTable @Test public void testSaSerializerForBase64UsePeriodicTable() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UsePeriodicTable.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); Assertions.assertEquals("钌磷碘氢氦铬硅锑氢氦锶氪镍铯碘银铜氮铌银锌氮钛碲锌锂铈钯镍铯氩锶镍锂铈钙镓铬硅钛铜铯钛锑碘铝镓铁铌硫氦钯钛钪铟锂氢氢氩氖氢氢硅硒铁铯钛钠氢氢氖钼铁硼铟氢氦氮铈硒镍钒钛碘氢氦氖铝钴钇碳铯锰钾钐铑锰钒铈锆镁氙硅碘铜钇锶银铁碲钯铝氢氢氩锑镍铯锡锶锌氢氢镍钙氮硅镉镍钾钐铟钴溴钐碘铁铬硅碘镁铯氙镉铁氮钛铑镁氙硅铈铜氙氖镉镍氮钪钕镓氧氢氢氢氢氢钾氢氢氢氢氢氢氢氢氖锡碳碘氢氢镍锶镉钠铍铷银氟锶铟", objectString); // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试:SaSerializerForBase64UseSpecialSymbols @Test public void testSaSerializerForBase64UseSpecialSymbols() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseSpecialSymbols.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); Assertions.assertEquals("→▃☶▲▼▌▂☳▲▼§♫▬☰☶↘〓▶↑↘◤▶▎☱◤●☀↓▬☰▆§▬●☀█◥▌▂▎〓☰▎☳☶▁◥▊↑▄▼↓▎▏☲●▲▲▆♥▲▲▂♩▊☰▎♦▲▲♥↗▊■☲▲▼▶☀♩▬▍▎☶▲▼♥▁▉〼★☰▋▇‥↙▋▍☀↖♣☵▂☶〓〼§↘▊☱↓▁▲▲▆☳▬☰☷§◤▲▲▬█▶▂☴▬▇‥☲▉♪‥☶▊▌▂☶♣☰☵☴▊▶▎↙♣☵▂☀〓☵♥☴▬▶▏▪◥◀▲▲▲▲▲▇▲▲▲▲▲▲▲▲♥☷★☶▲▲▬§☴♦◆♬↘♠§☲", objectString); // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试:SaSerializerForBase64UseEmoji @Test public void testSaSerializerForBase64UseEmoji() { SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji()); Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseEmoji.class); // test Object -> String SysUser user = new SysUser(10001, "张三", 18); String objectString = SaManager.getSaSerializerTemplate().objectToString(user); Assertions.assertEquals("😫😎😴😀😁😗😍😲😀😁😥😣😛😶😴😮😜😆😨😮😝😆😕😳😝😂😹😭😛😶😑😥😛😂😹😓😞😗😍😕😜😶😕😲😴😌😞😙😨😏😁😭😕😔😰😂😀😀😑😉😀😀😍😡😙😶😕😊😀😀😉😩😙😄😰😀😁😆😹😡😛😖😕😴😀😁😉😌😚😦😅😶😘😒😽😬😘😖😹😧😋😵😍😴😜😦😥😮😙😳😭😌😀😀😑😲😛😶😱😥😝😀😀😛😓😆😍😯😛😒😽😰😚😢😽😴😙😗😍😴😋😶😵😯😙😆😕😬😋😵😍😹😜😵😉😯😛😆😔😻😞😇😀😀😀😀😀😒😀😀😀😀😀😀😀😀😉😱😅😴😀😀😛😥😯😊😃😤😮😈😥😰", objectString); // test String -> Object SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class); Assertions.assertEquals(user2.toString(), user.toString()); // more testNull(); } // 测试 Null 值 private void testNull() { Assertions.assertNull(SaManager.getSaSerializerTemplate().objectToString(null)); Assertions.assertNull(SaManager.getSaSerializerTemplate().stringToObject(null, SysUser.class)); Assertions.assertNull(SaManager.getSaSerializerTemplate().stringToObject(null)); } } ================================================ FILE: sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysRole.java ================================================ package com.pj.test.model; import java.io.Serializable; /** * Role 实体类 * * @author click33 * @since 2022-10-15 */ public class SysRole implements Serializable { // // public SysRole() { // } // // public SysRole(long id, String name) { // super(); // this.id = id; // this.name = name; // } // // // /** // * 角色id // */ // private long id; // // /** // * 角色名称 // */ // private String name; // // /** // * @return id // */ // public long getId() { // return id; // } // // /** // * @param id 要设置的 id // */ // public void setId(long id) { // this.id = id; // } // // /** // * @return name // */ // public String getName() { // return name; // } // // /** // * @param name 要设置的 name // */ // public void setName(String name) { // this.name = name; // } // // @Override // public String toString() { // return "SysRole [id=" + id + ", name=" + name + "]"; // } // } ================================================ FILE: sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysUser.java ================================================ package com.pj.test.model; import java.io.Serializable; /** * User 实体类 * * @author click33 * @since 2022-10-15 */ public class SysUser implements Serializable { public SysUser() { } public SysUser(long id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } /** * 用户id */ private long id; /** * 用户名称 */ private String name; /** * 用户年龄 */ private int age; /** * 用户角色 */ private SysRole role; /** * @return id */ public long getId() { return id; } /** * @param id 要设置的 id */ public void setId(long id) { this.id = id; } /** * @return name */ public String getName() { return name; } /** * @param name 要设置的 name */ public void setName(String name) { this.name = name; } /** * @return age */ public int getAge() { return age; } /** * @param age 要设置的 age */ public void setAge(int age) { this.age = age; } public SysRole getRole() { return role; } public SysUser setRole(SysRole role) { this.role = role; return this; } @Override public String toString() { return "SysUser{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", role=" + role + '}'; } } ================================================ FILE: sa-token-test/sa-token-springboot-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-springboot-test sa-token-springboot-test sa-token-springboot-test cn.dev33 sa-token-spring-boot-starter cn.dev33 sa-token-sso test cn.dev33 sa-token-oauth2 test cn.dev33 sa-token-sign test cn.dev33 sa-token-servlet cn.dev33 sa-token-core ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/application/SaApplicationTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.application; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.application.SaApplication; import cn.dev33.satoken.context.SaHolder; /** * SaApplication 存取值测试 * * @author click33 * @since 2022-9-4 */ public class SaApplicationTest { // 测试 @Test public void testSaApplication() { SaApplication application = SaHolder.getApplication(); // 取值 application.set("age", "18"); Assertions.assertEquals(application.get("age").toString(), "18"); Assertions.assertEquals(application.getInt("age"), 18); Assertions.assertEquals(application.getLong("age"), 18L); Assertions.assertEquals(application.getFloat("age"), 18f); Assertions.assertEquals(application.getDouble("age"), 18.0); Assertions.assertEquals(application.getString("age"), "18"); Assertions.assertEquals(application.get("age", 20), 18); Assertions.assertEquals(application.get("age2", 20), 20); Assertions.assertEquals(application.getString("age2"), null); // lambda 取值,有值时依然是原值 Assertions.assertEquals(application.get("age", () -> "23"), "18"); Assertions.assertEquals(application.getInt("age"), 18); // lambda 取值,无值时被写入新值 Assertions.assertEquals(application.get("age2", () -> "23"), "23"); Assertions.assertEquals(application.getInt("age2"), 23); // getModel取值 Assertions.assertEquals(application.getModel("age", int.class), 18); Assertions.assertEquals(application.getModel("age", int.class, 30), 18); Assertions.assertEquals(application.getModel("age3", int.class, 30), 30); // 删除值 application.delete("age"); Assertions.assertNull(application.get("age")); // 是否为空 Assertions.assertTrue(application.valueIsNull(null)); Assertions.assertTrue(application.valueIsNull("")); Assertions.assertFalse(application.valueIsNull("abc")); // 为空时才能写入 application.setByNull("age4", "18"); Assertions.assertEquals(application.getInt("age4"), 18); application.setByNull("age4", "20"); Assertions.assertEquals(application.getInt("age4"), 18); // 清空 application.clear(); Assertions.assertEquals(application.keys().size(), 0); // 获取所有值 application.set("key1", "value1"); application.set("key2", "value2"); application.set("key3", "value3"); Assertions.assertEquals(application.keys().size(), 3); // 空列表 application.clear(); Assertions.assertEquals(application.keys().size(), 0); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/config/SaTokenConfigTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.config; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.config.SaCookieConfig; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.config.SaTokenConfigFactory; /** * 配置类测试 * * @author click33 * @since 2022-9-4 */ public class SaTokenConfigTest { // 基本 get set 测试 @Test public void testProp() { SaTokenConfig config = new SaTokenConfig(); config.setTokenName("nav-token"); Assertions.assertEquals(config.getTokenName(), "nav-token"); config.setTimeout(100204); Assertions.assertEquals(config.getTimeout(), 100204); config.setActiveTimeout(1804); Assertions.assertEquals(config.getActiveTimeout(), 1804); config.setIsConcurrent(false); Assertions.assertEquals(config.getIsConcurrent(), false); config.setIsShare(false); Assertions.assertEquals(config.getIsShare(), false); config.setMaxLoginCount(11); Assertions.assertEquals(config.getMaxLoginCount(), 11); config.setIsReadBody(false); Assertions.assertEquals(config.getIsReadBody(), false); config.setIsReadHeader(false); Assertions.assertEquals(config.getIsReadHeader(), false); config.setIsReadCookie(false); Assertions.assertEquals(config.getIsReadCookie(), false); config.setTokenStyle("tik"); Assertions.assertEquals(config.getTokenStyle(), "tik"); config.setDataRefreshPeriod(111); Assertions.assertEquals(config.getDataRefreshPeriod(), 111); config.setTokenSessionCheckLogin(false); Assertions.assertEquals(config.getTokenSessionCheckLogin(), false); config.setAutoRenew(false); Assertions.assertEquals(config.getAutoRenew(), false); config.setTokenPrefix("token"); Assertions.assertEquals(config.getTokenPrefix(), "token"); config.setIsPrint(false); Assertions.assertEquals(config.getIsPrint(), false); config.setIsLog(false); Assertions.assertEquals(config.getIsLog(), false); config.setJwtSecretKey("NgdfaXasARggr"); Assertions.assertEquals(config.getJwtSecretKey(), "NgdfaXasARggr"); config.setSameTokenTimeout(1004); Assertions.assertEquals(config.getSameTokenTimeout(), 1004); config.setHttpBasic("sa:123456"); Assertions.assertEquals(config.getHttpBasic(), "sa:123456"); config.setCurrDomain("http://127.0.0.1:8084"); Assertions.assertEquals(config.getCurrDomain(), "http://127.0.0.1:8084"); config.setCheckSameToken(false); Assertions.assertEquals(config.getCheckSameToken(), false); SaCookieConfig scc = new SaCookieConfig(); config.setCookie(scc); Assertions.assertEquals(config.getCookie(), scc); config.toString(); } // 从文件读取 @Test public void testSaTokenConfigFactory() { SaTokenConfig config = SaTokenConfigFactory.createConfig("sa-token2.properties"); Assertions.assertEquals(config.getTokenName(), "use-token"); Assertions.assertEquals(config.getTimeout(), 9000); Assertions.assertEquals(config.getActiveTimeout(), 240); Assertions.assertEquals(config.getIsConcurrent(), false); Assertions.assertEquals(config.getIsShare(), false); Assertions.assertEquals(config.getIsLog(), true); } // 测试 SaCookieConfig @Test public void testSaCookieConfig() { SaCookieConfig config = new SaCookieConfig(); config.setDomain("stp.cn"); Assertions.assertEquals(config.getDomain(), "stp.cn"); config.setPath("/pro/"); Assertions.assertEquals(config.getPath(), "/pro/"); config.setSecure(true); Assertions.assertEquals(config.getSecure(), true); config.setHttpOnly(false); Assertions.assertEquals(config.getHttpOnly(), false); config.setSameSite("lax"); Assertions.assertEquals(config.getSameSite(), "lax"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaCookieTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.context.model; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.context.model.SaCookie; /** * SaFoxUtil 工具类测试 * * @author click33 * @since 2022-2-8 22:14:25 */ public class SaCookieTest { @Test public void test() { SaCookie cookie = new SaCookie("satoken", "xxxx-xxxx-xxxx-xxxx") .setDomain("https://sa-token.cc/") .setMaxAge(-1) .setPath("/") .setSameSite("Lax") .setHttpOnly(true) .setSecure(true); Assertions.assertEquals(cookie.getName(), "satoken"); Assertions.assertEquals(cookie.getValue(), "xxxx-xxxx-xxxx-xxxx"); Assertions.assertEquals(cookie.getDomain(), "https://sa-token.cc/"); Assertions.assertEquals(cookie.getMaxAge(), -1); Assertions.assertEquals(cookie.getPath(), "/"); Assertions.assertEquals(cookie.getSameSite(), "Lax"); Assertions.assertEquals(cookie.getHttpOnly(), true); Assertions.assertEquals(cookie.getSecure(), true); Assertions.assertEquals(cookie.toHeaderValue(), "satoken=xxxx-xxxx-xxxx-xxxx; Domain=https://sa-token.cc/; Path=/; Secure; HttpOnly; SameSite=Lax"); Assertions.assertNotNull(cookie.toString()); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaTokenContextDefaultImplTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.context.model; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.context.SaTokenContextDefaultImpl; import cn.dev33.satoken.exception.SaTokenException; /** * 默认上下文测试 * * @author click33 * @since 2022-9-5 */ public class SaTokenContextDefaultImplTest { @Test public void testSaTokenContextDefaultImpl() { SaTokenContextDefaultImpl context = new SaTokenContextDefaultImpl(); Assertions.assertThrows(SaTokenException.class, () -> context.getStorage()); Assertions.assertThrows(SaTokenException.class, () -> context.getRequest()); Assertions.assertThrows(SaTokenException.class, () -> context.getResponse()); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/dao/SaTokenDaoTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.dao; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; import cn.dev33.satoken.session.SaSession; /** * SaTokenDao 持久层 测试 * * @author click33 * @since 2022-2-9 15:39:38 */ public class SaTokenDaoTest { SaTokenDao dao = new SaTokenDaoDefaultImpl(); // 字符串存取 @Test public void get() { dao.set("name", "zhangsan", 60); Assertions.assertEquals(dao.get("name"), "zhangsan"); Assertions.assertTrue(dao.getTimeout("name") <= 60); Assertions.assertEquals(dao.getTimeout("name2"), -2); dao.update("name", "lisi"); Assertions.assertEquals(dao.get("name"), "lisi"); dao.updateTimeout("name", 100); Assertions.assertTrue(dao.getTimeout("name") <= 100); dao.delete("name"); Assertions.assertEquals(dao.get("name"), null); } // 对象存取 @Test public void getObject() { dao.setObject("name", "zhangsan", 60); Assertions.assertEquals(dao.getObject("name"), "zhangsan"); Assertions.assertTrue(dao.getObjectTimeout("name") <= 60); dao.updateObject("name", "lisi"); Assertions.assertEquals(dao.getObject("name"), "lisi"); dao.updateObjectTimeout("name", 100); Assertions.assertTrue(dao.getObjectTimeout("name") <= 100); dao.deleteObject("name"); Assertions.assertEquals(dao.getObject("name"), null); } // SaSession 存取 @Test public void getSession() { SaSession session = new SaSession("session-1001"); dao.setSession(session, 60); Assertions.assertEquals(dao.getSession("session-1001").getId(), session.getId()); Assertions.assertTrue(dao.getSessionTimeout("session-1001") <= 60); SaSession session2 = new SaSession("session-1001"); dao.updateSession(session2); Assertions.assertEquals(dao.getSession("session-1001").getId(), session2.getId()); dao.updateSessionTimeout("session-1001", 100); Assertions.assertTrue(dao.getSessionTimeout("session-1001") <= 100); dao.deleteSession("session-1001"); Assertions.assertEquals(dao.getSession("session-1001"), null); } // 测试永久有效期的写值改值 @Test public void testUpdate() { // ----------- 字符串 相关 // 永久有效 dao.set("age", "20", -1); Assertions.assertEquals(dao.get("age"), "20"); Assertions.assertEquals(dao.getTimeout("age"), SaTokenDao.NEVER_EXPIRE); // 修改值 dao.update("age", "22"); Assertions.assertEquals(dao.get("age"), "22"); // 有效期应该不变,还是永久 Assertions.assertEquals(dao.getTimeout("age"), SaTokenDao.NEVER_EXPIRE); // ----------- Session 相关 // 永久有效 SaSession session = new SaSession("session-1001"); dao.setSession(session, -1); Assertions.assertEquals(dao.getSession("session-1001").getId(), session.getId()); Assertions.assertEquals(dao.getSessionTimeout("session-1001"), SaTokenDao.NEVER_EXPIRE); // 修改值 dao.updateSession(session); Assertions.assertEquals(dao.getSession("session-1001").getId(), session.getId()); // 有效期应该不变,还是永久 Assertions.assertEquals(dao.getSessionTimeout("session-1001"), SaTokenDao.NEVER_EXPIRE); // ----------- 无效update dao.update("mid", "zhang"); Assertions.assertNull(dao.get("mid")); } // timeout为0或者小于等于-2时,不写入 @Test public void test0Timeout() { // ----------- 字符串 相关 // 字符串 0 和 <-2 dao.set("avatar", "1.jpg", 0); Assertions.assertNull(dao.get("avatar")); dao.set("avatar", "1.jpg", -9); Assertions.assertNull(dao.get("avatar")); // ----------- Session 相关 // Session 0 和 <-2 SaSession session = new SaSession("session-1001"); dao.setSession(session, 0); Assertions.assertNull(dao.getSession("session-1001")); dao.setSession(session, -9); Assertions.assertNull(dao.getSession("session-1001")); } // TO-DO 和时间相关的测试 } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/fun/IsRunFunctionTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.fun; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.fun.IsRunFunction; /** * IsRunFunction 测试 * * @author click33 * @since 2022-2-9 16:11:10 */ public class IsRunFunctionTest { @Test public void test() { class TempClass{ int count = 1; } TempClass obj = new TempClass(); IsRunFunction fun = new IsRunFunction(true); fun.exe(()->{ obj.count = 2; }).noExe(()->{ obj.count = 3; }); Assertions.assertEquals(obj.count, 2); } @Test public void test2() { class TempClass{ int count = 1; } TempClass obj = new TempClass(); IsRunFunction fun = new IsRunFunction(false); fun.exe(()->{ obj.count = 2; }).noExe(()->{ obj.count = 3; }); Assertions.assertEquals(obj.count, 3); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/json/SaJsonTemplateDefaultImplTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.json; import cn.dev33.satoken.exception.NotImplException; import cn.dev33.satoken.json.SaJsonTemplateDefaultImpl; import cn.dev33.satoken.util.SoMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** * json默认实现类测试 * * @author click33 * @since 2022-9-1 */ public class SaJsonTemplateDefaultImplTest { @Test public void testSaJsonTemplateDefaultImpl() { SaJsonTemplateDefaultImpl saJsonTemplate = new SaJsonTemplateDefaultImpl(); // 组件未实现 Assertions.assertThrows(NotImplException.class, () -> { saJsonTemplate.jsonToMap("{}"); }); // 组件未实现 Assertions.assertThrows(NotImplException.class, () -> { saJsonTemplate.objectToJson(SoMap.getSoMap("name", "zhangsan")); }); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/package-info.java ================================================ /** * 核心包测试 */ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core; ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/BCryptTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.secure; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.secure.BCrypt; /** * BCrypt 加密测试 * * @author dream. * @since 2022/1/20 */ public class BCryptTest { @Test public void testCheckpw() { final String hashed = BCrypt.hashpw("12345"); // System.out.println(hashed); Assertions.assertTrue(BCrypt.checkpw("12345", hashed)); Assertions.assertFalse(BCrypt.checkpw("123456", hashed)); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaBase64UtilTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.secure; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.secure.SaBase64Util; /** * SaBase64Util 测试 * * @author click33 * @since 2022-2-9 */ public class SaBase64UtilTest { @Test public void test() { // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用Base64编码 String base64Text = SaBase64Util.encode(text); Assertions.assertEquals(base64Text, "U2EtVG9rZW4g5LiA5Liq6L276YeP57qnamF2Yeadg+mZkOiupOivgeahhuaetg=="); // 使用Base64解码 String text2 = SaBase64Util.decode(base64Text); Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaSecureUtilTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.secure; import java.util.HashMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.secure.SaSecureUtil; /** * SaSecureUtil 加密工具类 测试 * * @author click33 * @since 2022-2-9 */ public class SaSecureUtilTest { @Test public void test() { // md5加密 Assertions.assertEquals(SaSecureUtil.md5("123456"), "e10adc3949ba59abbe56e057f20f883e"); // sha1加密 Assertions.assertEquals(SaSecureUtil.sha1("123456"), "7c4a8d09ca3762af61e59520943dc26494f8941b"); // sha256加密 Assertions.assertEquals(SaSecureUtil.sha256("123456"), "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92"); // md5加盐加密: md5(md5(str) + md5(salt)) Assertions.assertEquals(SaSecureUtil.md5BySalt("123456", "salt"), "f52020dca765fd3943ed40a615dc2c5c"); } @Test public void aesEncrypt() { // 定义秘钥和明文 String key = "123456"; String text = "Sa-Token 一个轻量级java权限认证框架"; // 加密 String ciphertext = SaSecureUtil.aesEncrypt(key, text); Assertions.assertEquals(ciphertext, "KmSqfwxY5BRuWoHMWJqtebcOZ2lEEZaj2OSi1Ei8pRx4zdi24wsnwsTQVjbXRQ0M"); // 解密 String text2 = SaSecureUtil.aesDecrypt(key, ciphertext); Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); } @Test public void rsaEncryptByPublic() { // 定义私钥和公钥 String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg=="; String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB"; // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用公钥加密 String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text); // Assert.assertEquals(ciphertext, "d9e01fd105b059e975c524a1f4dccbe10dfc3a23b931a9e168ecb0a5758a29c45532254679f86cf83a63e5cc21ef631802fe70ea47e7519f5d96e0d1fab38a6f6dbebdb34b106ce7f27c341838e4e88a8ff3298c519c29a3f0944cf8f668bfecd9394f16945d85d84c4d813d12ecadf34bfb21850c383977b5b2de848fa40995"); // 使用私钥解密 String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext); Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); } @Test public void rsaEncryptByPrivate() throws Exception { // 生成私钥和公钥 HashMap map = SaSecureUtil.rsaGenerateKeyPair(); String privateKey = map.get("private"); String publicKey = map.get("public"); // 文本 String text = "Sa-Token 一个轻量级java权限认证框架"; // 使用公钥加密 String ciphertext = SaSecureUtil.rsaEncryptByPrivate(privateKey, text); // 使用私钥解密 String text2 = SaSecureUtil.rsaDecryptByPublic(publicKey, ciphertext); Assertions.assertEquals(text2, "Sa-Token 一个轻量级java权限认证框架"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionCustomUtilTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.session; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaSessionCustomUtil; /** * SaSession 测试 * * @author click33 * @since 2022-2-9 */ public class SaSessionCustomUtilTest { // 测试自定义Session @Test public void testCustomSession() { SaTokenDao dao = SaManager.getSaTokenDao(); // 刚开始不存在 Assertions.assertFalse(SaSessionCustomUtil.isExists("art-1")); SaSession session = dao.getSession("satoken:custom:session:" + "art-1"); Assertions.assertNull(session); // 调用一下 SaSessionCustomUtil.getSessionById("art-1"); SaSessionCustomUtil.getSessionById("art-1", false); // 就存在了 Assertions.assertTrue(SaSessionCustomUtil.isExists("art-1")); SaSession session2 = dao.getSession("satoken:custom:session:" + "art-1"); Assertions.assertNotNull(session2); // 给删除掉 SaSessionCustomUtil.deleteSessionById("art-1"); // 就又不存在了 Assertions.assertFalse(SaSessionCustomUtil.isExists("art-1")); SaSession session3 = dao.getSession("satoken:custom:session:" + "art-1"); Assertions.assertNull(session3); // 调用了也不会存在 SaSessionCustomUtil.getSessionById("art-4", false); SaSession session4 = dao.getSession("satoken:custom:session:" + "art-2"); Assertions.assertNull(session4); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.session; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.session.SaTerminalInfo; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * SaSession 测试 * * @author click33 * @since 2022-2-9 */ public class SaSessionTest { // 基础属性 @Test public void testProp() { SaSession session = new SaSession("session-1001"); Assertions.assertEquals(session.getId(), "session-1001"); // 属性读取 session = new SaSession(); session.setId("session-1009"); Assertions.assertEquals(session.getId(), "session-1009"); session.setCreateTime(1662241013902L); Assertions.assertEquals(session.getCreateTime(), 1662241013902L); } // 基础存取值 @Test public void testSetGet() { // 基础取值 SaSession session = new SaSession("session-1002"); session.set("name", "zhangsan"); session.set("age", 18); Assertions.assertEquals(session.get("name"), "zhangsan"); Assertions.assertEquals((int)session.get("age", 20), 18); Assertions.assertEquals((int)session.get("age2", 20), 20); Assertions.assertEquals(session.getModel("age", Double.class).getClass(), Double.class); // 原本无值时才会写入 session.setByNull("name", "lisi"); Assertions.assertEquals(session.get("name"), "zhangsan"); session.setByNull("name2", "lisi"); Assertions.assertEquals(session.get("name2"), "lisi"); // 复杂取值 class User { String name; int age; User(String name, int age) { this.name = name; this.age = age; } } User user = new User("zhangsan", 18); session.set("user", user); User user2 = session.getModel("user", User.class); Assertions.assertNotNull(user2); Assertions.assertEquals(user2.name, "zhangsan"); Assertions.assertEquals(user2.age, 18); } // 测试有效期 @Test public void testSessionTimeout() { // 修改剩余有效期 SaSession session = new SaSession("session-1005"); SaManager.getSaTokenDao().setSession(session, 20000); session.updateMaxTimeout(100); Assertions.assertTrue(session.timeout() <= 100); System.out.println(session.timeout()); // 仍然是 <=100 session.updateMaxTimeout(1000); Assertions.assertTrue(session.timeout() <= 100); System.out.println(session.timeout()); // Min 修改 session.updateMinTimeout(-1); System.out.println(session.timeout()); Assertions.assertTrue(session.timeout() == -1); } // 测试token 签名 @Test public void testSaTerminalInfo() { SaSession session = new SaSession("session-1002"); // 添加 Token 签名 session.addTerminal(new SaTerminalInfo(1, "xxxx-xxxx-xxxx-xxxx-1", "PC", null)); session.addTerminal(new SaTerminalInfo(2, "xxxx-xxxx-xxxx-xxxx-2", "APP", null)); // 查询 Assertions.assertEquals(session.getTerminalList().size(), 2); Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-1").getDeviceType(), "PC"); Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-2").getDeviceType(), "APP"); // 删除一个 session.removeTerminal("xxxx-xxxx-xxxx-xxxx-1"); Assertions.assertEquals(session.getTerminalList().size(), 1); // 删除一个不存在的,则不影响 SaTerminalInfo 列表 session.removeTerminal("xxxx-xxxx-xxxx-xxxx-999"); Assertions.assertEquals(session.getTerminalList().size(), 1); // 重置整个签名列表 List list = Arrays.asList( new SaTerminalInfo(1, "xxxx-xxxx-xxxx-xxxx-1", "WEB", null), new SaTerminalInfo(2, "xxxx-xxxx-xxxx-xxxx-2", "phone", null), new SaTerminalInfo(3, "xxxx-xxxx-xxxx-xxxx-3", "ipad", null) ); session.setTerminalList(list); Assertions.assertEquals(session.getTerminalList().size(), 3); Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-1").getDeviceType(), "WEB"); Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-2").getDeviceType(), "phone"); Assertions.assertEquals(session.getTerminal("xxxx-xxxx-xxxx-xxxx-3").getDeviceType(), "ipad"); } // 测试重置 DataMap @Test public void testDataMap() { SaSession session = new SaSession("session-1003"); session.set("key1", "value1"); session.set("key2", "value2"); session.set("key3", "value3"); // 所有数据 Assertions.assertEquals(session.keys().size(), 3); Assertions.assertEquals(session.getDataMap().size(), 3); // 重置所有数据 Map dataMap = new ConcurrentHashMap<>(); dataMap.put("aaa", "111"); dataMap.put("bbb", "222"); session.refreshDataMap(dataMap); Assertions.assertEquals(session.keys().size(), 2); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaTerminalInfoTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.session; import cn.dev33.satoken.session.SaTerminalInfo; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** * SaTerminalInfo 相关测试 * * @author click33 * @since 2022-9-4 */ public class SaTerminalInfoTest { // 测试 @Test public void testSaTerminalInfo() { SaTerminalInfo terminal = new SaTerminalInfo(); terminal.setDeviceType("PC"); terminal.setTokenValue("ttt-value"); Assertions.assertEquals(terminal.getDeviceType(), "PC"); Assertions.assertEquals(terminal.getTokenValue(), "ttt-value"); Assertions.assertNotNull(terminal.toString()); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/sign/SaSignTemplateTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.sign; import cn.dev33.satoken.sign.SaSignManager; import cn.dev33.satoken.sign.config.SaSignConfig; import cn.dev33.satoken.util.SoMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** * API 接口签名测试 * * @author click33 * @since 2022-9-2 */ public class SaSignTemplateTest { String key = "SwqFmsKxcbq23"; // 连接参数列表 @Test public void testJoinParamsDictSort() { SoMap map = SoMap.getSoMap() .set("name", "zhang") .set("age", 18) .set("sex", "女"); String str = SaSignManager.getSaSignTemplate().joinParamsDictSort(map); // 按照音序排列 Assertions.assertEquals(str, "age=18&name=zhang&sex=女"); } // 给参数签名 @Test public void testCreateSign() { SoMap map = SoMap.getSoMap() .set("name", "zhang") .set("age", 18) .set("sex", "女"); SaSignManager.getSaSignTemplate().setSignConfig(new SaSignConfig().setSecretKey(key)); String sign = SaSignManager.getSaSignTemplate().createSign(map); Assertions.assertEquals(sign, "6f5e844a53e74363c2f6b24f64c4f0ff"); // 多次签名,结果一致 String sign2 = SaSignManager.getSaSignTemplate().createSign(map); Assertions.assertEquals(sign, sign2); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/stp/TokenInfoTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.stp; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.util.SaTokenConsts; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; /** * Token 参数扩展 * * @author click33 * @since 2022-9-5 */ public class TokenInfoTest { @Test public void test() { SaTokenInfo info = new SaTokenInfo(); info.setTokenName("satoken"); info.setTokenValue("xxxxx-xxxxx-xxxxx-xxxxx"); info.setIsLogin(true); info.setLoginId(10001); info.setLoginType("login"); info.setTokenTimeout(1800); info.setSessionTimeout(120); info.setTokenSessionTimeout(1800); info.setTokenActiveTimeout(120); info.setLoginDeviceType("PC"); info.setTag("xxx"); Assertions.assertEquals(info.getTokenName(), "satoken"); Assertions.assertEquals(info.getTokenValue(), "xxxxx-xxxxx-xxxxx-xxxxx"); Assertions.assertEquals(info.getIsLogin(), true); Assertions.assertEquals(info.getLoginId(), 10001); Assertions.assertEquals(info.getLoginType(), "login"); Assertions.assertEquals(info.getTokenTimeout(), 1800); Assertions.assertEquals(info.getSessionTimeout(), 120); Assertions.assertEquals(info.getTokenSessionTimeout(), 1800); Assertions.assertEquals(info.getTokenActiveTimeout(), 120); Assertions.assertEquals(info.getLoginDeviceType(), "PC"); Assertions.assertEquals(info.getTag(), "xxx"); Assertions.assertNotNull(info.toString()); } @Test public void testLoginParameter() { Assertions.assertEquals(new SaLoginParameter().setDeviceType("PC").getDeviceType(), "PC"); Assertions.assertEquals(new SaLoginParameter().setIsLastingCookie(false).getIsLastingCookie(), false); Assertions.assertEquals(new SaLoginParameter().setTimeout(1600).getTimeout(), 1600); Assertions.assertEquals(new SaLoginParameter().setToken("token-xxx").getToken(), "token-xxx"); Assertions.assertEquals(new SaLoginParameter().setExtra("age", 18).getExtra("age"), 18); Map extraData = new HashMap<>(); extraData.put("age", 20); SaLoginParameter lm = new SaLoginParameter().setExtraData(extraData); Assertions.assertEquals(lm.getExtraData(), extraData); Assertions.assertEquals(lm.getExtra("age"), 20); Assertions.assertTrue(lm.haveExtraData()); Assertions.assertNotNull(lm.toString()); // 计算 CookieTimeout SaLoginParameter loginParameter = SaLoginParameter .create() .setTimeout(-1); Assertions.assertEquals(loginParameter.getCookieTimeout(), Integer.MAX_VALUE); Assertions.assertEquals(loginParameter.getDeviceType(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.temp; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.temp.SaTempUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.List; /** * 临时Token模块测试 * * @author click33 * @since 2022-9-1 */ public class SaTempTokenTest { // 测试:临时Token认证模块 @Test public void testSaTemp() { SaTokenDao dao = SaManager.getSaTokenDao(); // 生成token String token = SaTempUtil.createToken("group-1014", 200); // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet()); // System.out.println("satoken:temp-token:" + ":" + token); Assertions.assertNotNull(token); Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token), "group-1014"); // 解析token String value = SaTempUtil.parseToken(token, String.class); Assertions.assertEquals(value, "group-1014"); // 解析 token 并裁剪前缀 long value2 = SaTempUtil.parseToken(token, "group-", Long.class); Assertions.assertEquals(value2, 1014); // 默认类型 Object value3 = SaTempUtil.parseToken(token); Assertions.assertEquals(value3, "group-1014"); // 转换类型 String value4 = SaTempUtil.parseToken(token, String.class); Assertions.assertEquals(value4, "group-1014"); // 过期时间 long timeout = SaTempUtil.getTimeout(token); Assertions.assertTrue(timeout > 195); Assertions.assertTrue(timeout < 201); // 回收token SaTempUtil.deleteToken(token); String value5 = SaTempUtil.parseToken(token, String.class); Assertions.assertNull(value5); Assertions.assertNull(dao.getObject("satoken:temp-token:" + ":" + token)); } // 测试:临时Token认证模块索引 @Test public void testSaTempIndex() { SaTokenDao dao = SaManager.getSaTokenDao(); // 生成token String token1 = SaTempUtil.createToken("1001", 200, true); String token2 = SaTempUtil.createToken("1001", 300, true); String token3 = SaTempUtil.createToken("1001", 400, true); Assertions.assertNotNull(token1); Assertions.assertNotNull(token2); Assertions.assertNotNull(token3); // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); // 解析token Assertions.assertEquals(SaTempUtil.parseToken(token1, String.class), "1001"); Assertions.assertEquals(SaTempUtil.parseToken(token2, String.class), "1001"); Assertions.assertEquals(SaTempUtil.parseToken(token3, String.class), "1001"); // 缓存数据比对 Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token1), "1001"); Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token2), "1001"); Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token3), "1001"); // 索引 List tempTokenList = SaTempUtil.getTempTokenList("1001"); Assertions.assertEquals(tempTokenList.size(), 3); Assertions.assertTrue(tempTokenList.contains(token1)); Assertions.assertTrue(tempTokenList.contains(token2)); Assertions.assertTrue(tempTokenList.contains(token3)); long sessionTimeout = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); Assertions.assertTrue(sessionTimeout > 395); Assertions.assertTrue(sessionTimeout < 401); // 移除一个 token SaTempUtil.deleteToken(token3); Assertions.assertNull(SaTempUtil.parseToken(token3, String.class)); Assertions.assertNull(dao.getObject("satoken:temp-token:" + token3)); List tempTokenList2 = SaTempUtil.getTempTokenList("1001"); Assertions.assertEquals(tempTokenList2.size(), 2); Assertions.assertFalse(tempTokenList2.contains(token3)); long sessionTimeout2 = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); Assertions.assertTrue(sessionTimeout2 > 295); Assertions.assertTrue(sessionTimeout2 < 301); // 新增一个 token String token4 = SaTempUtil.createToken("1001", -1, true); Assertions.assertEquals(SaTempUtil.parseToken(token4, String.class), "1001"); List tempTokenList3 = SaTempUtil.getTempTokenList("1001"); Assertions.assertEquals(tempTokenList3.size(), 3); Assertions.assertTrue(tempTokenList3.contains(token4)); long sessionTimeout4 = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); Assertions.assertEquals(-1, sessionTimeout4); } @Test public void testGetJwtSecretKey() { // 秘钥默认为null String jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey(); Assertions.assertNull(jwtSecretKey); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaFoxUtilTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.util; import cn.dev33.satoken.util.SaFoxUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; /** * SaFoxUtil 工具类测试 * * @author click33 * @since 2022-2-8 22:14:25 */ public class SaFoxUtilTest { @Test public void getRandomString() { String randomString = SaFoxUtil.getRandomString(8); Assertions.assertEquals(randomString.length(), 8); } @Test public void isEmpty() { Assertions.assertTrue(SaFoxUtil.isEmpty("")); Assertions.assertTrue(SaFoxUtil.isEmpty(null)); Assertions.assertFalse(SaFoxUtil.isEmpty("abc")); Assertions.assertTrue(SaFoxUtil.isNotEmpty("abc")); Assertions.assertFalse(SaFoxUtil.isNotEmpty(null)); Assertions.assertFalse(SaFoxUtil.isNotEmpty("")); } @Test public void equals() { Assertions.assertTrue(SaFoxUtil.equals(null, null)); Assertions.assertTrue(SaFoxUtil.equals("a", "a")); Assertions.assertFalse(SaFoxUtil.equals("1", 1)); Assertions.assertFalse(SaFoxUtil.equals("1", null)); Assertions.assertFalse(SaFoxUtil.equals(null, "1")); } @Test public void getMarking28() { Assertions.assertNotEquals(SaFoxUtil.getMarking28(), SaFoxUtil.getMarking28()); } @Test public void formatDate() { Instant instant = Instant.ofEpochMilli(1644328600364L); ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai")); String formatDate = SaFoxUtil.formatDate(zonedDateTime); Assertions.assertEquals(formatDate, "2022-02-08 21:56:40"); } @Test public void searchList() { // 原始数据 List dataList = Arrays.asList("token1", "token2", "token3", "token4", "token5", "aaa1"); // 分页 List list1 = SaFoxUtil.searchList(dataList, 1, 2, true); Assertions.assertEquals(list1.size(), 2); Assertions.assertEquals(list1.get(0), "token2"); Assertions.assertEquals(list1.get(1), "token3"); // 前缀筛选 List list2 = SaFoxUtil.searchList(dataList, "token", "", 0, 10, true); Assertions.assertEquals(list2.size(), 5); // 关键字筛选 List list3 = SaFoxUtil.searchList(dataList, "", "1", 0, 10, true); Assertions.assertEquals(list3.size(), 2); // 综合筛选 List list4 = SaFoxUtil.searchList(dataList, "token", "1", 0, 10, true); Assertions.assertEquals(list4.size(), 1); // 关键字为null时,效果和 "" 等同 List list4_2 = SaFoxUtil.searchList(dataList, null, null, 0, 10, true); List list4_3 = SaFoxUtil.searchList(dataList, "", "", 0, 10, true); Assertions.assertEquals(list4_2.get(0), list4_3.get(0)); // 不做分页 List list5 = SaFoxUtil.searchList(dataList, "", "", 0, -1, true); Assertions.assertEquals(list5.size(), dataList.size()); // 反序排列 list6的第一个元素 == dataList最后一个元素 List list6 = SaFoxUtil.searchList(dataList, "", "", 0, -1, false); Assertions.assertEquals(list6.get(0), dataList.get(dataList.size() - 1)); } @Test public void vagueMatch() { // 不模糊 Assertions.assertTrue(SaFoxUtil.vagueMatch("hello", "hello")); // 正常模糊 Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello")); Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello world")); Assertions.assertTrue(SaFoxUtil.vagueMatch("hello*", "hello*")); Assertions.assertFalse(SaFoxUtil.vagueMatch("hello*", "he")); // 带 - Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-add")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*", "user-*")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*", "user")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user-*-add-*", "user-xx-add-1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*-add-*", "user-add-1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user-*", "usermgt-list")); // 带 / Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/add")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*", "user/*")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*", "user")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user/*/add/*", "user/xx/add/1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*/add/*", "user/add/1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user/*", "usermgt/list")); // 带 : Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:add")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*", "user:*")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*", "user")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user:*:add:*", "user:xx:add:1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*:add:*", "user:add:1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user:*", "usermgt:list")); // 带 . Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.add")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*", "user.*")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*", "user")); Assertions.assertTrue(SaFoxUtil.vagueMatch("user.*.add.*", "user.xx.add.1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*.add.*", "user.add.1")); Assertions.assertFalse(SaFoxUtil.vagueMatch("user.*", "usermgt.list")); // 极端情况 Assertions.assertTrue(SaFoxUtil.vagueMatch(null, null)); Assertions.assertFalse(SaFoxUtil.vagueMatch(null, "hello")); Assertions.assertFalse(SaFoxUtil.vagueMatch("hello*", null)); // url 匹配 Assertions.assertTrue(SaFoxUtil.vagueMatch("*", "http://sa-sso-client1.com:9001/sso/login")); Assertions.assertTrue(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9001/sso/login")); Assertions.assertTrue(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9001/sso/login?name=1")); Assertions.assertTrue(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9001/sso/login?name=1&age=2")); Assertions.assertFalse(SaFoxUtil.vagueMatch("http://sa-sso-client1.com:9001/*", "http://sa-sso-client1.com:9002")); } @Test public void isWrapperType() { Assertions.assertTrue(SaFoxUtil.isWrapperType(Integer.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Short.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Long.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Byte.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Float.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Double.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Boolean.class)); Assertions.assertTrue(SaFoxUtil.isWrapperType(Character.class)); Assertions.assertFalse(SaFoxUtil.isWrapperType(int.class)); Assertions.assertFalse(SaFoxUtil.isWrapperType(long.class)); Assertions.assertFalse(SaFoxUtil.isWrapperType(Object.class)); } @Test public void isBasicType() { Assertions.assertTrue(SaFoxUtil.isBasicType(int.class)); Assertions.assertTrue(SaFoxUtil.isBasicType(Integer.class)); Assertions.assertTrue(SaFoxUtil.isBasicType(long.class)); Assertions.assertTrue(SaFoxUtil.isBasicType(Long.class)); Assertions.assertTrue(SaFoxUtil.isBasicType(String.class)); Assertions.assertFalse(SaFoxUtil.isBasicType(List.class)); Assertions.assertFalse(SaFoxUtil.isBasicType(Map.class)); } @Test public void getValueByType() { // 基础类型,转换 Assertions.assertEquals(SaFoxUtil.getValueByType("1", int.class), 1); Assertions.assertEquals(SaFoxUtil.getValueByType("1", long.class), 1L); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Long.class), 1L); Assertions.assertEquals(SaFoxUtil.getValueByType("1", String.class), "1"); Assertions.assertEquals(SaFoxUtil.getValueByType("1", short.class), (short)1); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Short.class), (short)1); Assertions.assertEquals(SaFoxUtil.getValueByType("1", byte.class), (byte)1); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Byte.class), (byte)1); Assertions.assertEquals(SaFoxUtil.getValueByType("1", float.class), 1f); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Float.class), 1f); Assertions.assertEquals(SaFoxUtil.getValueByType("1", double.class), 1.0); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Double.class), 1.0); Assertions.assertEquals(SaFoxUtil.getValueByType("1", boolean.class), false); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Boolean.class), false); Assertions.assertEquals(SaFoxUtil.getValueByType("1", char.class), '1'); Assertions.assertEquals(SaFoxUtil.getValueByType("1", Character.class), '1'); Assertions.assertEquals(SaFoxUtil.getValueByType(1, String.class), "1"); // 复杂类型,还原 Object obj = new ArrayList<>(); Assertions.assertEquals(SaFoxUtil.getValueByType(obj, List.class).getClass(), ArrayList.class); } @Test public void joinParam() { // 参数为空时,返回原url Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc", null), "https://sa-token.cc"); Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc", ""), "https://sa-token.cc"); // url为空时,视为空字符串 Assertions.assertEquals(SaFoxUtil.joinParam(null, "id=1"), "?id=1"); Assertions.assertEquals(SaFoxUtil.joinParam("", "id=1"), "?id=1"); // 各种情况的测试 Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc", "id=1"), "https://sa-token.cc?id=1"); Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc?", "id=1"), "https://sa-token.cc?id=1"); Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc?name=zhang", "id=1"), "https://sa-token.cc?name=zhang&id=1"); Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc?name=zhang&", "id=1"), "https://sa-token.cc?name=zhang&id=1"); // 重载方法测试 Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc?name=zhang&", "id", 1), "https://sa-token.cc?name=zhang&id=1"); // url或key为null时,不拼接 Assertions.assertEquals(SaFoxUtil.joinParam(null, "id", 1), null); Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc", null, 1), "https://sa-token.cc"); // value为null时,会拼接出一个null字符串 Assertions.assertEquals(SaFoxUtil.joinParam("https://sa-token.cc", "id", null), "https://sa-token.cc?id=null"); } @Test public void joinSharpParam() { // 参数为空时,返回原url Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc", null), "https://sa-token.cc"); Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc", ""), "https://sa-token.cc"); // url为空时,视为空字符串 Assertions.assertEquals(SaFoxUtil.joinSharpParam(null, "id=1"), "#id=1"); Assertions.assertEquals(SaFoxUtil.joinSharpParam("", "id=1"), "#id=1"); // 各种情况的测试 Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc", "id=1"), "https://sa-token.cc#id=1"); Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc#", "id=1"), "https://sa-token.cc#id=1"); Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc#name=zhang", "id=1"), "https://sa-token.cc#name=zhang&id=1"); Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc#name=zhang&", "id=1"), "https://sa-token.cc#name=zhang&id=1"); // 重载方法测试 Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc#name=zhang&", "id", 1), "https://sa-token.cc#name=zhang&id=1"); // url或key为null时,不拼接 Assertions.assertEquals(SaFoxUtil.joinSharpParam(null, "id", 1), null); Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc", null, 1), "https://sa-token.cc"); // value为null时,会拼接出一个null字符串 Assertions.assertEquals(SaFoxUtil.joinSharpParam("https://sa-token.cc", "id", null), "https://sa-token.cc#id=null"); } @Test public void spliceTwoUrl() { // 其中一个为null时,直接返回另一个 Assertions.assertEquals(SaFoxUtil.spliceTwoUrl("https://sa-sso-server.com/sso/auth", null), "https://sa-sso-server.com/sso/auth"); Assertions.assertEquals(SaFoxUtil.spliceTwoUrl(null, "https://sa-sso-server.com/sso/auth"), "https://sa-sso-server.com/sso/auth"); // 正常情况,拼接 Assertions.assertEquals(SaFoxUtil.spliceTwoUrl("https://sa-sso-server.com", "/sso/auth"), "https://sa-sso-server.com/sso/auth"); // url2以http开头时,直接返回url2 Assertions.assertEquals(SaFoxUtil.spliceTwoUrl("https://sa-sso-server2.com", "https://sa-sso-server.com/sso/auth2"), "https://sa-sso-server.com/sso/auth2"); } @Test public void arrayJoin() { Assertions.assertEquals(SaFoxUtil.arrayJoin(new String[] {"a", "b", "c"}), "a,b,c"); Assertions.assertEquals(SaFoxUtil.arrayJoin(new String[] {}), ""); Assertions.assertEquals(SaFoxUtil.arrayJoin(null), ""); } @Test public void isUrl() { Assertions.assertTrue(SaFoxUtil.isUrl("https://sa-token.cc")); Assertions.assertTrue(SaFoxUtil.isUrl("https://www.baidu.com/")); Assertions.assertFalse(SaFoxUtil.isUrl(null)); Assertions.assertFalse(SaFoxUtil.isUrl("")); Assertions.assertFalse(SaFoxUtil.isUrl("htt://www.baidu.com/")); Assertions.assertFalse(SaFoxUtil.isUrl("https:www.baidu.com/")); Assertions.assertFalse(SaFoxUtil.isUrl("httpswwwbaiducom/")); Assertions.assertFalse(SaFoxUtil.isUrl("https://www.baidu.com/,")); } @Test public void encodeUrl() { Assertions.assertEquals(SaFoxUtil.encodeUrl("https://sa-token.cc"), "https%3A%2F%2Fsa-token.cc"); Assertions.assertEquals(SaFoxUtil.decoderUrl("https%3A%2F%2Fsa-token.cc"), "https://sa-token.cc"); } @Test public void convertStringToList() { List list = SaFoxUtil.convertStringToList("a,b,,c"); Assertions.assertEquals(list.size(), 3); Assertions.assertEquals(list.get(0), "a"); Assertions.assertEquals(list.get(1), "b"); Assertions.assertEquals(list.get(2), "c"); List list2 = SaFoxUtil.convertStringToList("a,"); Assertions.assertEquals(list2.size(), 1); List list3 = SaFoxUtil.convertStringToList(","); Assertions.assertEquals(list3.size(), 0); List list4 = SaFoxUtil.convertStringToList(""); Assertions.assertEquals(list4.size(), 0); List list5 = SaFoxUtil.convertStringToList(null); Assertions.assertEquals(list5.size(), 0); } @Test public void convertListToString() { // 正常 List list = Arrays.asList("a", "b", "c"); Assertions.assertEquals(SaFoxUtil.convertListToString(list), "a,b,c"); // 空数组 List list2 = Arrays.asList(); Assertions.assertEquals(SaFoxUtil.convertListToString(list2), ""); // 空 List list3 = null; Assertions.assertEquals(SaFoxUtil.convertListToString(list3), ""); } @Test public void convertStringToArray() { String[] array = SaFoxUtil.convertStringToArray("a,b,c"); Assertions.assertEquals(array.length, 3); Assertions.assertEquals(array[0], "a"); Assertions.assertEquals(array[1], "b"); Assertions.assertEquals(array[2], "c"); String[] array2 = SaFoxUtil.convertStringToArray("a,"); Assertions.assertEquals(array2.length, 1); String[] array3 = SaFoxUtil.convertStringToArray(","); Assertions.assertEquals(array3.length, 0); String[] array4 = SaFoxUtil.convertStringToArray(""); Assertions.assertEquals(array4.length, 0); String[] array5 = SaFoxUtil.convertStringToArray(null); Assertions.assertEquals(array5.length, 0); } @Test public void convertArrayToString() { // 正常 String[] array = new String[] {"a", "b", "c"}; Assertions.assertEquals(SaFoxUtil.convertArrayToString(array), "a,b,c"); // null String[] array2 = null; Assertions.assertEquals(SaFoxUtil.convertArrayToString(array2), ""); // 空数组 String[] array3 = new String[] {}; Assertions.assertEquals(SaFoxUtil.convertArrayToString(array3), ""); } @Test public void emptyList() { List list = SaFoxUtil.emptyList(); Assertions.assertEquals(list.size(), 0); } @Test public void toList() { List list = SaFoxUtil.toList("a","b", "c"); Assertions.assertEquals(list.size(), 3); Assertions.assertEquals(list.get(0), "a"); Assertions.assertEquals(list.get(1), "b"); Assertions.assertEquals(list.get(2), "c"); } @Test public void hasNonPrintableASCII() { Assertions.assertFalse(SaFoxUtil.hasNonPrintableASCII("Hello World!")); Assertions.assertTrue(SaFoxUtil.hasNonPrintableASCII("Hello\u0007World")); Assertions.assertTrue(SaFoxUtil.hasNonPrintableASCII("Hello\tWorld")); Assertions.assertTrue(SaFoxUtil.hasNonPrintableASCII("Hello\nWorld")); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaResultTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.core.util; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import cn.dev33.satoken.util.SaResult; /** * SaResult 结果集 测试 * * @author click33 * @since 2022-2-8 22:14:25 */ public class SaResultTest { // 构造函数构建 @Test public void test() { // 无参构造时,默认所有参数为null SaResult res = new SaResult(); Assertions.assertEquals(res.getCode(), null); Assertions.assertEquals(res.getMsg(), null); Assertions.assertEquals(res.getData(), null); // 全参数构造 SaResult res2 = new SaResult(200, "ok", "zhangsan"); Assertions.assertEquals((int)res2.getCode(), 200); Assertions.assertEquals(res2.getMsg(), "ok"); Assertions.assertEquals(res2.getData(), "zhangsan"); // 自定义写值取值 res.set("age", 18); Assertions.assertEquals(res.get("age"), 18); Assertions.assertEquals(res.get("age", String.class), "18"); Assertions.assertEquals(res.getOrDefault("age", 20), 18); Assertions.assertEquals(res.getOrDefault("age2", 20), 20); } // 静态函数快速构建 @Test public void test2() { // ok 和 error Assertions.assertEquals((int)SaResult.ok().getCode(), 200); Assertions.assertEquals((int)SaResult.error().getCode(), 500); Assertions.assertEquals(SaResult.error("错误").getMsg(), "错误"); // 指定code SaResult res = SaResult.code(201); Assertions.assertEquals((int)res.getCode(), 201); // // 全参数构造 SaResult res2 = SaResult.get(200, "ok", "zhangsan"); Assertions.assertEquals((int)res2.getCode(), 200); Assertions.assertEquals(res2.getMsg(), "ok"); Assertions.assertEquals(res2.getData(), "zhangsan"); // 序列化 Assertions.assertEquals(res2.toString(), "{\"code\": 200, \"msg\": \"ok\", \"data\": \"zhangsan\"}"); // data 为 int 时的序列化 res2.setData(1); Assertions.assertEquals(res2.toString(), "{\"code\": 200, \"msg\": \"ok\", \"data\": 1}"); // Map 构造 Map map = new HashMap<>(); map.put("key1", "value1"); map.put("key2", "value2"); SaResult res4 = new SaResult(map); Assertions.assertEquals(res4.get("key1"), "value1"); Assertions.assertEquals(res4.get("key2"), "value2"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/StartUpApplication.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动类 * @author Auster * */ @SpringBootApplication public class StartUpApplication { public static void main(String[] args) { SpringApplication.run(StartUpApplication.class, args); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.annotation; import cn.dev33.satoken.annotation.*; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试注解用的Controller * * @author click33 * @since 2022-9-2 */ @RestController @RequestMapping("/at/") public class SaAnnotationController { // 登录 @RequestMapping("login") public SaResult login(long id) { StpUtil.login(id); return SaResult.ok().set("token", StpUtil.getTokenValue()); } // 登录校验 @SaCheckLogin @RequestMapping("checkLogin") public SaResult checkLogin() { return SaResult.ok(); } // 角色校验 @SaCheckRole("admin") @RequestMapping("checkRole") public SaResult checkRole() { return SaResult.ok(); } // 权限校验 @SaCheckPermission("art-add") @RequestMapping("checkPermission") public SaResult checkPermission() { return SaResult.ok(); } // 权限校验 or 角色校验 @SaCheckPermission(value = "art-add2", orRole = "admin") @RequestMapping("checkPermission2") public SaResult checkPermission2() { return SaResult.ok(); } // 开启二级认证 @RequestMapping("openSafe") public SaResult openSafe() { StpUtil.openSafe(120); return SaResult.ok(); } // 二级认证校验 @SaCheckSafe @RequestMapping("checkSafe") public SaResult checkSafe() { return SaResult.ok(); } // 封禁账号 @RequestMapping("disable") public SaResult disable(long id) { StpUtil.disable(id, "comment", 200); return SaResult.ok(); } // 服务封禁校验 @SaCheckDisable("comment") @RequestMapping("checkDisable") public SaResult checkDisable() { return SaResult.ok(); } // 解封账号 @RequestMapping("untieDisable") public SaResult untieDisable(long id) { StpUtil.untieDisable(id, "comment"); return SaResult.ok(); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationControllerTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.annotation; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.integrate.StartUpApplication; import cn.dev33.satoken.util.SaResult; /** * 注解鉴权测试 * * @author Auster * */ @SpringBootTest(classes = StartUpApplication.class) public class SaAnnotationControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; // 每个方法前执行 @BeforeEach public void before() { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); } // 校验通过的情况 @Test public void testPassing() { // 登录拿到Token SaResult res = request("/at/login?id=10001"); String satoken = res.get("token", String.class); Assertions.assertNotNull(satoken); // 登录校验,通过 SaResult res2 = request("/at/checkLogin?satoken=" + satoken); Assertions.assertEquals(res2.getCode(), 200); // 角色校验,通过 SaResult res3 = request("/at/checkRole?satoken=" + satoken); Assertions.assertEquals(res3.getCode(), 200); // 权限校验,通过 SaResult res4 = request("/at/checkPermission?satoken=" + satoken); Assertions.assertEquals(res4.getCode(), 200); // 权限校验or角色校验,通过 SaResult res5 = request("/at/checkPermission2?satoken=" + satoken); Assertions.assertEquals(res5.getCode(), 200); // 开启二级认证 SaResult res6 = request("/at/openSafe?satoken=" + satoken); Assertions.assertEquals(res6.getCode(), 200); // 校验二级认证,通过 SaResult res7 = request("/at/checkSafe?satoken=" + satoken); Assertions.assertEquals(res7.getCode(), 200); // 访问校验封禁的接口 ,通过 SaResult res9 = request("/at/checkDisable?satoken=" + satoken); Assertions.assertEquals(res9.getCode(), 200); } // 校验不通过的情况 @Test public void testNotPassing() { // 登录拿到Token SaResult res = request("/at/login?id=10002"); String satoken = res.get("token", String.class); Assertions.assertNotNull(satoken); // 登录校验,不通过 SaResult res2 = request("/at/checkLogin"); Assertions.assertEquals(res2.getCode(), 401); // 角色校验,不通过 SaResult res3 = request("/at/checkRole?satoken=" + satoken); Assertions.assertEquals(res3.getCode(), 402); // 权限校验,不通过 SaResult res4 = request("/at/checkPermission?satoken=" + satoken); Assertions.assertEquals(res4.getCode(), 403); // 权限校验or角色校验,不通过 SaResult res5 = request("/at/checkPermission2?satoken=" + satoken); Assertions.assertEquals(res5.getCode(), 403); // 校验二级认证,不通过 SaResult res7 = request("/at/checkSafe?satoken=" + satoken); Assertions.assertEquals(res7.getCode(), 901); // -------- 登录拿到Token String satoken10042 = request("/at/login?id=10042").get("token", String.class); Assertions.assertNotNull(satoken10042); // 校验账号封禁 ,通过 SaResult res8 = request("/at/disable?id=10042"); Assertions.assertEquals(res8.getCode(), 200); // 访问校验封禁的接口 ,不通过 SaResult res9 = request("/at/checkDisable?satoken=" + satoken10042); Assertions.assertEquals(res9.getCode(), 904); // 解封后就能访问了 request("/at/untieDisable?id=10042"); SaResult res10 = request("/at/checkDisable?satoken=" + satoken10042); Assertions.assertEquals(res10.getCode(), 200); } // 测试忽略认证 @Test public void testIgnore() { // 必须登录才能访问的 SaResult res1 = request("/ig/show1"); Assertions.assertEquals(res1.getCode(), 401); // 不登录也可以访问的 SaResult res2 = request("/ig/show2"); Assertions.assertEquals(res2.getCode(), 200); } // 封装请求 private SaResult request(String path) { try { // 发请求 MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post(path) .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); // 转 Map String content = mvcResult.getResponse().getContentAsString(); Map map = SaManager.getSaJsonTemplate().jsonToMap(content); // 转 SaResult 对象 return new SaResult().setMap(map); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationIgnoreController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.annotation; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaIgnore; import cn.dev33.satoken.util.SaResult; /** * 测试注解用的Controller * * @author click33 * @since 2022-9-2 */ @SaCheckLogin @RestController @RequestMapping("/ig/") public class SaAnnotationIgnoreController { // 需要登录后访问 @RequestMapping("show1") public SaResult show1() { return SaResult.ok(); } // 不登录也可访问 @SaIgnore @RequestMapping("show2") public SaResult show2() { return SaResult.ok(); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/HandlerException.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import cn.dev33.satoken.exception.DisableServiceException; import cn.dev33.satoken.exception.NotHttpBasicAuthException; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.exception.NotRoleException; import cn.dev33.satoken.exception.NotSafeException; import cn.dev33.satoken.exception.SameTokenInvalidException; import cn.dev33.satoken.util.SaResult; /** * 全局异常处理 * @author click33 * */ @RestControllerAdvice public class HandlerException { // 未登录异常,code=401 @ExceptionHandler(NotLoginException.class) public SaResult handlerNotLoginException(NotLoginException e) { return SaResult.error().setCode(401); } // 缺少角色异常,code=402 @ExceptionHandler(NotRoleException.class) public SaResult handlerNotRoleException(NotRoleException e) { return SaResult.error().setCode(402); } // 缺少权限异常,code=403 @ExceptionHandler(NotPermissionException.class) public SaResult handlerNotPermissionException(NotPermissionException e) { return SaResult.error().setCode(403); } // 二级认证失败,code=901 @ExceptionHandler(NotSafeException.class) public SaResult handlerNotSafeException(NotSafeException e) { return SaResult.error().setCode(901); } // same-token 校验失败,code=902 @ExceptionHandler(SameTokenInvalidException.class) public SaResult handlerSameTokenInvalidException(SameTokenInvalidException e) { return SaResult.error().setCode(902); } // Http Basic 校验失败,code=903 @ExceptionHandler(NotHttpBasicAuthException.class) public SaResult handlerNotBasicAuthException(NotHttpBasicAuthException e) { return SaResult.error().setCode(903); } // 服务被封禁 ,code=904 @ExceptionHandler(DisableServiceException.class) public SaResult handlerDisableServiceException(DisableServiceException e) { return SaResult.error().setCode(904); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/SaTokenConfigure.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import cn.dev33.satoken.interceptor.SaInterceptor; /** * Sa-Token 相关配置类 * * @author click33 * @since 2022-9-2 */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 拦截器,打开注解式鉴权功能 @Override public void addInterceptors(InterceptorRegistry registry) { // 测试环境下上下文过滤器不生效,所以此处从拦截器需要补充上下文 registry.addInterceptor(new SaInterceptor(handle -> { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); }).isAnnotation(false)).addPathPatterns("/**"); // 注册 Sa-Token 拦截器,打开注解式鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/StpInterfaceImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure; import java.util.Arrays; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.util.SaFoxUtil; /** * 自定义权限验证接口扩展 * * @author click33 * */ @Component public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { int id = SaFoxUtil.getValueByType(loginId, int.class); if(id == 10001) { return Arrays.asList("user*", "art-add", "art-delete", "art-update", "art-get"); } else { return null; } } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { int id = SaFoxUtil.getValueByType(loginId, int.class); if(id == 10001) { return Arrays.asList("admin", "super-admin"); } else { return null; } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaBasicTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate; @Component public class MySaBasicTemplate extends SaHttpBasicTemplate { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaOAuth2Template.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; /** * 自定义 Sa-OAuth2 模板方法 * * @author click33 * @since 2022-9-5 */ @Component public class MySaOAuth2Template extends SaOAuth2Template { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSameTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.same.SaSameTemplate; @Component public class MySaSameTemplate extends SaSameTemplate { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSignTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import cn.dev33.satoken.sign.template.SaSignTemplate; import org.springframework.stereotype.Component; @Component public class MySaSignTemplate extends SaSignTemplate { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSsoTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.sso.template.SaSsoTemplate; /** * 自定义 Sa-SSO 模板方法 * * @author click33 * @since 2022-9-5 */ @Component public class MySaSsoTemplate extends SaSsoTemplate { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import cn.dev33.satoken.temp.SaTempTemplate; import org.springframework.stereotype.Component; @Component public class MySaTempTemplate extends SaTempTemplate { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenDao.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.dao.SaTokenDaoDefaultImpl; @Component public class MySaTokenDao extends SaTokenDaoDefaultImpl { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenListener.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.listener.SaTokenListenerForSimple; @Component public class MySaTokenListener extends SaTokenListenerForSimple { } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MyStpLogic.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.configure.inject; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; @Component public class MyStpLogic extends StpLogic { public MyStpLogic() { super(StpUtil.TYPE); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.login; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 登录测试 * * @author click33 * */ @RestController @RequestMapping("/acc/") public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.data(StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginControllerTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.login; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import cn.dev33.satoken.integrate.StartUpApplication; import cn.dev33.satoken.util.SoMap; /** * Sa-Token 登录API测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) @SuppressWarnings("deprecation") public class LoginControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; // 开始 @BeforeEach public void before() { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test public void testLogin() throws Exception{ // 请求 MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post("/acc/doLogin") .param("name", "zhang") .param("pwd", "123456") .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON_UTF8) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); // 拿到结果 SoMap so = SoMap.getSoMap().setJsonString( mvcResult.getResponse().getContentAsString() ); String token = so.getString("token"); // 断言 Assertions.assertTrue(mvcResult.getResponse().getHeader("Set-Cookie") != null); Assertions.assertEquals(so.getInt("code"), 200); Assertions.assertNotNull(token); } @Test @SuppressWarnings("unchecked") public void testLogin2() throws Exception{ // 获取token SoMap so = request("/acc/doLogin?name=zhang&pwd=123456"); Assertions.assertNotNull(so.getString("token")); String token = so.getString("token"); // 是否登录 SoMap so2 = request("/acc/isLogin?satoken=" + token); Assertions.assertTrue(so2.getBoolean("data")); // tokenInfo SoMap so3 = request("/acc/tokenInfo?satoken=" + token); SoMap so4 = SoMap.getSoMap((Map)so3.get("data")); Assertions.assertEquals(so4.getString("tokenName"), "satoken"); Assertions.assertEquals(so4.getString("tokenValue"), token); // 注销 request("/acc/logout?satoken=" + token); // 是否登录 SoMap so5 = request("/acc/isLogin?satoken=" + token); Assertions.assertFalse(so5.getBoolean("data")); } // 封装请求 private SoMap request(String path) throws Exception { MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post(path) .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); SoMap so = SoMap.getSoMap().setJsonString( mvcResult.getResponse().getContentAsString() ); return so; } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.more; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.util.SaFoxUtil; import cn.dev33.satoken.util.SaResult; /** * 其它测试 * * @author click33 * */ @RestController @RequestMapping("/more/") public class MoreController { // 一些基本的测试 @RequestMapping("getInfo") public SaResult getInfo() { SaRequest req = SaHolder.getRequest(); boolean flag = SaFoxUtil.equals(req.getParam("name"), "zhang") && SaFoxUtil.equals(req.getParam("name2", "li"), "li") && SaFoxUtil.equals(req.getParamNotNull("name"), "zhang") && req.isParam("name", "zhang") && req.isPath("/more/getInfo") && req.hasParam("name") && SaFoxUtil.equals(req.getHeader("div"), "val") && SaFoxUtil.equals(req.getHeader("div", "zhang"), "val") && SaFoxUtil.equals(req.getHeader("div2", "zhang"), "zhang") ; SaHolder.getResponse().setServer("sa-server"); return SaResult.data(flag); } // Http Basic 认证 @RequestMapping("basicAuth") public SaResult basicAuth() { SaHttpBasicUtil.check("sa:123456"); return SaResult.ok(); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreControllerTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.more; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.integrate.StartUpApplication; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.spring.SaTokenContextForSpring; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; /** * 其它测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) public class MoreControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; // 开始 @BeforeEach public void before() { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); // 在单元测试时,通过 request.getServletPath() 获取到的请求路径为空,导致路由拦截不正确 // 虽然不知道为什么会这样,但是暂时可以通过以下方式来解决 SaManager.setSaTokenContext(new SaTokenContextForSpring() { @Override public SaRequest getRequest() { return new SaRequestForServlet(SpringMVCUtil.getRequest()) { @Override public String getRequestPath() { return request.getRequestURI(); } }; } }); } // 基础API测试 @Test public void testApi() { SaResult res = request("/more/getInfo?name=zhang"); Assertions.assertEquals(res.getData(), true); } // Http Basic 认证 @Test public void testBasic() throws Exception { // ---------------- 认证不通过 MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post("/more/basicAuth") .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) ) .andExpect(MockMvcResultMatchers.status().is(401)) .andReturn(); // 转 Map String content = mvcResult.getResponse().getContentAsString(); Map map = SaManager.getSaJsonTemplate().jsonToMap(content); // 转 SaResult 对象 SaResult res = new SaResult().setMap(map); Assertions.assertEquals(res.getCode(), 903); // 会有一个特殊响应头 String header = mvcResult.getResponse().getHeader("WWW-Authenticate"); Assertions.assertEquals(header, "Basic Realm=Sa-Token"); // ---------------- 认证通过 MvcResult mvcResult2 = mvc.perform( MockMvcRequestBuilders.post("/more/basicAuth") .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) .header("Authorization", "Basic c2E6MTIzNDU2") ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); // 转 Map String content2 = mvcResult2.getResponse().getContentAsString(); Map map2 = SaManager.getSaJsonTemplate().jsonToMap(content2); // 转 SaResult 对象 SaResult res2 = new SaResult().setMap(map2); Assertions.assertEquals(res2.getCode(), 200); } // 封装请求 private SaResult request(String path) { try { // 发请求 MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post(path) .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) .header("div", "val") ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); // 转 Map String content = mvcResult.getResponse().getContentAsString(); Map map = SaManager.getSaJsonTemplate().jsonToMap(content); // 转 SaResult 对象 return new SaResult().setMap(map); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.router; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** * 路由鉴权测试 * * @author click33 * */ @RestController @RequestMapping("/rt/") public class RouterController { @RequestMapping("getInfo") public SaResult getInfo() { return SaResult.ok(); } @RequestMapping("getInfo*") public SaResult getInfo2() { return SaResult.ok(); } // 读url @RequestMapping("getInfo_101") public SaResult getInfo_101() { return SaResult.data(SaHolder.getRequest().getUrl()); } // 读Cookie @RequestMapping("getInfo_102") public SaResult getInfo_102() { return SaResult.data(SaHolder.getRequest().getCookieValue("x-token")); } // 测试转发 @RequestMapping("getInfo_103") public SaResult getInfo_103() { SaHolder.getRequest().forward("/rt/getInfo_102"); return SaResult.ok(); } // 空接口 @RequestMapping("getInfo_200") public SaResult getInfo_200() { return SaResult.ok(); } @RequestMapping("getInfo_201") public SaResult getInfo_201() { return SaResult.ok(); } @RequestMapping("getInfo_202") public SaResult getInfo_202() { return SaResult.ok(); } @RequestMapping("login") public SaResult login(long id) { StpUtil.login(id); return SaResult.ok().set("token", StpUtil.getTokenValue()); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterControllerTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.router; import java.util.Arrays; import java.util.Map; import javax.servlet.http.Cookie; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.integrate.StartUpApplication; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.router.SaRouterStaff; import cn.dev33.satoken.servlet.model.SaRequestForServlet; import cn.dev33.satoken.spring.SaTokenContextForSpring; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; /** * C Controller 测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) public class RouterControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; // 开始 @BeforeEach public void before() { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); // 在单元测试时,通过 request.getServletPath() 获取到的请求路径为空,导致路由拦截不正确 // 虽然不知道为什么会这样,但是暂时可以通过以下方式来解决 SaManager.setSaTokenContext(new SaTokenContextForSpring() { @Override public SaRequest getRequest() { return new SaRequestForServlet(SpringMVCUtil.getRequest()) { @Override public String getRequestPath() { return request.getRequestURI(); } }; } }); } // 基础API测试 @Test public void testApi() { // 是否命中 SaRouterStaff staff = SaRouter.match(false); Assertions.assertFalse(staff.isHit()); // 重置 staff.reset(); Assertions.assertTrue(staff.isHit()); // lambda 形式 SaRouterStaff staff2 = SaRouter.match(r -> false); Assertions.assertFalse(staff2.isHit()); // 匹配 Assertions.assertTrue(SaRouter.isMatch("/user/**", "/user/add")); Assertions.assertTrue(SaRouter.isMatch(new String[] {"/user/**", "/art/**", "/goods/**"}, "/art/delete")); Assertions.assertTrue(SaRouter.isMatch(Arrays.asList("/user/**", "/art/**", "/goods/**"), "/art/delete")); Assertions.assertTrue(SaRouter.isMatch(new String[] {"POST", "GET", "PUT"}, "GET")); // 不匹配的 Assertions.assertTrue(SaRouter.notMatch(false).isHit()); Assertions.assertTrue(SaRouter.notMatch(r -> false).isHit()); } // 各种路由测试 @Test public void testRouter() { // getInfo SaResult res = request("/rt/getInfo?name=zhang"); Assertions.assertEquals(res.getCode(), 201); // getInfo2 SaResult res2 = request("/rt/getInfo2"); Assertions.assertEquals(res2.getCode(), 202); // getInfo3 SaResult res3 = request("/rt/getInfo3"); Assertions.assertEquals(res3.getCode(), 203); // getInfo4 SaResult res4 = request("/rt/getInfo4"); Assertions.assertEquals(res4.getCode(), 204); // getInfo5 SaResult res5 = request("/rt/getInfo5"); Assertions.assertEquals(res5.getCode(), 205); // getInfo6 SaResult res6 = request("/rt/getInfo6"); Assertions.assertEquals(res6.getCode(), 206); // getInfo7 SaResult res7 = request("/rt/getInfo7"); Assertions.assertEquals(res7.getCode(), 200); // getInfo8 SaResult res8 = request("/rt/getInfo8"); Assertions.assertEquals(res8.getCode(), 200); // getInfo9 SaResult res9 = request("/rt/getInfo9"); Assertions.assertEquals(res9.getCode(), 209); // getInfo10 SaResult res10 = request("/rt/getInfo10"); Assertions.assertEquals(res10.getCode(), 200); // getInfo11 SaResult res11 = request("/rt/getInfo11"); Assertions.assertEquals(res11.getCode(), 211); // getInfo12 SaResult res12 = request("/rt/getInfo12"); Assertions.assertEquals(res12.getCode(), 212); // getInfo13 SaResult res13 = request("/rt/getInfo13"); Assertions.assertEquals(res13.getCode(), 213); // getInfo14 SaResult res14 = request("/rt/getInfo14"); Assertions.assertEquals(res14.getCode(), 214); // getInfo15 SaResult res15 = request("/rt/getInfo15"); Assertions.assertEquals(res15.getCode(), 215); } // 测试 getUrl() @Test public void testGetUrl() { // getInfo_101 SaResult res = request("/rt/getInfo_101"); Assertions.assertTrue(res.getData().toString().endsWith("/rt/getInfo_101")); // getInfo_101,不包括后面的参数 SaResult res2 = request("/rt/getInfo_101?id=1"); Assertions.assertTrue(res2.getData().toString().endsWith("/rt/getInfo_101")); // 自定义当前域名 SaManager.getConfig().setCurrDomain("http://xxx.com"); SaResult res3 = request("/rt/getInfo_101?id=1"); Assertions.assertEquals(res3.getData().toString(), "http://xxx.com/rt/getInfo_101"); SaManager.getConfig().setCurrDomain(null); } // 测试读取Cookie @Test public void testGetCookie() throws Exception { MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post("/rt/getInfo_102") .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) .cookie(new Cookie("x-token", "token-111")) ) .andExpect(MockMvcResultMatchers.status().is(200)) .andReturn(); // 转 Map String content = mvcResult.getResponse().getContentAsString(); Map map = SaManager.getSaJsonTemplate().jsonToMap(content); // 转 SaResult 对象 SaResult res = new SaResult().setMap(map); Assertions.assertEquals(res.getData(), "token-111"); } // 测试重定向 @Test public void testRedirect() throws Exception { MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post("/rt/getInfo16") .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) ) .andExpect(MockMvcResultMatchers.status().is(302)) .andReturn(); Assertions.assertEquals(mvcResult.getResponse().getHeader("Location"), "/rt/getInfo3"); } // 空接口 @Test public void testGetInfo200() { // SaResult res = request("/rt/getInfo_200"); // Assertions.assertEquals(res.getCode(), 200); // SaResult res1 = request("/rt/getInfo_201"); // Assertions.assertEquals(res1.getCode(), 201); // SaResult res2 = request("/rt/getInfo_202"); // Assertions.assertEquals(res2.getCode(), 401); // 登录拿到Token SaResult resLogin = request("/rt/login?id=10001"); String satoken = resLogin.get("token", String.class); SaResult res3 = request("/rt/getInfo_202?satoken=" + satoken); Assertions.assertEquals(res3.getCode(), 200); } // 测试转发 @Test public void testForward() { SaResult res = request("/rt/getInfo_103"); Assertions.assertEquals(res.getCode(), 200); } // 封装请求 private SaResult request(String path) { try { // 发请求 MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post(path) .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); // 转 Map String content = mvcResult.getResponse().getContentAsString(); Map map = SaManager.getSaJsonTemplate().jsonToMap(content); // 转 SaResult 对象 return new SaResult().setMap(map); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/SaTokenConfigure2.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.router; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; import org.junit.jupiter.api.Assertions; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Arrays; /** * Sa-Token 相关配置类 * * @author click33 * @since 2022-9-2 */ @Configuration public class SaTokenConfigure2 implements WebMvcConfigurer { // 路由鉴权 @Override public void addInterceptors(InterceptorRegistry registry) { // 测试环境下上下文过滤器不生效,所以此处从拦截器需要补充上下文 registry.addInterceptor(new SaInterceptor(handle -> { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); }).isAnnotation(false)).addPathPatterns("/**"); // 路由鉴权 registry.addInterceptor(new SaInterceptor(handle -> {}) .isAnnotation(true) .setAuth(handle -> { // 匹配 getInfo ,返回code=201 SaRouter.match("/**") .match(SaHttpMethod.POST) .matchMethod("POST") .match(SaHolder.getRequest().getMethod().equals("POST")) .match(r -> SaHolder.getRequest().isPath("/rt/getInfo")) .match(r -> SaHolder.getRequest().isParam("name", "zhang")) .back(SaResult.code(201)); // 匹配 getInfo2 ,返回code=202 SaRouter.match("/rt/getInfo2") .match(Arrays.asList("/rt/getInfo2", "/rt/*")) .notMatch("/rt/getInfo3") .notMatch(false) .notMatch(r -> false) .notMatch(SaHttpMethod.GET) .notMatchMethod("PUT") .notMatch(Arrays.asList("/rt/getInfo4", "/rt/getInfo5")) .back(SaResult.code(202)); // 匹配 getInfo3 ,返回code=203 SaRouter.match("/rt/getInfo3", "/rt/getInfo4", () -> SaRouter.back(SaResult.code(203))); SaRouter.match("/rt/getInfo4", "/rt/getInfo5", r -> SaRouter.back(SaResult.code(204))); SaRouter.match("/rt/getInfo5", () -> SaRouter.back(SaResult.code(205))); SaRouter.match("/rt/getInfo6", r -> SaRouter.back(SaResult.code(206))); // 通往 Controller SaRouter.match(Arrays.asList("/rt/getInfo7")).stop(); // 通往 Controller SaRouter.match("/rt/getInfo8", () -> SaRouter.stop()); SaRouter.matchMethod("POST").match("/rt/getInfo9").free(r -> SaRouter.back(SaResult.code(209))); SaRouter.match(SaHttpMethod.POST).match("/rt/getInfo10").setHit(false).back(); // 11 SaRouter.notMatch("/rt/getInfo11").reset().match("/rt/getInfo11").back(SaResult.code(211)); SaRouter.notMatch(SaHttpMethod.GET).match("/rt/getInfo12").back(SaResult.code(212)); SaRouter.notMatch(Arrays.asList("/rt/getInfo12", "/rt/getInfo14")).match("/rt/getInfo13").back(SaResult.code(213)); SaRouter.notMatchMethod("GET", "PUT").match("/rt/getInfo14").back(SaResult.code(214)); // SaRouter.match(Arrays.asList("/rt/getInfo15", "/rt/getInfo16")) if(SaRouter.isMatchCurrURI("/rt/getInfo15")) { if(SaHolder.getRequest().getCookieValue("ddd") == null && SaHolder.getStorage().getSource() == SpringMVCUtil.getRequest() && SaHolder.getRequest().getSource() == SpringMVCUtil.getRequest() && SaHolder.getResponse().getSource() == SpringMVCUtil.getResponse() ) { SaRouter.newMatch().free(r -> SaRouter.back(SaResult.code(215))); } } SaRouter.match("/rt/getInfo16", () -> { Assertions.assertThrows(Exception.class, () -> SaHolder.getResponse().redirect(null)); SaHolder.getResponse().redirect("/rt/getInfo3"); }); })).addPathPatterns("/**"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenController.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.same; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.util.SaResult; /** * same-token Controller * * @author click33 * */ @RestController @RequestMapping("/same/") public class SaSameTokenController { // 获取信息 @RequestMapping("getInfo") public SaResult getInfo() { // 获取并校验same-token String sameToken = SpringMVCUtil.getRequest().getHeader(SaSameUtil.SAME_TOKEN); SaSameUtil.checkToken(sameToken); // 返回信息 return SaResult.data("info=zhangsan"); } // 获取信息2 @RequestMapping("getInfo2") public SaResult getInfo2() { // 获取并校验same-token SaSameUtil.checkCurrentRequestToken(); // 返回信息 return SaResult.data("info=zhangsan2"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenControllerTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.integrate.same; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.SameTokenInvalidException; import cn.dev33.satoken.integrate.StartUpApplication; import cn.dev33.satoken.same.SaSameUtil; import cn.dev33.satoken.util.SaResult; /** * same-token Controller 测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) public class SaSameTokenControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; // 开始 @BeforeEach public void before() { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); } // 获取信息 @Test public void testGetInfo() { String token = SaSameUtil.getToken(); // 加token,能调通 SaResult res = request("/same/getInfo", token); Assertions.assertEquals(res.getCode(), 200); // 不加token,不能调通 SaResult res2 = request("/same/getInfo", "xxx"); Assertions.assertEquals(res2.getCode(), 902); // 获取信息2 token = SaSameUtil.getTokenNh(); // 加token,能调通 SaResult res3 = request("/same/getInfo2", token); Assertions.assertEquals(res3.getCode(), 200); // 不加token,不能调通 SaResult res4 = request("/same/getInfo2", "xxx"); Assertions.assertEquals(res4.getCode(), 902); } // 基础测试 @Test public void testApi() { String token = SaSameUtil.getToken(); // 刷新一下,会有变化 SaSameUtil.refreshToken(); String token2 = SaSameUtil.getToken(); Assertions.assertNotEquals(token, token2); // 旧token,变为次级token String pastToken = SaSameUtil.getPastTokenNh(); Assertions.assertEquals(token, pastToken); // dao中应该有值 String daoToken = SaManager.getSaTokenDao().get("satoken:var:same-token"); String daoToken2 = SaManager.getSaTokenDao().get("satoken:var:past-same-token"); Assertions.assertEquals(token2, daoToken); Assertions.assertEquals(token, daoToken2); // 新旧都有效 Assertions.assertTrue(SaSameUtil.isValid(token)); Assertions.assertTrue(SaSameUtil.isValid(token2)); // 空的不行 Assertions.assertFalse(SaSameUtil.isValid(null)); Assertions.assertFalse(SaSameUtil.isValid("")); // 不抛出异常 Assertions.assertDoesNotThrow(() -> SaSameUtil.checkToken(token)); Assertions.assertDoesNotThrow(() -> SaSameUtil.checkToken(token2)); // 抛出异常 Assertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken(null)); Assertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken("")); Assertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken("aaa")); } // 封装请求 private SaResult request(String path, String sameToken) { try { // 发请求 MvcResult mvcResult = mvc.perform( MockMvcRequestBuilders.post(path) .contentType(MediaType.APPLICATION_PROBLEM_JSON) .accept(MediaType.APPLICATION_PROBLEM_JSON) .header(SaSameUtil.SAME_TOKEN, sameToken) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); // 转 Map String content = mvcResult.getResponse().getContentAsString(); Map map = SaManager.getSaJsonTemplate().jsonToMap(content); // 转 SaResult 对象 return new SaResult().setMap(map); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/BasicsTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.springboot; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.*; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.json.SaJsonTemplate; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder; import cn.dev33.satoken.stp.SaLoginConfig; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.util.SaTokenConsts; import cn.dev33.satoken.util.SoMap; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockFilterChain; import org.springframework.util.PathMatcher; import javax.servlet.ServletException; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; /** * Sa-Token 基础API测试 * *

    注解详解参考: https://www.cnblogs.com/flypig666/p/11505277.html * @author Auster * */ @SpringBootTest(classes = StartUpApplication.class) public class BasicsTest { // 持久化Bean @Autowired(required = false) SaTokenDao dao = SaManager.getSaTokenDao(); @Autowired PathMatcher pathMatcher; // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ 基础测试 start ..."); SaManager.getConfig().setActiveTimeout(180); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ 基础测试 end ... \n"); } @BeforeEach public void beforeEach() { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); } // 结束 @AfterEach public void afterEach() { SaTokenContextServletUtil.clearContext(); } // 测试:基础API @Test public void testBasicsApi() { // 基本API Assertions.assertEquals(StpUtil.getLoginType(), "login"); Assertions.assertEquals(StpUtil.getStpLogic(), SaManager.getStpLogic("login")); Assertions.assertEquals(StpUtil.getTokenName(), "satoken"); // 安全的更新 StpUtil 的 StpLogic 对象 StpLogic loginStpLogic = new StpLogic("login"); StpUtil.setStpLogic(loginStpLogic); Assertions.assertEquals(StpUtil.getStpLogic(), loginStpLogic); Assertions.assertEquals(SaManager.getStpLogic("login"), loginStpLogic); } // 测试:登录 @Test public void testDoLogin() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); // token 存在 Assertions.assertNotNull(token); Assertions.assertEquals(token, StpUtil.getTokenValueNotCut()); Assertions.assertEquals(token, StpUtil.getTokenValueByLoginId(10001)); Assertions.assertEquals(token, StpUtil.getTokenValueByLoginId(10001, SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE)); // token 队列 List tokenList = StpUtil.getTokenValueListByLoginId(10001); List tokenList2 = StpUtil.getTokenValueListByLoginId(10001, SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); Assertions.assertEquals(token, tokenList.get(tokenList.size() - 1)); Assertions.assertEquals(token, tokenList2.get(tokenList.size() - 1)); // API 验证 Assertions.assertTrue(StpUtil.isLogin()); Assertions.assertDoesNotThrow(() -> StpUtil.checkLogin()); Assertions.assertNotNull(token); // token不为null Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginIdAsInt(), 10001); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginIdAsString(), "10001"); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginId(), "10001"); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginIdDefaultNull(), "10001"); // loginId=10001 Assertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE); // 登录设备类型 // db数据 验证 // token存在 Assertions.assertEquals(dao.get("satoken:login:token:" + token), "10001"); // Session 存在 SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNotNull(session); Assertions.assertEquals(session.getId(), "satoken:login:session:" + 10001); Assertions.assertTrue(session.getTerminalList().size() >= 1); } // 测试:注销 @Test public void testLogout() { // 登录 StpUtil.login(10001); String token = StpUtil.getTokenValue(); Assertions.assertEquals(dao.get("satoken:login:token:" + token), "10001"); // 注销 StpUtil.logout(); // token 应该被清除 Assertions.assertNull(StpUtil.getTokenValue()); Assertions.assertFalse(StpUtil.isLogin()); Assertions.assertNull(dao.get("satoken:login:token:" + token)); // 全部客户端注销掉 StpUtil.logout(10001); // Session 应该被清除 SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNull(session); // 在调用 getLoginId() 就会抛出异常 Assertions.assertEquals(StpUtil.getLoginId("无值"), "无值"); Assertions.assertThrows(NotLoginException.class, () -> StpUtil.getLoginId()); } // 测试:Session会话 @Test public void testSession() { StpUtil.login(10001); // API 应该可以获取 Session Assertions.assertNotNull(StpUtil.getSession()); Assertions.assertNotNull(StpUtil.getSession(false)); // db中应该存在 Session SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNotNull(session); // 存取值 session.set("name", "zhang"); session.set("age", "18"); Assertions.assertEquals(session.get("name"), "zhang"); Assertions.assertEquals(session.getInt("age"), 18); Assertions.assertEquals((int)session.getModel("age", int.class), 18); Assertions.assertEquals((int)session.get("age", 20), 18); Assertions.assertEquals((int)session.get("name2", 20), 20); Assertions.assertEquals((int)session.get("name2", () -> 30), 30); session.clear(); Assertions.assertEquals(session.get("name"), null); } // 测试:权限认证 @Test public void testCheckPermission() { StpUtil.login(10001); // 获取权限 List permissionList = StpUtil.getPermissionList(); List permissionList2 = StpUtil.getPermissionList(10001); Assertions.assertEquals(permissionList.size(), permissionList2.size()); // 权限校验 Assertions.assertTrue(StpUtil.hasPermission("user-add")); Assertions.assertTrue(StpUtil.hasPermission("user-list")); Assertions.assertTrue(StpUtil.hasPermission("user")); Assertions.assertTrue(StpUtil.hasPermission("art-add")); Assertions.assertFalse(StpUtil.hasPermission("get-user")); // and Assertions.assertTrue(StpUtil.hasPermissionAnd("art-add", "art-get")); Assertions.assertFalse(StpUtil.hasPermissionAnd("art-add", "comment-add")); // or Assertions.assertTrue(StpUtil.hasPermissionOr("art-add", "comment-add")); Assertions.assertFalse(StpUtil.hasPermissionOr("comment-add", "comment-delete")); // more Assertions.assertTrue(StpUtil.hasPermission(10001, "user-add")); Assertions.assertFalse(StpUtil.hasPermission(10002, "user-add")); // 抛异常 Assertions.assertThrows(NotPermissionException.class, () -> StpUtil.checkPermission("goods-add")); Assertions.assertThrows(NotPermissionException.class, () -> StpUtil.checkPermissionAnd("goods-add", "art-add")); // 不抛异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkPermission("user-add")); Assertions.assertDoesNotThrow(() -> StpUtil.checkPermissionAnd("art-get", "art-add")); Assertions.assertDoesNotThrow(() -> StpUtil.checkPermissionOr("goods-add", "art-add")); } // 测试:角色认证 @Test public void testCheckRole() { StpUtil.login(10001); // 获取角色 List roleList = StpUtil.getRoleList(); List roleList2 = StpUtil.getRoleList(10001); Assertions.assertEquals(roleList.size(), roleList2.size()); // 角色校验 Assertions.assertTrue(StpUtil.hasRole("admin")); Assertions.assertFalse(StpUtil.hasRole("teacher")); // and Assertions.assertTrue(StpUtil.hasRoleAnd("admin", "super-admin")); Assertions.assertFalse(StpUtil.hasRoleAnd("admin", "ceo")); // or Assertions.assertTrue(StpUtil.hasRoleOr("admin", "ceo")); Assertions.assertFalse(StpUtil.hasRoleOr("ceo", "cto")); // more Assertions.assertTrue(StpUtil.hasRole(10001, "admin")); Assertions.assertFalse(StpUtil.hasRole(10002, "admin2")); // 抛异常 Assertions.assertThrows(NotRoleException.class, () -> StpUtil.checkRole("ceo")); Assertions.assertThrows(NotRoleException.class, () -> StpUtil.checkRoleAnd("ceo", "admin")); // 不抛异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkRole("admin")); Assertions.assertDoesNotThrow(() -> StpUtil.checkRoleAnd("admin", "super-admin")); Assertions.assertDoesNotThrow(() -> StpUtil.checkRoleOr("ceo", "admin")); } // 测试:根据token强制注销 @Test public void testLogoutByToken() { StpUtil.logout(10001); // 先登录上 StpUtil.login(10001); Assertions.assertTrue(StpUtil.isLogin()); String token = StpUtil.getTokenValue(); // 根据token注销 StpUtil.logoutByTokenValue(token); Assertions.assertFalse(StpUtil.isLogin()); // token 应该被清除 Assertions.assertNull(dao.get("satoken:login:token:" + token)); // Session 应该被清除 SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNull(session); // 场景值应该是token无效 try { StpUtil.checkLogin(); } catch (NotLoginException e) { Assertions.assertEquals(e.getType(), NotLoginException.INVALID_TOKEN); } // 根据token踢下线 StpUtil.login(10001); StpUtil.kickoutByTokenValue(StpUtil.getTokenValue()); // 场景值应该是被踢下线 try { StpUtil.checkLogin(); } catch (NotLoginException e) { Assertions.assertEquals(e.getType(), NotLoginException.KICK_OUT); } } // 测试:根据账号id强制注销 @Test public void testLogoutByLoginId() { // 先登录上 StpUtil.login(10001); Assertions.assertTrue(StpUtil.isLogin()); String token = StpUtil.getTokenValue(); // 根据账号id注销 StpUtil.logout(10001); Assertions.assertFalse(StpUtil.isLogin()); // token 应该被清除 Assertions.assertNull(dao.get("satoken:login:token:" + token)); // Session 应该被清除 SaSession session = dao.getSession("satoken:login:session:" + 10001); Assertions.assertNull(session); // 场景值应该是token无效 try { StpUtil.checkLogin(); } catch (NotLoginException e) { Assertions.assertEquals(e.getType(), NotLoginException.INVALID_TOKEN); } } // 测试Token-Session @Test public void testTokenSession() { // 先登录上 StpUtil.login(10001); String token = StpUtil.getTokenValue(); // 刚开始不存在 Assertions.assertNull(StpUtil.stpLogic.getTokenSession(false)); SaSession session = dao.getSession("satoken:login:token-session:" + token); Assertions.assertNull(session); // 调用一次就存在了 StpUtil.getTokenSession(); Assertions.assertNotNull(StpUtil.stpLogic.getTokenSession(false)); SaSession session2 = dao.getSession("satoken:login:token-session:" + token); Assertions.assertNotNull(session2); // SaSession tokenSession = StpUtil.getTokenSession(); SaSession tokenSession2 = StpUtil.getTokenSessionByToken(token); Assertions.assertEquals(tokenSession.getId(), tokenSession2.getId()); } // 测试:根据账号id踢人 @Test public void kickoutByLoginId() { // 踢人下线 StpUtil.login(10001); String token = StpUtil.getTokenValue(); StpUtil.kickout(10001); // token 应该被打标记 Assertions.assertEquals(dao.get("satoken:login:token:" + token), NotLoginException.KICK_OUT); // 场景值应该是token已被踢下线 try { StpUtil.checkLogin(); } catch (NotLoginException e) { Assertions.assertEquals(e.getType(), NotLoginException.KICK_OUT); } } // 测试:账号封禁 @Test public void testDisable() { // 封号 StpUtil.disable(10007, 200); Assertions.assertTrue(StpUtil.isDisable(10007)); Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); // 封号后检测一下 (会抛出 DisableLoginException 异常) Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10007)); // 封号时间 long disableTime = StpUtil.getDisableTime(10007); Assertions.assertTrue(disableTime <= 200 && disableTime >= 199); // 解封 StpUtil.untieDisable(10007); Assertions.assertFalse(StpUtil.isDisable(10007)); Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10007), null); Assertions.assertDoesNotThrow(() -> StpUtil.checkDisable(10007)); } // 测试:分类封禁 @Test public void testDisableService() { // 封掉评论功能 StpUtil.disable(10008, "comment", 200); Assertions.assertTrue(StpUtil.isDisable(10008, "comment")); Assertions.assertEquals(dao.get("satoken:login:disable:comment:" + 10008), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); Assertions.assertNull(dao.get("satoken:login:disable:login:" + 10008)); // 封号后检测一下 Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10008, "comment")); // 检查多个,有一个不通过就报异常 Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10008, "comment", "login")); // 封号时间 long disableTime = StpUtil.getDisableTime(10008, "comment"); Assertions.assertTrue(disableTime <= 200 && disableTime >= 199); // 解封 (不加服务名不会成功) StpUtil.untieDisable(10008); Assertions.assertTrue(StpUtil.isDisable(10008, "comment")); Assertions.assertNotNull(dao.get("satoken:login:disable:comment:" + 10008)); // 解封 (加服务名才会成功) StpUtil.untieDisable(10008, "comment"); Assertions.assertFalse(StpUtil.isDisable(10008, "comment")); Assertions.assertEquals(dao.get("satoken:login:disable:comment:" + 10008), null); Assertions.assertDoesNotThrow(() -> StpUtil.checkDisable(10007, "comment")); } // 测试:阶梯封禁 @Test public void testDisableLevel() { // 封禁等级5 StpUtil.disableLevel(10009, 5, 200); Assertions.assertTrue(StpUtil.isDisableLevel(10009, 3)); Assertions.assertTrue(StpUtil.isDisableLevel(10009, 5)); // 未达到7级 Assertions.assertFalse(StpUtil.isDisableLevel(10009, 7)); // 账号未封禁 Assertions.assertFalse(StpUtil.isDisableLevel(20009, 3)); // dao中应该有值 Assertions.assertEquals(dao.get("satoken:login:disable:login:" + 10009), String.valueOf(5)); // 封号后检测一下 Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10009, 3)); Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10009, 5)); // 未达到等级,不抛出异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(10009, 7)); // 账号未被封禁,不抛出异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(20009, 3)); // 封号等级 Assertions.assertEquals(StpUtil.getDisableLevel(10009), 5); Assertions.assertEquals(StpUtil.getDisableLevel(20009), -2); // 解封 StpUtil.untieDisable(10009); Assertions.assertFalse(StpUtil.isDisable(10009)); Assertions.assertFalse(StpUtil.isDisableLevel(10009, 5)); Assertions.assertNull(dao.get("satoken:login:disable:login:" + 10009)); } // 测试:分类封禁 + 阶梯封禁 @Test public void testDisableServiceLevel() { // 封禁服务 shop,等级5 StpUtil.disableLevel(10010, "shop", 5, 200); Assertions.assertTrue(StpUtil.isDisableLevel(10010, "shop", 3)); Assertions.assertTrue(StpUtil.isDisableLevel(10010, "shop", 5)); // 未达到7级 Assertions.assertFalse(StpUtil.isDisableLevel(10010, "shop", 7)); // 账号未封禁 Assertions.assertFalse(StpUtil.isDisableLevel(20010, "shop", 3)); // 服务名不对 Assertions.assertFalse(StpUtil.isDisableLevel(10010, "shop2", 5)); // dao中应该有值 Assertions.assertEquals(dao.get("satoken:login:disable:shop:" + 10010), String.valueOf(5)); // 封号后检测一下 Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10010, "shop", 3)); Assertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10010, "shop", 5)); // 未达到等级,不抛出异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(10010, "shop", 7)); // 账号未被封禁,不抛出异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(20010, "shop", 3)); // 封号等级 Assertions.assertEquals(StpUtil.getDisableLevel(10010, "shop"), 5); Assertions.assertEquals(StpUtil.getDisableLevel(10010, "shop2"), -2); Assertions.assertEquals(StpUtil.getDisableLevel(20010, "shop"), -2); // 解封 StpUtil.untieDisable(10010, "shop"); Assertions.assertFalse(StpUtil.isDisable(10010, "shop")); Assertions.assertFalse(StpUtil.isDisableLevel(10010, "shop", 5)); Assertions.assertNull(dao.get("satoken:login:disable:shop:" + 10010)); } // 测试:身份切换 @Test public void testSwitch() { // 登录 StpUtil.login(10001); Assertions.assertFalse(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); // 开始身份切换 StpUtil.switchTo(10044); Assertions.assertTrue(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10044); // 开始身份切换 Lambda 方式 StpUtil.switchTo(10045, () -> { Assertions.assertTrue(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10045); }); // 结束切换 StpUtil.endSwitch(); Assertions.assertFalse(StpUtil.isSwitch()); Assertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001); } // 测试:会话管理 @Test public void testSearchTokenValue() { // 登录 StpUtil.login(10001); StpUtil.login(10002); StpUtil.login(10003); StpUtil.login(10004); StpUtil.login(10005); // 查询 Token 列表 List list = StpUtil.searchTokenValue("", 0, 10, true); Assertions.assertTrue(list.size() >= 5); // 查询 Session 列表 List list2 = StpUtil.searchSessionId("", 0, 10, true); Assertions.assertTrue(list2.size() >= 5); list2.stream().forEach(sessionId -> { Assertions.assertNotNull(StpUtil.getSessionBySessionId(sessionId)); }); } // 测试:会话管理(Token-Session) @Test public void testSearchTokenSession() { // 登录 StpUtil.login(10001); StpUtil.getTokenSession(); StpUtil.login(10002); StpUtil.getTokenSession(); StpUtil.login(10003); StpUtil.getTokenSession(); StpUtil.login(10004); StpUtil.getTokenSession(); StpUtil.login(10005); StpUtil.getTokenSession(); // 查询 Token-Session 列表 List list2 = StpUtil.searchTokenSessionId("", 0, 10, true); Assertions.assertTrue(list2.size() >= 5); list2.stream().forEach(sessionId -> { Assertions.assertNotNull(StpUtil.getSessionBySessionId(sessionId)); }); } // 测试:二级认证 @Test public void testSafe() { // 登录 StpUtil.login(10001); Assertions.assertFalse(StpUtil.isSafe()); // 开启二级认证 StpUtil.openSafe(2); Assertions.assertTrue(StpUtil.isSafe()); Assertions.assertTrue(StpUtil.getSafeTime() > 0); StpUtil.checkSafe(); // 自然结束 // Thread.sleep(2500); // Assertions.assertFalse(StpUtil.isSafe()); // 手动结束 // StpUtil.openSafe(2); StpUtil.closeSafe(); Assertions.assertFalse(StpUtil.isSafe()); // 抛异常 Assertions.assertThrows(NotSafeException.class, () -> StpUtil.checkSafe()); } // ------------- 复杂点的 // 测试:指定设备登录 @Test public void testDoLoginByDevice() { StpUtil.login(10001, "PC"); Assertions.assertEquals(StpUtil.getLoginDevice(), "PC"); // 指定一个其它的设备注销,应该注销不掉 StpUtil.logout(10001, "APP"); Assertions.assertTrue(StpUtil.isLogin()); // 指定当前设备踢掉,则能够踢掉 StpUtil.kickout(10001, "PC"); Assertions.assertFalse(StpUtil.isLogin()); // 顶掉 StpUtil.login(10001, "PC"); StpUtil.replaced(10001, "PC"); Assertions.assertFalse(StpUtil.isLogin()); try { StpUtil.checkLogin(); } catch (NotLoginException e) { // 场景值应该为-4 Assertions.assertEquals(e.getType(), NotLoginException.BE_REPLACED); } } // 测试:指定 timeout 登录 @Test public void testDoLoginByTimeout() { // 指定timeout 登录 StpUtil.login(10001, 100); long timeout = StpUtil.getTokenTimeout(); Assertions.assertTrue(timeout <= 100 && timeout >= 99); // 续期一下 StpUtil.renewTimeout(200); timeout = StpUtil.getTokenTimeout(); Assertions.assertTrue(timeout <= 200 && timeout >= 199); // 续期一下 StpUtil.renewTimeout(StpUtil.getTokenValue(), 300); timeout = StpUtil.getTokenTimeout(); Assertions.assertTrue(timeout <= 300 && timeout >= 299); // Session 也会续期 timeout = StpUtil.getSessionTimeout(); Assertions.assertTrue(timeout >= 299); StpUtil.getTokenSession(); timeout = StpUtil.getTokenSessionTimeout(); Assertions.assertTrue(timeout >= 299); // 注销后,就是-2 StpUtil.logout(); timeout = StpUtil.getTokenTimeout(); Assertions.assertTrue(timeout == SaTokenDao.NOT_VALUE_EXPIRE); } // 测试:预定 Token 登录 @Test public void testDoLoginBySetToken() { // 预定 Token 登录 StpUtil.login(10001, new SaLoginParameter().setToken("qwer-qwer-qwer-qwer")); Assertions.assertEquals(StpUtil.getTokenValue(), "qwer-qwer-qwer-qwer"); // 注销后,应该清除Token StpUtil.logout(); Assertions.assertNull(StpUtil.getTokenValue()); } // 测试:无上下文注入的登录 @Test public void testCreateLoginSession() { // 无上下文注入的登录 StpUtil.createLoginSession(10001); Assertions.assertNull(StpUtil.getTokenValue()); // 无上下文注入的登录 String token = StpUtil.createLoginSession(10001, new SaLoginParameter()); Assertions.assertNull(StpUtil.getTokenValue()); // 手动写入 StpUtil.setTokenValue(token); Assertions.assertNotNull(StpUtil.getTokenValue()); // 手动写入到Cookie StpUtil.setTokenValue(token, 10); Assertions.assertNotNull(StpUtil.getTokenValue()); } // 测试,匿名 Token-Session @Test public void testAnonTokenSession() { // token 不存在 StpUtil.logout(); Assertions.assertNull(StpUtil.getTokenValue()); // token 存在 SaSession anonTokenSession = StpUtil.getAnonTokenSession(); String token = StpUtil.getTokenValue(); Assertions.assertNotNull(token); // 写个值 anonTokenSession.set("code", "123456"); // 登录时,预定上 StpUtil.login(10001, SaLoginConfig.setToken(token)); // token不变 Assertions.assertEquals(token, StpUtil.getTokenValue()); // Token-Session 存在,且不变 SaSession tokenSession = StpUtil.getTokenSession(); Assertions.assertEquals(anonTokenSession.getId(), tokenSession.getId()); // 刚才写的值,仍然在 Assertions.assertEquals(tokenSession.get("code"), "123456"); } // 测试,token 最低活跃频率 @Test public void testActiveTimeout() { // 登录 StpUtil.login(10001); Assertions.assertNotNull(StpUtil.getTokenValue()); // 默认跟随全局 timeout StpUtil.updateLastActiveToNow(); long activeTimeout = StpUtil.getTokenActiveTimeout(); Assertions.assertTrue(activeTimeout <=180 || activeTimeout >=179); // 不会抛出异常 Assertions.assertDoesNotThrow(() -> StpUtil.checkActiveTimeout()); } // 测试,上下文 API @Test public void testSaTokenContext() { SaTokenContext context = SaHolder.getContext(); // path 匹配 // Assertions.assertTrue(context.matchPath("/user/**", "/user/add")); // context 是否有效 Assertions.assertTrue(context.isValid()); // 是否为web环境 Assertions.assertTrue(SpringMVCUtil.isWeb()); // pathMatcher // Assertions.assertEquals(pathMatcher, SaPathMatcherHolder.getPathMatcher()); // 自创建 SaPathMatcherHolder.pathMatcher = null; Assertions.assertNotNull(SaPathMatcherHolder.getPathMatcher()); SaPathMatcherHolder.pathMatcher = pathMatcher; } // 测试json转换 @Test public void testSaJsonTemplate() { SaJsonTemplate saJsonTemplate = SaManager.getSaJsonTemplate(); // map 转 json SoMap map = SoMap.getSoMap("name", "zhangsan"); String jsonString = saJsonTemplate.objectToJson(map); Assertions.assertEquals(jsonString, "{\"name\":\"zhangsan\"}"); // 抛异常 // Assertions.assertThrows(SaJsonConvertException.class, () -> saJsonTemplate.objectToJson(new Object())); // json 转 map Map map2 = saJsonTemplate.jsonToMap("{\"name\":\"zhangsan\"}"); Assertions.assertEquals(map2.get("name"), "zhangsan"); // 抛异常 Assertions.assertThrows(SaJsonConvertException.class, () -> saJsonTemplate.jsonToMap("x")); } // 测试过滤器、拦截器 基础API @Test public void testFilter() throws IOException, ServletException { // 过滤器 SaServletFilter filter = new SaServletFilter() .addInclude("/**") .setIncludeList(Arrays.asList("/**")) .addExclude("/favicon.ico") .setExcludeList(Arrays.asList("/favicon.ico")) .setAuth(obj -> {}) .setBeforeAuth(obj -> {}) ; Assertions.assertEquals(filter.includeList.get(0), "/**"); Assertions.assertEquals(filter.excludeList.get(0), "/favicon.ico"); // 以下功能无法测试 filter.init(null); filter.doFilter(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse(), new MockFilterChain()); filter.destroy(); Assertions.assertThrows(SaTokenException.class, () -> filter.error.run(new SaTokenException("xxx"))); filter.setError(e -> e.getMessage()); Assertions.assertEquals(filter.error.run(new SaTokenException("msg")), "msg"); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/ManyLoginTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.springboot; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.config.SaTokenConfig; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.servlet.util.SaTokenContextServletUtil; import cn.dev33.satoken.session.SaTerminalInfo; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; /** * Sa-Token 多端登录测试 * * @author click33 * */ @SpringBootTest(classes = StartUpApplication.class) public class ManyLoginTest { // 持久化Bean @Autowired(required = false) SaTokenDao dao = SaManager.getSaTokenDao(); // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n------------ 多端登录测试 star ..."); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n---------- 多端登录测试 end ... \n"); } @BeforeEach public void beforeEach() { SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse()); } @AfterEach public void afterEach() { SaTokenContextServletUtil.clearContext(); } // 测试:并发登录、共享token、同端 @Test public void login() { SaManager.setConfig(new SaTokenConfig().setIsShare(true)); StpUtil.login(10001); String token1 = StpUtil.getTokenValue(); StpUtil.login(10001); String token2 = StpUtil.getTokenValue(); Assertions.assertEquals(token1, token2); } // 测试:并发登录、共享token、不同端 @Test public void login2() { SaManager.setConfig(new SaTokenConfig()); StpUtil.login(10001, "APP"); String token1 = StpUtil.getTokenValue(); StpUtil.login(10001, "PC"); String token2 = StpUtil.getTokenValue(); Assertions.assertNotEquals(token1, token2); } // 测试:并发登录、不共享token @Test public void login3() { SaManager.setConfig(new SaTokenConfig().setIsShare(false)); StpUtil.login(10001); String token1 = StpUtil.getTokenValue(); StpUtil.login(10001); String token2 = StpUtil.getTokenValue(); Assertions.assertNotEquals(token1, token2); } // 测试:禁并发登录,后者顶出前者 @Test public void login4() { SaManager.setConfig(new SaTokenConfig().setIsConcurrent(false)); StpUtil.login(10001); String token1 = StpUtil.getTokenValue(); StpUtil.login(10001); String token2 = StpUtil.getTokenValue(); // token不同 Assertions.assertNotEquals(token1, token2); // token1会被标记为:已被顶下线 Assertions.assertEquals(dao.get("satoken:login:token:" + token1), "-4"); // Account-Session里的 token1 签名会被移除 List terminalList = StpUtil.getSessionByLoginId(10001).getTerminalList(); for (SaTerminalInfo terminal : terminalList) { Assertions.assertNotEquals(terminal.getTokenValue(), token1); } } // 测试:多端登录,一起强制注销 @Test public void login5() { SaManager.setConfig(new SaTokenConfig()); StpUtil.login(10001, "APP"); String token1 = StpUtil.getTokenValue(); StpUtil.login(10001, "PC"); String token2 = StpUtil.getTokenValue(); StpUtil.login(10001, "h5"); String token3 = StpUtil.getTokenValue(); // 注销 StpUtil.logout(10001); // 三个Token应该全部无效 Assertions.assertNull(dao.get("satoken:login:token:" + token1)); Assertions.assertNull(dao.get("satoken:login:token:" + token2)); Assertions.assertNull(dao.get("satoken:login:token:" + token3)); // Account-Session也应该被清除掉 Assertions.assertNull(StpUtil.getSessionByLoginId(10001, false)); Assertions.assertNull(dao.getSession("satoken:login:session:" + 10001)); } // 测试:多端登录,一起强制踢下线 @Test public void login6() { SaManager.setConfig(new SaTokenConfig()); StpUtil.login(10001, "APP"); String token1 = StpUtil.getTokenValue(); StpUtil.login(10001, "PC"); String token2 = StpUtil.getTokenValue(); StpUtil.login(10001, "h5"); String token3 = StpUtil.getTokenValue(); // 注销 StpUtil.kickout(10001); // 三个Token应该全部无效 Assertions.assertEquals(dao.get("satoken:login:token:" + token1), "-5"); Assertions.assertEquals(dao.get("satoken:login:token:" + token2), "-5"); Assertions.assertEquals(dao.get("satoken:login:token:" + token3), "-5"); // Account-Session也应该被清除掉 Assertions.assertNull(StpUtil.getSessionByLoginId(10001, false)); Assertions.assertNull(dao.getSession("satoken:login:session:" + 10001)); } // 测试:多账号模式,在一个账号体系里登录成功,在另一个账号体系不会校验通过 @Test public void login7() { SaManager.setConfig(new SaTokenConfig()); StpUtil.login(10001); String token1 = StpUtil.getTokenValue(); StpLogic stp = new StpLogic("user"); Assertions.assertNotNull(StpUtil.getLoginIdByToken(token1)); Assertions.assertNull(stp.getLoginIdByToken(token1)); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SaPathMatcherTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.springboot; import cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder; import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil; import cn.dev33.satoken.spring.pathmatch.SaPatternsRequestConditionHolder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootVersion; /** * SaPathMatcher 路由匹配测试 * * @author click33 * */ public class SaPathMatcherTest { // 开始 @BeforeAll public static void beforeClass() { } // 结束 @AfterAll public static void afterClass() { } // 测试,SaPathMatcherHolder @Test public void testSaPathMatcherHolder() { Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/get", "/user/get")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/*", "/user/get")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/**", "/user/get/list")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/**/page", "/user/get/list/page")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/get/{id}", "/user/get/123")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/get/{id}/page", "/user/get/123/page")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/*.js", "/sa.js")); Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match("/user/**/*.js", "/user/sa.js")); // SaPathMatcherHolder 无法匹配斜杠后缀 Assertions.assertFalse(SaPathMatcherHolder.getPathMatcher().match("/user/get", "/user/get/")); } // 测试,SaPatternsRequestConditionHolder @Test public void testSaPatternsRequestConditionHolder() { Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get", "/user/get")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/*", "/user/get")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/**", "/user/get/list")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/**/page", "/user/get/list/page")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get/{id}", "/user/get/123")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get/{id}/page", "/user/get/123/page")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/*.js", "/sa.js")); Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/**/*.js", "/user/sa.js")); // SaPatternsRequestConditionHolder 可匹配斜杠后缀 Assertions.assertTrue(SaPatternsRequestConditionHolder.match("/user/get", "/user/get/")); } // 测试,testSaPathPatternParserUtil @Test public void testSaPathPatternParserUtil() { Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get", "/user/get")); Assertions.assertTrue(SaPathPatternParserUtil.match("/user/*", "/user/get")); Assertions.assertTrue(SaPathPatternParserUtil.match("/user/**", "/user/get/list")); // PathPatternParser 不允许 ** 后面还有内容 // Assertions.assertTrue(SaPathPatternParserUtil.match("/user/**/page", "/user/get/list/page")); Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get/{id}", "/user/get/123")); Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get/{id}/page", "/user/get/123/page")); Assertions.assertTrue(SaPathPatternParserUtil.match("/*.js", "/sa.js")); // Assertions.assertTrue(SaPathPatternParserUtil.match("/user/**/*.js", "/user/sa.js")); // SaPathPatternParserUtil // 在 springboot2.x 版本下 可匹配斜杠后缀 // 在 springboot3.x 版本下 不可匹配斜杠后缀 if(SpringBootVersion.getVersion().startsWith("2.")) { Assertions.assertTrue(SaPathPatternParserUtil.match("/user/get", "/user/get/")); } if(SpringBootVersion.getVersion().startsWith("3.")) { Assertions.assertFalse(SaPathPatternParserUtil.match("/user/get", "/user/get/")); } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SpringMVCUtilTest.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.springboot; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.spring.SpringMVCUtil; /** * SpringMVCUtil 测试 * * @author click33 * */ public class SpringMVCUtilTest { // 开始 @BeforeAll public static void beforeClass() { } // 结束 @AfterAll public static void afterClass() { } // 测试,上下文 API @Test public void testSaTokenContext() { Assertions.assertThrows(SaTokenException.class, () -> SpringMVCUtil.getRequest()); Assertions.assertThrows(SaTokenException.class, () -> SpringMVCUtil.getResponse()); Assertions.assertFalse(SpringMVCUtil.isWeb()); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/StartUpApplication.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.springboot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动类 * @author Auster * */ @SpringBootApplication public class StartUpApplication { public static void main(String[] args) { SpringApplication.run(StartUpApplication.class, args); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/satoken/StpInterfaceImpl.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.springboot.satoken; import java.util.Arrays; import java.util.List; import org.springframework.stereotype.Component; import cn.dev33.satoken.stp.StpInterface; import cn.dev33.satoken.util.SaFoxUtil; /** * 自定义权限验证接口扩展 * * @author Auster * */ @Component public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List getPermissionList(Object loginId, String loginType) { int id = SaFoxUtil.getValueByType(loginId, int.class); if(id == 10001) { return Arrays.asList("user*", "art-add", "art-delete", "art-update", "art-get"); } else { return null; } } /** * 返回一个账号所拥有的角色标识集合 */ @Override public List getRoleList(Object loginId, String loginType) { int id = SaFoxUtil.getValueByType(loginId, int.class); if(id == 10001) { return Arrays.asList("admin", "super-admin"); } else { return null; } } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/util/SoMap.java ================================================ /* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.dev33.satoken.util; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.fasterxml.jackson.databind.ObjectMapper; /** * Map< String, Object> 是最常用的一种Map类型,但是它写着麻烦 *

    所以特封装此类,继承Map,进行一些扩展,可以让Map更灵活使用 *

    最新:2020-12-10 新增部分构造方法 * @author click33 */ public class SoMap extends LinkedHashMap { private static final long serialVersionUID = 1L; public SoMap() { } /** 以下元素会在isNull函数中被判定为Null, */ public static final Object[] NULL_ELEMENT_ARRAY = {null, ""}; public static final List NULL_ELEMENT_LIST; static { NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY); } // ============================= 读值 ============================= /** 获取一个值 */ @Override public Object get(Object key) { if("this".equals(key)) { return this; } return super.get(key); } /** 如果为空,则返回默认值 */ public Object get(Object key, Object defaultValue) { Object value = get(key); if(valueIsNull(value)) { return defaultValue; } return value; } /** 转为String并返回 */ public String getString(String key) { Object value = get(key); if(value == null) { return null; } return String.valueOf(value); } /** 如果为空,则返回默认值 */ public String getString(String key, String defaultValue) { Object value = get(key); if(valueIsNull(value)) { return defaultValue; } return String.valueOf(value); } /** 转为int并返回 */ public int getInt(String key) { Object value = get(key); if(valueIsNull(value)) { return 0; } return Integer.valueOf(String.valueOf(value)); } /** 转为int并返回,同时指定默认值 */ public int getInt(String key, int defaultValue) { Object value = get(key); if(valueIsNull(value)) { return defaultValue; } return Integer.valueOf(String.valueOf(value)); } /** 转为long并返回 */ public long getLong(String key) { Object value = get(key); if(valueIsNull(value)) { return 0; } return Long.valueOf(String.valueOf(value)); } /** 转为double并返回 */ public double getDouble(String key) { Object value = get(key); if(valueIsNull(value)) { return 0.0; } return Double.valueOf(String.valueOf(value)); } /** 转为boolean并返回 */ public boolean getBoolean(String key) { Object value = get(key); if(valueIsNull(value)) { return false; } return Boolean.valueOf(String.valueOf(value)); } /** 转为Date并返回,根据自定义格式 */ public Date getDateByFormat(String key, String format) { try { return new SimpleDateFormat(format).parse(getString(key)); } catch (Exception e) { throw new RuntimeException(e); } } /** 转为Date并返回,根据格式: yyyy-MM-dd */ public Date getDate(String key) { return getDateByFormat(key, "yyyy-MM-dd"); } /** 转为Date并返回,根据格式: yyyy-MM-dd HH:mm:ss */ public Date getDateTime(String key) { return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss"); } /** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */ @SuppressWarnings("unchecked") public List getList(String key) { Object value = get(key); List list = null; if(value == null || value.equals("")) { list = new ArrayList(); } else if(value instanceof List) { list = (List)value; } else { list = new ArrayList(); list.add(value); } return list; } /** 获取集合 (指定泛型类型) */ public List getList(String key, Class cs) { List list = getList(key); List list2 = new ArrayList(); for (Object obj : list) { T objC = getValueByClass(obj, cs); list2.add(objC); } return list2; } /** 获取集合(逗号分隔式),(指定类型) */ public List getListByComma(String key, Class cs) { String listStr = getString(key); if(listStr == null || listStr.equals("")) { return new ArrayList<>(); } // 开始转化 String [] arr = listStr.split(","); List list = new ArrayList(); for (String str : arr) { if(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) { str = str.trim(); } T objC = getValueByClass(str, cs); list.add(objC); } return list; } /** 根据指定类型从map中取值,返回实体对象 */ public T getModel(Class cs) { try { return getModelByObject(cs.newInstance()); } catch (Exception e) { throw new RuntimeException(e); } } /** 从map中取值,塞到一个对象中 */ public T getModelByObject(T obj) { // 获取类型 Class cs = obj.getClass(); // 循环复制 for (Field field : cs.getDeclaredFields()) { try { // 获取对象 Object value = this.get(field.getName()); if(value == null) { continue; } field.setAccessible(true); Object valueConvert = getValueByClass(value, field.getType()); field.set(obj, valueConvert); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException("属性取值出错:" + field.getName(), e); } } return obj; } /** * 将指定值转化为指定类型并返回 * @param obj * @param cs * @param * @return */ @SuppressWarnings("unchecked") public static T getValueByClass(Object obj, Class cs) { String obj2 = String.valueOf(obj); Object obj3 = null; if (cs.equals(String.class)) { obj3 = obj2; } else if (cs.equals(int.class) || cs.equals(Integer.class)) { obj3 = Integer.valueOf(obj2); } else if (cs.equals(long.class) || cs.equals(Long.class)) { obj3 = Long.valueOf(obj2); } else if (cs.equals(short.class) || cs.equals(Short.class)) { obj3 = Short.valueOf(obj2); } else if (cs.equals(byte.class) || cs.equals(Byte.class)) { obj3 = Byte.valueOf(obj2); } else if (cs.equals(float.class) || cs.equals(Float.class)) { obj3 = Float.valueOf(obj2); } else if (cs.equals(double.class) || cs.equals(Double.class)) { obj3 = Double.valueOf(obj2); } else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) { obj3 = Boolean.valueOf(obj2); } else { obj3 = (T)obj; } return (T)obj3; } // ============================= 写值 ============================= /** * 给指定key添加一个默认值(只有在这个key原来无值的情况先才会set进去) */ public void setDefaultValue(String key, Object defaultValue) { if(isNull(key)) { set(key, defaultValue); } } /** set一个值,连缀风格 */ public SoMap set(String key, Object value) { // 防止敏感key if(key.toLowerCase().equals("this")) { return this; } put(key, value); return this; } /** 将一个Map塞进SoMap */ public SoMap setMap(Map map) { if(map != null) { for (String key : map.keySet()) { this.set(key, map.get(key)); } } return this; } /** 将一个对象解析塞进SoMap */ public SoMap setModel(Object model) { if(model == null) { return this; } Field[] fields = model.getClass().getDeclaredFields(); for (Field field : fields) { try{ field.setAccessible(true); boolean isStatic = Modifier.isStatic(field.getModifiers()); if(!isStatic) { this.set(field.getName(), field.get(model)); } }catch (Exception e){ throw new RuntimeException(e); } } return this; } /** 将json字符串解析后塞进SoMap */ public SoMap setJsonString(String jsonString) { try { @SuppressWarnings("unchecked") Map map = new ObjectMapper().readValue(jsonString, Map.class); return this.setMap(map); } catch (Exception e) { throw new RuntimeException(e); } } // ============================= 删值 ============================= /** delete一个值,连缀风格 */ public SoMap delete(String key) { remove(key); return this; } /** 清理所有value为null的字段 */ public SoMap clearNull() { Iterator iterator = this.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); if(this.isNull(key)) { iterator.remove(); this.remove(key); } } return this; } /** 清理指定key */ public SoMap clearIn(String ...keys) { List keys2 = Arrays.asList(keys); Iterator iterator = this.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); if(keys2.contains(key) == true) { iterator.remove(); this.remove(key); } } return this; } /** 清理掉不在列表中的key */ public SoMap clearNotIn(String ...keys) { List keys2 = Arrays.asList(keys); Iterator iterator = this.keySet().iterator(); while(iterator.hasNext()) { String key = iterator.next(); if(keys2.contains(key) == false) { iterator.remove(); this.remove(key); } } return this; } /** 清理掉所有key */ public SoMap clearAll() { clear(); return this; } // ============================= 快速构建 ============================= /** 构建一个SoMap并返回 */ public static SoMap getSoMap() { return new SoMap(); } /** 构建一个SoMap并返回 */ public static SoMap getSoMap(String key, Object value) { return new SoMap().set(key, value); } /** 构建一个SoMap并返回 */ public static SoMap getSoMap(Map map) { return new SoMap().setMap(map); } /** 将一个对象集合解析成为SoMap */ public static SoMap getSoMapByModel(Object model) { return SoMap.getSoMap().setModel(model); } /** 将一个对象集合解析成为SoMap集合 */ public static List getSoMapByList(List list) { List listMap = new ArrayList(); for (Object model : list) { listMap.add(getSoMapByModel(model)); } return listMap; } /** 克隆指定key,返回一个新的SoMap */ public SoMap cloneKeys(String... keys) { SoMap so = new SoMap(); for (String key : keys) { so.set(key, this.get(key)); } return so; } /** 克隆所有key,返回一个新的SoMap */ public SoMap cloneSoMap() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(key, this.get(key)); } return so; } /** 将所有key转为大写 */ public SoMap toUpperCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(key.toUpperCase(), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key转为小写 */ public SoMap toLowerCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(key.toLowerCase(), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key中下划线转为中划线模式 (kebab-case风格) */ public SoMap toKebabCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(wordEachKebabCase(key), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key中下划线转为小驼峰模式 */ public SoMap toHumpCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(wordEachBigFs(key), this.get(key)); } this.clearAll().setMap(so); return this; } /** 将所有key中小驼峰转为下划线模式 */ public SoMap humpToLineCase() { SoMap so = new SoMap(); for (String key : this.keySet()) { so.set(wordHumpToLine(key), this.get(key)); } this.clearAll().setMap(so); return this; } // ============================= 辅助方法 ============================= /** 指定key是否为null,判定标准为 NULL_ELEMENT_ARRAY 中的元素 */ public boolean isNull(String key) { return valueIsNull(get(key)); } /** 指定key列表中是否包含value为null的元素,只要有一个为null,就会返回true */ public boolean isContainNull(String ...keys) { for (String key : keys) { if(this.isNull(key)) { return true; } } return false; } /** 与isNull()相反 */ public boolean isNotNull(String key) { return !isNull(key); } /** 指定key的value是否为null,作用同isNotNull() */ public boolean has(String key) { return !isNull(key); } /** 指定value在此SoMap的判断标准中是否为null */ public boolean valueIsNull(Object value) { return NULL_ELEMENT_LIST.contains(value); } /** 验证指定key不为空,为空则抛出异常 */ public SoMap checkNull(String ...keys) { for (String key : keys) { if(this.isNull(key)) { throw new RuntimeException("参数" + key + "不能为空"); } } return this; } static Pattern patternNumber = Pattern.compile("[0-9]*"); /** 指定key是否为数字 */ public boolean isNumber(String key) { String value = getString(key); if(value == null) { return false; } return patternNumber.matcher(value).matches(); } /** * 转为JSON字符串 */ public String toJsonString() { try { // SoMap so = SoMap.getSoMap(this); return new ObjectMapper().writeValueAsString(this); } catch (Exception e) { throw new RuntimeException(e); } } // // /** // * 转为JSON字符串, 带格式的 // */ // public String toJsonFormatString() { // try { // return JSON.toJSONString(this, true); // } catch (Exception e) { // throw new RuntimeException(e); // } // } // ============================= web辅助 ============================= /** * 返回当前request请求的的所有参数 * @return */ public static SoMap getRequestSoMap() { // 大善人SpringMVC提供的封装 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null) { throw new RuntimeException("当前线程非JavaWeb环境"); } // 当前request HttpServletRequest request = servletRequestAttributes.getRequest(); if (request.getAttribute("currentSoMap") == null || request.getAttribute("currentSoMap") instanceof SoMap == false ) { initRequestSoMap(request); } return (SoMap)request.getAttribute("currentSoMap"); } /** 初始化当前request的 SoMap */ private static void initRequestSoMap(HttpServletRequest request) { SoMap soMap = new SoMap(); Map parameterMap = request.getParameterMap(); // 获取所有参数 for (String key : parameterMap.keySet()) { try { String[] values = parameterMap.get(key); // 获得values if(values.length == 1) { soMap.set(key, values[0]); } else { List list = new ArrayList(); for (String v : values) { list.add(v); } soMap.set(key, list); } } catch (Exception e) { throw new RuntimeException(e); } } request.setAttribute("currentSoMap", soMap); } /** * 验证返回当前线程是否为JavaWeb环境 * @return */ public static boolean isJavaWeb() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 if(servletRequestAttributes == null) { return false; } return true; } // ============================= 常见key (以下key经常用,所以封装以下,方便写代码) ============================= /** get 当前页 */ public int getKeyPageNo() { int pageNo = getInt("pageNo", 1); if(pageNo <= 0) { pageNo = 1; } return pageNo; } /** get 页大小 */ public int getKeyPageSize() { int pageSize = getInt("pageSize", 10); if(pageSize <= 0 || pageSize > 1000) { pageSize = 10; } return pageSize; } /** get 排序方式 */ public int getKeySortType() { return getInt("sortType"); } // ============================= 分页相关(封装mybatis的page-help插件 ) ============================= // /** 分页插件 */ // private com.github.pagehelper.Page pagePlug; // /** 分页插件 - 开始分页 */ // public SoMap startPage() { // this.pagePlug= com.github.pagehelper.PageHelper.startPage(getKeyPageNo(), getKeyPageSize()); // return this; // } // /** 获取上次分页的记录总数 */ // public long getDataCount() { // if(pagePlug == null) { // return -1; // } // return pagePlug.getTotal(); // } // /** 分页插件 - 结束分页, 返回总条数 (该方法已过时,请调用更加符合语义化的getDataCount() ) */ // @Deprecated // public long endPage() { // return getDataCount(); // } // ============================= 工具方法 ============================= /** * 将一个一维集合转换为树形集合 * @param list 集合 * @param idKey id标识key * @param parentIdKey 父id标识key * @param childListKey 子节点标识key * @return 转换后的tree集合 */ public static List listToTree(List list, String idKey, String parentIdKey, String childListKey) { // 声明新的集合,存储tree形数据 List newTreeList = new ArrayList(); // 声明hash-Map,方便查找数据 SoMap hash = new SoMap(); // 将数组转为Object的形式,key为数组中的id for (int i = 0; i < list.size(); i++) { SoMap json = (SoMap) list.get(i); hash.put(json.getString(idKey), json); } // 遍历结果集 for (int j = 0; j < list.size(); j++) { // 单条记录 SoMap aVal = (SoMap) list.get(j); // 在hash中取出key为单条记录中pid的值 SoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, "").toString()); // 如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中 if (hashVp != null) { // 检查是否有child属性,有则添加,没有则新建 if (hashVp.get(childListKey) != null) { @SuppressWarnings("unchecked") List ch = (List) hashVp.get(childListKey); ch.add(aVal); hashVp.put(childListKey, ch); } else { List ch = new ArrayList(); ch.add(aVal); hashVp.put(childListKey, ch); } } else { newTreeList.add(aVal); } } return newTreeList; } /** 指定字符串的字符串下划线转大写模式 */ private static String wordEachBig(String str){ String newStr = ""; for (String s : str.split("_")) { newStr += wordFirstBig(s); } return newStr; } /** 返回下划线转小驼峰形式 */ private static String wordEachBigFs(String str){ return wordFirstSmall(wordEachBig(str)); } /** 将指定单词首字母大写 */ private static String wordFirstBig(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1, str.length()); } /** 将指定单词首字母小写 */ private static String wordFirstSmall(String str) { return str.substring(0, 1).toLowerCase() + str.substring(1, str.length()); } /** 下划线转中划线 */ private static String wordEachKebabCase(String str) { return str.replaceAll("_", "-"); } /** 驼峰转下划线 */ private static String wordHumpToLine(String str) { return str.replaceAll("[A-Z]", "_$0").toLowerCase(); } } ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/resources/application.yml ================================================ # 端口 server: port: 8081 # Sa-Token配置 sa-token: # Token名称 (同时也是cookie名称) token-name: satoken ================================================ FILE: sa-token-test/sa-token-springboot-test/src/test/resources/sa-token2.properties ================================================ # token 名称 (同时也是 cookie 名称) tokenName=use-token # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout=9000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 activeTimeout=240 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) isConcurrent=false # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) isShare=false # 是否输出操作日志 isLog=true ================================================ FILE: sa-token-test/sa-token-temp-jwt-test/pom.xml ================================================ 4.0.0 cn.dev33 sa-token-test ${revision} ../pom.xml jar sa-token-temp-jwt-test sa-token-temp-jwt-test sa-token-temp-jwt-test cn.dev33 sa-token-spring-boot-starter cn.dev33 sa-token-temp-jwt ================================================ FILE: sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java ================================================ package com.pj.test; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.temp.SaTempUtil; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; /** * Sa-Token 整合 temp jwt * * @author click33 * @since 1.42.0 */ @SpringBootTest(classes = StartUpApplication.class) public class SaTempTemplateForJwtTest { // 开始 @BeforeAll public static void beforeClass() { System.out.println("\n\n------------------------ SaTempTemplateForJwtTest star ..."); } // 结束 @AfterAll public static void afterClass() { System.out.println("\n\n------------------------ SaTempTemplateForJwtTest end ... \n"); } // 测试:临时Token认证模块 @Test public void testSaTemp() { // 生成token String token = SaTempUtil.createToken("group-1014", 200); // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet()); // System.out.println("satoken:temp-token:" + ":" + token); Assertions.assertNotNull(token); // 解析token String value = SaTempUtil.parseToken(token, String.class); Assertions.assertEquals(value, "group-1014"); // 解析 token 并裁剪前缀 long value2 = SaTempUtil.parseToken(token, "group-", Long.class); Assertions.assertEquals(value2, 1014); // 默认类型 Object value3 = SaTempUtil.parseToken(token); Assertions.assertEquals(value3, "group-1014"); // 转换类型 String value4 = SaTempUtil.parseToken(token, String.class); Assertions.assertEquals(value4, "group-1014"); // 过期时间 long timeout = SaTempUtil.getTimeout(token); Assertions.assertTrue(timeout > 195); Assertions.assertTrue(timeout < 201); // 回收token Assertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.deleteToken(token) ); } // 测试:临时Token认证模块索引 @Test public void testSaTempIndex() { SaTokenDao dao = SaManager.getSaTokenDao(); // 生成token String token1 = SaTempUtil.createToken("1001", 200, true); String token2 = SaTempUtil.createToken("1001", 300, true); String token3 = SaTempUtil.createToken("1001", 400, true); Assertions.assertNotNull(token1); Assertions.assertNotNull(token2); Assertions.assertNotNull(token3); // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); // 解析token Assertions.assertEquals(SaTempUtil.parseToken(token1, String.class), "1001"); Assertions.assertEquals(SaTempUtil.parseToken(token2, String.class), "1001"); Assertions.assertEquals(SaTempUtil.parseToken(token3, String.class), "1001"); // 缓存数据比对 Assertions.assertNull(dao.getObject("satoken:temp-token:" + token1)); Assertions.assertNull(dao.getObject("satoken:temp-token:" + token2)); Assertions.assertNull(dao.getObject("satoken:temp-token:" + token3)); // 索引 Assertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.getTempTokenList("1001") ); } @Test public void testGetJwtSecretKey() { // 秘钥默认为null String jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey(); Assertions.assertNotNull(jwtSecretKey); } } ================================================ FILE: sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java ================================================ package com.pj.test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动类 * @author Auster * */ @SpringBootApplication public class StartUpApplication { public static void main(String[] args) { SpringApplication.run(StartUpApplication.class, args); } } ================================================ FILE: sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml ================================================ # 端口 server: port: 8081 # sa-token 配置 sa-token: # token 名称 (同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天,-1 代表永久有效 timeout: 2592000 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: # 连接超时时间(毫秒) timeout: 10000ms lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0