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');