十.shiro加密解密,短信免密登录,限制密码重试次数、防止暴力破解

一。加密解密

存储尽量不要用明文密码,且不可逆的加密存储

1.加密算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的 数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐),比如 加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一 些 md5 解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相 对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐); 这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。

2.过程简介

我们知道用户登录时,在realm的doGetAuthenticationInfo方法中,我们将根据账户查出来的密码放在了SimpleAuthenticationInfo对象返回,shiro会帮我们比对用户输入的密码和从数据库查出来的密码,如果不正确就会抛异常IncorrectCredentialsException,我们在登录方法外catch异常显示在页面,比对正确就没有异常。

简单来说,加密就是:创建用户的时候我们将 用户传进来的明文密码 用算法工具类加密,存入数据库。

而解密就是:shiro提供了CredentialsMatcher 将我们采用的加密算法注入,将CredentialsMatcher注入realm,shiro通过这个CredentialsMatcher将用户输入的密码也加密,进行密码比对。 所以接下来短信免密登录、限制密码重试次数我们都可以通过继承重写CredentialsMatcher的方法实现

3.实现

1.用户注册时 生成密码,加密存储。

采用md5,盐用随机数,2代表做两次哈希运算

public void registeStu(StuRegister stuRegister) {
		String salt=CodeGenerateUtils.getRandNum(4);
		DlStudentInfo stu=new DlStudentInfo();
		stu.setDlStudentPassword(new SimpleHash("md5", stuRegister.getPassword(), salt, 2).toString());
		stu.setDlStudentSalt(salt);
		studentInfoMapper.insert(stu);
}

2。realm doGetAuthenticationInfo ,返回SimpleAuthenticationInfo,加入盐(从用户表查到的对应的盐),和从库中查出的密码

	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken authcToken) throws AuthenticationException {
		userInfo是根据账户查出的用户对象
		SimpleAuthenticationInfo authcInfo=new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), this.getName());
		if(userInfo.getSalt()!=null) {
			authcInfo.setCredentialsSalt(ByteSource.Util.bytes(userInfo.getSalt()));
		}
		return authcInfo;
	}

3.shiro配置在realm中注入密码凭证器

	/**
	 * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
	 * @return
	 */
	@Bean
	public DLingShiroRealm DLingShiroRealm() {
		DLingShiroRealm myShiroRealm = new DLingShiroRealm();
		myShiroRealm.setCredentialsMatcher(credentialsMatcher());
		myShiroRealm.setUserService(userService);
		return myShiroRealm;
	}
	
	public HashedCredentialsMatcher credentialsMatcher() {
		HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
		credentialsMatcher.setHashAlgorithmName("md5");
		credentialsMatcher.setHashSalted(true);
		credentialsMatcher.setHashIterations(2);
		credentialsMatcher.setStoredCredentialsHexEncoded(true);
		return credentialsMatcher;
	}

通过 credentialsMatcher.hashAlgorithmName=md5 指定散列算法为 md5,需要和生成密 码时的一样;

credentialsMatcher.hashIterations=2,散列迭代次数,需要和生成密码时的意义;

credentialsMatcher.storedCredentialsHexEncoded=true 表示是否存储散列后的密码为 16 进 制,需要和生成密码时的一样,默认是 base64;

此处最需要注意的就是 HashedCredentialsMatcher 的算法需要和生成密码时的算法一样。另 外 HashedCredentialsMatcher 会 自 动 根 据 AuthenticationInfo 的 类 型 是 否 是 SaltedAuthenticationInfo 来获取 credentialsSalt 盐

二。免密登录

如果采用短信登录的话,主要是校验验证码,不需要校验密码。但需求是密码登录和短信登录都有,该怎么做?

发送短信看我这篇:https://blog.csdn.net/u014203449/article/details/80683014

1.思路

HashedCredentialsMatcher 中完成密码校验的方法是doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info),参数是令牌和认证信息。这个令牌token 就是在登录时subject.login(token)的这个令牌。

所在我们可以继承UsernamePasswordToken 令牌 ,多加一个loginType属性,用于区分登录方式。

在doCredentialsMatch方法中,如果登录方式是免密登录,就直接放过返回true密码校对成功(前提自己校对验证码),如果是密码登录,则直接返回父类UsernamePasswordToken的doCredentialsMatch方法执行

2.实现

自定义NoPasswordToken

public class NoPasswordToken extends UsernamePasswordToken {
	
	private static final long serialVersionUID = -2564928913725078138L;

	private String loginType;

	
	/**
	 * 账号密码登录
	 * @param type
	 * @param username
	 * @param password
	 */
	public NoPasswordToken(String username, String password) {
		super(username,password);
		
		this.loginType=BaseConstants.LoginType.PASSWORD.getCode();
	}
	
	/**
	 * 免密登录
	 */
	public NoPasswordToken(String username) {
		super(username,"");
	
		this.loginType=BaseConstants.LoginType.NOPASSWD.getCode();
	}
}

