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){
|
// logger.debug(".............用户不存在:" + requestLogin.getUsername());
|
throw new LoginException("用户账号不存在,或已停用:" + requestLogin.getUsername(), e, 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;
|
}
|
}
|