Java后端如何保证用户注册登录接口的安全性之用户登录篇

       用户登录接口的设计,首先要考虑的是防止用户的账号被暴力破解,常用的是使用谷歌图形验证码,同时还需要对用户每天的“连续”登录错误次数进行限制,假设一个账号用户每天连续登陆的错误次数是十次,那么当用户第十一次输入错误时,就应该锁住当前用户的账户。但是我们真正的目的是为了保证用户的账号安全,屏蔽攻击者的恶意调用的同时,又要保证真实用户的正常使用,此时就应该提供重置登录密码的操作,密码重置成功之后,用户在当天又能恢复登录。其次需要考虑的是用户登录密码在传输过程中的安全性的问题,虽说现在的HTTPS 的安全性已经足够,但是对于一些安全性很高的项目,仍旧需要有自己的加密方式来保护用户的敏感信息,比如我这里使用的RSA加密方法,具体的论述以及使用,请参考我的另一篇博客:https://blog.csdn.net/weixin_42023666/article/details/89706659。

        下面废话也不多说,直接上代码,当然了,还是老规矩,只分享service层的代码来陈述具体的思路,至于全套代码无法提供(还请谅解)。另外代码中的注释已经有足够多的解释,千万别忘了他们。

 


package sy.service.impl;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;

import DateUtil;
import sy.util.mobile.MobileUtil;

@Service("userService")
public class UserServiceImpl implements UserService {

	private static final Logger logger = Logger.getLogger(UserServiceImpl.class);

	/**
	登录时,当用户输入手机号后,就调用该接口判断当前手机号是否注册,
	如果是已经注册过的,后台就会生成rsa公私钥对并将公钥返回给前端;
	如果没有注册,就提醒用户当前手机号未注册,以此来提高用户体验
	*/
	@Override
	public Result findUser(String mobile, HttpServletRequest request) {

		Result result = new Result();

		// 判断传入的手机号格式是否正确
		if (StringUtils.isBlank(mobile)) {
			result.setSuccess(false);
			result.setMsg("缺少必要参数");
			return result;
		}
		mobile = mobile.trim();

		// 判断传入的手机号格式是否正确
		if (mobile.length() != 11 || !MobileUtil.isMobileNum(mobile)) {
			result.setSuccess(false);
			result.setMsg("传入的手机号格式不正确");
			return result;
		}

		//查询当前手机号是否注册,此处省略具体的数据库相关的操作
		if(未注册){//伪代码,实际使用需换成你自己的dao层查询
			result.setSuccess(false);
			result.setMsg("当前手机号没有注册");
			return result;
		}

		String rsaPublicKey = "login_rsa_public_" + mobile;
		String rsaPrivateKey = "login_rsa_private_" + mobile;

		Long rsa = RedisUtils.ttl(rsaPublicKey);
		if (rsa > 60 *15+5) {// 原来的公钥有效时间大于15分钟,继续使用
			result.setStatus(RedisUtils.get(rsaPublicKey));//该字段保存生成的rsa公钥并返回给前端
		} else {
			Map map = RSAEncrypt.genKeyPair();
			if (map != null && map.get(0) != null) {
				RedisUtils.set(rsaPublicKey, map.get(0), 60 * 120 + 120);// 保存公钥
				RedisUtils.set(rsaPrivateKey, map.get(1), 60 * 120 + 150);// 保存私钥,2小时有效
				result.setStatus(map.get(0));
			}
		}
		
		// 登录失败超过三次就要输入验证码,判断当前用户是否需要设置验证码
		String times = RedisUtils.get( mobile + "_login_error_times");
		if (times != null && new Integer(times).intValue() >= 3) {
			result.setPage(1);//告诉前端,此次请求需要输入谷歌图形验证码;此时公钥其实已经一并返回给前端,
				//如果前端没有携带谷歌图形验证码就直接调用登录接口,请求依旧会失败,因为登录接口有做检验,有判断当前用户今日登录次数是否超次数
		}
		
		result.setSuccess(true);

		return result;
	}

