diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index 8891da9e819f..d88db8536df2 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 d2fdbc7cb125..2f259975feb1 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 bbaee3d598b2..97a90b57fe26 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 6812ba975897..aa44e96a3355 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 23cc1577a510..f8e452297a18 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 08f21c0b9e16..e75d568cc42a 100644
--- a/shenyu-admin/pom.xml
+++ b/shenyu-admin/pom.xml
@@ -74,6 +74,11 @@
spring-boot-starter-actuator
+
+ org.springframework.security
+ spring-security-crypto
+
+
org.springframework.boot
spring-boot-starter-integration
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 e5c5b429de1a..d812a066d89a 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.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 @@
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 ShenyuAdminResult detailDashboardUser(@PathVariable("id") final String id
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 ShenyuAdminResult updateDashboardUser(@PathVariable("id")
@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 ShenyuAdminResult modifyPassword(@PathVariable("id")
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 7b94a94b50af..4da544f8d9d5 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 3144874442df..d967ef3c3847 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 000000000000..29b10509f0c6
--- /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 87ea9546c2da..cf3e643a1b0c 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.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.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.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 DashboardUserServiceImpl(final DashboardUserMapper dashboardUserMapper,
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 DashboardUserServiceImpl(final DashboardUserMapper dashboardUserMapper,
this.properties = properties;
this.secretProperties = secretProperties;
this.namespaceUserService = namespaceUserService;
+ this.passwordHashService = passwordHashService;
}
/**
@@ -147,6 +156,7 @@ public int create(final DashboardUserDTO dashboardUserDTO) {
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 int create(final DashboardUserDTO dashboardUserDTO) {
@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 DashboardUserEditVO findById(final String id) {
* @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 @@ private boolean isPotentialEncryptedPassword(final String password) {
}
private Optional 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 int modifyPassword(final DashboardUserModifyPasswordDTO dashboardUserModi
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 @@ private DashboardUserVO loginByLdap(final String userName, final String password
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 @@ private DashboardUserVO loginByLdap(final String userName, final String password
}
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 cfb561e4148b..c38496b3e4bb 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 02b03645d593..a5bca13dbf8f 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.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 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 void detailDashboardUser() throws Exception {
@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 void createDashboardUser() throws Exception {
.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 void updateDashboardUser() throws Exception {
.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 791496e8c2f6..53b679bcf353 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.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.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 void testLogin() {
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 void testLogin() {
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();