Shiro+Redis控制用户并发登录
前言
最近些项目时需要控制用户同一时间在线登陆的人数,所以就从网上找了一些相关代码学习学习。不料各种方法使我学的头昏眼花,最后索性自己造一个简易版的算球。以下是这个过滤器的具体写法。
思路
用户登录时在redis中存入一个值(这个值可以是sessionid,也可以是IP地址转成的密文,目的是验证用户是否是在同一设备或者同一环境登录。或者直接使用redis管理session,然后用session验证也是可以的),跟session的更新思路相同,在用户操作时更新该值的过期时间。设置一个过滤器,判断当前用户的环境是否与redis中存储的值相同,如果不同,则使当前用户登出。
以下使具体实现:
过滤器设置
@Slf4j
public class KickoutSessionFilter extends AccessControlFilter {
// 使用RedisCacheManager 存储的cache前缀名
public static String ONLINE_USER = "online_user";
private RedisTemplate<String , Object> redisTemplate;
public void setStringRedisTemplate(RedisTemplate<String , Object> redisTemplate){
this.redisTemplate = redisTemplate;
}
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = SecurityUtils.getSubject();
//如果用户未登录,跳过此过程
if(!subject.isAuthenticated() && !subject.isRemembered()) {
return true;
}
String username = (String) subject.getPrincipal();
Serializable id = subject.getSession().getId();
String s = (String) redisTemplate.opsForValue().get("online_user:" + username);
log.info(username);
if(s == null){
return true;
}
if(!s.equals(id.toString())){
subject.logout();
response.setContentType("application/json; charset=utf-8");
Result result = new Result(Code.FORBIDDEN, "您已在别处登录,请重新登录!");
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
return false;
}else{
return true;
}
}
}
ShiroConfig(配置过滤器)
@Slf4j
@Configuration
public class ShiroConfiguration {
@Autowired
private DefinitionRealm realm;
@Autowired
private RedisTemplate<String , Object> redisTemplate;
@Bean(name = {"shiroFilterFactoryBean"})
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("authc", new ShiroLoginFilter());
filters.put("roles", new ShiroRoleFilter());
filters.put("kickout" , kickoutSessionFilter());
bean.setFilters(filters);
//设置内置过滤器
//拦截
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/sign/signout", "logout");
filterChainDefinitionMap.put("/sign/signin", "anon");
filterChainDefinitionMap.put("/pwd/updatePwd", "anon");
filterChainDefinitionMap.put("/code/sendEmailCode", "anon");
filterChainDefinitionMap.put("/code/sendSmsCode", "anon");
filterChainDefinitionMap.put("/teacher/autoMatching","anon");
filterChainDefinitionMap.put("/code/*", "kickout ,authc");
filterChainDefinitionMap.put("/search/**","kickout ,authc");
filterChainDefinitionMap.put("/student/**", "kickout ,authc,roles[student]");
filterChainDefinitionMap.put("/teacher/**", "kickout ,authc,roles[teacher]");
filterChainDefinitionMap.put("/admin/**", "kickout ,authc,roles[admin]");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
//Coolie设置
public SimpleCookie rememberMeCookie() {
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域
//cookie.setDomain(domain);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30 * 24 * 60 * 60);
return cookie;
}
@Bean
//创建Shiro的cookie管理对象
//设置rememberMe管理器
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey("123456".getBytes(StandardCharsets.UTF_8));
return cookieRememberMeManager;
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//设置加密编码器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//设置加密方法
matcher.setHashAlgorithmName(HASH_ALGORITHM_NAME);
//设置加密次数
matcher.setHashIterations(HASH_ITERATORS);
//开启全局缓存
realm.setCachingEnabled(true);
//开启认证缓存,指定缓存名称
realm.setAuthenticationCachingEnabled(true);
realm.setAuthenticationCacheName("authenticationCache");
//开启授权缓存,指定缓存名称
realm.setAuthorizationCachingEnabled(true);
realm.setAuthorizationCacheName("authorizationCache");
//将加密对象存到realm中
realm.setCredentialsMatcher(matcher);
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
@Bean
public KickoutSessionFilter kickoutSessionFilter(){
KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
kickoutSessionFilter.setStringRedisTemplate(redisTemplate);
return kickoutSessionFilter;
}
}
登录(重点看更新redis的那一行)
@Service
public class SignServiceImpl implements SignService {
@Autowired
private StudentMapper studentMapper;
@Autowired
private TeacherMapper teacherMapper;
@Autowired
private RedisTemplate<String ,Object> redisTemplate;
@Override
public Result signIn(String uId, String pwd, boolean rememberMe ,HttpSession session) {
//生成token
AuthenticationToken token = new UsernamePasswordToken(uId , pwd , rememberMe);
//获得subject
Subject subject = SecurityUtils.getSubject();
//执行登陆操作
subject.login(token);
//将登录信息传入redis
redisTemplate.opsForValue().set("online_user:"+uId , session.getId());
return new Result();
}
}
@Override
public Result signOut() {
//获得subject
Subject subject = SecurityUtils.getSubject();
//登出
subject.logout();
return new Result(
Code.OK ,
"sign out success!" ,
null
);
}
}
Realm(这里是为了更新过期时间)
@Component
public class DefinitionRealm extends AuthorizingRealm {
@Autowired
private StudentService studentService;
@Autowired
private TeacherService teacherService;
@Autowired
private RedisTemplate<String , Object> redisTemplate;
/**
* 授权方法
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("鉴权发生");
//获得主身份信息
//根据用户名获取当前用户的角色信息、权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//获取角色
//如果角色为学生,给角色赋予学生权限
Student studentByStuId = studentService.getStudentByStuId((String) principalCollection.getPrimaryPrincipal());
if(studentByStuId!=null){
simpleAuthorizationInfo.addRole(STUDENT);
}
//如果角色为教师,给角色赋予教师权限
Teacher teacherByJobId = teacherService.getTeacherByJobId((String) principalCollection.getPrimaryPrincipal());
if(teacherByJobId!=null){
if(teacherByJobId.getStatus()==1){
simpleAuthorizationInfo.addRole(ADMIN);
}else {
simpleAuthorizationInfo.addRole(TEACHER);
}
}
redisTemplate.expire("online_user:"+principalCollection.getPrimaryPrincipal() , Duration.ofSeconds(60 * 30));//更新过期时间
//返回角色权限
return simpleAuthorizationInfo;
}
/**
* 认证方法
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken){
//获得用户名
String principal = (String) authToken.getPrincipal();
//获取学生身份信息
Student student = studentService.getStudentByStuId(principal);
if(student!=null){
return new SimpleAuthenticationInfo(
principal,
student.getPassword(),
ByteSourceUtil.bytes(student.getSalt()),
principal
);
}
//获取教师身份信息
Teacher teacher = teacherService.getTeacherByJobId(principal);
if(teacher!=null){
return new SimpleAuthenticationInfo(
principal,
teacher.getPassword(),
ByteSourceUtil.bytes(teacher.getSalt()),
principal
);
}
redisTemplate.expire("online_user:"+principal , Duration.ofSeconds(60 * 30));
return null;
}
@Override
protected void doClearCache(PrincipalCollection principals) {
super.doClearCache(principals);
}
}
测试
在Edge中使用swagger登录:
登陆成功,redis中出现值
使用ie登录:
登陆成功,redis中的值更新,此时再返回edge进行操作
返回值与预期结果相同!过滤器成功执行。
该过滤器的进阶版
学习了其他大佬的思想,其实redis中可以存储一个队列,通过控制队列内容的数量来控制同时在线的人数。但这个项目比较简单,所以写成了同时在线的用户只能有一个,感兴趣的可以去学习学习。