| | |
| | | |
| | | |
| | | // 接口请求地址 |
| | | 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',//开发 |
| | |
| | | // apiBaseURL: protocol + '//'+ host + '/lowapi',// 正式, |
| | | |
| | | |
| | | // ftpUrl: protocol + '//'+ host + '/lowapi',// 正式, |
| | | // apiBaseURL: protocol + '//'+ host + '/lowapi',// 正式, |
| | | ftpUrl: protocol + '//'+ host + '/lowapi',// 正式, |
| | | apiBaseURL: protocol + '//'+ host + '/lowapi',// 正式, |
| | | |
| | | debug: false //调试开关 true时会输出请求日志 |
| | | }; |
| | |
| | | <!-- 引入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> |
| | |
| | | spring: |
| | | profiles: |
| | | active: test |
| | | active: prod |
| | | |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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(); |
| | | |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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()); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | package com.iplatform.security.callback; |
| | | |
| | | /** |
| | | * 不带验证码类型的(账户、密码)登录方式扩展。 |
| | | * <p>仅为了定义一种类型,没有任何代码可重写</p> |
| | | * @author 时克英 |
| | | * @date 2023-04-07 |
| | | */ |
| | | public class NoneCaptchaLoginCallback extends EncryptPasswordLoginCallback{ |
| | | |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | // } |
| | | // } |
| | | // }; |
| | | // } |
| | | } |
New file |
| | |
| | | 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("*************************************************"); |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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用户无法登录手机端!"; |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | package com.iplatform.security.util; |
| | | |
| | | public class ResourceLoaderUtils { |
| | | |
| | | } |
New file |
| | |
| | | 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | <module>deploy-jar-single</module> |
| | | <module>consum-base</module> |
| | | <module>consum-model-pojo</module> |
| | | <module>iplatform-base-security-consum</module> |
| | | </modules> |
| | | |
| | | <parent> |
| | |
| | | <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> |
| | | |