Index: ssts-datasync-default-impl/src/main/java/com/forgon/disinfectsystem/datasynchronization/dao/zsyy/VerificationCodeDaoImpl.java =================================================================== diff -u --- ssts-datasync-default-impl/src/main/java/com/forgon/disinfectsystem/datasynchronization/dao/zsyy/VerificationCodeDaoImpl.java (revision 0) +++ ssts-datasync-default-impl/src/main/java/com/forgon/disinfectsystem/datasynchronization/dao/zsyy/VerificationCodeDaoImpl.java (revision 40894) @@ -0,0 +1,133 @@ +package com.forgon.disinfectsystem.datasynchronization.dao.zsyy; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Node; +import org.dom4j.XPath; + +import com.forgon.disinfectsystem.common.CssdUtils; +import com.forgon.disinfectsystem.verification.dao.VerificationCodeDao; +import com.forgon.disinfectsystem.verification.model.VerificationCode; +import com.forgon.exception.SystemException; + +/** + * 短信验证码的dao + * ZSYY-438 + */ +public class VerificationCodeDaoImpl implements VerificationCodeDao { + + public static final Logger logger = Logger.getLogger(VerificationCodeDaoImpl.class.getSimpleName()); + + /** + * 短信发送接口地址 + */ + public static final String URL = "http://168.168.253.35:8089/sms/Api/Send.do"; + /** + * 企业编号 + */ + public static final String SP_CODE = "2041"; + /** + * 用户名称 + */ + public static final String LOGIN_NAME = "XDGYXT"; + /** + * 用户密码 + */ + public static final String PASSWORD = "2D44_faw2"; + /** + * 短信发送成功的返回值 + */ + public static final String RESULT_SUCESS = "0"; + + @Override + public void generateverificationCode(String messageId, String extCode, + String destAddr, String messageContent, Integer reqDeliveryReport, + Integer msgFormat, Integer sendMethod, Date requestDateTime, + String applicationId) { + + } + + @Override + public VerificationCode getVerificationCodeByMessageId(String messageId) { + return null; + } + + @Override + public void sendVerificationCodeSms(String smsMumber, String messageContent) { + if(StringUtils.isBlank(smsMumber)){ + throw new SystemException("手机号码不能为空!"); + } + if(StringUtils.isBlank(messageContent)){ + throw new SystemException("短信内容不能为空!"); + } + logger.debug(String.format("开始发送短信,接收短信的手机号码为【%s】,短信内容为【%s】", smsMumber, messageContent)); + Map params = new HashMap(); + params.put("SpCode", SP_CODE); + params.put("LoginName", LOGIN_NAME); + params.put("Password", PASSWORD); + params.put("MessageContent", messageContent); + + params.put("UserNumber", smsMumber); + params.put("SerialNumber", ""); + params.put("ScheduleTime", ""); + //params.put("f", "1"); + String responce = CssdUtils.postRequest(URL, params, "UTF-8"); + //String responce = ""; + logger.debug(String.format("完成发送短信,接收短信的手机号码为【%s】,短信内容为【%s】,短信发送接口返回的信息为【%s】", smsMumber, messageContent, responce)); + if(StringUtils.isBlank(responce)){ + throw new RuntimeException("短信发送接口返回的消息为空!"); + } + try { + Document document = DocumentHelper.parseText(responce); + String dataXpath = "/data"; + responce = processDataByXpath(document, dataXpath); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(String.format("短信发送失败:返回参数解析失败,%s", e.getMessage())); + } + if(StringUtils.isBlank(responce)){ + throw new RuntimeException("短信发送接口返回的消息为空!"); + } + String result = ""; + String description = ""; + String[] responceParams = responce.split("&"); + for (String keyValue : responceParams) { + String[] keyValueArray = keyValue.split("="); + if(StringUtils.equals("result", keyValueArray[0])){ + result = keyValueArray[1]; + }else if(StringUtils.equals("description", keyValueArray[0])){ + description = keyValueArray[1]; + } + } + + if(!StringUtils.equals(result, RESULT_SUCESS)){ + throw new SystemException(String.format("短信发送失败:%s", description)); + } + + } + + /** + * 根据document和Xpath表达式解析数据 + * @param document + * @param nameSpaceURIMap + * @param xpathExp + * @return + */ + public static String processDataByXpath(Document document, String xpathExp) { + if ((StringUtils.isNotBlank(xpathExp)) && (document != null)) { + XPath xpath = document.createXPath(xpathExp); + Node node = xpath.selectSingleNode(document); + if (node != null) { + return node.getStringValue(); + } + } + return null; + } + +} Index: ssts-web/src/main/webapp/disinfectsystem/config/zsyy/spring/his.xml =================================================================== diff -u -r40881 -r40894 --- ssts-web/src/main/webapp/disinfectsystem/config/zsyy/spring/his.xml (.../his.xml) (revision 40881) +++ ssts-web/src/main/webapp/disinfectsystem/config/zsyy/spring/his.xml (.../his.xml) (revision 40894) @@ -191,5 +191,15 @@ + + + + + + + + + \ No newline at end of file Index: forgon-core/src/main/java/com/forgon/security/model/SmsVerificationCode.java =================================================================== diff -u --- forgon-core/src/main/java/com/forgon/security/model/SmsVerificationCode.java (revision 0) +++ forgon-core/src/main/java/com/forgon/security/model/SmsVerificationCode.java (revision 40894) @@ -0,0 +1,123 @@ +package com.forgon.security.model; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +/** + * 短信验证码 + * ZSYY-438 + */ +@Entity +@DynamicInsert(false) +@DynamicUpdate(true) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class SmsVerificationCode { + + private Long id; + + /** + * 登录用户名 + */ + private String userName; + /** + * 手机号码 + */ + private String smsNumber; + /** + * 验证码 + */ + private String verificationCode; + /** + * 创建时间 + */ + private Date createDateTime; + /** + * 验证失败次数 + */ + private Integer verifyFailTimes; + /** + * 是否验证成功 + * 0未验证成功(初始状态);1验证成功或者长时间没用已失效(已验证状态); + */ + private Integer verified = STATUS_UNVERIFIED; + /** + * 待验证状态 + */ + public static final Integer STATUS_UNVERIFIED = 0; + /** + * 已验证状态 + */ + public static final Integer STATUS_VERIFIED = 1; + /** + * 短信发送的最小时间间隔(分钟),同一个用户每次点击发送获取验证码后,有一分钟的冷却时间 + */ + public static final Integer SMS_SEND_RATE_LIMIT_MIN = 1; + /** + * 验证码的有效时长(分钟) + */ + public static final Integer SMS_VALIDITY_DURATION_MIN = 5; + /** + * 验证码验证失败的最大次数 + */ + public static final Integer MAX_VERIFY_FAIL_TIMES = 5; + /** + * 每天最多发送的短信数量 + */ + public static final Integer MAX_SMS_AMOUNT_PER_DAY = 20; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getUserName() { + return userName; + } + public void setUserName(String userName) { + this.userName = userName; + } + public String getSmsNumber() { + return smsNumber; + } + public void setSmsNumber(String smsNumber) { + this.smsNumber = smsNumber; + } + public String getVerificationCode() { + return verificationCode; + } + public void setVerificationCode(String verificationCode) { + this.verificationCode = verificationCode; + } + public Date getCreateDateTime() { + return createDateTime; + } + public void setCreateDateTime(Date createDateTime) { + this.createDateTime = createDateTime; + } + public Integer getVerifyFailTimes() { + return verifyFailTimes; + } + public void setVerifyFailTimes(Integer verifyFailTimes) { + this.verifyFailTimes = verifyFailTimes; + } + @Column(columnDefinition = "int default 0 not null ") + public Integer getVerified() { + return verified; + } + public void setVerified(Integer verified) { + this.verified = verified; + } +} Index: forgon-core/src/main/java/com/forgon/directory/controller/PersonalSettingController.java =================================================================== diff -u -r12335 -r40894 --- forgon-core/src/main/java/com/forgon/directory/controller/PersonalSettingController.java (.../PersonalSettingController.java) (revision 12335) +++ forgon-core/src/main/java/com/forgon/directory/controller/PersonalSettingController.java (.../PersonalSettingController.java) (revision 40894) @@ -19,6 +19,7 @@ import com.forgon.security.model.User; import com.forgon.security.model.UserAttribute; import com.forgon.tools.AppKeys; +import com.forgon.tools.crypto.rsa.RSAEncrypt; /** * @author yuanbin @@ -46,6 +47,7 @@ PasswordEncoder passwordEncoder = new Md5PasswordEncoder(); String oldPwd = request.getParameter("oldPassword"); String newPwd = request.getParameter("newPassword"); + String smsVerificationCode = request.getParameter("smsVerificationCode"); if(!user.getPasswd().equals(passwordEncoder.encodePassword(oldPwd, null))){ request.setAttribute(AppKeys.TipMsgKey, "原密码不正确!"); request.setAttribute(AppKeys.RedirectTo, request.getContextPath()+"/personalSetting/modifyPWD.mhtml"); @@ -55,6 +57,14 @@ msg = "找不到当前用户!" ; } else { user.setPasswd(newPwd); + String j_smsVerificationCode = null; + try { + j_smsVerificationCode = RSAEncrypt.decrypt(smsVerificationCode); + } catch (Exception e) {} + if(StringUtils.isBlank(j_smsVerificationCode)){ + j_smsVerificationCode = smsVerificationCode; + } + user.setSmsVerificationCode(j_smsVerificationCode); personalSettingManager.modifyPersonalPassWord(user); msg = "" ; } Index: forgon-core/src/main/java/com/forgon/security/service/UserManager.java =================================================================== diff -u -r39758 -r40894 --- forgon-core/src/main/java/com/forgon/security/service/UserManager.java (.../UserManager.java) (revision 39758) +++ forgon-core/src/main/java/com/forgon/security/service/UserManager.java (.../UserManager.java) (revision 40894) @@ -5,6 +5,8 @@ import java.util.Map; import java.util.Set; +import javax.servlet.http.HttpServletRequest; + import net.sf.json.JSONArray; import net.sf.json.JSONObject; @@ -193,4 +195,11 @@ */ public void updateUserLastOnlineTime(Long userId, Date lastOnlineTime, boolean delay); + /** + * 若同时启用了配置项“loginSecurirtyConfig”时,验证码输入后的验证失败次数不需要计算为登录失败的次数ZSYY-438 + * @param currentLoginedUser 当前登录用户 + * @param request 请求 + */ + public void loginSecurirtyConfig(User currentLoginedUser, HttpServletRequest request); + } Index: forgon-core/src/main/java/com/forgon/directory/action/PersonalSettingAction.java =================================================================== diff -u -r40761 -r40894 --- forgon-core/src/main/java/com/forgon/directory/action/PersonalSettingAction.java (.../PersonalSettingAction.java) (revision 40761) +++ forgon-core/src/main/java/com/forgon/directory/action/PersonalSettingAction.java (.../PersonalSettingAction.java) (revision 40894) @@ -57,7 +57,15 @@ public void save() { Long id = AcegiHelper.getLoginUser().getUserId(); String userName = StrutsParamUtils.getPraramValue("userName", ""); + String smsVerificationCode = StrutsParamUtils.getPraramValue("smsVerificationCode", ""); String j_useNameAfterRsaDecrypt = RSAEncrypt.decrypt(userName); + String j_smsVerificationCode = null; + try { + j_smsVerificationCode = RSAEncrypt.decrypt(smsVerificationCode); + } catch (Exception e) {} + if(StringUtils.isBlank(j_smsVerificationCode)){ + j_smsVerificationCode = smsVerificationCode; + } User user = null; //判断是否扫码登录 boolean userNameIsBarcode = false; @@ -84,6 +92,7 @@ user.setModifiedPwd(true); //已修改 user.setPasswd(newPassword); try{ + user.setSmsVerificationCode(j_smsVerificationCode); personalSettingManager.modifyPersonalPassWord(user); }catch(SystemException e){ jSONObject.put("success", false); Index: forgon-core/src/main/java/com/forgon/security/service/UserManagerImpl.java =================================================================== diff -u -r40748 -r40894 --- forgon-core/src/main/java/com/forgon/security/service/UserManagerImpl.java (.../UserManagerImpl.java) (revision 40748) +++ forgon-core/src/main/java/com/forgon/security/service/UserManagerImpl.java (.../UserManagerImpl.java) (revision 40894) @@ -5,6 +5,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -14,6 +15,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; + import net.sf.json.JSONArray; import net.sf.json.JSONObject; @@ -48,6 +51,7 @@ import com.forgon.log.service.LogManager; import com.forgon.runwithtrans.model.RunWithTransNewTask; import com.forgon.runwithtrans.service.RunWithTransNewManager; +import com.forgon.security.model.IpLoginLockRecord; import com.forgon.security.model.ModifyPwdRecord; import com.forgon.security.model.Operation; import com.forgon.security.model.Role; @@ -59,6 +63,7 @@ import com.forgon.tools.Constants; import com.forgon.tools.GB2Alpha; import com.forgon.tools.GB2WB; +import com.forgon.tools.IPAddressValidator; import com.forgon.tools.SpringBeanManger; import com.forgon.tools.SqlBuilder; import com.forgon.tools.crypto.coder.CoderEncryption; @@ -89,6 +94,8 @@ */ @Autowired private RunWithTransNewManager runWithTransNewManager; + @Autowired + private IpLoginLockRecordManager ipLoginLockRecordManager; public UserManagerImpl() { super(User.class); @@ -1094,4 +1101,119 @@ return false; } + @Override + public void loginSecurirtyConfig(User currentLoginedUser, HttpServletRequest request) { + //统计用户验证失败次数,当达到最大次数时,锁定用户 + saveUserLogonFailRecord(currentLoginedUser, request); + //统计用户IP验证失败次数,当达到最大次数时,锁定IP + saveIpLoginLockRecord(request); + } + + /** + * 统计用户IP验证失败次数,当达到最大次数时,锁定IP + * @param currentLoginedUser 当前登录用户 + * @param request 请求 + */ + private void saveIpLoginLockRecord(HttpServletRequest request) { + if(request == null){ + return; + } + String ip = IPAddressValidator.getClientIp(request); + if(StringUtils.isBlank(ip)){ + return; + } + //连续登录失败的次数,达到这个次数后,账号会被锁定。(连续2次及以下登录失败只提示用户名或密码错误,不提示可以尝登录失败次数) + int loginFailuresCountOfIP = 0; + //锁定的时间,单位为分钟。 + int lockTimeInMinutesOfIP = 0; + String loginSecurirtyConfig = ConfigUtils.getSystemSetConfigByName("loginSecurirtyConfig"); + if(StringUtils.isNotBlank(loginSecurirtyConfig)){ + try { + JSONObject json = JSONObject.fromObject(loginSecurirtyConfig); + loginFailuresCountOfIP = json.optInt("loginFailuresCountOfIP", 0); + lockTimeInMinutesOfIP = json.optInt("lockTimeInMinutesOfIP", 0); + } catch (Exception e) {} + } + if(loginFailuresCountOfIP == 0 || lockTimeInMinutesOfIP == 0){ + //不开启IP登录失败锁定的配置项 + return; + } + IpLoginLockRecord ipLoginLockRecord = ipLoginLockRecordManager.getIpLoginLockRecordByIp(ip); + if(ipLoginLockRecord == null){ + ipLoginLockRecord = new IpLoginLockRecord(); + ipLoginLockRecord.setIpAddress(ip); + ipLoginLockRecord.setFailCount(1); + }else{ + ipLoginLockRecord.setFailCount(ipLoginLockRecord.getFailCount()+1); + } + //过了锁定截止时间后,自动解锁后的第一次登录就登录失败时,需要重新计算登录失败时间 + if(ipLoginLockRecord.getLockEndDateTime() != null && !ipLoginLockRecord.getLockEndDateTime().after(new Date())){ + ipLoginLockRecord.setFailCount(1); + ipLoginLockRecord.setLockEndDateTime(null); + } + + if(ipLoginLockRecord.getFailCount() >= loginFailuresCountOfIP){ + Calendar cal1 = Calendar.getInstance(); + cal1.add(Calendar.MINUTE, lockTimeInMinutesOfIP); + ipLoginLockRecord.setLockEndDateTime(cal1.getTime()); + } + //保存IP锁定记录 + ipLoginLockRecordManager.save(ipLoginLockRecord); + } + + /** + * 保存登录失败记录,当达到最大次数时,锁定用户 + * @param currentLoginedUser 登录用户 + * @param request 请求 + */ + private void saveUserLogonFailRecord(User currentLoginedUser, HttpServletRequest request) { + if(currentLoginedUser == null || request == null){ + return; + } + //连续登录失败的次数,达到这个次数后,账号会被锁定。如果没配置,则缺省值为6,即连续6次失败,会被锁定。(连续2次及以下登录失败只提示用户名或密码错误,不提示可以尝登录失败次数) + int allowLogonFailTimes = 6; + //锁定的时间,单位为分钟。如果没配置,则缺省值为180,即3个小时。 + int lockTimeInMinutes = 180; + String loginSecurirtyConfig = ConfigUtils.getSystemSetConfigByName("loginSecurirtyConfig"); + if(StringUtils.isNotBlank(loginSecurirtyConfig)){ + try { + JSONObject json = JSONObject.fromObject(loginSecurirtyConfig); + allowLogonFailTimes = json.optInt("loginFailuresCount", 6); + if(allowLogonFailTimes <= 0){ + allowLogonFailTimes = 6; + } + lockTimeInMinutes = json.optInt("lockTimeInMinutes", 180); + if(lockTimeInMinutes <= 0){ + lockTimeInMinutes = 180; + } + } catch (Exception e) { + } + } + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, -lockTimeInMinutes); + Date threeHoursAgoTime = cal.getTime(); + //查出该用户在3小时内的连续登录失败次数,不包含本次登录。(如果3小时内有登录成功过,则以最后一次登录成功后往后推计算连续失败次数) + int seriesLogonFailTimes = this.getSeriesLogonFailTimes(currentLoginedUser.getId(), threeHoursAgoTime); + if(seriesLogonFailTimes+1 >= allowLogonFailTimes){ + Calendar cal1 = Calendar.getInstance(); + cal1.add(Calendar.MINUTE, lockTimeInMinutes); + Date nextLockEndDate = cal1.getTime(); + currentLoginedUser.setLockEndDate(nextLockEndDate); + objectDao.saveOrUpdate(currentLoginedUser); + } + //保存登录失败记录 + UserLogonRecord userLogonRecord = new UserLogonRecord(); + userLogonRecord.setUserId(currentLoginedUser.getId()); + userLogonRecord.setLogonName(currentLoginedUser.getName()); + userLogonRecord.setPassword(currentLoginedUser.getPasswd()); + userLogonRecord.setSucc(UserLogonRecord.SUCC_FALSE); + if(request != null){ + //客户端IP地址 + String clientIp = IPAddressValidator.getClientIp(request); + userLogonRecord.setIp(clientIp); + } + //插入登录记录 + objectDao.saveOrUpdate(userLogonRecord); + } + } Index: forgon-core/src/main/java/com/forgon/security/model/User.java =================================================================== diff -u -r40748 -r40894 --- forgon-core/src/main/java/com/forgon/security/model/User.java (.../User.java) (revision 40748) +++ forgon-core/src/main/java/com/forgon/security/model/User.java (.../User.java) (revision 40894) @@ -267,6 +267,12 @@ */ private String thirdPartyAcountID; + /** + * 短信验证码,不保存到数据库 + * ZSYY-438 + */ + private String smsVerificationCode; + public String getPasswd() { return this.passwd; } @@ -940,5 +946,14 @@ } this.setPasswd(newPassword); } + + @Transient + public String getSmsVerificationCode() { + return smsVerificationCode; + } + + public void setSmsVerificationCode(String smsVerificationCode) { + this.smsVerificationCode = smsVerificationCode; + } } Index: forgon-core/src/main/java/com/forgon/security/action/UserAction.java =================================================================== diff -u -r40755 -r40894 --- forgon-core/src/main/java/com/forgon/security/action/UserAction.java (.../UserAction.java) (revision 40755) +++ forgon-core/src/main/java/com/forgon/security/action/UserAction.java (.../UserAction.java) (revision 40894) @@ -87,6 +87,8 @@ if(userNameIsBarcode){ property = "barcode"; } + //启用密码二次验证功能后,登录时,无论用户输入的密码是否正确,都需要判断该用户的密码是否符合ZSYY-438 + boolean enableTwoFactorAuthentication = StringUtils.equals("1", ConfigUtils.getSystemSetConfigByName("enableTwoFactorAuthentication")); User user = sysUserManager.getUserByPropertyWithLower(property, j_useNameAfterRsaDecrypt); if(user == null){ throw new RuntimeException("用户名异常!"); @@ -107,9 +109,13 @@ throw new RuntimeException("密码错误!"); } }else{ - String j_passwordRsaMd5 = CoderEncryption.encryptMD5ForSpringSecurity(j_passwordAfterRsaDecrypt); - if(!StringUtils.equals(user.getPasswd(), j_passwordRsaMd5)){ - throw new RuntimeException("密码错误!"); + //启用密码二次验证功能后,登录时,无论用户输入的密码是否正确,都需要判断该用户的密码是否符合ZSYY-438 + if(!enableTwoFactorAuthentication){ + //不启用密码二次验证功能时,先验证密码是否正确 + String j_passwordRsaMd5 = CoderEncryption.encryptMD5ForSpringSecurity(j_passwordAfterRsaDecrypt); + if(!StringUtils.equals(user.getPasswd(), j_passwordRsaMd5)){ + throw new RuntimeException("密码错误!"); + } } } Boolean modifiedPwd = user.getModifiedPwd() == null ? false :user.getModifiedPwd(); Index: ssts-datasync-default-impl/src/main/java/com/forgon/disinfectsystem/verification/VerificationCodeManagerImpl.java =================================================================== diff -u -r31653 -r40894 --- ssts-datasync-default-impl/src/main/java/com/forgon/disinfectsystem/verification/VerificationCodeManagerImpl.java (.../VerificationCodeManagerImpl.java) (revision 31653) +++ ssts-datasync-default-impl/src/main/java/com/forgon/disinfectsystem/verification/VerificationCodeManagerImpl.java (.../VerificationCodeManagerImpl.java) (revision 40894) @@ -1,35 +1,81 @@ package com.forgon.disinfectsystem.verification; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; + import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; -import org.springframework.security.authentication.encoding.Md5PasswordEncoder; -import org.springframework.security.authentication.encoding.PasswordEncoder; +import org.apache.log4j.Logger; +import org.hibernate.Query; +import org.hibernate.Session; + import net.sf.json.JSONObject; -import com.forgon.directory.acegi.tools.AcegiHelper; + +import com.forgon.databaseadapter.service.DateQueryAdapter; import com.forgon.directory.mailremotemanager.service.RemoteManagerClient; +import com.forgon.directory.service.SmsVerificationCodeManager; import com.forgon.disinfectsystem.verification.dao.VerificationCodeDao; import com.forgon.disinfectsystem.verification.model.VerificationCode; import com.forgon.disinfectsystem.verification.service.VerificationCodeManager; import com.forgon.exception.SystemException; +import com.forgon.runwithtrans.model.RunWithTransNewTask; +import com.forgon.runwithtrans.service.RunWithTransNewManager; +import com.forgon.security.model.SmsVerificationCode; import com.forgon.security.model.User; +import com.forgon.security.service.IpLoginLockRecordManager; import com.forgon.security.service.UserManager; import com.forgon.tools.StrutsParamUtils; -import com.forgon.tools.StrutsResponseUtils; +import com.forgon.tools.date.DateTools; +import com.forgon.tools.hibernate.ObjectDao; import com.forgon.tools.json.JSONUtil; +import com.forgon.tools.util.ConfigUtils; -public class VerificationCodeManagerImpl implements VerificationCodeManager { +public class VerificationCodeManagerImpl implements VerificationCodeManager, SmsVerificationCodeManager { + public static final Logger logger = Logger.getLogger(VerificationCodeManagerImpl.class); + private UserManager userManager; private VerificationCodeDao verificationCodeDaoMybatis; + private VerificationCodeDao verificationCodeDao; + private RemoteManagerClient remoteManagerClient; + + private ObjectDao objectDao; + + private RunWithTransNewManager runWithTransNewManager; + + private DateQueryAdapter dateQueryAdapter; + + private IpLoginLockRecordManager ipLoginLockRecordManager; + + public void setIpLoginLockRecordManager(IpLoginLockRecordManager ipLoginLockRecordManager) { + this.ipLoginLockRecordManager = ipLoginLockRecordManager; + } + public void setDateQueryAdapter(DateQueryAdapter dateQueryAdapter) { + this.dateQueryAdapter = dateQueryAdapter; + } + + public void setVerificationCodeDao(VerificationCodeDao verificationCodeDao) { + this.verificationCodeDao = verificationCodeDao; + } + + public void setRunWithTransNewManager(RunWithTransNewManager runWithTransNewManager) { + this.runWithTransNewManager = runWithTransNewManager; + } + + public void setObjectDao(ObjectDao objectDao) { + this.objectDao = objectDao; + } + public void setRemoteManagerClient(RemoteManagerClient remoteManagerClient) { this.remoteManagerClient = remoteManagerClient; } @@ -155,4 +201,175 @@ return messageContent.substring(startIndex + 1, endIndex); } + @SuppressWarnings("unchecked") + @Override + public void sendVerificationCodeSms(String loginName) { + Date nowDateTime = new Date(); + Session session = objectDao.getHibernateSession(); + Query query = session.createQuery(String.format("select po from %s po where name = :name", User.class.getSimpleName())); + query.setParameter("name", loginName); + List userList = query.list(); + if(CollectionUtils.isEmpty(userList)){ + throw new RuntimeException(String.format("用户【%s】不存在!", loginName)); + } + if(userList.size() > 1){ + throw new RuntimeException(String.format("存在多个用户名为【%s】的用户!", loginName)); + } + User loginUser = userList.get(0); + String smsMumber = loginUser.getSmsMumber(); + if(StringUtils.isBlank(smsMumber)){ + throw new SystemException("未绑定手机号码,请补充或联系管理员。"); + } + //若同时启用了配置项“loginSecurirtyConfig”时,验证码输入后的验证失败次数也计算为登录失败的次数。 + loginSecurirtyConfig(loginUser, nowDateTime); + //查询最后发送的验证码,用于校验用户是否频繁发送短信;同一个用户每次点击发送获取验证码后,有一分钟的冷却时间; + SmsVerificationCode lastSmsVerificationCode = getLastSmsVerificationCode(loginName, smsMumber); + if(lastSmsVerificationCode != null){ + //同一个用户每次点击发送获取验证码后,有一分钟的冷却时间; + //验证码在验证成功后直接废弃,废弃时冷却时间需要同步重置; + if(lastSmsVerificationCode.getVerified() == SmsVerificationCode.STATUS_UNVERIFIED + && DateTools.getDateDiff(lastSmsVerificationCode.getCreateDateTime(), nowDateTime, TimeUnit.SECONDS) < SmsVerificationCode.SMS_SEND_RATE_LIMIT_MIN * 60){ + throw new RuntimeException(String.format("用户【%s】短信发送间隔过短,请稍后再试!", loginName)); + } + } + //限制每天最多发送的短息数量 + String dateAreaSql = dateQueryAdapter.dateAreaSql("createDateTime", DateTools.getFormatDateStr(DateTools.startOfDate(nowDateTime), DateTools.COMMON_DATE_HMS), DateTools.getFormatDateStr(nowDateTime, DateTools.COMMON_DATE_HMS)); + String countSql = String.format("select count(1) from %s where %s", SmsVerificationCode.class.getSimpleName(), dateAreaSql); + if(objectDao.countBySql(countSql) >= SmsVerificationCode.MAX_SMS_AMOUNT_PER_DAY){ + throw new RuntimeException(String.format("用户【%s】短信发送频繁,请稍后再试!", loginName)); + } + //生成随机验证码 + Integer num = (int)((Math.random()*9+1)*1000); + String verificationCode = num.toString(); + //verificationCode="0000"; + String messageContent = generateMessageContent(verificationCode); + //调用第三方接口,发送验证码短信 + verificationCodeDao.sendVerificationCodeSms(smsMumber, messageContent); + //保存短信验证码 + SmsVerificationCode newSmsVerificationCode = new SmsVerificationCode(); + newSmsVerificationCode.setCreateDateTime(nowDateTime); + newSmsVerificationCode.setSmsNumber(smsMumber); + newSmsVerificationCode.setUserName(loginName); + newSmsVerificationCode.setVerificationCode(verificationCode); + objectDao.saveOrUpdate(newSmsVerificationCode); + } + + /** + * 若同时启用了配置项“loginSecurirtyConfig”时,验证码输入后的验证失败次数也计算为登录失败的次数。 + * @param loginUser 当前登录用户 + * @param nowDateTime 当前时间 + */ + private void loginSecurirtyConfig(User loginUser, Date nowDateTime) { + Date lockEndDate = loginUser.getLockEndDate(); + if(lockEndDate != null){ + //1.判断该用户的锁定截止时间是否晚于当时时间 + if(lockEndDate.after(nowDateTime)){ + throw new SystemException("当前用户被锁定,请联系管理员。"); + } + } + //判断IP是否被锁定 + try { + ipLoginLockRecordManager.isLockedIP(StrutsParamUtils.getRequest()); + } catch (Exception e) { + throw new SystemException("当前用户被锁定,请联系管理员。"); + } + } + + /** + * 获取已经发送成功的最新一条短信验证码 + * @param userName 用户名 + * @param smsMumber 手机号码 + * @return 短信验证码 + */ + @SuppressWarnings("unchecked") + private SmsVerificationCode getLastSmsVerificationCode(String userName, String smsNumber) { + String condition = " where userName = :userName and smsNumber = :smsNumber order by createDateTime desc"; + Map params = new HashMap(); + params.put("userName", userName); + params.put("smsNumber", smsNumber); + List smsVerificationCodeList = objectDao.getCollection(SmsVerificationCode.class.getSimpleName(), condition, params, 0, 1); + if(CollectionUtils.isNotEmpty(smsVerificationCodeList)){ + return smsVerificationCodeList.get(0); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public void validateVerificationCode(String loginName, String smsVerificationCode) { + if(!StringUtils.equals("1", ConfigUtils.getSystemSetConfigByName("enableTwoFactorAuthentication"))){ + return; + } + String needBeStrongPwdWhenModifyPwd = ConfigUtils.getSystemSetConfigByName("needBeStrongPwdWhenModifyPwd"); + if(StringUtils.isBlank(needBeStrongPwdWhenModifyPwd)){ + return; + } + if(StringUtils.isBlank(loginName)){ + throw new SystemException("用户名不能为空!"); + } + if(StringUtils.isBlank(smsVerificationCode)){ + throw new SystemException("验证码不能为空!"); + } + Date nowDateTime = new Date(); + Session session = objectDao.getHibernateSession(); + Query query = session.createQuery(String.format("select po from %s po where name = :name", User.class.getSimpleName())); + query.setParameter("name", loginName); + List userList = query.list(); + if(CollectionUtils.isEmpty(userList)){ + throw new RuntimeException(String.format("用户【%s】不存在!", loginName)); + } + if(userList.size() > 1){ + throw new RuntimeException(String.format("存在多个用户名为【%s】的用户!", loginName)); + } + User loginUser = userList.get(0); + String smsMumber = loginUser.getSmsMumber(); + if(StringUtils.isBlank(smsMumber)){ + throw new SystemException("未绑定手机号码,请补充或联系管理员。"); + } + //若同时启用了配置项“loginSecurirtyConfig”时,验证码输入后的验证失败次数也计算为登录失败的次数。 + loginSecurirtyConfig(loginUser, nowDateTime); + //获取已经发送成功的最新一条短信验证码 + SmsVerificationCode lastSmsVerificationCode = getLastSmsVerificationCode(loginName, smsMumber); + if(lastSmsVerificationCode == null){ + throw new SystemException("验证码失效,请重新获取验证码。"); + } + //验证码在验证成功后直接废弃,需要重新获取验证码 + if(lastSmsVerificationCode.getVerified() == SmsVerificationCode.STATUS_VERIFIED){ + throw new SystemException("验证码失效,请重新获取验证码。"); + } + //同一个验证码有五分钟时效,超过五分钟后需要重新获取,需要重新获取验证码 + if(DateTools.getDateDiff(lastSmsVerificationCode.getCreateDateTime(), nowDateTime, TimeUnit.SECONDS) > SmsVerificationCode.SMS_VALIDITY_DURATION_MIN * 60){ + throw new SystemException("验证码失效,请重新获取验证码。"); + } + if(!StringUtils.equals(lastSmsVerificationCode.getVerificationCode(), smsVerificationCode)){ + //验证码校验失败 + Integer verifyFailTimes = lastSmsVerificationCode.getVerifyFailTimes(); + if(verifyFailTimes == null){ + verifyFailTimes = 0; + } + verifyFailTimes++; + lastSmsVerificationCode.setVerifyFailTimes(verifyFailTimes); + if(verifyFailTimes >= SmsVerificationCode.MAX_VERIFY_FAIL_TIMES){ + //多次验证失败,验证码失效,当成已经验证验证码处理 + lastSmsVerificationCode.setVerified(SmsVerificationCode.STATUS_VERIFIED); + } + runWithTransNewManager.runWith_TRANS_NEW(new RunWithTransNewTask() { + @Override + public void runTask() { + //验证码校验失败时,失败次数加一;达到最大失败次数时,验证码失效并要求用户重新获取 + objectDao.saveOrUpdate(lastSmsVerificationCode); + } + }); + if(verifyFailTimes >= SmsVerificationCode.MAX_VERIFY_FAIL_TIMES){ + throw new SystemException("验证码失效,请重新获取验证码。"); + }else{ + throw new SystemException("验证码错误,请重新输入。"); + } + }else{ + //验证成功后,验证码不能再用 + lastSmsVerificationCode.setVerified(SmsVerificationCode.STATUS_VERIFIED); + objectDao.saveOrUpdate(lastSmsVerificationCode); + } + } + } Index: forgon-core/src/main/java/com/forgon/directory/service/PersonalSettingManagerImpl.java =================================================================== diff -u -r29341 -r40894 --- forgon-core/src/main/java/com/forgon/directory/service/PersonalSettingManagerImpl.java (.../PersonalSettingManagerImpl.java) (revision 29341) +++ forgon-core/src/main/java/com/forgon/directory/service/PersonalSettingManagerImpl.java (.../PersonalSettingManagerImpl.java) (revision 40894) @@ -22,7 +22,13 @@ private UserManager userManager; private RemoteManagerClient remoteManagerClient; + + private SmsVerificationCodeManager smsVerificationCodeManager; + public void setSmsVerificationCodeManager(SmsVerificationCodeManager smsVerificationCodeManager) { + this.smsVerificationCodeManager = smsVerificationCodeManager; + } + public void setSysUserManager(SysUserManager sysUserManager) { this.sysUserManager = sysUserManager; } @@ -54,6 +60,10 @@ Boolean passwordComplexityReq1 = json.optBoolean("passwordComplexityReq1", false); // windowsPasswordComplexityReq:true:值为true则检查密码是否包含英文大写字母(A 到 Z)、英文小写字母(a 到 z)、0 个基本数字(0 到 9)、非字母字符(例如 !、$、#、%)中的两类字符,不检查是否包含账号 Boolean windowsPasswordComplexityReq = json.optBoolean("windowsPasswordComplexityReq", false); + //修改密码前需要校验验证码ZSYY-438 + if(smsVerificationCodeManager != null){ + smsVerificationCodeManager.validateVerificationCode(user.getName(), user.getSmsVerificationCode()); + } if(!passwordComplexityReq1 && !windowsPasswordComplexityReq && passwordNotContainLoginName && user.getPasswd().toLowerCase().indexOf(user.getName().toLowerCase()) != -1){ throw new SystemException("密码不可以包含账号!"); } Index: ssts-datasync/src/main/java/com/forgon/disinfectsystem/verification/action/VerificationCodeAction.java =================================================================== diff -u -r36130 -r40894 --- ssts-datasync/src/main/java/com/forgon/disinfectsystem/verification/action/VerificationCodeAction.java (.../VerificationCodeAction.java) (revision 36130) +++ ssts-datasync/src/main/java/com/forgon/disinfectsystem/verification/action/VerificationCodeAction.java (.../VerificationCodeAction.java) (revision 40894) @@ -1,20 +1,21 @@ package com.forgon.disinfectsystem.verification.action; import java.util.List; - import net.sf.json.JSONObject; - import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; import org.apache.struts2.convention.annotation.Action; import org.apache.struts2.convention.annotation.Namespace; import org.apache.struts2.convention.annotation.ParentPackage; import org.hibernate.Query; import org.hibernate.Session; - +import com.forgon.directory.service.SmsVerificationCodeManager; +import com.forgon.directory.service.SysUserManager; import com.forgon.disinfectsystem.verification.service.VerificationCodeManager; import com.forgon.exception.SystemException; import com.forgon.security.model.User; +import com.forgon.security.service.UserManager; import com.forgon.tools.StrutsParamUtils; import com.forgon.tools.StrutsResponseUtils; import com.forgon.tools.crypto.rsa.RSAEncrypt; @@ -31,8 +32,28 @@ @Action(value = "verificationCodeAction") public class VerificationCodeAction { + final static Logger logger = Logger.getLogger(VerificationCodeAction.class); + private VerificationCodeManager verificationCodeManager; + private SmsVerificationCodeManager smsVerificationCodeManager; + + private SysUserManager sysUserManager; + + private UserManager userManager; + + public void setUserManager(UserManager userManager) { + this.userManager = userManager; + } + + public void setSysUserManager(SysUserManager sysUserManager) { + this.sysUserManager = sysUserManager; + } + + public void setSmsVerificationCodeManager(SmsVerificationCodeManager smsVerificationCodeManager) { + this.smsVerificationCodeManager = smsVerificationCodeManager; + } + private ObjectDao objectDao; public void setObjectDao(ObjectDao objectDao) { @@ -130,4 +151,62 @@ } StrutsResponseUtils.output(result); } + + /** + * 发送短信验证码ZSYY-438 + */ + public void sendAuthenticationCodeSms(){ + JSONObject result = JSONUtil.buildJsonObject(true, "短信验证码发送成功!"); + String loginName = StrutsParamUtils.getPraramValue("loginName", ""); + try { + loginName = RSAEncrypt.decrypt(loginName); + smsVerificationCodeManager.sendVerificationCodeSms(loginName); + } catch (SystemException e) { + result = JSONUtil.buildJsonObject(false, e.getMessage()); + e.printStackTrace(); + logger.error(String.format("用户【%s】验证码发送失败:", loginName, e.getMessage())); + } catch (Exception e) { + result = JSONUtil.buildJsonObject(false, "验证码发送失败,请联系管理员!"); + e.printStackTrace(); + logger.error(String.format("用户【%s】验证码发送失败:", loginName, e.getMessage())); + } + StrutsResponseUtils.output(result); + } + + /** + * 验证码校验ZSYY-438 + */ + public void validateAuthenticationCode(){ + JSONObject result = JSONUtil.buildJsonObject(true, "短信验证码校验成功!"); + String loginName = StrutsParamUtils.getPraramValue("loginName", ""); + String verificationCode = StrutsParamUtils.getPraramValue("verificationCode", ""); + boolean validateSucc = false; + User currentLoginedUser = null; + try { + loginName = RSAEncrypt.decrypt(loginName); + verificationCode = RSAEncrypt.decrypt(verificationCode); + currentLoginedUser = sysUserManager.getUserByPropertyWithLower("name",loginName); + smsVerificationCodeManager.validateVerificationCode(loginName, verificationCode); + logger.info(String.format("用户【%s】验证码校验成功,验证码为:【%s】", loginName, verificationCode)); + validateSucc = true; + } catch (SystemException e) { + result = JSONUtil.buildJsonObject(false, e.getMessage()); + e.printStackTrace(); + logger.error(String.format("用户【%s】验证码校验失败:%s", loginName, e.getMessage())); + } catch (Exception e) { + result = JSONUtil.buildJsonObject(false, "验证码校验失败,请联系管理员!"); + e.printStackTrace(); + logger.error(String.format("用户【%s】验证码校验失败:%s", loginName, e.getMessage())); + } + try { + if(!validateSucc && StringUtils.isNotBlank(verificationCode)){ + //若同时启用了配置项“loginSecurirtyConfig”时,验证码输入后的验证失败次数不需要计算为登录失败的次数。 + userManager.loginSecurirtyConfig(currentLoginedUser, StrutsParamUtils.getRequest()); + } + } catch (Exception e) { + e.printStackTrace(); + } + StrutsResponseUtils.output(result); + } + } Index: forgon-core/src/main/java/com/forgon/directory/service/SmsVerificationCodeManager.java =================================================================== diff -u --- forgon-core/src/main/java/com/forgon/directory/service/SmsVerificationCodeManager.java (revision 0) +++ forgon-core/src/main/java/com/forgon/directory/service/SmsVerificationCodeManager.java (revision 40894) @@ -0,0 +1,22 @@ +package com.forgon.directory.service; + +/** + * 发送验证码的短信的manager + * ZSYY-438 + */ +public interface SmsVerificationCodeManager { + + /** + * 发送验证码的短信 + * @param loginName 当前登录用户名称 + */ + public void sendVerificationCodeSms(String loginName); + + /** + * 校验验证码 + * @param loginName 当前登录用户名称 + * @param authenticationCode 用户输入的验证码 + */ + public void validateVerificationCode(String loginName, String authenticationCode); + +} Index: ssts-datasync/src/main/java/com/forgon/disinfectsystem/verification/dao/VerificationCodeDao.java =================================================================== diff -u -r31653 -r40894 --- ssts-datasync/src/main/java/com/forgon/disinfectsystem/verification/dao/VerificationCodeDao.java (.../VerificationCodeDao.java) (revision 31653) +++ ssts-datasync/src/main/java/com/forgon/disinfectsystem/verification/dao/VerificationCodeDao.java (.../VerificationCodeDao.java) (revision 40894) @@ -34,4 +34,11 @@ */ public VerificationCode getVerificationCodeByMessageId(@Param("messageId") String messageId); + /** + * 发送短信验证码ZSYY-438 + * @param smsMumber 手机号码 + * @param messageContent 短信内容 + */ + public void sendVerificationCodeSms(String smsMumber, String messageContent); + }