未验证 提交 72a04989 编写于 作者: V vdisk-group 提交者: GitHub

feature: add the delegating password encoder for apollo-portal simple auth (#3804)

* DelegatingPasswordEncoder

* sql

* extend Password to 512

* update CHANGES.md

* add an adapter for old password

* fix the unit test NullPointerException

* only throws Exception on password has an id

* mark add prefix for `Users`.`Password` optional

* modify unit test

* remove {bcrypt} prefix on sql
上级 907dbad1
......@@ -55,6 +55,7 @@ Apollo 1.9.0
* [feature: modify item comment valid size](https://github.com/ctripcorp/apollo/pull/3803)
* [set default session store-type](https://github.com/ctripcorp/apollo/pull/3812)
* [speed up the stale issue mark and close phase](https://github.com/ctripcorp/apollo/pull/3808)
* [feature: add the delegating password encoder for apollo-portal simple auth](https://github.com/ctripcorp/apollo/pull/3804)
------------------
All issues and pull requests are [here](https://github.com/ctripcorp/apollo/milestone/6?closed=1)
......@@ -33,9 +33,9 @@ import com.google.common.util.concurrent.SettableFuture;
import java.io.IOException;
import java.util.Collections;
import java.util.Properties;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Test;
......@@ -383,7 +383,8 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
}
@Test
public void testApolloConfigChangeListenerWithInterestedKeyPrefixes_fire() {
public void testApolloConfigChangeListenerWithInterestedKeyPrefixes_fire()
throws InterruptedException {
// default mock, useless here
// just for speed up test without waiting
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, mock(Config.class));
......@@ -946,7 +947,7 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
static final String SPECIAL_NAMESPACE = "special-namespace-2021";
private final Queue<ConfigChangeEvent> configChangeEventQueue = new ArrayBlockingQueue<>(100);
private final BlockingQueue<ConfigChangeEvent> configChangeEventQueue = new ArrayBlockingQueue<>(100);
@ApolloConfigChangeListener(value = SPECIAL_NAMESPACE, interestedKeyPrefixes = {"number",
"logging.level"})
......@@ -954,8 +955,8 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
this.configChangeEventQueue.add(changeEvent);
}
public ConfigChangeEvent getConfigChangeEvent() {
return this.configChangeEventQueue.poll();
public ConfigChangeEvent getConfigChangeEvent() throws InterruptedException {
return this.configChangeEventQueue.poll(5, TimeUnit.SECONDS);
}
}
......
......@@ -41,6 +41,7 @@ import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserService;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserServiceImpl;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLogoutHandler;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.ApolloPasswordEncoderFactory;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
import com.google.common.collect.Maps;
......@@ -64,7 +65,7 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
......@@ -241,6 +242,12 @@ public class AuthConfiguration {
return new DefaultSsoHeartbeatHandler();
}
@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public static PasswordEncoder passwordEncoder() {
return ApolloPasswordEncoderFactory.createDelegatingPasswordEncoder();
}
@Bean
@ConditionalOnMissingBean(UserInfoHolder.class)
public UserInfoHolder springSecurityUserInfoHolder(UserService userService) {
......@@ -254,10 +261,10 @@ public class AuthConfiguration {
}
@Bean
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth,
DataSource datasource) throws Exception {
public static JdbcUserDetailsManager jdbcUserDetailsManager(PasswordEncoder passwordEncoder,
AuthenticationManagerBuilder auth, DataSource datasource) throws Exception {
JdbcUserDetailsManager jdbcUserDetailsManager = auth.jdbcAuthentication()
.passwordEncoder(new BCryptPasswordEncoder()).dataSource(datasource)
.passwordEncoder(passwordEncoder).dataSource(datasource)
.usersByUsernameQuery("select Username,Password,Enabled from `Users` where Username = ?")
.authoritiesByUsernameQuery(
"select Username,Authority from `Authorities` where Username = ?")
......@@ -281,8 +288,10 @@ public class AuthConfiguration {
@Bean
@ConditionalOnMissingBean(UserService.class)
public UserService springSecurityUserService() {
return new SpringSecurityUserService();
public UserService springSecurityUserService(PasswordEncoder passwordEncoder,
JdbcUserDetailsManager userDetailsManager,
UserRepository userRepository) {
return new SpringSecurityUserService(passwordEncoder, userDetailsManager, userRepository);
}
}
......@@ -471,11 +480,18 @@ public class AuthConfiguration {
return new OidcLogoutHandler();
}
@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public PasswordEncoder passwordEncoder() {
return SpringSecurityAuthAutoConfiguration.passwordEncoder();
}
@Bean
@ConditionalOnMissingBean(JdbcUserDetailsManager.class)
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth,
DataSource datasource) throws Exception {
return new SpringSecurityAuthAutoConfiguration().jdbcUserDetailsManager(auth, datasource);
public JdbcUserDetailsManager jdbcUserDetailsManager(PasswordEncoder passwordEncoder,
AuthenticationManagerBuilder auth, DataSource datasource) throws Exception {
return SpringSecurityAuthAutoConfiguration
.jdbcUserDetailsManager(passwordEncoder, auth, datasource);
}
@Bean
......
......@@ -31,6 +31,8 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
......@@ -43,6 +45,10 @@ public class OidcLocalUserServiceImpl implements OidcLocalUserService {
private final Collection<? extends GrantedAuthority> authorities = Collections
.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
private final PasswordEncoder placeholderDelegatingPasswordEncoder = new DelegatingPasswordEncoder(
PlaceholderPasswordEncoder.ENCODING_ID, Collections
.singletonMap(PlaceholderPasswordEncoder.ENCODING_ID, new PlaceholderPasswordEncoder()));
private final JdbcUserDetailsManager userDetailsManager;
private final UserRepository userRepository;
......@@ -58,20 +64,11 @@ public class OidcLocalUserServiceImpl implements OidcLocalUserService {
@Override
public void createLocalUser(UserInfo newUserInfo) {
UserDetails user = new User(newUserInfo.getUserId(),
"{nonsensical}" + this.nonsensicalPassword(), authorities);
this.placeholderDelegatingPasswordEncoder.encode(""), authorities);
userDetailsManager.createUser(user);
this.updateUserInfoInternal(newUserInfo);
}
/**
* generate a random password with no meaning
*/
private String nonsensicalPassword() {
byte[] bytes = new byte[32];
ThreadLocalRandom.current().nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
private void updateUserInfoInternal(UserInfo newUserInfo) {
UserPO managedUser = userRepository.findByUsername(newUserInfo.getUserId());
if (!StringUtils.isBlank(newUserInfo.getEmail())) {
......
/*
* Copyright 2021 Apollo Authors
*
* Licensed 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 com.ctrip.framework.apollo.portal.spi.oidc;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author vdisk <vdisk@foxmail.com>
*/
public class PlaceholderPasswordEncoder implements PasswordEncoder {
public static final String ENCODING_ID = "placeholder";
/**
* generate a random string as a password placeholder.
*/
@Override
public String encode(CharSequence rawPassword) {
byte[] bytes = new byte[32];
ThreadLocalRandom.current().nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
/**
* placeholder will never matches a password
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return false;
}
}
/*
* Copyright 2021 Apollo Authors
*
* Licensed 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 com.ctrip.framework.apollo.portal.spi.springsecurity;
import com.ctrip.framework.apollo.portal.spi.oidc.PlaceholderPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
/**
* @author vdisk <vdisk@foxmail.com>
*/
public final class ApolloPasswordEncoderFactory {
private ApolloPasswordEncoderFactory() {
}
/**
* Creates a {@link DelegatingPasswordEncoder} with default mappings {@link
* PasswordEncoderFactories#createDelegatingPasswordEncoder()}, and add a placeholder encoder for
* oidc {@link PlaceholderPasswordEncoder}
*
* @return the {@link PasswordEncoder} to use
*/
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
// copy from PasswordEncoderFactories, and it's should follow the upgrade of the PasswordEncoderFactories
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop",
org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders
.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
// placeholder encoder for oidc
encoders.put(PlaceholderPasswordEncoder.ENCODING_ID, new PlaceholderPasswordEncoder());
DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId,
encoders);
// todo: adapt the old password, and it should be removed in the next feature version of the 1.9.x
delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new PasswordEncoderAdapter(encoders.get(encodingId)));
return delegatingPasswordEncoder;
}
}
/*
* Copyright 2021 Apollo Authors
*
* Licensed 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 com.ctrip.framework.apollo.portal.spi.springsecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.StringUtils;
/**
* @author vdisk <vdisk@foxmail.com>
*/
@Deprecated
public class PasswordEncoderAdapter implements PasswordEncoder {
private static final String PREFIX = "{";
private static final String SUFFIX = "}";
private final PasswordEncoder encoder;
public PasswordEncoderAdapter(
PasswordEncoder encoder) {
this.encoder = encoder;
}
@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
boolean matches = this.encoder.matches(rawPassword, encodedPassword);
if (matches) {
return true;
}
String id = this.extractId(encodedPassword);
if (StringUtils.hasText(id)) {
throw new IllegalArgumentException(
"There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
return false;
}
private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
}
int start = prefixEncodedPassword.indexOf(PREFIX);
if (start != 0) {
return null;
}
int end = prefixEncodedPassword.indexOf(SUFFIX, start);
if (end < 0) {
return null;
}
return prefixEncodedPassword.substring(start + 1, end);
}
}
......@@ -16,55 +16,54 @@
*/
package com.ctrip.framework.apollo.portal.spi.springsecurity;
import com.google.common.collect.Lists;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
import com.ctrip.framework.apollo.portal.entity.po.UserPO;
import com.ctrip.framework.apollo.portal.repository.UserRepository;
import com.ctrip.framework.apollo.portal.spi.UserService;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
/**
* @author lepdou 2017-03-10
*/
public class SpringSecurityUserService implements UserService {
private PasswordEncoder encoder = new BCryptPasswordEncoder();
private List<GrantedAuthority> authorities;
private final List<GrantedAuthority> authorities = Collections
.unmodifiableList(Arrays.asList(new SimpleGrantedAuthority("ROLE_user")));
private final PasswordEncoder passwordEncoder;
@Autowired
private JdbcUserDetailsManager userDetailsManager;
@Autowired
private UserRepository userRepository;
private final JdbcUserDetailsManager userDetailsManager;
@PostConstruct
public void init() {
authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_user"));
private final UserRepository userRepository;
public SpringSecurityUserService(
PasswordEncoder passwordEncoder,
JdbcUserDetailsManager userDetailsManager,
UserRepository userRepository) {
this.passwordEncoder = passwordEncoder;
this.userDetailsManager = userDetailsManager;
this.userRepository = userRepository;
}
@Transactional
public void createOrUpdate(UserPO user) {
String username = user.getUsername();
User userDetails = new User(username, encoder.encode(user.getPassword()), authorities);
User userDetails = new User(username, passwordEncoder.encode(user.getPassword()), authorities);
if (userDetailsManager.userExists(username)) {
userDetailsManager.updateUser(userDetails);
......@@ -121,12 +120,6 @@ public class SpringSecurityUserService implements UserService {
return Collections.emptyList();
}
List<UserInfo> result = Lists.newArrayList();
result.addAll(users.stream().map(UserPO::toUserInfo).collect(Collectors.toList()));
return result;
return users.stream().map(UserPO::toUserInfo).collect(Collectors.toList());
}
}
......@@ -298,7 +298,7 @@ DROP TABLE IF EXISTS `Users`;
CREATE TABLE `Users` (
`Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`Username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户登录账户',
`Password` varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码',
`Password` varchar(512) NOT NULL DEFAULT 'default' COMMENT '密码',
`UserDisplayName` varchar(512) NOT NULL DEFAULT 'default' COMMENT '用户名称',
`Email` varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址',
`Enabled` tinyint(4) DEFAULT NULL COMMENT '是否有效',
......
......@@ -298,7 +298,7 @@ DROP TABLE IF EXISTS `Users`;
CREATE TABLE `Users` (
`Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`Username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户登录账户',
`Password` varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码',
`Password` varchar(512) NOT NULL DEFAULT 'default' COMMENT '密码',
`UserDisplayName` varchar(512) NOT NULL DEFAULT 'default' COMMENT '用户名称',
`Email` varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址',
`Enabled` tinyint(4) DEFAULT NULL COMMENT '是否有效',
......
--
-- Copyright 2021 Apollo Authors
--
-- Licensed 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.
--
Use ApolloPortalDB;
ALTER TABLE `Users`
MODIFY COLUMN `Password` varchar(512) NOT NULL DEFAULT 'default' COMMENT '密码';
UPDATE `Users` SET `Password` = REPLACE(`Password`, '{nonsensical}', '{placeholder}') WHERE `Password` LIKE '{nonsensical}%';
......@@ -298,7 +298,7 @@ DROP TABLE IF EXISTS `Users`;
CREATE TABLE `Users` (
`Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`Username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户登录账户',
`Password` varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码',
`Password` varchar(512) NOT NULL DEFAULT 'default' COMMENT '密码',
`UserDisplayName` varchar(512) NOT NULL DEFAULT 'default' COMMENT '用户名称',
`Email` varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址',
`Enabled` tinyint(4) DEFAULT NULL COMMENT '是否有效',
......
......@@ -65,3 +65,10 @@ ALTER TABLE `Users`
MODIFY COLUMN `Username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户登录账户',
ADD COLUMN `UserDisplayName` varchar(512) NOT NULL DEFAULT 'default' COMMENT '用户名称' AFTER `Password`;
UPDATE `Users` SET `UserDisplayName`=`Username` WHERE `UserDisplayName` = 'default';
ALTER TABLE `Users`
MODIFY COLUMN `Password` varchar(512) NOT NULL DEFAULT 'default' COMMENT '密码';
UPDATE `Users` SET `Password` = REPLACE(`Password`, '{nonsensical}', '{placeholder}') WHERE `Password` LIKE '{nonsensical}%';
-- note: add the {bcrypt} prefix for `Users`.`Password` is not mandatory, and it may break the old version of apollo-portal while upgrading.
-- 注意: 向 `Users`.`Password` 添加 {bcrypt} 是非必须操作, 并且这个操作会导致升级 apollo-portal 集群的过程中旧版的 apollo-portal 无法使用.
-- UPDATE `Users` SET `Password` = CONCAT('{bcrypt}', `Password`) WHERE `Password` NOT LIKE '{%}%';
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册