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


Reply via email to