	/**
	用户登录接口service层的代码,按照惯例,controller层就不贴出来了
	mobile是登录的手机号,pwd是使用rsa公钥加密后的密码,code是谷歌图形验证码
	*/
	@Override
	public Result login(String mobile,String pwd,String code, HttpServletRequest request) {
		Result result = new Result();

		// 判断请求参数是否正确
		if (StringUtils.isBlank(mobile) || StringUtils.isBlank(pwd)) {
			result.setSuccess(false);
			result.setMsg("缺少必要的参数");
			return result;
		}

		mobile = mobile.trim();
		// 判断传入的手机号格式是否正确
		if (mobile.length() != 11 || !MobileUtil.isMobileNum(mobile)) {
			result.setSuccess(false);
			result.setMsg("传入的手机号格式不正确");
			return result;
		}

		// 判断是否有图片验证码
		String kaptcha = RedisUtils.get("login_captcha_" + mobile);
		if (kaptcha != null && code != null && !code.toLowerCase().equals(kaptcha)) {
			result.setPage(1);
			result.setSuccess(false);
			result.setMsg("您输入的验证码有误,请重新输入");
			return result;
		}

		String rsaPublicKey = "login_rsa_public_" + mobile;
		String rsaPrivateKey = "login_rsa_private_" + mobile;

		// 从redis数据库获取私钥
		String privateKey = RedisUtils.get(rsaPrivateKey);
		if (StringUtils.isBlank(privateKey)) {

			RedisUtils.del(rsaPublicKey);
			RedisUtils.del(rsaPrivateKey);

			result.setPage(1);
			result.setSuccess(false);
			result.setMsg("密钥验证失败,请输入新的验证码后再登录");
			result.setStatus("1");
			return result;
		}

		// 判断当前手机号的登录失败次数,防止有人暴力破解用户的密码
		String limitKey =  mobile + "_login_error_times";
		String limitTimes = RedisUtils.get(limitKey);
		Integer times = 1;
		if (limitTimes != null) {
			if (new Integer(limitTimes).intValue() >= 6) {
				result.setSuccess(false);
				result.setMsg("当前账号今日登录失败次数超过6次,为保证您的账号安全,系统已锁定当前账号,您可明天再登录或立即重置密码后使用新密码登录!");
				return result;
			}
			times = new Integer(limitTimes) + 1;

			if (kaptcha == null && times > 3) {
				result.setPage(1);//该字段如果为1,表示前端需要用户输入谷歌图形验证码接口
				result.setSuccess(false);
				result.setMsg("您没有输入验证码");
				return result;
			}
		}

		// 使用私钥解密后的密码
		pwd = RSAEncrypt.decrypt(pwd, privateKey);
		if (pwd == null) {
			result.setSuccess(false);
			result.setMsg("解密失败,您传入的数据有误");
			result.setStatus("1");
			return result;
		}

		//核心业务隐身符,此处通过手机号获取用户的密码,然后通过对应的加密方式来检验密码是否一致性
		if(密码不正确){//伪代码
			// 记录密码输入错误数
			RedisUtils.set(limitKey, times + "", DateUtil.getTodaySurplusTime());

			if (times > 2) {
				result.setPage(1);// 密码输错两次,就需要输入谷歌图形验证码
			}

			result.setSuccess(false);
			result.setMsg("登录密码错误");
			return result;
		}
		
		//密码登录成功,执行登录成功后的核心业务,此处省略
		
		
		// 删除登录失败次数的标识
		if (limitTimes != null) {
			RedisUtils.del(limitKey);
		}
		// 删除验证码
		if (kaptcha != null) {
			RedisUtils.del("login_captcha_" + mobile);
		}
		// 删除保存的公钥私钥
		RedisUtils.del(rsaPublicKey);
		RedisUtils.del(rsaPrivateKey);
				
		result.setSuccess(true);

		return result;
	}
	
