1.Maven
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.8</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.代码
//(1)主配置包含shiro,集成redis,在线剔除
@Configuration
public class ShiroConfig{
/**
* 注册踢出用户过滤器
* @param cacheManager
* @param sessionManager
* @return
*/
@Bean(name = "kickoutFilter")
@Scope("prototype")
@DependsOn(value = {"cacheManager","sessionManager"})
public KickoutFilter kickoutSessionControlFilter(CacheManager cacheManager, SessionManager sessionManager) {
KickoutFilter kickoutSessionControlFilter = new KickoutFilter();
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
//这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
//也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
kickoutSessionControlFilter.setCacheManager(cacheManager);
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager);
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/");
return kickoutSessionControlFilter;
}
/**
* 注册sessionDao
* @param redisUtil
* @return
*/
@Bean
public SessionDao sessionDao(RedisUtil redisUtil) {
SessionDao sessionDao = new SessionDao();
sessionDao.setRedisUtil(redisUtil);
return sessionDao;
}
/**
* 注册redisUtil
* @param redisTemplate
* @return
*/
@Bean
public RedisUtil redisUtil(RedisTemplate<String,Object> redisTemplate) {
RedisUtil redisUtil = new RedisUtil();
redisUtil.setRedisTemplate(redisTemplate);
return redisUtil;
}
/**
* Cookie管理
* @return
*/
@Bean(name = "rememberMeCookie")
public SimpleCookie rememberMeCookie() {
//参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//记住我cookie生效时间10天 ,单位秒;
simpleCookie.setMaxAge(TimeConstant.LOGIN_COOKIE_TIME);
return simpleCookie;
}
@Bean(name = "rememberMeManager")
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
/**
* Ehcache管理
* @return
*/
@Bean(name = "shiroEhcacheManager")
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return em;
}
/**
* session管理器
* @return
*/
@Bean(name = "sessionValidationScheduler")
public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() {
ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
scheduler.setInterval(TimeConstant.LOGIN_SESSION_VALIDATE_TIME);
return scheduler;
}
@Bean(name = "sessionManager")
public DefaultWebSessionManager defaultWebSessionManager(SessionDao sessionDao) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(TimeConstant.LOGIN_SESSION_TIME);
//url中是否显示session Id
sessionManager.setSessionIdUrlRewritingEnabled(false);
// 删除失效的session
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionValidationInterval(TimeConstant.LOGIN_SESSION_TIME);
sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
//不从新设置新的cookie,从sessionManager获取sessionIdCookie
sessionManager.getSessionIdCookie().setName("app-session-id");
sessionManager.getSessionIdCookie().setPath("/");
sessionManager.getSessionIdCookie().setMaxAge(TimeConstant.LOGIN_COOKIE_TIME);
//更新redis缓存
sessionManager.setSessionDAO(sessionDao);
Collection<SessionListener> c=new ArrayList<>();
c.add(new ShiroSessionListener());
sessionManager.setSessionListeners(c);
return sessionManager;
}
/**
* 初始化shiro权限管理器
* @return
*/
@Bean
@DependsOn(value = "lifecycleBeanPostProcessor")
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCacheManager(getEhCacheManager());
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于md5(md5(""))
hashedCredentialsMatcher.setHashIterations(1);
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* 初始化shiro安全过滤器
* @param defaultWebSecurityManager
* @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager,KickoutFilter kickoutFilter) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map<String,String> map = new HashMap<String, String>();
//swagger静态资源
//swagger接口权限 开放
map.put("/swagger-ui.demo", "anon");
map.put("/webjars/**", "anon");
map.put("/v2/**", "anon");
map.put("/swagger-resources/**", "anon");
//普通静态资源
map.put("/common/**","anon");
map.put("/demo/**","anon");
//登录请求
map.put("/login/**","anon");
map.put("/logout","logout");
map.put("/**", "kickout,authc");
//过滤器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("kickout", kickoutFilter);
filterMap.put("authc",new SuccessUrlFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//登录页
shiroFilterFactoryBean.setLoginUrl("/login/page");
//登录成功页
shiroFilterFactoryBean.setSuccessUrl("/index/page");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 注册shiro过滤器注册器
* @return
*/
@Bean(name = "filterRegistrationBean1")
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new DelegatingFilterProxy("shiroFilter"));
filterRegistrationBean.addInitParameter("targetFilterLifecycle", "true");
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/");
return filterRegistrationBean;
}
/**
* 修改全局默认shiro安全管理器
* @param shiroRealm
* @param defaultWebSessionManager
* @return
*/
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(ShiroRealm shiroRealm, DefaultWebSessionManager defaultWebSessionManager) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(shiroRealm);
defaultWebSecurityManager.setSessionManager(defaultWebSessionManager);
defaultWebSecurityManager.setCacheManager(getEhCacheManager());
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
return defaultWebSecurityManager;
}
/**
* shiro权限通知
* @param defaultWebSecurityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(defaultWebSecurityManager);
return aasa;
}
/**
* shiro生命周期
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();
return lifecycleBeanPostProcessor;
}
}
认证+权限
public class ShiroRealm extends AuthorizingRealm {
@Lazy
@Autowired
private IUserService iUserService;
/**
* 角色权限和对应权限添加
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name= (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = iUserService.getByUsername(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role:iUserService.getRoles(user.getId())) {
//添加角色
simpleAuthorizationInfo.addRole(role.getMark());
for (Permission permission:iUserService.getRolePermissions(role.getId())) {
//添加权限
simpleAuthorizationInfo.addStringPermission(permission.getMark());
}
}
return simpleAuthorizationInfo;
}
/**
* 用户认证
* @param authenticationToken
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken){
//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
if (authenticationToken.getPrincipal() == null) {
return null;
}
//获取用户信息
String username = authenticationToken.getPrincipal().toString();
User user = iUserService.getByUsername(username);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
}
}
}
//(2)在线剔除
public class KickoutFilter extends AccessControlFilter {
/**
* 踢出后到的地址
*/
private String kickoutUrl;
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickoutAfter = false;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession = 1;
/**
* 会话管理器
*/
private SessionManager sessionManager;
/**
* 缓存管理器
*/
private Cache<String, Deque<Serializable>> cache;
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
/**
* 设置Cache的key的前缀
* @param cacheManager
*/
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("deque_session_");
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
//读取缓存 没有就存入
Deque<Serializable> deque = cache.get(username);
//初始化
if(deque == null){
deque = new LinkedList<>();
}
//用户在线数量为0
if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
//将sessionId存入队列
deque.push(sessionId);
//将用户的sessionId队列缓存
cache.put(username, deque);
}
//用户数量>max
while (deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if (kickoutAfter) {
//如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
}
//踢出后再更新下缓存队列
cache.put(username, deque);
try {
//获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
System.out.println("===========可忽略异常=============");
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if ((Boolean) session.getAttribute("kickout") != null && (Boolean) session.getAttribute("kickout") == true) {
//会话被踢出了
try {
//退出登录
subject.logout();
} catch (Exception e) { //ignore
}
saveRequest(request);
//重定向
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
}
//(3)持久化部分
public class SessionDao extends EnterpriseCacheSessionDAO {
private static final Logger log = LoggerFactory.getLogger(SessionDao.class);
private RedisUtil redisUtil;
public RedisUtil getRedisUtil() {
return redisUtil;
}
public void setRedisUtil(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
/**
* 创建session,保存到redis(1,1)
* @param session
* @return
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
if(null == ThreadContext.getSubject()){
return sessionId;
}
//判断是否登录
Subject subject = SecurityUtils.getSubject();
if(subject != null && subject.isAuthenticated()){
String username = (String) SecurityUtils.getSubject().getPrincipal();
session.setAttribute("username",username);
redisUtil.set(CacheConstant.USER_LOGIN_SESSION+sessionId, SeriaUtil.toBytes(session), TimeConstant.LOGIN_SESSION_CACHE_TIME);
}
return sessionId;
}
/**
* 获取session(1,0)
* @param sessionId
* @return
*/
@Override
protected Session doReadSession(Serializable sessionId) {
// 先从缓存中获取session,如果没有再去数据库中获取
Session session = session = super.doReadSession(sessionId);
if(session == null){
Object o = redisUtil.get(CacheConstant.USER_LOGIN_SESSION+sessionId);
if(o != null){
String str = (String) o;
session = (Session) SeriaUtil.toObject(str);
}
}
return session;
}
/**
* 更新session的最后一次访问时间
* @param session
*/
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
if(null == ThreadContext.getSubject()){
return;
}
//判断是否存在key
String key = CacheConstant.USER_LOGIN_SESSION+session.getId();
//判断是否登录
Subject subject = SecurityUtils.getSubject();
if(subject != null && subject.isAuthenticated()){
if(null == session.getAttribute("username")){
String username = (String) SecurityUtils.getSubject().getPrincipal();
session.setAttribute("username",username);
}
redisUtil.set(key, SeriaUtil.toBytes(session),TimeConstant.LOGIN_SESSION_CACHE_TIME);
}
}
/**
* 删除session
* @param session
*/
@Override
protected void doDelete(Session session) {
super.doDelete(session);
//判断是否存在key
String key = CacheConstant.USER_LOGIN_SESSION+session.getId();
if(redisUtil.exists(key)){
redisUtil.remove(key.toString());
}
}
}
//(4)若登录不成功,需要自定义过滤器重定向处理
public class SuccessUrlFilter extends FormAuthenticationFilter {
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
String successUrl = "/index/page";
WebUtils.issueRedirect(request,response,successUrl);
return false;
}
}
(5)ehcache-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--
diskStore:为缓存路径,ehcache分为内存和磁盘 2级,此属性定义磁盘的缓存位置
user.home - 用户主目录
user.dir - 用户当前工作目录
java.io.tmpdir - 默认临时文件路径
-->
<diskStore path="java.io.tmpdir/Tmp_Ehcache" />
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"
/>
<cache
name="demo"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
(6)springboot.properties
#server
server.port=8080
#jdbc
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_ssh?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#jpa
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database=mysql
#都加上innodb(事务,行级锁,占用资源比较多)
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#redis
spring.redis.hostName=127.0.0.1
spring.redis.port=6379
spring.redis.password=123zgf
spring.redis.database=0
spring.redis.timeOut=1000
spring.redis.maxIdle=10
spring.redis.maxWaitMillis=15000
spring.redis.testOnBorrow=true
spring.redis.testWhileIdle=false
#哨兵名称
spring.redis.sentinel.master=mymaster
#哨兵端口号
spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381