1.pom文件
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
2.shiro配置类
package club.jiajiajia.bulider.config.shiro;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import club.jiajiajia.bulider.entity.sys.SysPermission;
import club.jiajiajia.bulider.entity.sys.SysRole;
import club.jiajiajia.bulider.service.SystemService;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
/**
* shiro配置类
* @author JIA_JIAJIA
* @website http://www.jiajiajia.club
* @da2019年5月6日
*/
@Configuration
public class ShiroConfig {
/**
* 注入service查询系统全部权限
*/
@Autowired
private SystemService systemService;
/**
* 自定义realm
* 用于认证和授权
* @return
*/
@Bean(name="userRealm")
public UserRealm getUserRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCachingEnabled(true);
//启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
userRealm.setAuthenticationCachingEnabled(true);
//缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
userRealm.setAuthenticationCacheName("authenticationCache");
//启用授权缓存,即缓存AuthorizationInfo信息,默认false
userRealm.setAuthorizationCachingEnabled(true);
//缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
/**
* 安全管理器
* @return
*/
@Bean(name="securityManager")
public DefaultWebSecurityManager getSecurityManager(@Qualifier("userRealm")UserRealm userRealm,
@Qualifier("ehCacheManager")EhCacheManager ehCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setCacheManager(ehCacheManager);
return securityManager;
}
/***
* 开启权限缓存
* 避免每次请求都会调用UserRealm中的授权方法
* @return
*/
@Bean(name="ehCacheManager")
public EhCacheManager getEhCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return ehCacheManager;
}
/**
* 设置过滤规则
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("roles", new CustomRolesAuthorizationFilter());//覆盖原来的shiro拦截器
/**
* 自定义权限拦截器,重写了shiro自带的roles拦截器。
* 主要目的是为了重写认证失败后返回的信息,例如ajax请求没有权限的路径是,弹出提示,您没有访问权限等。
*/
shiroFilterFactoryBean.setSecurityManager(securityManager);//
shiroFilterFactoryBean.setLoginUrl("/builder");
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/errorPage");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
initPower(filterChainDefinitionMap);//掉用加载数据库中的权限
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 从数据库中初始化权限到过滤规则中
* @param power
*/
public void initPower(Map<String,String> power) {
power.put("/css/**", "anon");
power.put("/img/**", "anon");
power.put("/js/**", "anon");
power.put("/layuiadmin/**", "anon");
power.put("/views/**", "anon");
power.put("/login", "anon");
power.put("/user/check", "anon");
power.put("/logout", "anon");
List<SysRole> role=systemService.initAllPower();
for(SysRole r:role) {
for(SysPermission sp:r.getPermission()) {
if(power.containsKey(sp.getUrl())) {
String u=power.get(sp.getUrl());
u=u.substring(0,u.length()-1);
u+=","+r.getRoleName()+"]";
power.put(sp.getUrl(),u);
}else {
power.put(sp.getUrl(),"roles["+r.getRoleName()+"]");
}
}
}
power.put("/**", "authc");
}
}
注入SystemService主要用于初始化查询数据库中的权限相关。
UserRealm 是自定义的登录的授权类。
EhCacheManager 是 EhCache 缓存管理器
ShiroFilterFactoryBean 自定义过滤规则(主要用于过滤配置,或从数据库中获取权限配置拦截)
3.自定义登录验证和授权
package club.jiajiajia.bulider.config.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
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 club.jiajiajia.bulider.entity.sys.SysPermission;
import club.jiajiajia.bulider.entity.sys.SysRole;
import club.jiajiajia.bulider.entity.sys.SysUser;
import club.jiajiajia.bulider.service.SystemService;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* UserRealm自定义登录验证和授权(一般用于从数据库或缓存中查询数据)
* @author JIA_JIAJIA
* @website http://www.jiajiajia.club
* @da2019年5月5日
*
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
private SystemService sysUserService;
/**
* 授权(权限拦截时调用,因为有缓存,所以一般情况下只调用一次,除非系统修改权限,清除缓存时会再次调用)
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUser sysUser = (SysUser) principals.getPrimaryPrincipal();
List<SysRole> sysRoles = sysUserService.selectRoleByUserId(sysUser.getId());
System.err.println(sysRoles);
/**
* 授权
*/
Set<String> roles = new HashSet<String>();
List<String> sysPermissions=new ArrayList<String>();
for(int i=0;i<sysRoles.size();i++) {
SysRole sysRole=sysRoles.get(i);
roles.add(sysRole.getRoleName());
List<SysPermission> sp=sysRole.getPermission();
if(sp!=null) {
for(int j=0;j<sp.size();j++) {
sysPermissions.add(sp.get(i).getUrl());
}
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.addStringPermissions(sysPermissions);
return info;
}
/**
* 认证(登录时调用)
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
SysUser sysUser = sysUserService.findByUserName(token.getUsername());
System.err.println("用户认证");
if(sysUser==null){
throw new UnknownAccountException("用户不存在!");
}else {
if(!sysUser.getPassword().equals(new String(token.getPassword()))){
System.out.println(sysUser.getPassword()+":"+new String(token.getPassword()));
throw new LockedAccountException("密码错误");
}
}
return new SimpleAuthenticationInfo(sysUser,sysUser.getPassword(),getName());
}
/****
*清除所有的权限缓存
*/
public void clearAuthz(){
System.err.println("清空所有的用户缓存");
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
/**
* 重写方法,清除当前用户的的 授权缓存
* @param principals
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 重写方法,清除当前用户的 认证缓存
* @param principals
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
/**
* 自定义方法:清除所有 授权缓存
*/
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
/**
* 自定义方法:清除所有 认证缓存
*/
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
/**
* 自定义方法:清除所有的 认证缓存 和 授权缓存
*/
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
使用如下方法进行登录的时候会调用doGetAuthorizationInfo方法进行认证,一般是从数据库中查询用户信息,比对验证。
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
如果配置了缓存,那么在第一次遇到需要拦截的资源时,会调用doGetAuthorizationInfo方法从数据库中查询用户的相关权限,然后交给shiro定义的拦截器或者是我们自定义的拦截器。在这个项目中是交给了我们自定义的拦截器。因为在上面的配置中可以看出,我们是重写了shiro的roles拦截器。并重写的他的认证方法。
filters.put("roles", new CustomRolesAuthorizationFilter());//覆盖原来的shiro拦截器
4.重写shiro的角色拦截器CustomRolesAuthorizationFilter
package club.jiajiajia.bulider.config.shiro;
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
import com.alibaba.fastjson.JSONObject;
import club.jiajiajia.bulider.util.ResultMap;
/**
* 重写RolesAuthorizationFilter类的onAccessDenied方法和isAccessAllowed方法
* 项目中的每一个请求都会进isAccessAllowed方法判断是否有权限访问,或者时候有角色限制,
* 返回true代表有权限访问,返回false代表没有权限访问
*
* 在授权失败的时候,也就是isAccessAllowed方法返回false的时候,会自动调用onAccessDenied方法。
* 重写onAccessDenied回调方法的目的就是为了返回json数据。友好提示
*
* @author JIAJIAJIA
* @data 2018年8月31日 下午2:56:12
* @description TODO
*/
public class CustomRolesAuthorizationFilter extends RolesAuthorizationFilter {
/***
* isAccessAllowed返回false时掉用
* 请求过滤的回调方法
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
if (subject.getPrincipal() == null) {
if (isAjaxRequest((HttpServletRequest)request)) {//是ajax请求
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
ResultMap resultData = ResultMap.fail("登录认证失效,请重新登录!",1081);
response.getWriter().write(JSONObject.toJSONString(resultData));
}else {
saveRequestAndRedirectToLogin(request, response);
}
} else {
String unauthorizedUrl = getUnauthorizedUrl();
if(isAjaxRequest((HttpServletRequest)request)) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
ResultMap resultData = ResultMap.fail("您没有权限访问",1081);
response.getWriter().write(JSONObject.toJSONString(resultData));
}else {
if (StringUtils.hasText(unauthorizedUrl))
WebUtils.issueRedirect(request, response, unauthorizedUrl);
else
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
return false;
}
/**
* 请求过滤
*/
@Override
public boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) {
Subject subject = getSubject(req, resp);
String[] rolesArray = (String[]) mappedValue;
if (rolesArray == null || rolesArray.length == 0)//没有角色限制,有权限访问
return true;
for (int i = 0; i < rolesArray.length; i++)
if (subject.hasRole(rolesArray[i]))//若当前用户是rolesArray中的任何一个,则有权限访问
return true;
return false;
}
/**
* 判断是否是ajax请求
* @param request
* @return
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String requestedWith = request.getHeader("x-requested-with");
if (requestedWith != null && requestedWith.equalsIgnoreCase("XMLHttpRequest")) {
return true;
} else {
return false;
}
}
}
这个类就是上面提到的自定义的shiro拦截器。当需要进行资源拦截的时候回执行isAccessAllowed方法进行权限验证。如果验证通过(返回true),则正常执行后边的拦截器(如果还有的话),如果认证失败(返回false)则执行onAccessDenied回调函数,进行后续的操作。比如现在重写roles拦截器,并重写onAccessDenied方法的目的就是为了当用户用ajax请求一些没有权限的资源时能够返回json字符串,能够在ajax请求后弹出框或其他形式提示用户。
5.因为项目中用到了缓存,下面贴出缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
<!--
缓存对象存放路径
java.io.tmpdir:默认的临时文件存放路径。
user.home:用户的主目录。
user.dir:用户的当前工作目录,即当前程序所对应的工作路径。
其它通过命令行指定的系统属性,如“java –DdiskStore.path=D:\\abc ……”。
-->
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!-- 授权缓存 -->
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<!-- 认证缓存 -->
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
经过上边的配置后基本拦截功能应该已经能够实现了
6.值得注意的是:
没有配置缓存的时候遇到需要拦截的资源会调用自定义的isAccessAllowed()方法进行拦截。其中执行的
subject.hasRole(rolesArray[i])
或
subject.isPermitted(String str)
就是在调用自定义UserRealm中的doGetAuthorizationInfo授权方法获取权限(一般从数据库中查询)。
但是在配置了缓存管理器以后,再调用hasRole方法或者isPermitted方法,如果缓存管理器中有缓存的存在,将会直接缓存中获取,将不再执行AuthorizationInfo授权方法。那么就有必要考虑和解决一个问题:当用户修改了对某个用户的角色,或者是某个角色的权限的时候。如何动态的加载更新后的权限,而不至于重启项目,并且清除缓存中的用户权限,能够实时的更改权限的相关配置。(实现的效果是两个同时在线的用户a,b当用户a修改了b用户的权限时,b用户能够实时做出修改权限后的回应,不应该重启项目,或重新登录)
对上边的问题,当然有解决的方案:
/**
* 更新权限配置
*/
public ResultMap updatePermission() {
RealmSecurityManager rsm = (RealmSecurityManager)SecurityUtils.getSecurityManager();
UserRealm realm = (UserRealm)rsm.getRealms().iterator().next();
realm.clearAllCache();
synchronized (shiroFilterFactoryBean) {
AbstractShiroFilter shiroFilter;
try {
shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
} catch (Exception e) {
throw new RuntimeException("get ShiroFilter from shiroFilterFactoryBean error!");
}
PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
// 清空老的权限控制
manager.getFilterChains().clear();
shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
shiroConfig.initPower(filterChainDefinitionMap);//掉用加载数据库中的权限
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 重新构建生成
Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue().trim().replace(" ", "");
manager.createChain(url, chainDefinition);
}
}
return ResultMap.success(Message.SUCCESS);
}
意思就是在更改权限的时候调用相应的方法(这里是updatePermission方法)重置shiro的过滤规则,以及调用相应的方法清空shiro用户权限的缓存,这里调用的
RealmSecurityManager rsm = (RealmSecurityManager)SecurityUtils.getSecurityManager();
UserRealm realm = (UserRealm)rsm.getRealms().iterator().next();
realm.clearAllCache();
这段代码就是在清空用户缓存。这样再次遇到需要拦截的请求就会再次执行授权方法doGetAuthorizationInfo()
当然了shiro也可以这么用:
自定义一个拦截器,拦截所有的资源(配置需要不拦截的除外)然后在isAccessAllowed方法中验证用户是否有访问某个资源的权限。如果这样的话,项目中的所有的可访问资源都需要加入数据库配置,似乎有点太严格了,并且开发的时候,也比较麻烦。
完结。。。
后续将会贴出配置源码包