概述
假设我们需要有这样一个场景:如果用户连续输错5次密码,那可能说明有人在搞事情,所以需要暂时冻结该账户的登录功能
关于Shiro整合JWT,可以看这里:Springboot实现Shiro+JWT认证
假设我们的项目中用到了shiro,因为Shiro是建立在完善的接口驱动设计和面向对象原则之上的,支持各种自定义行为,所以我们可以结合Shiro框架的认证模块和redis来实现这个功能。
思路
我们大体的思路如下:

- 用户登录
- Shiro去Redis检查账户的登录错误次数是否超过规定范围(超过了就是所谓的冻结)
- Shiro进行密码比对
- 如果登录失败,则去Redis里记录:登录错误次数+1
- 如果密码正确,则登录成功,删除Redis里的登录错误记录
前期准备
除了需要用到Shiro以外,我们也需要用到Redis,这里需要先配置好RedisTemplate,(由于这个不是重点,我就把代码和配置方法贴在文章的最后了),另外,在Controller层,登录接口的异常处理除了之前的登录错误,还需要新增一个账户冻结类的异常,代码如下:
@PostMapping(value = "/login")
public AccountVO login(String userName, String password){
//尝试登录
Subject subject = SecurityUtils.getSubject();
try {
//通过shiro提供的安全接口来进行认证
subject.login(new UsernamePasswordToken(userName, password));
} catch (ExcessiveAttemptsException e1) {
//新增一个账户锁定类错误
throw new AccountLockedException();
} catch (Exception e) {
//其他的错误判定
throw new LoginFailed();
}
//聚合登录信息
AccountVO account = accountService.getAccountByUserName(userName);
//返回正确登录的结果
return account;
}
自定义Shiro认证管理器
HashedCredentialsMatcher
当你在上面的Controller层调用subject.login方法后,会进入到自定义的Realm里去,然后慢慢进入到Shiro当前的Security Manager里定义的HashedCredentialsMatcher认证管理器的doCredentialsMatch方法,进行密码匹配,原版代码如下:
/**
* This implementation first hashes the {@code token}'s credentials, potentially using a
* {@code salt} if the {@code info} argument is a
* {@link org.apache.shiro.authc.SaltedAuthenticationInfo SaltedAuthenticationInfo}. It then compares the hash
* against the {@code AuthenticationInfo}'s
* {@link #getCredentials(org.apache.shiro.authc.AuthenticationInfo) already-hashed credentials}. This method
* returns {@code true} if those two values are {@link #equals(Object, Object) equal}, {@code false} otherwise.
*
* @param token the {@code AuthenticationToken} submitted during the authentication attempt.
* @param info the {@code AuthenticationInfo} stored in the system matching the token principal
* @return {@code true} if the provided token credentials hash match to the stored account credentials hash,
* {@code false} otherwise
* @since 1.1
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
可以发现,原版的逻辑很简单,就做了两件事,获取密码,比对密码。
由于我们需要联动Redis,在每次登录前都做一次冻结检查,每次遇到登录失败之后还需要实现对redis的写操作,所以现在需要重写一个认证管理器去配置到Security Manager里。
CustomMatcher
我们自定义一个CustomMatcher,这个类继承了HashedCredentialsMatcher,唯独重写了doCredentialsMatch方法,在这里面加入了我们自己的逻辑,代码如下:
import com.imlehr.internship.redis.RedisStringService;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Lehr
* @create: 2020-02-25
*/
public class CustomMatcher extends HashedCredentialsMatcher {
//这个是redis里的key的统一前缀
private static final String PREFIX = "USER_LOGIN_FAIL:";
@Autowired
RedisStringService redisUtils;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//检查本账号是否被冻结
//先获取用户的登录名字
UsernamePasswordToken myToken = (UsernamePasswordToken) token;
String userName = myToken.getUsername();
//初始化错详7FVB7G&VF5FVFR&VF5FVFSРТXiXZW&VF>{ZW&^ZkKi{n{N&WТ&fPТ&WGW&ТV&2&WBf7G&W7G&fRТ&W7VfSТG'ТfTFF&VF5FVFR4fRТF6WBWfRТ&W7VG'VSТ6F6W6WFТRD76vRТ&WGW&W7VТТXiXZW&VF>{Z&^ZkKi{n{N&WТ&fPТ&W&PТ&WGW&ТV&2&WBf7G&W7G&fRW&RТ&W7VfSТG'ТfTFF&VF5FVFR4fRТF6WBWfRТ&VF5FVFR&RWW&RFV4Т&W7VG'VSТ6F6W6WFТRD76vRТ&WGW&W7VТРТ&VF>{ZТ&WТ&WGW&ТV&27BvWBf7G&WТ7B&W7VТG'ТfTFF&VF5FVFR4fRТ&W7VFvWBWТ6F6W6WFТRD76vRТ&WGW&W7VТТXNijVF>{ZKZyFWТ&WТ&WGW&ТV&2&7G2f7G&WТ&W7VfSТG'Т&W7V&VF5FVFR4WWТ6F6W6WFТRD76vRТ&WGW&W7VТТ&VF>jhWXNZyGfPТ&WТ&WGW&ТV&2&VfRf7G&WТ&W7VfSТG'ТbW7G2WТ&VF5FVFRFRWТ&W7VG'VSТ6F6W6WFТRD76vRТ&WGW&W7VТТ&VF>jhW>hxNZyGfPТ&W0Т&WGW&ТV&2f&VfRf7G&W2ТfG&WW2Т&VfRWТУFcУjX[>K&F>Z[ji[Xk{>yNih~z[K{NZIyX[56&F>y[Xk{>Xh^Z{J.zKK^XNih~zhn{~{XyNyX[>ih~z[ZJ~Z^YZIiJ |