本文主要是为让大家快速上手整合Shiro进入项目,完成拦截,认证,授权等功能,具体的原理可能并不会过多的描述请大家谅解 。
什么是Shiro
Shiro
是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。
下文配置的安全管理器和会话管理器中有用到Redis实现的自定义缓存,大家可以要是有自定义缓存的需要可以参考下这篇文章:
Redis实现自定义Shiro的缓存管理器_六木老师的博客-CSDN博客
依赖
<!-- Shiro JWT begin -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>${thymeleaf-layout-dialect.version}</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${thymeleaf-extras-shiro.version}</version>
</dependency>
<!-- Shiro JWT end -->
Shiro使用自定义Relam实现认证\授权
@Slf4j
public class JwtRealm extends AuthorizingRealm {
@Resource
private JwtTokenUtil jwtTokenUtil;
@DubboReference
private ISysUserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
* 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("===============Shiro权限认证开始============ [ roles、permissions]==========");
Long userId = null;
String loginName = null;
if (null != principalCollection) {
LoginUserVo loginUserVoVo = (LoginUserVo) principalCollection.getPrimaryPrincipal();
userId = loginUserVoVo.getId();
loginName = loginUserVoVo.getLoginName();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//TODO: 设置用户拥有的角色集合
Set<String> roleSet = userService.selectUserRoles(userId);
info.setRoles(roleSet);
//TODO: 设置用户拥有的权限集合
Set<String> permissionSet = userService.selectUserPermissions(userId);
info.addStringPermissions(permissionSet);
log.info("===============Shiro权限认证成功==============");
return info;
}
/**
* 用户信息认证是在用户进行登录的时候进行验证
* 验证用户输入的账号和密码是否正确,错误抛出异常
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("===============Shiro登录认证开始============");
//TODO: 校验token
String token = (String) authenticationToken.getCredentials();
if (StringUtils.isEmpty(token)) {
throw new AuthenticationException("token为空!");
}
//TODO:从token中取出用户名
String username = jwtTokenUtil.getUserNameFromToken(token);
if (org.apache.commons.lang3.StringUtils.isEmpty(username)) {
throw new AuthenticationException("token非法无效!");
}
//TODO: 判断用户是否存在
LoginUserVo loginUserVoVo = userService.selectLoginUserVoByLoginName(username);
if (ObjectUtils.isEmpty(loginUserVoVo)) {
throw new AuthenticationException("用户不存在!");
}
//TODO: 判断用户状态
if (loginUserVoVo.getStatus() == 1) {
throw new AuthenticationException("账号已被锁定,请联系管理员!");
}
//TODO:校验token是否超时失效
if (!jwtTokenUtil.validateToken(token, username)) {
throw new AuthenticationException("Token失效,请重新登录!");
}
log.info("===============Shiro登录认证成功============");
return new SimpleAuthenticationInfo(loginUserVoVo, token, getName());
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}
通过代码实现拦截
@Configuration
public class ShiroJwtConfig {
@Resource(name = "shiroRedisTemplate")
private RedisTemplate redisTemplate;
/**
* 全局缓存时间,单位为秒
*/
@Value("${hdw.jwt.expiration}")
private int cacheLive;
/**
* 全局缓存名称前缀,默认为应用名
*/
@Value("${spring.application.name}")
private String cacheKeyPrefix;
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* Filter Chain定义说明
* <p>
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//TODO:配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/sys/captcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/encrypt", "anon");//加密
filterChainDefinitionMap.put("/api/**", "anon");// API接口
//TODO:开放的静态资源
filterChainDefinitionMap.put("/favicon.ico", "anon");// 网站图标
filterChainDefinitionMap.put("/bootstrap/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/font/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/upload/**", "anon");
filterChainDefinitionMap.put("/qr/**", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.html", "anon");
filterChainDefinitionMap.put("/**/*.svg", "anon");
filterChainDefinitionMap.put("/**/*.pdf", "anon");
filterChainDefinitionMap.put("/**/*.jpg", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.ico", "anon");
//TODO:排除字体格式的后缀
filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.woff2", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
//TODO:性能监控
filterChainDefinitionMap.put("/actuator/**", "anon");
//TODO:测试示例
filterChainDefinitionMap.put("/test/**", "anon"); //模板页面
//TODO:websocket排除
filterChainDefinitionMap.put("/ws/**", "anon");
//TODO:添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//TODO:过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 安全管理器配置
*
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义缓存
securityManager.setCacheManager(shiroRedisCacheManager());
// 设置自定义realm
securityManager.setRealm(jwtRealm());
securityManager.setSubjectDAO(subjectDAO());
// 设置自定义会话
securityManager.setSessionManager(sessionManager());
return securityManager;
}
//创建自定义realm
@Bean
public Realm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm();
jwtRealm.setCacheManager(shiroRedisCacheManager());
jwtRealm.setCachingEnabled(true);
return jwtRealm;
}
/**
* 缓存管理器
*
* @return
*/
@Bean
public CacheManager shiroRedisCacheManager() {
ShiroRedisCacheManager redisCacheManager = new ShiroRedisCacheManager(cacheLive * 1000, cacheKeyPrefix + ":shiro-cache:", redisTemplate);
return redisCacheManager;
}
/**
* 会话管理器
*
* @return
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setCacheManager(shiroRedisCacheManager());
/**
* 会话验证
* Shiro提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;
* 出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;
* 但是如在web环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,
* Shiro提供了会话验证调度器SessionValidationScheduler来做这件事情。
*/
//TODO:单位为毫秒(1秒=1000毫秒)设置为7天
sessionManager.setSessionValidationInterval(1000 * cacheLive);
//TODO: 设置全局session超时时间
sessionManager.setGlobalSessionTimeout(1000 * cacheLive);
//TODO:删除过期session
sessionManager.setDeleteInvalidSessions(true);
//TODO: 开启/禁用绘画验证
sessionManager.setSessionValidationSchedulerEnabled(false);
SimpleCookie cookie = new SimpleCookie();
//TODO:设置Cookie名字,默认为JSESSIONID;
cookie.setName(cacheKeyPrefix + "-");
//TODO:设置Cookie的域名,默认空,即当前访问的域名;
cookie.setDomain("");
//TODO:设置Cookie的路径,默认空,即存储在域名根下;
cookie.setPath("");
//TODO:设置Cookie的过期时间,单位为秒,默认-1表示关闭浏览器时过期Cookie;
cookie.setMaxAge(cacheLive);
//TODO:如果设置为true,则客户端不会暴露给客户端脚本代码,使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击
cookie.setHttpOnly(true);
//TODO:sessionManager创建会话Cookie的模板
sessionManager.setSessionIdCookie(cookie);
//TODO:是否启用/禁用Session Id Cookie,默认是启用的;如果禁用后将不会设置Session Id Cookie,即默认使用了Servlet容器的JSESSIONID,且通过URL重写(URL中的“;JSESSIONID=id”部分)保存Session Id。
sessionManager.setSessionIdCookieEnabled(false);
return sessionManager;
}
/**
* 关闭Shiro自带的Session,详见文档
* http://shiro.apache.org/session-management.html#SessionManagement
*
* @return
*/
@Bean
public DefaultSubjectDAO subjectDAO() {
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
return subjectDAO;
}
/**
* 下面的代码是添加注解支持
*
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
以上拦截认证授权的具体业务场景可以从各自的实际情况进行修改