	/**
	获取用户重置登录密码所需要的手机短信验证码的接口
	mobile是手机号,contents是谷歌图形验证码(该值可能为null)
	*/
	@Override
	public Result sendMobileCode(String mobile, String contents, HttpServletRequest request) {
		Result result = new Result();

		if (StringUtils.isBlank(mobile)) {
			result.setSuccess(false);
			result.setMsg("请求参数不全");
			return result;
		}

		mobile = mobile.trim();
		// 判断传入的手机号格式是否正确
		if (mobile.length() != 11 || !MobileUtil.isMobile(mobile)) {
			result.setSuccess(false);
			result.setMsg("传入的手机号格式不正确");
			return result;
		}

		//业务代码隐身符1,此处需要用该手机号去查询当前帐号是否注册,只有注册的号码才能调用重置密码的手机短信验证码
		if(手机号未注册){
			result.setSuccess(false);
			result.setMsg("当前手机号未注册,请先注册");
			return result;
		}

		String kapchatKey = "kaptcha_reset_login_" + mobile;//记录此次的谷歌图形验证码,如果有的话
		String mobileKey = "mobile_reset_login_" + mobile;//记录此次的重置登录密码的手机短信验证码
		String todayKey = "mobile_code_times_" + mobile;//记录今日发送短信验证码的次数

		// 验证码十分钟内有效,2分钟后才能重新发送
		Long times = RedisUtils.ttl(mobileKey);
		if (times > 60 * 18) {
			result.setSuccess(false);
			result.setMsg("距离您上次发送验证码不足两分钟,请两分钟后再尝试获取");
			return result;
		}
		
		// 判断当前手机号今天发送密码次数是否已达上线,每天9条,具体条数根据自己的需求制定
		String todayTimes = RedisUtils.get(todayKey);
		int todayCount = 1;
		if (todayTimes != null) {
			todayCount = new Integer(todayTimes);
			if (todayCount >= 9) {
				result.setSuccess(false);
				result.setMsg("当前手机号今日发送验证码已达上限,请明日再来");
				return result;
			}
			todayCount++;
		}
		
		//今天调用接口超过三次,就需要调谷歌图形验证码,这也是对短信接口的一种保护机制
		if(todayCount>3){
			result.setPage(1);//只要今日获取验证码次数超过三次,之后每次调用短信验证码接口时都要先调用谷歌验证码
			
			if(StringUtils.isBlank(contents)){
				result.setSuccess(false);
				result.setMsg("为保证您账号安全,请输入图形验证码获取手机短信");
				return result;
			}
			
			// 检验图形验证码
			String kapchat = RedisUtils.get(kapchatKey);
			if (kapchat == null) {
				result.setMsg("图形验证码已失效,请重新输入");
				result.setSuccess(false);
				return result;
			} else if (!kapchat.equals(contents.toLowerCase())) {
				result.setSuccess(false);
				result.setMsg("您输入的验证码错误,请重新输入");
				return result;
			}
		}else if(todayCount==3){
			result.setPage(1);
		}

		String msg = null;
		Map map = null;
		String publicKey = null;
		String rsaPublicKey = "rsa_public_reset_login_"  + mobile;// redis保存的公钥的key
		String rsaPrivateKey = "rsa_private_reset_login_"  + mobile;// redis中保存私钥的key

		try {
			
			//判断原来的公钥是否可以使用,如果可以,就不需要重新生成新的rsa公钥私钥对
			Long rsa = RedisUtils.ttl(rsaPublicKey);
			if (rsa > 60 * 20 + 10) {// 原来的公钥有效时间大于20分钟,继续使用
				publicKey = RedisUtils.get(rsaPublicKey);
			} else {
				// 生成公钥和私钥
				map = RSAEncrypt.genKeyPair();
				if (map == null || map.get(0) == null) {
					result.setSuccess(false);
					result.setMsg("生成公钥私钥失败,请联系管理员");
					return result;
				}
			}

			// 要发送的验证码
			String code = (int) ((Math.random() * 9 + 1) * 100000) + "";
		
			// 业务代码隐身符2,此处省略调用第三方接口给用户手机发送验证码的操作
			
			if(短信发送成功){
				result.setSuccess(true);
				result.setMsg("您的手机验证码发送成功,请注意查收,本验证码20分钟内有效");

				// 保存私钥,返回公钥
				if (map != null) {
					RedisUtils.set(rsaPublicKey, map.get(0), 60 * 90 + 250);// 2小时有效
					RedisUtils.set(rsaPrivateKey, map.get(1), 60 * 90 + 300);// 2小时有效
					result.setStatus(map.get(0));
				}
				if (publicKey != null) {
					result.setStatus(publicKey);
				}

				// 保存验证码
				RedisUtils.set(mobileKey, code, 60 * 20 + 5);

				// 记录本号码发送验证码次数
				RedisUtils.set(todayKey, todayCount + "", MobileUtil.getTodaySurplusTime());

				// 删除图形验证码
				RedisUtils.del(kapchatKey);
				
			} else {
				result.setSuccess(false);
				result.setMsg("短信验证码发送失败,请联系管理员:" + msg);
			}

		} catch (Exception e) {
			result.setSuccess(false);
			result.setMsg("获取短信验证码异常:" + e.getMessage());
		}
		return result;
	}


