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