第八章 限制密码重试次数
1、实现原理
保证原子性:
单系统:AtomicLong计数
? 集群系统:RedissionClient提供的RAtomicLong计数
1、获取系统中是否已有登录次数缓存,缓存对象结构预期为:"用户名--登录次数"。
2、如果之前没有登录缓存,则创建一个登录次数缓存。
3、如果缓存次数已经超过限制,则驳回本次登录请求。
4、将缓存记录的登录次数加1,设置指定时间内有效
5、验证用户本次输入的帐号密码,如果登录登录成功,则清除掉登录次数的缓存
思路有了,那我们在哪里实现呢?我们知道AuthenticatingRealm里有比较密码的入口doCredentialsMatch方法
查看其实现
2、自定义密码比较器
新建项目shiro-day01-14shiro-RetryLimit
【1】RetryLimitCredentialsMatcher
package com.itheima.shiro.core.impl;
import com.itheima.shiro.core.base.ShiroUser;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/**
* @Description:密码重试比较器
*/
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
private RedissonClient redissonClient;
private static Long RETRY_LIMIT_NUM = 4L;
/**
* @Description 构造函数
* @param hashAlgorithmName 匹配次数
* @return
*/
public RetryLimitCredentialsMatcher(String hashAlgorithmName,RedissonClient redissonClient) {
super(hashAlgorithmName);
this.redissonClient = redissonClient;
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//获得登录吗
String loginName = (String) token.getPrincipal();
//获得缓存
RAtomicLong atomicLong = redissonClient.getAtomicLong(loginName);
long retryFlag = atomicLong.get();
//判断次数
if (retryFlag>RETRY_LIMIT_NUM){
//超过次数设计10分钟后重试
atomicLong.expire(10, TimeUnit.MICROSECONDS);
throw new ExcessiveAttemptsException("密码错误5次,请10分钟以后再试");
}
//累加次数
atomicLong.incrementAndGet();
atomicLong.expire(10, TimeUnit.MICROSECONDS);
//密码校验
boolean flag = super.doCredentialsMatch(token, info);
if (flag){
//校验成功删除限制
atomicLong.delete();
}
return flag;
}
}
【2】重写ShiroDbRealmImpl
修改initCredentialsMatcher方法,使用RetryLimitCredentialsMatcher
package com.itheima.shiro.core.impl;
import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.constant.SuperConstant;
import com.itheima.shiro.core.SimpleCacheManager;
import com.itheima.shiro.core.base.ShiroUser;
import com.itheima.shiro.core.base.SimpleToken;
import com.itheima.shiro.core.ShiroDbRealm;
import com.itheima.shiro.core.bridge.UserBridgeService;
import com.itheima.shiro.pojo.User;
import com.itheima.shiro.utils.*;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Resource;
/**
* @Description:自定义shiro的实现
*/
public class ShiroDbRealmImpl extends ShiroDbRealm {
@Autowired
private UserBridgeService userBridgeService;
@Autowired
private SimpleCacheManager simpleCacheManager;
@Resource(name = "redissonClientForShiro")
private RedissonClient redissonClient;
/**
* @Description 认证方法
* @param authcToken 校验传入令牌
* @return AuthenticationInfo
*/
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
SimpleToken token = (SimpleToken)authcToken;
User user = userBridgeService.findUserByLoginName(token.getUsername());
if(EmptyUtil.isNullOrEmpty(user)){
throw new UnknownAccountException("账号不存在");
}
ShiroUser shiroUser = BeanConv.toBean(user, ShiroUser.class);
String sessionId = ShiroUserUtil.getShiroSessionId();
String cacheKeyResourcesIds = CacheConstant.RESOURCES_KEY_IDS+sessionId;
shiroUser.setResourceIds(userBridgeService.findResourcesIdsList(cacheKeyResourcesIds,user.getId()));
String salt = user.getSalt();
String password = user.getPassWord();
return new SimpleAuthenticationInfo(shiroUser, password, ByteSource.Util.bytes(salt), getName());
}
/**
* @Description 授权方法
* @param principals SimpleAuthenticationInfo对象第一个参数
* @return
*/
@Override
public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
return userBridgeService.getAuthorizationInfo(shiroUser);
}
/**
* @Description 清理缓存
*/
@Override
public void doClearCache(PrincipalCollection principalcollection) {
String sessionId = ShiroUtil.getShiroSessionId();
simpleCacheManager.removeCache(CacheConstant.ROLE_KEY+sessionId);
simpleCacheManager.removeCache(CacheConstant.RESOURCES_KEY+sessionId);
simpleCacheManager.removeCache(CacheConstant.TOKEN+sessionId);
}
/**
* @Description 加密方式
*/
@Override
public void initCredentialsMatcher() {
RetryLimitCredentialsMatcher matcher = new RetryLimitCredentialsMatcher(SuperConstant.HASH_ALGORITHM,redissonClient);
matcher.setHashIterations(SuperConstant.HASH_INTERATIONS);
setCredentialsMatcher(matcher);
}
}
3、测试
访问http://127.0.0.1/shiro/login,使用admin账号输入错误密码5次
第九章 在线并发登录人数控制
1、实现原理
在实际开发中,我们可能会遇到这样的需求,一个账号只允许同时一个在线,当账号在其他地方登陆的时候,会踢出前面登陆的账号,那我们怎么实现
- 自定义过滤器:继承AccessControlFilter
- 使用redis队列控制账号在线数目
实现步骤:
1、只针对登录用户处理,首先判断是否登录
2、使用RedissionClien创建队列
3、判断当前sessionId是否存在于此用户的队列=key:登录名 value:多个sessionId
4、不存在则放入队列尾端==>存入sessionId
5、判断当前队列大小是否超过限定此账号的可在线人数
6、超过:
*从队列头部拿到用户sessionId
*从sessionManger根据sessionId拿到session
*从sessionDao中移除session会话
7、未超过:放过操作
2、代码实现
【1】KickedOutAuthorizationFilter
package com.itheima.shiro.filter;
import com.itheima.shiro.core.impl.RedisSessionDao;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.utils.ShiroUserUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.redisson.api.RDeque;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* @Description:
*/
@Log4j2
public class KickedOutAuthorizationFilter extends AccessControlFilter {
private RedissonClient redissonClient;
private SessionDAO redisSessionDao;
private DefaultWebSessionManager sessionManager;
public KickedOutAuthorizationFilter(RedissonClient redissonClient, SessionDAO redisSessionDao, DefaultWebSessionManager sessionManager) {
this.redissonClient = redissonClient;
this.redisSessionDao = redisSessionDao;
this.sessionManager = sessionManager;
}
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
if (!subject.isAuthenticated()) {
//如果没有登录,直接进行之后的流程
return true;
}
//存放session对象进入队列
String sessionId = ShiroUserUtil.getShiroSessionId();
String LoginName = ShiroUserUtil.getShiroUser().getLoginName();
RDeque<String> queue = redissonClient.getDeque("KickedOutAuthorizationFilter:"+LoginName);
//判断sessionId是否存在于此用户的队列中
boolean flag = queue.contains(sessionId);
if (!flag) {
queue.addLast(sessionId);
}
//如果此时队列大于1,则开始踢人
if (queue.size() > 1) {
sessionId = queue.getFirst();
queue.removeFirst();
Session session = null;
try {
session = sessionManager.getSession(new DefaultSessionKey(sessionId));
}catch (UnknownSessionException ex){
log.info("session已经失效");
}catch (ExpiredSessionException expiredSessionException){
log.info("session已经过期");
}
if (!EmptyUtil.isNullOrEmpty(session)){
redisSessionDao.delete(session);
}
}
return true;
}
}
【2】修改ShiroConfig
/**
* @Description 自定义过滤器定义
*/
private Map<String, Filter> filters() {
Map<String, Filter> map = new HashMap<String, Filter>();
map.put("roleOr", new RolesOrAuthorizationFilter());
map.put("kickedOut", new KickedOutAuthorizationFilter(redissonClient(), redisSessionDao(), shiroSessionManager()));
return map;
}
【3】修改authentication.properties
#静态资源不过滤
/static/**=anon
#登录链接不过滤
/login/**=anon
#访问/resource/**需要有admin的角色
/resource/**=role-or[MangerRole,SuperAdmin]
#其他链接是需要登录的
/**=kickedOut,auth
3、测试
使用谷歌访问http://127.0.0.1/shiro/login,使用admin/pass登陆
使用IE再访问http://127.0.0.1/shiro/login,使用admin/pass登陆
再刷新谷歌浏览器,发现账号被踢出