1.shiro配置类
@Configuration
@Lazy
public class ShiroConfiguration {
/**
*shiro的过滤器配置
*/
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc", new LoginAuthenticationFilter());
//要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/");
//登录成功后要跳转的连接,逻辑也可以自定义,例如返回上次请求的页面
shiroFilterFactoryBean.setSuccessUrl("/home");
//用户访问未对其授权的资源时,所显示的连接
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置退出过滤器,过滤按顺序执行,全局anon需要放到最后面
/**
* 1、建议配置URL地址过滤,可以快速过滤没有,不要等待注解层抛出异常
*以下示例,实际根据需求来
*/
filterChainDefinitionMap.put("/api/**", "anon");
filterChainDefinitionMap.put("/client/**", "anon");
filterChainDefinitionMap.put("/**.js", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 配置自己写的认证代码类
*/
@Bean
public ShiroDBAuthorizingRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setAuthenticationCachingEnabled(true);
return shiroDBAuthorizingRealm;
}
/**
* shiro的安全管理器
*
* @return
* @Bean(name = "securityManager")
*/
@Bean
public SecurityManager securityManager(ShiroDBAuthorizingRealm shiroDBAuthorizingRealm, CacheManager cacheManager, Environment environment) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroDBAuthorizingRealm);
boolean online = Boolean.valueOf(environment.getProperty("user.online","true"));
if (online) {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
securityManager.setSessionManager(defaultWebSessionManager);
} else {
securityManager.setSessionManager(new ServletContainerSessionManager());
}
return securityManager;
}
/**
* Shiro生命周期处理器
*
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
online表示是否需要显示用户状态。
这我使用online参数来决定使用哪一个session管理器,默认为ServletContainerSessionManager,session缓存到 WEB容器中,热加载不需要重新登录。(即我们重启项目时,不需要每次都重新登录,大大提高开发效率),而这个管理器中没有获取sessionDAO的方法,无法获取登录用户。所用使用了DefaultWebSessionManager,他继承了DefaultSessionManager类,点进源码中看,有sessionDAO的get,set方法。而sessionDAO则是session会话持久化的关键。
2.创建一个session管理工具类
@Service
public class SessionManagerService {
@Autowired
private SecurityManager securityManager;
@Value("${user.online:true}")
private boolean online;
/**
*通过配置控制用户登录的方式,登录方式不同,存入session的用户信息不同
*/
@Value("${user.login.method:phoneNumber}")
private String method;
private DefaultWebSessionManager getSessionManager() {
DefaultWebSecurityManager defaultWebSecurityManager = (DefaultWebSecurityManager) securityManager;
DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) defaultWebSecurityManager.getSessionManager();
return sessionManager;
}
/**
* 获取账号对应的 shiro session
* 1、管理员登录时,传管理员ID
* 2、用户账号登录时,传用户ID
* 3、用户手机号码登录时,传手机号码
* @param id
* @return
*/
public Session getSessionByUserIdOr(String id) {
if(!online) {
return null;
}
Map<String, Session> sessionsMap = getSessionsMap();
return sessionsMap.get(id);
}
/**
* 删除会话,强制用户退出,用于一个账号同时只能有一个地方登录
* 并判断当前登录的session与查出的session是否相同,相同则不删除。
* (一台电脑上同一个浏览器在 不退出登录直接关闭浏览器时再次登录相同账号session是相同的,这时删除会造成报错)
* @param session
*/
public void deleteSession(Session session) {
if (online && session != null && !SecurityUtils.getSubject().getSession().getId().equals(session.getId())) {
DefaultWebSessionManager sessionManager = getSessionManager();
sessionManager.getSessionDAO().delete(session);
}
}
/**
* 获取所有在线用户的session,从session中获取登录用户的信息,与sessionId存与map中
* @return
*/
public Map<String, Session> getSessionsMap() {
if(!online) {
return null;
}
User user = null;
Admin admin = null;
SimplePrincipalCollection principalCollection = null;
Map<String, Session> userSessionMap = new HashMap<>();
DefaultWebSessionManager sessionManager = getSessionManager();
Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
for (Session session : sessions) {
if (session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) {
principalCollection = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
if (primaryPrincipal instanceof User) {
user = (User) primaryPrincipal;
if (method.equals("phoneNumber")) {
userSessionMap.put(user.getPhoneNumber(), session);
} else {
userSessionMap.put(user.getUserId(), session);
}
} else if (primaryPrincipal instanceof Admin) {
admin = (Admin) primaryPrincipal;
userSessionMap.put(admin.getUserId(), session);
} else {
throw new BizException("不支持的会话类:" + primaryPrincipal.getClass());
}
}
}
return userSessionMap;
}
}
1.这里获取sessionManager需要进行两次强转,然后通过sessionManager.getSessionDAO()获取sessionDAO进行session的增删改查。
2.获取到当前所有登录的用户后,再登录接口进行判断,进行登录的用户id是否有对应的session,有则直接删除session,使上一个登录的用户被强制退出。
3.同时获取到登录用户的所有session,查询用户列表时通过判断,是否存在用户Id对应的session从而修改用户是否在线的状态。
3.注意
1.在大用户量情况下,该方法非常消耗内存,此时不建议使用该方法。
2.在配置类中我并没有使用注解的方式引用yml文件里配置,原因是注入不进去。注入配置的方法很多种,有兴趣的可以借鉴一下这篇文章。
SpringBoot读取配置文件的六种方式-CSDN博客
··