WEB采用Shiro联合Redis的示意图
Redis实现shiro缓存,达到分布式共享session和授权信息,把session和授权持久化到redis数据库或者缓存shiro集群为了防止多次插查询数据库,解决web在授权的时候每次都去查询数据库,对于频繁访问的接口,性能和响应速度比较慢的问题
客户登录,发起请求,调用Shiro与redis的过程
所用到的依赖
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- shiro-thymeleaf -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
友情链接:
- https://blog.csdn.net/qq_34021712/article/details/80791219
- https://www.dzou.top/post/712f837.html
- https://blog.csdn.net/weixin_39973810/article/details/85113351
ShiroConfig.java
package com.AAAAAAAAAAAA.common.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.AAAAAAAAAAAA.common.config.FiresProperties;
import com.AAAAAAAAAAAA.common.listener.ShiroSessionListener;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
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.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 配置类
*
* @author 阿啄debugIT
*/
@Configuration
public class ShiroConfig {
/**
* 缓存和session的管理
*/
@Autowired
private FiresProperties firesProperties;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
/**
* 用于开启 Thymeleaf 中的 shiro 标签的使用
* @return ShiroDialect shiro 方言对象
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* shiro 中配置 redis 缓存
* @return RedisManager
*/
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
// 缓存时间,单位为秒
//redisManager.setExpire(firesProperties.getShiro().getExpireIn()); // removed from shiro-redis v3.1.0 api
redisManager.setHost(host);
redisManager.setPort(port);
if (StringUtils.isNotBlank(password))
redisManager.setPassword(password);
redisManager.setTimeout(timeout);
return redisManager;
}
private RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager Filter Chain定义说明
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters
filters.put("user", new CustomUserFilter());
//必须设置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录的 url
//需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
shiroFilterFactoryBean.setLoginUrl(firesProperties.getShiro().getLoginUrl());
// 登录成功后跳转的 url
//登录成功,跳转url,如果前后端分离,则没这个调用
shiroFilterFactoryBean.setSuccessUrl(firesProperties.getShiro().getSuccessUrl());
// 未授权 url
//没有权限,未授权就会调用此方法, 先验证登录-》再验证是否有权限
shiroFilterFactoryBean.setUnauthorizedUrl(firesProperties.getShiro().getUnauthorizedUrl());
//拦截器路径,坑一,部分路径无法进行拦截,时有时无;因为同学使用的是hashmap, 无序的,应该改为LinkedHashMap
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 设置免认证 url
String[] anonUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(firesProperties.getShiro().getAnonUrl(), ",");
for (String url : anonUrls) {
//匿名可以访问,也是就游客模式
//filterChainDefinitionMap.put("/pub/**","anon");
filterChainDefinitionMap.put(url, "anon");
}
// 配置退出过滤器,其中具体的退出代码 Shiro已经替我们实现了
filterChainDefinitionMap.put(firesProperties.getShiro().getLogoutUrl(), "logout");
// 除上以外所有 url都必须认证通过才可以访问,未通过认证自动访问 LoginUrl
filterChainDefinitionMap.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 rememberMeCookie
securityManager.setRememberMeManager(rememberMeManager());
// 配置 缓存管理类 cacheManager
//自定义缓存实现 使用redis ,生产环境才需要这么设置,开发环境需要清空全选,所以不建议开启这个
securityManager.setCacheManager(cacheManager());
securityManager.setSessionManager(sessionManager());
// 配置 SecurityManager,并注入 shiroRealm
//设置realm(推荐放到最后,不然某些情况会不生效)
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public ShiroRealm shiroRealm() {
// 配置 Realm,需自己实现,见 com.AAAAAAAAAAAA.common.shiro.ShiroRealm
return new ShiroRealm();
}
/**
* rememberMe cookie 效果是重开浏览器后无需重新登录
*
* @return SimpleCookie
*/
private SimpleCookie rememberMeCookie() {
// 设置 cookie 名称,对应 login.html 页面的 <input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// 设置 cookie 的过期时间,单位为秒,这里为一天
cookie.setMaxAge(firesProperties.getShiro().getCookieTimeout());
return cookie;
}
/**
* cookie管理对象
*
* @return CookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie 加密的密钥
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
/**
*@description:
* 作用:加入注解的使用,不加入这个AOP注解不生效(shiro的注解 例如 @RequiresGuest)
* 使shiro的注解生效
*@params: []
*@return: org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
**/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* session 管理对象
*
* @return DefaultWebSessionManager
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<>();
listeners.add(new ShiroSessionListener());
// 设置session超时时间,单位为毫秒
sessionManager.setGlobalSessionTimeout(firesProperties.getShiro().getSessionTimeout());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**
* 注册DelegatingFilterProxy(Shiro)
* 集成Shiro有2种方法:
* 1. 按这个方法自己组装一个FilterRegistrationBean(这种方法更为灵活,可以自己定义UrlPattern,
* 在项目使用中你可能会因为一些很但疼的问题最后采用它, 想使用它你可能需要看官网或者已经很了解Shiro的处理原理了)
* 2. 直接使用ShiroFilterFactoryBean(这种方法比较简单,其内部对ShiroFilter做了组装工作,无法自己定义UrlPattern,* 默认拦截 /*)
*
* @param dispatcherServlet
* @return
* @create 2016年1月13日
*/
// @Bean
// public FilterRegistrationBean filterRegistrationBean() {
// FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
// filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
// // 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
// filterRegistration.addInitParameter("targetFilterLifecycle", "true");
// filterRegistration.setEnabled(true);
// filterRegistration.addUrlPatterns("/*");// 可以自己灵活的定义很多,避免一些根本不需要被Shiro处理的请求被包含进来
// return filterRegistration;
// }
}
ShiroRealm .Java
package com.AAAAAAAAAAAA.common.shiro;
import com.AAAAAAAAAAAA.common.util.Constant;
import com.AAAAAAAAAAAA.system.domain.Menu;
import com.AAAAAAAAAAAA.system.domain.Role;
import com.AAAAAAAAAAAA.system.domain.User;
import com.AAAAAAAAAAAA.system.service.MenuService;
import com.AAAAAAAAAAAA.system.service.RoleService;
import com.AAAAAAAAAAAA.system.service.UserService;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 自定义实现 ShiroRealm,包含认证和授权两大模块
* @description: 在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的
* 在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO
* @author 阿啄debugIT
*/
@Component("shiroRealm")
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
private String key;
Integer count = 0;
/**
* 用户认证
*
* @param token AuthenticationToken 身份认证 token
* @return AuthenticationInfo 身份认证信息
* @throws AuthenticationException 认证相关异常
*/
@Resource
RedisTemplate<String, Integer> redisTemplate;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户输入的用户名和密码
String userName = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
int MaxCount = 5;
// 通过用户名到数据库查询用户信息
User user = this.userService.findByName(userName);
if (user == null) {
setMaxLoginCount(userName, MaxCount);
// throw new UnknownAccountException("账号或密码错误,或账号异常 请联系管理员");
throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
}
// 最大有有效时间不为空的时候
if (user.getMaxTime() != null && !"".equals(user.getMaxTime())) {
// 数据库获取到的是date类型
// String dateStr = "2019-10-19";
Date dataDate = DateUtil.parse(DateUtil.formatDate(user.getMaxTime()));
// 当前时间也是date类型
//当前日期字符串,格式:yyyy-MM-dd
String today = DateUtil.today();
Date todayDate = DateUtil.parse(today);
//dataDate - todayDate >=0 表示可用
long betweenDay = DateUtil.between(todayDate, dataDate, DateUnit.DAY, false);
if (betweenDay < 0) {
user.setStatus(User.STATUS_LOCK);
userService.updateNotNull(user);
}
}
// 判断用户最后一次修改密码时间
updatePwdTime(user);
if (!password.equals(user.getPassword())) {
setMaxLoginCount(userName, MaxCount);
// throw new IncorrectCredentialsException("用户名或密码错误!");
throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
}
if (User.STATUS_LOCK.equals(user.getStatus())) {
// throw new LockedAccountException("账号已被锁定,请联系管理员!");
throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
}
setMaxLoginCount(userName, MaxCount);
//删除key
redisTemplate.delete(key);
user.setUsername(userName);
return new SimpleAuthenticationInfo(user, password, getName());
}
/**
* 授权模块,获取用户角色和权限
*
* @param principal principal
* @return AuthorizationInfo 权限信息
*/
/**
* 授权用户权限
* 授权的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>标签的时候调用的
* 它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,如果有,里面的内容显示
* 如果没有,里面的内容不予显示(这就完成了对于权限的认证.)
*
* shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
* 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
*
* 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。
* authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
*
* 当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
* authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
*
* 就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]");
* 就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问
*
* 如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]");
* 就说明访问/add这个链接必须要有 "权限添加" 这个权限和具有 "100002" 这个角色才可以访问
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
String userName = user.getUsername();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取用户角色集
List<Role> roleList = this.roleService.findUserRole(userName);
Set<String> roleSet = roleList.stream().map(Role::getRoleName).collect(Collectors.toSet());
simpleAuthorizationInfo.setRoles(roleSet);
// 获取用户权限集
List<Menu> permissionList = this.menuService.findUserPermissions(userName);
Set<String> permissionSet = permissionList.stream().map(Menu::getPerms).collect(Collectors.toSet());
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
private void updatePwdTime(User user) {
Date updatePwdTime;
// 判断最后一次修改密码时间
if (user.getLastUpdatePwdTime() == null) {
return;
// updatePwdTime = user.getCrateTime();
} else {
updatePwdTime = user.getLastUpdatePwdTime();
}
Date todayDate = DateUtil.date();
long betweenDay = DateUtil.between(updatePwdTime, todayDate, DateUnit.DAY, false);
// 超过90天
if (betweenDay > 90) {
System.out.println("密码时间超过90天");
throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
}
}
private void setMaxLoginCount(String userName, int maxCount) {
//当前时间
Date CurrentDate = DateUtil.date();
//明天时间
Date date = DateUtil.tomorrow();
String formatDate = DateUtil.formatDate(date) + " 00:00:00";
date = DateUtil.parse(formatDate);
// key以日期为单位
key = "user:" + userName + ":" + DateUtil.formatDate(date);
count = redisTemplate.opsForValue().get(key);
// 设置间隔多少秒
long second = DateUtil.between(CurrentDate, date, DateUnit.SECOND);
// long second = 30;
if (count == null) {
redisTemplate.opsForValue().set(key, 1, second, TimeUnit.SECONDS);
} else {
count++;
redisTemplate.opsForValue().set(key, count, second, TimeUnit.SECONDS);
System.out.println("redis查询的次数: " + count);
if (count >= maxCount) {
// throw new UnknownAccountException("今天输入用户名密码错误超过" + maxCount + "次,请明天在来试");
throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
}
}
}
/**
* 清除权限缓存
* 使用方法:在需要清除用户权限的地方注入 ShiroRealm,
* 然后调用其clearCache方法。
*/
public void clearCache() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
public static void main(String[] args) {
Date CurrentDate = DateUtil.date();
Date date = DateUtil.tomorrow();
String formatDate = DateUtil.formatDate(date) + " 00:00:00";
date = DateUtil.parse(formatDate);
System.out.println(DateUtil.formatDateTime(date));
// 设置间隔多少秒
long second = DateUtil.between(CurrentDate, date, DateUnit.SECOND);
System.out.println(second);
}
}
ShiroConfig.java代码解析
RedisManager.java、RedisSessionDAO和RedisCacheManager采用用shiro-redis中的原始类
使用crazycake开源shiro-redis实现好的工具
- RedisSessionDAO 可以继承EnterpriseCacheSessionDAO实现session控制
- RedisCache 继承Cache类实现具体redis操作缓存(remove、get、set、keys
- RedisCacheManager 实现接口CacheManager的getCache获得RedisCache交给securityManager管理
加载配置类
sessionManager管理session
登录时间cookie的安全管理
同时使用了ConcurrentMap管理数据和缓存,更加高效
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
ShiroRealm .Java代码解析
友情链接:https://www.qingtingip.com/h_335109.html
doGetAuthorizationInfo 授权
在调用一下代码的时候,doGetAuthorizationInfo
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
doGetAuthenticationInfo 认证
在调用一下代码的时候,doGetAuthenticationInfo
- subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
- @RequiresRoles("admin") :在方法上加注解的时候;
- [@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
credentialsMatcher 配置凭证匹配器,双md5加盐加密
详情实现请见gif