免密登录和密码登录写两个接口,密码登录:

	public Message<String> userLogin(UserStuLogin user){
		if(user==null){
			return Message.ok("登录失败");
		}
		String account=user.getAccount();
		String password=user.getPassword();
		NoPasswordToken token = new NoPasswordToken(account,password);
		Subject currentUser = SecurityUtils.getSubject();
		
		try {
			currentUser.login(token);
		} catch(IncorrectCredentialsException e){
			return Message.ok("密码错误");
		} catch (AuthenticationException e) {
			return Message.ok("登录失败"+e.getMessage());
		}

免密登录:

	public void userMsgLogin(UserMsgLogin userMsgLogin) {
		NoPasswordToken token=new NoPasswordToken(userMsgLogin.getAccount());
		Subject subject = SecurityUtils.getSubject();
		try {
			subject.login(token);
		}  catch (AuthenticationException e) {
			throw new DataValidateFiledException("登录失败");
		}
	}

shiro配置密码凭证器

	@Bean
	public DLingShiroRealm DLingShiroRealm() {
		DLingShiroRealm myShiroRealm = new DLingShiroRealm();
		myShiroRealm.setCredentialsMatcher(credentialsMatcher());
		myShiroRealm.setUserService(userService);
		return myShiroRealm;
	}
	
	/**
	 * 密码凭证器,免密,避免暴力破解
	 * @return
	 */
	@Bean
	public RetryLimitHashedCredentialsMatcher credentialsMatcher() {
		RetryLimitHashedCredentialsMatcher credentialsMatcher=new RetryLimitHashedCredentialsMatcher();
		
		credentialsMatcher.setHashAlgorithmName("md5");
		credentialsMatcher.setHashSalted(true);
		credentialsMatcher.setHashIterations(2);
		credentialsMatcher.setStoredCredentialsHexEncoded(true);
		credentialsMatcher.setRedissonClient(redissonClient);
		return credentialsMatcher;
	}

自定义RetryLimitHashedCredentialsMatcher:

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

	@Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        
		NoPasswordToken noPasswordToken=(NoPasswordToken) token;
		//如果是免密,就不需要核对密码了
		if(noPasswordToken.getLoginType().equals(BaseConstants.LoginType.NOPASSWD.getCode())) {
			return true;
		}
    return super.doCredentialsMatch(token, info);
}
		

三。限制密码重试次数、防止暴力破解

用redis记录一个账号尝试密码的次数和时间,如果到5次 就锁定30分钟

1.思路

在自定义的RetryLimitHashedCredentialsMatcher,调用HashedCredentialsMatcher 密码校验方法前,从redis中取出此账号上次登录的时间和重试的次数,如果次数超过5次并且距离上次登录的时间在30分钟内,就抛异常不允许登录,时间超过30分钟清空记录。

2.实现

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

	//@Autowired
	RedissonClient redissonClient;
    
    public RetryLimitHashedCredentialsMatcher() {
    	
    }
    public RedissonClient getRedissonClient() {
		return redissonClient;
	}
	public void setRedissonClient(RedissonClient redissonClient) {
		this.redissonClient = redissonClient;
	}
	@Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        
		NoPasswordToken noPasswordToken=(NoPasswordToken) token;
		//如果是免密,就不需要核对密码了
		if(noPasswordToken.getLoginType().equals(BaseConstants.LoginType.NOPASSWD.getCode())) {
			return true;
		}
		
		
		String username = (String)token.getPrincipal();
        //retry count + 1
        RMap<String, UserRetrycredentials> map = redissonClient.getMap("retrycredentials");
        UserRetrycredentials userRetrycredentials = map.get(username);
        
        Date now = new Date();
        
        if(userRetrycredentials==null) {
        	userRetrycredentials = new UserRetrycredentials();
        	
        	userRetrycredentials.setRetryCount(0);
        	
        	userRetrycredentials.setTime(now);
        	map.put(username,userRetrycredentials);
        }else if(userRetrycredentials.getRetryCount()>5
        		&&now.getTime()-userRetrycredentials.getTime().getTime()<1800000) {
        	throw new ExcessiveAttemptsException("密码填写错误5次,账号已被锁定,请30分钟后再登录");
        }else if(now.getTime()-userRetrycredentials.getTime().getTime()>1800000) {
        	
        	userRetrycredentials.setRetryCount(0);
        	userRetrycredentials.setTime(now);
        }
        
        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            //clear retry count
        	map.remove("retrycredentials");
        }else {
        	 userRetrycredentials.setRetryCount(userRetrycredentials.getRetryCount()+1);
        	 userRetrycredentials.setTime(now);
             map.put(username, userRetrycredentials);
        }
        return matches;
    }
}

猜你喜欢

转载自blog.csdn.net/u014203449/article/details/80888650
今日推荐