This is an automated email from the ASF dual-hosted git repository.

benjobs pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-streampark.git


The following commit(s) were added to refs/heads/dev by this push:
     new ce76631a0 [Feature] Support Single Sign-On (SSO) (#2776)
ce76631a0 is described below

commit ce76631a0c8f07005512e5e022f19b88d56d3f0f
Author: Leomax_Sun <[email protected]>
AuthorDate: Mon Jun 19 12:56:24 2023 +0800

    [Feature] Support Single Sign-On (SSO) (#2776)
    
    * [Feature] Support SSO login (#2736)
    
    ---------
    
    Co-authored-by: asuiah <[email protected]>
---
 dist-material/release-docs/LICENSE                 |   5 +
 .../streampark-console-service/pom.xml             |  39 ++++++
 .../main/assembly/script/schema/mysql-schema.sql   |   4 +-
 .../main/assembly/script/schema/pgsql-schema.sql   |   4 +-
 .../main/assembly/script/upgrade/mysql/2.2.0.sql   |   4 +
 .../main/assembly/script/upgrade/pgsql/2.2.0.sql   |   4 +
 .../streampark/console/core/enums/LoginType.java   |   5 +-
 .../system/authentication/ShiroService.java        |  95 +++++++++++++++
 .../system/authentication/SsoShiroPlugin.java      |  86 +++++++++++++
 .../console/system/controller/SsoController.java   | 134 +++++++++++++++++++++
 .../system/security/impl/AuthenticatorImpl.java    |  38 ++++--
 .../system/service/impl/UserServiceImpl.java       |  10 +-
 .../src/main/resources/application-sso.yml         |  26 ++++
 .../src/main/resources/application.yml             |   9 +-
 .../src/main/resources/db/schema-h2.sql            |   4 +-
 .../src/api/system/user.ts                         |  15 ++-
 .../src/enums/pageEnum.ts                          |   2 +
 .../src/locales/lang/en/sys.ts                     |   1 +
 .../src/locales/lang/zh-CN/sys.ts                  |   1 +
 .../src/router/guard/index.ts                      |   2 +
 .../src/router/guard/permissionGuard.ts            |   5 +-
 .../src/router/guard/ssoGuard.ts                   |  58 +++++++++
 .../src/views/base/login/LoginForm.vue             |  15 ++-
 23 files changed, 538 insertions(+), 28 deletions(-)

diff --git a/dist-material/release-docs/LICENSE 
b/dist-material/release-docs/LICENSE
index be34de4d7..1820b467b 100644
--- a/dist-material/release-docs/LICENSE
+++ b/dist-material/release-docs/LICENSE
@@ -581,6 +581,11 @@ The text of each license is the standard Apache 2.0 
license. https://www.apache.
     
https://mvnrepository.com/artifact/org.wildfly.client/wildfly-client-config/1.0.1.Final
 Apache-2.0
     
https://mvnrepository.com/artifact/org.wildfly.common/wildfly-common/1.5.4.Final
 Apache-2.0
     https://mvnrepository.com/artifact/org.javassist/javassist/3.24.0-GA 
Apache-2.0
+    https://mvnrepository.com/artifact/io.buji/buji-pac4j/5.0.1 Apache-2.0
+    https://mvnrepository.com/artifact/org.pac4j/pac4j-core/4.5.7 Apache-2.0
+    https://mvnrepository.com/artifact/org.pac4j/pac4j-springboot/4.5.7 
Apache-2.0
+    https://mvnrepository.com/artifact/org.pac4j/pac4j-oauth/4.5.7 Apache-2.0
+    https://mvnrepository.com/artifact/org.pac4j/pac4j-oidc/4.5.7 Apache-2.0
     https://maven.apache.org/wrapper Apache-2.0
     mvnw files from https://github.com/apache/maven-wrapper Apache 2.0
     
streampark-console/streampark-console-service/src/main/assembly/bin/setclasspath.sh
 from https://github.com/apache/tomcat
diff --git a/streampark-console/streampark-console-service/pom.xml 
b/streampark-console/streampark-console-service/pom.xml
index c8a587042..5705bded4 100644
--- a/streampark-console/streampark-console-service/pom.xml
+++ b/streampark-console/streampark-console-service/pom.xml
@@ -46,6 +46,12 @@
         <commons-compress.version>1.21</commons-compress.version>
         <javax-mail.version>1.4.7</javax-mail.version>
         <shiro.version>1.10.0</shiro.version>
+
+        <!-- Pac4j 4.x for jdk 8 -->
+        <pac4jVersion>4.5.7</pac4jVersion>
+        <!-- buji-pac4j 5.x for jdk 8-->
+        <bujiVersion>5.0.1</bujiVersion>
+
         <p6spy.version>3.9.1</p6spy.version>
         <freemarker.version>2.3.30</freemarker.version>
         <commons-email.version>1.5</commons-email.version>
@@ -185,6 +191,39 @@
             </exclusions>
         </dependency>
 
+        <!-- pac4j for SSO-->
+        <dependency>
+            <groupId>io.buji</groupId>
+            <artifactId>buji-pac4j</artifactId>
+            <version>${bujiVersion}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.pac4j</groupId>
+            <artifactId>pac4j-core</artifactId>
+            <version>${pac4jVersion}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.pac4j</groupId>
+            <artifactId>pac4j-springboot</artifactId>
+            <version>${pac4jVersion}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-collections</groupId>
+                    <artifactId>commons-collections</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.pac4j</groupId>
+            <artifactId>pac4j-oauth</artifactId>
+            <version>${pac4jVersion}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.pac4j</groupId>
+            <artifactId>pac4j-oidc</artifactId>
+            <version>${pac4jVersion}</version>
+        </dependency>
+
         <!-- spring cache -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
diff --git 
a/streampark-console/streampark-console-service/src/main/assembly/script/schema/mysql-schema.sql
 
b/streampark-console/streampark-console-service/src/main/assembly/script/schema/mysql-schema.sql
index 3a8f9ce4f..01d037191 100644
--- 
a/streampark-console/streampark-console-service/src/main/assembly/script/schema/mysql-schema.sql
+++ 
b/streampark-console/streampark-console-service/src/main/assembly/script/schema/mysql-schema.sql
@@ -379,10 +379,10 @@ create table `t_user` (
   `username` varchar(64) collate utf8mb4_general_ci not null comment 'user 
name',
   `nick_name` varchar(64) collate utf8mb4_general_ci not null comment 'nick 
name',
   `salt` varchar(26) collate utf8mb4_general_ci default null comment 'salt',
-  `password` varchar(64) collate utf8mb4_general_ci not null comment 
'password',
+  `password` varchar(64) collate utf8mb4_general_ci default null comment 
'password',
   `email` varchar(64) collate utf8mb4_general_ci default null comment 'email',
   `user_type` int  not null comment 'user type 1:admin 2:user',
-  `login_type` tinyint default 0 comment 'login type 0:password 1:ldap',
+  `login_type` tinyint default 0 comment 'login type 0:password 1:ldap 2:sso',
   `last_team_id` bigint default null comment 'last team id',
   `status` char(1) collate utf8mb4_general_ci not null comment 'status 
0:locked 1:active',
   `create_time` datetime not null default current_timestamp comment 'create 
time',
diff --git 
a/streampark-console/streampark-console-service/src/main/assembly/script/schema/pgsql-schema.sql
 
b/streampark-console/streampark-console-service/src/main/assembly/script/schema/pgsql-schema.sql
index a8706ebe5..1e68b967b 100644
--- 
a/streampark-console/streampark-console-service/src/main/assembly/script/schema/pgsql-schema.sql
+++ 
b/streampark-console/streampark-console-service/src/main/assembly/script/schema/pgsql-schema.sql
@@ -706,7 +706,7 @@ create table "public"."t_user" (
   "username" varchar(64) collate "pg_catalog"."default" not null,
   "nick_name" varchar(64) collate "pg_catalog"."default" not null,
   "salt" varchar(26) collate "pg_catalog"."default",
-  "password" varchar(64) collate "pg_catalog"."default" not null,
+  "password" varchar(64) collate "pg_catalog"."default",
   "email" varchar(64) collate "pg_catalog"."default",
   "user_type" int4,
   "login_type" int2 default 0,
@@ -727,7 +727,7 @@ comment on column "public"."t_user"."salt" is 'salt';
 comment on column "public"."t_user"."password" is 'password';
 comment on column "public"."t_user"."email" is 'email';
 comment on column "public"."t_user"."user_type" is 'user type 1:admin 2:user';
-comment on column "public"."t_user"."login_type" is 'login type 0:password 
1:ldap';
+comment on column "public"."t_user"."login_type" is 'login type 0:password 
1:ldap 2:sso';
 comment on column "public"."t_user"."last_team_id" is 'last team id';
 comment on column "public"."t_user"."status" is 'status 0:locked 1:active';
 comment on column "public"."t_user"."create_time" is 'creation time';
diff --git 
a/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/mysql/2.2.0.sql
 
b/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/mysql/2.2.0.sql
index 581f6c145..87406e9e8 100644
--- 
a/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/mysql/2.2.0.sql
+++ 
b/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/mysql/2.2.0.sql
@@ -64,4 +64,8 @@ insert into `t_role_menu` (role_id, menu_id) values (100002, 
120401);
 insert into `t_role_menu` (role_id, menu_id) values (100002, 120402);
 insert into `t_role_menu` (role_id, menu_id) values (100002, 120403);
 
+-- add sso as login type
+alter table `t_user` modify column `password` varchar(64) collate 
utf8mb4_general_ci default null comment 'password';
+alter table `t_user` modify column `login_type` tinyint default 0 comment 
'login type 0:password 1:ldap 2:sso';
+
 set foreign_key_checks = 1;
diff --git 
a/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/pgsql/2.2.0.sql
 
b/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/pgsql/2.2.0.sql
index 6178798fd..f2720b43f 100644
--- 
a/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/pgsql/2.2.0.sql
+++ 
b/streampark-console/streampark-console-service/src/main/assembly/script/upgrade/pgsql/2.2.0.sql
@@ -73,3 +73,7 @@ insert into "public"."t_role_menu" (role_id, menu_id) values 
(100002, 120400);
 insert into "public"."t_role_menu" (role_id, menu_id) values (100002, 120401);
 insert into "public"."t_role_menu" (role_id, menu_id) values (100002, 120402);
 insert into "public"."t_role_menu" (role_id, menu_id) values (100002, 120403);
+
+-- add sso as login type
+alter table "public"."t_user" alter column "password" TYPE varchar(64) collate 
"pg_catalog"."default";
+comment on column "public"."t_user"."login_type" is 'login type 0:password 
1:ldap 2:sso';
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/enums/LoginType.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/enums/LoginType.java
index 7c0060cae..7467d6531 100644
--- 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/enums/LoginType.java
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/core/enums/LoginType.java
@@ -29,7 +29,10 @@ public enum LoginType {
   PASSWORD(0),
 
   /** sign in with ldap */
-  LDAP(1);
+  LDAP(1),
+
+  /** sign in with SSO */
+  SSO(2);
 
   @EnumValue private final int code;
 
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/ShiroService.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/ShiroService.java
new file mode 100644
index 000000000..2abe1bbac
--- /dev/null
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/ShiroService.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.streampark.console.system.authentication;
+
+import org.apache.shiro.realm.Realm;
+import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
+import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
+import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
+import org.apache.shiro.web.servlet.AbstractShiroFilter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.Filter;
+
+import java.util.Map;
+
+@Component
+@Slf4j
+/** Service used to change shiro filter and filterChains dynamically */
+public class ShiroService {
+  @Autowired private ShiroFilterFactoryBean shiroFilterFactoryBean;
+
+  private DefaultFilterChainManager filterChainManager;
+  private DefaultWebSecurityManager securityManager;
+
+  @PostConstruct
+  public void init() {
+    AbstractShiroFilter shiroFilter = null;
+    try {
+      shiroFilter = shiroFilterFactoryBean.getObject();
+    } catch (Exception e) {
+      throw new RuntimeException("Fail to get ShiroFilter from 
shiroFilterFactoryBean!");
+    }
+    securityManager = (DefaultWebSecurityManager) 
shiroFilter.getSecurityManager();
+    PathMatchingFilterChainResolver filterChainResolver =
+        (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
+    filterChainManager = (DefaultFilterChainManager) 
filterChainResolver.getFilterChainManager();
+  }
+
+  public void addRealm(Realm realm) {
+    synchronized (this) {
+      securityManager.getRealms().add(realm);
+    }
+  }
+
+  public void addFilters(Map<String, Filter> filters) {
+    synchronized (this) {
+      filters.forEach(
+          (key, value) -> {
+            // The new filter can be appended after the existing map.
+            // As the sequence doesn't matter.
+            shiroFilterFactoryBean.getFilters().put(key, value);
+            filterChainManager.addFilter(key, value);
+          });
+    }
+  }
+
+  public void addFilterChains(Map<String, String> filterChainDefinitionMap) {
+    synchronized (this) {
+      // Append the new filterchain onto the head of the current 
filterChainDefinitionMap.
+      // To prevent being captured by filter chain ("/**", "jwt").
+      
filterChainDefinitionMap.putAll(shiroFilterFactoryBean.getFilterChainDefinitionMap());
+      // Remove and re-insert the filterChainDefinitionMap
+      filterChainManager.getFilterChains().clear();
+      shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
+      // Reset the filterChainDefinitionMap
+      
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
+      filterChainDefinitionMap.forEach(
+          (key, value) -> {
+            String url = key;
+            String chainDefinition = value.trim();
+            filterChainManager.createChain(url, chainDefinition);
+          });
+    }
+  }
+}
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/SsoShiroPlugin.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/SsoShiroPlugin.java
new file mode 100644
index 000000000..326f1a608
--- /dev/null
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/authentication/SsoShiroPlugin.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.streampark.console.system.authentication;
+
+import io.buji.pac4j.filter.CallbackFilter;
+import io.buji.pac4j.filter.LogoutFilter;
+import io.buji.pac4j.filter.SecurityFilter;
+import io.buji.pac4j.realm.Pac4jRealm;
+import lombok.extern.slf4j.Slf4j;
+import org.pac4j.core.config.Config;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.Filter;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+
+@Component
+@Configuration
+@Slf4j
+/** Plugin for {@link ShiroConfig.java} to load SSO config if enabled */
+public class SsoShiroPlugin {
+  @Autowired private Config ssoConfig;
+  @Autowired private ShiroService shiroService;
+
+  @Value("${sso.enable:#{false}}")
+  private Boolean ssoEnable;
+
+  @PostConstruct
+  public void init() {
+    // Make sso controller anon if it's not enabled
+    if (!ssoEnable) {
+      LinkedHashMap<String, String> filterChainDefinitionMap = new 
LinkedHashMap<>();
+      filterChainDefinitionMap.put("/sso/signin", "anon");
+      filterChainDefinitionMap.put("/sso/token", "anon");
+      shiroService.addFilterChains(filterChainDefinitionMap);
+      return;
+    }
+
+    // Add Pac4jRealm into shiro
+    shiroService.addRealm(new Pac4jRealm());
+
+    // Construct the shiro filter for SSO
+    SecurityFilter securityFilter = new SecurityFilter();
+    CallbackFilter callbackFilter = new CallbackFilter();
+    LogoutFilter logoutFilter = new LogoutFilter();
+    securityFilter.setConfig(ssoConfig);
+    callbackFilter.setConfig(ssoConfig);
+    logoutFilter.setConfig(ssoConfig);
+    logoutFilter.setDefaultUrl("/?defaulturlafterlogout");
+    LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
+    filters.put("ssoSecurityFilter", securityFilter);
+    filters.put("ssoCallbackFilter", callbackFilter);
+    filters.put("ssoLogoutFilter", logoutFilter);
+    shiroService.addFilters(filters);
+
+    // Construct the filterChainDefinitionMap for SSO
+    LinkedHashMap<String, String> filterChainDefinitionMap = new 
LinkedHashMap<>();
+    filterChainDefinitionMap.put("/sso/signin", "ssoSecurityFilter");
+    filterChainDefinitionMap.put("/sso/token", "ssoSecurityFilter");
+    filterChainDefinitionMap.put("/pac4jLogout", "ssoLogoutFilter");
+    // Get callback endpoint from callbackUrl
+    String callbackEndpoint = 
URI.create(ssoConfig.getClients().getCallbackUrl()).getPath();
+    filterChainDefinitionMap.put(callbackEndpoint, "ssoCallbackFilter");
+    shiroService.addFilterChains(filterChainDefinitionMap);
+  }
+}
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/controller/SsoController.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/controller/SsoController.java
new file mode 100644
index 000000000..f40b22376
--- /dev/null
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/controller/SsoController.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.streampark.console.system.controller;
+
+import org.apache.streampark.common.util.DateUtils;
+import org.apache.streampark.console.base.domain.ResponseCode;
+import org.apache.streampark.console.base.domain.RestResponse;
+import org.apache.streampark.console.base.exception.ApiAlertException;
+import org.apache.streampark.console.base.properties.ShiroProperties;
+import org.apache.streampark.console.base.util.WebUtils;
+import org.apache.streampark.console.core.enums.LoginType;
+import org.apache.streampark.console.system.authentication.JWTToken;
+import org.apache.streampark.console.system.authentication.JWTUtil;
+import org.apache.streampark.console.system.entity.User;
+import org.apache.streampark.console.system.security.Authenticator;
+import org.apache.streampark.console.system.service.UserService;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.subject.Subject;
+
+import io.buji.pac4j.subject.Pac4jPrincipal;
+import lombok.extern.slf4j.Slf4j;
+import org.pac4j.core.profile.CommonProfile;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Controller
+@RequestMapping("sso")
+public class SsoController {
+  @Autowired private UserService userService;
+
+  @Autowired private ShiroProperties properties;
+
+  @Autowired private Authenticator authenticator;
+
+  @Value("${pac4j.properties.principalNameAttribute:#{null}}")
+  private String principalNameAttribute;
+
+  @Value("${sso.enable:#{false}}")
+  private Boolean ssoEnable;
+
+  @GetMapping("signin")
+  public ModelAndView signin(RedirectAttributes attributes) throws Exception {
+    // Redirect to home page with identity
+    String url = "/#/?from=sso";
+    return new ModelAndView("redirect:" + url);
+  }
+
+  @GetMapping("token")
+  @ResponseBody
+  public RestResponse token(HttpServletRequest request, HttpServletResponse 
response)
+      throws Exception {
+    if (!ssoEnable) {
+      throw new ApiAlertException(
+          "Single Sign On (SSO) is not available, please contact the 
administrator to enable");
+    }
+    // Based on User Profile from Shiro and build Pac4jPrincipal
+    Subject subject = SecurityUtils.getSubject();
+    PrincipalCollection principals = subject.getPrincipals();
+    Pac4jPrincipal principal = principals.oneByType(Pac4jPrincipal.class);
+    List<CommonProfile> profiles = null;
+    if (principal != null) {
+      profiles = principal.getProfiles();
+    }
+    principal = new Pac4jPrincipal(profiles, principalNameAttribute);
+    if (principal.getName() == null) {
+      log.error(
+          "Please configure correct principalNameAttribute from UserProfile: "
+              + principal.toString());
+      throw new ApiAlertException("Please configure the correct Principal Name 
Attribute");
+    }
+    User user = authenticator.authenticate(principal.getName(), null, 
LoginType.SSO.toString());
+    return this.login(user.getUsername(), user);
+  }
+
+  private RestResponse login(String username, User user) throws Exception {
+    if (user == null) {
+      return RestResponse.success().put("code", 0);
+    }
+
+    if (User.STATUS_LOCK.equals(user.getStatus())) {
+      return RestResponse.success().put("code", 1);
+    }
+
+    userService.fillInTeam(user);
+
+    // no team.
+    if (user.getLastTeamId() == null) {
+      return RestResponse.success().data(user.getUserId()).put("code", 
ResponseCode.CODE_FORBIDDEN);
+    }
+
+    this.userService.updateLoginTime(username);
+    String token = WebUtils.encryptToken(JWTUtil.sign(user.getUserId(), 
username));
+    LocalDateTime expireTime = 
LocalDateTime.now().plusSeconds(properties.getJwtTimeOut());
+    String expireTimeStr = DateUtils.formatFullTime(expireTime);
+    JWTToken jwtToken = new JWTToken(token, expireTimeStr);
+    String userId = RandomStringUtils.randomAlphanumeric(20);
+    user.setId(userId);
+    Map<String, Object> userInfo =
+        userService.generateFrontendUserInfo(user, user.getLastTeamId(), 
jwtToken);
+    return new RestResponse().data(userInfo);
+  }
+}
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/security/impl/AuthenticatorImpl.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/security/impl/AuthenticatorImpl.java
index 916f4ae27..73956a6e6 100644
--- 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/security/impl/AuthenticatorImpl.java
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/security/impl/AuthenticatorImpl.java
@@ -44,11 +44,16 @@ public class AuthenticatorImpl implements Authenticator {
       throw new ApiAlertException(
           String.format("the login type [%s] is not supported.", loginType));
     }
-
-    if (loginTypeEnum.equals(LoginType.PASSWORD)) {
-      return passwordAuthenticate(username, password);
-    } else {
-      return ldapAuthenticate(username, password);
+    switch (loginTypeEnum) {
+      case PASSWORD:
+        return passwordAuthenticate(username, password);
+      case LDAP:
+        return ldapAuthenticate(username, password);
+      case SSO:
+        return ssoAuthenticate(username);
+      default:
+        throw new ApiAlertException(
+            String.format("the login type [%s] is not supported.", loginType));
     }
   }
 
@@ -93,16 +98,35 @@ public class AuthenticatorImpl implements Authenticator {
       }
       return user;
     }
+    return this.newUserCreate(LoginType.LDAP, username, password);
+  }
+
+  private User ssoAuthenticate(String username) throws Exception {
+    // check if user exist
+    User user = usersService.findByName(username);
+    if (user != null) {
+      if (user.getLoginType() != LoginType.SSO) {
+        throw new ApiAlertException(
+            String.format("user [%s] can only sign in with %s", username, 
user.getLoginType()));
+      }
+      return user;
+    }
+    return this.newUserCreate(LoginType.SSO, username, null);
+  }
 
+  private User newUserCreate(LoginType loginType, String username, String 
password)
+      throws Exception {
     User newUser = new User();
     newUser.setCreateTime(new Date());
     newUser.setUsername(username);
     newUser.setNickName(username);
+    newUser.setLoginType(loginType);
     newUser.setUserType(UserType.USER);
-    newUser.setLoginType(LoginType.LDAP);
     newUser.setStatus(User.STATUS_VALID);
     newUser.setSex(User.SEX_UNKNOWN);
-    newUser.setPassword(password);
+    if (password != null) {
+      newUser.setPassword(password);
+    }
     usersService.createUser(newUser);
     return newUser;
   }
diff --git 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/service/impl/UserServiceImpl.java
 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/service/impl/UserServiceImpl.java
index 504d6026e..afe3d169d 100644
--- 
a/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/service/impl/UserServiceImpl.java
+++ 
b/streampark-console/streampark-console-service/src/main/java/org/apache/streampark/console/system/service/impl/UserServiceImpl.java
@@ -103,10 +103,12 @@ public class UserServiceImpl extends 
ServiceImpl<UserMapper, User> implements Us
   @Transactional(rollbackFor = Exception.class)
   public void createUser(User user) {
     user.setCreateTime(new Date());
-    String salt = ShaHashUtils.getRandomSalt();
-    String password = ShaHashUtils.encrypt(salt, user.getPassword());
-    user.setSalt(salt);
-    user.setPassword(password);
+    if (StringUtils.isNoneBlank(user.getPassword())) {
+      String salt = ShaHashUtils.getRandomSalt();
+      String password = ShaHashUtils.encrypt(salt, user.getPassword());
+      user.setSalt(salt);
+      user.setPassword(password);
+    }
     save(user);
   }
 
diff --git 
a/streampark-console/streampark-console-service/src/main/resources/application-sso.yml
 
b/streampark-console/streampark-console-service/src/main/resources/application-sso.yml
new file mode 100644
index 000000000..d74390ffd
--- /dev/null
+++ 
b/streampark-console/streampark-console-service/src/main/resources/application-sso.yml
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+pac4j:
+  callbackUrl: http://localhost:10000/callback
+  # Put all parameters under `properties`
+  # Check supported sso config parameters for different authentication clients 
from the below link
+  # 
https://github.com/pac4j/pac4j/blob/master/documentation/docs/config-module.md
+  properties:
+    # principalNameAttribute:
+    # Optional, change by authentication client
+    # Please replace and fill in your client config below when enabled SSO
diff --git 
a/streampark-console/streampark-console-service/src/main/resources/application.yml
 
b/streampark-console/streampark-console-service/src/main/resources/application.yml
index 5b832f547..e2c7dd198 100644
--- 
a/streampark-console/streampark-console-service/src/main/resources/application.yml
+++ 
b/streampark-console/streampark-console-service/src/main/resources/application.yml
@@ -44,7 +44,10 @@ springdoc:
   packages-to-scan: org.apache.streampark.console
 
 spring:
-  profiles.active: h2 #[h2,pgsql,mysql]
+  profiles:
+    active: h2 #[h2,pgsql,mysql]
+    # Please uncomment the below config if enable sso
+    # include: sso
   application.name: StreamPark
   devtools.restart.enabled: false
   mvc.pathmatch.matching-strategy: ant_path_matcher
@@ -144,3 +147,7 @@ ldap:
   user:
     identity-attribute: uid
     email-attribute: mail
+
+sso:
+  # If turn to true, please provide the sso properties the application-sso.yml
+  enable: false
diff --git 
a/streampark-console/streampark-console-service/src/main/resources/db/schema-h2.sql
 
b/streampark-console/streampark-console-service/src/main/resources/db/schema-h2.sql
index f2f22da5e..fd625c21e 100644
--- 
a/streampark-console/streampark-console-service/src/main/resources/db/schema-h2.sql
+++ 
b/streampark-console/streampark-console-service/src/main/resources/db/schema-h2.sql
@@ -338,10 +338,10 @@ create table if not exists `t_user` (
   `username` varchar(64)  not null comment 'user name',
   `nick_name` varchar(64)  not null comment 'nick name',
   `salt` varchar(26)  default null comment 'salt',
-  `password` varchar(64)  not null comment 'password',
+  `password` varchar(64)  default null comment 'password',
   `email` varchar(64)  default null comment 'email',
   `user_type` int  not null comment 'user type 1:admin 2:user',
-  `login_type` tinyint default 0 comment 'login type 0:password 1:ldap',
+  `login_type` tinyint default 0 comment 'login type 0:password 1:ldap 2:sso',
   `last_team_id` bigint default null comment 'last team id',
   `status` char(1)  not null comment 'status 0:locked 1:active',
   `create_time` datetime not null default current_timestamp comment 'create 
time',
diff --git 
a/streampark-console/streampark-console-webapp/src/api/system/user.ts 
b/streampark-console/streampark-console-webapp/src/api/system/user.ts
index 7182afed9..cf2d5df2e 100644
--- a/streampark-console/streampark-console-webapp/src/api/system/user.ts
+++ b/streampark-console/streampark-console-webapp/src/api/system/user.ts
@@ -44,6 +44,7 @@ enum Api {
   INIT_TEAM = '/user/initTeam',
   APP_OWNERS = '/user/appOwners',
   TransferUserResource = '/user/transferResource',
+  SSO_TOKEN = '/sso/token',
 }
 
 /**
@@ -100,8 +101,7 @@ export function addUser(data: Recordable) {
 }
 
 export function resetPassword(data): Promise<AxiosResponse<Result<string>>> {
-  return defHttp.put({ url: Api.ResetPassword, data },
-    { isReturnNativeResponse: true },);
+  return defHttp.put({ url: Api.ResetPassword, data }, { 
isReturnNativeResponse: true });
 }
 
 export function checkUserName(data) {
@@ -147,6 +147,15 @@ export function fetchSetUserTeam(data: { teamId: string 
}): Promise<TeamSetRespo
   });
 }
 
-export function transferUserResource(data: { userId: string, targetUserId: 
string }): Promise<TeamSetResponse> {
+export function transferUserResource(data: {
+  userId: string;
+  targetUserId: string;
+}): Promise<TeamSetResponse> {
   return defHttp.put({ url: Api.TransferUserResource, data });
 }
+
+export function fetchSsoToken(): Promise<LoginResultModel> {
+  return defHttp.get({
+    url: Api.SSO_TOKEN,
+  });
+}
diff --git a/streampark-console/streampark-console-webapp/src/enums/pageEnum.ts 
b/streampark-console/streampark-console-webapp/src/enums/pageEnum.ts
index 2c98a274a..6245c7aca 100644
--- a/streampark-console/streampark-console-webapp/src/enums/pageEnum.ts
+++ b/streampark-console/streampark-console-webapp/src/enums/pageEnum.ts
@@ -24,4 +24,6 @@ export enum PageEnum {
   ERROR_PAGE = '/exception',
   // error log page path
   ERROR_LOG_PAGE = '/error-log/list',
+  // other login path
+  SSO_LOGIN = '/sso/signin',
 }
diff --git 
a/streampark-console/streampark-console-webapp/src/locales/lang/en/sys.ts 
b/streampark-console/streampark-console-webapp/src/locales/lang/en/sys.ts
index f41e40c3e..0db8bca22 100644
--- a/streampark-console/streampark-console-webapp/src/locales/lang/en/sys.ts
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/en/sys.ts
@@ -104,6 +104,7 @@ export default {
     rememberMe: 'Remember me',
     forgetPassword: 'Forget Password?',
     otherSignIn: 'Sign in with',
+    ssoSignIn: 'Sign in with SSO',
 
     ldapTip: 'Sign in with LDAP',
     passwordTip: 'Sign in with password',
diff --git 
a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/sys.ts 
b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/sys.ts
index 00f3fbefa..0b03f71f2 100644
--- a/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/sys.ts
+++ b/streampark-console/streampark-console-webapp/src/locales/lang/zh-CN/sys.ts
@@ -97,6 +97,7 @@ export default {
     rememberMe: '记住我',
     forgetPassword: '忘记密码?',
     otherSignIn: '其他登录方式',
+    ssoSignIn: '通过SSO登录',
 
     ldapTip: '通过LDAP登录',
     passwordTip: '通过密码登录',
diff --git 
a/streampark-console/streampark-console-webapp/src/router/guard/index.ts 
b/streampark-console/streampark-console-webapp/src/router/guard/index.ts
index 7c9b323a4..78458b39f 100644
--- a/streampark-console/streampark-console-webapp/src/router/guard/index.ts
+++ b/streampark-console/streampark-console-webapp/src/router/guard/index.ts
@@ -28,6 +28,7 @@ import { createStateGuard } from './stateGuard';
 import nProgress from 'nprogress';
 import projectSetting from '/@/settings/projectSetting';
 import { createParamMenuGuard } from './paramMenuGuard';
+import { createSsoGuard } from './ssoGuard';
 
 // Don't change the order of creation
 export function setupRouterGuard(router: Router) {
@@ -37,6 +38,7 @@ export function setupRouterGuard(router: Router) {
   createScrollGuard(router);
   createMessageGuard(router);
   createProgressGuard(router);
+  createSsoGuard(router);
   createPermissionGuard(router);
   createParamMenuGuard(router); // must after createPermissionGuard (menu has 
been built.)
   createStateGuard(router);
diff --git 
a/streampark-console/streampark-console-webapp/src/router/guard/permissionGuard.ts
 
b/streampark-console/streampark-console-webapp/src/router/guard/permissionGuard.ts
index 315e38b57..c72dd804c 100644
--- 
a/streampark-console/streampark-console-webapp/src/router/guard/permissionGuard.ts
+++ 
b/streampark-console/streampark-console-webapp/src/router/guard/permissionGuard.ts
@@ -27,9 +27,11 @@ import { RootRoute } from '/@/router/routes';
 
 const LOGIN_PATH = PageEnum.BASE_LOGIN;
 
+const SSO_LOGIN_PATH = PageEnum.SSO_LOGIN;
+
 const ROOT_PATH = RootRoute.path;
 
-const whitePathList: PageEnum[] = [LOGIN_PATH];
+const whitePathList: PageEnum[] = [LOGIN_PATH, SSO_LOGIN_PATH];
 
 export function createPermissionGuard(router: Router) {
   const userStore = useUserStoreWithOut();
@@ -44,7 +46,6 @@ export function createPermissionGuard(router: Router) {
       next(userStore.getUserInfo.homePath);
       return;
     }
-
     const token = userStore.getToken;
     // Whitelist can be directly entered
     if (whitePathList.includes(to.path as PageEnum)) {
diff --git 
a/streampark-console/streampark-console-webapp/src/router/guard/ssoGuard.ts 
b/streampark-console/streampark-console-webapp/src/router/guard/ssoGuard.ts
new file mode 100644
index 000000000..92927427b
--- /dev/null
+++ b/streampark-console/streampark-console-webapp/src/router/guard/ssoGuard.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { Router } from 'vue-router';
+import { fetchSsoToken } from '/@/api/system/user';
+import { APP_TEAMID_KEY_ } from '/@/enums/cacheEnum';
+import { PageEnum } from '/@/enums/pageEnum';
+import { useUserStoreWithOut } from '/@/store/modules/user';
+import { useMessage } from '/@/hooks/web/useMessage';
+import { useI18n } from '/@/hooks/web/useI18n';
+import { computed } from 'vue';
+
+const HOME_PATH = PageEnum.BASE_HOME;
+
+const { createMessage } = useMessage();
+
+export function createSsoGuard(router: Router) {
+  const userStore = useUserStoreWithOut();
+  const { t } = useI18n();
+  router.beforeEach(async (to, _, next) => {
+    const token = userStore.getToken;
+    const isFromBESso = computed(() => to.path === HOME_PATH && 
to.query['from'] === 'sso');
+    if (!token && isFromBESso.value) {
+      try {
+        const data = await fetchSsoToken();
+        if (data?.user) {
+          const { lastTeamId, nickName } = data.user;
+          userStore.teamId = lastTeamId || '';
+          sessionStorage.setItem(APP_TEAMID_KEY_, userStore.teamId);
+          localStorage.setItem(APP_TEAMID_KEY_, userStore.teamId);
+          userStore.setData(data);
+          const ssoLoginSuccess = await userStore.afterLoginAction();
+          if (ssoLoginSuccess) {
+            let successText = t('sys.login.loginSuccessDesc');
+            if (nickName) successText += `: ${nickName}`;
+            createMessage.success(`${t('sys.login.loginSuccessTitle')} 
${successText}`);
+            next(userStore.getUserInfo.homePath || HOME_PATH);
+            return;
+          }
+        }
+      } catch {}
+    }
+    next();
+  });
+}
diff --git 
a/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
 
b/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
index 571b862ee..2da26b2f4 100644
--- 
a/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
+++ 
b/streampark-console/streampark-console-webapp/src/views/base/login/LoginForm.vue
@@ -37,7 +37,7 @@
         class="fix-auto-fill"
       >
         <template #prefix>
-          <user-outlined type="user" />
+          <user-outlined />
         </template>
       </Input>
     </FormItem>
@@ -49,7 +49,7 @@
         :placeholder="t('sys.login.password')"
       >
         <template #prefix>
-          <lock-outlined type="user" />
+          <lock-outlined />
         </template>
       </InputPassword>
     </FormItem>
@@ -66,7 +66,12 @@
       </Button>
     </FormItem>
     <FormItem class="enter-x text-left">
-      <Button type="link" @click="changeLoginType"> {{ loginText.linkText }} 
</Button>
+      <Button :href="SSO_LOGIN_PATH" type="link">
+        {{ t('sys.login.ssoSignIn') }}
+      </Button>
+      <Button type="link" class="float-right" @click="changeLoginType">
+        {{ loginText.linkText }}
+      </Button>
     </FormItem>
   </Form>
   <TeamModal v-model:visible="modelVisible" :userId="userId" 
@success="handleTeamSuccess" />
@@ -79,7 +84,6 @@
 
   import { useI18n } from '/@/hooks/web/useI18n';
   import { useMessage } from '/@/hooks/web/useMessage';
-
   import { useUserStore } from '/@/store/modules/user';
   import {
     LoginStateEnum,
@@ -95,9 +99,12 @@
   import { fetchUserTeam } from '/@/api/system/member';
   import { LoginResultModel } from '/@/api/system/model/userModel';
   import { Result } from '/#/axios';
+  import { PageEnum } from '/@/enums/pageEnum';
   const FormItem = Form.Item;
   const InputPassword = Input.Password;
 
+  const SSO_LOGIN_PATH = PageEnum.SSO_LOGIN;
+
   const { t } = useI18n();
   const { createErrorModal, createMessage } = useMessage();
   const { prefixCls } = useDesign('login');


Reply via email to