feat(auth): 增强认证逻辑,支持密码过期校验及定时攻击保护机制

This commit is contained in:
lengleng authored and pigcloud committed 2025-10-09 14:48:11 +08:00
1 parent 2551ef2606
commit 79b27b6d7d
6 files changed
+395 -240

No files matched your search

@@ -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,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;
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
@@ -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());
}
}