黎星凯
2024-05-17 3520e86e2b00b9c1ee3f4fffd4ab49fe3d6c259e
20240517修改:
关联源模块
30个文件已添加
4个文件已修改
3342 ■■■■■ 已修改文件
admin-web/public/static/config.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
consum-base/pom.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
deploy-jar-single/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/pom.xml 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationFailureHandler.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationFilter.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationProvider.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationSuccessHandler.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationToken.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultLogoutSuccessHandler.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultResourceLoaderProvider.java 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultSecuritySpi.java 397 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultUserDetails.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultUserDetailsService.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/FailedAuthenticationEntryPoint.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/JwtAuthenticationTokenFilter.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/EncryptPasswordLoginCallback.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/MobilePassCaptchaLoginCallback.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/NoneCaptchaLoginCallback.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/SimplePasswordLoginCallback.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/SmsCodeLoginCallback.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/ThirdPartyLoginCallback.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/WechatLoginCallback.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/config/SecurityProperties.java 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/config/WebSecurityConfig.java 492 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/controller/SecurityController.java 439 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/event/RoleSecurityUpdateListener.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/exception/PcUserStopAppException.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/LoginCallbackUtils.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/MockPrincipalUtils.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/ResourceLoaderUtils.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/SecurityConfigUtils.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iplatform-base-security-consum/src/test/java/com/iplatform/security/TestSecurity.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-web/public/static/config.js
@@ -9,8 +9,8 @@
  // 接口请求地址
  ftpUrl: 'http://172.16.20.9:8083/lowConsum',//开发
  apiBaseURL: 'http://172.16.20.9:8083/lowConsum',//开发
  // ftpUrl: 'http://172.16.20.9:8083/lowConsum',//开发
  // apiBaseURL: 'http://172.16.20.9:8083/lowConsum',//开发
  // apiBaseURL: 'http://172.16.60.110:8083/lowConsum',//开发
@@ -20,8 +20,8 @@
  // apiBaseURL: protocol + '//'+ host + '/lowapi',// 正式,
  // ftpUrl: protocol + '//'+ host + '/lowapi',// 正式,
  // apiBaseURL: protocol + '//'+ host + '/lowapi',// 正式,
  ftpUrl: protocol + '//'+ host + '/lowapi',// 正式,
  apiBaseURL: protocol + '//'+ host + '/lowapi',// 正式,
  debug: false //调试开关  true时会输出请求日志
};
consum-base/pom.xml
@@ -39,7 +39,7 @@
        <!-- 引入web安全认证模块,如果不引入则不会做任何权限拦截(这种模式通常可用于快速测试), 2022/10/31 -->
        <dependency>
            <groupId>com.iplatform</groupId>
            <artifactId>iplatform-base-security</artifactId>
            <artifactId>iplatform-base-security-consum</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-security-crypto</artifactId>
deploy-jar-single/src/main/resources/application.yml
@@ -1,4 +1,4 @@
spring:
  profiles:
    active: test
    active: prod
iplatform-base-security-consum/pom.xml
New file
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>iplatform</artifactId>
        <groupId>com.iplatform</groupId>
        <version>2.3.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iplatform-base-security-consum</artifactId>
    <packaging>jar</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <properties>
    </properties>
    <!--
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ~ WEB安全模块依赖 iplatform-base,作为一个可选支持模块。
    ~ 业务应用如果需要引入该模块即可,如果使用 gateway 模式则无需使用该模块。
    ~ 2022-10-26
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     -->
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 通过基础库引入: spring-security, 2022/10/31 -->
        <dependency>
            <groupId>com.walkersoft</groupId>
            <artifactId>walker-web-security</artifactId>
        </dependency>
        <!-- 平台基础模块必须存在, 2022/10/31 -->
        <dependency>
            <groupId>com.iplatform</groupId>
            <artifactId>iplatform-base</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationFailureHandler.java
