This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 659da659ef fix(admin): migrate dashboard passwords to bcrypt (#6380)
659da659ef is described below
commit 659da659efd93a8aeead627f3efc6d9668d3a396
Author: Honglei <[email protected]>
AuthorDate: Thu Jul 23 10:09:07 2026 +0800
fix(admin): migrate dashboard passwords to bcrypt (#6380)
* fix(admin): migrate dashboard passwords to bcrypt
* fix(admin): address password hashing review feedback
* fix(admin): align bcrypt encoder strength
---------
Co-authored-by: aias00 <[email protected]>
---
db/init/mysql/schema.sql | 2 +-
db/init/ob/schema.sql | 2 +-
db/init/og/create-table.sql | 2 +-
db/init/oracle/schema.sql | 4 +-
db/init/pg/create-table.sql | 2 +-
shenyu-admin/pom.xml | 5 ++
.../admin/controller/DashboardUserController.java | 14 +--
.../shenyu/admin/mapper/DashboardUserMapper.java | 4 +-
.../shenyu/admin/service/DashboardUserService.java | 4 +-
.../shenyu/admin/service/PasswordHashService.java | 100 +++++++++++++++++++++
.../service/impl/DashboardUserServiceImpl.java | 69 ++++++++++++--
.../src/main/resources/sql-script/h2/schema.sql | 2 +-
.../controller/DashboardUserControllerTest.java | 28 ++++++
.../admin/service/DashboardUserServiceTest.java | 82 ++++++++++++++---
14 files changed, 286 insertions(+), 34 deletions(-)
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index 8891da9e81..d88db8536d 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -165,7 +165,7 @@ CREATE TABLE `dashboard_user` (
-- ----------------------------
-- Records of dashboard_user
-- ----------------------------
-INSERT INTO `dashboard_user` VALUES ('1', 'admin',
'ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
1, 1, null, '2022-05-25 18:02:52', '2022-05-25 18:02:52');
+INSERT INTO `dashboard_user` VALUES ('1', 'admin',
'$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG', 1, 1, null,
'2022-05-25 18:02:52', '2022-05-25 18:02:52');
-- ----------------------------
-- Table structure for data_permission
diff --git a/db/init/ob/schema.sql b/db/init/ob/schema.sql
index d2fdbc7cb1..2f259975fe 100644
--- a/db/init/ob/schema.sql
+++ b/db/init/ob/schema.sql
@@ -165,7 +165,7 @@ CREATE TABLE `dashboard_user` (
-- ----------------------------
-- Records of dashboard_user
-- ----------------------------
-INSERT INTO `dashboard_user` VALUES ('1', 'admin',
'ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
1, 1, null, '2022-05-25 18:02:52', '2022-05-25 18:02:52');
+INSERT INTO `dashboard_user` VALUES ('1', 'admin',
'$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG', 1, 1, null,
'2022-05-25 18:02:52', '2022-05-25 18:02:52');
-- ----------------------------
-- Table structure for data_permission
diff --git a/db/init/og/create-table.sql b/db/init/og/create-table.sql
index bbaee3d598..97a90b57fe 100644
--- a/db/init/og/create-table.sql
+++ b/db/init/og/create-table.sql
@@ -190,7 +190,7 @@ COMMENT ON COLUMN "public"."dashboard_user"."date_updated"
IS 'update time';
-- ----------------------------
-- Records of dashboard_user
-- ----------------------------
-INSERT INTO "public"."dashboard_user" VALUES ('1', 'admin',
'ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
1, 1, null, '2022-05-25 18:08:01', '2022-05-25 18:08:01');
+INSERT INTO "public"."dashboard_user" VALUES ('1', 'admin',
'$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG', 1, 1, null,
'2022-05-25 18:08:01', '2022-05-25 18:08:01');
-- ----------------------------
-- Table structure for data_permission
diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql
index 6812ba9758..aa44e96a33 100644
--- a/db/init/oracle/schema.sql
+++ b/db/init/oracle/schema.sql
@@ -891,7 +891,7 @@ comment on column FIELD.date_updated
is 'update time';
/**default admin user**/
-INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(dashboard_user(id)) */ INTO
dashboard_user (id, user_name, password, role, enabled) VALUES
('1','admin','ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
'1', '1');
+INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(dashboard_user(id)) */ INTO
dashboard_user (id, user_name, password, role, enabled) VALUES
('1','admin','$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG',
'1', '1');
/** insert admin role */
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(user_role(id)) */ INTO user_role (id,
user_id, role_id) VALUES ('1351007709096976384', '1', '1346358560427216896');
@@ -3813,4 +3813,4 @@ INSERT INTO permission (id, object_id, resource_id,
date_created, date_updated)
INSERT INTO permission (id, object_id, resource_id, date_created,
date_updated) VALUES ('1953049887387303902', '1346358560427216896',
'1953048313980116901', sysdate, sysdate);
INSERT INTO permission (id, object_id, resource_id, date_created,
date_updated) VALUES ('1953049887387303903', '1346358560427216896',
'1953048313980116902', sysdate, sysdate);
INSERT INTO permission (id, object_id, resource_id, date_created,
date_updated) VALUES ('1953049887387303904', '1346358560427216896',
'1953048313980116903', sysdate, sysdate);
-INSERT INTO permission (id, object_id, resource_id, date_created,
date_updated) VALUES ('1953049887387303905', '1346358560427216896',
'1953048313980116904', sysdate, sysdate);
\ No newline at end of file
+INSERT INTO permission (id, object_id, resource_id, date_created,
date_updated) VALUES ('1953049887387303905', '1346358560427216896',
'1953048313980116904', sysdate, sysdate);
diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql
index 23cc1577a5..f8e452297a 100644
--- a/db/init/pg/create-table.sql
+++ b/db/init/pg/create-table.sql
@@ -187,7 +187,7 @@ COMMENT ON COLUMN "public"."dashboard_user"."date_updated"
IS 'update time';
-- ----------------------------
-- Records of dashboard_user
-- ----------------------------
-INSERT INTO "public"."dashboard_user" VALUES ('1', 'admin',
'ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
1, 1, null, '2022-05-25 18:08:01', '2022-05-25 18:08:01');
+INSERT INTO "public"."dashboard_user" VALUES ('1', 'admin',
'$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG', 1, 1, null,
'2022-05-25 18:08:01', '2022-05-25 18:08:01');
-- ----------------------------
-- Table structure for data_permission
diff --git a/shenyu-admin/pom.xml b/shenyu-admin/pom.xml
index 9d4cb7d508..cd0806b1ec 100644
--- a/shenyu-admin/pom.xml
+++ b/shenyu-admin/pom.xml
@@ -74,6 +74,11 @@
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-crypto</artifactId>
+ </dependency>
+
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DashboardUserController.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DashboardUserController.java
index e5c5b429de..d812a066d8 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DashboardUserController.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DashboardUserController.java
@@ -33,12 +33,12 @@ import
org.apache.shenyu.admin.model.result.ShenyuAdminResult;
import org.apache.shenyu.admin.model.vo.DashboardUserEditVO;
import org.apache.shenyu.admin.model.vo.DashboardUserVO;
import org.apache.shenyu.admin.service.DashboardUserService;
+import org.apache.shenyu.admin.service.PasswordHashService;
import org.apache.shenyu.admin.utils.Assert;
import org.apache.shenyu.admin.utils.ResultUtil;
import org.apache.shenyu.admin.utils.SessionUtil;
import org.apache.shenyu.admin.utils.ShenyuResultMessage;
import org.apache.shenyu.admin.validation.annotation.Existed;
-import org.apache.shenyu.common.utils.DigestUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -65,9 +65,13 @@ import java.util.Optional;
public class DashboardUserController {
private final DashboardUserService dashboardUserService;
+
+ private final PasswordHashService passwordHashService;
- public DashboardUserController(final DashboardUserService
dashboardUserService) {
+ public DashboardUserController(final DashboardUserService
dashboardUserService,
+ final PasswordHashService
passwordHashService) {
this.dashboardUserService = dashboardUserService;
+ this.passwordHashService = passwordHashService;
}
/**
@@ -119,7 +123,7 @@ public class DashboardUserController {
public ShenyuAdminResult createDashboardUser(@Valid @RequestBody final
DashboardUserDTO dashboardUserDTO) {
return Optional.ofNullable(dashboardUserDTO)
.map(item -> {
-
item.setPassword(DigestUtils.sha512Hex(item.getPassword()));
+
item.setPassword(passwordHashService.encode(item.getPassword()));
Integer createCount =
dashboardUserService.createOrUpdate(item);
return
ShenyuAdminResult.success(ShenyuResultMessage.CREATE_SUCCESS, createCount);
})
@@ -141,7 +145,7 @@ public class DashboardUserController {
@Valid @RequestBody final
DashboardUserDTO dashboardUserDTO) {
dashboardUserDTO.setId(id);
if (StringUtils.isNotBlank(dashboardUserDTO.getPassword())) {
-
dashboardUserDTO.setPassword(DigestUtils.sha512Hex(dashboardUserDTO.getPassword()));
+
dashboardUserDTO.setPassword(passwordHashService.encode(dashboardUserDTO.getPassword()));
}
Integer updateCount =
dashboardUserService.createOrUpdate(dashboardUserDTO);
return ShenyuAdminResult.success(ShenyuResultMessage.UPDATE_SUCCESS,
updateCount);
@@ -167,8 +171,6 @@ public class DashboardUserController {
if (!userInfo.getUserId().equals(id) &&
!userInfo.getUserName().equals(dashboardUserModifyPasswordDTO.getUserName())) {
return
ShenyuAdminResult.error(ShenyuResultMessage.DASHBOARD_MODIFY_PASSWORD_ERROR);
}
-
dashboardUserModifyPasswordDTO.setPassword(DigestUtils.sha512Hex(dashboardUserModifyPasswordDTO.getPassword()));
-
dashboardUserModifyPasswordDTO.setOldPassword(DigestUtils.sha512Hex(dashboardUserModifyPasswordDTO.getOldPassword()));
return ShenyuAdminResult.success(ShenyuResultMessage.UPDATE_SUCCESS,
dashboardUserService.modifyPassword(dashboardUserModifyPasswordDTO));
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DashboardUserMapper.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DashboardUserMapper.java
index 7b94a94b50..4da544f8d9 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DashboardUserMapper.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DashboardUserMapper.java
@@ -62,9 +62,11 @@ public interface DashboardUserMapper extends ExistProvider {
* find dashboard user by query.
*
* @param userName user name
- * @param password user password
+ * @param password exact stored password value
* @return {@linkplain DashboardUserDO}
+ * @deprecated do not use this method for authentication with raw
passwords.
*/
+ @Deprecated
DashboardUserDO findByQuery(@Param("userName") String userName,
@Param("password") String password);
/**
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DashboardUserService.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DashboardUserService.java
index 3144874442..d967ef3c38 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DashboardUserService.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DashboardUserService.java
@@ -84,9 +84,11 @@ public interface DashboardUserService {
* find dashboard user by query.
*
* @param userName user name
- * @param password user password
+ * @param password exact stored password value
* @return {@linkplain DashboardUserVO}
+ * @deprecated use {@link #findByUserName(String)} and {@link
PasswordHashService} for authentication.
*/
+ @Deprecated
DashboardUserVO findByQuery(String userName, String password);
/**
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PasswordHashService.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PasswordHashService.java
new file mode 100644
index 0000000000..29b10509f0
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/PasswordHashService.java
@@ -0,0 +1,100 @@
+/*
+ * 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.shenyu.admin.service;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.common.utils.DigestUtils;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.regex.Pattern;
+
+/**
+ * Central password storage policy for dashboard users.
+ */
+@Service
+public class PasswordHashService {
+
+ private static final Pattern BCRYPT_PATTERN =
Pattern.compile("^\\$2[aby]\\$\\d{2}\\$[./A-Za-z0-9]{53}$");
+
+ private static final Pattern LEGACY_SHA512_PATTERN =
Pattern.compile("^[A-Fa-f0-9]{128}$");
+
+ private static final int BCRYPT_STRENGTH = 12;
+
+ private final BCryptPasswordEncoder passwordEncoder = new
BCryptPasswordEncoder(BCRYPT_STRENGTH);
+
+ /**
+ * Encode a raw dashboard user password for storage.
+ * Returns the input unchanged if it is blank.
+ *
+ * @param requestPassword raw password
+ * @return bcrypt encoded password
+ */
+ public String encode(final String requestPassword) {
+ if (StringUtils.isBlank(requestPassword)) {
+ return requestPassword;
+ }
+ return passwordEncoder.encode(requestPassword);
+ }
+
+ /**
+ * Verify a raw password against a bcrypt hash.
+ *
+ * @param requestPassword raw password
+ * @param storedPasswordHash stored password hash
+ * @return true when the password matches
+ */
+ public boolean matches(final String requestPassword, final String
storedPasswordHash) {
+ if (!isBcryptHash(storedPasswordHash)) {
+ return false;
+ }
+ return passwordEncoder.matches(requestPassword, storedPasswordHash);
+ }
+
+ /**
+ * Determine whether a stored password uses bcrypt format.
+ *
+ * @param storedPasswordHash stored password hash
+ * @return true when the stored password is bcrypt
+ */
+ public boolean isBcryptHash(final String storedPasswordHash) {
+ return StringUtils.isNotBlank(storedPasswordHash) &&
BCRYPT_PATTERN.matcher(storedPasswordHash).matches();
+ }
+
+ /**
+ * Verify a raw password against the legacy SHA-512 hex format.
+ *
+ * @param requestPassword raw password
+ * @param storedPasswordHash stored password hash
+ * @return true when the password matches the legacy hash
+ */
+ public boolean matchesLegacySha512(final String requestPassword, final
String storedPasswordHash) {
+ return isLegacySha512Hash(storedPasswordHash)
+ && StringUtils.equals(DigestUtils.sha512Hex(requestPassword),
storedPasswordHash);
+ }
+
+ /**
+ * Determine whether a stored password uses the legacy SHA-512 hex format.
+ *
+ * @param storedPasswordHash stored password hash
+ * @return true when the stored password is a legacy SHA-512 hex hash
+ */
+ public boolean isLegacySha512Hash(final String storedPasswordHash) {
+ return StringUtils.isNotBlank(storedPasswordHash) &&
LEGACY_SHA512_PATTERN.matcher(storedPasswordHash).matches();
+ }
+}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
index 87ea9546c2..cf3e643a1b 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DashboardUserServiceImpl.java
@@ -43,6 +43,7 @@ import org.apache.shenyu.admin.model.vo.LoginDashboardUserVO;
import org.apache.shenyu.admin.model.vo.RoleVO;
import org.apache.shenyu.admin.service.DashboardUserService;
import org.apache.shenyu.admin.service.NamespaceUserService;
+import org.apache.shenyu.admin.service.PasswordHashService;
import org.apache.shenyu.admin.service.publish.UserEventPublisher;
import org.apache.shenyu.admin.transfer.DashboardUserTransfer;
import org.apache.shenyu.admin.utils.Assert;
@@ -52,7 +53,6 @@ import org.apache.shenyu.admin.utils.SessionUtil;
import org.apache.shenyu.admin.utils.WebI18nAssert;
import org.apache.shenyu.common.constant.AdminConstants;
import org.apache.shenyu.common.constant.Constants;
-import org.apache.shenyu.common.utils.DigestUtils;
import org.apache.shenyu.common.utils.ListUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
@@ -69,6 +69,7 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
+import java.sql.Timestamp;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
@@ -86,6 +87,10 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
private static final int AES_BLOCK_SIZE = 16;
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
private final DashboardUserMapper dashboardUserMapper;
private final UserRoleMapper userRoleMapper;
@@ -108,6 +113,8 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
private final NamespaceUserService namespaceUserService;
+ private final PasswordHashService passwordHashService;
+
public DashboardUserServiceImpl(final DashboardUserMapper
dashboardUserMapper,
final UserRoleMapper userRoleMapper,
final RoleMapper roleMapper,
@@ -117,7 +124,8 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
final UserEventPublisher publisher,
final DashboardProperties properties,
final SecretProperties secretProperties,
- final NamespaceUserService
namespaceUserService) {
+ final NamespaceUserService
namespaceUserService,
+ final PasswordHashService
passwordHashService) {
this.dashboardUserMapper = dashboardUserMapper;
this.userRoleMapper = userRoleMapper;
this.roleMapper = roleMapper;
@@ -128,6 +136,7 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
this.properties = properties;
this.secretProperties = secretProperties;
this.namespaceUserService = namespaceUserService;
+ this.passwordHashService = passwordHashService;
}
/**
@@ -147,6 +156,7 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
Assert.notBlack(dashboardUserDTO.getPassword(), "password is not
null");
Assert.notEmpty(dashboardUserDTO.getRoles(), "role is not empty");
Assert.isNull(dashboardUserMapper.selectByUserName(dashboardUserDTO.getUserName()),
"the user is existed");
+
dashboardUserDTO.setPassword(encodePasswordIfNecessary(dashboardUserDTO.getPassword()));
DashboardUserDO dashboardUserDO =
DashboardUserDO.buildDashboardUserDO(dashboardUserDTO);
// create new user
final int insertCount =
dashboardUserMapper.insertSelective(dashboardUserDO);
@@ -161,8 +171,11 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
@Override
public int update(final DashboardUserDTO dashboardUserDTO) {
- // 【mandatory】This function can only be used by the admin user
+ // mandatory: This function can only be used by the admin user
Assert.isTrue(SessionUtil.isAdmin(), "This function can only be used
by the admin(root) user");
+ if (StringUtils.isNotBlank(dashboardUserDTO.getPassword())) {
+
dashboardUserDTO.setPassword(encodePasswordIfNecessary(dashboardUserDTO.getPassword()));
+ }
DashboardUserDO dashboardUserDO =
DashboardUserDO.buildDashboardUserDO(dashboardUserDTO);
if (Objects.equals(dashboardUserDO.getUserName(),
SessionUtil.visitorName())) {
Assert.isTrue(Boolean.TRUE.equals(dashboardUserDO.getEnabled()),
"You cannot disable yourself");
@@ -247,8 +260,10 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
* @param userName user name
* @param password user password
* @return {@linkplain DashboardUserVO}
+ * @deprecated use {@link #findByUserName(String)} and {@link
PasswordHashService} for authentication.
*/
@Override
+ @Deprecated
public DashboardUserVO findByQuery(final String userName, final String
password) {
return
DashboardUserVO.buildDashboardUserVO(dashboardUserMapper.findByQuery(userName,
password));
}
@@ -338,7 +353,6 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
}
private Optional<String> tryDecryptPassword(final String password) {
- Security.addProvider(new BouncyCastleProvider());
byte[] secretKeyBytes =
secretProperties.getKey().getBytes(StandardCharsets.UTF_8);
byte[] ivBytes =
secretProperties.getIv().getBytes(StandardCharsets.UTF_8);
try {
@@ -365,7 +379,9 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
DashboardUserDO before =
dashboardUserMapper.selectById(dashboardUserModifyPasswordDTO.getId());
Assert.notNull(before, "current user is not found");
Assert.isTrue(Boolean.TRUE.equals(before.getEnabled()), "current user
is locked");
- Assert.isTrue(Objects.equals(before.getPassword(),
dashboardUserModifyPasswordDTO.getOldPassword()), "old password is error");
+
Assert.isTrue(matchesPassword(dashboardUserModifyPasswordDTO.getOldPassword(),
before.getPassword()), "old password is error");
+
+
dashboardUserModifyPasswordDTO.setPassword(passwordHashService.encode(dashboardUserModifyPasswordDTO.getPassword()));
DashboardUserDO dashboardUserDO =
DashboardUserDO.buildDashboardUserDO(dashboardUserModifyPasswordDTO);
int updateCount = dashboardUserMapper.updateSelective(dashboardUserDO);
@@ -401,13 +417,15 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
RoleDO role = roleMapper.findByRoleName("default");
DashboardUserDTO dashboardUserDTO =
DashboardUserDTO.builder()
.userName(userName)
- .password(DigestUtils.sha512Hex(password))
+ .password(passwordHashService.encode(password))
.role(1)
.roles(Lists.newArrayList(role.getId()))
.enabled(true)
.build();
createOrUpdate(dashboardUserDTO);
dashboardUserVO =
DashboardUserTransfer.INSTANCE.transferDTO2VO(dashboardUserDTO);
+ } else {
+ dashboardUserVO =
upgradeLegacyPasswordIfNeeded(dashboardUserVO, password);
}
}
return dashboardUserVO;
@@ -420,7 +438,44 @@ public class DashboardUserServiceImpl implements
DashboardUserService {
}
private DashboardUserVO loginByDatabase(final String userName, final
String password) {
- return findByQuery(userName, DigestUtils.sha512Hex(password));
+ DashboardUserVO dashboardUserVO = findByUserName(userName);
+ if (Objects.isNull(dashboardUserVO)) {
+ return null;
+ }
+ if (passwordHashService.matches(password,
dashboardUserVO.getPassword())) {
+ return dashboardUserVO;
+ }
+ if (passwordHashService.matchesLegacySha512(password,
dashboardUserVO.getPassword())) {
+ return upgradeLegacyPasswordIfNeeded(dashboardUserVO, password);
+ }
+ return null;
+ }
+
+ private String encodePasswordIfNecessary(final String password) {
+ if (passwordHashService.isBcryptHash(password)) {
+ return password;
+ }
+ return passwordHashService.encode(password);
+ }
+
+ private DashboardUserVO upgradeLegacyPasswordIfNeeded(final
DashboardUserVO dashboardUserVO, final String password) {
+ if (Objects.isNull(dashboardUserVO) ||
!passwordHashService.matchesLegacySha512(password,
dashboardUserVO.getPassword())) {
+ return dashboardUserVO;
+ }
+ String encodedPassword = passwordHashService.encode(password);
+ DashboardUserDO dashboardUserDO = DashboardUserDO.builder()
+ .id(dashboardUserVO.getId())
+ .password(encodedPassword)
+ .dateUpdated(new Timestamp(System.currentTimeMillis()))
+ .build();
+ dashboardUserMapper.updateSelective(dashboardUserDO);
+ dashboardUserVO.setPassword(encodedPassword);
+ return dashboardUserVO;
+ }
+
+ private boolean matchesPassword(final String rawPassword, final String
storedPasswordHash) {
+ return passwordHashService.matches(rawPassword, storedPasswordHash)
+ || passwordHashService.matchesLegacySha512(rawPassword,
storedPasswordHash);
}
/**
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index cfb561e414..c38496b3e4 100644
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -419,7 +419,7 @@ CREATE TABLE IF NOT EXISTS `api_rule_relation`
/**default admin user**/
-INSERT IGNORE INTO `dashboard_user` (`id`, `user_name`, `password`, `role`,
`enabled`) VALUES
('1','admin','ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413',
'1', '1');
+INSERT IGNORE INTO `dashboard_user` (`id`, `user_name`, `password`, `role`,
`enabled`) VALUES
('1','admin','$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG',
'1', '1');
/** insert admin role */
INSERT IGNORE INTO `user_role` (`id`, `user_id`, `role_id`) VALUES
('1351007709096976384', '1', '1346358560427216896');
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DashboardUserControllerTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DashboardUserControllerTest.java
index 02b03645d5..a5bca13dbf 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DashboardUserControllerTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DashboardUserControllerTest.java
@@ -26,6 +26,7 @@ import org.apache.shenyu.admin.model.vo.DashboardUserEditVO;
import org.apache.shenyu.admin.model.vo.DashboardUserVO;
import org.apache.shenyu.admin.model.vo.RoleVO;
import org.apache.shenyu.admin.service.DashboardUserService;
+import org.apache.shenyu.admin.service.PasswordHashService;
import org.apache.shenyu.admin.utils.SessionUtil;
import org.apache.shenyu.admin.utils.ShenyuResultMessage;
import org.apache.shenyu.common.utils.GsonUtils;
@@ -47,11 +48,15 @@ import
org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import static org.hamcrest.core.Is.is;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -74,6 +79,9 @@ public final class DashboardUserControllerTest {
@Mock
private DashboardUserService dashboardUserService;
+ @Mock
+ private PasswordHashService passwordHashService;
+
private final DashboardUserVO dashboardUserVO = new DashboardUserVO("id",
"userName",
"bbiB8zbUo3z3oA0VqEB/IA==",
@@ -136,6 +144,7 @@ public final class DashboardUserControllerTest {
@Test
public void createDashboardUser() throws Exception {
final String url = "/dashboardUser";
+
given(passwordHashService.encode("Admin@123")).willReturn("$2b$12$encoded-create");
given(dashboardUserService.createOrUpdate(any())).willReturn(1);
mockMvc.perform(post(url, dashboardUserDTO)
.content(GsonUtils.getInstance().toJson(dashboardUserDTO))
@@ -144,11 +153,28 @@ public final class DashboardUserControllerTest {
.andDo(print())
.andExpect(jsonPath("$.message",
is(ShenyuResultMessage.CREATE_SUCCESS)))
.andExpect(jsonPath("$.data", is(1)));
+ verify(dashboardUserService).createOrUpdate(argThat(dto ->
"$2b$12$encoded-create".equals(dto.getPassword())));
}
@Test
public void updateDashboardUser() throws Exception {
final String url = "/dashboardUser/2";
+
given(passwordHashService.encode("Admin@123")).willReturn("$2b$12$encoded-update");
+ given(dashboardUserService.createOrUpdate(any())).willReturn(1);
+ mockMvc.perform(put(url, dashboardUserDTO)
+ .content(GsonUtils.getInstance().toJson(dashboardUserDTO))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(jsonPath("$.message",
is(ShenyuResultMessage.UPDATE_SUCCESS)))
+ .andExpect(jsonPath("$.data", is(1)));
+ verify(dashboardUserService).createOrUpdate(argThat(dto ->
"$2b$12$encoded-update".equals(dto.getPassword())));
+ }
+
+ @Test
+ public void updateDashboardUserWithBlankPassword() throws Exception {
+ final String url = "/dashboardUser/2";
+ dashboardUserDTO.setPassword(null);
given(dashboardUserService.createOrUpdate(any())).willReturn(1);
mockMvc.perform(put(url, dashboardUserDTO)
.content(GsonUtils.getInstance().toJson(dashboardUserDTO))
@@ -157,6 +183,8 @@ public final class DashboardUserControllerTest {
.andDo(print())
.andExpect(jsonPath("$.message",
is(ShenyuResultMessage.UPDATE_SUCCESS)))
.andExpect(jsonPath("$.data", is(1)));
+ verify(passwordHashService, never()).encode(any());
+ verify(dashboardUserService).createOrUpdate(argThat(dto ->
Objects.isNull(dto.getPassword())));
}
@Test
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
index 791496e8c2..53b679bcf3 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DashboardUserServiceTest.java
@@ -25,9 +25,7 @@ import org.apache.shenyu.admin.mapper.RoleMapper;
import org.apache.shenyu.admin.mapper.UserRoleMapper;
import org.apache.shenyu.admin.model.custom.UserInfo;
import org.apache.shenyu.admin.model.dto.DashboardUserDTO;
-import org.apache.shenyu.admin.model.dto.RoleDTO;
import org.apache.shenyu.admin.model.entity.DashboardUserDO;
-import org.apache.shenyu.admin.model.entity.RoleDO;
import org.apache.shenyu.admin.model.page.CommonPager;
import org.apache.shenyu.admin.model.page.PageParameter;
import org.apache.shenyu.admin.model.query.DashboardUserQuery;
@@ -52,15 +50,18 @@ import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -108,18 +109,24 @@ public final class DashboardUserServiceTest {
@Mock
private NamespaceUserService namespaceUserService;
+ @Mock
+ private PasswordHashService passwordHashService;
+
@Test
public void testCreateOrUpdate() {
SessionUtil.setLocalVisitor(UserInfo.builder().userId("1").userName("admin").build());
- DashboardUserDTO dashboardUserDTO = DashboardUserDTO.builder()
+ final DashboardUserDTO dashboardUserDTO = DashboardUserDTO.builder()
.userName(TEST_USER_NAME).password(TEST_PASSWORD).roles(Collections.singletonList("1"))
.build();
+
given(passwordHashService.isBcryptHash(TEST_PASSWORD)).willReturn(false);
+
given(passwordHashService.encode(TEST_PASSWORD)).willReturn("bcrypt-password");
given(dashboardUserMapper.insertSelective(any(DashboardUserDO.class))).willReturn(1);
given(namespaceUserService.create(any(), any())).willReturn(new
NamespaceUserRelVO());
assertEquals(1, dashboardUserService.createOrUpdate(dashboardUserDTO));
verify(dashboardUserMapper).insertSelective(any(DashboardUserDO.class));
dashboardUserDTO.setId(TEST_ID);
+
given(dashboardUserMapper.selectById(TEST_ID)).willReturn(createDashboardUserDO());
given(dashboardUserMapper.updateSelective(any(DashboardUserDO.class))).willReturn(2);
assertEquals(2, dashboardUserService.createOrUpdate(dashboardUserDTO));
verify(dashboardUserMapper).updateSelective(any(DashboardUserDO.class));
@@ -205,25 +212,25 @@ public final class DashboardUserServiceTest {
ReflectionTestUtils.setField(dashboardUserService, "secretProperties",
secretProperties);
DashboardUserDO dashboardUserDO = createDashboardUserDO();
- when(dashboardUserMapper.findByQuery(eq(TEST_USER_NAME),
anyString())).thenReturn(dashboardUserDO);
+
when(dashboardUserMapper.selectByUserName(eq(TEST_USER_NAME))).thenReturn(dashboardUserDO);
given(ldapTemplate.authenticate(anyString(), anyString(),
anyString())).willReturn(true);
-
given(roleMapper.findByRoleName("default")).willReturn(RoleDO.buildRoleDO(new
RoleDTO("1", "test", null, null)));
-
// test loginByLdap
LdapProperties ldapProperties = new LdapProperties();
ldapProperties.setBaseDn("test");
ReflectionTestUtils.setField(dashboardUserService, "ldapProperties",
ldapProperties);
ReflectionTestUtils.setField(dashboardUserService, "ldapTemplate",
ldapTemplate);
+ given(passwordHashService.matches(TEST_PASSWORD,
TEST_PASSWORD)).willReturn(true);
LoginDashboardUserVO loginDashboardUserVO =
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null);
assertEquals(TEST_USER_NAME, loginDashboardUserVO.getUserName());
- assertEquals(DigestUtils.sha512Hex(TEST_PASSWORD),
loginDashboardUserVO.getPassword());
+ assertEquals(TEST_PASSWORD, loginDashboardUserVO.getPassword());
// test loginByDatabase
ReflectionTestUtils.setField(dashboardUserService, "ldapTemplate",
null);
+ given(passwordHashService.matches(TEST_PASSWORD,
TEST_PASSWORD)).willReturn(true);
assertLoginSuccessful(dashboardUserDO,
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null));
- verify(dashboardUserMapper).findByQuery(eq(TEST_USER_NAME),
anyString());
+ verify(dashboardUserMapper,
times(2)).selectByUserName(eq(TEST_USER_NAME));
assertLoginSuccessful(dashboardUserDO,
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null));
- verify(dashboardUserMapper, times(2)).findByQuery(eq(TEST_USER_NAME),
anyString());
+ verify(dashboardUserMapper,
times(3)).selectByUserName(eq(TEST_USER_NAME));
// test loginByDatabase AES password
SecretProperties secretPropertiesTmp = new SecretProperties();
@@ -231,20 +238,71 @@ public final class DashboardUserServiceTest {
secretPropertiesTmp.setIv(TEST_AES_IV);
ReflectionTestUtils.setField(dashboardUserService, "secretProperties",
secretPropertiesTmp);
ReflectionTestUtils.setField(dashboardUserService, "ldapTemplate",
null);
+ given(passwordHashService.matches(TEST_PASSWORD,
TEST_PASSWORD)).willReturn(true);
assertLoginSuccessful(dashboardUserDO,
dashboardUserService.login(TEST_USER_NAME, AesUtils.cbcEncrypt(TEST_AES_KEY,
TEST_AES_IV, TEST_PASSWORD), null));
- verify(dashboardUserMapper, times(3)).findByQuery(eq(TEST_USER_NAME),
anyString());
+ verify(dashboardUserMapper,
times(4)).selectByUserName(eq(TEST_USER_NAME));
assertLoginSuccessful(dashboardUserDO,
dashboardUserService.login(TEST_USER_NAME, AesUtils.cbcEncrypt(TEST_AES_KEY,
TEST_AES_IV, TEST_PASSWORD), null));
- verify(dashboardUserMapper, times(4)).findByQuery(eq(TEST_USER_NAME),
anyString());
+ verify(dashboardUserMapper,
times(5)).selectByUserName(eq(TEST_USER_NAME));
// test loginByDatabase plain password fallback when secret endpoint
does not provide key material
+ given(passwordHashService.matches(TEST_PASSWORD,
TEST_PASSWORD)).willReturn(true);
assertLoginSuccessful(dashboardUserDO,
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null));
- verify(dashboardUserMapper, times(5)).findByQuery(eq(TEST_USER_NAME),
anyString());
+ verify(dashboardUserMapper,
times(6)).selectByUserName(eq(TEST_USER_NAME));
+ }
+
+ @Test
+ public void testLoginMigratesLegacySha512Password() {
+ ReflectionTestUtils.setField(dashboardUserService, "jwtProperties",
jwtProperties);
+ ReflectionTestUtils.setField(dashboardUserService, "ldapTemplate",
null);
+ DashboardUserDO legacyUser = createDashboardUserDO();
+ legacyUser.setPassword(DigestUtils.sha512Hex(TEST_PASSWORD));
+
when(dashboardUserMapper.selectByUserName(eq(TEST_USER_NAME))).thenReturn(legacyUser);
+ given(passwordHashService.matches(TEST_PASSWORD,
legacyUser.getPassword())).willReturn(false);
+ given(passwordHashService.matchesLegacySha512(TEST_PASSWORD,
legacyUser.getPassword())).willReturn(true);
+
given(passwordHashService.encode(TEST_PASSWORD)).willReturn("bcrypt-encoded");
+
+ LoginDashboardUserVO loginDashboardUserVO =
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null);
+ assertEquals("bcrypt-encoded", loginDashboardUserVO.getPassword());
+ verify(dashboardUserMapper).updateSelective(argThat(user ->
+ TEST_ID.equals(user.getId())
+ && "bcrypt-encoded".equals(user.getPassword())
+ && Objects.nonNull(user.getDateUpdated())));
+ }
+
+ @Test
+ public void testLoginDoesNotMigrateBcryptPassword() {
+ ReflectionTestUtils.setField(dashboardUserService, "jwtProperties",
jwtProperties);
+ ReflectionTestUtils.setField(dashboardUserService, "ldapTemplate",
null);
+ DashboardUserDO bcryptUser = createDashboardUserDO();
+
bcryptUser.setPassword("$2b$12$VRoSQ/.z8C/ldOO9TBfclesgVQ8BxyQK/4Rg/e.DNCisEd.gSyCBG");
+
when(dashboardUserMapper.selectByUserName(eq(TEST_USER_NAME))).thenReturn(bcryptUser);
+ given(passwordHashService.matches(TEST_PASSWORD,
bcryptUser.getPassword())).willReturn(true);
+ LoginDashboardUserVO loginDashboardUserVO =
dashboardUserService.login(TEST_USER_NAME, TEST_PASSWORD, null);
+ assertEquals(bcryptUser.getPassword(),
loginDashboardUserVO.getPassword());
+ verify(dashboardUserMapper,
never()).updateSelective(any(DashboardUserDO.class));
+ }
+
+ @Test
+ public void testModifyPasswordAcceptsLegacySha512OldPassword() {
+ DashboardUserDO legacyUser = createDashboardUserDO();
+ legacyUser.setPassword(DigestUtils.sha512Hex("oldPassword"));
+ given(dashboardUserMapper.selectById(TEST_ID)).willReturn(legacyUser);
+
given(dashboardUserMapper.updateSelective(any(DashboardUserDO.class))).willReturn(1);
+ given(passwordHashService.matches("oldPassword",
legacyUser.getPassword())).willReturn(false);
+ given(passwordHashService.matchesLegacySha512("oldPassword",
legacyUser.getPassword())).willReturn(true);
+
given(passwordHashService.encode("newPassword")).willReturn("bcrypt-new-password");
+
+ assertEquals(1, dashboardUserService.modifyPassword(
+ new
org.apache.shenyu.admin.model.dto.DashboardUserModifyPasswordDTO(TEST_ID,
TEST_USER_NAME, "newPassword", "oldPassword")));
+ verify(dashboardUserMapper).updateSelective(argThat(user ->
+ TEST_ID.equals(user.getId()) &&
"bcrypt-new-password".equals(user.getPassword())));
}
private DashboardUserDO createDashboardUserDO() {
return DashboardUserDO.builder()
.id(TEST_ID).userName(TEST_USER_NAME).password(TEST_PASSWORD)
+ .enabled(true)
.dateCreated(new Timestamp(System.currentTimeMillis()))
.dateUpdated(new Timestamp(System.currentTimeMillis()))
.build();