	/**
	通过手机号重置用户登录密码的service层代码
	mobile是用户手机号,contents是短信验证码,pwd是新的登录密码
	*/
	@Override
	public Result resetSecret(String mobile, String contents, String pwd,HttpServletRequest request) {
		Result result = new Result();

		if (StringUtils.isBlank(mobile) || StringUtils.isBlank(contents) || StringUtils.isBlank(pwd)) {
			result.setSuccess(false);
			result.setMsg("缺少必要的参数");
			return result;
		}

		mobile = mobile.trim();
		// 判断传入的手机号格式是否正确
		if (mobile.length() != 11 || !MobileUtil.isMobile(mobile)) {
			result.setSuccess(false);
			result.setMsg("传入的手机号格式不正确");
			return result;
		}

		//此处可以再次考虑判断当前的手机号是否已经注册,防止前端调错了接口
		//······
		
		
		String mobileKey = "mobile_reset_login_" + mobile;// 存储到redis中的验证码的key
		String rsaPublicKey = "rsa_public_reset_login_" + mobile;// 存储到redis的公钥的key
		String rsaPrivateKey = "rsa_private_reset_login_" + mobile;// 存储到redis的私钥的key

		// 校验短信验证码
		String code = RedisUtils.get(mobileKey);
		if (code == null) {
			result.setPage(1);// 需要重新获取验证码
			result.setSuccess(false);
			result.setMsg("当前验证码已失效,请获取最新验证码后再进行此操作");
			return result;
		} else if (!code.equals(contents)) {
			result.setSuccess(false);
			result.setMsg("您输入的验证码不正确,请重新输入(不用重新获取)");
			return result;
		}

		// 获取保存的私钥
		String privateKey = RedisUtils.get(rsaPrivateKey);
		if (privateKey == null) {
			result.setSuccess(false);
			result.setMsg("没有获取到私钥信息,请重新获取验证码");
			return result;
		}

		// 私钥解析密码
		pwd = RSAEncrypt.decrypt(pwd, privateKey);
		if (pwd == null) {
			result.setSuccess(false);
			result.setMsg("非法请求,传入的密码无法解析");
			return result;
		}

		//此处的密码应该是用户的密码明文,可以对用户输入新密码格式进行后端校验
		

		// 删除缓存的key
		RedisUtils.del(mobileKey);
		RedisUtils.del(RsaPublicKey);
		RedisUtils.del(RsaPrivateKey);
		
		// 删除用户今日登录失败次数的标识,如果有
		RedisUtils.del( mobile + "_login_error_times");

		/* 以下几行是添加操作流水 */

		result.setSuccess(true);
		result.setMsg("密码重置成功");
		return result;
	}

}

 

下面是工具类 MobileUtil.java

package util;

import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;

/**
    * @ClassName: MobileUtil  
    * @author hqq  
    *
 */
public class MobileUtil {
	
	/**
	 * 正则表达式:验证手机号
	 */
	private static final String REGEX_MOBILE = "^((13[0-9])|(15[^4,\\D])|(18[0-3,5-9])|(17[0-9]))\\d{8}$";
	
	


	/**
	 * 判断是否是手机号格式,如果传入的是空串,返回false
	 * @param mobile
	 * @return 校验通过返回true,否则返回false
	 */
	public static boolean isMobile(String mobile) {
		if(StringUtils.isBlank(mobile)){
			return false;
		} 
		
		return Pattern.matches(REGEX_MOBILE, mobile);
	}

	/**
	 * 获取今日的剩余时间,单位是秒
	 * @return
	 */
	public static Integer getTodaySurplusTime(){
		Calendar c = Calendar.getInstance();
		long now = c.getTimeInMillis();
		c.add(Calendar.DAY_OF_MONTH, 1);
		c.set(Calendar.HOUR_OF_DAY, 0);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.MILLISECOND, 0);
		long millis = c.getTimeInMillis() - now+2000;

		return (int)(millis/1000);
	}
}

另外里面涉及到的短信验证码接口和谷歌图形验证码的接口,可以参考一下我的另外几篇博客:

 

Jedis常用工具类,包含一些具有事务的设置值的方法

springmvc使用谷歌captcha生成图片验证码,并将验证码图片以二进制流的方式返回给前端(app和pc端都能调用)

Java后端防止获取短信验证码接口被恶意调用的代码实现

Java后端生成RSA随机密钥对,并实现前端(app和web)使用公钥加密,后端使用私钥解密

Java后端如何保证用户注册登录接口的安全性之用户注册篇

 

 

你可能感兴趣的:(Java后端)