mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
feat(auth): 增强认证逻辑,支持密码过期校验及定时攻击保护机制
This commit is contained in:
1 parent
2551ef2606
commit
79b27b6d7d
6 files changed
+395
-240
No files matched your search
+190
-31
@@ -2,14 +2,17 @@ package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.pig4cloud.pigx.auth.support.filter.AuthSecurityConfigProperties;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
|
||||
@@ -25,44 +28,97 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Pigx 数据访问对象认证提供者
|
||||
* <p>
|
||||
* 扩展自 Spring Security 的 {@link AbstractUserDetailsAuthenticationProvider},
|
||||
* 仿照 {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider} 实现,
|
||||
* 提供了更丰富的认证功能。
|
||||
* </p>
|
||||
*
|
||||
* <h3>主要功能:</h3>
|
||||
* <ul>
|
||||
* <li>1. 增加用户过期、锁定、凭证过期、禁用校验</li>
|
||||
* <li>2. 增加密码过期校验,支持配置密码有效期</li>
|
||||
* <li>3. 增加多种登录方式,灵活使用 UserDetailsService</li>
|
||||
* <li>4. 支持 APP 模式免密码验证</li>
|
||||
* <li>5. 提供定时攻击保护机制</li>
|
||||
* <li>6. 支持多租户上下文设置</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>使用场景:</h3>
|
||||
* <p>
|
||||
* 适用于需要复杂认证逻辑的系统,特别是多租户、多客户端的应用场景。
|
||||
* 可以根据不同的客户端 ID 和授权类型选择合适的用户详情服务。
|
||||
* </p>
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2022-06-04
|
||||
* @version 1.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* The plaintext password used to perform PasswordEncoder#matches(CharSequence,
|
||||
* String)} on when the user is not found to avoid SEC-2056.
|
||||
*/
|
||||
/** 用户未找到时使用的默认密码字符串 */
|
||||
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
|
||||
|
||||
/** 基础认证转换器,用于从请求中提取客户端凭证 */
|
||||
private final static BasicAuthenticationConverter basicConvert = new BasicAuthenticationConverter();
|
||||
|
||||
/** 密码编码器,用于密码的编码和验证 */
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
* The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
|
||||
* on when the user is not found to avoid SEC-2056. This is necessary, because some
|
||||
* {@link PasswordEncoder} implementations will short circuit if the password is not
|
||||
* in a valid format.
|
||||
*/
|
||||
/** 用户未找到时的编码密码,用于防止定时攻击 */
|
||||
private volatile String userNotFoundEncodedPassword;
|
||||
|
||||
/** 用户详情服务,用于加载用户信息 */
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
/** 用户详情密码服务,用于密码升级 */
|
||||
private UserDetailsPasswordService userDetailsPasswordService;
|
||||
|
||||
/**
|
||||
* 构造函数,初始化认证提供者。
|
||||
* <p>
|
||||
* 执行以下初始化操作:
|
||||
* <ul>
|
||||
* <li>设置国际化消息源,用于错误信息的本地化</li>
|
||||
* <li>配置委托密码编码器,支持多种密码编码格式</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @see org.springframework.security.crypto.factory.PasswordEncoderFactories#createDelegatingPasswordEncoder()
|
||||
*/
|
||||
public PigxDaoAuthenticationProvider() {
|
||||
setMessageSource(SpringUtil.getBean("securityMessageSource"));
|
||||
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行额外的身份验证检查。
|
||||
* <p>
|
||||
* 该方法在用户详情加载完成后执行,主要进行以下验证:
|
||||
* <ul>
|
||||
* <li>1. 检查授权类型,APP 模式下跳过密码验证</li>
|
||||
* <li>2. 验证密码凭证是否为空</li>
|
||||
* <li>3. 验证提供的密码是否与存储的密码匹配</li>
|
||||
* <li>4. 检查密码是否过期(如果启用了密码过期策略)</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @param userDetails 表示用户的 UserDetails 对象,其凭证应该被检查
|
||||
* @param authentication 认证请求,此时仍然不受信任
|
||||
* @throws AuthenticationException 如果身份验证失败
|
||||
* @throws BadCredentialsException 如果密码不匹配或为空
|
||||
* @throws CredentialsExpiredException 如果密码已过期
|
||||
*/
|
||||
@Override
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails,
|
||||
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
||||
@@ -84,8 +140,46 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
throw new BadCredentialsException(this.messages
|
||||
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
|
||||
}
|
||||
|
||||
// 密码过期判断,如果开启的
|
||||
AuthSecurityConfigProperties configProperties = SpringUtil.getBean(AuthSecurityConfigProperties.class);
|
||||
if (configProperties.isExpirePassword() && userDetails instanceof PigxUser pigxUser
|
||||
&& Objects.nonNull(pigxUser.getPasswordModifyTime())) {
|
||||
Long expireDays = ParamResolver.getLong("PASSWORD_EXPIRE_DAYS", 90L);
|
||||
long daysBetween = ChronoUnit.DAYS.between(pigxUser.getPasswordModifyTime(), LocalDateTime.now());
|
||||
if (daysBetween > expireDays) {
|
||||
this.logger.debug("Failed to authenticate since password expired");
|
||||
throw new CredentialsExpiredException("User credentials have expired");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名检索用户详情。
|
||||
* <p>
|
||||
* 该方法实现了智能的用户详情服务选择机制:
|
||||
* <ul>
|
||||
* <li>1. 从请求中提取授权类型和客户端 ID</li>
|
||||
* <li>2. 如果客户端 ID 为空,则从 Basic 认证头中提取</li>
|
||||
* <li>3. 根据客户端 ID 和授权类型选择合适的 UserDetailsService</li>
|
||||
* <li>4. 按照优先级(Order)选择最合适的服务</li>
|
||||
* <li>5. 加载用户详情并设置租户上下文(如果是多租户用户)</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>注意:</strong>该方法包含定时攻击保护机制,确保在用户不存在时
|
||||
* 仍然执行相同的时间消耗操作。
|
||||
* </p>
|
||||
*
|
||||
* @param username 要检索的用户名
|
||||
* @param authentication 认证请求,此时仍然不受信任
|
||||
* @return 加载的用户详情对象
|
||||
* @throws UsernameNotFoundException 如果用户名不存在
|
||||
* @throws InternalAuthenticationServiceException 如果没有注册合适的 UserDetailsService 或服务返回 null
|
||||
* @see PigxUserDetailsService#support(String, String)
|
||||
* @see TenantContextHolder#setTenantId(Long)
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Override
|
||||
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
|
||||
@@ -130,6 +224,30 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功的身份验证对象。
|
||||
* <p>
|
||||
* 该方法在身份验证成功后被调用,主要执行以下操作:
|
||||
* <ul>
|
||||
* <li>1. 检查是否需要升级密码编码格式</li>
|
||||
* <li>2. 如果需要升级,使用新的编码格式重新编码密码</li>
|
||||
* <li>3. 通过 UserDetailsPasswordService 更新用户密码</li>
|
||||
* <li>4. 调用父类方法创建最终的认证对象</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>密码升级机制:</strong>当系统升级了密码编码算法时,
|
||||
* 该方法会自动将用户的旧格式密码升级为新格式,提高系统安全性。
|
||||
* </p>
|
||||
*
|
||||
* @param principal 由认证提供者返回的主体对象
|
||||
* @param authentication 原始的认证请求
|
||||
* @param user 由认证提供者返回的 UserDetails 实例
|
||||
* @return 成功的认证对象
|
||||
* @see UserDetailsPasswordService#updatePassword(UserDetails, String)
|
||||
* @see PasswordEncoder#upgradeEncoding(String)
|
||||
*/
|
||||
@Override
|
||||
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
|
||||
UserDetails user) {
|
||||
@@ -143,12 +261,51 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
return super.createSuccessAuthentication(principal, authentication, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备定时攻击保护机制。
|
||||
* <p>
|
||||
* 该方法为防止定时攻击做准备工作。定时攻击是一种安全攻击方式,
|
||||
* 攻击者通过测量系统响应时间的差异来推断用户是否存在。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>工作原理:</strong>
|
||||
* <ul>
|
||||
* <li>预先编码一个固定的假密码</li>
|
||||
* <li>当用户不存在时,使用这个假密码进行密码验证操作</li>
|
||||
* <li>确保无论用户是否存在,密码验证的时间消耗基本一致</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @see #mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken)
|
||||
*/
|
||||
private void prepareTimingAttackProtection() {
|
||||
if (this.userNotFoundEncodedPassword == null) {
|
||||
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行定时攻击缓解措施。
|
||||
* <p>
|
||||
* 当用户不存在时,该方法会执行一次假的密码验证操作,
|
||||
* 确保系统响应时间与真实用户验证时间保持一致,
|
||||
* 防止攻击者通过响应时间差异判断用户是否存在。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>安全意义:</strong>
|
||||
* 没有这种保护机制时,攻击者可能会发现:
|
||||
* <ul>
|
||||
* <li>用户存在时:查询数据库 + 密码验证 = 较长时间</li>
|
||||
* <li>用户不存在时:仅返回错误 = 较短时间</li>
|
||||
* </ul>
|
||||
* 通过时间差异,攻击者可以枚举出系统中存在的用户名。
|
||||
* </p>
|
||||
*
|
||||
* @param authentication 包含用户凭证的认证对象
|
||||
* @see #prepareTimingAttackProtection()
|
||||
*/
|
||||
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
|
||||
if (authentication.getCredentials() != null) {
|
||||
String presentedPassword = authentication.getCredentials().toString();
|
||||
@@ -156,16 +313,30 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
}
|
||||
}
|
||||
|
||||
protected PasswordEncoder getPasswordEncoder() {
|
||||
return this.passwordEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PasswordEncoder instance to be used to encode and validate passwords. If
|
||||
* not set, the password will be compared using
|
||||
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
|
||||
* @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
|
||||
* types.
|
||||
* 设置密码编码器实例,用于密码的编码和验证。
|
||||
* <p>
|
||||
* 密码编码器负责以下功能:
|
||||
* <ul>
|
||||
* <li>对明文密码进行安全编码(加密/哈希)</li>
|
||||
* <li>验证用户输入的密码是否与存储的编码密码匹配</li>
|
||||
* <li>支持密码编码格式的升级迁移</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>注意事项:</strong>
|
||||
* <ul>
|
||||
* <li>如果不设置编码器,密码将以明文形式进行比较(不推荐)</li>
|
||||
* <li>对于已经使用旧版本编码器的系统,建议使用兼容的编码器类型</li>
|
||||
* <li>设置新编码器后,会重置用户未找到时的编码密码缓存</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @param passwordEncoder 密码编码器实例,必须是 {@code PasswordEncoder} 类型之一
|
||||
* @throws IllegalArgumentException 如果 passwordEncoder 为 null
|
||||
* @see org.springframework.security.crypto.password.PasswordEncoder
|
||||
* @see org.springframework.security.crypto.factory.PasswordEncoderFactories
|
||||
*/
|
||||
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
|
||||
@@ -173,16 +344,4 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
this.userNotFoundEncodedPassword = null;
|
||||
}
|
||||
|
||||
protected UserDetailsService getUserDetailsService() {
|
||||
return this.userDetailsService;
|
||||
}
|
||||
|
||||
public void setUserDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
|
||||
this.userDetailsPasswordService = userDetailsPasswordService;
|
||||
}
|
||||
|
||||
}
|
||||
+6
-3
@@ -6,10 +6,10 @@ import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 安全认证配置属性类
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020/10/4
|
||||
* <p>
|
||||
* 网关配置文件
|
||||
* @date 2025/08/06
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@@ -27,4 +27,7 @@ public class AuthSecurityConfigProperties {
|
||||
*/
|
||||
private String encodeKey;
|
||||
|
||||
/** 是否开启密码超期强制修改密码 */
|
||||
private boolean expirePassword = false;
|
||||
|
||||
}
|
||||
+67
-71
@@ -52,92 +52,88 @@ import java.util.*;
|
||||
@RequiredArgsConstructor
|
||||
public class PigxTocDefaultUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
private final Optional<RemoteAppUserService> remoteAppUserServiceOptional;
|
||||
|
||||
private final Optional<RemoteAppUserService> remoteAppUserServiceOptional;
|
||||
/**
|
||||
* 用户密码登录
|
||||
* @param username 用户密码登录
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS_MINI);
|
||||
if (Objects.nonNull(cache) && Objects.nonNull(cache.get(username))) {
|
||||
AppUserInfo appUserInfo = cache.get(username, AppUserInfo.class);
|
||||
return getUserDetailsAppUser(R.ok(appUserInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户密码登录
|
||||
*
|
||||
* @param username 用户密码登录
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS_MINI);
|
||||
if (Objects.nonNull(cache) && Objects.nonNull(cache.get(username))) {
|
||||
AppUserInfo appUserInfo = cache.get(username, AppUserInfo.class);
|
||||
return getUserDetailsAppUser(R.ok(appUserInfo));
|
||||
}
|
||||
R<AppUserInfo> result = remoteAppUserServiceOptional.get().info(username);
|
||||
Optional<AppUserInfo> userInfoOptional = RetOps.of(result).getData();
|
||||
userInfoOptional.ifPresent(userInfo -> Objects.requireNonNull(cache).put(username, userInfo));
|
||||
return getUserDetailsAppUser(result);
|
||||
}
|
||||
|
||||
R<AppUserInfo> result = remoteAppUserServiceOptional.get().info(username);
|
||||
Optional<AppUserInfo> userInfoOptional = RetOps.of(result).getData();
|
||||
userInfoOptional.ifPresent(userInfo -> Objects.requireNonNull(cache).put(username, userInfo));
|
||||
return getUserDetailsAppUser(result);
|
||||
}
|
||||
@Override
|
||||
public UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return pigxUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return pigxUser;
|
||||
}
|
||||
|
||||
UserDetails getUserDetailsAppUser(R<AppUserInfo> result) {
|
||||
// @formatter:off
|
||||
UserDetails getUserDetailsAppUser(R<AppUserInfo> result) {
|
||||
// @formatter:off
|
||||
return RetOps.of(result)
|
||||
.assertSuccess(r -> new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE),null)))
|
||||
.getData()
|
||||
.map(this::convertUserDetailsAppUser)
|
||||
.orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE)));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UserInfo 转 UserDetails
|
||||
*
|
||||
* @param info
|
||||
* @return 返回UserDetails对象
|
||||
*/
|
||||
UserDetails convertUserDetailsAppUser(AppUserInfo info) {
|
||||
Set<String> dbAuthsSet = new HashSet<>();
|
||||
if (ArrayUtil.isNotEmpty(info.getRoles())) {
|
||||
// 获取角色
|
||||
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
|
||||
// 获取资源
|
||||
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
|
||||
/**
|
||||
* UserInfo 转 UserDetails
|
||||
* @param info
|
||||
* @return 返回UserDetails对象
|
||||
*/
|
||||
UserDetails convertUserDetailsAppUser(AppUserInfo info) {
|
||||
Set<String> dbAuthsSet = new HashSet<>();
|
||||
if (ArrayUtil.isNotEmpty(info.getRoles())) {
|
||||
// 获取角色
|
||||
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
|
||||
// 获取资源
|
||||
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
|
||||
|
||||
}
|
||||
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
|
||||
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
|
||||
AppUser user = info.getAppUser();
|
||||
// 构造security用户
|
||||
}
|
||||
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
|
||||
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
|
||||
AppUser user = info.getAppUser();
|
||||
// 构造security用户
|
||||
|
||||
return new PigxUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
|
||||
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
|
||||
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true,
|
||||
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
|
||||
}
|
||||
return new PigxUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
|
||||
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
|
||||
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true, null,
|
||||
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 10;
|
||||
}
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持所有的 mobile 类型
|
||||
*
|
||||
* @param clientId 目标客户端
|
||||
* @param grantType 授权类型
|
||||
* @return true/false
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String clientId, String grantType) {
|
||||
if (Objects.isNull(WebUtils.getRequest())) {
|
||||
return false;
|
||||
}
|
||||
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
|
||||
return SecurityConstants.HEADER_TOC_YES.equals(header);
|
||||
}
|
||||
/**
|
||||
* 支持所有的 mobile 类型
|
||||
* @param clientId 目标客户端
|
||||
* @param grantType 授权类型
|
||||
* @return true/false
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String clientId, String grantType) {
|
||||
if (Objects.isNull(WebUtils.getRequest())) {
|
||||
return false;
|
||||
}
|
||||
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
|
||||
return SecurityConstants.HEADER_TOC_YES.equals(header);
|
||||
}
|
||||
|
||||
}
|
||||
+62
-67
@@ -40,8 +40,6 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.pig4cloud.pigx.common.security.service.PigxTocDefaultUserDetailsServiceImpl.NOTFOUND_USER_ERROR_CODE;
|
||||
|
||||
/**
|
||||
* 用户详细信息
|
||||
*
|
||||
@@ -51,85 +49,82 @@ import static com.pig4cloud.pigx.common.security.service.PigxTocDefaultUserDetai
|
||||
@RequiredArgsConstructor
|
||||
public class PigxTocMobileUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
|
||||
private final Optional<RemoteAppUserService> remoteAppUserServiceOptional;
|
||||
private final Optional<RemoteAppUserService> remoteAppUserServiceOptional;
|
||||
|
||||
/**
|
||||
* 用户密码登录
|
||||
*
|
||||
* @param phone 用户密码登录
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public UserDetails loadUserByUsername(String phone) {
|
||||
if (!remoteAppUserServiceOptional.isPresent()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
R<AppUserInfo> info = remoteAppUserServiceOptional.get().social(phone);
|
||||
return this.getUserDetailsAppUser(info);
|
||||
}
|
||||
/**
|
||||
* 用户密码登录
|
||||
* @param phone 用户密码登录
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public UserDetails loadUserByUsername(String phone) {
|
||||
if (!remoteAppUserServiceOptional.isPresent()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
R<AppUserInfo> info = remoteAppUserServiceOptional.get().social(phone);
|
||||
return this.getUserDetailsAppUser(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return pigxUser;
|
||||
}
|
||||
@Override
|
||||
public UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return pigxUser;
|
||||
}
|
||||
|
||||
UserDetails getUserDetailsAppUser(R<AppUserInfo> result) {
|
||||
// @formatter:off
|
||||
UserDetails getUserDetailsAppUser(R<AppUserInfo> result) {
|
||||
// @formatter:off
|
||||
return RetOps.of(result)
|
||||
.assertSuccess(r -> new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE),null)))
|
||||
.getData()
|
||||
.map(this::convertUserDetailsAppUser)
|
||||
.orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE)));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UserInfo 转 UserDetails
|
||||
*
|
||||
* @param info
|
||||
* @return 返回UserDetails对象
|
||||
*/
|
||||
UserDetails convertUserDetailsAppUser(AppUserInfo info) {
|
||||
Set<String> dbAuthsSet = new HashSet<>();
|
||||
if (ArrayUtil.isNotEmpty(info.getRoles())) {
|
||||
// 获取角色
|
||||
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
|
||||
// 获取资源
|
||||
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
|
||||
/**
|
||||
* UserInfo 转 UserDetails
|
||||
* @param info
|
||||
* @return 返回UserDetails对象
|
||||
*/
|
||||
UserDetails convertUserDetailsAppUser(AppUserInfo info) {
|
||||
Set<String> dbAuthsSet = new HashSet<>();
|
||||
if (ArrayUtil.isNotEmpty(info.getRoles())) {
|
||||
// 获取角色
|
||||
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
|
||||
// 获取资源
|
||||
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
|
||||
|
||||
}
|
||||
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
|
||||
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
|
||||
AppUser user = info.getAppUser();
|
||||
// 构造security用户
|
||||
}
|
||||
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
|
||||
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
|
||||
AppUser user = info.getAppUser();
|
||||
// 构造security用户
|
||||
|
||||
return new PigxUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
|
||||
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
|
||||
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true,
|
||||
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
|
||||
}
|
||||
return new PigxUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
|
||||
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
|
||||
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true, null,
|
||||
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 15;
|
||||
}
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持所有的 mobile 类型
|
||||
*
|
||||
* @param clientId 目标客户端
|
||||
* @param grantType 授权类型
|
||||
* @return true/false
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String clientId, String grantType) {
|
||||
if (Objects.isNull(WebUtils.getRequest())) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 支持所有的 mobile 类型
|
||||
* @param clientId 目标客户端
|
||||
* @param grantType 授权类型
|
||||
* @return true/false
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String clientId, String grantType) {
|
||||
if (Objects.isNull(WebUtils.getRequest())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
|
||||
return SecurityConstants.HEADER_TOC_YES.equals(header) && SecurityConstants.GRANT_MOBILE.equals(grantType);
|
||||
}
|
||||
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
|
||||
return SecurityConstants.HEADER_TOC_YES.equals(header) && SecurityConstants.GRANT_MOBILE.equals(grantType);
|
||||
}
|
||||
|
||||
}
|
||||
+10
@@ -26,6 +26,8 @@ import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -36,6 +38,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class PigxUser extends User implements OAuth2AuthenticatedPrincipal {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
/**
|
||||
@@ -91,9 +94,14 @@ public class PigxUser extends User implements OAuth2AuthenticatedPrincipal {
|
||||
@Getter
|
||||
private String email;
|
||||
|
||||
/** 用户类型 */
|
||||
@Getter
|
||||
private String userType;
|
||||
|
||||
/** 密码修改时间 */
|
||||
@Getter
|
||||
private LocalDateTime passwordModifyTime;
|
||||
|
||||
/**
|
||||
* Construct the <code>User</code> with the details required by
|
||||
* {@link DaoAuthenticationProvider}.
|
||||
@@ -125,6 +133,7 @@ public class PigxUser extends User implements OAuth2AuthenticatedPrincipal {
|
||||
@JsonProperty("enabled") boolean enabled, @JsonProperty("accountNonExpired") boolean accountNonExpired,
|
||||
@JsonProperty("userType") String userType,
|
||||
@JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
|
||||
@JsonProperty("passwordModifyTime") LocalDateTime passwordModifyTime,
|
||||
@JsonProperty("accountNonLocked") boolean accountNonLocked,
|
||||
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
|
||||
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
|
||||
@@ -137,6 +146,7 @@ public class PigxUser extends User implements OAuth2AuthenticatedPrincipal {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.userType = userType;
|
||||
this.passwordModifyTime = passwordModifyTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+60
-68
@@ -40,85 +40,77 @@ import java.util.Set;
|
||||
*/
|
||||
public interface PigxUserDetailsService extends UserDetailsService, Ordered {
|
||||
|
||||
/**
|
||||
* Notfound 用户错误代码
|
||||
*/
|
||||
String NOTFOUND_USER_ERROR_CODE = "UserDetailsService.notFound";
|
||||
/**
|
||||
* Notfound 用户错误代码
|
||||
*/
|
||||
String NOTFOUND_USER_ERROR_CODE = "UserDetailsService.notFound";
|
||||
|
||||
/**
|
||||
* 是否支持此客户端校验
|
||||
* @param clientId 请求客户端
|
||||
* @param grantType 授权类型
|
||||
* @return true/false
|
||||
*/
|
||||
default boolean support(String clientId, String grantType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持此客户端校验
|
||||
*
|
||||
* @param clientId 请求客户端
|
||||
* @param grantType 授权类型
|
||||
* @return true/false
|
||||
*/
|
||||
default boolean support(String clientId, String grantType) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 排序值 默认取最大的
|
||||
* @return 排序值
|
||||
*/
|
||||
default int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序值 默认取最大的
|
||||
*
|
||||
* @return 排序值
|
||||
*/
|
||||
default int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详细信息
|
||||
*
|
||||
* @param userInfoOptional 用户信息:可选
|
||||
* @return {@link UserDetails }
|
||||
*/
|
||||
default UserDetails getUserDetails(Optional<UserInfo> userInfoOptional) {
|
||||
// @formatter:off
|
||||
/**
|
||||
* 获取用户详细信息
|
||||
* @param userInfoOptional 用户信息:可选
|
||||
* @return {@link UserDetails }
|
||||
*/
|
||||
default UserDetails getUserDetails(Optional<UserInfo> userInfoOptional) {
|
||||
// @formatter:off
|
||||
return userInfoOptional
|
||||
.map(this::convertUserDetails)
|
||||
.orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE)));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UserInfo 转 UserDetails
|
||||
*
|
||||
* @param info
|
||||
* @return 返回UserDetails对象
|
||||
*/
|
||||
default UserDetails convertUserDetails(UserInfo info) {
|
||||
Set<String> dbAuthsSet = new HashSet<>();
|
||||
/**
|
||||
* UserInfo 转 UserDetails
|
||||
* @param info
|
||||
* @return 返回UserDetails对象
|
||||
*/
|
||||
default UserDetails convertUserDetails(UserInfo info) {
|
||||
Set<String> dbAuthsSet = new HashSet<>();
|
||||
|
||||
// 维护角色列表
|
||||
info.getRoleList().forEach(role -> dbAuthsSet.add(SecurityConstants.ROLE + role.getRoleId()));
|
||||
// 维护角色列表
|
||||
info.getRoleList().forEach(role -> dbAuthsSet.add(SecurityConstants.ROLE + role.getRoleId()));
|
||||
|
||||
// 维护权限列表
|
||||
dbAuthsSet.addAll(info.getPermissions());
|
||||
Collection<GrantedAuthority> authorities = AuthorityUtils
|
||||
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
|
||||
// 维护权限列表
|
||||
dbAuthsSet.addAll(info.getPermissions());
|
||||
Collection<GrantedAuthority> authorities = AuthorityUtils
|
||||
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
|
||||
|
||||
// 构造security用户
|
||||
Long deptId = null;
|
||||
if (CollUtil.isNotEmpty(info.getDeptList())) {
|
||||
deptId = CollUtil.getFirst(info.getDeptList()).getDeptId();
|
||||
}
|
||||
return new PigxUser(info.getUserId(), info.getUsername(), deptId, info.getPhone(), info.getAvatar(),
|
||||
info.getNickname(), info.getName(), info.getEmail(), info.getTenantId(),
|
||||
SecurityConstants.BCRYPT + info.getPassword(), true, true, UserTypeEnum.TOB.getStatus(),
|
||||
!CommonConstants.STATUS_LOCK.equals(info.getPasswordExpireFlag()) // 密码过期判断
|
||||
, info.getPasswordModifyTime(), !CommonConstants.STATUS_LOCK.equals(info.getLockFlag()), authorities);
|
||||
}
|
||||
|
||||
// 构造security用户
|
||||
Long deptId = null;
|
||||
if (CollUtil.isNotEmpty(info.getDeptList())) {
|
||||
deptId = CollUtil.getFirst(info.getDeptList()).getDeptId();
|
||||
}
|
||||
return new PigxUser(info.getUserId(), info.getUsername(), deptId, info.getPhone(), info.getAvatar(),
|
||||
info.getNickname(), info.getName(), info.getEmail(), info.getTenantId(),
|
||||
SecurityConstants.BCRYPT + info.getPassword()
|
||||
, true, true, UserTypeEnum.TOB.getStatus()
|
||||
, !CommonConstants.STATUS_LOCK.equals(info.getPasswordExpireFlag()) // 密码过期判断
|
||||
, !CommonConstants.STATUS_LOCK.equals(info.getLockFlag()), authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户实体查询
|
||||
*
|
||||
* @param pigxUser user
|
||||
* @return
|
||||
*/
|
||||
default UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return this.loadUserByUsername(pigxUser.getUsername());
|
||||
}
|
||||
/**
|
||||
* 通过用户实体查询
|
||||
* @param pigxUser user
|
||||
* @return
|
||||
*/
|
||||
default UserDetails loadUserByUser(PigxUser pigxUser) {
|
||||
return this.loadUserByUsername(pigxUser.getUsername());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user