New file
@@ -0,0 +1,59 @@
package com.iplatform.security;
import com.iplatform.security.exception.PcUserStopAppException;
import com.walker.infrastructure.utils.JsonUtils;
import com.walker.web.ResponseCode;
import com.walker.web.ResponseValue;
import com.walker.web.util.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public void onAuthenticationFailure(HttpServletRequest request
            , HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String message = null;  //提示信息
        int code = ResponseCode.USER_CREDENTIALS_ERROR.getCode();         //错误编码
        if(exception instanceof AccountExpiredException){
            message = ResponseCode.USER_ACCOUNT_EXPIRED.getMessage();
            logger.debug("---------> " + message);
        }else if(exception instanceof BadCredentialsException){
            message = ResponseCode.USER_CREDENTIALS_ERROR.getMessage();
        }else if(exception instanceof CredentialsExpiredException){
            message = "密码过期,登录失败!";
        }else if(exception instanceof DisabledException){
            message = ResponseCode.USER_ACCOUNT_DISABLE.getMessage();
        }else if(exception instanceof LockedException){
            message = ResponseCode.USER_ACCOUNT_LOCKED.getMessage();
        }else if(exception instanceof InternalAuthenticationServiceException){
            message = ResponseCode.USER_CREDENTIALS_ERROR.getMessage();
        }else if(exception instanceof PcUserStopAppException){
            message = PcUserStopAppException.MESSAGE;
        }else{
            message = "登录失败!";
        }
        try {
            ServletUtils.renderString(response, JsonUtils.objectToJsonString(ResponseValue.error(message)));
        } catch (Exception e) {
            logger.error("认证失败:" + request.getRequestURI(), e);
        }
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationFilter.java
New file
@@ -0,0 +1,35 @@
package com.iplatform.security;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * DefaultAuthenticationToken 重写后,登录总是报错: 用户名或密码错误,尝试重写该过滤器。
 * @author 时克英
 * @date 2023-01-27
 */
@Deprecated
public class DefaultAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//        if (this.postOnly && !request.getMethod().equals("POST")) {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username.trim() : "";
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
//            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
            DefaultAuthenticationToken authRequest = (DefaultAuthenticationToken)DefaultAuthenticationToken.unauthenticated(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationProvider.java
New file
@@ -0,0 +1,99 @@
package com.iplatform.security;
import com.iplatform.base.PlatformLoginCallback;
import com.iplatform.base.pojo.RequestLogin;
import com.iplatform.security.exception.PcUserStopAppException;
import com.iplatform.security.util.LoginCallbackUtils;
import com.walker.web.CaptchaType;
import com.walker.web.ClientType;
import com.walker.web.LoginType;
import com.walker.web.ResponseCode;
import com.walker.web.UserType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
 * 自定义认证提供者,由平台自己决定如何对比密码验证,如果不定制则spring会自动比较密码。
 * @author 时克英
 * @date 2023-01-28
 */
public class DefaultAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails
            , UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if(!(authentication instanceof DefaultAuthenticationToken)){
            throw new InternalAuthenticationServiceException("UsernamePasswordAuthenticationToken 必须是: DefaultAuthenticationToken", null);
        }
        DefaultAuthenticationToken defaultAuthenticationToken = (DefaultAuthenticationToken)authentication;
        RequestLogin requestLogin = defaultAuthenticationToken.getRequestLogin();
        // 非APP用户能否登录手机APP?可以在这里判断,2023-03-20
        if(!this.allowPcUserAccessApp){
            if(requestLogin.getClientType().equalsIgnoreCase(ClientType.MOBILE.getIndex())){
                DefaultUserDetails defaultUserDetails = (DefaultUserDetails) userDetails;
                if(defaultUserDetails.getUserPrincipal().getUserInfo().getUser_type() != UserType.TYPE_APP_REG){
                    // 登录方式为移动端,同时用户类别为非app用户,禁止登录
                    throw new PcUserStopAppException(null);
                }
            }
        }
        // 通过登录类型,获取配置的登录回调对象,委派实现密码验证。
        // 2023-12-28 移动端登录验证
        CaptchaType captchaType = CaptchaType.getType(requestLogin.getVerifyType());
        PlatformLoginCallback loginCallback = LoginCallbackUtils.getLoginCallbackBean(LoginType.getType(requestLogin.getLoginType()), true, captchaType);
        if(loginCallback == null){
            throw new InternalAuthenticationServiceException("loginCallback未找到:" + requestLogin.getLoginType());
        }
        boolean success = loginCallback.validatePassword(((DefaultUserDetails)userDetails).getUserPrincipal());
        if(!success){
            throw new BadCredentialsException(ResponseCode.USER_CREDENTIALS_ERROR.getMessage());
        }
        logger.debug("++++++++++++ 自动验证密码为正确, loginType = " + requestLogin.getLoginType());
    }
    @Override
    protected UserDetails retrieveUser(String username
            , UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        try{
            UserDetails loadedUser = this.userDetailsService.loadUserByUsername(username);
            if(loadedUser == null){
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (Exception ex){
            if(ex instanceof UsernameNotFoundException){
                logger.debug("+++++++++++++++ " + ex.getMessage());
                throw ex;
            }
            if(ex instanceof InternalAuthenticationServiceException){
                throw ex;
            }
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    /**
     * 设置是否允许'后台PC用户'访问登录手机APP
     * @param allowPcUserAccessApp
     * @date 2023-03-20
     */
    public void setAllowPcUserAccessApp(boolean allowPcUserAccessApp) {
        this.allowPcUserAccessApp = allowPcUserAccessApp;
    }
    private boolean allowPcUserAccessApp = true;
    private UserDetailsService userDetailsService;
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationSuccessHandler.java
New file
@@ -0,0 +1,38 @@
package com.iplatform.security;
import com.walker.web.TokenGenerator;
import com.walker.web.UserPrincipal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Deprecated
public class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    private TokenGenerator tokenGenerator;
    public void setTokenGenerator(TokenGenerator tokenGenerator) {
        this.tokenGenerator = tokenGenerator;
    }
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request
            , HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 1.获取通过认证用户对象
        // 2.生成Token信息
        // 3.关联Token与用户信息,并存储到缓存(内存)中
        // 4.返回客户端:用户以及Token
        logger.info("onAuthenticationSuccess = " + authentication.getPrincipal());
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultAuthenticationToken.java
New file
@@ -0,0 +1,31 @@
package com.iplatform.security;
import com.iplatform.base.pojo.RequestLogin;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
/**
 * 重写默认的认证token对象,添加自定义属性。
 * @author 时克英
 * @date 2023-01-27
 */
public class DefaultAuthenticationToken extends UsernamePasswordAuthenticationToken {
    private RequestLogin requestLogin;
    /**
     * 返回请求登录提交的原始数据
     * @return
     */
    public RequestLogin getRequestLogin() {
        return requestLogin;
    }
    public DefaultAuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }
    public DefaultAuthenticationToken(Object principal, Object credentials, RequestLogin requestLogin){
        super(principal, credentials);
        this.requestLogin = requestLogin;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultLogoutSuccessHandler.java
New file
@@ -0,0 +1,81 @@
package com.iplatform.security;
import com.iplatform.base.UserLoginCache;
import com.iplatform.base.VariableConstants;
import com.iplatform.base.callback.AfterLoginCallback;
import com.iplatform.base.callback.PlatformCallbackPostProcessor;
import com.iplatform.base.util.TokenUtils;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.ResponseValue;
import com.walker.web.TokenException;
import com.walker.web.TokenGenerator;
import com.walker.web.UserOnlineProvider;
import com.walker.web.util.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DefaultLogoutSuccessHandler implements LogoutSuccessHandler {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    private UserOnlineProvider userOnlineProvider;
    private TokenGenerator tokenGenerator;
    private UserLoginCache userLoginCache;
    public void setUserLoginCache(UserLoginCache userLoginCache) {
        this.userLoginCache = userLoginCache;
    }
    public void setTokenGenerator(TokenGenerator tokenGenerator) {
        this.tokenGenerator = tokenGenerator;
    }
    public void setUserOnlineProvider(UserOnlineProvider userOnlineProvider) {
        this.userOnlineProvider = userOnlineProvider;
    }
    @Override
    public void onLogoutSuccess(HttpServletRequest request
            , HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String token = TokenUtils.getAuthorizationToken(request);
        if(StringUtils.isNotEmpty(token)){
            try{
                String data = tokenGenerator.validateToken(token, VariableConstants.TOKEN_SECRET);
                String[] userIdAndKey = TokenUtils.getUserIdAndKey(data);
                this.userOnlineProvider.removeUserPrincipal(userIdAndKey[2]);
                // 2023-07-11 用户登录策略缓存也删除
                this.userLoginCache.removeUserLogin(userIdAndKey[1]);
                // 2023-08-18,登录成功回调
                AfterLoginCallback afterLoginCallback = PlatformCallbackPostProcessor.getCallbackObject(AfterLoginCallback.class);
                if(afterLoginCallback != null){
                    afterLoginCallback.onLogout(userIdAndKey[1]);
                }
                logger.debug("用户 logout success: " + userIdAndKey[1]);
            } catch (TokenException ex){
                logger.error("logout注销时,token解析错误:" + ex.getMessage() + ", token=" + token, ex);
                if(ex.isExpired()){
                    this.recordLogoutInfo("null", "logout", "token超时,退出操作无法删除缓存");
                } else {
                    this.recordLogoutInfo("null", "logout", "token超时,退出操作异常:" + ex.getMessage());
                }
            }
        }
        ServletUtils.renderString(response, ResponseValue.success("退出成功"));
    }
    private void recordLogoutInfo(String loginId, String status, String message){
        logger.debug("异步记录退出日志,后续要补充:" + status + ", " + message);
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultResourceLoaderProvider.java
New file
@@ -0,0 +1,257 @@
package com.iplatform.security;
import com.iplatform.base.SecurityConstants;
import com.iplatform.base.cache.MenuCacheProvider;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.Constants;
import com.walker.web.security.ResourceLoadProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 默认资源加载提供者,security框架需要的权限点集合。
 * @author 时克英
 * @date 2022-11-02
 */
public class DefaultResourceLoaderProvider implements ResourceLoadProvider {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    private MenuCacheProvider menuCacheProvider = null;
    private Map<String, Collection<ConfigAttribute>> resultMap = new HashMap<String, Collection<ConfigAttribute>>();
    /** 请求匹配对象放入Map中,避免重复创建。key = url,value = AntPathRequestMatcher */
    private Map<String, AntPathRequestMatcher> requestMatchers = new ConcurrentHashMap<String, AntPathRequestMatcher>();
    private Collection<ConfigAttribute> emptyAttributes = null;
    private Collection<ConfigAttribute> anonymousAttributes = null;
    // 2023-03-21 activiti7工作流必须拦截权限,变态。
    private Collection<ConfigAttribute> activiti7Attributes = null;
    private List<String> permitAccessUrls = null;
    private Map<String, String> anonymousUrlMap = null;
    /**
     * 设置可匿名访问的公开地址集合,如: ["/login","/register", "/image/**"]
     * @param anonymousUrlList
     */
    public void setAnonymousUrlList(List<String> anonymousUrlList) {
        if(!StringUtils.isEmptyList(anonymousUrlList)){
            this.anonymousUrlMap = new HashMap<>();
            for(String url : anonymousUrlList){
                this.anonymousUrlMap.put(url, StringUtils.EMPTY_STRING);
            }
        }
    }
    /**
     * 设置一些只要认证用户都可以访问的常用url,如: /api/**, /report/** 等
     * @param permitAccessUrls
     */
    public void setPermitAccessUrls(List<String> permitAccessUrls) {
        this.permitAccessUrls = permitAccessUrls;
    }
    public void setMenuCacheProvider(MenuCacheProvider menuCacheProvider) {
        this.menuCacheProvider = menuCacheProvider;
    }
    @Override
    public Map<String, Collection<ConfigAttribute>> loadResource() {
        resultMap.clear();
        requestMatchers.clear();
        // 1-把菜单和功能点url角色关系整理到对象中:urlRoleMap
        Map<String, List<String>> urlRoleMap = new HashMap<String, List<String>>();
        // 2-所有角色对应的URL都加入
        List<String[]> roleUrlMap = this.menuCacheProvider.getAllRoleMenuMap();
        if(roleUrlMap != null && roleUrlMap.size() > 0){
            String url = null;
            for(String[] entry : roleUrlMap){
                url = entry[1];
                if(StringUtils.isEmpty(url)){
                    throw new NullPointerException("url is not null! role = " + entry[0]);
                }
                setUrlRoleMap(urlRoleMap, url, entry[0]);
            }
        }
        // 3-超级管理员默认加上所有菜单权限
        List<String> allMenuUrl = this.menuCacheProvider.getAllMenuUrlList();
        if(!StringUtils.isEmptyList(allMenuUrl)){
            for(String url : allMenuUrl){
                setUrlRoleMap(urlRoleMap, url, SecurityConstants.ROLE_SUPER_ADMIN);
            }
        }
        setUrlRoleMap(urlRoleMap, "/supervisor/**", SecurityConstants.ROLE_SUPER_ADMIN);
//        setUrlRoleMap(urlRoleMap, "/wf/**", SecurityConstants.ROLE_SUPER_ADMIN);
        // 4-所有已登录用户,都可以访问默认公开地址: /permit
        setUrlRoleMap(urlRoleMap, "/permit/**", SecurityConstants.ROLE_USER);
        // 5-其他一些可用地址,已认证用户都可以访问,无需单独配置权限
        if(!StringUtils.isEmptyList(this.permitAccessUrls)){
            for(String url : this.permitAccessUrls){
                setUrlRoleMap(urlRoleMap, url, SecurityConstants.ROLE_USER);
//                logger.debug("公共访问资源:" + url);
//                logger.debug(urlRoleMap.toString());
          }
        }
        // 6-匿名URL不拦截
        if(this.anonymousUrlMap != null){
            for(String url : this.anonymousUrlMap.keySet()){
                setUrlRoleMap(urlRoleMap, url, Constants.ROLE_ANONYMOUS);
            }
        }
        // 7-activiti7工作流权限,必须加上。2023-03-21
        this.setUrlRoleMap(urlRoleMap, "/wf/**", Constants.ROLE_ACTIVITI_USER);
        logger.info("共加载权限点: " + urlRoleMap.size());
//        if(logger.isDebugEnabled()){
//            for(Map.Entry<String, List<String>> entry : urlRoleMap.entrySet()){
//                logger.debug(entry.getKey() + " = " + entry.getValue());
//            }
//        }
        // 6-转成spring security 特定权限属性
        if(urlRoleMap.size() > 0){
            List<ConfigAttribute> caList = null;
            for(Map.Entry<String, List<String>> entry : urlRoleMap.entrySet()){
                caList = new ArrayList<ConfigAttribute>(entry.getValue().size()+1);
                for(String s : entry.getValue()){
                    ConfigAttribute ca = new SecurityConfig(s);
                    caList.add(ca);
                }
                resultMap.put(entry.getKey(), caList);
            }
        }
        // 7-初始化请求路径匹配校验对象
        Iterator<String> it = resultMap.keySet().iterator();
        String resURL = null;
        AntPathRequestMatcher requestMatcher = null;
        while(it.hasNext()){
            resURL = it.next();
            requestMatcher = requestMatchers.get(resURL);
            // 把requestMatcher对象放到Map中,避免重复创建
            if(requestMatcher == null){
                requestMatcher = new AntPathRequestMatcher(resURL);
                requestMatchers.put(resURL, requestMatcher);
            }
        }
        return resultMap;
    }
    @Override
    public void reloadResource() {
        this.loadResource();
        logger.info("ResourceLoaderProvider reload success!");
    }
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) {
        if(resultMap == null) return getEmptyAttributes();
        Iterator<String> it = resultMap.keySet().iterator();
        String srcURL = null;
        String resURL = null;
        AntPathRequestMatcher requestMatcher = null;
        while(it.hasNext()){
            resURL = it.next();
            requestMatcher = requestMatchers.get(resURL);
            // 把requestMatcher对象放到Map中,避免重复创建
            if(requestMatcher == null){
                requestMatcher = new AntPathRequestMatcher(resURL);
                requestMatchers.put(resURL, requestMatcher);
            }
            srcURL = ((FilterInvocation)object).getRequestUrl();
            if(requestMatcher.matches(((FilterInvocation)object).getRequest())){
                logger.debug("............> 找到了匹配的资源: " + resURL + ", 请求的资源: " + srcURL);
                // 2023-03-21 activiti7修改,当访问流程相关功能,给予流程专用角色
                // 这是activiti7机制需要的,变态!
                if(resURL.startsWith(SecurityConstants.URL_WORKFLOW_PREFIX)){
                    logger.debug("返回activiti7的角色集合");
                    return this.getActiviti7Attributes();
                }
                return resultMap.get(resURL);
            }
        }
        // 2022-11-13
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // 如果通过表达式没有匹配到URL,这里要检查是否匿名路径
        // 注意:不能把判断放在前面,因为用户配置匿名地址可能带有通配符,如:/nano/**
        // 但是这里request获得的URI是特定地址,如: /nano/123
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if(this.anonymousUrlMap != null && this.anonymousUrlMap.get(srcURL) != null){
            return this.getAnonymousAttributes();
        }
        // 如果根据URL没有找到对应的角色集合,就返回一个空集合;
        // 因为spring security默认对于空集合都放行。
        return getEmptyAttributes();
    }
    @Override
    public Collection<ConfigAttribute> getAttributesByUri(String uri) {
        return null;
    }
    private void setUrlRoleMap(Map<String, List<String>> urlRoleMap, String _url, String _roleId){
        List<String> _roles = urlRoleMap.get(_url);
        if(_roles == null){
            _roles = new ArrayList<String>(8);
            _roles.add(_roleId);
            urlRoleMap.put(_url, _roles);
        } else if(!_roles.contains(_roleId)){
            _roles.add(_roleId);
        }
    }
    protected Collection<ConfigAttribute> getEmptyAttributes(){
        if(emptyAttributes == null){
            emptyAttributes = new ArrayList<ConfigAttribute>(2);
            emptyAttributes.add(new SecurityConfig(Constants.ROLE_EMPTY));
        }
        return emptyAttributes;
    }
    protected Collection<ConfigAttribute> getAnonymousAttributes(){
        if(anonymousAttributes == null){
            anonymousAttributes = new ArrayList<ConfigAttribute>(2);
            anonymousAttributes.add(new SecurityConfig(Constants.ROLE_ANONYMOUS));
        }
        return emptyAttributes;
    }
    /**
     * 返回Activiti7需要的角色集合。
     * @return
     * @date 2023-03-21
     */
    protected Collection<ConfigAttribute> getActiviti7Attributes(){
        if(this.activiti7Attributes == null){
            activiti7Attributes = new ArrayList<ConfigAttribute>(2);
            activiti7Attributes.add(new SecurityConfig(Constants.ROLE_ACTIVITI_USER));
        }
        return this.activiti7Attributes;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultSecuritySpi.java
New file
@@ -0,0 +1,397 @@
package com.iplatform.security;
import com.iplatform.base.ArgumentsConstants;
import com.iplatform.base.AsyncManager;
import com.iplatform.base.PlatformLoginCallback;
import com.iplatform.base.SecurityConstants;
import com.iplatform.base.SecuritySpi;
import com.iplatform.base.VariableConstants;
import com.iplatform.base.callback.AfterLoginCallback;
import com.iplatform.base.callback.PlatformCallbackPostProcessor;
import com.iplatform.base.config.LogProperties;
import com.iplatform.base.exception.LoginException;
import com.iplatform.base.pojo.RequestLogin;
import com.iplatform.base.service.LogServiceImpl;
import com.iplatform.base.service.LoginServiceImpl;
import com.iplatform.base.support.strategy.LoginStrategyManager;
import com.iplatform.base.util.TokenUtils;
import com.iplatform.base.util.UserUtils;
import com.iplatform.core.BeanContextAware;
import com.iplatform.model.po.S_login_info;
import com.iplatform.model.po.S_user_core;
import com.iplatform.model.po.S_user_login;
import com.iplatform.security.config.SecurityProperties;
import com.iplatform.security.util.LoginCallbackUtils;
import com.iplatform.security.util.SecurityConfigUtils;
import com.walker.cache.CacheProvider;
import com.walker.infrastructure.arguments.ArgumentsManager;
import com.walker.infrastructure.arguments.Variable;
import com.walker.infrastructure.utils.DateUtils;
import com.walker.infrastructure.utils.NumberGenerator;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.CaptchaProvider;
import com.walker.web.CaptchaResult;
import com.walker.web.CaptchaType;
import com.walker.web.Constants;
import com.walker.web.LoginType;
import com.walker.web.ResponseCode;
import com.walker.web.TokenGenerator;
import com.walker.web.UserOnlineProvider;
import com.walker.web.UserPrincipal;
import com.walker.web.WebAgentService;
import com.walker.web.WebRuntimeException;
import com.walker.web.WebUserAgent;
import com.walker.web.util.IdUtils;
import com.walker.web.util.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
public class DefaultSecuritySpi implements SecuritySpi {
    protected final transient Logger logger = LoggerFactory.getLogger(this.getClass());
    public DefaultSecuritySpi(){}
    @Override
    public boolean isAllowMobileLoginRegister(){
        return this.getSecurityProperties().isAllowMobileLoginReg();
    }
    @Override
    public Map<String, Object> login(RequestLogin requestLogin) throws LoginException {
        String username = requestLogin.getUsername();
        // 2023-01-26 为了支持多种登录方式,使用登录回调处理具体登录细节。
        // 2023-12-28 移动端登录验证
        CaptchaType captchaType = CaptchaType.getType(requestLogin.getVerifyType());
        PlatformLoginCallback loginCallback = LoginCallbackUtils.getLoginCallbackBean(LoginType.getType(requestLogin.getLoginType()), true, captchaType);
        boolean captchaEnabled = this.getArgumentVariable(ArgumentsConstants.KEY_SECURITY_CAPTCHA_ENABLED).getBooleanValue();
        // 2023-01-26 这里APP端是不需要验证码的
        if(captchaEnabled){
            CaptchaProvider<CaptchaResult> captchaProvider = loginCallback.getCaptchaProvider();
            if(captchaProvider == null){
//                return ResponseValue.error("系统需要验证码,但登录未配置:" + loginCallback.getClass().getName());
                throw new LoginException("系统需要验证码,但登录未配置:" + loginCallback.getClass().getName());
            }
            if(StringUtils.isEmpty(requestLogin.getVerifyType())){
//                return ResponseValue.error("请求错误:验证码类型为空");
                throw new LoginException("请求错误:验证码类型为空");
            }
            // 第三方对接的认证登录,不需要检测验证码类型是否与后台一致。2023-07-03
            if(captchaProvider.getCaptchaType() != CaptchaType.ThirdParty
                    && !requestLogin.getVerifyType().equals(captchaProvider.getCaptchaType().getIndex())){
                throw new LoginException("前端配置的验证码类型与后台不一致! verifyType = " + captchaProvider.getCaptchaType());
            }
            if(loginCallback.isValidateCaptcha()){
                logger.debug("需要验证码,getCaptchaType={}", loginCallback.getCaptchaProvider().getCaptchaType());
                String error = this.validateCaptcha(requestLogin.getCode(), requestLogin.getUuid(), captchaProvider);
                if(error != null){
//                    return ResponseValue.error(error);
                    throw new LoginException(error);
                }
            }
        }
        // 2023-07-11 检查登录策略是否满足
        if(this.getLogProperties().isLoginEnabled()){
            String error = this.getLoginStrategyManager().execute(requestLogin);
            if(error != null){
                throw new LoginException(error);
            }
        }
        // 用户验证
        Authentication authentication = null;
        try{
//            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            DefaultAuthenticationToken authenticationToken = new DefaultAuthenticationToken(username, requestLogin.getPassword(), requestLogin);
            // 这里放入线程,后面UserDetailsService会使用
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            authentication = this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (Exception e){
            this.recordLoginInfo(requestLogin.getUsername(), String.valueOf(ResponseCode.ERROR.getCode()), "登录未成功认证", 0, null, null);
            if(e instanceof UsernameNotFoundException){
//                throw new LoginException("用户账号不存在,或已停用:" + requestLogin.getUsername(), e, true);
                throw new LoginException("用户账号不存在,或已停用:" + requestLogin.getUsername(), null, true);
            }
            if (e instanceof BadCredentialsException){
//                return ResponseValue.error(ResponseCode.USER_CREDENTIALS_ERROR.getCode(), ResponseCode.USER_CREDENTIALS_ERROR.getMessage());
                logger.debug("++++++++++++++++++ 密码错误");
                throw new LoginException(ResponseCode.USER_CREDENTIALS_ERROR.getMessage());
            } else {
                // 2023-03-20
                // 登录验证抛出异常,会在这里统一处理,如:PcUserStopAppException。
                // DefaultAuthenticationProvider
//                return ResponseValue.error(ResponseCode.USER_CREDENTIALS_ERROR.getCode(), e.getMessage());
                throw new LoginException(e.getMessage());
            }
        }
        DefaultUserDetails userDetails = (DefaultUserDetails)authentication.getPrincipal();
        if(this.logger.isDebugEnabled()){
            logger.debug(userDetails.getUserPrincipal().toString());
        }
        // 2023-03-20,对于空验证码类型(登录方式),需要后台自动生成uuid,因为前端没有机会请求验证码获得uuid
        // 2023-07-03, 添加第三方对接的验证类型,也需要自动生成UUID,因为前端直接请求后台(目前预算一体化使用)
        CaptchaType currentCaptchaType = loginCallback.getCaptchaProvider().getCaptchaType();
        if(currentCaptchaType == CaptchaType.None
                || currentCaptchaType == CaptchaType.ThirdParty){
            requestLogin.setUuid(IdUtils.simpleUUID());
        }
        // 添加token失效时间(从配置获取),2023-03-28
        long expiredMinutes = SecurityConfigUtils.getTokenExpireMinutes(requestLogin.getClientType(), this.getSecurityProperties());
        String token = TokenUtils.generateToken(userDetails.getUserPrincipal().getId()
                , userDetails.getUsername(), requestLogin.getUuid(), this.getTokenGenerator(), expiredMinutes);
        logger.debug("token失效分钟:{}", expiredMinutes);
//        String token = this.tokenGenerator.createToken(requestLogin.getUuid(), userDetails.getUserPrincipal().getId()
//                , VariableConstants.DEFAULT_TOKEN_EXPIRED_MINUTES, VariableConstants.TOKEN_SECRET);
        // 缓存登录信息
        // 2024-01-05 登录缓存,失效时间应当长一些,系统默认30天。这和token
        long loginCacheExpiredTime = VariableConstants.DEFAULT_TOKEN_EXPIRED_MINUTES;
        if(expiredMinutes > loginCacheExpiredTime){
            loginCacheExpiredTime = expiredMinutes;
        }
        this.getUserOnlineProvider().cacheUserPrincipal(requestLogin.getUuid(), userDetails.getUserPrincipal(), loginCacheExpiredTime);
        // 把成功登录日志,与uuid关联保存放一起,在异步中调用。2023-03-23
        this.recordLoginInfo(username, String.valueOf(ResponseCode.SUCCESS.getCode()), "登录成功"
                , userDetails.getUserPrincipal().getUserInfo().getId(), requestLogin.getUuid(), requestLogin.getClientType());
        // 2023-08-18,登录成功回调
        AfterLoginCallback afterLoginCallback = PlatformCallbackPostProcessor.getCallbackObject(AfterLoginCallback.class);
        if(afterLoginCallback != null){
            afterLoginCallback.onSuccess(requestLogin, userDetails.getUserPrincipal());
        }
        // 2023-05-12,加上用户信息(商户系统用)
        Map<String, Object> param = new HashMap<>(2);
        param.put(com.walker.web.Constants.TOKEN_NAME, token);
        param.put(SecurityConstants.KEY_USER_INFO, UserUtils.acquireClientUserInfo(userDetails.getUserPrincipal().getUserInfo(), token));
        param.put(SecurityConstants.KEY_USER_INFO_APP, userDetails.getUserPrincipal());
//        return ResponseValue.success(ResponseValueUtils.acquireOneParam(com.walker.web.Constants.TOKEN_NAME, token));
        return param;
    }
    private String validateCaptcha(String code, String uuid, CaptchaProvider<CaptchaResult> captchaProvider){
        if(StringUtils.isEmpty(uuid) || StringUtils.isEmpty(code)){
//            throw new CaptchaException();
            return "请输入验证码";
        }
        CaptchaResult captchaResult = new CaptchaResult();
        captchaResult.setUuid(uuid);
        captchaResult.setCode(code);
        boolean success = captchaProvider.validateCaptcha(captchaResult);
        // 2023-04-07 调整,使用提供者判断验证码是否正确。
        // 2023-06-30 只有短信验证码时,不能删除缓存的验证码,因为存在手机端登录(并同时注册)情况,会调用两次login操作。
        if(captchaProvider.getCaptchaType() != CaptchaType.SmsCode){
            this.getCaptchaCacheProvider().removeCacheData(com.iplatform.base.Constants.CAPTCHA_CODE_PREFIX + uuid);// 删除缓存的验证码
        }
        if(!success){
            logger.error("验证码校验失败: code = " + code);
            return "验证码错误";
        }
        /*String verifyKey = Constants.CAPTCHA_CODE_PREFIX + uuid;
        String captchaCode = this.captchaCacheProvider.getCacheData(verifyKey);
        if(StringUtils.isEmpty(captchaCode)){
            return "验证码不存在,可能已经失效";
        }
        this.captchaCacheProvider.removeCacheData(verifyKey);   // 删除缓存的验证码
        if(!code.equalsIgnoreCase(captchaCode)){
            logger.error("验证码校验失败:" + captchaCode + ", code = " + code);
            return "验证码错误";
        }*/
        return null;
    }
    private void recordLoginInfo(String loginId, String status, String message, long userId, String uuid, String clientType){
        if(this.getLogProperties().isLoginEnabled()){
            logger.debug("异步记录登录日志,后续要补充:" + status + ", " + message);
            AsyncManager.me().execute(this.acquireLoginInfoTask(loginId, status, message, userId, uuid, clientType));
        }
    }
    private TimerTask acquireLoginInfoTask(String loginId, String status, String message, Long userId
            , String uuid, String clientType){
        HttpServletRequest request = ServletUtils.getRequest();
        final WebUserAgent webUserAgent = this.getWebAgentService().getWebUserAgent(request.getHeader("User-Agent"), request);
        return new TimerTask() {
            @Override
            public void run() {
                S_login_info login_info = new S_login_info();
                login_info.setLogin_time(Long.parseLong(DateUtils.getDateTimeSecondForShow()));
                login_info.setUser_name(loginId);
                login_info.setMsg(message);
                login_info.setStatus(status);
                login_info.setInfo_id(NumberGenerator.getLongSequenceNumber());
                if(webUserAgent != null){
                    login_info.setLogin_location(webUserAgent.getLocation());
                    login_info.setBrowser(webUserAgent.getBrowserName());
                    login_info.setIpaddr(webUserAgent.getIp());
                    login_info.setOs(webUserAgent.getOsName());
                }
                // 2023-03-23
                if(status.equals(String.valueOf(ResponseCode.SUCCESS.getCode()))){
                    S_user_login user_login = null;
                    if(getLoginStrategyManager().hasUserLogin(loginId)){
                        user_login = getLoginService().execUpdateUserLogin(userId, loginId, uuid, clientType, login_info, true);
                        getLoginStrategyManager().updateUserLoginCache(user_login);
                    } else {
                        // 登录成功才记录uuid关联缓存
                        user_login = getLoginService().execUpdateUserLogin(userId, loginId, uuid, clientType, login_info, false);
                        getLoginStrategyManager().putUserLoginCache(user_login);
                    }
                } else {
                    // 登录失败,仅记录登录日志
                    getLogService().execInsertLoginLog(login_info, userId);
                }
            }
        };
    }
    private AuthenticationManager getAuthenticationManager(){
        return BeanContextAware.getBeanByType(AuthenticationManager.class);
    }
    private TokenGenerator getTokenGenerator(){
        return BeanContextAware.getBeanByType(TokenGenerator.class);
    }
    private CacheProvider<String> getCaptchaCacheProvider(){
        return (CacheProvider<String>)BeanContextAware.getBeanByName(BEAN_NAME_CAPTCHA_CACHE);
    }
    private WebAgentService getWebAgentService(){
        return BeanContextAware.getBeanByType(WebAgentService.class);
    }
    private UserOnlineProvider getUserOnlineProvider(){
        return BeanContextAware.getBeanByType(UserOnlineProvider.class);
    }
    private SecurityProperties getSecurityProperties(){
        return BeanContextAware.getBeanByType(SecurityProperties.class);
    }
    private LogProperties getLogProperties(){
        return BeanContextAware.getBeanByType(LogProperties.class);
    }
    private LoginServiceImpl getLoginService(){
        return BeanContextAware.getBeanByType(LoginServiceImpl.class);
    }
    private LogServiceImpl getLogService(){
        return BeanContextAware.getBeanByType(LogServiceImpl.class);
    }
    private LoginStrategyManager getLoginStrategyManager(){
        return BeanContextAware.getBeanByType(LoginStrategyManager.class);
    }
    private static final String BEAN_NAME_CAPTCHA_CACHE = "captchaCacheProvider";
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`
    //~ 以上是登录方法抽取代码。2023-06-28
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`
    @Deprecated
    public void loginAsWorkflowRole(){
        DefaultUserDetails userDetails = this.getCurrentUserDetails();
        userDetails.addGrantedAuthority(Constants.ROLE_ACTIVITI_USER);
        logger.debug("......loginAsWorkflowRole(), {}", userDetails.getRoleIdList());
    }
    @Override
    public List<String> getCurrentUserRoleIdList(){
        DefaultUserDetails userDetails = this.getCurrentUserDetails();
        if(userDetails == null){
            logger.error("获取当前登录用户错误:getCurrentUserDetails() == null");
            return null;
        }
        return userDetails.getRoleIdList();
    }
    @Override
    public UserPrincipal<S_user_core> getCurrentUserPrincipal() {
        DefaultUserDetails userDetails = this.getCurrentUserDetails();
        if(userDetails == null){
            logger.error("获取当前登录用户错误:getCurrentUserDetails() == null");
            return null;
        }
        return userDetails.getUserPrincipal();
    }
    @Override
    public S_user_core getCurrentUser() {
        UserPrincipal<S_user_core> userPrincipal = this.getCurrentUserPrincipal();
        if(userPrincipal == null){
            throw new WebRuntimeException("当前操作未找到登录人员: userPrincipal not found in thread!");
        }
        return userPrincipal.getUserInfo();
    }
    @Override
    public long getCurrentUserId() {
        S_user_core user = this.getCurrentUser();
        return user.getId();
    }
    @Override
    public String encryptPassword(String password) {
        PasswordEncoder passwordEncoder = BeanContextAware.getBeanByType(PasswordEncoder.class);
        return passwordEncoder.encode(password);
    }
    @Override
    public boolean matchesPassword(String rawPassword, String encodedPassword) {
        PasswordEncoder passwordEncoder = BeanContextAware.getBeanByType(PasswordEncoder.class);
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
    private DefaultUserDetails getCurrentUserDetails(){
        Authentication authentication = this.getAuthentication();
        if(authentication == null){
            throw new WebRuntimeException("当前操作未找到登录认证对象: authentication not found in thread!");
        }
        Object userDetails = authentication.getPrincipal();
        if(userDetails instanceof DefaultUserDetails){
            return (DefaultUserDetails) userDetails;
        }
        return null;
    }
    /**
     * 获取Authentication
     * @author 时克英
     * @date 2022-11-10
     */
    private Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }
    /**
     * 返回系统配置可变参数对象。
     * @param key 参数 key
     * @return
     * @date 2022-11-29
     */
    private Variable getArgumentVariable(String key){
        Variable v =  BeanContextAware.getBeanByType(ArgumentsManager.class).getVariable(key);
        if(v == null){
            throw new IllegalArgumentException("可变配置参数不存在: " + key);
        }
        return v;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultUserDetails.java
New file
@@ -0,0 +1,171 @@
package com.iplatform.security;
import com.iplatform.model.po.S_user_core;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.UserPrincipal;
import com.walker.web.UserType;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * Spring Security 定义的用户细节对象,在认证以及过滤权限时使用。
 * @author 时克英
 * @date 2022-10-31
 */
public class DefaultUserDetails implements UserDetails {
    private List<GrantedAuthority> grantedList = new ArrayList<GrantedAuthority>(4);
    private List<String> roleIdList = new ArrayList<>(4);   // 用户包含角色ID集合,增加冗余看前端是否需要
    private UserPrincipal<S_user_core> userPrincipal = null;
    public DefaultUserDetails(UserPrincipal<S_user_core> userPrincipal){
        if(userPrincipal == null){
            throw new IllegalArgumentException("UserPrincipal is required!");
        }
        this.userPrincipal = userPrincipal;
    }
    public UserPrincipal<S_user_core> getUserPrincipal(){
        return this.userPrincipal;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.grantedList;
    }
    @Override
    public String getPassword() {
        return this.userPrincipal.getPassword();
    }
    @Override
    public String getUsername() {
        return this.userPrincipal.getUserName();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return !this.userPrincipal.isAccountLocked();
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return this.userPrincipal.isEnabled();
    }
    /**
     * 返回用户包含的角色ID集合。
     * @return
     * @date 2022-11-11
     */
    public List<String> getRoleIdList(){
        return this.roleIdList;
    }
    /**
     * 把角色ID集合也存入principal,因为该对象会放入缓存,而 UserDetails 不会。
     * @date 2022-11-11
     */
    public void setRoleIdListToPrincipal(){
        this.userPrincipal.setRoleIdList(this.roleIdList);
    }
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    //~ 遗留代码,看是否能用上。2022-10-31
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public boolean isSupervisor(){
        return this.userPrincipal.getUserInfo().getUser_type() == UserType.TYPE_SUPER;
    }
    private static final Map<String, GrantedAuthority> gas = new HashMap<String, GrantedAuthority>(3);
    public void addGrantedAuthority(String roleName){
//        if(roleName != null
//                && (roleName.equals(ROLE_SUPER_ADMIN) || roleName.equals(ROLE_ADMIN)
//                        || roleName.equals(ROLE_USER))){
//            this.grantedList.add(gas.get(roleName));
//        }
        if(StringUtils.isNotEmpty(roleName)){
            GrantedAuthority ga = gas.get(roleName);
            if(ga == null){
                ga = new SimpleGrantedAuthority(roleName);
                gas.put(roleName, ga);
            }
            this.grantedList.add(ga);
            //
            if(!this.roleIdList.contains(roleName)){
                this.roleIdList.add(roleName);
            }
        }
    }
    public boolean equals(Object o){
        if(o == null) return false;
        if(o == this) return true;
        if(o instanceof DefaultUserDetails){
            DefaultUserDetails _o = (DefaultUserDetails)o;
            if(_o.getUsername().equals(this.getUsername())
                    && _o.getPassword().equals(this.getPassword())
                    && _o.isEnabled() == this.isEnabled()){
                return true;
            }
        }
        return false;
    }
    public int hashCode(){
        int result = 31;
        return result + 31*this.getUsername().hashCode()
                + this.getPassword().hashCode() + 31*(this.isEnabled() ? 1 : 0);
    }
    private String ip;
    public String getIp() {
        return ip;
    }
    public void setIp(String ip) {
        this.ip = ip;
    }
//    private UserDiagram uerDiagram;
//
//    /**
//     * 返回用户具有的菜单权限图信息。
//     * @return
//     */
//    public UserDiagram getUerDiagram() {
//        return uerDiagram;
//    }
//
//    public void setUerDiagram(UserDiagram uerDiagram) {
//        this.uerDiagram = uerDiagram;
//    }
    @Override
    public String toString(){
        return new StringBuilder().append("[userDetails, name=").append(this.getUsername()).append("]").toString();
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/DefaultUserDetailsService.java
New file
@@ -0,0 +1,209 @@
package com.iplatform.security;
import com.iplatform.base.Constants;
import com.iplatform.base.DefaultUserPrincipal;
import com.iplatform.base.SecurityConstants;
import com.iplatform.base.cache.MenuCacheProvider;
import com.iplatform.base.service.UserServiceImpl;
import com.iplatform.base.util.UserUtils;
import com.iplatform.model.po.S_menu;
import com.iplatform.model.po.S_user_core;
import com.iplatform.security.config.SecurityProperties;
import com.walker.infrastructure.utils.PhoneNumberUtils;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.UserType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DefaultUserDetailsService implements UserDetailsService {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    private UserServiceImpl userService = null;
    private SecurityProperties securityProperties;
    private MenuCacheProvider menuCacheProvider;
    public void setMenuCacheProvider(MenuCacheProvider menuCacheProvider) {
        this.menuCacheProvider = menuCacheProvider;
    }
    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
//    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
//        this.passwordEncoder = passwordEncoder;
//    }
    public void setUserService(UserServiceImpl userService) {
        this.userService = userService;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        UserPrincipal<S_user_core> userPrincipal = this.acquireUserPrincipal(username);
        logger.debug("loadUserByUsername = " + username);
        DefaultUserDetails userDetails = this.acquireUserPrincipal(username);
        if(userDetails == null) {
            throw new UsernameNotFoundException("not found user: " + username);
        }
        // 被禁用的用户也不能登录
        if(!userDetails.isEnabled()) {
            throw new UsernameNotFoundException("user is forbidden: " + username);
        }
        // 验证用户
        // 2023-01-28 验证密码放在登录回调中。
//        this.validateUserInfo(userDetails.getUserPrincipal());
        return userDetails;
    }
    /*private void validateUserInfo(UserPrincipal<S_user_core> userPrincipal){
        // 2023-01-26 通过 PlatformLoginCallback 来校验密码。
        PlatformLoginCallback loginCallback = LoginCallbackUtils.getLoginCallbackBean(UserType.getType(userPrincipal.getUserInfo().getUser_type()));
        if(loginCallback == null){
            throw new ApplicationRuntimeException("未找到: loginCallback, userType = " + userPrincipal.getUserInfo().getUser_type());
        }
        boolean success = loginCallback.validatePassword(userPrincipal);
        if(!success){
            throw new BadCredentialsException(ResponseCode.USER_CREDENTIALS_ERROR.getMessage());
        }
//        Authentication usernamePasswordAuthenticationToken = SecurityContextHolder.getContext().getAuthentication();
//        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
//        if(!this.passwordEncoder.matches(password, userPrincipal.getPassword())){
//            throw new BadCredentialsException(ResponseCode.USER_CREDENTIALS_ERROR.getMessage());
//        }
        logger.debug("清空必要的缓存:或更新登录缓存");
    }*/
    public DefaultUserDetails acquireUserPrincipal(String loginId){
        if(StringUtils.isEmpty(loginId)){
            return null;
        }
        DefaultUserPrincipal userPrincipal = null;
        // 如果超级管理员,不能查数据库,直接创建对象。2022-11-06
        if(loginId.equals(Constants.SUPERVISOR_NAME_DEFAULT)){
            userPrincipal = (DefaultUserPrincipal) UserUtils.createSupervisor(this.securityProperties.getSupervisorPassword());
            DefaultUserDetails userDetails = new DefaultUserDetails(userPrincipal);
            // 2022-12-02
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_SUPER_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
//            userDetails.addGrantedAuthority(com.walker.web.Constants.ROLE_ACTIVITI_USER);
            userDetails.setRoleIdListToPrincipal();
            return userDetails;
        }
        // 2023-06-28 根据登录账号是否手机号,支持移动端和PC端手机登录方式。
        boolean isPhoneNumber = PhoneNumberUtils.isCellPhoneNumber(loginId);
        Object[] userInfo = null;
        if(isPhoneNumber && !this.securityProperties.isUserNameIsPhone()){
            // 通过手机号字段查询用户
            userInfo = this.userService.queryLoginUser(loginId, true);
        } else {
            // 通过用户名自动查询用户
            userInfo = this.userService.queryLoginUser(loginId, false);
        }
        if(userInfo == null){
            logger.debug("用户不存在: " + loginId);
            return null;
        }
        S_user_core user = (S_user_core) userInfo[0];       // 用户信息一定存在
        List<String> roleIdList = (List<String>)userInfo[1];// 角色ID集合,不一定存在
        logger.debug("找到用户: " + user.getUser_name());
        // 加入普通用户具有的角色ID,权限根据角色ID判断
        userPrincipal = new DefaultUserPrincipal(user);
        // 设置登录时间,后面要计算过期,2022-11-14
        userPrincipal.setLastLoginTime(System.currentTimeMillis());
        DefaultUserDetails userDetails = new DefaultUserDetails(userPrincipal);
        if(roleIdList != null){
            for(String roleId : roleIdList){
                userDetails.addGrantedAuthority(roleId);
            }
        }
        // 2022-12-21,用户数据权限 =========================
        // 2023-07-17,由于oracle查询报错(in (:roleIds)),暂时关闭数据权限查询
//        Map<String, String> dataScopeMap = this.acquireUserDataScopeList(roleIdList);
//        if(dataScopeMap != null){
//            logger.debug("用户数据权限集合 = " + dataScopeMap);
//            userPrincipal.setDataScopeMap(dataScopeMap);
//        }
        // ================================================
        // 2022-11-15
        int userType = userDetails.getUserPrincipal().getUserInfo().getUser_type();
        /* 设置用户的角色类型 */
        if(userType == UserType.TYPE_SUPER){
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_SUPER_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
        } else if(userType == UserType.TYPE_ADMIN || userType == UserType.TYPE_ADMIN_DEPT){
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
//        } else if(userType == UserType.TYPE_NORMAL || userType == UserType.TYPE_APP_REG){
        } else if(userType == UserType.TYPE_NORMAL){
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
        } else if(userType == UserType.TYPE_APP_REG){
            // 2023-01-27 app注册用户,不给任何角色权限
            // 2023-03-20 app注册用户,需要给'ROLE_USER',因为还需要访问公共权限(登录后都能看的如:/permit/**,/getInfo),但不给任何角色!
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
        } else if(userType == UserType.TYPE_MERCHANT_ADMIN){
            // 2023-06-06 商户管理员(泛指独立业务单位的:单位管理员)
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_MERCHANT);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
        } else
            throw new IllegalArgumentException("unknown user type: " + userType);
        // 添加工作流角色,否则activiti7老报错'无权限',2023-03-21
//        userDetails.addGrantedAuthority(com.walker.web.Constants.ROLE_ACTIVITI_USER);
        //
        userDetails.setRoleIdListToPrincipal();
        return userDetails;
    }
    /**
     * 由于oracle查询报错(in (:roleIds)),暂时关闭数据权限查询
     * @param roleIdList
     * @return
     * @date 2023-07-17
     */
    @Deprecated
    private Map<String, String> acquireUserDataScopeList(List<String> roleIdList){
        if(StringUtils.isEmptyList(roleIdList)){
            return null;
        }
        List<String> dataScopeMenuIdList = this.userService.queryUserDataScopeMenuIdList(roleIdList);
        if(StringUtils.isEmptyList(dataScopeMenuIdList)){
            return null;
        }
        Map<String, String> dataScopeMap = new HashMap<>(8);
        S_menu menu = null;
        for(String menuId : dataScopeMenuIdList){
            menu = this.menuCacheProvider.getCacheData(menuId);
            if(menu == null){
                throw new IllegalStateException("菜单缓存不存在:" + menuId);
            }
            dataScopeMap.put(menu.getParent_id(), menuId.replaceFirst(Constants.DATA_SCOPE_NAME, StringUtils.EMPTY_STRING));
        }
        return dataScopeMap;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/FailedAuthenticationEntryPoint.java
New file
@@ -0,0 +1,36 @@
package com.iplatform.security;
import com.walker.infrastructure.utils.JsonUtils;
import com.walker.web.ResponseCode;
import com.walker.web.ResponseValue;
import com.walker.web.util.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import java.io.IOException;
/**
 * 匿名用户访问无权限资源时的异常。
 * @author 时克英
 * @date 2022-10-31
 */
public class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public void commence(javax.servlet.http.HttpServletRequest request
            , javax.servlet.http.HttpServletResponse response
            , AuthenticationException authException) throws IOException, ServletException {
        String msg = "认证失败,无权限访问系统资源" + request.getRequestURI();
        try {
            ServletUtils.renderString(response, JsonUtils.objectToJsonString(ResponseValue.error(ResponseCode.EXCEPTION.getCode(), msg)));
        } catch (Exception e) {
            logger.error("无权限访问系统资源" + request.getRequestURI());
        }
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/JwtAuthenticationTokenFilter.java
New file
@@ -0,0 +1,195 @@
package com.iplatform.security;
import com.iplatform.base.DefaultUserPrincipal;
import com.iplatform.base.SecurityConstants;
import com.iplatform.base.VariableConstants;
import com.iplatform.base.util.TokenUtils;
import com.iplatform.core.TokenAwareContext;
import com.iplatform.core.TokenEntity;
import com.iplatform.model.po.S_user_core;
import com.iplatform.security.config.SecurityProperties;
import com.walker.infrastructure.ApplicationRuntimeException;
import com.walker.web.Constants;
import com.walker.web.ResponseCode;
import com.walker.web.ResponseValue;
import com.walker.web.TokenException;
import com.walker.web.TokenGenerator;
import com.walker.web.UserOnlineProvider;
import com.walker.web.UserPrincipal;
import com.walker.web.util.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    protected final transient Logger logger = LoggerFactory.getLogger(getClass());
    private TokenGenerator tokenGenerator;
    private UserOnlineProvider userOnlineProvider;
    private DefaultUserDetailsService defaultUserDetailsService;
    // 2023-03-28
    private SecurityProperties securityProperties;
    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
    public void setDefaultUserDetailsService(DefaultUserDetailsService defaultUserDetailsService) {
        this.defaultUserDetailsService = defaultUserDetailsService;
    }
    public void setUserOnlineProvider(UserOnlineProvider userOnlineProvider) {
        this.userOnlineProvider = userOnlineProvider;
    }
    public void setTokenGenerator(TokenGenerator tokenGenerator) {
        this.tokenGenerator = tokenGenerator;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request
            , HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = TokenUtils.getAuthorizationToken(request);
        if(token != null){
            try{
                String data = tokenGenerator.validateToken(token, VariableConstants.TOKEN_SECRET);
                logger.debug("token_data = " + data);
                String[] userIdAndKey = TokenUtils.getUserIdAndKey(data);
                if(userIdAndKey == null || userIdAndKey.length != 3){
                    throw new ApplicationRuntimeException("token携带用户信息解析错误:" + data);
                }
                String uuid = userIdAndKey[2];
                String loginId = userIdAndKey[1];
                DefaultUserDetails userDetails = null;
                // 根据token获取用户对象
                DefaultUserPrincipal userPrincipal = this.acquireAuthenticationUser(uuid);
                if(userPrincipal == null){
                    // 2022-11-15,这里要告诉客户端刷新token
                    // redis中缓存登录时间应小于token失效时间
                    userDetails = this.defaultUserDetailsService.acquireUserPrincipal(loginId);
                    // 2023-09-03 当前端传入的 token 是无效的情况下,如:用户已不存在,需要删除登录缓存
                    if(userDetails == null){
                        this.userOnlineProvider.removeUserPrincipal(uuid);
                        this.logger.warn("用户已不存在,删除登录状态缓存:{}", uuid);
                        return;
                    }
                    userPrincipal = (DefaultUserPrincipal)userDetails.getUserPrincipal();
                    logger.debug("token需要刷新: " + userPrincipal.getUserName());
                    this.tellClientRefreshToken(response, uuid, userPrincipal);
                    // 2022-11-15,以下响应代码不再需要,因为已经刷新token,重新缓存登录用户,
//                    ServletUtils.renderString(response, ResponseValue.error(ResponseCode.RE_LOGIN.getCode(), "token不存在或已过期,请重新认证"));
//                    return;
                } else {
                    userDetails = this.acquireUserDetails(userPrincipal);
                }
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
                // 2023-08-05
                TokenAwareContext.setCurrentToken(new TokenEntity(uuid, loginId));
            } catch (TokenException ex){
                if(ex.isExpired()){
                    ServletUtils.renderString(response, ResponseValue.error(ResponseCode.RE_LOGIN.getCode(), "token已过期,请重新获取"));
                    return;
                }
                System.out.println(ex.getTitle());
                ServletUtils.renderString(response, ResponseValue.error(ResponseCode.ERROR.getCode(), ex.getTitle()));
                return;
            } catch (Exception ex){
                logger.error("根据token获得登录信息错误:" + ex.getMessage(), ex);
                ServletUtils.renderString(response, ResponseValue.error(ResponseCode.ERROR.getCode(), ex.getMessage()));
                return;
            }
        }
        // 要尝试:token不存在不能访问
//        logger.warn("token不存在,无法访问系统更能");
        filterChain.doFilter(request, response);
        // 2023-08-05
        TokenAwareContext.clearCurrentToken();
    }
    /**
     * 通知终端刷新token,token会写入响应头中: TokenRefresh 字段。
     * @param response
     * @param uuid
     * @param userPrincipal
     */
    private void tellClientRefreshToken(HttpServletResponse response, String uuid, DefaultUserPrincipal userPrincipal){
//        String token = this.tokenGenerator.createToken(uuid, userPrincipal.getId()
//                , VariableConstants.DEFAULT_TOKEN_EXPIRED_MINUTES, VariableConstants.TOKEN_SECRET);
        // 刷新token,通常PC端会用到,移动端时间长使用几率低,因此这里找PC端配置。2023-03-28
        String token = TokenUtils.generateToken(userPrincipal.getId()
                , userPrincipal.getUserName(), uuid, this.tokenGenerator, securityProperties.getTokenExpireWeb());
        // 重新设置用户创建token时间
        userPrincipal.setLastLoginTime(System.currentTimeMillis());
        this.userOnlineProvider.cacheUserPrincipal(uuid, userPrincipal, securityProperties.getTokenExpireWeb());
        response.addHeader(Constants.TOKEN_HEADER_REFRESH, token);
        if(this.logger.isDebugEnabled()){
            logger.debug("刷新token, uuid = " + uuid + ", " + token);
        }
    }
    private DefaultUserDetails acquireUserDetails(UserPrincipal<S_user_core> userPrincipal){
        DefaultUserDetails userDetails = new DefaultUserDetails(userPrincipal);
//        if(userPrincipal.getUserInfo().isSupervisor()){
        if(userDetails.isSupervisor()){
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_SUPER_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_ADMIN);
            userDetails.addGrantedAuthority(SecurityConstants.ROLE_USER);
        } else {
            // 当前数据库只有一个普通角色:2
//            userDetails.addGrantedAuthority("2");
            // 在用户登录后,缓存的就有角色ID集合,这里每次访问重新设置到对象中。2022-11-11
            List<String> roleIdList = userPrincipal.getRoleIdList();
            logger.info("缓存中获取 userPrincipal.getRoleIdList() = " + roleIdList);
            if(roleIdList != null){
                for(String roleId : roleIdList){
                    userDetails.addGrantedAuthority(roleId);
                }
            }
        }
        return userDetails;
    }
    /**
     * 模拟测试,后续修改为数据库方式。<p></p>
     * 从缓存中读取用户登录信息,如果换成失效为空。
     * @param uuid
     * @return
     * @date 2022-11-15
     */
    private DefaultUserPrincipal acquireAuthenticationUser(String uuid){
        /*String userId = userIdAndKey[0];
        if(userId.equals("0")){
            // 0 为超级管理员
            return MockPrincipalUtils.createSupervisor();
        } else {
            // 其他值模拟为普通用户
            return MockPrincipalUtils.createNormalUser("100");
        }*/
        // uuid
//        String loginKey = userIdAndKey[2];
        return (DefaultUserPrincipal)this.userOnlineProvider.getUserPrincipal(uuid);
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/EncryptPasswordLoginCallback.java
New file
@@ -0,0 +1,57 @@
package com.iplatform.security.callback;
import com.iplatform.base.util.PlatformRSAUtils;
import com.iplatform.model.po.S_user_core;
import com.walker.web.CaptchaType;
import com.walker.web.UserPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
/**
 * 移动端通过:账号、密码方式登录,<br>
 * 不需要任何验证码。
 * @author 时克英
 * @date 2023-03-20
 * @date 2023-04-07 修改名字为'密码加密'登录回调实现。
 */
public class EncryptPasswordLoginCallback extends
//        GeneralLoginCallback
        SimplePasswordLoginCallback
{
//    @Deprecated
//    @Override
//    public Object[] queryLoginUser(String loginId) {
//        return new Object[0];
//    }
    @Override
    public boolean validatePassword(UserPrincipal<S_user_core> userPrincipal) {
        Authentication usernamePasswordAuthenticationToken = SecurityContextHolder.getContext().getAuthentication();
        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
        try {
            // APP传过来的密码是RSA加密内容。
//            byte[] data = RSAUtil.decryptByPrivateKey(password.getBytes(StandardCharsets.UTF_8), PRIK);
//            String pass = new String(data);
            // 2023-06-30 目前电商移动端登录:手机号 + 密码,使用的是明文密码,后续要改成一致。
            String originPassword = null;
            if(this.getCaptchaProvider().getCaptchaType() == CaptchaType.None){
//                originPassword = password;
                // 2023-08-06 uniapp端使用对称密码算法
                originPassword = PlatformRSAUtils.getAesDecryptValue(password);
            } else {
                originPassword = PlatformRSAUtils.getRsaDecryptValue(password, PlatformRSAUtils.PRIK);
            }
            logger.debug("......... 解密的password = {}", originPassword);
            return this.passwordEncoder.matches(originPassword, userPrincipal.getPassword());
        } catch (Exception e) {
            logger.error("", e);
            throw new RuntimeException("解析登录密码错误:" + e.getMessage(), e);
        }
    }
//    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
//        this.passwordEncoder = passwordEncoder;
//    }
//    private PasswordEncoder passwordEncoder = null;
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/MobilePassCaptchaLoginCallback.java
New file
@@ -0,0 +1,31 @@
package com.iplatform.security.callback;
import com.iplatform.base.util.PlatformRSAUtils;
import com.iplatform.model.po.S_user_core;
import com.walker.web.UserPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
/**
 * 移动端(H5),使用:用户名、密码(AES) + 普通验证码登录方式。
 * <p>因为移动端与PC端密码加密方式不一致,所以无法复用PC登录验证模式,因此重新定义!</p>
 * @author 时克英
 * @date 2023-12-28
 */
public class MobilePassCaptchaLoginCallback extends EncryptPasswordLoginCallback{
    @Override
    public boolean validatePassword(UserPrincipal<S_user_core> userPrincipal) {
        Authentication usernamePasswordAuthenticationToken = SecurityContextHolder.getContext().getAuthentication();
        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
        try {
            // 移动端,只支持AES密码加密,目前情况。2023-12-28
            String originPassword = PlatformRSAUtils.getAesDecryptValue(password);
            logger.debug("......... 解密的password = {}", originPassword);
            return this.passwordEncoder.matches(originPassword, userPrincipal.getPassword());
        } catch (Exception e) {
            logger.error("", e);
            throw new RuntimeException("解析登录密码错误:" + e.getMessage(), e);
        }
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/NoneCaptchaLoginCallback.java
New file
@@ -0,0 +1,11 @@
package com.iplatform.security.callback;
/**
 * 不带验证码类型的(账户、密码)登录方式扩展。
 * <p>仅为了定义一种类型,没有任何代码可重写</p>
 * @author 时克英
 * @date 2023-04-07
 */
public class NoneCaptchaLoginCallback extends EncryptPasswordLoginCallback{
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/SimplePasswordLoginCallback.java
New file
@@ -0,0 +1,39 @@
package com.iplatform.security.callback;
import com.iplatform.base.callback.GeneralLoginCallback;
import com.iplatform.model.po.S_user_core;
import com.walker.web.UserPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
 * <s>PC端网页登录回调实现。</s><p></p>
 * 用户名密码登录方式回调实现。
 * @author 时克英
 * @date 2023-01-26
 * @date 2023-01-28 用户名、密码方式登录的回调实现。
 * @date 2023-04-07 重构名字为'明文密码'登录回调实现。参考:{@linkplain EncryptPasswordLoginCallback}
 */
public class SimplePasswordLoginCallback extends GeneralLoginCallback {
    @Deprecated
    @Override
    public Object[] queryLoginUser(String loginId) {
        return this.userService.queryLoginUser(loginId, false);
    }
    @Override
    public boolean validatePassword(UserPrincipal<S_user_core> userPrincipal) {
        Authentication usernamePasswordAuthenticationToken = SecurityContextHolder.getContext().getAuthentication();
        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
        logger.debug("用户输入:" + password + ", " + userPrincipal.getPassword());
        return this.passwordEncoder.matches(password, userPrincipal.getPassword());
    }
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
    protected PasswordEncoder passwordEncoder = null;
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/SmsCodeLoginCallback.java
New file
@@ -0,0 +1,76 @@
package com.iplatform.security.callback;
import com.iplatform.base.Constants;
import com.iplatform.base.callback.GeneralLoginCallback;
import com.iplatform.model.po.S_user_core;
import com.walker.cache.CacheProvider;
import com.walker.web.UserPrincipal;
/**
 * APP方式登录回调实现。
 * @author 时克英
 * @date 2023-01-26
 * @date 2023-01-28 短信验证码登录方式回调
 */
public class SmsCodeLoginCallback extends GeneralLoginCallback {
    /**
     * 重写方法,短信验证码登录,需要首先验证。否则在移动端(特别是用户不存在,还没注册情况下)直接跳过并调用接口。
     * @return
     * @date 2023-06-30
     */
    @Override
    public boolean isValidateCaptcha() {
         /*APP登录提交不需要使用验证码,仅在接收短信时使用
         短信验证码作为密码使用,所以没有验证码了!*/
        return true;
    }
    /**
     * 暂未使用到该方法,看后续情况。
     * @param loginId
     * @return
     */
    @Deprecated
    @Override
    public Object[] queryLoginUser(String loginId) {
        Object[] userInfo = new Object[2];
        S_user_core userCore =  this.userService.queryLoginUserOnly(loginId);
        userInfo[0] = userCore;
        userInfo[1] = null;
        return userInfo;
    }
    /**
     * 验证密码实现。
     * @param userPrincipal
     * @return
     * @date 2023-06-30 重新修改,由于短信验证码仍然必须提前校验,所以现在这里就无需作为密码。
     */
    @Override
    public boolean validatePassword(UserPrincipal<S_user_core> userPrincipal) {
//        logger.warn("App登录密码验证: 可能是短信或者密码,根据业务情况实现,这里'默认验证通过'");
        /*DefaultAuthenticationToken authenticationToken = (DefaultAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
        RequestLogin requestLogin = authenticationToken.getRequestLogin();
        String smsCode = this.getSmsCode(requestLogin.getUuid());
        if(StringUtils.isNotEmpty(smsCode) && requestLogin.getPassword().equals(smsCode)){
            logger.debug("app验证密码成功,smsCode={}, password={}", smsCode, requestLogin.getPassword());
            return true;
        }
        logger.error("app登录验证码错误,smsCode = " + smsCode + ", inputPass = " + requestLogin.getPassword());
        return false;*/
        return true;
    }
    private String getSmsCode(String uuid){
        String verifyKey = Constants.CAPTCHA_CODE_PREFIX + uuid;
        // 缓存中获取之前生成的短信验证码
        return this.captchaCacheProvider.getCacheData(verifyKey);
    }
    public void setCaptchaCacheProvider(CacheProvider<String> captchaCacheProvider) {
        this.captchaCacheProvider = captchaCacheProvider;
    }
    private CacheProvider<String> captchaCacheProvider;
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/ThirdPartyLoginCallback.java
New file
@@ -0,0 +1,33 @@
package com.iplatform.security.callback;
import com.iplatform.model.po.S_user_core;
import com.walker.web.UserPrincipal;
/**
 * 第三方对接登录回调实现。
 * <pre>
 *     1)该实现目前仅为预算一体化对接实验用,具体需要看后面更多情况。
 *     2)对接无需验证码,并且采用密码明文方式,因为都是后台操作,不存在风险。
 * </pre>
 * @author 时克英
 * @date 2023-07-03
 */
public class ThirdPartyLoginCallback extends SimplePasswordLoginCallback{
    @Override
    public boolean isValidateCaptcha() {
        // 第三方对接,无验证码。暂时为预算一体化对接使用。
        return false;
    }
    /**
     * 第三方(无需)验证密码。
     * @param userPrincipal
     * @return
     * @date 2023-10-23 新添加方法,重写验证密码,由于第三方会给我们用户信息,这里一般不需要验证密码。
     */
    @Override
    public boolean validatePassword(UserPrincipal<S_user_core> userPrincipal) {
        return true;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/callback/WechatLoginCallback.java
New file
@@ -0,0 +1,34 @@
package com.iplatform.security.callback;
import com.iplatform.model.po.S_user_core;
import com.walker.web.UserPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
/**
 * 微信授权登录,目前公众号H5自动登录使用。
 * <pre>
 *     1)前端处理微信对接获取accessToken;
 *     2)调用后台接口登录,后台组织参数,密码从数据库查询出来,
 *     3)因此密码直接就是加密的,直接比对就行(相等的)
 * </pre>
 * @author 时克英
 * @date 2023-07-27
 */
public class WechatLoginCallback extends SimplePasswordLoginCallback{
    @Override
    public boolean validatePassword(UserPrincipal<S_user_core> userPrincipal) {
        Authentication usernamePasswordAuthenticationToken = SecurityContextHolder.getContext().getAuthentication();
        String password = usernamePasswordAuthenticationToken.getCredentials().toString();
        logger.debug("用户输入:" + password + ", " + userPrincipal.getPassword());
        // 微信登录,传过来的密码就是数据库中查询的,因为后台代码调用登录!
        return password.equals(userPrincipal.getPassword());
    }
    @Override
    public boolean isValidateCaptcha() {
        // 无验证码。
        return false;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/config/SecurityProperties.java
New file
@@ -0,0 +1,172 @@
package com.iplatform.security.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@ConfigurationProperties(prefix = "iplatform.security")
public class SecurityProperties {
    private List<String> anonymousList = null;
    private List<String> permitList = null;
    private String supervisorPassword = null;
    private boolean corsEnabled = false;
    private boolean allowPcUserAccessApp = true;
    private long tokenExpireWeb = 120;              // PC端默认2小时
    private long tokenExpireMobile = 60 * 24 * 15;  // 移动端默认15天
    // 支持手机登录时自动注册(手机不存在则,直接注册)
    private boolean allowMobileLoginReg = false;
    private boolean userNameIsPhone = false;
    /**
     * 用户体系(用户名)都是手机号。
     * @return
     * @date 2023-06-28
     */
    public boolean isUserNameIsPhone() {
        return userNameIsPhone;
    }
    public void setUserNameIsPhone(boolean userNameIsPhone) {
        this.userNameIsPhone = userNameIsPhone;
    }
    /**
     * 支持手机登录时自动注册(手机不存在则,直接注册)
     * @return
     * @date 2023-06-28
     */
    public boolean isAllowMobileLoginReg() {
        return allowMobileLoginReg;
    }
    public void setAllowMobileLoginReg(boolean allowMobileLoginReg) {
        this.allowMobileLoginReg = allowMobileLoginReg;
    }
    /**
     * 返回PC端token失效时间(分钟),默认:120
     * @return
     * @date 2023-03-28
     */
    public long getTokenExpireWeb() {
        return tokenExpireWeb;
    }
    public void setTokenExpireWeb(long tokenExpireWeb) {
        this.tokenExpireWeb = tokenExpireWeb;
    }
    /**
     * 返回移动端token失效时间(分钟),默认:60 * 24 * 15 (15天)
     * @return
     * @date 2023-03-28
     */
    public long getTokenExpireMobile() {
        return tokenExpireMobile;
    }
    public void setTokenExpireMobile(long tokenExpireMobile) {
        this.tokenExpireMobile = tokenExpireMobile;
    }
    public boolean isAllowPcUserAccessApp() {
        return allowPcUserAccessApp;
    }
    /**
     * 设置是否允许'后台PC用户'访问登录手机APP
     * @param allowPcUserAccessApp
     * @date 2023-03-20
     */
    public void setAllowPcUserAccessApp(boolean allowPcUserAccessApp) {
        this.allowPcUserAccessApp = allowPcUserAccessApp;
    }
    /**
     * 是否启用跨域配置,在 Gateway 模式下,由于网关已经配置,这里业务就不需要重复配置了。
     * @return
     * @date 2022-12-28
     */
    public boolean isCorsEnabled() {
        return corsEnabled;
    }
    public void setCorsEnabled(boolean corsEnabled) {
        this.corsEnabled = corsEnabled;
    }
    /**
     * 获取超级管理员密码(秘文)
     * @return
     * @date 2022-12-02
     */
    public String getSupervisorPassword() {
        return supervisorPassword;
    }
    public void setSupervisorPassword(String supervisorPassword) {
        this.supervisorPassword = supervisorPassword;
    }
    /**
     * 获得配置的已认证用户都能访问的地址集合。
     * @return
     * @date 2022-11-13
     */
    public List<String> getPermitList() {
        return permitList;
    }
    public void setPermitList(List<String> permitList) {
        this.permitList = permitList;
    }
    /**
     * 获得配置的匿名访问地址集合
     * @return
     */
    public List<String> getAnonymousList() {
        return anonymousList;
    }
    public void setAnonymousList(List<String> anonymousList) {
        this.anonymousList = anonymousList;
    }
    /**
     * 用户名密码方式登录,配置的验证码提供者。
     * @return
     * @date 2023-03-14
     */
    public String getLoginCaptchaUserPass() {
        return loginCaptchaUserPass;
    }
    public void setLoginCaptchaUserPass(String loginCaptchaUserPass) {
        this.loginCaptchaUserPass = loginCaptchaUserPass;
    }
    /**
     * 手机短信登录方式,配置的验证码提供者。
     * @return
     * @date 2023-03-14
     */
    public String getLoginCaptchaSmsCode() {
        return loginCaptchaSmsCode;
    }
    public void setLoginCaptchaSmsCode(String loginCaptchaSmsCode) {
        this.loginCaptchaSmsCode = loginCaptchaSmsCode;
    }
    private String loginCaptchaUserPass = null;
    private String loginCaptchaSmsCode = null;
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/config/WebSecurityConfig.java
New file
@@ -0,0 +1,492 @@
package com.iplatform.security.config;
import com.iplatform.base.UserCacheProvider;
import com.iplatform.base.UserLoginCache;
import com.iplatform.base.cache.MenuCacheProvider;
import com.iplatform.base.captcha.JigsawCaptchaProvider;
import com.iplatform.base.captcha.NoneCaptchaProvider;
import com.iplatform.base.captcha.ThirdPartyCaptchaProvider;
import com.iplatform.base.service.UserServiceImpl;
import com.iplatform.base.util.UserUtils;
import com.iplatform.core.PlatformConfiguration;
import com.iplatform.model.po.S_user_core;
import com.iplatform.security.DefaultAuthenticationFailureHandler;
import com.iplatform.security.DefaultAuthenticationProvider;
import com.iplatform.security.DefaultLogoutSuccessHandler;
import com.iplatform.security.DefaultResourceLoaderProvider;
import com.iplatform.security.DefaultUserDetailsService;
import com.iplatform.security.FailedAuthenticationEntryPoint;
import com.iplatform.security.JwtAuthenticationTokenFilter;
import com.iplatform.security.callback.EncryptPasswordLoginCallback;
import com.iplatform.security.callback.MobilePassCaptchaLoginCallback;
import com.iplatform.security.callback.NoneCaptchaLoginCallback;
import com.iplatform.security.callback.SmsCodeLoginCallback;
import com.iplatform.security.callback.ThirdPartyLoginCallback;
import com.iplatform.security.callback.WechatLoginCallback;
import com.iplatform.security.event.RoleSecurityUpdateListener;
import com.iplatform.security.util.SecurityConfigUtils;
import com.walker.cache.CacheProvider;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.CaptchaProvider;
import com.walker.web.CaptchaResult;
import com.walker.web.TokenGenerator;
import com.walker.web.UserOnlineProvider;
import com.walker.web.UserPrincipal;
import com.walker.web.security.DefaultAccessDecisionManager;
import com.walker.web.security.DefaultAccessDeniedHandler;
import com.walker.web.security.DefaultSecurityMetadataSource;
import com.walker.web.security.ResourceLoadProvider;
import com.walker.web.token.JwtTokenGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.List;
@Configuration
public class WebSecurityConfig extends PlatformConfiguration {
    private MenuCacheProvider menuCacheProvider;
    private UserServiceImpl userService;
    private UserOnlineProvider userOnlineProvider;
    private UserCacheProvider userCacheProvider;
    private UserLoginCache userLoginCache;
    @Autowired
    public WebSecurityConfig(MenuCacheProvider menuCacheProvider
            , UserServiceImpl userService, UserOnlineProvider userOnlineProvider, UserCacheProvider userCacheProvider
            , UserLoginCache userLoginCache){
        this.menuCacheProvider = menuCacheProvider;
        this.userService = userService;
        this.userOnlineProvider = userOnlineProvider;
        this.userCacheProvider = userCacheProvider;
        // 2023-07-11
        this.userLoginCache = userLoginCache;
    }
    @Bean
    public SecurityProperties securityProperties(){
        return new SecurityProperties();
    }
    /**
     * HttpSecurity:忽略 antMatchers 中使用的端点的身份验证,其他安全功能将生效。<br></br>
     * WebSecurity:直接忽略也不会进行 CSRF xss等攻击保护。
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        DefaultUserDetailsService userDetailsService = userDetailsService(this.securityProperties(), this.userCacheProvider);
        http.userDetailsService(userDetailsService);
        // CSRF禁用,因为不使用session
        http.csrf().disable();
        // ???
        http.headers().frameOptions().disable();
        // 登录行为由自己实现,参考 AuthController#login
        http.formLogin().disable().httpBasic().disable();
        // 匿名资源访问权限,返回无权限提示接口
        http.exceptionHandling().authenticationEntryPoint(failedAuthenticationEntryPoint())
                // 已认证用户无权限访问配置
                .accessDeniedHandler(this.accessDeniedHandler())
                .and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//        http.formLogin().loginProcessingUrl("/login")
//                        .failureHandler(this.authenticationFailureHandler());
        // 注意:这里不能配置上面的登录,否则就不会执行自己实现的/login方法。2022-11-11
        http.logout().logoutUrl("/logout").logoutSuccessHandler(this.logoutSuccessHandler()).permitAll();
        // 匿名访问集合,2022-11-07
        List<String> anonymousList = this.securityProperties().getAnonymousList();
        if(!StringUtils.isEmptyList(anonymousList)){
            http.authorizeHttpRequests().antMatchers(anonymousList.toArray(new String[]{})).permitAll();
        }
//        http.authorizeHttpRequests().antMatchers("/login", "/register", "/captchaImage", "/test/**").permitAll();
//        http.authorizeHttpRequests().antMatchers("/static/**", "/test/**").permitAll();
//        http.authorizeHttpRequests().antMatchers("/security/**").hasAuthority("query_user");
        // 2023-03-21 注释掉,调试activiti7时发现和下面重复,
        // http.addFilterBefore(securityInterceptor(), FilterSecurityInterceptor.class);
        /*http.authorizeHttpRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>(){
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                object.setAccessDecisionManager(accessDecisionManager());//决策管理器
                object.setSecurityMetadataSource(securityMetadataSource());//安全元数据源
                return object;
            }
        });*/
        // 2023-01-28 配置自定义认证提供者(密码验证用)
        http.authenticationProvider(this.authenticationProvider(userDetailsService, securityProperties()));
        // 所有请求都需要认证
        http.authorizeHttpRequests().anyRequest().authenticated();
        // 使用自定义动态拦截器,拦截所有权限请求,2022-11-02
        http.addFilterBefore(securityInterceptor(), FilterSecurityInterceptor.class);
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // token拦截过滤器,2022-11-02
        // 必须在这里添加拦截,不能放在'FilterSecurityInterceptor'之后,因为如果放在之后,那么就无法获得用户信息,从而无法
        // 获得用户所具有的权限角色集合:roleIdList。2022-11-14(2)
        http.addFilterBefore(jwtAuthenticationTokenFilter(userDetailsService), UsernamePasswordAuthenticationFilter.class);
//        http.addFilterBefore(jwtAuthenticationTokenFilter(), DefaultAuthenticationFilter.class);
        // 尝试让jwt在URL权限之后才拦截, 2022-11-14(1)
        // 注意:以上 UsernamePasswordAuthenticationFilter 需要去掉才能生效
//        http.addFilterAfter(jwtAuthenticationTokenFilter(), FilterSecurityInterceptor.class);
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if(this.securityProperties().isCorsEnabled()){
            // 解决跨域过滤器,2022-11-06
            http.addFilterBefore(this.corsFilter().getFilter(), JwtAuthenticationTokenFilter.class);
            // 未知?2022-11-11
            http.addFilterBefore(this.corsFilter().getFilter(), LogoutFilter.class);
        } else {
            System.out.println("不添加跨域过滤器: ");
        }
        return http.build();
    }
    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    @Bean
    public DefaultUserDetailsService userDetailsService(SecurityProperties securityProperties, UserCacheProvider userCacheProvider){
        DefaultUserDetailsService userDetailsService = new DefaultUserDetailsService();
        userDetailsService.setUserService(this.userService);
//        userDetailsService.setPasswordEncoder(passwordEncoder());
        userDetailsService.setSecurityProperties(securityProperties);
        userDetailsService.setMenuCacheProvider(this.menuCacheProvider);
        System.out.println("create UserDetailsService = " + userDetailsService);
        // 2023-05-07,这里把超级管理员初始化到缓存
        UserPrincipal<S_user_core> userPrincipal = UserUtils.createSupervisor(securityProperties.getSupervisorPassword());
        userCacheProvider.putUser(userPrincipal.getUserInfo());
        return userDetailsService;
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 匿名用户无法访问异常返回
     * @return
     */
    @Bean
    public AuthenticationEntryPoint failedAuthenticationEntryPoint(){
        return new FailedAuthenticationEntryPoint();
    }
    /**
     * 已认证用户拒绝访问资源。
     * @return
     */
    @Bean
    public AccessDeniedHandler accessDeniedHandler(){
        return new DefaultAccessDeniedHandler();
    }
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler(){
        return new DefaultAuthenticationFailureHandler();
    }
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler(){
        DefaultLogoutSuccessHandler logoutSuccessHandler = new DefaultLogoutSuccessHandler();
        logoutSuccessHandler.setUserOnlineProvider(this.userOnlineProvider);
        logoutSuccessHandler.setTokenGenerator(this.tokenGenerator());
        logoutSuccessHandler.setUserLoginCache(this.userLoginCache);
        return logoutSuccessHandler;
    }
    @Bean
    public AccessDecisionManager accessDecisionManager(){
        return new DefaultAccessDecisionManager();
    }
    @Bean
    public DefaultSecurityMetadataSource securityMetadataSource(ResourceLoadProvider resourceLoadProvider){
//        DefaultResourceLoaderProvider resourceLoaderProvider = new DefaultResourceLoaderProvider();
//        resourceLoaderProvider.setMenuCacheProvider(this.menuCacheProvider);
//        resourceLoaderProvider.setPermitAccessUrls(this.securityProperties().getPermitList());
//        resourceLoaderProvider.setAnonymousUrlList(this.securityProperties().getAnonymousList());
//        resourceLoaderProvider.loadResource();
        DefaultSecurityMetadataSource securityMetadataSource = new DefaultSecurityMetadataSource();
        securityMetadataSource.setResourceLoaderProvider(resourceLoadProvider);
        return securityMetadataSource;
    }
    @Bean
    public FilterSecurityInterceptor securityInterceptor(){
//        DefaultSecurityInterceptor securityInterceptor = new DefaultSecurityInterceptor();
        FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
        securityInterceptor.setSecurityMetadataSource(securityMetadataSource(this.resourceLoadProvider()));
        securityInterceptor.setAccessDecisionManager(accessDecisionManager());
        logger.info("创建:FilterSecurityInterceptor");
        return securityInterceptor;
    }
    /**
     * 定义token过滤器
     * @return
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(DefaultUserDetailsService userDetailsService){
        JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter = new JwtAuthenticationTokenFilter();
        jwtAuthenticationTokenFilter.setTokenGenerator(tokenGenerator());
        jwtAuthenticationTokenFilter.setUserOnlineProvider(this.userOnlineProvider);
        jwtAuthenticationTokenFilter.setDefaultUserDetailsService(userDetailsService);
        jwtAuthenticationTokenFilter.setSecurityProperties(this.securityProperties());
        return jwtAuthenticationTokenFilter;
    }
    @Bean
    public TokenGenerator tokenGenerator(){
        return new JwtTokenGenerator();
    }
    /**
     * 微信登录回调实现。
     * @param passwordEncoder
     * @param tokenGenerator
     * @return
     * @date 2023-07-27
     */
    @Bean
    public WechatLoginCallback wechatLoginCallback(PasswordEncoder passwordEncoder, TokenGenerator tokenGenerator){
        WechatLoginCallback callback = new WechatLoginCallback();
        callback.setTokenGenerator(tokenGenerator);
        callback.setPasswordEncoder(passwordEncoder);
        callback.setCaptchaProvider(new NoneCaptchaProvider());
        return callback;
    }
    /**
     * 第三方的对接登录回调实现。(为预算一体化对接使用,更多需要抽象)
     * @param passwordEncoder
     * @param tokenGenerator
     * @return
     * @date 2023-07-03
     */
    @Bean
    public ThirdPartyLoginCallback thirdPartyLoginCallback(PasswordEncoder passwordEncoder, TokenGenerator tokenGenerator){
        ThirdPartyLoginCallback callback = new ThirdPartyLoginCallback();
        callback.setTokenGenerator(tokenGenerator);
        callback.setPasswordEncoder(passwordEncoder);
        callback.setCaptchaProvider(new ThirdPartyCaptchaProvider());
        return callback;
    }
    /**
     * 移动端使用:用户名、密码(AES) + 普通验证码登录方式。
     * @param passwordEncoder
     * @param tokenGenerator
     * @param securityProperties
     * @param smsCaptchaProvider
     * @param imageCaptchaProvider
     * @param jigsawCaptchaProvider
     * @return
     * @date 2023-12-28
     */
    @Bean
    public MobilePassCaptchaLoginCallback mobilePassCaptchaLoginCallback(PasswordEncoder passwordEncoder
            , TokenGenerator tokenGenerator, SecurityProperties securityProperties
            , CaptchaProvider<CaptchaResult> smsCaptchaProvider
            , CaptchaProvider<CaptchaResult> imageCaptchaProvider
            , JigsawCaptchaProvider jigsawCaptchaProvider){
        MobilePassCaptchaLoginCallback loginCallback = new MobilePassCaptchaLoginCallback();
        loginCallback.setTokenGenerator(tokenGenerator);
        loginCallback.setPasswordEncoder(passwordEncoder);
        // 配置组装验证码提供者,2023-12-28
        CaptchaProvider<CaptchaResult> captchaProvider = SecurityConfigUtils
                .findCaptchaProvider(securityProperties.getLoginCaptchaUserPass()
                        , smsCaptchaProvider, imageCaptchaProvider, jigsawCaptchaProvider);
        loginCallback.setCaptchaProvider(captchaProvider);
        return loginCallback;
    }
    /**
     * 账号、密码(加密方式)登录的回调实现,不包含:用户验证码。<p>适合在移动端使用</p>
     * @param passwordEncoder
     * @param tokenGenerator
     * @return
     * @date 2023-03-20
     */
    @Bean
    public NoneCaptchaLoginCallback noneCaptchaPasswordLoginCallback(PasswordEncoder passwordEncoder
            , TokenGenerator tokenGenerator){
        NoneCaptchaLoginCallback encryptPasswordLoginCallback = new NoneCaptchaLoginCallback();
        encryptPasswordLoginCallback.setTokenGenerator(tokenGenerator);
        encryptPasswordLoginCallback.setPasswordEncoder(passwordEncoder);
        encryptPasswordLoginCallback.setCaptchaProvider(new NoneCaptchaProvider());
        return encryptPasswordLoginCallback;
    }
    /**
     * 用户名、密码(明文)登录方式回调实现,包含:验证码组件,适合PC端使用。
     * @param tokenGenerator
     * @param passwordEncoder
     * @return
     * @date 2023-01-26
     */
    @Bean
    public EncryptPasswordLoginCallback captchaPasswordLoginCallback(TokenGenerator tokenGenerator
            , PasswordEncoder passwordEncoder, SecurityProperties securityProperties
            , CaptchaProvider<CaptchaResult> smsCaptchaProvider
            , CaptchaProvider<CaptchaResult> imageCaptchaProvider
            , JigsawCaptchaProvider jigsawCaptchaProvider){
//        SimplePasswordLoginCallback webLoginCallback = new SimplePasswordLoginCallback();
        EncryptPasswordLoginCallback webLoginCallback = new EncryptPasswordLoginCallback();
        webLoginCallback.setTokenGenerator(tokenGenerator);
        webLoginCallback.setUserOnlineProvider(this.userOnlineProvider);
        webLoginCallback.setUserService(this.userService);
        webLoginCallback.setPasswordEncoder(passwordEncoder);
        // 配置组装验证码提供者,2023-03-14
        CaptchaProvider<CaptchaResult> captchaProvider = SecurityConfigUtils
                .findCaptchaProvider(securityProperties.getLoginCaptchaUserPass()
                        , smsCaptchaProvider, imageCaptchaProvider, jigsawCaptchaProvider);
        webLoginCallback.setCaptchaProvider(captchaProvider);
        return webLoginCallback;
    }
    /**
     * 短信验证码登录方式回调实现。
     * @param tokenGenerator
     * @param captchaCacheProvider
     * @return
     * @date 2023-01-26
     */
    @Bean
    public SmsCodeLoginCallback smsCodeLoginCallback(TokenGenerator tokenGenerator
            , CacheProvider<String> captchaCacheProvider, SecurityProperties securityProperties
            , CaptchaProvider<CaptchaResult> smsCaptchaProvider
            , CaptchaProvider<CaptchaResult> imageCaptchaProvider
            , JigsawCaptchaProvider jigsawCaptchaProvider){
        SmsCodeLoginCallback smsCodeLoginCallback = new SmsCodeLoginCallback();
        smsCodeLoginCallback.setTokenGenerator(tokenGenerator);
        smsCodeLoginCallback.setUserOnlineProvider(this.userOnlineProvider);
        smsCodeLoginCallback.setUserService(this.userService);
        smsCodeLoginCallback.setCaptchaCacheProvider(captchaCacheProvider);
        // 配置组装验证码提供者,2023-03-14
        CaptchaProvider<CaptchaResult> captchaProvider = SecurityConfigUtils
                .findCaptchaProvider(securityProperties.getLoginCaptchaSmsCode()
                        , smsCaptchaProvider, imageCaptchaProvider, jigsawCaptchaProvider);
        smsCodeLoginCallback.setCaptchaProvider(captchaProvider);
        return smsCodeLoginCallback;
    }
    /**
     * 解决同源跨域问题
     * @return
     */
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        if(!this.securityProperties().isCorsEnabled()){
            return new FilterRegistrationBean<>(new CorsFilter(source));
        }
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", buildConfig());
        System.out.println("配置跨域过滤器,this.securityProperties().isCorsEnabled() = true");
        //有多个filter时此处设置改CorsFilter的优先执行顺序
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader("*");
        return corsConfiguration;
    }
    /**
     * 配置自定义认证提供者,自己实现密码认证细节,否则spring会默认密码比较,导致手机短信验证码作为密码比较失效。
     * @param userDetailsService
     * @return
     * @date 2023-01-28
     */
    @Bean
    public DefaultAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService
            , SecurityProperties securityProperties){
        DefaultAuthenticationProvider authenticationProvider = new DefaultAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        System.out.println("isAllowPcUserAccessApp = " + securityProperties.isAllowPcUserAccessApp());
        authenticationProvider.setAllowPcUserAccessApp(securityProperties.isAllowPcUserAccessApp());
        authenticationProvider.setHideUserNotFoundExceptions(false);    // 必须设置,否则不会抛出该异常,2023-06-28
        return authenticationProvider;
    }
    /**
     * 把资源提供者独立出来,可以复用。
     * @return
     * @date 2023-05-07
     */
    @Bean
    public ResourceLoadProvider resourceLoadProvider(){
        DefaultResourceLoaderProvider resourceLoaderProvider = new DefaultResourceLoaderProvider();
        resourceLoaderProvider.setMenuCacheProvider(this.menuCacheProvider);
        resourceLoaderProvider.setPermitAccessUrls(this.securityProperties().getPermitList());
        resourceLoaderProvider.setAnonymousUrlList(this.securityProperties().getAnonymousList());
        resourceLoaderProvider.loadResource();
        return resourceLoaderProvider;
    }
    /**
     * 角色权限变更后,处理通知,并重新加载资源信息。
     * @param resourceLoadProvider
     * @return
     * @date 2023-05-07
     */
    @Bean
    public RoleSecurityUpdateListener roleSecurityUpdateListener(ResourceLoadProvider resourceLoadProvider){
        RoleSecurityUpdateListener listener = new RoleSecurityUpdateListener();
        listener.setResourceLoaderProvider(resourceLoadProvider);
        return listener;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/controller/SecurityController.java
New file
@@ -0,0 +1,439 @@
package com.iplatform.security.controller;
import com.iplatform.base.SecuritySpi;
import com.iplatform.base.SystemController;
import com.iplatform.base.cache.MenuCacheProvider;
import com.iplatform.base.callback.SecurityCallback;
import com.iplatform.base.config.SecurityUserProperties;
import com.iplatform.base.config.TcpProperties;
import com.iplatform.base.exception.LoginException;
import com.iplatform.base.pojo.RequestLogin;
import com.iplatform.base.service.MenuServiceImpl;
import com.iplatform.base.util.MenuUtils;
import com.iplatform.base.util.menu.MenuTree;
import com.iplatform.base.util.menu.SystemMenu;
import com.iplatform.model.po.S_user_core;
import com.iplatform.model.vo.MenuVo;
import com.iplatform.model.vo.RouterVo;
import com.walker.infrastructure.utils.JsonUtils;
import com.walker.infrastructure.utils.StringUtils;
import com.walker.web.ResponseValue;
import com.walker.web.UserPrincipal;
import com.walker.web.UserType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RestController
public class SecurityController extends SystemController {
//    private CacheProvider<String> captchaCacheProvider;
//    private AuthenticationManager authenticationManager;
//    private TokenGenerator tokenGenerator;
//    private UserOnlineProvider userOnlineProvider;
    private MenuServiceImpl menuService;
    private MenuCacheProvider menuCacheProvider;
//    private WebAgentService webAgentService;
//    private LogServiceImpl logService;
//    private LogProperties logProperties;
//    // 2023-03-22
//    private LoginServiceImpl loginService;
//    // 2023-03-28
//    private SecurityProperties securityProperties;
    private TcpProperties tcpProperties;
    private SecuritySpi securitySpi;
    private SecurityUserProperties securityUserProperties;
    @Autowired
    public SecurityController(
//            CacheProvider<String> captchaCacheProvider
//            , AuthenticationManager authenticationManager, TokenGenerator tokenGenerator
//            , UserOnlineProvider userOnlineProvider
            MenuServiceImpl menuService, MenuCacheProvider menuCacheProvider
//            , WebAgentService webAgentService, LogServiceImpl logService, LogProperties logProperties
//            , LoginServiceImpl loginService
//            , SecurityProperties securityProperties
            , TcpProperties tcpProperties, SecuritySpi securitySpi, SecurityUserProperties securityUserProperties){
//        this.captchaCacheProvider = captchaCacheProvider;
//        this.authenticationManager = authenticationManager;
//        this.tokenGenerator = tokenGenerator;
//        this.userOnlineProvider = userOnlineProvider;
        this.menuService = menuService;
        this.menuCacheProvider = menuCacheProvider;
//        this.webAgentService = webAgentService;
//        this.logService = logService;
//        this.logProperties = logProperties;
//        this.loginService = loginService;
//        this.securityProperties = securityProperties;
        this.tcpProperties = tcpProperties;
        this.securitySpi = securitySpi;
        this.securityUserProperties = securityUserProperties;
    }
    @PostMapping("/login")
//    public ResponseValue login(@RequestBody RequestLogin requestLogin){
    public ResponseValue login(@RequestBody String raw){
        logger.debug("login = " + raw);
        RequestLogin requestLogin = null;
        try {
            requestLogin = JsonUtils.jsonStringToObject(raw, RequestLogin.class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        String username = requestLogin.getUsername();
        String password = requestLogin.getPassword();
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            return ResponseValue.error("请输入用户名或密码");
        }
        /*// 2023-01-26 为了支持多种登录方式,使用登录回调处理具体登录细节。
        PlatformLoginCallback loginCallback = LoginCallbackUtils.getLoginCallbackBean(LoginType.getType(requestLogin.getLoginType()), true);
        boolean captchaEnabled = this.getArgumentVariable(ArgumentsConstants.KEY_SECURITY_CAPTCHA_ENABLED).getBooleanValue();
        // 2023-01-26 这里APP端是不需要验证码的
        if(captchaEnabled){
            CaptchaProvider<CaptchaResult> captchaProvider = loginCallback.getCaptchaProvider();
            if(captchaProvider == null){
                return ResponseValue.error("系统需要验证码,但登录未配置:" + loginCallback.getClass().getName());
            }
            if(StringUtils.isEmpty(requestLogin.getVerifyType())){
                return ResponseValue.error("请求错误:验证码类型为空");
            }
            if(!requestLogin.getVerifyType().equals(captchaProvider.getCaptchaType().getIndex())){
                throw new IllegalArgumentException("前端配置的验证码类型与后台不一致! verifyType = " + captchaProvider.getCaptchaType());
            }
            if(loginCallback.isValidateCaptcha()){
                logger.debug("需要验证码,getCaptchaType={}", loginCallback.getCaptchaProvider().getCaptchaType());
                String error = this.validateCaptcha(username, requestLogin.getCode(), requestLogin.getUuid(), captchaProvider);
                if(error != null){
                    return ResponseValue.error(error);
                }
            }
        }
        // 用户验证
        Authentication authentication = null;
        try{
//            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            DefaultAuthenticationToken authenticationToken = new DefaultAuthenticationToken(username, password, requestLogin);
            // 这里放入线程,后面UserDetailsService会使用
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            authentication = authenticationManager.authenticate(authenticationToken);
        } catch (Exception e){
            this.recordLoginInfo(username, String.valueOf(ResponseCode.ERROR.getCode()), "登录未成功认证", 0, null, null);
            if (e instanceof BadCredentialsException){
                return ResponseValue.error(ResponseCode.USER_CREDENTIALS_ERROR.getCode(), ResponseCode.USER_CREDENTIALS_ERROR.getMessage());
            } else {
                if(e instanceof UsernameNotFoundException){
                    logger.debug(".............用户不存在:" + username);
                }
                // 2023-03-20
                // 登录验证抛出异常,会在这里统一处理,如:PcUserStopAppException。
                // DefaultAuthenticationProvider
                return ResponseValue.error(ResponseCode.USER_CREDENTIALS_ERROR.getCode(), e.getMessage());
            }
        }
        DefaultUserDetails userDetails = (DefaultUserDetails)authentication.getPrincipal();
        if(this.logger.isDebugEnabled()){
            logger.debug(userDetails.getUserPrincipal().toString());
        }
        // 2023-03-20,对于空验证码类型(登录方式),需要后台自动生成uuid,因为前端没有机会请求验证码获得uuid
        if(loginCallback.getCaptchaProvider().getCaptchaType() == CaptchaType.None){
            requestLogin.setUuid(IdUtils.simpleUUID());
        }
        // 添加token失效时间(从配置获取),2023-03-28
        long expiredMinutes = SecurityConfigUtils.getTokenExpireMinutes(requestLogin.getClientType(), securityProperties);
        String token = TokenUtils.generateToken(userDetails.getUserPrincipal().getId()
                , userDetails.getUsername(), requestLogin.getUuid(), this.tokenGenerator, expiredMinutes);
        logger.debug("token失效分钟:{}", expiredMinutes);
//        String token = this.tokenGenerator.createToken(requestLogin.getUuid(), userDetails.getUserPrincipal().getId()
//                , VariableConstants.DEFAULT_TOKEN_EXPIRED_MINUTES, VariableConstants.TOKEN_SECRET);
        // 缓存登录信息
        this.userOnlineProvider.cacheUserPrincipal(requestLogin.getUuid(), userDetails.getUserPrincipal());
        // 把成功登录日志,与uuid关联保存放一起,在异步中调用。2023-03-23
        this.recordLoginInfo(username, String.valueOf(ResponseCode.SUCCESS.getCode()), "登录成功"
                , userDetails.getUserPrincipal().getUserInfo().getId(), requestLogin.getUuid(), requestLogin.getClientType());
        // 2023-05-12,加上用户信息(商户系统用)
        Map<String, Object> param = new HashMap<>(2);
        param.put(com.walker.web.Constants.TOKEN_NAME, token);
        param.put(SecurityConstants.KEY_USER_INFO, UserUtils.acquireClientUserInfo(userDetails.getUserPrincipal().getUserInfo(), token));
//      */
        Map<String, Object> param = null;
        try {
            param = this.securitySpi.login(requestLogin);
            return ResponseValue.success(param);
        } catch (LoginException e) {
            return ResponseValue.error(e.getMessage());
        }
    }
    @RequestMapping("/getInfo")
    public ResponseValue getUserInfo(){
        UserPrincipal<S_user_core> userPrincipal = this.getCurrentUserPrincipal();
        Map<String, Object> data = new HashMap<>(4);
        // 2023-10-13,增加权限自定义加载回调,为业务预留钩子
        SecurityCallback securityCallback = this.getPlatformCallback(SecurityCallback.class);
        // permissions,
        // 该集合用于适配若依前端,后续前端将不需要自己保留权限路由,统一后台处理返回错误信息。2022-11-12
        Set<String> perms = new HashSet<String>();
        if(userPrincipal.getUserInfo().getUser_type() == UserType.TYPE_SUPER){
            perms.add("*:*:*");
        } else {
            // 获得当前用户所属的角色ID集合
            List<String> menuIdList = this.menuService.queryRoleMenuIdList(userPrincipal.getRoleIdList());
//            perms.addAll(this.menuCacheProvider.getPermissionSet(menuIdList));
            if(securityCallback == null){
                // 2023-06-01 增加顶级单位可使用的菜单范围。
                int menuScope = this.getDeptCacheProvider().getDept(userPrincipal.getUserInfo().getOrg_id()).getMenu_type();
                if(menuScope == MenuUtils.MENU_SCOPE_PLATFORM){
                    // 平台菜单,需要根据角色查询
                    perms.addAll(this.menuCacheProvider.getPermissionSet(menuIdList, false, menuScope));
                } else {
                    // 商户(顶级机构)独立菜单,不需要角色
                    perms.addAll(this.menuCacheProvider.getPermissionSet(null, true, menuScope));
                }
            } else {
                // 2023-10-13
                logger.debug("存在业务自定义权限回调:{}", securityCallback.getClass().getName());
                Set<String> userPermission = securityCallback.loadUserPermission(userPrincipal.getUserInfo(), menuIdList);
                if(userPermission != null){
                    perms.addAll(userPermission);
                }
            }
        }
        logger.debug("用户 permissions = " + perms);
//        data.put("user", userPrincipal.getUserInfo());
        data.put("roles", userPrincipal.getRoleIdList());
        data.put("permissions", perms);
        data.put("isPerfectInfo", true);    // 是否已完善信息,暂时没用,仅配合前端展示。2023-03-20
        // 2023-08-05 是否强制修改密码
        if(this.securityUserProperties.isPassDefaultModify()){
            data.put("force_change_pass", userPrincipal.getUserInfo().getModify_pwd().intValue() == 1? false : true);
        } else {
            data.put("force_change_pass", false);
        }
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        //~ 适配电商模块,增加其他用户属性。2023-05-12
        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        S_user_core user = userPrincipal.getUserInfo();
        data.put("id", user.getId());
        data.put("account", user.getUser_name());
        data.put("realName", user.getNick_name());
        data.put("roleNames", null);
        data.put("roleIds", StringUtils.collectionToCommaDelimitedString(userPrincipal.getRoleIdList()));
//        data.put("Token", null);
        data.put("phone", user.getPhonenumber());
        data.put("isSms", true);
        data.put("merStarLevel", 0);
        // 2023-04-17,websocket连接信息
        if(!this.tcpProperties.isEnabled()){
            logger.warn("未开启'WebSocket'");
            data.put("uri", "-1");
        } else {
            data.put("uri", this.tcpProperties.getWebsocketUri());
            data.put("uid", userPrincipal.getId());
        }
        if(securityCallback != null){
            securityCallback.acquireUserInfo(data, user);
        }
        return ResponseValue.success(data);
    }
    /**
     * 若依前端方法,获取路由权限。暂时废弃
     * @return
     */
    @Deprecated
    @GetMapping("/getRouters")
    public ResponseValue getRouters(){
        List<SystemMenu> menuList = this.acquireUserMenuList();
        List<RouterVo> routerList = this.menuCacheProvider.buildMenus(menuList);
        return ResponseValue.success(routerList);
    }
    /**
     * 新界面返回菜单树,组装数据。
     * @return
     * @date 2023-05-12
     */
    @GetMapping("/getMenus")
    public ResponseValue getMenus(){
        List<SystemMenu> menuList = this.acquireUserMenuList();
//        logger.debug("acquireUserMenuList = {}", menuList);
        List<MenuVo> menuVoList = new ArrayList<>(32);
        if(menuList != null){
            MenuVo menuVo = null;
            for(SystemMenu menu: menuList){
                if(menu.getMenu_type().equals(MenuUtils.MENU_TYPE_BUTTON)){
                    // 这里注意,这次前端不要功能点,只要菜单本身。2023-05-13
                    continue;
                }
                menuVo = new MenuVo();
                menuVo.setId(Long.parseLong(menu.getMenu_id()));
                menuVo.setPid(Long.parseLong(menu.getParent_id()));
                menuVo.setName(menu.getMenu_name());
                menuVo.setPerms(menu.getPerms());
                menuVo.setComponent(menu.getComponent());
                menuVo.setSort(menu.getOrder_num());
                menuVo.setIcon(menu.getIcon());
                menuVo.setMenuType(menu.getMenu_type());
//                if(menu.getMenu_type().equals(MenuUtils.MENU_TYPE_FOLDER)){
//                    menuVo.setMenuType(MenuUtils.MENU_TYPE_FOLDER);
//                } else if(menu.getMenu_type().equals(MenuUtils.MENU_TYPE_ITEM)){
//                    menuVo.setMenuType(MenuUtils.MENU_TYPE_ITEM);
//                } else {
//                    menuVo.setMenuType(MenuUtils.MENU_TYPE_POINT);
//                }
                if(StringUtils.isNotEmpty(menu.getIcon_info())){
                    String[] customIcons = StringUtils.commaDelimitedListToStringArray(menu.getIcon_info());
                    if(customIcons.length != 2){
                        throw new IllegalArgumentException("自定义菜单图标(相对路径)必须配置两个,并用英文逗号分隔,name=" + menu.getMenu_name());
                    }
                    menuVo.setIconNormal(customIcons[0]);
                    menuVo.setIconActive(customIcons[1]);
                }
                menuVoList.add(menuVo);
            }
        }
//        logger.debug("menuVoList = {}", menuVoList);
        MenuTree menuTree = new MenuTree(menuVoList);
        menuVoList = menuTree.buildTree();
        return ResponseValue.success(menuVoList);
    }
    private List<SystemMenu> acquireUserMenuList(){
        List<SystemMenu> menuList = null;
        UserPrincipal<S_user_core> userPrincipal = this.getCurrentUserPrincipal();
        // 2023-10-13 当业务配置有自定义菜单加载回调对象时,需要按照业务规则加载。
        SecurityCallback securityCallback = this.getPlatformCallback(SecurityCallback.class);
        if(securityCallback == null){
            if(userPrincipal.getUserInfo().getUser_type() == UserType.TYPE_SUPER){
//            menuList = this.menuCacheProvider.getMenuTree(null, false, true);
                menuList = this.menuCacheProvider.getMenuList(null, MenuUtils.MENU_SCOPE_PLATFORM);
            } else {
                // 获得当前用户所属的角色ID集合
//            List<String> menuIdList = this.menuService.queryRoleMenuIdList(userPrincipal.getRoleIdList());
//            menuList = this.menuCacheProvider.getMenuTree(menuIdList, false, false);
              // 2023-06-01 增加顶级单位可使用的菜单范围。
              int menuScope = this.getDeptCacheProvider().getDept(userPrincipal.getUserInfo().getOrg_id()).getMenu_type();
              if(menuScope == MenuUtils.MENU_SCOPE_PLATFORM){
                  // 平台菜单,需要根据角色查询
                  menuList = this.menuCacheProvider.getMenuList(userPrincipal.getRoleIdList(), menuScope);
              } else {
                  // 商户(顶级机构)独立菜单,不需要角色
                  menuList = this.menuCacheProvider.getMenuList(null, menuScope);
              }
//            menuList = this.menuCacheProvider.getMenuList(userPrincipal.getRoleIdList(), menuScope);
            }
        } else {
            // 2023-10-13
            menuList = securityCallback.loadUserMenu(userPrincipal.getUserInfo(), userPrincipal.getRoleIdList());
            if(this.logger.isDebugEnabled()){
                logger.debug("存在业务自定义菜单回调:{}", menuList);
            }
        }
        if(menuList != null){
            // 过滤掉按钮权限,前端只需要菜单,按钮权限在permisstion中控制
            SystemMenu menu = null;
            for(Iterator<SystemMenu> it = menuList.iterator(); it.hasNext();){
                menu = it.next();
                // 1.去掉按钮菜单,同时,停用的菜单也要去掉。2023-05-14
                // 2.不能展示的也要去掉,is_show = 0
                if(menu.getMenu_type().equals(MenuUtils.MENU_TYPE_BUTTON)
                        || menu.getStatus().equals(MenuUtils.MENU_STATUS_DISABLED)
                        || menu.getVisible().equals(MenuUtils.MENU_INVISIBLE)){
                    it.remove();
                }
            }
        }
        return menuList;
    }
//    private String validateCaptcha(String username, String code, String uuid, CaptchaProvider<CaptchaResult> captchaProvider){
//        if(StringUtils.isEmpty(uuid) || StringUtils.isEmpty(code)){
//            return "请输入验证码";
//        }
//
//        CaptchaResult captchaResult = new CaptchaResult();
//        captchaResult.setUuid(uuid);
//        captchaResult.setCode(code);
//        boolean success = captchaProvider.validateCaptcha(captchaResult);
//
//        // 2023-04-07 调整,使用提供者判断验证码是否正确。
//        this.captchaCacheProvider.removeCacheData(Constants.CAPTCHA_CODE_PREFIX + uuid);// 删除缓存的验证码
//
//        if(!success){
//            logger.error("验证码校验失败: code = " + code);
//            return "验证码错误";
//        }
//        return null;
//    }
//    private void recordLoginInfo(String loginId, String status, String message, long userId, String uuid, String clientType){
//        if(this.logProperties.isLoginEnabled()){
//            logger.debug("异步记录登录日志,后续要补充:" + status + ", " + message);
//            AsyncManager.me().execute(this.acquireLoginInfoTask(loginId, status, message, userId, uuid, clientType));
//        }
//    }
//
//    private TimerTask acquireLoginInfoTask(String loginId, String status, String message, Long userId
//            , String uuid, String clientType){
//        HttpServletRequest request = this.getRequest();
//        final WebUserAgent webUserAgent = this.webAgentService.getWebUserAgent(request.getHeader("User-Agent"), request);
//
//        return new TimerTask() {
//            @Override
//            public void run() {
//                S_login_info login_info = new S_login_info();
//                login_info.setLogin_time(Long.parseLong(DateUtils.getDateTimeSecondForShow()));
//                login_info.setUser_name(loginId);
//                login_info.setMsg(message);
//                login_info.setStatus(status);
//                login_info.setInfo_id(NumberGenerator.getLongSequenceNumber());
//                if(webUserAgent != null){
//                    login_info.setLogin_location(webUserAgent.getLocation());
//                    login_info.setBrowser(webUserAgent.getBrowserName());
//                    login_info.setIpaddr(webUserAgent.getIp());
//                    login_info.setOs(webUserAgent.getOsName());
//                }
//
//                // 2023-03-23
//                if(status.equals(String.valueOf(ResponseCode.SUCCESS.getCode()))){
//                    // 登录成功才记录uuid关联缓存
//                    loginService.execUpdateUserLogin(userId, loginId, uuid, clientType, login_info);
//                } else {
//                    // 登录失败,仅记录登录日志
//                    logService.execInsertLoginLog(login_info, userId);
//                }
//            }
//        };
//    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/event/RoleSecurityUpdateListener.java
New file
@@ -0,0 +1,40 @@
package com.iplatform.security.event;
import com.iplatform.base.event.RoleSecurityChangeEvent;
import com.walker.web.security.ResourceLoadProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
/**
 * 角色对应权限更新监听器,在角色权限修改后,会触发该事件来响应。
 * <p>如果不通知重新加载,则角色分过权限后,用户仍然看不到(需要重新启动)</p>
 * @author shikeying
 * @date 2023-05-07 移植老代码
 */
public class RoleSecurityUpdateListener implements ApplicationListener<RoleSecurityChangeEvent> {
    private Logger logger = LoggerFactory.getLogger(getClass());
//    private MySecurityMetadataSource securityMetaSource;
//
//    public void setSecurityMetaSource(MySecurityMetadataSource securityMetaSource) {
//        assert (securityMetaSource != null);
//        this.securityMetaSource = securityMetaSource;
//    }
    private ResourceLoadProvider resourceLoaderProvider;
    public void setResourceLoaderProvider(ResourceLoadProvider securityMetaSource) {
        this.resourceLoaderProvider = securityMetaSource;
    }
    @Override
    public void onApplicationEvent(RoleSecurityChangeEvent event) {
        resourceLoaderProvider.reloadResource();
        logger.info("*************************************************");
        logger.info("*  系统重新加载了角色权限数据");
        logger.info("*************************************************");
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/exception/PcUserStopAppException.java
New file
@@ -0,0 +1,20 @@
package com.iplatform.security.exception;
import org.springframework.security.core.AuthenticationException;
/**
 * PC(后台用户)无法访问App异常定义。
 * @author 时克英
 * @date 2023-03-20
 */
public class PcUserStopAppException extends AuthenticationException {
    public PcUserStopAppException(Throwable cause) {
        super(MESSAGE, cause);
    }
    public PcUserStopAppException(String msg, Throwable cause) {
        super(msg, cause);
    }
    public static final String MESSAGE = "非App用户无法登录手机端!";
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/LoginCallbackUtils.java
New file
@@ -0,0 +1,69 @@
package com.iplatform.security.util;
import com.iplatform.base.PlatformLoginCallback;
import com.iplatform.base.callback.PlatformCallbackPostProcessor;
import com.iplatform.security.callback.EncryptPasswordLoginCallback;
import com.iplatform.security.callback.MobilePassCaptchaLoginCallback;
import com.iplatform.security.callback.NoneCaptchaLoginCallback;
import com.iplatform.security.callback.SimplePasswordLoginCallback;
import com.iplatform.security.callback.SmsCodeLoginCallback;
import com.iplatform.security.callback.ThirdPartyLoginCallback;
import com.iplatform.security.callback.WechatLoginCallback;
import com.walker.web.CaptchaType;
import com.walker.web.LoginType;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 登录回调工具类。
 * @author 时克英
 * @date 2023-01-26
 */
public class LoginCallbackUtils {
    protected static final Logger logger = LoggerFactory.getLogger(LoginCallbackUtils.class);
//    public static final PlatformLoginCallback getLoginCallbackBean(UserType userType){
//        if(userType == UserType.UserEquipment){
//            throw new UnsupportedOperationException("暂不支持设备连接登录回调");
//        } else if(userType == UserType.UserApp){
//            return getLoginCallbackBean(LoginType.MOBILE);
//        } else {
//            return getLoginCallbackBean(LoginType.PC);
//        }
//    }
    /**
     * 根据登录类型,查找登录回调定义实现。
     * @param loginType 登录类型,参考:{@linkplain LoginType}
     * @param encryptPassword 如果密码方式,密码是否加密
     * @return
     */
    public static final PlatformLoginCallback getLoginCallbackBean(LoginType loginType, boolean encryptPassword, CaptchaType captchaType){
        if(loginType == LoginType.UserPassword){
            if(encryptPassword){
                return PlatformCallbackPostProcessor.getCallbackMultipleBean(EncryptPasswordLoginCallback.class);
            } else {
                logger.error("'SimplePasswordLoginCallback'还未配置,明文密码登录已不再支持!仅用于测试。");
                return PlatformCallbackPostProcessor.getCallbackMultipleBean(SimplePasswordLoginCallback.class);
            }
        } else if(loginType == LoginType.SmsCode){
            return PlatformCallbackPostProcessor.getCallbackMultipleBean(SmsCodeLoginCallback.class);
        } else if(loginType == LoginType.MobilePassword){
            if(captchaType == CaptchaType.None){
                return PlatformCallbackPostProcessor.getCallbackMultipleBean(NoneCaptchaLoginCallback.class);
            } else {
                return PlatformCallbackPostProcessor.getCallbackMultipleBean(MobilePassCaptchaLoginCallback.class);
            }
//            return PlatformCallbackPostProcessor.getCallbackMultipleBean(NoneCaptchaLoginCallback.class);
        } else if(loginType == LoginType.ThirdParty){
            return PlatformCallbackPostProcessor.getCallbackMultipleBean(ThirdPartyLoginCallback.class);
        } else if(loginType == LoginType.Wechat){
            return PlatformCallbackPostProcessor.getCallbackMultipleBean(WechatLoginCallback.class);
        } else {
            throw new UnsupportedOperationException("暂不支持登录类型:" + loginType);
        }
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/MockPrincipalUtils.java
New file
@@ -0,0 +1,53 @@
package com.iplatform.security.util;
import com.iplatform.base.DefaultUserPrincipal;
import com.iplatform.model.po.S_user_core;
import com.walker.infrastructure.utils.DateUtils;
import com.walker.web.DataStatus;
import com.walker.web.UserPrincipal;
import com.walker.web.UserType;
public class MockPrincipalUtils {
//    /**
//     * 正式代码,创建超级管理员用户登录对象
//     * @return
//     */
//    public static final UserPrincipal<S_user_core> createSupervisor(String supervisorPassword){
//        S_user_core userCore = new S_user_core();
//        userCore.setId(Constants.SUPERVISOR_ID);
////        userCore.setCreateTime(DateUtils.getDateTimeNumber(System.currentTimeMillis()));
//        userCore.setCreate_time(DateUtils.getDateTimeNumber(System.currentTimeMillis()));
//        userCore.setStatus(DataStatus.CONST_NORMAL);
//        userCore.setUser_name(Constants.SUPERVISOR_NAME_DEFAULT);
//        userCore.setNick_name(Constants.SUPERVISOR_NAME_ZH);
////        userCore.setPassword("123456"); // 这里要修改,应该存储加密过后的密码信息
//        userCore.setPassword(supervisorPassword);
//        userCore.setUser_type(UserType.TYPE_SUPER);
//
//        DefaultUserPrincipal userPrincipal = new DefaultUserPrincipal(userCore);
//        return userPrincipal;
//    }
    /**
     * 模拟一个普通用户
     * @param id 用户ID(以及登录ID)
     * @return
     */
    @Deprecated
    public static final UserPrincipal<S_user_core> createNormalUser(String id){
        S_user_core userCore = new S_user_core();
        userCore.setId(Long.parseLong(id));
//        userCore.setCreateTime(DateUtils.getDateTimeNumber(System.currentTimeMillis()));
        userCore.setCreate_time(DateUtils.getDateTimeNumber(System.currentTimeMillis()));
        userCore.setStatus(DataStatus.CONST_NORMAL);
        userCore.setUser_name(id);
        userCore.setNick_name("演示用户");
        userCore.setPassword("123456"); // 这里要修改,应该存储加密过后的密码信息
        userCore.setUser_type(UserType.TYPE_NORMAL);
        DefaultUserPrincipal userPrincipal = new DefaultUserPrincipal(userCore);
        return userPrincipal;
    }
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/ResourceLoaderUtils.java
New file
@@ -0,0 +1,5 @@
package com.iplatform.security.util;
public class ResourceLoaderUtils {
}
iplatform-base-security-consum/src/main/java/com/iplatform/security/util/SecurityConfigUtils.java
New file
@@ -0,0 +1,58 @@
package com.iplatform.security.util;
import com.iplatform.base.captcha.JigsawCaptchaProvider;
import com.iplatform.base.captcha.NoneCaptchaProvider;
import com.iplatform.security.config.SecurityProperties;
import com.walker.web.CaptchaProvider;
import com.walker.web.CaptchaResult;
import com.walker.web.CaptchaType;
import com.walker.web.ClientType;
import com.walker.web.captcha.SlideCaptchaProvider;
public class SecurityConfigUtils {
    /**
     * 根据设备类型,返回token失效时间(分钟)
     * @param clientType
     * @param securityProperties
     * @return
     * @date 2023-03-28
     */
    public static final long getTokenExpireMinutes(String clientType, SecurityProperties securityProperties){
        if(clientType.equals(ClientType.INDEX_PC)){
            return securityProperties.getTokenExpireWeb();
        } else {
            return securityProperties.getTokenExpireMobile();
        }
    }
    /**
     * 根据'登录回调方式'查找需要装配的'验证码类型'。
     * @param loginCaptchaUserPass
     * @param smsCaptchaProvider
     * @param imageCaptchaProvider
     * @return
     * @date 2023-03-14
     */
    public static final CaptchaProvider<CaptchaResult> findCaptchaProvider(String loginCaptchaUserPass
            , CaptchaProvider<CaptchaResult> smsCaptchaProvider
            , CaptchaProvider<CaptchaResult> imageCaptchaProvider
            , JigsawCaptchaProvider jigsawCaptchaProvider){
        CaptchaProvider<CaptchaResult> captchaProvider = null;
        CaptchaType captchaType = CaptchaType.getType(loginCaptchaUserPass);
        if(captchaType == CaptchaType.InputCode){
            captchaProvider = imageCaptchaProvider;
        } else if(captchaType == CaptchaType.SmsCode){
            captchaProvider = smsCaptchaProvider;
        } else if(captchaType == CaptchaType.Slide){
            captchaProvider = new SlideCaptchaProvider();
        } else if(captchaType == CaptchaType.Jigsaw){
            captchaProvider = jigsawCaptchaProvider;
        } else if(captchaType == CaptchaType.None){
            captchaProvider = new NoneCaptchaProvider();
        } else {
            throw new UnsupportedOperationException("不支持的CaptchaType:" + loginCaptchaUserPass);
        }
        return captchaProvider;
    }
}
iplatform-base-security-consum/src/test/java/com/iplatform/security/TestSecurity.java
New file
@@ -0,0 +1,37 @@
package com.iplatform.security;
import com.iplatform.base.util.PlatformRSAUtils;
import com.walker.infrastructure.utils.Base64;
import com.walker.infrastructure.utils.Base64Utils;
import com.walker.infrastructure.utils.RSAUtil;
import org.junit.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
public class TestSecurity {
//    @Test
    public void testDecryptPassword() throws Exception{
        String encodePass = "DOEUaXiOa0y8Kq0De+P4OL/bdydlEFC+330I2lmXbz8VwHJYugLV/IPeXp31fZ5yOQvelMLwDutNtgQaRVS9L8n5ctjpYQZC3HAVDZ+6sXhE3TIH14Q8S3RhD3kE8iBVKrWd7423iCjflNwUPedFcQ0zVpJt3pC3wvDUayXIJnI=";
//        String decode = Base64.decodeBase64(encode.getBytes(StandardCharsets.UTF_8));
        byte[] keyBytes = Base64Utils.decode(PlatformRSAUtils.PRIK);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        String password = RSAUtil.decrypt(privateK, Base64.decode(encodePass.getBytes()));
        System.out.println(password);
    }
//    @Test
    public void testGeneratePassword(){
        String raw = "123456";
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encrypt = passwordEncoder.encode(raw);
        System.out.println("encrypt = " + encrypt);
    }
}
pom.xml
@@ -7,6 +7,7 @@
    <module>deploy-jar-single</module>
    <module>consum-base</module>
    <module>consum-model-pojo</module>
    <module>iplatform-base-security-consum</module>
  </modules>
  <parent>
@@ -44,6 +45,12 @@
        <version>${consum-model-pojo.version}</version>
      </dependency>
      <dependency>
        <groupId>com.iplatform</groupId>
        <artifactId>iplatform-base-security-consum</artifactId>
        <version>1.0.0-SNAPSHOT</version>
      </dependency>
    </dependencies>
  </dependencyManagement>