├── .github └── workflows │ └── maven-build.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── README_CN.md └── openapi.yaml ├── pom.xml ├── sso-common ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qingyou │ └── sso │ ├── api │ ├── constants │ │ ├── DataType.java │ │ ├── EncodeStringMap.java │ │ └── PlatformType.java │ ├── dto │ │ ├── BaseDependency.java │ │ ├── Result.java │ │ └── ThirdPartySSOUserInfo.java │ └── event │ │ └── RbacAuth.java │ ├── auth │ ├── api │ │ ├── AuthService.java │ │ └── dto │ │ │ ├── Action.java │ │ │ ├── AuthServiceWithEventBus.java │ │ │ └── RoleAction.java │ ├── exception │ │ └── AuthException.java │ └── internal │ │ ├── Enforcement.java │ │ └── rbac │ │ ├── IRbac.java │ │ ├── Rbac.java │ │ ├── RbacUserInfo.java │ │ └── TargetInfo.java │ ├── domain │ ├── auth │ │ ├── Role.java │ │ ├── TargetRole.java │ │ └── UserRole.java │ ├── oauth │ │ ├── ThirdPartyApp.java │ │ ├── ThirdPartyRedirect.java │ │ └── ThirdPartyRequiredUserInfo.java │ └── user │ │ ├── Account.java │ │ ├── User.java │ │ └── UserInfo.java │ ├── infra │ ├── Constants.java │ ├── cache │ │ ├── Cache.java │ │ └── DefaultCache.java │ ├── config │ │ ├── Configuration.java │ │ ├── ConfigurationSource.java │ │ └── ConfigurationSourceLoader.java │ ├── exception │ │ ├── BizException.java │ │ └── ErrorType.java │ ├── repository │ │ └── domain │ │ │ ├── AccountRepository.java │ │ │ ├── ThirdPartyRedirectRepository.java │ │ │ ├── ThirdPartyRepository.java │ │ │ ├── ThirdPartyRequiredUserInfoRepository.java │ │ │ ├── UserInfoRepository.java │ │ │ ├── UserRepository.java │ │ │ └── impl │ │ │ ├── AccountRepositoryImpl.java │ │ │ ├── ThirdPartyRedirectRepositoryImpl.java │ │ │ ├── ThirdPartyRepositoryImpl.java │ │ │ ├── ThirdPartyRequiredUserInfoRepositoryImpl.java │ │ │ ├── UserInfoRepositoryImpl.java │ │ │ └── UserRepositoryImpl.java │ ├── request │ │ └── HttpRequestBody.java │ └── response │ │ ├── EventMessageHandler.java │ │ ├── GlobalHttpResponse.java │ │ ├── GlobalHttpResponseWrap.java │ │ ├── OAuth2HttpResponse.java │ │ └── ServiceApi.java │ └── utils │ ├── PasswordEncodeUtils.java │ ├── ResourceUtils.java │ └── StringUtils.java ├── sso-core ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── qingyou │ │ │ └── sso │ │ │ ├── CoreSSOApp.java │ │ │ ├── inject │ │ │ ├── RouterHandlerRegisterComponent.java │ │ │ └── provider │ │ │ │ ├── BaseModule.java │ │ │ │ ├── RepositoryModule.java │ │ │ │ ├── RouterHandlerModule.java │ │ │ │ └── ServiceModule.java │ │ │ ├── router │ │ │ ├── admin │ │ │ │ └── AdminRouterHandler.java │ │ │ ├── global │ │ │ │ ├── NotFoundRouterHandler.java │ │ │ │ └── SessionRouterHandler.java │ │ │ ├── oauth │ │ │ │ └── OAuth2RouterHandler.java │ │ │ └── sso │ │ │ │ ├── CustomSSORouterHandler.java │ │ │ │ ├── LoginRouterHandler.java │ │ │ │ └── SSORouterHandlerRegistry.java │ │ │ └── verticle │ │ │ └── CoreVerticle.java │ └── resources │ │ ├── conf.json │ │ ├── logback.xml │ │ ├── tables.sql │ │ └── verticles.json │ └── test │ ├── java │ └── com │ │ └── qingyou │ │ └── sso │ │ └── test │ │ ├── ApiTest.java │ │ └── SystemTest.java │ └── resources │ ├── conf.json │ ├── logback.xml │ └── verticles.json ├── sso-server-admin ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qingyou │ └── sso │ ├── api │ ├── ThirdPartyAppManagement.java │ ├── param │ │ ├── AppCreate.java │ │ ├── RedirectURIsUpdate.java │ │ └── RequiredInfosUpdate.java │ └── result │ │ └── ThirdPartyAppResult.java │ ├── handler │ ├── AuthHandler.java │ └── ThirdPartyAppManagementService.java │ └── service │ ├── ThirdPartyAppService.java │ └── ThirdPartyAppServiceImpl.java ├── sso-server-login-email-code ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qingyou │ └── sso │ ├── api │ ├── SendEmail.java │ └── param │ │ └── Email.java │ ├── handler │ └── platform │ │ └── EmailSSOHandler.java │ ├── router │ └── sso │ │ └── EmailSSORouterHandler.java │ └── service │ ├── DefaultEmailSSOService.java │ └── EmailSSOService.java ├── sso-server-login ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qingyou │ └── sso │ ├── api │ ├── Info.java │ ├── Login.java │ ├── Logout.java │ ├── Register.java │ ├── State.java │ ├── param │ │ ├── Code.java │ │ └── UsernamePassword.java │ └── result │ │ └── LoginResult.java │ ├── handler │ ├── platform │ │ ├── CustomSSOHandler.java │ │ └── SSOHandlerRegistry.java │ └── sso │ │ └── SSOHandler.java │ └── service │ ├── BaseSSOService.java │ ├── DataChecker.java │ └── DefaultBaseSSOService.java ├── sso-server-oauth2 ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── qingyou │ └── sso │ ├── api │ ├── Authorization.java │ ├── Error.java │ ├── Info.java │ ├── Password.java │ ├── Token.java │ ├── param │ │ └── OAuth2Params.java │ └── result │ │ ├── AccessToken.java │ │ ├── AuthorizationCode.java │ │ ├── ResourceData.java │ │ └── UserInfo.java │ ├── handler │ ├── ErrorPageHandler.java │ ├── OAuth2Handler.java │ └── OAuth2ParamHandler.java │ └── serviece │ ├── DefaultOAuth2Service.java │ └── OAuth2Service.java └── sso-site ├── pom.xml ├── src └── main │ ├── java │ └── com │ │ └── qingyou │ │ └── sso │ │ └── router │ │ └── site │ │ └── EmailSSORouterHandler.java │ └── resources │ └── note └── sso-client-site ├── .gitignore ├── README.md ├── components.json ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── pom.xml ├── public └── logo.svg ├── src ├── App.tsx ├── api │ └── UserApi.ts ├── assets │ └── logo.svg ├── components │ ├── Form │ │ ├── FormContainer.tsx │ │ └── controllers │ │ │ └── InputController.tsx │ ├── base │ │ ├── Background.tsx │ │ ├── ErrorBoundary.tsx │ │ └── Reveal.tsx │ ├── business │ │ └── LoginForm │ │ │ ├── index.tsx │ │ │ └── schema.ts │ └── ui │ │ ├── button.tsx │ │ └── input.tsx ├── hooks │ └── useFormContext.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── pages │ ├── Home.tsx │ └── LoginPage.tsx ├── router.tsx ├── utils │ ├── http.ts │ ├── response.ts │ └── showSnack.ts └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts /.github/workflows/maven-build.yml: -------------------------------------------------------------------------------- 1 | name: Maven Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'releases/*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'temurin' 20 | java-version: '17' 21 | cache: 'maven' 22 | 23 | - name: Build with Maven 24 | run: mvn clean package -DskipTests 25 | 26 | - name: Upload artifact 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: maven-build-output 30 | path: target/*.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Vert.x ### 2 | .vertx/ 3 | 4 | ### Eclipse ### 5 | 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | ## Idea 19 | .idea/ 20 | 21 | # External tool builders 22 | .externalToolBuilders/ 23 | 24 | # Locally stored "Eclipse launch configurations" 25 | *.launch 26 | 27 | # PyDev specific (Python IDE for Eclipse) 28 | *.pydevproject 29 | 30 | # CDT-specific (C/C++ Development Tooling) 31 | .cproject 32 | 33 | # Java annotation processor (APT) 34 | .factorypath 35 | 36 | # PDT-specific (PHP Development Tools) 37 | .buildpath 38 | 39 | # sbteclipse plugin 40 | .target 41 | 42 | # Tern plugin 43 | .tern-project 44 | 45 | # TeXlipse plugin 46 | .texlipse 47 | 48 | # STS (Spring Tool Suite) 49 | .springBeans 50 | 51 | # Code Recommenders 52 | .recommenders/ 53 | 54 | # Scala IDE specific (Scala & Java development for Eclipse) 55 | .cache-main 56 | .scala_dependencies 57 | .worksheet 58 | 59 | ### Intellij+iml ### 60 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 61 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 62 | 63 | # User-specific stuff: 64 | .idea/**/workspace.xml 65 | .idea/**/tasks.xml 66 | .idea/dictionaries 67 | 68 | # Sensitive or high-churn files: 69 | .idea/**/dataSources/ 70 | .idea/**/dataSources.ids 71 | .idea/**/dataSources.xml 72 | .idea/**/dataSources.local.xml 73 | .idea/**/sqlDataSources.xml 74 | .idea/**/dynamic.xml 75 | .idea/**/uiDesigner.xml 76 | 77 | # Gradle: 78 | .idea/**/gradle.xml 79 | .idea/**/libraries 80 | 81 | # CMake 82 | cmake-buildTool-debug/ 83 | 84 | # Mongo Explorer plugin: 85 | .idea/**/mongoSettings.xml 86 | 87 | ## File-based project format: 88 | *.iws 89 | 90 | ## Plugin-specific files: 91 | 92 | # IntelliJ 93 | /out/ 94 | 95 | # mpeltonen/sbt-idea plugin 96 | .idea_modules/ 97 | 98 | # JIRA plugin 99 | atlassian-ide-plugin.xml 100 | 101 | # Cursive Clojure plugin 102 | .idea/replstate.xml 103 | 104 | # Crashlytics plugin (for Android Studio and IntelliJ) 105 | com_crashlytics_export_strings.xml 106 | crashlytics.properties 107 | crashlytics-buildTool.properties 108 | fabric.properties 109 | 110 | ### Intellij+iml Patch ### 111 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 112 | 113 | *.iml 114 | modules.xml 115 | .idea/misc.xml 116 | *.ipr 117 | 118 | ### macOS ### 119 | *.DS_Store 120 | .AppleDouble 121 | .LSOverride 122 | 123 | # Icon must end with two \r 124 | Icon 125 | 126 | # Thumbnails 127 | ._* 128 | 129 | # Files that might appear in the root of a volume 130 | .DocumentRevisions-V100 131 | .fseventsd 132 | .Spotlight-V100 133 | .TemporaryItems 134 | .Trashes 135 | .VolumeIcon.icns 136 | .com.apple.timemachine.donotpresent 137 | 138 | # Directories potentially created on remote AFP share 139 | .AppleDB 140 | .AppleDesktop 141 | Network Trash Folder 142 | Temporary Items 143 | .apdisk 144 | 145 | ### Maven ### 146 | target/ 147 | pom.xml.tag 148 | pom.xml.releaseBackup 149 | pom.xml.versionsBackup 150 | pom.xml.next 151 | release.properties 152 | dependency-reduced-pom.xml 153 | buildNumber.properties 154 | .mvn/timing.properties 155 | 156 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 157 | !/.mvn/wrapper/maven-wrapper.jar 158 | 159 | ### Gradle ### 160 | .gradle 161 | /buildTool/ 162 | 163 | # Ignore Gradle GUI config 164 | gradle-app.setting 165 | 166 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 167 | !gradle-wrapper.jar 168 | 169 | # Cache of project 170 | .gradletasknamecache 171 | 172 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 173 | # gradle/wrapper/gradle-wrapper.properties 174 | 175 | ### NetBeans ### 176 | nbproject/private/ 177 | buildTool/ 178 | nbbuild/ 179 | dist/ 180 | nbdist/ 181 | .nb-gradle/ 182 | 183 | ### VisualStudioCode ### 184 | .vscode/* 185 | !.vscode/settings.json 186 | !.vscode/tasks.json 187 | !.vscode/launch.json 188 | !.vscode/extensions.json 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 feellmoose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pomelo SSO [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) 2 | 3 | **Pomelo SSO** is a lightweight single sign-on (SSO) system built on the OAuth 2.0 and OpenID Connect (OIDC) standards. It utilizes HTTP Cookie-based SSO session management and is designed for high-concurrency scenarios based on the Vert.x framework. 4 | 5 | English/[中文](https://github.com/feellmoose/core-sso/blob/main/docs/README_CN.md) 6 | 7 | ## ✨ Core Features 8 | 9 | - **Standards Compliance**: Full implementation of OAuth 2.0 and OpenID Connect (OIDC) protocols 10 | - **Session Management**: Stateful cookie-based session mechanism 11 | - **High-Performance Architecture**: Asynchronous non-blocking I/O model powered by Vert.x 12 | - **Database Support**: PostgreSQL integration with Hibernate ORM 13 | - **Extensible Design**: Modular architecture supporting custom authentication flows 14 | 15 | ## 🚀 Quick Start 16 | 17 | ### Prerequisites 18 | 19 | - Java 17+ 20 | - PostgreSQL 13+ 21 | 22 | ### Configuration 23 | 24 | Create `config.json` configuration file: 25 | 26 | ```json 27 | { 28 | "server" : { 29 | "host" : "0.0.0.0", 30 | "port" : 8080 31 | }, 32 | "database": { 33 | "url" : "jdbc:postgresql://localhost:5432/sso", 34 | "user" : "postgres", 35 | "password" : "passw0rd", 36 | "connection" : { 37 | "poolSize" : 10 38 | }, 39 | "hibernate": { 40 | "showSql" : true, 41 | "action": "update" 42 | } 43 | }, 44 | "security": { 45 | "jwt": { 46 | "secret" : "jwt_secret" 47 | }, 48 | "cookie": { 49 | "name" : "session_id", 50 | "maxAge" : 60000000, 51 | "timeout" : 60000000, 52 | "path" : "/", 53 | "domain" : "localhost", 54 | "secure" : false, 55 | "httpOnly" : false 56 | } 57 | }, 58 | "mail": { 59 | "expire": 60000, 60 | "username": "user@example.com", 61 | "password": "", 62 | "host" : "", 63 | "port" : "", 64 | "from" : "user@example.com", 65 | "subject" : "[Pomelo SSO] One-Time Passcode (OTP)", 66 | "pattern" : "Verification Code: %s" 67 | } 68 | } 69 | ``` 70 | 71 | ### Standalone Deployment 72 | 73 | ```bash 74 | java -jar target/pomelo-sso-1.0.0.jar -conf config.json 75 | ``` 76 | 77 | **Security Note**: Use strong secrets in production and disable debug configurations. 78 | 79 | ### 🌀 Cluster Deployment (High Availability) 80 | 81 | > Please refer to the official Vert.x documentation 82 | 83 | **Cluster configuration file** `cluster.xml`: 84 | 85 | ```xml 86 | 87 | 88 | 89 | 5701 90 | 91 | 92 | 93 | node1.example.com 94 | node2.example.com 95 | 96 | 97 | 98 | 99 | 100 | ``` 101 | 102 | **Startup command**: 103 | 104 | ```bash 105 | java -jar target/pomelo-sso-1.0.0.jar \ 106 | -conf config.json \ 107 | -cluster -cluster-host 192.168.1.100 \ 108 | -cluster-port 15701 \ 109 | -cluster-config cluster.xml 110 | ``` 111 | 112 | ## 📌 Roadmap 113 | 114 | ### Implemented Features 115 | 116 | - Core OAuth 2.0/OIDC protocol implementation 117 | - JWT & Cookie session management 118 | - PostgreSQL integration 119 | - Email OTP authentication flow 120 | 121 | ### Planned Features 122 | 123 | - Docker & Docker Compose support 124 | - Vert.x cluster configuration 125 | - Admin console 126 | - Multi-database support (MySQL/MongoDB) 127 | - Performance benchmarking 128 | - Social login extensions 129 | - Configuration wizard UI 130 | 131 | ## 🤝 Contributing 132 | 133 | We welcome contributions through the following process: 134 | 135 | 1. Fork the repository 136 | 2. Create a feature branch (`git checkout -b feature/new-feature`) 137 | 3. Commit standardized code changes 138 | 4. Push to the branch (`git push origin feature/new-feature`) 139 | 5. Open a Pull Request 140 | 141 | **Contribution Guidelines:** 142 | 143 | - Follow existing code style 144 | - Include unit tests for new features 145 | - Update relevant documentation 146 | - Use conventional commit messages 147 | 148 | Found a vulnerability? [Open an issue](https://github.com/feellmoose/core-sso/issues) 149 | 150 | ## ⚖️ License 151 | 152 | Distributed under the MIT License. See `LICENSE` for details. 153 | 154 | ------ 155 | 156 | **Like what you see?** ⭐ Star the repository to support development! 157 | 158 | -------------------------------------------------------------------------------- /docs/README_CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Pomelo SSO [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) 6 | 7 | **Pomelo SSO** 是一款基于 OAuth 2.0 和 OpenID Connect (OIDC) 标准实现的轻量级单点登录系统,采用基于 HTTP Cookie 的 SSO 网页会话管理,并基于 Vert.x 框架专为高并发场景设计。 8 | 9 | [English](https://github.com/feellmoose/core-sso/blob/main/README.md)/中文 10 | 11 | ## ✨ 核心特性 12 | 13 | - **标准协议支持**:完整实现 OAuth 2.0 和 OpenID Connect (OIDC) 协议 14 | - **会话管理**:基于 Cookie 的有状态会话机制 15 | - **高性能架构**:基于 Vert.x 的异步非阻塞 I/O 模型 16 | - **数据存储支持**:集成 PostgreSQL 与 Hibernate ORM 17 | - **可扩展设计**:模块化架构支持自定义认证流程 18 | 19 | ## 🚀 快速入门 20 | 21 | ### 环境要求 22 | 23 | - Java 17+ 24 | - PostgreSQL 13+ 25 | 26 | ### 配置说明 27 | 28 | 创建 `config.json` 配置文件模板: 29 | 30 | ```json 31 | { 32 | "server" : { 33 | "host" : "0.0.0.0", 34 | "port" : 8080 35 | }, 36 | "database": { 37 | "url" : "jdbc:postgresql://localhost:5432/sso", 38 | "user" : "postgres", 39 | "password" : "passw0rd", 40 | "connection" : { 41 | "poolSize" : 10 42 | }, 43 | "hibernate": { 44 | "showSql" : true, 45 | "action": "update" 46 | } 47 | }, 48 | "security": { 49 | "jwt": { 50 | "secret" : "jwt_secret" 51 | }, 52 | "cookie": { 53 | "name" : "session_id", 54 | "maxAge" : 60000000, 55 | "timeout" : 60000000, 56 | "path" : "/", 57 | "domain" : "localhost", 58 | "secure" : false, 59 | "httpOnly" : false 60 | } 61 | }, 62 | "mail": { 63 | "expire": 60000, 64 | "username": "user@example.com", 65 | "password": "", 66 | "host" : "", 67 | "port" : "", 68 | "from" : "user@example.com", 69 | "subject" : "[Pomelo SSO] One-Time Passcode (OTP)", 70 | "pattern" : "Verification Code: %s" 71 | } 72 | } 73 | ``` 74 | 75 | ### 单机启动服务 76 | 77 | ```bash 78 | java -jar target/pomelo-sso-1.0.0.jar -conf config.json 79 | ``` 80 | 81 | **安全提示**:生产环境请使用高强度密钥并关闭调试配置。 82 | 83 | ### 🌀 集群部署 (高可用模式) 84 | 85 | > 请参阅 vertx 官方文档 86 | 87 | **集群配置文件** `cluster.xml`: 88 | 89 | ```xml 90 | 91 | 92 | 93 | 5701 94 | 95 | 96 | 97 | node1.example.com 98 | node2.example.com 99 | 100 | 101 | 102 | 103 | 104 | ``` 105 | 106 | **启动参数**: 107 | 108 | ```bash 109 | java -jar target/pomelo-sso-1.0.0.jar \ 110 | -conf config.json \ 111 | -cluster -cluster-host 192.168.1.100 \ 112 | -cluster-port 15701 \ 113 | -cluster-config cluster.xml 114 | ``` 115 | 116 | ## 📌 Roadmap 117 | 118 | ### 已实现功能 119 | 120 | - 核心 OAuth 2.0/OIDC 协议实现 121 | - JWT & Cookie 会话管理 122 | - PostgreSQL 集成支持 123 | - 邮件 OTP 认证流程 124 | 125 | ### 规划功能 126 | 127 | - Docker & Docker compose 支持 128 | - Vert.x 集群配置 129 | - 管理控制台 130 | - 多数据库支持(MySQL/MongoDB) 131 | - 性能基准测试 132 | - 社交登录扩展 133 | - 配置向导界面 134 | 135 | ## 🤝 参与贡献 136 | 137 | 欢迎通过以下流程参与贡献: 138 | 139 | 1. Fork 项目仓库 140 | 2. 创建特性分支 (`git checkout -b feature/新功能`) 141 | 3. 提交规范化的代码变更 142 | 4. 推送分支到远程仓库 (`git push origin feature/新功能`) 143 | 5. 发起 Pull Request 144 | 145 | **贡献规范:** 146 | 147 | - 遵循现有代码风格 148 | - 新增功能需包含单元测试 149 | - 及时更新相关文档 150 | - 使用约定式提交规范 151 | 152 | 发现漏洞?[提交 issue](https://github.com/feellmoose/core-sso/issues) 153 | 154 | ## ⚖️ 开源协议 155 | 156 | 本项目采用 MIT 开源协议,详见 `LICENSE` 文件。 157 | 158 | ------ 159 | 160 | **觉得不错?** ⭐ 给项目加星支持开发! -------------------------------------------------------------------------------- /sso-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | core-sso 9 | 1.0.1 10 | 11 | 12 | sso-common 13 | 14 | 15 | 17 16 | 17 17 | UTF-8 18 | 19 | 20 | 21 | 22 | org.projectlombok 23 | lombok 24 | 1.18.34 25 | provided 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/constants/DataType.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.constants; 2 | 3 | import java.util.Map; 4 | 5 | public enum DataType implements EncodeStringMap{ 6 | Json(){ 7 | @Override 8 | public Map decode(String value) { 9 | return io.vertx.core.json.Json.decodeValue(value, Map.class); 10 | } 11 | 12 | @Override 13 | public String encode(Map obj) { 14 | return Json.encode(obj); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/constants/EncodeStringMap.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.constants; 2 | 3 | import java.util.Map; 4 | 5 | public interface EncodeStringMap { 6 | Map decode(String value); 7 | String encode(Map obj); 8 | } 9 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/constants/PlatformType.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.constants; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | 8 | public enum PlatformType { 9 | Email("email"), 10 | Phone("phone"), 11 | Google("google"), 12 | Gitlab("gitlab"), 13 | Github("github"), 14 | OAuth2("oauth2"), 15 | Custom("custom"), 16 | ; 17 | 18 | private final String obj; 19 | 20 | PlatformType(String obj) { 21 | this.obj = obj; 22 | } 23 | 24 | public String obj() { 25 | return obj; 26 | } 27 | 28 | public String scope() { 29 | return "info:" + obj; 30 | } 31 | 32 | private final static Set values = Arrays.stream(PlatformType.values()).collect(Collectors.toSet()); 33 | 34 | public static Set fromObj(@Nullable String obj) { 35 | if (obj.equals("all")) return values; 36 | for (PlatformType platformType : PlatformType.values()) { 37 | if (platformType.obj.equals(obj)) { 38 | return Collections.singleton(platformType); 39 | } 40 | } 41 | return Collections.emptySet(); 42 | } 43 | 44 | public static Set fromObjs(@Nullable List objs) { 45 | Set result = new HashSet<>(); 46 | for (String obj : objs) { 47 | if (obj.equals("all")) return values; 48 | for (PlatformType platformType : PlatformType.values()) { 49 | if (platformType.obj.equals(obj)) { 50 | result.add(platformType); 51 | } 52 | } 53 | } 54 | return result; 55 | } 56 | 57 | public static Set fromScope(@Nullable String scope) { 58 | if (scope.equals("info:all")) return values; 59 | for (PlatformType platformType : PlatformType.values()) { 60 | if (platformType.scope().equals(scope)) { 61 | return Collections.singleton(platformType); 62 | } 63 | } 64 | return Collections.emptySet(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/dto/BaseDependency.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.dto; 2 | 3 | 4 | import com.qingyou.sso.infra.cache.Cache; 5 | import com.qingyou.sso.infra.config.ConfigurationSource; 6 | import io.vertx.core.Vertx; 7 | import io.vertx.ext.web.client.WebClient; 8 | import io.vertx.sqlclient.SqlClient; 9 | import lombok.Getter; 10 | 11 | @Getter 12 | public abstract class BaseDependency { 13 | protected Vertx vertx; 14 | protected ConfigurationSource configuration; 15 | protected Cache cache; 16 | protected WebClient webClient; 17 | protected SqlClient sqlClient; 18 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/dto/Result.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.dto; 2 | 3 | public record Result( 4 | boolean success, 5 | int code, 6 | String message, 7 | T data 8 | ) { 9 | 10 | public static Result success(T data) { 11 | return new Result<>(true, 0, "", data); 12 | } 13 | 14 | public static Result failed(int code, String message) { 15 | return new Result<>(false, code, message, null); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/dto/ThirdPartySSOUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.dto; 2 | 3 | import com.qingyou.sso.api.constants.DataType; 4 | import com.qingyou.sso.api.constants.PlatformType; 5 | 6 | import java.util.Map; 7 | 8 | public record ThirdPartySSOUserInfo ( 9 | String name, 10 | String email, 11 | String phone, 12 | Info info 13 | ){ 14 | public record Info( 15 | String metadata, 16 | DataType dataType, 17 | PlatformType platformType 18 | ){ 19 | public Map getValue(){ 20 | return dataType.decode(metadata); 21 | } 22 | public static Info from(Map obj, DataType dataType, PlatformType platformType){ 23 | return new Info(dataType.encode(obj),dataType,platformType); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/api/event/RbacAuth.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.event; 2 | 3 | public record RbacAuth() { 4 | } 5 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/api/AuthService.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.api; 2 | 3 | import com.qingyou.sso.auth.api.dto.Action; 4 | import com.qingyou.sso.api.dto.Result; 5 | import com.qingyou.sso.auth.internal.rbac.TargetInfo; 6 | import com.qingyou.sso.auth.internal.rbac.RbacUserInfo; 7 | import io.vertx.core.Future; 8 | 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | 13 | public interface AuthService { 14 | 15 | Future> rbac(Action action); 16 | Future> rbac(Collection actions); 17 | } 18 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/api/dto/Action.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.api.dto; 2 | 3 | import com.qingyou.sso.auth.internal.rbac.RbacUserInfo; 4 | import com.qingyou.sso.auth.internal.rbac.TargetInfo; 5 | import com.qingyou.sso.domain.user.UserInfo; 6 | import io.vertx.ext.auth.User; 7 | 8 | import java.util.List; 9 | 10 | public record Action(RbacUserInfo owned, TargetInfo target) { 11 | 12 | public static Action required(RbacUserInfo owned, TargetInfo required) { 13 | return new Action(owned, required); 14 | } 15 | } 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/api/dto/AuthServiceWithEventBus.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.api.dto; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.qingyou.sso.api.dto.Result; 5 | import com.qingyou.sso.auth.api.AuthService; 6 | import com.qingyou.sso.auth.internal.rbac.RbacUserInfo; 7 | import com.qingyou.sso.auth.internal.rbac.TargetInfo; 8 | import io.vertx.core.Future; 9 | import io.vertx.core.eventbus.EventBus; 10 | import io.vertx.core.json.Json; 11 | import lombok.AllArgsConstructor; 12 | 13 | import java.util.Collection; 14 | 15 | @AllArgsConstructor 16 | public class AuthServiceWithEventBus implements AuthService { 17 | private final EventBus eventBus; 18 | 19 | @Override 20 | public Future> rbac(Action action) { 21 | try { 22 | return eventBus.request("auth_rbac", Json.encode(action)).map(objectMessage -> 23 | (Result) Json.decodeValue((String) objectMessage.body(),Result.class) 24 | ); 25 | } catch (Exception e) { 26 | return Future.failedFuture(new RuntimeException(e)); 27 | } 28 | } 29 | 30 | @Override 31 | public Future> rbac(Collection actions) { 32 | try { 33 | return eventBus.request("multi_auth_rbac", Json.encode(actions)).map(objectMessage -> 34 | (Result) Json.decodeValue((String) objectMessage.body(),Result.class) 35 | ); 36 | } catch (Exception e) { 37 | return Future.failedFuture(new RuntimeException(e)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/api/dto/RoleAction.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.api.dto; 2 | 3 | import com.qingyou.sso.auth.internal.rbac.TargetInfo; 4 | import com.qingyou.sso.domain.auth.TargetRole; 5 | import com.qingyou.sso.domain.auth.UserRole; 6 | import com.qingyou.sso.domain.user.UserInfo; 7 | 8 | import java.util.List; 9 | 10 | public record RoleAction(List owned, TargetRole target) { 11 | 12 | public static RoleAction required(List owned, TargetRole target) { 13 | return new RoleAction(owned, target); 14 | } 15 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/exception/AuthException.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.exception; 2 | 3 | public class AuthException extends RuntimeException { 4 | 5 | public AuthException(String reason) { 6 | super("Auth error: " + reason); 7 | } 8 | 9 | public AuthException(Throwable cause, String reason) { 10 | super("Auth error: " + reason, cause); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/internal/Enforcement.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.internal; 2 | 3 | import com.qingyou.sso.auth.api.dto.Action; 4 | import com.qingyou.sso.auth.exception.AuthException; 5 | import io.vertx.core.Future; 6 | 7 | 8 | public interface Enforcement { 9 | Future enforce(Action info); 10 | 11 | default Future enforceAndThrows(Action info) throws AuthException { 12 | return enforce(info).flatMap(res -> { 13 | if (!res) return Future.failedFuture(new AuthException("unknown reason")); 14 | return Future.succeededFuture(); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/internal/rbac/IRbac.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.internal.rbac; 2 | 3 | import com.qingyou.sso.auth.internal.Enforcement; 4 | 5 | public interface IRbac extends Enforcement { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/internal/rbac/RbacUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.internal.rbac; 2 | 3 | 4 | public record RbacUserInfo( 5 | Long id, 6 | String name 7 | ) { 8 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/auth/internal/rbac/TargetInfo.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.auth.internal.rbac; 2 | 3 | public record TargetInfo( 4 | Long appId, 5 | String action, 6 | String object 7 | ){ 8 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/auth/Role.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.auth; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.ToString; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | public class Role { 11 | private Long id; 12 | private String name; 13 | private String description; 14 | @ToString.Exclude 15 | @EqualsAndHashCode.Exclude 16 | private List userRole; 17 | @ToString.Exclude 18 | @EqualsAndHashCode.Exclude 19 | private List targetRole; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/auth/TargetRole.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.auth; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class TargetRole { 9 | 10 | private Long id; 11 | 12 | private Long appid; 13 | 14 | private String action; 15 | 16 | private String object; 17 | 18 | private List roles; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/auth/UserRole.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.auth; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserRole { 7 | private Long id; 8 | private Long appid; 9 | private Long userId; 10 | private Role role; 11 | } 12 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/oauth/ThirdPartyApp.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.oauth; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class ThirdPartyApp { 9 | 10 | private Long id; 11 | 12 | private String appName; 13 | 14 | private String clientId; 15 | 16 | private String clientSecret; 17 | 18 | private List redirectURIs; 19 | 20 | private List requiredUserInfos; 21 | } 22 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/oauth/ThirdPartyRedirect.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.oauth; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.ToString; 6 | 7 | @Data 8 | public class ThirdPartyRedirect { 9 | 10 | private Long id; 11 | 12 | private String URI; 13 | @ToString.Exclude 14 | @EqualsAndHashCode.Exclude 15 | private ThirdPartyApp thirdPartyApp; 16 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/oauth/ThirdPartyRequiredUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.oauth; 2 | 3 | import com.qingyou.sso.api.constants.DataType; 4 | import com.qingyou.sso.api.constants.PlatformType; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | @Data 10 | public class ThirdPartyRequiredUserInfo { 11 | 12 | private Long id; 13 | private DataType dataType; 14 | private PlatformType platformType; 15 | @ToString.Exclude 16 | @EqualsAndHashCode.Exclude 17 | private ThirdPartyApp thirdPartyApp; 18 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/user/Account.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.user; 2 | 3 | import lombok.*; 4 | 5 | @Data 6 | @AllArgsConstructor 7 | @NoArgsConstructor 8 | public class Account { 9 | 10 | private Long userId; 11 | 12 | private String username; 13 | 14 | private String password; 15 | 16 | private String salt; 17 | @ToString.Exclude 18 | @EqualsAndHashCode.Exclude 19 | private User user; 20 | } 21 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/user/User.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.user; 2 | 3 | import lombok.*; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class User { 11 | private Long id; 12 | private String name; 13 | private String email; 14 | private String phone; 15 | 16 | @ToString.Exclude 17 | @EqualsAndHashCode.Exclude 18 | private Account account; 19 | 20 | @ToString.Exclude 21 | @EqualsAndHashCode.Exclude 22 | private List userInfo; 23 | } 24 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/domain/user/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.domain.user; 2 | 3 | import com.qingyou.sso.api.constants.DataType; 4 | import com.qingyou.sso.api.constants.PlatformType; 5 | import com.qingyou.sso.api.dto.ThirdPartySSOUserInfo; 6 | import lombok.*; 7 | 8 | import java.util.Map; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class UserInfo { 14 | 15 | private Long id; 16 | 17 | private String metadata; 18 | 19 | private DataType dataType; 20 | 21 | private PlatformType platformType; 22 | @ToString.Exclude 23 | @EqualsAndHashCode.Exclude 24 | private User user; 25 | 26 | public Map getValue(){ 27 | return dataType.decode(metadata); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/Constants.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra; 2 | 3 | public interface Constants { 4 | 5 | static String classPath() { 6 | var resource = Constants.class.getClassLoader().getResource(""); 7 | if(resource == null) return null; 8 | return resource.getPath(); 9 | } 10 | 11 | String _logo = """ 12 | _____ __ ____ ____ 13 | _/ __ \\ _(_)_ ___ _ ___ __ __ ___/ _ )/ __/ 14 | / /_/ / / / _ \\/ _ `/ // / _ \\/ // /___/ _ / _/ 15 | \\___\\_\\/_/_//_/\\_, /\\_, /\\___/\\_,_/ _/____/___/ 16 | __/___//___/ 17 | Lite And Fast"""; 18 | 19 | static String logo(String name, String version) { 20 | return _logo + " ::" + name + ":: " + version; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/cache/Cache.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.cache; 2 | 3 | import io.vertx.core.Future; 4 | 5 | import java.time.Duration; 6 | import java.util.Collection; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public interface Cache { 11 | Future exists(String key); 12 | 13 | Future delete(String key); 14 | 15 | Future set(String key, T obj); 16 | 17 | Future set(String key, T obj, Duration expire); 18 | 19 | Future get(String key, Class clazz); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/cache/DefaultCache.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.cache; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Vertx; 6 | import io.vertx.core.json.Json; 7 | import io.vertx.core.shareddata.AsyncMap; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.time.Duration; 11 | import java.util.Objects; 12 | 13 | @Slf4j 14 | public class DefaultCache implements Cache { 15 | private final AsyncMap cache; 16 | 17 | private DefaultCache(AsyncMap cache) { 18 | this.cache = cache; 19 | } 20 | 21 | public static Future build(Vertx vertx) { 22 | if (vertx.isClustered()) { 23 | return vertx.sharedData().getClusterWideMap("clusterCache") 24 | .map(cache -> (Cache) new DefaultCache(cache)) 25 | .onFailure(cause -> log.error("ClusterCache initialize error", cause)); 26 | } else { 27 | return vertx.sharedData().getLocalAsyncMap("clusterCache") 28 | .map(cache -> (Cache) new DefaultCache(cache)) 29 | .onFailure(cause -> log.error("ClusterCache initialize error", cause)); 30 | } 31 | } 32 | 33 | @Override 34 | public Future exists(String key) { 35 | return cache.get(key).map(Objects::nonNull); 36 | } 37 | 38 | @Override 39 | public Future delete(String key) { 40 | return cache.remove(key).map(Objects::nonNull); 41 | } 42 | 43 | @Override 44 | public Future set(String key, T obj) { 45 | return cache.put(key, Json.encode(obj)); 46 | } 47 | 48 | @Override 49 | public Future set(String key, T obj, Duration expire) { 50 | return cache.put(key, Json.encode(obj), expire.toMillis()); 51 | } 52 | 53 | @Override 54 | public Future<@Nullable T> get(String key, Class clazz) { 55 | return cache.get(key).map(v -> Json.decodeValue(v, clazz)); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/config/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public record Configuration( 7 | Server server, 8 | Database database, 9 | Security security, 10 | Mail mail 11 | ) { 12 | public record Server(String host, int port) { 13 | } 14 | 15 | @JsonIgnoreProperties(ignoreUnknown = true) 16 | public record Database(String host, int port, String database, String user, String password, Pool pool) { 17 | public record Pool(int maxSize) { 18 | } 19 | } 20 | 21 | public record Security(Jwt jwt, Cookie cookie) { 22 | public record Jwt(String secret) { 23 | } 24 | 25 | @JsonIgnoreProperties(ignoreUnknown = true) 26 | public record Cookie(String name, long expire, long maxAge, long timeout, String path, String domain, 27 | boolean secure, boolean httpOnly) { 28 | } 29 | } 30 | 31 | @JsonIgnoreProperties(ignoreUnknown = true) 32 | public record Mail(Message message) { 33 | public record Message(String from, String subject, String pattern, long expire){} 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/config/ConfigurationSource.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.config; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import lombok.Getter; 5 | 6 | public class ConfigurationSource{ 7 | private volatile Configuration configuration; 8 | @Getter 9 | private final JsonObject source; 10 | 11 | public ConfigurationSource(JsonObject source) { 12 | this.source = source; 13 | } 14 | 15 | public Configuration getConfiguration() { 16 | if (this.configuration == null) { 17 | synchronized (source) { 18 | if (this.configuration == null) { 19 | this.configuration = source.mapTo(Configuration.class); 20 | } 21 | } 22 | } 23 | return configuration; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/config/ConfigurationSourceLoader.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.config; 2 | 3 | import io.vertx.config.ConfigRetriever; 4 | import io.vertx.config.ConfigRetrieverOptions; 5 | import io.vertx.config.ConfigStoreOptions; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.json.JsonObject; 9 | import lombok.AllArgsConstructor; 10 | 11 | /** 12 | * Load Configuration by Vertx Config 13 | */ 14 | @AllArgsConstructor 15 | public class ConfigurationSourceLoader { 16 | private final Vertx vertx; 17 | 18 | public Future loadSource() { 19 | ConfigRetriever retriever = ConfigRetriever.create(vertx, defaultOptions()); 20 | return retriever.getConfig().map(ConfigurationSource::new); 21 | } 22 | 23 | private static ConfigRetrieverOptions defaultOptions() { 24 | return new ConfigRetrieverOptions() 25 | .setIncludeDefaultStores(true) 26 | .setScanPeriod(-1) 27 | .addStore(new ConfigStoreOptions() 28 | .setType("file") 29 | .setFormat("json") 30 | .setOptional(true) 31 | .setConfig(new JsonObject().put("path", "verticles.json"))) 32 | .addStore(new ConfigStoreOptions() 33 | .setType("file") 34 | .setFormat("json") 35 | .setOptional(true) 36 | .setConfig(new JsonObject().put("path", "conf.json"))); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/exception/BizException.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class BizException extends RuntimeException { 7 | private final ErrorType errorType; 8 | 9 | public BizException(String message) { 10 | super(message); 11 | this.errorType = ErrorType.Inner.Default; 12 | } 13 | public BizException(Throwable cause) { 14 | super(cause); 15 | this.errorType = ErrorType.Inner.Default; 16 | } 17 | public BizException(String message, Throwable cause) { 18 | super(message, cause); 19 | this.errorType = ErrorType.Inner.Default; 20 | } 21 | 22 | public BizException(ErrorType errorType,String message) { 23 | super(message); 24 | this.errorType = errorType; 25 | } 26 | public BizException(ErrorType errorType,String message, Throwable cause) { 27 | super(message, cause); 28 | this.errorType = errorType; 29 | } 30 | public BizException(ErrorType errorType,Throwable cause) { 31 | super(cause); 32 | this.errorType = errorType; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/exception/ErrorType.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.exception; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | public sealed interface ErrorType permits ErrorType.Inner, ErrorType.Showed, ErrorType.OAuth2 { 6 | String message(); 7 | 8 | int code(); 9 | 10 | @AllArgsConstructor 11 | enum Inner implements ErrorType { 12 | Init("Service init", 0), 13 | Default("Default error", 0), 14 | Login("Login failed", 5001), 15 | ; 16 | private final String message; 17 | private final int code; 18 | 19 | @Override 20 | public String message() { 21 | return message; 22 | } 23 | 24 | @Override 25 | public int code() { 26 | return code; 27 | } 28 | } 29 | 30 | @AllArgsConstructor 31 | enum Showed implements ErrorType { 32 | Params("Params invalid", 1001), 33 | Auth("Auth check failed", 1002), 34 | ; 35 | private final String message; 36 | private final int code; 37 | 38 | @Override 39 | public String message() { 40 | return message; 41 | } 42 | 43 | @Override 44 | public int code() { 45 | return code; 46 | } 47 | 48 | } 49 | 50 | @AllArgsConstructor 51 | enum OAuth2 implements ErrorType { 52 | INVALID_GRANT("invalid_grant", "Invalid or expired grant", 400), 53 | INVALID_REQUEST("invalid_request", "Invalid request parameters", 400), 54 | INVALID_RESPONSE_TYPE("invalid_response_type", "Invalid response type", 400), 55 | INVALID_SCOPE("invalid_scope", "Invalid or unauthorized scope", 400), 56 | INVALID_REDIRECT_URI("invalid_redirect_uri", "Invalid redirect URI", 400), 57 | UNSUPPORTED_GRANT_TYPE("unsupported_grant_type", "Unsupported grant type", 400), 58 | INVALID_ACCOUNT("invalid_account", "Invalid or unauthorized account", 400), 59 | 60 | INVALID_CLIENT("invalid_client", "Client authentication failed", 401), 61 | 62 | UNAUTHORIZED_CLIENT("unauthorized_client", "Unauthorized client for this grant type", 403), 63 | ACCESS_DENIED("access_denied", "Resource access denied", 403), 64 | INSUFFICIENT_SCOPE("insufficient_scope", "Insufficient scope for this resource", 403), 65 | INTERACTION_REQUIRED("interaction_required", "User interaction required", 403), 66 | LOGIN_REQUIRED("login_required", "User login required", 403), 67 | CONSENT_REQUIRED("consent_required", "User consent required", 403), 68 | ACCOUNT_SELECTION_REQUIRED("account_selection_required", "Account selection required", 403), 69 | 70 | SERVER_ERROR("server_error", "Unexpected server error", 500), 71 | 72 | TEMPORARILY_UNAVAILABLE("temporarily_unavailable", "Service temporarily unavailable", 503);; 73 | 74 | private final String error; 75 | private final String description; 76 | private final String errorURI; 77 | private final int code; 78 | 79 | OAuth2(String error, String description, int code) { 80 | this.error = error; 81 | this.description = description; 82 | this.code = code; 83 | this.errorURI = null; 84 | } 85 | 86 | public String description() { 87 | return description; 88 | } 89 | 90 | public String error() { 91 | return error; 92 | } 93 | 94 | public String errorUri() { 95 | return errorURI; 96 | } 97 | 98 | @Override 99 | public String message() { 100 | return error; 101 | } 102 | 103 | @Override 104 | public int code() { 105 | return code; 106 | } 107 | 108 | public static OAuth2 of(String error, String description) { 109 | for (OAuth2 o : values()) { 110 | if (o.error().equals(error) && o.description().equals(description)) { 111 | return o; 112 | } 113 | } 114 | return null; 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain; 2 | 3 | import com.qingyou.sso.domain.user.Account; 4 | import io.vertx.codegen.annotations.Nullable; 5 | import io.vertx.core.Future; 6 | 7 | public interface AccountRepository{ 8 | 9 | Future findByUsername(String username); 10 | 11 | Future<@Nullable Account> insert(Account account); 12 | 13 | Future updatePasswordByIdAndUsername(Long userId, String username, String password, String salt); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/ThirdPartyRedirectRepository.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain; 2 | 3 | import com.qingyou.sso.domain.oauth.ThirdPartyApp; 4 | import com.qingyou.sso.domain.oauth.ThirdPartyRedirect; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | 11 | public interface ThirdPartyRedirectRepository { 12 | Future> findByThirdPartyApp(ThirdPartyApp thirdPartyApp); 13 | Future refreshByThirdPartyApp(ThirdPartyApp thirdPartyApp, Collection collection); 14 | 15 | Future<@Nullable ThirdPartyRedirect> insert(ThirdPartyRedirect thirdPartyRedirect); 16 | } 17 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/ThirdPartyRepository.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain; 2 | 3 | 4 | import com.qingyou.sso.domain.oauth.ThirdPartyApp; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | 8 | public interface ThirdPartyRepository { 9 | Future findById(Long id); 10 | Future findByClientId(String clientId); 11 | 12 | Future<@Nullable ThirdPartyApp> insert(ThirdPartyApp app); 13 | } 14 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/ThirdPartyRequiredUserInfoRepository.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain; 2 | 3 | import com.qingyou.sso.domain.oauth.ThirdPartyApp; 4 | import com.qingyou.sso.domain.oauth.ThirdPartyRequiredUserInfo; 5 | import com.qingyou.sso.domain.user.User; 6 | import io.vertx.codegen.annotations.Nullable; 7 | import io.vertx.core.Future; 8 | 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | public interface ThirdPartyRequiredUserInfoRepository { 13 | Future> findByThirdPartyApp(ThirdPartyApp thirdPartyApp); 14 | Future refreshByThirdPartyApp(ThirdPartyApp thirdPartyApp, Collection collection); 15 | 16 | Future<@Nullable ThirdPartyRequiredUserInfo> insert(ThirdPartyRequiredUserInfo info); 17 | } 18 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/UserInfoRepository.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain; 2 | 3 | import com.qingyou.sso.api.constants.PlatformType; 4 | import com.qingyou.sso.domain.user.UserInfo; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | 11 | public interface UserInfoRepository { 12 | 13 | Future> findByUserId(Long userId); 14 | Future> findByUserIdAndPlatformTypes(Long userId, Collection platformTypes); 15 | 16 | Future<@Nullable UserInfo> insert(UserInfo info); 17 | } 18 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain; 2 | 3 | 4 | import com.qingyou.sso.domain.user.User; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | 8 | public interface UserRepository { 9 | Future<@Nullable User> findById(Object id); 10 | Future<@Nullable User> findByUsername(String username); 11 | Future<@Nullable User> findByName(String name); 12 | Future<@Nullable User> findByEmail(String email); 13 | Future<@Nullable User> findByPhone(String phone); 14 | Future<@Nullable User> insert(User user); 15 | } 16 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/impl/AccountRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain.impl; 2 | 3 | import com.qingyou.sso.domain.user.Account; 4 | import com.qingyou.sso.infra.repository.domain.AccountRepository; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | import io.vertx.sqlclient.Row; 8 | import io.vertx.sqlclient.SqlClient; 9 | import io.vertx.sqlclient.Tuple; 10 | import lombok.AllArgsConstructor; 11 | 12 | @AllArgsConstructor 13 | public class AccountRepositoryImpl implements AccountRepository { 14 | private final SqlClient client; 15 | 16 | 17 | @Override 18 | public Future findByUsername(String username) { 19 | return client.preparedQuery("SELECT user_id,username,password,salt FROM sso_user.account WHERE username = $1") 20 | .execute(Tuple.of(username)) 21 | .map(rows -> { 22 | for (Row row : rows) { 23 | Account account = new Account(); 24 | account.setUserId(row.getLong("user_id")); 25 | account.setUsername(username); 26 | account.setPassword(row.getString("password")); 27 | account.setSalt(row.getString("salt")); 28 | return account; 29 | } 30 | return null; 31 | }); 32 | } 33 | 34 | @Override 35 | public Future<@Nullable Account> insert(Account account) { 36 | return client.preparedQuery("INSERT INTO sso_user.account(user_id,username,password,salt) VALUES ($1, $2, $3, $4)") 37 | .execute(Tuple.of(account.getUserId(),account.getUsername(),account.getPassword(),account.getSalt())) 38 | .map(rows -> account); 39 | } 40 | 41 | @Override 42 | public Future updatePasswordByIdAndUsername(Long userId, String username, String password, String salt) { 43 | return client.preparedQuery("UPDATE sso_user.account SET password = $1, salt = $2 WHERE user_id = $3 AND username = $4") 44 | .execute(Tuple.of(password,salt,userId,username)).mapEmpty(); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/impl/ThirdPartyRedirectRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain.impl; 2 | 3 | import com.qingyou.sso.domain.oauth.ThirdPartyApp; 4 | import com.qingyou.sso.domain.oauth.ThirdPartyRedirect; 5 | import com.qingyou.sso.infra.repository.domain.ThirdPartyRedirectRepository; 6 | import io.vertx.codegen.annotations.Nullable; 7 | import io.vertx.core.Future; 8 | import io.vertx.sqlclient.Row; 9 | import io.vertx.sqlclient.SqlClient; 10 | import io.vertx.sqlclient.Tuple; 11 | import lombok.AllArgsConstructor; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | 17 | @AllArgsConstructor 18 | public class ThirdPartyRedirectRepositoryImpl implements ThirdPartyRedirectRepository { 19 | 20 | private final SqlClient client; 21 | 22 | @Override 23 | public Future> findByThirdPartyApp(ThirdPartyApp thirdPartyApp) { 24 | return client.preparedQuery("SELECT id,uri FROM sso_oauth.third_party_redirect WHERE third_party_app_id = $1") 25 | .execute(Tuple.of(thirdPartyApp.getId())) 26 | .map(rows -> { 27 | List thirdPartyRedirects = new ArrayList<>(); 28 | for (Row row : rows) { 29 | ThirdPartyRedirect redirect = new ThirdPartyRedirect(); 30 | redirect.setThirdPartyApp(thirdPartyApp); 31 | redirect.setId(row.getLong("id")); 32 | redirect.setURI(row.getString("uri")); 33 | thirdPartyRedirects.add(redirect); 34 | } 35 | return thirdPartyRedirects; 36 | }); 37 | } 38 | 39 | @Override 40 | public Future refreshByThirdPartyApp(ThirdPartyApp thirdPartyApp, Collection collection) { 41 | return client.preparedQuery("DELETE FROM sso_oauth.third_party_redirect WHERE third_party_app_id = $1") 42 | .execute(Tuple.of(thirdPartyApp.getId())).flatMap(rows -> { 43 | return client.preparedQuery("INSERT INTO sso_oauth.third_party_redirect(uri,third_party_app_id) VALUES ($1, $2, ) RETURNING id") 44 | .executeBatch(collection.stream() 45 | .map(thirdPartyRedirect -> Tuple.of(thirdPartyRedirect.getURI(),thirdPartyRedirect.getThirdPartyApp().getId())) 46 | .toList()) 47 | .map(r -> { 48 | var i = r.iterator(); 49 | for(var thirdPartyRedirect: collection){ 50 | if (i.hasNext()){ 51 | thirdPartyRedirect.setId(i.next().getLong("id")); 52 | } 53 | } 54 | return null; 55 | }); 56 | }).mapEmpty(); 57 | } 58 | 59 | @Override 60 | public Future<@Nullable ThirdPartyRedirect> insert(ThirdPartyRedirect thirdPartyRedirect) { 61 | return client.preparedQuery("INSERT INTO sso_oauth.third_party_redirect(id,uri,third_party_app_id) VALUES ($1, $2, $3) RETURNING id") 62 | .execute(Tuple.of(thirdPartyRedirect.getId(),thirdPartyRedirect.getURI(),thirdPartyRedirect.getThirdPartyApp().getId())) 63 | .map(rows -> { 64 | for(Row row: rows){ 65 | thirdPartyRedirect.setId(row.getLong("id")); 66 | } 67 | return thirdPartyRedirect; 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/impl/ThirdPartyRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain.impl; 2 | 3 | import com.qingyou.sso.domain.oauth.ThirdPartyApp; 4 | import com.qingyou.sso.infra.repository.domain.ThirdPartyRepository; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | import io.vertx.sqlclient.Row; 8 | import io.vertx.sqlclient.SqlClient; 9 | import io.vertx.sqlclient.Tuple; 10 | import lombok.AllArgsConstructor; 11 | 12 | @AllArgsConstructor 13 | public class ThirdPartyRepositoryImpl implements ThirdPartyRepository { 14 | 15 | private final SqlClient client; 16 | 17 | @Override 18 | public Future findById(Long id) { 19 | return client.preparedQuery("SELECT id,app_name,client_id,client_secret FROM sso_oauth.third_party_app WHERE id = $1") 20 | .execute(Tuple.of(id)) 21 | .map(rows -> { 22 | for (Row row: rows){ 23 | ThirdPartyApp app = new ThirdPartyApp(); 24 | app.setId(row.getLong("id")); 25 | app.setClientId(row.getString("client_id")); 26 | app.setAppName(row.getString("app_name")); 27 | app.setClientSecret(row.getString("client_secret")); 28 | return app; 29 | } 30 | return null; 31 | }); 32 | } 33 | 34 | @Override 35 | public Future findByClientId(String clientId) { 36 | return client.preparedQuery("SELECT id,app_name,client_id,client_secret FROM sso_oauth.third_party_app WHERE client_id = $1") 37 | .execute(Tuple.of(clientId)) 38 | .map(rows -> { 39 | for (Row row: rows){ 40 | ThirdPartyApp app = new ThirdPartyApp(); 41 | app.setId(row.getLong("id")); 42 | app.setClientId(row.getString("client_id")); 43 | app.setAppName(row.getString("app_name")); 44 | app.setClientSecret(row.getString("client_secret")); 45 | return app; 46 | } 47 | return null; 48 | }); 49 | } 50 | 51 | @Override 52 | public Future<@Nullable ThirdPartyApp> insert(ThirdPartyApp app) { 53 | return client.preparedQuery("INSERT INTO sso_oauth.third_party_app(app_name,client_id,client_secret) VALUES ($1, $2, $3) RETURNING id") 54 | .execute(Tuple.of(app.getAppName(),app.getClientId(),app.getClientSecret())) 55 | .map(rows -> { 56 | for (Row row: rows){ 57 | app.setId(row.getLong("id")); 58 | } 59 | return app; 60 | }); 61 | } 62 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/impl/ThirdPartyRequiredUserInfoRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain.impl; 2 | 3 | import com.qingyou.sso.api.constants.DataType; 4 | import com.qingyou.sso.api.constants.PlatformType; 5 | import com.qingyou.sso.domain.oauth.ThirdPartyApp; 6 | import com.qingyou.sso.domain.oauth.ThirdPartyRequiredUserInfo; 7 | import com.qingyou.sso.infra.repository.domain.ThirdPartyRequiredUserInfoRepository; 8 | import io.vertx.codegen.annotations.Nullable; 9 | import io.vertx.core.Future; 10 | import io.vertx.sqlclient.Row; 11 | import io.vertx.sqlclient.SqlClient; 12 | import io.vertx.sqlclient.Tuple; 13 | import lombok.AllArgsConstructor; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collection; 17 | import java.util.List; 18 | 19 | @AllArgsConstructor 20 | public class ThirdPartyRequiredUserInfoRepositoryImpl implements ThirdPartyRequiredUserInfoRepository { 21 | private final SqlClient client; 22 | 23 | @Override 24 | public Future> findByThirdPartyApp(ThirdPartyApp thirdPartyApp) { 25 | return client.preparedQuery("SELECT id,data_type,platform_type FROM sso_oauth.third_party_required_user_info WHERE third_party_app_id = $1") 26 | .execute(Tuple.of(thirdPartyApp.getId())) 27 | .map(rows -> { 28 | List infos = new ArrayList<>(); 29 | for (Row row : rows) { 30 | ThirdPartyRequiredUserInfo info = new ThirdPartyRequiredUserInfo(); 31 | info.setThirdPartyApp(thirdPartyApp); 32 | info.setId(row.getLong("id")); 33 | info.setDataType(DataType.values()[row.getInteger("data_type")]); 34 | info.setPlatformType(PlatformType.values()[row.getInteger("platform_type")]); 35 | } 36 | return infos; 37 | }); 38 | } 39 | 40 | @Override 41 | public Future refreshByThirdPartyApp(ThirdPartyApp thirdPartyApp, Collection collection) { 42 | return client.preparedQuery("DELETE FROM sso_oauth.third_party_required_user_info WHERE third_party_app_id = $1") 43 | .execute(Tuple.of(thirdPartyApp.getId())).flatMap(rows -> { 44 | return client.preparedQuery("INSERT INTO sso_oauth.third_party_required_user_info(data_type,platform_type,third_party_app_id) VALUES ($1, $2, $3) RETURNING id") 45 | .executeBatch(collection.stream() 46 | .map(info -> Tuple.of(info.getDataType().ordinal(),info.getPlatformType().ordinal(),info.getThirdPartyApp().getId())) 47 | .toList()) 48 | .map(r -> { 49 | var i = r.iterator(); 50 | for(var info: collection){ 51 | if (i.hasNext()){ 52 | info.setId(i.next().getLong("id")); 53 | } 54 | } 55 | return null; 56 | }); 57 | }).mapEmpty(); 58 | } 59 | 60 | @Override 61 | public Future<@Nullable ThirdPartyRequiredUserInfo> insert(ThirdPartyRequiredUserInfo info) { 62 | return client.preparedQuery("INSERT INTO sso_oauth.third_party_required_user_info(data_type,platform_type,third_party_app_id) VALUES ($1, $2, $3) RETURNING id") 63 | .execute(Tuple.of(info.getDataType().ordinal(),info.getPlatformType().ordinal(),info.getThirdPartyApp().getId())) 64 | .map(rows -> { 65 | for(Row row: rows){ 66 | info.setId(row.getLong("id")); 67 | } 68 | return info; 69 | }); 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/impl/UserInfoRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain.impl; 2 | 3 | import com.qingyou.sso.api.constants.DataType; 4 | import com.qingyou.sso.domain.user.User; 5 | import com.qingyou.sso.domain.user.UserInfo; 6 | import com.qingyou.sso.api.constants.PlatformType; 7 | import com.qingyou.sso.infra.repository.domain.UserInfoRepository; 8 | import com.qingyou.sso.utils.StringUtils; 9 | import io.vertx.codegen.annotations.Nullable; 10 | import io.vertx.core.Future; 11 | import io.vertx.sqlclient.Row; 12 | import io.vertx.sqlclient.SqlClient; 13 | import io.vertx.sqlclient.Tuple; 14 | import lombok.AllArgsConstructor; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.List; 19 | 20 | @AllArgsConstructor 21 | public class UserInfoRepositoryImpl implements UserInfoRepository { 22 | private final SqlClient client; 23 | 24 | @Override 25 | public Future> findByUserId(Long userId) { 26 | return client.preparedQuery("SELECT id,metadata,data_type,platform_type FROM sso_user.user_info WHERE user_id = $1") 27 | .execute(Tuple.of(userId)) 28 | .map(rows -> { 29 | List infos = new ArrayList<>(); 30 | for (Row row : rows) { 31 | UserInfo info = new UserInfo(); 32 | info.setId(row.getLong("id")); 33 | info.setMetadata(row.getString("metadata")); 34 | info.setDataType(DataType.values()[row.getInteger("data_type")]); 35 | info.setPlatformType(PlatformType.values()[row.getInteger("platform_type")]); 36 | infos.add(info); 37 | } 38 | return infos; 39 | }); 40 | } 41 | 42 | @Override 43 | public Future> findByUserIdAndPlatformTypes(Long userId, Collection platformTypes) { 44 | return client.preparedQuery("SELECT id,metadata,data_type,platform_type FROM sso_user.user_info WHERE user_id = $1 AND platform_type IN " + StringUtils.union(2, platformTypes)) 45 | .execute(Tuple.of(userId) 46 | .addArrayOfInteger(platformTypes.stream() 47 | .mapToInt(Enum::ordinal) 48 | .boxed() 49 | .toArray(Integer[]::new))) 50 | .map(rows -> { 51 | List infos = new ArrayList<>(); 52 | for (Row row : rows) { 53 | UserInfo info = new UserInfo(); 54 | info.setId(row.getLong("id")); 55 | info.setMetadata(row.getString("metadata")); 56 | info.setDataType(DataType.values()[row.getInteger("data_type")]); 57 | info.setPlatformType(PlatformType.values()[row.getInteger("platform_type")]); 58 | infos.add(info); 59 | } 60 | return infos; 61 | }); 62 | } 63 | 64 | @Override 65 | public Future<@Nullable UserInfo> insert(UserInfo info) { 66 | return client.preparedQuery("INSERT INTO sso_user.user_info(metadata,data_type,platform_type,user_id) VALUES ($1, $2, $3, $4) RETURNING id") 67 | .execute(Tuple.of(info.getMetadata(),info.getDataType().ordinal(),info.getPlatformType().ordinal(),info.getUser().getId())) 68 | .map(rows -> { 69 | for (Row row : rows) { 70 | info.setId(row.getLong("id")); 71 | } 72 | return info; 73 | }); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/repository/domain/impl/UserRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.repository.domain.impl; 2 | 3 | import com.qingyou.sso.domain.user.User; 4 | import com.qingyou.sso.infra.repository.domain.UserRepository; 5 | import io.vertx.codegen.annotations.Nullable; 6 | import io.vertx.core.Future; 7 | import io.vertx.sqlclient.Row; 8 | import io.vertx.sqlclient.SqlClient; 9 | import io.vertx.sqlclient.Tuple; 10 | import lombok.AllArgsConstructor; 11 | 12 | 13 | @AllArgsConstructor 14 | public class UserRepositoryImpl implements UserRepository { 15 | private SqlClient client; 16 | 17 | @Override 18 | public Future<@Nullable User> findById(Object id) { 19 | return client.preparedQuery("SELECT id,name,email,phone FROM sso_user.user WHERE id = $1") 20 | .execute(Tuple.of(id)) 21 | .map(rows -> { 22 | for (Row row : rows) { 23 | User user = new User(); 24 | user.setId(row.getLong("id")); 25 | user.setName(row.getString("name")); 26 | user.setEmail(row.getString("email")); 27 | user.setPhone(row.getString("phone")); 28 | return user; 29 | } 30 | return null; 31 | }); 32 | } 33 | 34 | @Override 35 | public Future findByUsername(String username) { 36 | return client.preparedQuery("SELECT id,name,email,phone FROM sso_user.user WHERE id IN (SELECT user_id FROM sso_user.account WHERE username = $1)") 37 | .execute(Tuple.of(username)) 38 | .map(rows -> { 39 | for (Row row : rows) { 40 | User user = new User(); 41 | user.setId(row.getLong("id")); 42 | user.setName(row.getString("name")); 43 | user.setEmail(row.getString("email")); 44 | user.setPhone(row.getString("phone")); 45 | return user; 46 | } 47 | return null; 48 | }); 49 | } 50 | 51 | @Override 52 | public Future<@Nullable User> findByName(String name) { 53 | return client.preparedQuery("SELECT id,name,email,phone FROM sso_user.user WHERE name = $1") 54 | .execute(Tuple.of(name)) 55 | .map(rows -> { 56 | for (Row row : rows) { 57 | User user = new User(); 58 | user.setId(row.getLong("id")); 59 | user.setName(row.getString("name")); 60 | user.setEmail(row.getString("email")); 61 | user.setPhone(row.getString("phone")); 62 | return user; 63 | } 64 | return null; 65 | }); 66 | } 67 | 68 | @Override 69 | public Future<@Nullable User> findByEmail(String email) { 70 | return client.preparedQuery("SELECT id,name,email,phone FROM sso_user.user WHERE email = $1") 71 | .execute(Tuple.of(email)) 72 | .map(rows -> { 73 | for (Row row : rows) { 74 | User user = new User(); 75 | user.setId(row.getLong("id")); 76 | user.setName(row.getString("name")); 77 | user.setEmail(row.getString("email")); 78 | user.setPhone(row.getString("phone")); 79 | return user; 80 | } 81 | return null; 82 | }); 83 | } 84 | 85 | @Override 86 | public Future<@Nullable User> findByPhone(String phone) { 87 | return client.preparedQuery("SELECT id,name,email,phone FROM sso_user.user WHERE phone = $1") 88 | .execute(Tuple.of(phone)) 89 | .map(rows -> { 90 | for (Row row : rows) { 91 | User user = new User(); 92 | user.setId(row.getLong("id")); 93 | user.setName(row.getString("name")); 94 | user.setEmail(row.getString("email")); 95 | user.setPhone(row.getString("phone")); 96 | return user; 97 | } 98 | return null; 99 | }); 100 | } 101 | 102 | @Override 103 | public Future<@Nullable User> insert(User user) { 104 | return client.preparedQuery("INSERT INTO sso_user.user(name,email,phone) VALUES ($1, $2, $3) RETURNING id") 105 | .execute(Tuple.of(user.getName(),user.getEmail(),user.getPhone())) 106 | .map(rows -> { 107 | for (Row row : rows) { 108 | user.setId(row.getLong("id")); 109 | } 110 | return user; 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/request/HttpRequestBody.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.request; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.qingyou.sso.infra.exception.BizException; 5 | import io.vertx.core.Future; 6 | import io.vertx.ext.web.RoutingContext; 7 | import lombok.AllArgsConstructor; 8 | 9 | import java.io.IOException;; 10 | 11 | @AllArgsConstructor 12 | public class HttpRequestBody { 13 | private final ObjectMapper objectMapper; 14 | 15 | public Future json(RoutingContext routingContext, Class clazz) { 16 | return routingContext.request().body().map(buffer -> { 17 | try { 18 | return objectMapper.readValue(buffer.getBytes(), clazz); 19 | } catch (IOException e) { 20 | throw new BizException(e); 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/response/EventMessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.response; 2 | 3 | import com.qingyou.sso.api.dto.Result; 4 | import com.qingyou.sso.infra.exception.BizException; 5 | import com.qingyou.sso.infra.exception.ErrorType; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.Handler; 8 | import io.vertx.core.eventbus.Message; 9 | import io.vertx.core.json.Json; 10 | import lombok.AllArgsConstructor; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.function.Function; 15 | 16 | @AllArgsConstructor 17 | public class EventMessageHandler implements Handler> { 18 | 19 | private final Function> dataHandler; 20 | 21 | private static final Logger log = LoggerFactory.getLogger(EventMessageHandler.class); 22 | 23 | @Override 24 | public void handle(Message message) { 25 | try { 26 | Future future; 27 | try { 28 | future = dataHandler.apply(message.body()); 29 | } catch (Throwable ex) { 30 | fail(message, ex); 31 | return; 32 | } 33 | end(message, future); 34 | } catch (Throwable ex) { 35 | log.error("Un handle RuntimeException(message={})", ex.getMessage(), ex); 36 | message.fail(500, ex.getMessage()); 37 | } 38 | } 39 | 40 | public static EventMessageHandler wrap(Function> dataHandler) { 41 | return new EventMessageHandler<>(dataHandler); 42 | } 43 | 44 | public static EventMessageHandler wrap(Function deserializer, Function> dataHandler) { 45 | return new EventMessageHandler<>(json -> { 46 | T data = deserializer.apply(json); 47 | return dataHandler.apply(data); 48 | }); 49 | } 50 | 51 | private void end(Message message, Future data) { 52 | data.onComplete(result -> { 53 | if (result.succeeded()) success(message, result.result()); 54 | else fail(message, result.cause()); 55 | }); 56 | } 57 | 58 | private void success(Message message, R data) { 59 | message.reply(Json.encodePrettily(Result.success(data))); 60 | } 61 | 62 | private void fail(Message message, Throwable ex) { 63 | if (ex == null) { 64 | Result result = Result.failed(ErrorType.Inner.Default.code(), ErrorType.Inner.Default.message()); 65 | message.reply(Json.encodePrettily(result)); 66 | log.error("RuntimeException(message=)"); 67 | } else if (ex instanceof BizException bizException) { 68 | var error = bizException.getErrorType(); 69 | Result result; 70 | if (error instanceof ErrorType.Inner) result = Result.failed(error.code(), error.message()); 71 | else result = Result.failed(error.code(), ex.getMessage()); 72 | message.reply(Json.encodePrettily(result)); 73 | log.error("BizException(code={},error={},message={})", error.code(), error.message(), ex.getMessage(), ex); 74 | } else { 75 | Result result = Result.failed(ErrorType.Inner.Default.code(), ErrorType.Inner.Default.message()); 76 | message.reply(Json.encodePrettily(result)); 77 | log.error("RuntimeException(message={})", ex.getMessage(), ex); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/response/GlobalHttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.response; 2 | 3 | import com.qingyou.sso.api.dto.Result; 4 | import com.qingyou.sso.infra.exception.BizException; 5 | import com.qingyou.sso.infra.exception.ErrorType; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.json.Json; 8 | import io.vertx.ext.web.RoutingContext; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public interface GlobalHttpResponse { 13 | 14 | String CONTENT_TYPE_HEADER = "Content-Type"; 15 | String APPLICATION_JSON = "application/json"; 16 | Logger log = LoggerFactory.getLogger(GlobalHttpResponse.class); 17 | 18 | static void end(RoutingContext routingContext, Future data, Logger log) { 19 | data.onComplete(result -> { 20 | if (result.succeeded()) success(routingContext, result.result()); 21 | else fail(routingContext, result.cause(), log); 22 | }); 23 | } 24 | 25 | static void end(RoutingContext routingContext, Future data) { 26 | end(routingContext, data, log); 27 | } 28 | 29 | static void success(RoutingContext routingContext, T data) { 30 | routingContext.response().setStatusCode(200) 31 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 32 | .end(Json.encodeToBuffer(Result.success(data))); 33 | } 34 | 35 | static void fail(RoutingContext routingContext, Throwable ex) { 36 | fail(routingContext, ex, log); 37 | } 38 | 39 | static void fail(RoutingContext routingContext, Throwable ex, Logger log) { 40 | if (ex == null) { 41 | Result result = Result.failed(ErrorType.Inner.Default.code(), ErrorType.Inner.Default.message()); 42 | routingContext.response().setStatusCode(500) 43 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 44 | .end(Json.encodeToBuffer(result)); 45 | log.error("RuntimeException(message=)"); 46 | } else if (ex instanceof BizException bizException) { 47 | var error = bizException.getErrorType(); 48 | Result result; 49 | if (error instanceof ErrorType.Inner) result = Result.failed(error.code(), error.message()); 50 | else result = Result.failed(error.code(), ex.getMessage()); 51 | routingContext.response().setStatusCode(200) 52 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 53 | .end(Json.encodeToBuffer(result)); 54 | log.error("BizException(code={},error={},message={})", error.code(), error.message(), ex.getMessage(), ex); 55 | } else { 56 | Result result = Result.failed(ErrorType.Inner.Default.code(), ErrorType.Inner.Default.message()); 57 | routingContext.response().setStatusCode(500) 58 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 59 | .end(Json.encodeToBuffer(result)); 60 | log.error("RuntimeException(message={})", ex.getMessage(), ex); 61 | } 62 | } 63 | 64 | static GlobalHttpResponseWrap wrap(ServiceApi serviceApi) { 65 | return new GlobalHttpResponseWrap<>(serviceApi, log); 66 | } 67 | 68 | static GlobalHttpResponseWrap wrap(ServiceApi serviceApi, Logger log) { 69 | return new GlobalHttpResponseWrap<>(serviceApi, log); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/response/GlobalHttpResponseWrap.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.response; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Handler; 5 | import io.vertx.ext.web.RoutingContext; 6 | import lombok.AllArgsConstructor; 7 | import org.slf4j.Logger; 8 | 9 | @AllArgsConstructor 10 | public class GlobalHttpResponseWrap implements Handler { 11 | 12 | private final ServiceApi serviceApi; 13 | private final Logger log; 14 | 15 | @Override 16 | public void handle(RoutingContext routingContext) { 17 | try { 18 | Future future; 19 | try { 20 | future = serviceApi.apply(routingContext); 21 | } catch (Throwable ex) { 22 | GlobalHttpResponse.fail(routingContext, ex,log); 23 | return; 24 | } 25 | GlobalHttpResponse.end(routingContext, future,log); 26 | } catch (Throwable ex) { 27 | log.error("Un handle RuntimeException(message={})", ex.getMessage(), ex); 28 | routingContext.fail(500); 29 | } 30 | } 31 | 32 | 33 | } -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/response/OAuth2HttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.response; 2 | 3 | import com.qingyou.sso.infra.exception.BizException; 4 | import com.qingyou.sso.infra.exception.ErrorType; 5 | import io.vertx.core.json.Json; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.web.RoutingContext; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public interface OAuth2HttpResponse { 12 | 13 | String CONTENT_TYPE_HEADER = "Content-Type"; 14 | String APPLICATION_JSON = "application/json"; 15 | Logger log = LoggerFactory.getLogger(OAuth2HttpResponse.class); 16 | 17 | static void fail(RoutingContext routingContext, Throwable ex) { 18 | fail(routingContext, ex, log); 19 | } 20 | 21 | static void fail(RoutingContext routingContext, Throwable ex, Logger log) { 22 | if (ex == null) { 23 | log.error("RuntimeException(message=)"); 24 | fail(routingContext, ErrorType.OAuth2.SERVER_ERROR); 25 | } else if (ex instanceof BizException bizException) { 26 | ErrorType error = bizException.getErrorType(); 27 | log.error("BizException(code={},error={},message={})", error.code(), error.message(), ex.getMessage(), ex); 28 | if (error instanceof ErrorType.OAuth2) fail(routingContext, (ErrorType.OAuth2) error); 29 | else fail(routingContext, ErrorType.OAuth2.SERVER_ERROR); 30 | } else { 31 | log.error("RuntimeException(message={})", ex.getMessage(), ex); 32 | fail(routingContext, ErrorType.OAuth2.SERVER_ERROR); 33 | } 34 | } 35 | 36 | static void fail(RoutingContext routingContext, ErrorType.OAuth2 errorType) { 37 | fail(routingContext, errorType, log); 38 | } 39 | 40 | static void fail(RoutingContext routingContext, ErrorType.OAuth2 errorType, Logger log) { 41 | log.error("Invalid Oauth 2.0 request, error={}, description={}", errorType.error(), errorType.description()); 42 | routingContext.response() 43 | .setStatusCode(errorType.code()) 44 | .end(Json.encodeToBuffer(new JsonObject() 45 | .put("error", errorType.error()) 46 | .put("error_description", errorType.description())) 47 | ); 48 | } 49 | 50 | static void success(RoutingContext routingContext, T data) { 51 | routingContext.response().setStatusCode(200) 52 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 53 | .end(Json.encodeToBuffer(data)); 54 | } 55 | 56 | static void failRedirect(RoutingContext routingContext, String url, Throwable ex) { 57 | failRedirect(routingContext, url, ex, log); 58 | } 59 | 60 | static void failRedirect(RoutingContext routingContext, String url, Throwable ex, Logger log) { 61 | if (ex instanceof BizException bizException) { 62 | ErrorType errorType = bizException.getErrorType(); 63 | log.error("BizException(code={},error={},message={})", errorType.code(), errorType.message(), ex.getMessage(), ex); 64 | if (errorType instanceof ErrorType.OAuth2 error) { 65 | failRedirect(routingContext, url, error, log); 66 | return; 67 | } 68 | } else { 69 | log.error("RuntimeException(message={})", ex.getMessage(), ex); 70 | } 71 | ErrorType.OAuth2 error = ErrorType.OAuth2.SERVER_ERROR; 72 | failRedirect(routingContext, url, error, log); 73 | } 74 | 75 | static void failRedirect(RoutingContext routingContext, String url, ErrorType.OAuth2 errorType) { 76 | failRedirect(routingContext, url, errorType, log); 77 | } 78 | 79 | static void failRedirect(RoutingContext routingContext, String url, ErrorType.OAuth2 errorType, Logger log) { 80 | log.error("Invalid Oauth 2.0 request redirect to url={}, error={}, description={}", url, errorType.error(), errorType.description()); 81 | routingContext.redirect(url +"?error=" +errorType.error() +"&error_description=" + errorType.description()); 82 | } 83 | 84 | static void redirect(RoutingContext routingContext, String url) { 85 | routingContext.redirect(url); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/infra/response/ServiceApi.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.infra.response; 2 | 3 | 4 | import io.vertx.core.Future; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | import java.util.function.Function; 8 | 9 | /** 10 | * A service handle routing params and convert to response results 11 | * @param 12 | */ 13 | public interface ServiceApi extends Function> { 14 | } 15 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/utils/PasswordEncodeUtils.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.utils; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import javax.crypto.SecretKeyFactory; 6 | import javax.crypto.spec.PBEKeySpec; 7 | import java.security.SecureRandom; 8 | import java.util.Base64; 9 | 10 | public class PasswordEncodeUtils { 11 | 12 | @SneakyThrows 13 | public static EncodedPassword encode(String password) { 14 | SecureRandom random = new SecureRandom(); 15 | byte[] salt = new byte[16]; 16 | random.nextBytes(salt); 17 | PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256); 18 | SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); 19 | byte[] bytes = factory.generateSecret(spec).getEncoded(); 20 | return new EncodedPassword(Base64.getEncoder().encodeToString(bytes), Base64.getEncoder().encodeToString(salt)); 21 | } 22 | 23 | @SneakyThrows 24 | public static EncodedPassword encode(String password, String salt) { 25 | PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), Base64.getDecoder().decode(salt), 65536, 256); 26 | SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); 27 | byte[] bytes = factory.generateSecret(spec).getEncoded(); 28 | return new EncodedPassword(Base64.getEncoder().encodeToString(bytes), salt); 29 | } 30 | 31 | public record EncodedPassword(String encoded, String salt) { 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/utils/ResourceUtils.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.utils; 2 | 3 | import java.io.InputStream; 4 | 5 | public class ResourceUtils { 6 | public static InputStream load(String file){ 7 | return ResourceUtils.class.getClassLoader().getResourceAsStream(file); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sso-common/src/main/java/com/qingyou/sso/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.utils; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.Collection; 5 | 6 | public final class StringUtils { 7 | 8 | private static final SecureRandom RANDOM = new SecureRandom(); 9 | private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 10 | 11 | public static String random(int length) { 12 | if (length <= 0) { 13 | throw new IllegalArgumentException("Length must be greater than 0"); 14 | } 15 | StringBuilder sb = new StringBuilder(length); 16 | for (int i = 0; i < length; i++) { 17 | sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); 18 | } 19 | return sb.toString(); 20 | } 21 | 22 | public static String randomNum(int length) { 23 | int num = RANDOM.nextInt((int) Math.pow(10,length-1), (int) Math.pow(10,length)-1); 24 | return String.valueOf(num); 25 | } 26 | 27 | public static String union(int num) { 28 | return union(1 , num); 29 | } 30 | 31 | public static String union(int from, int num) { 32 | if (from < 1 || num < 1) return ""; 33 | if (num == 1) return "($" + from + ")"; 34 | StringBuilder sb = new StringBuilder("($1"); 35 | for (int i = from; i < from + num; i++) { 36 | sb.append(",$").append(i); 37 | } 38 | return sb.append(")").toString(); 39 | } 40 | 41 | public static String union(Collection collection) { 42 | if (collection.isEmpty()) return ""; 43 | return union(1, collection.size()); 44 | } 45 | 46 | public static String union(int from, Collection collection) { 47 | if (collection.isEmpty()) return ""; 48 | return union(from, collection.size()); 49 | } 50 | 51 | public static String unionRepeat(int num, int repeat) { 52 | return unionRepeat(1, num, repeat); 53 | } 54 | 55 | public static String unionRepeat(int from, int num, int repeat) { 56 | if (repeat <= 0) return null; 57 | if (repeat == 1) return union(from, num); 58 | StringBuilder sb = new StringBuilder(union(from, num)); 59 | for (int i = 1; i < repeat; i++) { 60 | sb.append(",").append(union(i, repeat)); 61 | } 62 | return sb.toString(); 63 | } 64 | 65 | public static String unionRepeat(int from, int fields, Collection collection) { 66 | return unionRepeat(from,fields, collection.size()); 67 | } 68 | 69 | public static String unionRepeat(int fields, Collection collection) { 70 | return unionRepeat(fields, collection.size()); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /sso-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | core-sso 9 | 1.0.1 10 | 11 | 12 | sso-core 13 | 14 | 15 | 17 16 | 17 17 | UTF-8 18 | io.vertx.core.Launcher 19 | 20 | com.qingyou.sso.verticle.CoreVerticle 21 | 22 | 23 | 24 | 25 | 26 | org.projectlombok 27 | lombok 28 | 1.18.34 29 | provided 30 | 31 | 32 | com.google.dagger 33 | dagger 34 | ${dagger.version} 35 | 36 | 37 | com.google.dagger 38 | dagger-compiler 39 | ${dagger.version} 40 | provided 41 | 42 | 43 | 44 | com.qingyou 45 | sso-common 46 | ${project.version} 47 | 48 | 49 | com.qingyou 50 | sso-server-oauth2 51 | ${project.version} 52 | 53 | 54 | com.qingyou 55 | sso-server-login 56 | ${project.version} 57 | 58 | 59 | com.qingyou 60 | sso-server-admin 61 | ${project.version} 62 | 63 | 64 | com.qingyou 65 | sso-server-login-email-code 66 | ${project.version} 67 | compile 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | maven-shade-plugin 78 | ${maven-shade-plugin.version} 79 | 80 | 81 | package 82 | 83 | shade 84 | 85 | 86 | 87 | 88 | 90 | 91 | ${sso.main.class} 92 | ${sso.main.verticle} 93 | 94 | 95 | 96 | 98 | 99 | 100 | 101 | ${project.parent.build.directory}/${project.parent.artifactId}-${project.version}-fat.jar 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/CoreSSOApp.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso; 2 | 3 | import com.qingyou.sso.verticle.CoreVerticle; 4 | import io.netty.util.internal.logging.InternalLoggerFactory; 5 | import io.netty.util.internal.logging.Slf4JLoggerFactory; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.Vertx; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class CoreSSOApp { 14 | 15 | private static final Logger log = LoggerFactory.getLogger(CoreSSOApp.class); 16 | 17 | static { 18 | InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE); 19 | } 20 | 21 | public static void main(String[] args) { 22 | new CoreSSOApp().start() 23 | .toCompletionStage() 24 | .toCompletableFuture() 25 | .join(); 26 | } 27 | 28 | public Future start(){ 29 | long start = System.currentTimeMillis(); 30 | 31 | // ClusterManager clusterManager = new HazelcastClusterManager(); 32 | // 33 | // var vertx = Vertx.builder() 34 | // .withClusterManager(clusterManager) 35 | // .buildClustered(); 36 | var vertx = Future.succeededFuture(Vertx.vertx()); 37 | 38 | return vertx.flatMap(v -> { 39 | var core = new CoreVerticle(); 40 | return v.deployVerticle(core).timeout(10, TimeUnit.SECONDS); 41 | }).onComplete(result -> { 42 | if (result.succeeded()) { 43 | log.info("CoreSSOApp start in {}ms", System.currentTimeMillis() - start); 44 | } else { 45 | log.error("CoreSSOApp start failed", result.cause()); 46 | } 47 | }).mapEmpty(); 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/inject/RouterHandlerRegisterComponent.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.inject; 2 | 3 | import com.qingyou.sso.inject.provider.BaseModule; 4 | import com.qingyou.sso.inject.provider.RepositoryModule; 5 | import com.qingyou.sso.inject.provider.RouterHandlerModule; 6 | import com.qingyou.sso.inject.provider.ServiceModule; 7 | import com.qingyou.sso.router.admin.AdminRouterHandler; 8 | import com.qingyou.sso.router.global.NotFoundRouterHandler; 9 | import com.qingyou.sso.router.global.SessionRouterHandler; 10 | import com.qingyou.sso.router.oauth.OAuth2RouterHandler; 11 | import com.qingyou.sso.router.sso.EmailSSORouterHandler; 12 | import com.qingyou.sso.router.sso.LoginRouterHandler; 13 | import com.qingyou.sso.router.sso.SSORouterHandlerRegistry; 14 | import dagger.Component; 15 | import jakarta.inject.Inject; 16 | import jakarta.inject.Singleton; 17 | import lombok.AllArgsConstructor; 18 | 19 | import javax.annotation.Nullable; 20 | 21 | @Singleton 22 | @Component(modules = { 23 | ServiceModule.class, 24 | RepositoryModule.class, 25 | BaseModule.class, 26 | RouterHandlerModule.class, 27 | }) 28 | public interface RouterHandlerRegisterComponent { 29 | LoginRouterHandler registerLoginRouters(); 30 | 31 | OAuth2RouterHandler registerOauthRouters(); 32 | 33 | SessionRouterHandler registerSessionRouters(); 34 | 35 | SSORouterHandlerRegistry registerSSORouterHandlerRegister(); 36 | 37 | AdminRouterHandler registerAdminRouters(); 38 | 39 | NotFoundRouterHandler registerNotFoundRouter(); 40 | 41 | @Nullable 42 | EmailSSORouterHandler registerEmailSSORouters(); 43 | 44 | RouterGroups registerAllGroups(); 45 | 46 | @AllArgsConstructor(onConstructor = @__(@Inject)) 47 | class RouterGroups { 48 | //session 49 | private final SessionRouterHandler sessionRouterHandler; 50 | //login 51 | private final LoginRouterHandler loginRouterHandler; 52 | //oauth-service 53 | private final OAuth2RouterHandler OAuth2RouterHandler; 54 | //custom-login-module 55 | private final SSORouterHandlerRegistry SSORouterHandlerRegister; 56 | //email-login-module 57 | @Nullable 58 | private final EmailSSORouterHandler emailSSORouterHandler; 59 | //third-party-app 60 | private final AdminRouterHandler adminRouterHandler; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/inject/provider/BaseModule.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.inject.provider; 2 | 3 | import com.qingyou.sso.api.dto.BaseDependency; 4 | import com.qingyou.sso.auth.api.AuthService; 5 | import com.qingyou.sso.auth.api.dto.AuthServiceWithEventBus; 6 | import com.qingyou.sso.infra.cache.Cache; 7 | import com.qingyou.sso.infra.config.ConfigurationSource; 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.ext.mail.MailClient; 12 | import io.vertx.ext.mail.MailConfig; 13 | import io.vertx.ext.web.client.WebClient; 14 | import io.vertx.ext.web.sstore.ClusteredSessionStore; 15 | import io.vertx.ext.web.sstore.LocalSessionStore; 16 | import io.vertx.ext.web.sstore.SessionStore; 17 | import io.vertx.sqlclient.SqlClient; 18 | import jakarta.inject.Singleton; 19 | 20 | 21 | @Module 22 | public class BaseModule extends BaseDependency { 23 | 24 | public BaseModule(ConfigurationSource configurationSource, Vertx vertx, SqlClient sqlClient, Cache cache) { 25 | this.configuration = configurationSource; 26 | this.vertx = vertx; 27 | this.cache = cache; 28 | this.sqlClient = sqlClient; 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | public SessionStore sessionStore() { 34 | if (vertx.isClustered()) ClusteredSessionStore.create(vertx); 35 | return LocalSessionStore.create(vertx); 36 | } 37 | 38 | @Provides 39 | @Singleton 40 | public SqlClient sqlClient() { 41 | return sqlClient; 42 | } 43 | 44 | @Provides 45 | @Singleton 46 | public ConfigurationSource configuration() { 47 | return configuration; 48 | } 49 | 50 | @Provides 51 | @Singleton 52 | public Cache cache() { 53 | return this.cache; 54 | } 55 | 56 | @Provides 57 | @Singleton 58 | public WebClient webClient() { 59 | webClient = WebClient.create(vertx); 60 | return webClient; 61 | } 62 | 63 | @Provides 64 | @Singleton 65 | public MailClient provideMailClient() { 66 | var mail = configuration.getSource().getJsonObject("mail"); 67 | if (mail == null) return null; 68 | return MailClient.create(vertx, new MailConfig(mail)); 69 | } 70 | 71 | @Provides 72 | @Singleton 73 | public AuthService authService() { 74 | return new AuthServiceWithEventBus(vertx.eventBus()); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/inject/provider/RepositoryModule.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.inject.provider; 2 | 3 | import com.qingyou.sso.infra.repository.domain.*; 4 | import com.qingyou.sso.infra.repository.domain.impl.*; 5 | import dagger.Module; 6 | import dagger.Provides; 7 | import io.vertx.sqlclient.SqlClient; 8 | import jakarta.inject.Singleton; 9 | 10 | @Module 11 | public class RepositoryModule { 12 | 13 | @Provides 14 | @Singleton 15 | public AccountRepository provideAccountRepository(SqlClient client) { 16 | return new AccountRepositoryImpl(client); 17 | } 18 | 19 | @Provides 20 | @Singleton 21 | public UserRepository provideUserRepository(SqlClient client) { 22 | return new UserRepositoryImpl(client); 23 | } 24 | 25 | @Provides 26 | @Singleton 27 | public UserInfoRepository provideUserInfoRepository(SqlClient client) { 28 | return new UserInfoRepositoryImpl(client); 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | public ThirdPartyRepository provideThirdPartyRepository(SqlClient client) { 34 | return new ThirdPartyRepositoryImpl(client); 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | public ThirdPartyRedirectRepository provideThirdPartyRedirectRepository(SqlClient client) { 40 | return new ThirdPartyRedirectRepositoryImpl(client); 41 | } 42 | 43 | @Provides 44 | @Singleton 45 | public ThirdPartyRequiredUserInfoRepository provideThirdPartyRequiredUserInfoRepository(SqlClient client) { 46 | return new ThirdPartyRequiredUserInfoRepositoryImpl(client); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/inject/provider/RouterHandlerModule.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.inject.provider; 2 | 3 | import com.qingyou.sso.handler.AuthHandler; 4 | import com.qingyou.sso.handler.platform.EmailSSOHandler; 5 | import com.qingyou.sso.infra.config.ConfigurationSource; 6 | import com.qingyou.sso.router.admin.AdminRouterHandler; 7 | import com.qingyou.sso.router.global.NotFoundRouterHandler; 8 | import com.qingyou.sso.router.global.SessionRouterHandler; 9 | import com.qingyou.sso.router.oauth.OAuth2RouterHandler; 10 | import com.qingyou.sso.router.sso.*; 11 | import com.qingyou.sso.service.BaseSSOService; 12 | import com.qingyou.sso.service.EmailSSOService; 13 | import com.qingyou.sso.service.ThirdPartyAppService; 14 | import com.qingyou.sso.serviece.OAuth2Service; 15 | import dagger.Module; 16 | import dagger.Provides; 17 | import io.vertx.ext.web.Router; 18 | import io.vertx.ext.web.sstore.SessionStore; 19 | import jakarta.inject.Singleton; 20 | import lombok.AllArgsConstructor; 21 | 22 | import javax.annotation.Nullable; 23 | 24 | @Module 25 | @AllArgsConstructor 26 | public class RouterHandlerModule { 27 | 28 | private final Router router; 29 | 30 | @Provides 31 | @Singleton 32 | SessionRouterHandler provideSessionRouterHandler(SessionStore sessionStore, ConfigurationSource configurationSource) { 33 | var handler = new SessionRouterHandler(sessionStore, configurationSource); 34 | handler.handle(router); 35 | return handler; 36 | } 37 | 38 | @Provides 39 | @Singleton 40 | public EmailSSORouterHandler provideEmailSSORouterHandler(@Nullable EmailSSOService emailSSOService) { 41 | if (emailSSOService == null) return null; 42 | var handler = new EmailSSORouterHandler(new EmailSSOHandler(emailSSOService)); 43 | handler.handle(router); 44 | return handler; 45 | } 46 | 47 | @Provides 48 | @Singleton 49 | LoginRouterHandler provideLoginRouterHandler(BaseSSOService baseSSOService) { 50 | var handler = new LoginRouterHandler(baseSSOService); 51 | handler.handle(router); 52 | return handler; 53 | } 54 | 55 | @Provides 56 | @Singleton 57 | OAuth2RouterHandler provideOauthRouterHandler(OAuth2Service oAuth2Service) { 58 | var handler = new OAuth2RouterHandler(oAuth2Service); 59 | handler.handle(router); 60 | return handler; 61 | } 62 | 63 | @Provides 64 | @Singleton 65 | SSORouterHandlerRegistry provideCustomRouterHandlerRegister(BaseSSOService baseSSOService) { 66 | SSORouterHandlerRegistry registry = new SSORouterHandlerRegistry(baseSSOService); 67 | for (CustomSSORouterHandler handler: registry.getAll()){ 68 | handler.handle(router); 69 | } 70 | return registry; 71 | } 72 | 73 | @Provides 74 | @Singleton 75 | NotFoundRouterHandler provideNotFoundRouterHandler() { 76 | var handler = new NotFoundRouterHandler(); 77 | handler.handle(router); 78 | return handler; 79 | } 80 | 81 | @Provides 82 | @Singleton 83 | AdminRouterHandler provideAppRouterHandler(ThirdPartyAppService thirdPartyAppService, AuthHandler.Factory factory) { 84 | var handler = new AdminRouterHandler(thirdPartyAppService, factory); 85 | handler.handle(router); 86 | return handler; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/inject/provider/ServiceModule.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.inject.provider; 2 | 3 | import com.qingyou.sso.auth.api.AuthService; 4 | import com.qingyou.sso.handler.AuthHandler; 5 | import com.qingyou.sso.infra.cache.Cache; 6 | import com.qingyou.sso.infra.config.Configuration; 7 | import com.qingyou.sso.infra.config.ConfigurationSource; 8 | import com.qingyou.sso.infra.repository.domain.*; 9 | import com.qingyou.sso.service.*; 10 | import com.qingyou.sso.serviece.DefaultOAuth2Service; 11 | import com.qingyou.sso.serviece.OAuth2Service; 12 | import dagger.Module; 13 | import dagger.Provides; 14 | import io.vertx.ext.mail.MailClient; 15 | import jakarta.inject.Singleton; 16 | 17 | import javax.annotation.Nullable; 18 | 19 | @Module 20 | public class ServiceModule { 21 | 22 | @Provides 23 | @Singleton 24 | public BaseSSOService provideLoginService(UserRepository userRepository, UserInfoRepository userInfoRepository, AccountRepository accountRepository) { 25 | return new DefaultBaseSSOService(userRepository, accountRepository, userInfoRepository); 26 | } 27 | 28 | @Provides 29 | @Singleton 30 | public OAuth2Service provideOauthServiceService(AuthService authService, UserInfoRepository userInfoRepository, UserRepository userRepository, ThirdPartyRepository thirdPartyRepository, ConfigurationSource configurationSource, Cache cache) { 31 | return new DefaultOAuth2Service(authService, thirdPartyRepository, userInfoRepository, userRepository, configurationSource, cache); 32 | } 33 | 34 | @Provides 35 | @Singleton 36 | public ThirdPartyAppService partyAppService(ThirdPartyRepository thirdPartyRepository, ThirdPartyRedirectRepository thirdPartyRedirectRepository, ThirdPartyRequiredUserInfoRepository thirdPartyRequiredUserInfoRepository) { 37 | return new ThirdPartyAppServiceImpl(thirdPartyRepository, thirdPartyRedirectRepository, thirdPartyRequiredUserInfoRepository); 38 | } 39 | 40 | @Provides 41 | @Singleton 42 | public AuthHandler.Factory provideAuthHandlerFactory(AuthService authService, UserRepository userRepository) { 43 | return new AuthHandler.Factory(userRepository, authService); 44 | } 45 | 46 | @Provides 47 | @Singleton 48 | public EmailSSOService provideEmailSSOService(@Nullable MailClient mailClient, ConfigurationSource configurationSource, Cache cache, UserRepository userRepository, AccountRepository accountRepository) { 49 | if (mailClient == null) return null; 50 | return new DefaultEmailSSOService(mailClient, configurationSource, cache, userRepository, accountRepository); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/admin/AdminRouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.admin; 2 | 3 | import com.qingyou.sso.api.ThirdPartyAppManagement; 4 | import com.qingyou.sso.handler.AuthHandler; 5 | import com.qingyou.sso.handler.ThirdPartyAppManagementService; 6 | import com.qingyou.sso.infra.response.GlobalHttpResponse; 7 | import com.qingyou.sso.service.ThirdPartyAppService; 8 | import io.vertx.core.Handler; 9 | import io.vertx.ext.web.Router; 10 | import io.vertx.ext.web.handler.BodyHandler; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | @Slf4j 14 | public class AdminRouterHandler implements Handler { 15 | private final ThirdPartyAppManagement thirdPartyAppManagement; 16 | private final AuthHandler.Factory factory; 17 | 18 | public AdminRouterHandler(ThirdPartyAppService thirdPartyAppService, AuthHandler.Factory factory) { 19 | this.factory = factory; 20 | this.thirdPartyAppManagement = new ThirdPartyAppManagementService(thirdPartyAppService); 21 | } 22 | 23 | @Override 24 | public void handle(Router router) { 25 | router.route("/admin/*").handler(BodyHandler.create()); 26 | router.post("/admin/app/register") 27 | .handler(factory.target("add","app").build()) 28 | .handler(GlobalHttpResponse.wrap(thirdPartyAppManagement::create, log)); 29 | router.post("/admin/app/info/update") 30 | .handler(factory.target("update","app").build()) 31 | .handler(GlobalHttpResponse.wrap(thirdPartyAppManagement::updateRequiredInfos, log)); 32 | router.post("/admin/app/redirect/update") 33 | .handler(factory.target("update","app").build()) 34 | .handler(GlobalHttpResponse.wrap(thirdPartyAppManagement::updateRedirectURIs, log)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/global/NotFoundRouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.global; 2 | 3 | import com.qingyou.sso.api.dto.Result; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.json.Json; 6 | import io.vertx.ext.web.Router; 7 | 8 | import static com.qingyou.sso.infra.response.GlobalHttpResponse.APPLICATION_JSON; 9 | import static com.qingyou.sso.infra.response.GlobalHttpResponse.CONTENT_TYPE_HEADER; 10 | 11 | public class NotFoundRouterHandler implements Handler { 12 | @Override 13 | public void handle(Router router) { 14 | router.route().handler(routingContext -> routingContext.response().setStatusCode(404) 15 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 16 | .end(Json.encodeToBuffer(Result.failed(404, "Not Found")))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/global/SessionRouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.global; 2 | 3 | import com.qingyou.sso.infra.config.ConfigurationSource; 4 | import io.vertx.core.Handler; 5 | import io.vertx.ext.web.Router; 6 | import io.vertx.ext.web.handler.SessionHandler; 7 | import io.vertx.ext.web.sstore.SessionStore; 8 | import lombok.AllArgsConstructor; 9 | 10 | public class SessionRouterHandler implements Handler { 11 | private final SessionStore sessionStore; 12 | private final ConfigurationSource configurationSource; 13 | 14 | public SessionRouterHandler(SessionStore sessionStore, ConfigurationSource configurationSource) { 15 | this.sessionStore = sessionStore; 16 | this.configurationSource = configurationSource; 17 | } 18 | 19 | @Override 20 | public void handle(Router router) { 21 | var config = configurationSource.getConfiguration().security().cookie(); 22 | router.route().handler( 23 | SessionHandler.create(sessionStore) 24 | .setCookieHttpOnlyFlag(config.httpOnly()) 25 | .setCookieSecureFlag(config.secure()) 26 | .setCookieMaxAge(config.maxAge()) 27 | .setSessionTimeout(config.timeout()) 28 | .setSessionCookiePath(config.path()) 29 | .setSessionCookieName(config.name()) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/oauth/OAuth2RouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.oauth; 2 | 3 | import com.qingyou.sso.handler.ErrorPageHandler; 4 | import com.qingyou.sso.handler.OAuth2Handler; 5 | import com.qingyou.sso.handler.OAuth2ParamHandler; 6 | import com.qingyou.sso.serviece.OAuth2Service; 7 | import io.vertx.core.Handler; 8 | import io.vertx.ext.web.Router; 9 | import io.vertx.ext.web.handler.BodyHandler; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | @Slf4j 13 | public class OAuth2RouterHandler implements Handler { 14 | private final OAuth2Handler oauth2Handler; 15 | private final ErrorPageHandler errorHandler = new ErrorPageHandler(); 16 | 17 | public OAuth2RouterHandler(OAuth2Service oauth2Service) { 18 | this.oauth2Handler = new OAuth2Handler(oauth2Service); 19 | } 20 | 21 | @Override 22 | public void handle(Router router) { 23 | router.get("/oauth/authorize").handler(OAuth2ParamHandler.authorization()::inject).handler(oauth2Handler::authorize); 24 | 25 | router.post("/oauth/token").handler(BodyHandler.create()).handler(OAuth2ParamHandler.token()::inject).handler(oauth2Handler::token); 26 | 27 | router.get("/oauth/userinfo").handler(OAuth2ParamHandler.info()::inject).handler(oauth2Handler::info); 28 | 29 | router.get("/oauth/.well-known/openid-configuration"); 30 | //{ 31 | // "issuer": "string", 32 | // "authorization_endpoint": "string", 33 | // "token_endpoint": "string", 34 | // "userinfo_endpoint": "string", 35 | // "jwks_uri": "string", 36 | // "response_types_supported": [ 37 | // "string" 38 | // ], 39 | // "scopes_supported": [ 40 | // "string" 41 | // ], 42 | // "id_token_signing_alg_values_supported": [ 43 | // "string" 44 | // ] 45 | //} 46 | 47 | router.get("/oauth/logout"); 48 | // 49 | 50 | router.get("/oauth/jwks"); 51 | 52 | router.post("/oauth/password").handler(BodyHandler.create()).handler(OAuth2ParamHandler.password()::inject).handler(oauth2Handler::password); 53 | 54 | router.route("/error").handler(errorHandler::error); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/sso/CustomSSORouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.sso; 2 | 3 | import com.qingyou.sso.handler.platform.CustomSSOHandler; 4 | import com.qingyou.sso.infra.response.GlobalHttpResponse; 5 | import io.vertx.core.Handler; 6 | import io.vertx.ext.web.Router; 7 | import io.vertx.ext.web.handler.BodyHandler; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | public class CustomSSORouterHandler implements Handler { 12 | private final CustomSSOHandler customHandler; 13 | 14 | public CustomSSORouterHandler(CustomSSOHandler customHandler) { 15 | this.customHandler = customHandler; 16 | } 17 | 18 | @Override 19 | public void handle(Router router) { 20 | BodyHandler bodyHandler = BodyHandler.create(); 21 | router.post("/sso/login/" + customHandler.getName()) 22 | .handler(bodyHandler) 23 | .handler(GlobalHttpResponse.wrap(customHandler::login, log)); 24 | router.post("/sso/register/" + customHandler.getName()) 25 | .handler(bodyHandler) 26 | .handler(GlobalHttpResponse.wrap(customHandler::register, log)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/sso/LoginRouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.sso; 2 | 3 | import com.qingyou.sso.handler.sso.SSOHandler; 4 | import com.qingyou.sso.infra.response.GlobalHttpResponse; 5 | import com.qingyou.sso.service.BaseSSOService; 6 | import io.vertx.core.Handler; 7 | import io.vertx.ext.web.Router; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | public class LoginRouterHandler implements Handler { 12 | private final SSOHandler ssoHandler; 13 | 14 | public LoginRouterHandler(BaseSSOService baseSSOService) { 15 | this.ssoHandler = new SSOHandler(baseSSOService); 16 | } 17 | 18 | @Override 19 | public void handle(Router router) { 20 | router.get("/sso/login").handler(GlobalHttpResponse.wrap(ssoHandler::login, log)); 21 | router.get("/sso/logout").handler(GlobalHttpResponse.wrap(ssoHandler::logout, log)); 22 | router.get("/sso/state").handler(GlobalHttpResponse.wrap(ssoHandler::state, log)); 23 | router.get("/sso/info").handler(GlobalHttpResponse.wrap(ssoHandler::info, log)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /sso-core/src/main/java/com/qingyou/sso/router/sso/SSORouterHandlerRegistry.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.sso; 2 | 3 | import com.qingyou.sso.handler.platform.CustomSSOHandler; 4 | import com.qingyou.sso.handler.platform.SSOHandlerRegistry; 5 | import com.qingyou.sso.service.BaseSSOService; 6 | 7 | import java.util.List; 8 | import java.util.ServiceLoader; 9 | import java.util.stream.Collectors; 10 | 11 | public class SSORouterHandlerRegistry { 12 | 13 | private final List customSSORouterHandlers; 14 | 15 | public SSORouterHandlerRegistry(BaseSSOService baseSSOService) { 16 | this.customSSORouterHandlers = load(baseSSOService); 17 | } 18 | 19 | public List getAll() { 20 | return this.customSSORouterHandlers; 21 | } 22 | 23 | public List load(BaseSSOService ssoService) { 24 | ServiceLoader serviceLoader = ServiceLoader.load(SSOHandlerRegistry.class); 25 | return serviceLoader.stream().map(custom -> 26 | new CustomSSORouterHandler(custom.get().getSSOHandler(ssoService)) 27 | ).collect(Collectors.toList()); 28 | } 29 | 30 | public SSORouterHandlerRegistry register(CustomSSORouterHandler customRouterHandler) { 31 | customSSORouterHandlers.add(customRouterHandler); 32 | return this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sso-core/src/main/resources/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "server" : { 3 | "host" : "0.0.0.0", 4 | "port" : 8080 5 | }, 6 | "database": { 7 | "host" : "localhost", 8 | "port" : 5432, 9 | "database" : "sso", 10 | "user" : "postgres", 11 | "password" : "123456789", 12 | "pool" : { 13 | "maxSize" : 10 14 | } 15 | }, 16 | "security": { 17 | "jwt": { 18 | "secret" : "jwt_secret" 19 | }, 20 | "cookie": { 21 | "name" : "session_id", 22 | "maxAge" : 60000000, 23 | "timeout" : 60000000, 24 | "path" : "/", 25 | "domain" : "localhost", 26 | "secure" : false, 27 | "httpOnly" : false 28 | } 29 | }, 30 | "mail": { 31 | "message": { 32 | "from" : "", 33 | "subject" : "[Pomelo SSO] One-Time Passcode (OTP)", 34 | "pattern" : "Code: %s", 35 | "expire": 60000 36 | }, 37 | "username": "feell@163.com", 38 | "password": "", 39 | "host" : "" 40 | } 41 | } -------------------------------------------------------------------------------- /sso-core/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread{10}] %highlight(%-5level) %logger{36} - %msg%n%throwable{5} 6 | 7 | 8 | 9 | 10 | 11 | s 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sso-core/src/main/resources/tables.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists auth_rbac; 2 | 3 | create table if not exists auth_rbac.role 4 | ( 5 | id bigint not null 6 | primary key, 7 | description varchar(255) not null, 8 | name varchar(255) not null 9 | ); 10 | 11 | create table if not exists auth_rbac.target 12 | ( 13 | id bigint not null 14 | primary key, 15 | action varchar(255), 16 | app_id bigint, 17 | object varchar(255) 18 | ); 19 | 20 | create table if not exists auth_rbac.target_role 21 | ( 22 | target_id bigint not null 23 | constraint fk_target_role_target 24 | references auth_rbac.target, 25 | role_id bigint not null 26 | constraint fk_target_role_role 27 | references auth_rbac.role 28 | ); 29 | 30 | create table if not exists auth_rbac.user_role 31 | ( 32 | id bigint not null 33 | primary key, 34 | app_id bigint, 35 | user_id bigint, 36 | role_id bigint 37 | constraint fk_user_role_role 38 | references auth_rbac.role 39 | ); 40 | 41 | 42 | 43 | create schema if not exists sso_oauth; 44 | 45 | create table if not exists sso_oauth.third_party_app 46 | ( 47 | id bigint not null 48 | primary key, 49 | app_name varchar(255), 50 | client_id varchar(255) 51 | constraint uk_client_id 52 | unique, 53 | client_secret varchar(400) 54 | ); 55 | 56 | create table if not exists sso_oauth.third_party_redirect 57 | ( 58 | id bigint not null 59 | primary key, 60 | uri varchar(255), 61 | third_party_app_id bigint 62 | constraint fk_redirect_app 63 | references sso_oauth.third_party_app 64 | ); 65 | 66 | create table if not exists sso_oauth.third_party_required_user_info 67 | ( 68 | id bigint not null 69 | primary key, 70 | data_type smallint not null 71 | constraint third_party_required_user_info_data_type_check 72 | check ((data_type >= 0) AND (data_type <= 1)), 73 | platform_type smallint not null 74 | constraint third_party_required_user_info_platform_type_check 75 | check ((platform_type >= 0) AND (platform_type <= 4)), 76 | third_party_app_id bigint 77 | constraint fk_user_info_app 78 | references sso_oauth.third_party_app 79 | ); 80 | 81 | create schema if not exists sso_user; 82 | 83 | create table if not exists sso_user.account 84 | ( 85 | user_id bigint not null 86 | primary key, 87 | password varchar(255) not null, 88 | salt varchar(255) not null, 89 | username varchar(255) not null 90 | unique 91 | constraint uk_account 92 | unique 93 | ); 94 | 95 | create table if not exists sso_user."user" 96 | ( 97 | id bigint not null 98 | primary key, 99 | name varchar(255), 100 | student_id varchar(255) 101 | unique 102 | constraint uk_user 103 | unique 104 | ); 105 | 106 | create table if not exists sso_user.user_info 107 | ( 108 | data_type smallint not null 109 | constraint user_info_data_type_check 110 | check ((data_type >= 0) AND (data_type <= 1)), 111 | platform_type smallint not null 112 | constraint user_info_platform_type_check 113 | check ((platform_type >= 0) AND (platform_type <= 2)), 114 | id bigint not null 115 | primary key, 116 | user_id bigint 117 | constraint fk_user_info_user 118 | references sso_user."user", 119 | metadata varchar(255) not null 120 | ); 121 | -------------------------------------------------------------------------------- /sso-core/src/main/resources/verticles.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /sso-core/src/test/java/com/qingyou/sso/test/ApiTest.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.test; 2 | 3 | import com.qingyou.sso.verticle.CoreVerticle; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.junit5.VertxExtension; 6 | import io.vertx.junit5.VertxTestContext; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | 11 | 12 | @ExtendWith(VertxExtension.class) 13 | class ApiTest { 14 | 15 | static final CoreVerticle coreVerticle = new CoreVerticle(); 16 | 17 | @BeforeAll 18 | static void deployVerticle(Vertx vertx, VertxTestContext testContext) { 19 | vertx.deployVerticle(coreVerticle, testContext.succeedingThenComplete()); 20 | } 21 | 22 | @Test 23 | public void test(Vertx vertx, VertxTestContext testContext){ 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /sso-core/src/test/java/com/qingyou/sso/test/SystemTest.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.test; 2 | 3 | import com.qingyou.sso.verticle.CoreVerticle; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.http.HttpMethod; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.ext.auth.User; 8 | import io.vertx.ext.auth.oauth2.*; 9 | import io.vertx.ext.web.Router; 10 | import io.vertx.ext.web.client.HttpResponse; 11 | import io.vertx.ext.web.client.WebClient; 12 | import io.vertx.junit5.VertxExtension; 13 | import io.vertx.junit5.VertxTestContext; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.junit.jupiter.api.BeforeAll; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.junit.jupiter.api.extension.ExtendWith; 19 | 20 | import java.util.Collections; 21 | 22 | @Slf4j 23 | @ExtendWith(VertxExtension.class) 24 | class SystemTest { 25 | 26 | 27 | static final String clientId = "1"; 28 | static final String clientSecret = "1"; 29 | static final String username = "test"; 30 | static final String password = "test"; 31 | static OAuth2Auth oauth2; 32 | 33 | @BeforeAll 34 | static void deployVerticle(Vertx vertx, VertxTestContext testContext) { 35 | vertx.deployVerticle(new CoreVerticle(), testContext.succeedingThenComplete()); 36 | oauth2 = OAuth2Auth.create(vertx, new OAuth2Options() 37 | .setClientId(clientId) 38 | .setClientSecret(clientSecret) 39 | .setSite("http://localhost:8080") 40 | .setAuthorizationPath("/oauth/authorize") 41 | .setTokenPath("/oauth/token") 42 | .setUserInfoPath("/oauth/info") 43 | ); 44 | } 45 | 46 | @BeforeEach 47 | void startThirdPartyServer(Vertx vertx, VertxTestContext testContext) { 48 | //client BE 49 | Router router = Router.router(vertx); 50 | router.get("/callback") 51 | .handler(routingContext -> { 52 | log.info("Handle callback request {}", routingContext.request().uri()); 53 | var params = routingContext.queryParams(); 54 | oauth2.authenticate( 55 | new Oauth2Credentials() 56 | .setFlow(OAuth2FlowType.AUTH_CODE) 57 | .setRedirectUri("http://localhost:18080/callback") 58 | .setCodeVerifier(params.get("code_verifier")) 59 | .setCode(params.get("code")) 60 | .setScopes(Collections.singletonList(params.get("scope"))) 61 | ).onSuccess(user -> { 62 | routingContext.response().end(user.attributes().toBuffer()); 63 | }).onFailure(ex -> log.error("BE Server Request error:", ex)); 64 | }); 65 | vertx.createHttpServer() 66 | .requestHandler(router) 67 | .listen(18080,"localhost") 68 | .onSuccess(httpServer -> log.info("Server Started Success on {}",httpServer.actualPort())) 69 | .onComplete(testContext.succeedingThenComplete()); 70 | } 71 | 72 | @Test 73 | void oauth2(Vertx vertx, VertxTestContext testContext) { 74 | 75 | //client FE 76 | WebClient webClient = WebClient.create(vertx); 77 | //login 78 | webClient.get(8080, "localhost", "/sso/login") 79 | .basicAuthentication(username, password) 80 | .send() 81 | .map(res -> { 82 | log.info("Login response: {}", res.bodyAsString()); 83 | return res.cookies(); 84 | }).flatMap(cookies -> { 85 | //auth 86 | String authorization_uri = oauth2.authorizeURL( 87 | new OAuth2AuthorizationURL() 88 | .setRedirectUri("http://localhost:18080/callback") 89 | .setScopes(Collections.singletonList("add:app update:app")) 90 | .setState("xyz") 91 | ).replace("http://localhost:8080",""); 92 | log.info("Authorization URI: {}", authorization_uri); 93 | return webClient.get(8080,"localhost", authorization_uri) 94 | .putHeader("Cookie",cookies) 95 | .followRedirects(true) 96 | .send() 97 | .map(HttpResponse::bodyAsString); 98 | }).onSuccess(res-> log.info("Auth Success response: {}", res)) 99 | .onComplete(testContext.succeedingThenComplete()); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /sso-core/src/test/resources/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "server" : { 3 | "host" : "0.0.0.0", 4 | "port" : 8080 5 | }, 6 | "database": { 7 | "host" : "localhost", 8 | "port" : 5432, 9 | "database" : "sso", 10 | "user" : "postgres", 11 | "password" : "passw0rd", 12 | "pool" : { 13 | "maxSize" : 10 14 | } 15 | }, 16 | "security": { 17 | "jwt": { 18 | "secret" : "jwt_secret" 19 | }, 20 | "cookie": { 21 | "name" : "session_id", 22 | "maxAge" : 60000000, 23 | "timeout" : 60000000, 24 | "path" : "/", 25 | "domain" : "localhost", 26 | "secure" : false, 27 | "httpOnly" : false 28 | } 29 | }, 30 | "mail": { 31 | "message": { 32 | "expire": 60000, 33 | "from" : "", 34 | "subject" : "[Pomelo SSO] One-Time Passcode (OTP)", 35 | "pattern" : "Code: %s" 36 | }, 37 | "username": "feell@163.com", 38 | "password": "", 39 | "host" : "" 40 | } 41 | } -------------------------------------------------------------------------------- /sso-core/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread{10}] %highlight(%-5level) %logger{36} - %msg%n%throwable{5} 6 | 7 | 8 | 9 | 10 | 11 | s 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sso-core/src/test/resources/verticles.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /sso-server-admin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | core-sso 9 | 1.0.1 10 | 11 | 12 | sso-server-admin 13 | 14 | 15 | 17 16 | 17 17 | UTF-8 18 | 19 | 20 | 21 | 22 | com.qingyou 23 | sso-common 24 | ${project.version} 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.34 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/api/ThirdPartyAppManagement.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import com.qingyou.sso.api.result.ThirdPartyAppResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | 8 | public interface ThirdPartyAppManagement { 9 | Future create(RoutingContext routingContext); 10 | Future updateRequiredInfos(RoutingContext routingContext); 11 | Future updateRedirectURIs(RoutingContext routingContext); 12 | } 13 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/api/param/AppCreate.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | public record AppCreate( 4 | String name 5 | ) { 6 | } 7 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/api/param/RedirectURIsUpdate.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | import java.util.List; 4 | 5 | public record RedirectURIsUpdate( 6 | String clientId, 7 | List redirectURIs 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/api/param/RequiredInfosUpdate.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | import com.qingyou.sso.api.constants.PlatformType; 4 | 5 | import java.util.List; 6 | 7 | public record RequiredInfosUpdate( 8 | String clientId, 9 | List platformTypes 10 | ){} 11 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/api/result/ThirdPartyAppResult.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.result; 2 | 3 | import com.qingyou.sso.api.constants.PlatformType; 4 | 5 | import java.util.List; 6 | 7 | public record ThirdPartyAppResult( 8 | String name, 9 | String clientId, 10 | String clientSecret, 11 | List requiredInfos, 12 | List redirectURIs 13 | ){ 14 | } 15 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/handler/AuthHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler; 2 | 3 | import com.qingyou.sso.auth.api.AuthService; 4 | import com.qingyou.sso.auth.api.dto.Action; 5 | import com.qingyou.sso.auth.internal.rbac.RbacUserInfo; 6 | import com.qingyou.sso.auth.internal.rbac.TargetInfo; 7 | import com.qingyou.sso.domain.user.User; 8 | import com.qingyou.sso.infra.exception.BizException; 9 | import com.qingyou.sso.infra.exception.ErrorType; 10 | import com.qingyou.sso.infra.repository.domain.UserRepository; 11 | import com.qingyou.sso.infra.response.GlobalHttpResponse; 12 | import io.vertx.core.Handler; 13 | import io.vertx.ext.web.RoutingContext; 14 | import io.vertx.ext.web.Session; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | @Slf4j 18 | public class AuthHandler implements Handler { 19 | private final UserRepository userRepository; 20 | private final AuthService authService; 21 | private final String action; 22 | private final String object; 23 | 24 | @Override 25 | public void handle(RoutingContext routingContext) { 26 | Session session = routingContext.session(); 27 | if (session == null) { 28 | GlobalHttpResponse.fail(routingContext, new BizException(ErrorType.Showed.Auth, "Session not exist"), log); 29 | return; 30 | } 31 | Long userId = session.get("userId"); 32 | if (userId == null) { 33 | GlobalHttpResponse.fail(routingContext, new BizException(ErrorType.Showed.Auth, "Login required"), log); 34 | return; 35 | } 36 | userRepository.findById(userId) 37 | .flatMap(user -> { 38 | if (user == null) throw new BizException(ErrorType.Showed.Auth,"User not exist"); 39 | return authService.rbac(Action.required(new RbacUserInfo(user.getId(), user.getName()), new TargetInfo(0L,action,object))).map(result -> { 40 | if (!result.success()) throw new BizException(ErrorType.Showed.Auth, "Auth failed, " + result.message()); 41 | return user; 42 | }); 43 | }) 44 | .onSuccess(user -> routingContext.put("user", user).next()) 45 | .onFailure(ex -> GlobalHttpResponse.fail(routingContext, ex, log)); 46 | } 47 | 48 | public record Factory( 49 | UserRepository userRepository, 50 | AuthService authService 51 | ){ 52 | public TargetSpec target(String action, String object) { 53 | return new TargetSpec(this, action, object); 54 | } 55 | 56 | public record TargetSpec( 57 | Factory factory, 58 | String action, 59 | String object 60 | ){ 61 | public AuthHandler build() { 62 | return new AuthHandler(factory.userRepository, factory.authService, action,object); 63 | } 64 | } 65 | } 66 | 67 | private AuthHandler(UserRepository userRepository, AuthService authService, String action, String object) { 68 | this.userRepository = userRepository; 69 | this.authService = authService; 70 | this.action = action; 71 | this.object = object; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/handler/ThirdPartyAppManagementService.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler; 2 | 3 | import com.qingyou.sso.api.ThirdPartyAppManagement; 4 | import com.qingyou.sso.api.param.AppCreate; 5 | import com.qingyou.sso.api.param.RedirectURIsUpdate; 6 | import com.qingyou.sso.api.param.RequiredInfosUpdate; 7 | import com.qingyou.sso.api.result.ThirdPartyAppResult; 8 | import com.qingyou.sso.service.ThirdPartyAppService; 9 | import io.vertx.core.Future; 10 | import io.vertx.ext.web.RoutingContext; 11 | 12 | public class ThirdPartyAppManagementService implements ThirdPartyAppManagement { 13 | 14 | private final ThirdPartyAppService thirdPartyAppService; 15 | 16 | public ThirdPartyAppManagementService(ThirdPartyAppService thirdPartyAppService) { 17 | this.thirdPartyAppService = thirdPartyAppService; 18 | } 19 | 20 | @Override 21 | public Future create(RoutingContext routingContext) { 22 | AppCreate appCreate = routingContext.body().asPojo(AppCreate.class); 23 | return thirdPartyAppService.registerApp(appCreate.name()); 24 | } 25 | 26 | @Override 27 | public Future updateRequiredInfos(RoutingContext routingContext) { 28 | RequiredInfosUpdate requiredInfosUpdate = routingContext.body().asPojo(RequiredInfosUpdate.class); 29 | return thirdPartyAppService.updateRequiredInfos(requiredInfosUpdate.clientId(), requiredInfosUpdate.platformTypes()); 30 | } 31 | 32 | @Override 33 | public Future updateRedirectURIs(RoutingContext routingContext) { 34 | RedirectURIsUpdate redirectURIsUpdate = routingContext.body().asPojo(RedirectURIsUpdate.class); 35 | return thirdPartyAppService.updateRedirectURIs(redirectURIsUpdate.clientId(), redirectURIsUpdate.redirectURIs()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sso-server-admin/src/main/java/com/qingyou/sso/service/ThirdPartyAppService.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.service; 2 | 3 | 4 | import com.qingyou.sso.api.constants.PlatformType; 5 | import com.qingyou.sso.api.result.ThirdPartyAppResult; 6 | import io.vertx.core.Future; 7 | 8 | import java.util.List; 9 | 10 | public interface ThirdPartyAppService { 11 | 12 | Future registerApp(String name); 13 | 14 | Future updateRequiredInfos(String clientId, List platformTypes); 15 | 16 | Future updateRedirectURIs(String clientId, List redirectUris); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sso-server-login-email-code/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | com.qingyou 7 | core-sso 8 | 1.0.1 9 | 10 | 11 | sso-server-login-email-code 12 | 13 | 14 | 17 15 | 17 16 | UTF-8 17 | 18 | 19 | 20 | 21 | com.qingyou 22 | sso-common 23 | ${project.version} 24 | 25 | 26 | com.qingyou 27 | sso-server-login 28 | ${project.version} 29 | 30 | 31 | 32 | 33 | io.vertx 34 | vertx-mail-client 35 | 36 | 37 | 38 | org.projectlombok 39 | lombok 40 | 1.18.34 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /sso-server-login-email-code/src/main/java/com/qingyou/sso/api/SendEmail.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.ext.web.RoutingContext; 5 | 6 | public interface SendEmail { 7 | Future email(RoutingContext routingContext); 8 | } 9 | -------------------------------------------------------------------------------- /sso-server-login-email-code/src/main/java/com/qingyou/sso/api/param/Email.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | public record Email(String email) { 4 | } 5 | -------------------------------------------------------------------------------- /sso-server-login-email-code/src/main/java/com/qingyou/sso/handler/platform/EmailSSOHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler.platform; 2 | 3 | import com.qingyou.sso.api.Login; 4 | import com.qingyou.sso.api.Register; 5 | import com.qingyou.sso.api.SendEmail; 6 | import com.qingyou.sso.api.param.Code; 7 | import com.qingyou.sso.api.param.Email; 8 | import com.qingyou.sso.api.result.LoginResult; 9 | import com.qingyou.sso.service.EmailSSOService; 10 | import io.vertx.core.Future; 11 | import io.vertx.ext.web.RoutingContext; 12 | 13 | public class EmailSSOHandler implements Login, Register, SendEmail{ 14 | 15 | private final EmailSSOService emailSSOService; 16 | 17 | public EmailSSOHandler(EmailSSOService emailSSOService) { 18 | this.emailSSOService = emailSSOService; 19 | } 20 | 21 | @Override 22 | public Future email(RoutingContext routingContext) { 23 | Email email = routingContext.body().asPojo(Email.class); 24 | return emailSSOService.email(email); 25 | } 26 | 27 | @Override 28 | public Future login(RoutingContext routingContext) { 29 | Code code = routingContext.body().asPojo(Code.class); 30 | return emailSSOService.login(code).onSuccess(u -> 31 | routingContext.session().put("userId",u.userId())); 32 | } 33 | 34 | @Override 35 | public Future register(RoutingContext routingContext) { 36 | Code code = routingContext.body().asPojo(Code.class); 37 | return emailSSOService.register(code).onSuccess(u -> 38 | routingContext.session().put("userId",u.userId())); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /sso-server-login-email-code/src/main/java/com/qingyou/sso/router/sso/EmailSSORouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.sso; 2 | 3 | import com.qingyou.sso.handler.platform.EmailSSOHandler; 4 | import com.qingyou.sso.infra.response.GlobalHttpResponse; 5 | import io.vertx.core.Handler; 6 | import io.vertx.ext.web.Router; 7 | import io.vertx.ext.web.handler.BodyHandler; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | public class EmailSSORouterHandler implements Handler { 12 | 13 | private final EmailSSOHandler emailSSOHandler; 14 | 15 | public EmailSSORouterHandler(EmailSSOHandler emailSSOHandler) { 16 | this.emailSSOHandler = emailSSOHandler; 17 | } 18 | 19 | @Override 20 | public void handle(Router router) { 21 | router.route("/sso/email/*").handler(BodyHandler.create()); 22 | router.get("/sso/email/code").handler(GlobalHttpResponse.wrap(emailSSOHandler::email, log)); 23 | router.get("/sso/email/login").handler(GlobalHttpResponse.wrap(emailSSOHandler::login, log)); 24 | router.get("/sso/email/register").handler(GlobalHttpResponse.wrap(emailSSOHandler::register, log)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sso-server-login-email-code/src/main/java/com/qingyou/sso/service/DefaultEmailSSOService.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.service; 2 | 3 | import com.qingyou.sso.api.param.Code; 4 | import com.qingyou.sso.api.param.Email; 5 | import com.qingyou.sso.api.param.UsernamePassword; 6 | import com.qingyou.sso.api.result.LoginResult; 7 | import com.qingyou.sso.domain.user.Account; 8 | import com.qingyou.sso.domain.user.User; 9 | import com.qingyou.sso.infra.cache.Cache; 10 | import com.qingyou.sso.infra.config.Configuration; 11 | import com.qingyou.sso.infra.config.ConfigurationSource; 12 | import com.qingyou.sso.infra.exception.BizException; 13 | import com.qingyou.sso.infra.exception.ErrorType; 14 | import com.qingyou.sso.infra.repository.domain.AccountRepository; 15 | import com.qingyou.sso.infra.repository.domain.UserRepository; 16 | import com.qingyou.sso.utils.PasswordEncodeUtils; 17 | import com.qingyou.sso.utils.StringUtils; 18 | import io.vertx.core.Future; 19 | import io.vertx.ext.mail.MailClient; 20 | import io.vertx.ext.mail.MailMessage; 21 | 22 | import java.time.Duration; 23 | 24 | public class DefaultEmailSSOService implements EmailSSOService { 25 | private final MailClient mailClient; 26 | private final Configuration.Mail.Message config; 27 | private final Cache cache; 28 | private final UserRepository userRepository; 29 | private final AccountRepository accountRepository; 30 | 31 | public DefaultEmailSSOService(MailClient mailClient, ConfigurationSource configurationSource, Cache cache, UserRepository userRepository, AccountRepository accountRepository) { 32 | this.mailClient = mailClient; 33 | this.config = configurationSource.getConfiguration().mail().message(); 34 | this.cache = cache; 35 | this.userRepository = userRepository; 36 | this.accountRepository = accountRepository; 37 | } 38 | 39 | 40 | @Override 41 | public Future email(Email email) { 42 | String code = StringUtils.randomNum(5); 43 | return cache.set(code, email, Duration.ofMillis(config.expire())).flatMap(v -> 44 | mailClient.sendMail(new MailMessage(config.from(), email.email() ,config.subject(),config.pattern().formatted(code))) 45 | .map(mailResult -> mailResult.getMessageID() != null && !mailResult.getMessageID().isEmpty()) 46 | ); 47 | } 48 | 49 | @Override 50 | public Future login(Code code) { 51 | return cache.get(code.code(), String.class).flatMap(email -> { 52 | if (email == null) throw new BizException(ErrorType.Showed.Auth,"Code expire or login failed"); 53 | else { 54 | return userRepository.findByEmail(email).map(user -> { 55 | if (user == null) throw new BizException(ErrorType.Showed.Auth,"Please register first"); 56 | return new LoginResult(user.getId(),user.getName()); 57 | }); 58 | } 59 | }); 60 | } 61 | 62 | @Override 63 | public Future register(Code code) { 64 | return cache.get(code.code(), String.class).flatMap(email -> { 65 | if (email == null) throw new BizException(ErrorType.Showed.Auth,"Code expire or login failed"); 66 | else { 67 | return userRepository.findByEmail(email).flatMap(user -> { 68 | if (user != null) throw new BizException(ErrorType.Showed.Auth,"Register failed, please login"); 69 | User register = new User(); 70 | register.setEmail(email); 71 | register.setName(email); 72 | return userRepository.insert(register); 73 | }).flatMap(user -> { 74 | return accountRepository.findByUsername(code.username()).map(account -> { 75 | if (account != null) throw new BizException(ErrorType.Showed.Auth,"Account of this username already exists"); 76 | return null; 77 | }).flatMap(v -> { 78 | var encoded = PasswordEncodeUtils.encode(code.password()); 79 | Account account = new Account( 80 | user.getId(), 81 | code.username() == null? user.getEmail() : code.username(), 82 | encoded.encoded(),encoded.salt(), 83 | user); 84 | return accountRepository.insert(account).map(user); 85 | }); 86 | }).map(user -> new LoginResult(user.getId(),user.getName())); 87 | } 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sso-server-login-email-code/src/main/java/com/qingyou/sso/service/EmailSSOService.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.service; 2 | 3 | import com.qingyou.sso.api.param.Code; 4 | import com.qingyou.sso.api.param.Email; 5 | import com.qingyou.sso.api.param.UsernamePassword; 6 | import com.qingyou.sso.api.result.LoginResult; 7 | import io.vertx.core.Future; 8 | 9 | public interface EmailSSOService { 10 | Future email(Email email); 11 | Future login(Code code); 12 | Future register(Code code); 13 | } 14 | -------------------------------------------------------------------------------- /sso-server-login/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | core-sso 9 | 1.0.1 10 | 11 | 12 | sso-server-login 13 | 14 | 15 | 17 16 | 17 17 | UTF-8 18 | 19 | 20 | 21 | 22 | com.qingyou 23 | sso-common 24 | ${project.version} 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.34 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/Info.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import com.qingyou.sso.api.result.LoginResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | public interface Info { 8 | Future info(RoutingContext routingContext); 9 | } 10 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/Login.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import com.qingyou.sso.api.result.LoginResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | public interface Login { 8 | Future login(RoutingContext routingContext); 9 | } 10 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/Logout.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import com.qingyou.sso.api.result.LoginResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | public interface Logout { 8 | Future logout(RoutingContext routingContext); 9 | } 10 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/Register.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import com.qingyou.sso.api.result.LoginResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | public interface Register { 8 | Future register(RoutingContext routingContext); 9 | } 10 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/State.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.ext.web.RoutingContext; 5 | 6 | public interface State { 7 | Future state(RoutingContext routingContext); 8 | } 9 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/param/Code.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | 5 | public record Code(String email, String code, @Nullable String username,@Nullable String password) { 6 | } 7 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/param/UsernamePassword.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | public record UsernamePassword( 4 | String username, 5 | String password 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/api/result/LoginResult.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.result; 2 | 3 | public record LoginResult(Long userId, String nickname) { 4 | } 5 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/handler/platform/CustomSSOHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler.platform; 2 | 3 | import com.qingyou.sso.api.Login; 4 | import com.qingyou.sso.api.Register; 5 | 6 | 7 | public interface CustomSSOHandler extends Login, Register { 8 | default String getName() { return "";} 9 | } 10 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/handler/platform/SSOHandlerRegistry.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler.platform; 2 | 3 | import com.qingyou.sso.service.BaseSSOService; 4 | 5 | public interface SSOHandlerRegistry { 6 | CustomSSOHandler getSSOHandler(BaseSSOService ssoService); 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/handler/sso/SSOHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler.sso; 2 | 3 | import com.qingyou.sso.api.Info; 4 | import com.qingyou.sso.api.Login; 5 | import com.qingyou.sso.api.Logout; 6 | import com.qingyou.sso.api.State; 7 | import com.qingyou.sso.api.result.LoginResult; 8 | import com.qingyou.sso.infra.exception.BizException; 9 | import com.qingyou.sso.infra.exception.ErrorType; 10 | import com.qingyou.sso.service.BaseSSOService; 11 | import io.vertx.core.Future; 12 | import io.vertx.ext.web.RoutingContext; 13 | import io.vertx.ext.web.Session; 14 | import lombok.AllArgsConstructor; 15 | 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.Base64; 18 | 19 | @AllArgsConstructor 20 | public class SSOHandler implements Login, Logout, State, Info { 21 | 22 | private final BaseSSOService baseSSOService; 23 | 24 | @Override 25 | public Future info(RoutingContext routingContext) { 26 | Session session = routingContext.session(); 27 | if (session == null) throw new BizException(ErrorType.Showed.Params, "session not exist, user already logged out"); 28 | Long userId = session.get("userId"); 29 | return baseSSOService.userinfo(userId); 30 | } 31 | 32 | @Override 33 | public Future login(RoutingContext routingContext) { 34 | var authorization = routingContext.request().getHeader("Authorization"); 35 | if (authorization == null || !authorization.startsWith("Basic")) 36 | throw new BizException(ErrorType.Showed.Params, "Authorization format incorrect"); 37 | byte[] base64 = Base64.getDecoder().decode(authorization.substring(6)); 38 | String[] words = new String(base64, StandardCharsets.UTF_8).split(":"); 39 | if (words.length != 2) throw new BizException(ErrorType.Showed.Params, "Authorization format incorrect"); 40 | if (words[0].isBlank()) throw new BizException(ErrorType.Showed.Params, "Username format incorrect"); 41 | if (words[1].isBlank()) throw new BizException(ErrorType.Showed.Params, "Password format incorrect"); 42 | return baseSSOService.login(words[0], words[1]).onSuccess(loginResult -> 43 | routingContext.session().put("userId", loginResult.userId()) 44 | ); 45 | } 46 | 47 | @Override 48 | public Future logout(RoutingContext routingContext) { 49 | Session session = routingContext.session(); 50 | if (session == null) throw new BizException(ErrorType.Showed.Params, "session not exist, user already logged out"); 51 | Long userId = session.get("userId"); 52 | return baseSSOService.logout(userId).onSuccess(loginResult -> 53 | routingContext.session().destroy() 54 | ); 55 | } 56 | 57 | @Override 58 | public Future state(RoutingContext routingContext) { 59 | Session session = routingContext.session(); 60 | if (session == null) throw new BizException(ErrorType.Showed.Params, "session not exist, user already logged out"); 61 | Long userId = session.get("userId"); 62 | return baseSSOService.state(userId); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/service/BaseSSOService.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.service; 2 | 3 | import com.qingyou.sso.api.dto.ThirdPartySSOUserInfo; 4 | import com.qingyou.sso.api.result.LoginResult; 5 | import io.vertx.core.Future; 6 | 7 | public interface BaseSSOService { 8 | Future login(String username, String password); 9 | Future login(ThirdPartySSOUserInfo thirdPartyUserInfo, DataChecker dataChecker); 10 | Future register(String username, String password); 11 | Future registerByThirdParty(String username, ThirdPartySSOUserInfo thirdPartyUserInfo); 12 | Future registerThirdParty(String username, String password, ThirdPartySSOUserInfo thirdPartyUserInfo, DataChecker dataChecker); 13 | Future changePassword(String username, String changed, ThirdPartySSOUserInfo thirdPartyUserInfo, DataChecker dataChecker); 14 | Future logout(Long userId); 15 | Future userinfo(Long userId); 16 | Future state(Long userId); 17 | } 18 | -------------------------------------------------------------------------------- /sso-server-login/src/main/java/com/qingyou/sso/service/DataChecker.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.service; 2 | 3 | import java.util.Map; 4 | 5 | @FunctionalInterface 6 | public interface DataChecker { 7 | boolean check(Map metadata); 8 | } 9 | -------------------------------------------------------------------------------- /sso-server-oauth2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | core-sso 9 | 1.0.1 10 | 11 | 12 | sso-server-oauth2 13 | 14 | 15 | 17 16 | 17 17 | UTF-8 18 | 19 | 20 | 21 | 22 | com.qingyou 23 | sso-common 24 | ${project.version} 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.34 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/Authorization.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.ext.web.RoutingContext; 4 | 5 | public interface Authorization { 6 | void authorize(RoutingContext routingContext); 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/Error.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.ext.web.RoutingContext; 4 | 5 | public interface Error { 6 | void error(RoutingContext routingContext); 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/Info.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.ext.web.RoutingContext; 4 | 5 | public interface Info { 6 | void info(RoutingContext routingContext); 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/Password.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.ext.web.RoutingContext; 4 | 5 | public interface Password { 6 | void password(RoutingContext routingContext); 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/Token.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api; 2 | 3 | import io.vertx.ext.web.RoutingContext; 4 | 5 | public interface Token { 6 | void token(RoutingContext routingContext); 7 | } 8 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/param/OAuth2Params.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.param; 2 | 3 | import com.qingyou.sso.infra.exception.ErrorType; 4 | import io.vertx.codegen.annotations.Nullable; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | import java.net.URI; 8 | 9 | public interface OAuth2Params { 10 | void inject(RoutingContext routingContext); 11 | 12 | record ValidationResult( 13 | @Nullable T result, 14 | ErrorType.OAuth2 error 15 | ){ 16 | public boolean success(){ 17 | return error == null; 18 | } 19 | public static ValidationResult result(T result) { 20 | return new ValidationResult<>(result, null); 21 | } 22 | public static ValidationResult fail(ErrorType.OAuth2 error) { 23 | return new ValidationResult<>(null, error); 24 | } 25 | } 26 | 27 | record Authorization( 28 | String clientId, 29 | String redirectURI, 30 | ResponseType responseType, 31 | @Nullable String codeChallenge, 32 | @Nullable String codeChallengeMethod, 33 | @Nullable String scope, 34 | @Nullable String state 35 | ) { 36 | } 37 | 38 | enum ResponseType { 39 | code, 40 | token 41 | } 42 | 43 | enum GrantType { 44 | authorization_code, 45 | refresh_token, 46 | client_credentials, 47 | } 48 | 49 | record Info( 50 | String accessToken 51 | ) { 52 | 53 | } 54 | 55 | /** 56 | * Combine refresh & verify 57 | */ 58 | record Token( 59 | GrantType grantType, 60 | String clientId, 61 | String clientSecret, 62 | @Nullable String refreshToken, 63 | @Nullable String redirectURI, 64 | @Nullable String code, 65 | @Nullable String codeVerifier 66 | ){ 67 | 68 | public ValidationResult credentials() { 69 | return ValidationResult.result(new Credentials(grantType, clientId, clientSecret)); 70 | } 71 | 72 | public ValidationResult refresh(){ 73 | if(refreshToken == null) return ValidationResult.fail(ErrorType.OAuth2.INVALID_REQUEST); 74 | return ValidationResult.result(new Refresh(grantType,clientId,clientSecret,refreshToken)); 75 | } 76 | 77 | public ValidationResult verify(){ 78 | if(redirectURI == null || code == null) return ValidationResult.fail(ErrorType.OAuth2.INVALID_REQUEST); 79 | try { 80 | URI uri = URI.create(redirectURI); 81 | if (uri.getHost() == null || uri.getHost().isEmpty()) 82 | return ValidationResult.fail(ErrorType.OAuth2.INVALID_REDIRECT_URI); 83 | if (uri.getScheme() == null 84 | || (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https")) 85 | ) return ValidationResult.fail(ErrorType.OAuth2.INVALID_REDIRECT_URI); 86 | } catch (IllegalArgumentException ex) { 87 | return ValidationResult.fail(ErrorType.OAuth2.INVALID_REDIRECT_URI); 88 | } 89 | return ValidationResult.result(new Verify(grantType,clientId,clientSecret,redirectURI,code,codeVerifier)); 90 | } 91 | 92 | } 93 | 94 | record Credentials( 95 | GrantType grantType, 96 | String clientId, 97 | String clientSecret 98 | ){} 99 | 100 | record Refresh( 101 | GrantType grantType, 102 | String clientId, 103 | String clientSecret, 104 | String refreshToken 105 | ) { 106 | 107 | } 108 | 109 | record Verify( 110 | GrantType grantType, 111 | String clientId, 112 | String clientSecret, 113 | String redirectURI, 114 | String code, 115 | @Nullable String codeVerifier 116 | ) { 117 | 118 | } 119 | 120 | record Password( 121 | String clientId, 122 | String clientSecret, 123 | String username, 124 | String password, 125 | @Nullable String scope 126 | ){ 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/result/AccessToken.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.result; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | 5 | public record AccessToken( 6 | String access_token, 7 | String token_type, 8 | @Nullable String refresh_token, 9 | long expires_in, 10 | String scope 11 | ) { 12 | } 13 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/result/AuthorizationCode.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.result; 2 | 3 | public record AuthorizationCode( 4 | String code) { 5 | } 6 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/result/ResourceData.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.result; 2 | 3 | public record ResourceData ( 4 | T data 5 | ){} 6 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/api/result/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.api.result; 2 | 3 | 4 | import com.qingyou.sso.api.constants.DataType; 5 | import com.qingyou.sso.api.constants.PlatformType; 6 | 7 | import java.util.List; 8 | 9 | public record UserInfo( 10 | String name, 11 | Long id, 12 | List additional 13 | ) { 14 | public record Additional( 15 | PlatformType platform, 16 | DataType dataType, 17 | String metadata 18 | ) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/handler/ErrorPageHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.handler; 2 | 3 | import com.qingyou.sso.api.Error; 4 | import com.qingyou.sso.infra.exception.BizException; 5 | import com.qingyou.sso.infra.exception.ErrorType; 6 | import com.qingyou.sso.infra.response.GlobalHttpResponse; 7 | import com.qingyou.sso.infra.response.OAuth2HttpResponse; 8 | import io.vertx.ext.web.RoutingContext; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | public class ErrorPageHandler implements Error { 13 | 14 | @Override 15 | public void error(RoutingContext routingContext) { 16 | String error = routingContext.queryParams().get("error"); 17 | String description = routingContext.queryParams().get("error_description"); 18 | ErrorType.OAuth2 errorType = ErrorType.OAuth2.of(error,description); 19 | if (errorType != null) { 20 | OAuth2HttpResponse.fail(routingContext, errorType, log); 21 | }else { 22 | GlobalHttpResponse.fail(routingContext, new BizException("Error redirect page[error=" + error +", description="+ description +"]"), log); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sso-server-oauth2/src/main/java/com/qingyou/sso/serviece/OAuth2Service.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.serviece; 2 | 3 | 4 | import com.qingyou.sso.api.param.OAuth2Params; 5 | import com.qingyou.sso.api.result.AccessToken; 6 | import com.qingyou.sso.api.result.AuthorizationCode; 7 | import com.qingyou.sso.api.result.ResourceData; 8 | import com.qingyou.sso.api.result.UserInfo; 9 | import io.vertx.core.Future; 10 | 11 | 12 | public interface OAuth2Service { 13 | Future authorization( 14 | Long userId, 15 | OAuth2Params.Authorization authorization 16 | ); 17 | 18 | Future verify( 19 | OAuth2Params.Verify verify 20 | ); 21 | 22 | Future implicitAuthorization( 23 | Long userId, 24 | OAuth2Params.Authorization authorization 25 | ); 26 | 27 | Future refresh( 28 | OAuth2Params.Refresh refresh 29 | ); 30 | 31 | Future> info( 32 | OAuth2Params.Info info 33 | ); 34 | 35 | Future password( 36 | OAuth2Params.Password password 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /sso-site/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | core-sso 9 | 1.0.1 10 | 11 | 12 | sso-site 13 | jar 14 | 15 | 16 | sso-client-site 17 | 18 | 19 | 20 | 17 21 | 17 22 | UTF-8 23 | 24 | 25 | 26 | 27 | com.qingyou 28 | sso-common 29 | ${project.version} 30 | 31 | 32 | 33 | org.projectlombok 34 | lombok 35 | 1.18.34 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sso-site/src/main/java/com/qingyou/sso/router/site/EmailSSORouterHandler.java: -------------------------------------------------------------------------------- 1 | package com.qingyou.sso.router.site; 2 | 3 | 4 | import io.vertx.core.Handler; 5 | import io.vertx.ext.web.Router; 6 | import io.vertx.ext.web.handler.StaticHandler; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | public class EmailSSORouterHandler implements Handler { 11 | 12 | @Override 13 | public void handle(Router router) { 14 | router.route("/sso/*").handler(StaticHandler.create()); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /sso-site/src/main/resources/note: -------------------------------------------------------------------------------- 1 | note: copy frontend index.html here -------------------------------------------------------------------------------- /sso-site/sso-client-site/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/README.md: -------------------------------------------------------------------------------- 1 | # Core-SSO Site 2 | 3 | Powered by React + Vite + TypeScript -------------------------------------------------------------------------------- /sso-site/sso-client-site/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /sso-site/sso-client-site/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Core-SSO Site. Lite and fast sso platform. 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sso-client-site", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^5.0.1", 14 | "@radix-ui/react-slot": "^1.2.0", 15 | "@tailwindcss/vite": "^4.0.0", 16 | "axios": "^1.8.4", 17 | "class-variance-authority": "^0.7.1", 18 | "clsx": "^2.1.1", 19 | "framer-motion": "^12.6.5", 20 | "install": "^0.13.0", 21 | "lucide-react": "^0.488.0", 22 | "notistack": "^3.0.2", 23 | "npm": "^11.3.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-error-boundary": "^5.0.0", 27 | "react-hook-form": "^7.55.0", 28 | "react-router": "^7.5.0", 29 | "react-router-dom": "^7.5.0", 30 | "tailwind-merge": "^3.2.0", 31 | "tailwindcss": "^4.0.0", 32 | "tw-animate-css": "^1.2.5", 33 | "zod": "^3.24.2" 34 | }, 35 | "devDependencies": { 36 | "@eslint/js": "^9.21.0", 37 | "@types/node": "^22.14.0", 38 | "@types/react": "^18.2.15", 39 | "@types/react-dom": "^18.2.7", 40 | "@vitejs/plugin-react": "^4.2.0", 41 | "eslint": "^9.21.0", 42 | "eslint-plugin-react-hooks": "^5.1.0", 43 | "eslint-plugin-react-refresh": "^0.4.19", 44 | "globals": "^15.15.0", 45 | "typescript": "~5.7.2", 46 | "typescript-eslint": "^8.24.1", 47 | "vite": "^5.2.10" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.qingyou 8 | sso-site 9 | 1.0.1 10 | 11 | 12 | sso-client-site 13 | pom 14 | 15 | 16 | 17 17 | 17 18 | UTF-8 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | com.github.eirslett 29 | frontend-maven-plugin 30 | 1.15.1 31 | 32 | 33 | install node and npm 34 | 35 | install-node-and-npm 36 | 37 | 38 | generate-resources 39 | 40 | 41 | 42 | v18.17.0 43 | 9.6.7 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from "react-router-dom"; 2 | import { router } from "./router"; 3 | import { SnackbarProvider } from "notistack"; 4 | import { ErrorBoundaryContainer } from "./components/base/ErrorBoundary"; 5 | 6 | function App() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/api/UserApi.ts: -------------------------------------------------------------------------------- 1 | 2 | const LoginApi = async (username: string, password: string) => { 3 | try { 4 | await new Promise(resolve => setTimeout(resolve, 1000)); 5 | console.log('✅ Login Success:', { username, password }); 6 | } catch (e) { 7 | console.error('❌ Login Failed', e); 8 | } 9 | }; 10 | 11 | export {LoginApi} -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/Form/FormContainer.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react"; 2 | import { 3 | DefaultValues, 4 | FieldValues, 5 | SubmitErrorHandler, 6 | SubmitHandler, 7 | useForm as useRHF, 8 | UseFormProps, 9 | FormProvider, 10 | UseFormReturn, 11 | } from "react-hook-form"; 12 | import { ZodSchema } from "zod"; 13 | import { zodResolver } from "@hookform/resolvers/zod"; 14 | 15 | type FormProps = { 16 | schema: ZodSchema; 17 | title?: string; 18 | onSubmit: SubmitHandler; 19 | onError?: SubmitErrorHandler; 20 | submitButtonText?: string; 21 | values?: UseFormProps["values"]; 22 | defaultValues?: DefaultValues; 23 | className?: string; 24 | } & PropsWithChildren; 25 | 26 | export type FormContext = { 27 | handleReset: () => void; 28 | } & UseFormReturn; 29 | 30 | export const FormContainer = ({ 31 | defaultValues, 32 | className, 33 | onSubmit, 34 | onError, 35 | schema, 36 | children, 37 | }: FormProps) => { 38 | const form = useRHF({ 39 | defaultValues, 40 | resolver: zodResolver(schema), 41 | }); 42 | const handleReset = () => { 43 | form.reset(defaultValues); 44 | }; 45 | 46 | const extendedForm: FormContext = { 47 | ...form, 48 | handleReset, 49 | }; 50 | 51 | return ( 52 | 53 |
{children}
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/Form/controllers/InputController.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, type ReactElement, type Ref } from "react"; 2 | import { 3 | Controller, 4 | type FieldValues, 5 | type Path, 6 | type ControllerRenderProps, 7 | } from "react-hook-form"; 8 | import { useFormContext } from "@/hooks/useFormContext"; 9 | import { Input } from "@/components/ui/input"; 10 | import { cn } from "@/lib/utils"; 11 | 12 | export type InputControllerProps = Omit< 13 | React.ComponentProps<"input">, 14 | "name" 15 | > & { 16 | name: Path; 17 | errorClassName?: string; 18 | rules?: Parameters["0"]["rules"]; 19 | }; 20 | 21 | const InputControllerInner = ( 22 | props: InputControllerProps, 23 | ref: Ref 24 | ): ReactElement => { 25 | const { name, className, errorClassName, placeholder, ...restProps } = props; 26 | const { 27 | control, 28 | formState: { errors }, 29 | } = useFormContext(); 30 | const error = errors[name]; 31 | 32 | return ( 33 | ( 37 |
38 | 45 | {error?.message && ( 46 |
{error.message}
47 | )} 48 |
49 | )} 50 | /> 51 | ); 52 | }; 53 | 54 | const fieldAdapter = ( 55 | field: ControllerRenderProps 56 | ) => ({ 57 | ...field, 58 | value: field.value as React.ComponentProps<"input">["value"], 59 | }); 60 | 61 | export const InputController = forwardRef(InputControllerInner) as < 62 | T extends FieldValues 63 | >( 64 | props: InputControllerProps & { ref?: Ref } 65 | ) => ReactElement; 66 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/base/Background.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | type BackgroundProps = React.HTMLProps & PropsWithChildren; 5 | export const Background: React.FC = ({ 6 | className, 7 | children, 8 | ...props 9 | }) => { 10 | return ( 11 |
12 |
19 |
20 | {children} 21 |
22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/base/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | import { ErrorBoundary, FallbackProps } from "react-error-boundary"; 3 | 4 | const ErrorFallback: React.FC = ({ 5 | error, 6 | resetErrorBoundary, 7 | }) => { 8 | return ( 9 |
10 |

出错了:

11 |
{error.message}
12 | 13 |
14 | ); 15 | }; 16 | 17 | export const ErrorBoundaryContainer: React.FC = ({ children }) => { 18 | return ( 19 | { 22 | window.location.reload(); 23 | }} 24 | > 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/base/Reveal.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useEffect, useRef } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface RevealProps { 5 | className?: string 6 | } 7 | export const Reveal: React.FC = ({ className, children }) => { 8 | 9 | const ref = useRef(null); 10 | 11 | useEffect(() => { 12 | const observer = new IntersectionObserver(([entry]) => { 13 | if (entry.isIntersecting && ref.current) { 14 | ref.current.classList.add('visible') 15 | } 16 | }, { threshold: 0.2, rootMargin: '0px 0px -50px 0px' }) 17 | if (ref.current) observer.observe(ref.current) 18 | 19 | return () => { 20 | observer.disconnect() 21 | } 22 | }, []) 23 | 24 | return ( 25 |
26 | { children } 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/business/LoginForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormContainer } from "@components/Form/FormContainer"; 2 | import { InputController } from "@components/Form/controllers/InputController"; 3 | import { Button } from "@/components/ui/button"; 4 | import { cn } from "@/lib/utils"; 5 | import { LoginFormSchema, LoginFormType } from "./schema"; 6 | 7 | interface LoginFormProps { 8 | className?: string; 9 | } 10 | export const LoginForm: React.FC = ({ className }) => { 11 | return ( 12 | { 22 | console.log(data); 23 | }} 24 | onError={(errors) => { 25 | console.log(errors); 26 | }} 27 | schema={LoginFormSchema} 28 | > 29 |
30 | 31 | placeholder="请输入用户名" 32 | name="username" 33 | type="text" 34 | /> 35 | 36 | placeholder="密码" 37 | name="password" 38 | type="password" 39 | /> 40 | 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/business/LoginForm/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | export const LoginFormSchema = z.object({ 3 | username: z.string().min(1, "用户名不能为空"), 4 | password: z.string().min(8, "密码长度至少8位"), 5 | }); 6 | 7 | export type LoginFormType = z.infer -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/hooks/useFormContext.ts: -------------------------------------------------------------------------------- 1 | import { FormContext } from "@/components/Form/FormContainer"; 2 | import { useFormContext as useRHFFormContext } from "react-hook-form"; 3 | 4 | const useFormContext = >() => { 5 | const context = useRHFFormContext() as FormContext; 6 | 7 | if (context === undefined) { 8 | throw new Error("useFormContext must be used within a FormConatiner"); 9 | } 10 | 11 | return context; 12 | }; 13 | 14 | export { useFormContext }; 15 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --radius-sm: calc(var(--radius) - 4px); 8 | --radius-md: calc(var(--radius) - 2px); 9 | --radius-lg: var(--radius); 10 | --radius-xl: calc(var(--radius) + 4px); 11 | --color-background: var(--background); 12 | --color-foreground: var(--foreground); 13 | --color-card: var(--card); 14 | --color-card-foreground: var(--card-foreground); 15 | --color-popover: var(--popover); 16 | --color-popover-foreground: var(--popover-foreground); 17 | --color-primary: var(--primary); 18 | --color-primary-foreground: var(--primary-foreground); 19 | --color-secondary: var(--secondary); 20 | --color-secondary-foreground: var(--secondary-foreground); 21 | --color-muted: var(--muted); 22 | --color-muted-foreground: var(--muted-foreground); 23 | --color-accent: var(--accent); 24 | --color-accent-foreground: var(--accent-foreground); 25 | --color-destructive: var(--destructive); 26 | --color-border: var(--border); 27 | --color-input: var(--input); 28 | --color-ring: var(--ring); 29 | --color-chart-1: var(--chart-1); 30 | --color-chart-2: var(--chart-2); 31 | --color-chart-3: var(--chart-3); 32 | --color-chart-4: var(--chart-4); 33 | --color-chart-5: var(--chart-5); 34 | --color-sidebar: var(--sidebar); 35 | --color-sidebar-foreground: var(--sidebar-foreground); 36 | --color-sidebar-primary: var(--sidebar-primary); 37 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 38 | --color-sidebar-accent: var(--sidebar-accent); 39 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 40 | --color-sidebar-border: var(--sidebar-border); 41 | --color-sidebar-ring: var(--sidebar-ring); 42 | } 43 | 44 | :root { 45 | --radius: 0.625rem; 46 | --background: oklch(1 0 0); 47 | --foreground: oklch(0.145 0 0); 48 | --card: oklch(1 0 0); 49 | --card-foreground: oklch(0.145 0 0); 50 | --popover: oklch(1 0 0); 51 | --popover-foreground: oklch(0.145 0 0); 52 | --primary: oklch(0.205 0 0); 53 | --primary-foreground: oklch(0.985 0 0); 54 | --secondary: oklch(0.97 0 0); 55 | --secondary-foreground: oklch(0.205 0 0); 56 | --muted: oklch(0.97 0 0); 57 | --muted-foreground: oklch(0.556 0 0); 58 | --accent: oklch(0.97 0 0); 59 | --accent-foreground: oklch(0.205 0 0); 60 | --destructive: oklch(0.577 0.245 27.325); 61 | --border: oklch(0.922 0 0); 62 | --input: oklch(0.922 0 0); 63 | --ring: oklch(0.708 0 0); 64 | --chart-1: oklch(0.646 0.222 41.116); 65 | --chart-2: oklch(0.6 0.118 184.704); 66 | --chart-3: oklch(0.398 0.07 227.392); 67 | --chart-4: oklch(0.828 0.189 84.429); 68 | --chart-5: oklch(0.769 0.188 70.08); 69 | --sidebar: oklch(0.985 0 0); 70 | --sidebar-foreground: oklch(0.145 0 0); 71 | --sidebar-primary: oklch(0.205 0 0); 72 | --sidebar-primary-foreground: oklch(0.985 0 0); 73 | --sidebar-accent: oklch(0.97 0 0); 74 | --sidebar-accent-foreground: oklch(0.205 0 0); 75 | --sidebar-border: oklch(0.922 0 0); 76 | --sidebar-ring: oklch(0.708 0 0); 77 | } 78 | 79 | .dark { 80 | --background: oklch(0.145 0 0); 81 | --foreground: oklch(0.985 0 0); 82 | --card: oklch(0.205 0 0); 83 | --card-foreground: oklch(0.985 0 0); 84 | --popover: oklch(0.205 0 0); 85 | --popover-foreground: oklch(0.985 0 0); 86 | --primary: oklch(0.922 0 0); 87 | --primary-foreground: oklch(0.205 0 0); 88 | --secondary: oklch(0.269 0 0); 89 | --secondary-foreground: oklch(0.985 0 0); 90 | --muted: oklch(0.269 0 0); 91 | --muted-foreground: oklch(0.708 0 0); 92 | --accent: oklch(0.269 0 0); 93 | --accent-foreground: oklch(0.985 0 0); 94 | --destructive: oklch(0.704 0.191 22.216); 95 | --border: oklch(1 0 0 / 10%); 96 | --input: oklch(1 0 0 / 15%); 97 | --ring: oklch(0.556 0 0); 98 | --chart-1: oklch(0.488 0.243 264.376); 99 | --chart-2: oklch(0.696 0.17 162.48); 100 | --chart-3: oklch(0.769 0.188 70.08); 101 | --chart-4: oklch(0.627 0.265 303.9); 102 | --chart-5: oklch(0.645 0.246 16.439); 103 | --sidebar: oklch(0.205 0 0); 104 | --sidebar-foreground: oklch(0.985 0 0); 105 | --sidebar-primary: oklch(0.488 0.243 264.376); 106 | --sidebar-primary-foreground: oklch(0.985 0 0); 107 | --sidebar-accent: oklch(0.269 0 0); 108 | --sidebar-accent-foreground: oklch(0.985 0 0); 109 | --sidebar-border: oklch(1 0 0 / 10%); 110 | --sidebar-ring: oklch(0.556 0 0); 111 | } 112 | 113 | 114 | .reveal { 115 | opacity: 0; 116 | transform: translateY(20px); 117 | transition: opacity 0.7s, transform 0.7s ease; 118 | } 119 | 120 | .reveal.visible { 121 | opacity: 1; 122 | transform: translateY(0); 123 | } 124 | 125 | @layer base { 126 | * { 127 | @apply border-border outline-ring/50; 128 | } 129 | body { 130 | @apply bg-background text-foreground; 131 | } 132 | } -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | import { Background } from "@/components/base/Background"; 3 | const Home = () => { 4 | return ( 5 | 6 |
7 | 8 |
9 |
10 | ); 11 | }; 12 | export default Home; -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/pages/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { Reveal } from "@/components/base/Reveal"; 2 | import { LoginForm } from "@/components/business/LoginForm"; 3 | import Logo from "@/assets/logo.svg"; 4 | const LoginPage = () => { 5 | return ( 6 | 7 |
8 | feellmoose 9 |

𝓯𝓮𝓮𝓵𝓵𝓶𝓸𝓸𝓼𝓮

10 |
11 | 12 |
13 | ); 14 | }; 15 | 16 | export default LoginPage; 17 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/router.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom'; 2 | import Home from './pages/Home'; 3 | import LoginPage from './pages/LoginPage'; 4 | 5 | export const router = createBrowserRouter( 6 | createRoutesFromElements( 7 | }> 8 | } /> // 默认子路由 9 | 10 | ) 11 | ); -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; 2 | import type { Response } from "./response"; 3 | import { showSnack } from "@/utils/showSnack"; 4 | export function Http( 5 | url: string, 6 | header: Record = {}, 7 | hooks: { 8 | redirectToLogin?: () => void; 9 | errorHandler?: (response: AxiosResponse) => void; 10 | beforeRequest?: (config: AxiosRequestConfig) => AxiosRequestConfig; 11 | } = {} 12 | ) { 13 | let config: AxiosRequestConfig = {}; 14 | config.headers = header; 15 | config.baseURL = import.meta.env.VITE_API_BASE_URL; 16 | 17 | const { redirectToLogin, errorHandler, beforeRequest } = hooks; 18 | // const navigate = useNavigate(); 19 | 20 | config.headers.Authorization = localStorage.getItem("token") || ""; 21 | if (beforeRequest) { 22 | config = beforeRequest(config); 23 | } 24 | //axios包装的那一层的http状态 25 | const STATUS = { 26 | SUCCESS: 200, 27 | UNAUTHORIZED: 401, 28 | }; 29 | //自己和服务端协商的code格式 30 | const CODE = { 31 | SUCCESS: 200, 32 | UNAUTHORIZED: 401, 33 | }; 34 | let response: Promise>>; 35 | 36 | async function respond( 37 | response: Promise>> 38 | ) { 39 | return new Promise(async (resolve, reject) => { 40 | const res = await response; 41 | if (res.status !== STATUS.SUCCESS) { 42 | if (errorHandler) { 43 | errorHandler(res); 44 | } else { 45 | showSnack("请求出现异常,请稍后再试", { variant: "error" }); 46 | } 47 | reject(res); 48 | return; 49 | } 50 | if (res.data.code === CODE.UNAUTHORIZED) { 51 | if (redirectToLogin) { 52 | redirectToLogin(); 53 | } else { 54 | showSnack("登录信息已过期,请重新登录", { variant: "error" }); 55 | //这里改成跳到login 56 | // navigate({ to: ".." }); 57 | } 58 | reject("登录信息已过期,请重新登录"); 59 | return; 60 | } 61 | if (res.data.code !== CODE.SUCCESS) { 62 | if (errorHandler) { 63 | errorHandler(res); 64 | } else { 65 | showSnack(res.data.message, { variant: "error" }); 66 | } 67 | reject(res.data); 68 | return; 69 | } 70 | resolve(res.data.data); 71 | }); 72 | } 73 | 74 | async function post(data?: any): Promise { 75 | response = axios.post(url, data, config); 76 | return respond(response); 77 | } 78 | async function get(): Promise { 79 | response = axios.get(url, config); 80 | return respond(response); 81 | } 82 | return { 83 | post, 84 | get, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/utils/response.ts: -------------------------------------------------------------------------------- 1 | export interface Response { 2 | code: number 3 | message: string 4 | data: T 5 | } 6 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/utils/showSnack.ts: -------------------------------------------------------------------------------- 1 | import { enqueueSnackbar, VariantType, OptionsObject } from "notistack"; 2 | 3 | type ShowSnackOptions = Partial & { 4 | variant?: VariantType; 5 | duration?: number; 6 | }; 7 | 8 | export const showSnack = (message: string, options: ShowSnackOptions = {}) => { 9 | const defaultOptions: ShowSnackOptions = { 10 | variant: "success", 11 | duration: 3000, 12 | }; 13 | return enqueueSnackbar(message, { 14 | ...defaultOptions, 15 | ...options, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"], 6 | "@components/*": ["src/components/*"], 7 | "@api/*": ["src/api/*"], 8 | "@hooks/*": ["src/hooks/*"] 9 | }, 10 | 11 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 12 | "target": "ES2020", 13 | "useDefineForClassFields": true, 14 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 15 | "module": "ESNext", 16 | "skipLibCheck": true, 17 | 18 | /* Bundler mode */ 19 | "moduleResolution": "bundler", 20 | "allowImportingTsExtensions": true, 21 | "isolatedModules": true, 22 | "moduleDetection": "force", 23 | "noEmit": true, 24 | "jsx": "react-jsx", 25 | 26 | /* Linting */ 27 | "strict": true, 28 | "noUnusedLocals": true, 29 | "noUnusedParameters": true, 30 | "noFallthroughCasesInSwitch": true, 31 | "noUncheckedSideEffectImports": true 32 | }, 33 | "types": ["vite/client", "node"], 34 | "include": ["vite.config.ts","src"] 35 | } 36 | -------------------------------------------------------------------------------- /sso-site/sso-client-site/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite'; 4 | import path from 'path' 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(),tailwindcss()], 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, 'src'), 12 | '@components': path.resolve(__dirname, 'src/components'), 13 | '@api': path.resolve(__dirname, 'src/api'), 14 | '@hooks': path.resolve(__dirname, 'src/hooks'), 15 | }, 16 | }, 17 | }) 18 | --------------------------------------------------------------------------------