springboot整合shiro权限

该博客详细介绍了如何在SpringBoot项目中整合Shiro进行权限管理,包括pom文件配置、Shiro配置类、自定义登录验证与授权、重写角色拦截器以及缓存配置。特别强调了在使用缓存时,如何处理用户权限动态更新的问题,通过重置过滤规则和清空用户权限缓存来实现权限的实时变更。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文http://www.jiajiajia.club/blog/artical/205

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方法中验证用户是否有访问某个资源的权限。如果这样的话,项目中的所有的可访问资源都需要加入数据库配置,似乎有点太严格了,并且开发的时候,也比较麻烦。

 

 

完结。。。

后续将会贴出配置源码包

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值