您的位置:首页 > 数据库 > Redis

spring boot 前后端分离整合shiro(五)整合redis并实现并发登录控制

2019-07-23 10:49 1221 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/HuYiAn1004/article/details/96965909

pom文件添加依赖:

<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--shiro-redis-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>

yml添加redis的配置:

#redis
spring:
redis:
database: 0 # Redis数据库索引(默认为0)
host: 127.0.0.1
#    host: 192.168.65.130
port: 6379
#    password: 111111
# 连接超时时间(毫秒)
timeout: 1000
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0

redis的配置类:

package com.example.shiroStudy.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* @author 黄豪琦
* 日期:2019-07-05 13:27
* 说明:
*/
@Configuration
public class RedisConfig {

@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}

}

一个简单的redis增删改查工具类:

package com.example.shiroStudy.config.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
* @author 黄豪琦
* 日期:2019-07-05 13:28
* 说明:
*/
public class RedisUtils {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

//添加 获取

/**
* 获取普通缓存
* @param key
* @return
*/
public Object get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}

public boolean set(String key, Object obj){
try {
redisTemplate.opsForValue().set(key, obj);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 添加普通缓存
* @param key
* @param value
* @param time
* @return
*/
public boolean set(String key, Object value, long time){
try {
if(time > 0){
redisTemplate.opsForValue().set(key, value, time);
return true;
} else {
redisTemplate.opsForValue().set(key, value);
return true;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 获取某个list中所有的值
* @param key
* @return
*/
public List<Object> getList(String key){
try {
return redisTemplate.opsForList().range(key, 0, -1);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 存一个list
* @param key 键
* @param value 值
* @return false失败
*/
public boolean setList(String key, Object value){
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}

}

在shiroconfig中添加bean:

/**
* redisManager
*/
@Bean
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
//设置过期时间
redisManager.setExpire(2000);
return redisManager;
}

/**
* 配置具体catch实现类
* @return
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());

return redisCacheManager;
}

/**
* redis操作
* @return
*/
@Bean
public RedisUtils redisUtils(){
RedisUtils redisUtils = new RedisUtils();
return redisUtils;
}

session持久化

package com.example.shiroStudy.config.shiro;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
* @author 黄豪琦
* 日期:2019-07-03 11:16
* 说明: 自定义session管理
*/
public class CustomSessionManager extends DefaultWebSessionManager {

private static final String TOKEN = "token";

public CustomSessionManager() {
super();
}

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

String sessionId = WebUtils.toHttp(request).getHeader(TOKEN);
if(null != sessionId){
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
//让shiro去判断sessionId是否过期 是否存在
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
//标记sessionId是有效的,如果是false则会抛异常
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);

return sessionId;
} else {
return super.getSessionId(request, response);
}

}
}

说明

RedisManager 和 RedisCacheManager 都是crazycake包下面的,这个是大犇写的工具,会用就行。RedisManager可以使用

redisManager.setHost(String host);
redisManager.setPort(String port);
来设置ip地址和端口号,如果不设置的话默认就是本地的6379端口:

测试

启动项目,登录一下

使用rdm工具查看缓存中的内容:

可以看到shiro已经帮我们把用户信息存到redis中了。
编写并发登录控制类:
KicoutSessionControlFilter

import com.alibaba.fastjson.JSONObject;
import com.example.shiroStudy.config.redis.RedisUtils;
import com.example.shiroStudy.entity.JsonData;
import com.example.shiroStudy.entity.Users;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
//import org.crazycake.shiro.RedisManager;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;

/**
* @author 黄豪琦
* 日期:2019-07-04 15:34
* 说明:自定义并发登录拦截器
*/
public class KicoutSessionControlFilter extends AccessControlFilter {

/**
* 被踢出后重定向的地址  后台接口路径
*/
private String kicOutUrl;

/**
* 最大登录人数 默认为1
*/
private int maxNum = 1;

/**
* 踢出前者还是后者 为true踢出后者 默认踢出前者
*/
private boolean kicoutAfter = false;

private SessionManager sessionManager;

private RedisUtils redisUtils;

private static final String DEFAULT_KICKOUT_CACHE_KEY_PREFIX = "shiro:cache:kickout:";

private String keyPrefix = DEFAULT_KICKOUT_CACHE_KEY_PREFIX;

private String getRedisKickoutKey(String username) {
return this.keyPrefix + username;
}

private static final String KICOUT_PROPERTY_NAME = "kicout";

/**
* 是否允许访问,true表示允许
* @param servletRequest
* @param servletResponse
* @param o
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}

/**
* 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
Subject subject = SecurityUtils.getSubject();
//如果用户没有登录且没有配置『记住我』,则跳过
if(!subject.isAuthenticated() && !subject.isRemembered()){
return true;
}

Users user = (Users)subject.getPrincipal();
Session session = subject.getSession();

Serializable sessionId = session.getId();

//初始化用户队列 放进缓存
Deque<Serializable> deque = (Deque<Serializable>)redisUtils.get(getRedisKickoutKey(user.getLoginName()));
if(deque == null || deque.size() == 0){
deque = new LinkedList<>();
}

//如果用户队列里没有此sessionId,且没有被踢出 则放入队列 并放入缓存
if(!deque.contains(sessionId) && session.getAttribute(KICOUT_PROPERTY_NAME) == null){
deque.push(sessionId);
redisUtils.set(getRedisKickoutKey(user.getLoginName()), deque, -1);
}

//如果队列里的用户数量超过最大值 开始踢人
while (deque.size() > maxNum){
Serializable kicoutSessionId;
//为true踢出前者  kicoutAfter默认是false
if(!kicoutAfter){
kicoutSessionId = deque.removeLast();
} else {
//否则踢出后者
kicoutSessionId = deque.removeFirst();
}
try {
Session kicoutSession = sessionManager.getSession(new DefaultSessionKey(kicoutSessionId));
if(kicoutSession != null){
//设置此属性为true表示这个会话被踢出了
kicoutSession.setAttribute(KICOUT_PROPERTY_NAME, true);
//更新redis
redisUtils.set(getRedisKickoutKey(user.getLoginName()), deque, -1);
}

} catch (SessionException e) {
e.printStackTrace();
}

}

if(session.getAttribute(KICOUT_PROPERTY_NAME) != null){
//会话被踢出了
try {
//从shiro退出登录
subject.logout();
//给前端返回信息
HttpServletResponse response = WebUtils.toHttp(servletResponse);
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write(JSONObject.toJSON(new JsonData(-3,
"您的账号在另一台设备登录。如果不是本人操作,请及时修改密码")).toString());
writer.close();
return false;
} catch (Exception e) {
e.printStackTrace();
}

}
return true;
}

public String getKicOutUrl() {
return kicOutUrl;
}

public void setKicOutUrl(String kicOutUrl) {
this.kicOutUrl = kicOutUrl;
}

public int getMaxNum() {
return maxNum;
}

public void setMaxNum(int maxNum) {
this.maxNum = maxNum;
}

public boolean isKicoutAfter() {
return kicoutAfter;
}

public void setKicoutAfter(boolean kicoutAfter) {
this.kicoutAfter = kicoutAfter;
}

public SessionManager getSessionManager() {
return sessionManager;
}

public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}

public void setRedisUtils(RedisUtils redisUtils){
this.redisUtils = redisUtils;
}

public static String getKicoutPrefix(){return DEFAULT_KICKOUT_CACHE_KEY_PREFIX;}
}

完整的shiroconfig:

shiroconfig

import com.example.shiroStudy.config.redis.RedisUtils;
import com.example.shiroStudy.config.shiro.filter.CustomRolesAuthorizationFilter;
import com.example.shiroStudy.config.shiro.filter.KicoutSessionControlFilter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author 黄豪琦
* 日期:2019-07-02 15:04
* 说明:
*/
@Configuration
public class ShiroConfig {

@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

//SecurityManager 安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);

//登录,为后台接口名,非前台页面名, 未登录跳转
shiroFilterFactoryBean.setLoginUrl("/login");
//登录成功后跳转的地址,为后台接口名,非前台页面名
shiroFilterFactoryBean.setSuccessUrl("/pub/index");
//无权限跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error/unauthorized");

//设置自定义filter
Map<String, Filter> cusFilterMap = new LinkedHashMap<>();
cusFilterMap.put("roleOn", new CustomRolesAuthorizationFilter());
cusFilterMap.put("kicout", kicoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(cusFilterMap);

// 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
// 过滤链定义,从上向下顺序执行
LinkedHashMap<String, String> filterMap = new LinkedHashMap<String, String>();

filterMap.put("/dev/**", "anon");
filterMap.put("/wechart/**", "anon");
filterMap.put("/js/**", "anon");

//公开的请求,都可以访问
filterMap.put("/pub/**", "anon");
//需要登录
filterMap.put("/auth/**", "authc");
//需要root权限
filterMap.put("/root/**", "roles[root]");

filterMap.put("/manage/**", "roleOn[root, admin]");

//漏掉的都需要认证
filterMap.put("/**", "kicout,authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}

/**
* 配置核心安全管理器
* @return
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(cusRealm());

securityManager.setCacheManager(redisCacheManager());

securityManager.setSessionManager(sessionManager());
return securityManager;
}

@Bean
public CusRealm cusRealm(){
CusRealm realm = new CusRealm();
//配置密码比较器
realm.setCredentialsMatcher(hashedCredentialsMatcher());

return realm;
}

/**
* 加密器
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

//设置散列算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");

//散列次数 表示加密几次 此处为加密两次
hashedCredentialsMatcher.setHashIterations(2);

return hashedCredentialsMatcher;
}

/**
* 自定义session管理
* @return
*/
@Bean
public SessionManager sessionManager(){
CustomSessionManager sessionManager = new CustomSessionManager();
//session过期时间 1800000为半小时
sessionManager.setGlobalSessionTimeout(1800000);

//配置session持久化
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}

/**
* 开启 shiro 注解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;

/**
* redisManager
*/
@Bean
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();

//        redisManager.setHost(redisHost);
//        redisManager.setPort(redisPort);
//设置过期时间
redisManager.setExpire(2000);
return redisManager;
}

/**
* 配置具体catch实现类
* @return
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());

return redisCacheManager;
}

/**
* session持久化
* @return
*/
@Bean
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}

/**
* 管理shiro一些bean的生命周期 初始化和销毁
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}

/**
* redis操作
* @return
*/
@Bean
public RedisUtils redisUtils(){
RedisUtils redisUtils = new RedisUtils();
return redisUtils;
}

@Bean
public KicoutSessionControlFilter kicoutSessionControlFilter(){
KicoutSessionControlFilter kicoutSessionControlFilter = new KicoutSessionControlFilter();

kicoutSessionControlFilter.setMaxNum(1);
kicoutSessionControlFilter.setSessionManager(sessionManager());
kicoutSessionControlFilter.setRedisUtils(redisUtils());

return kicoutSessionControlFilter;
}
}

思路:
主要是利用缓存来实现并发登录。当用户登录时,以用户名为键,以一个deque(集合的一种)为值存入缓存中。当同一个用户在另一个地方登录时,那么deque中就会有两个sessionId。当deque的大小,超过了设定值的时候就开始踢人。给被踢的session添加一个kicout属性。然后判断当前subject的session,如果有kicout这个属性,说明是被踢下线了,这时就返回给前端信息,并返回false给shiro表示我自定义的拦截器已经处理过了,剩下的拦截器不用执行了。
前端返回有两个情况,一个是前后端分离一个是不分离。分离的情况下用response获取流然后输出就可以了;不分离的情况下可以使用shiro提供的WebUtils进行重定向:

WebUtils.issueRedirect(request, response, url);

参数url就是要重定向的地址,可以在这个地址上添加一个标志,比如

/login?kickout=1

然后在前台js里获取浏览器地址 截取url中kicout的位置,如果有,说明是被踢出的,就可以提示消息。

//被踢出时的提示消息
function kickout(){
var href=location.href;
if(href.indexOf("kickout")>0){
alert("您的账号在另一台设备上登录,如非本人操作,请立即修改密码!");
}
}

测试:
第一个用户登录:

随便访问一个接口:

第二个用户登录:

(之所以用这样的方式登录是因为potman有cookie,不能模拟)
第二个用户登录成功:

第一个用户再次请求时已被踢出

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