springboot整合shiro权限

硅谷探秘者 1486 0 0

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



完结。。。

后续将会贴出配置源码包


评论区
请写下您的评论...
暂无评论...
猜你喜欢
spring/springmvc 1540 springmvc+mybatisshiro1.需要的jar包propertiesshiro.version1.3.2/shiro.version/propertiesdependency
框架 2563 springbootmybatis1.创建maven项目2.sql文件SETNAMESutf8mb4;SETFOREIGN_KEY_CHECKS=0
框架 2671 1.配置springboot支持websocketpackagecom.example.demo.websocket
weblog 1031 pomparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.1.3.RELEASE/version /parentdependencies dependency groupIdorg.springframework.boot/group
java框架 1378 springbootelasticsearch框架实现全文索引demo配置说明参考:http://www.jiajiajia.club/blog/artical/Ja4t7X/378
框架 2413 安装redis数据库参考:http://www.jiajiajia.club/blog/artical/166redis配置详解参考:http://www.jiajiajia.club/blog/artical/210安装完数据库以后如果不是本地连接记得修改密码requirepass。默认是没有密码需要后台运行修改daemonizeyes默认是noyml配置文件spring:redis:host:
框架 2346 springboot视图层,官方推荐使用thymeleaf。thymeleaf只是渲染html的一种方式,是一种模板。第一步创建一个maven项目第二步:修改Jdk版本,添加thymeleaf
框架 1342 版本说明,不同的springboot版本也对应着不同的elasticsearch版本,如果版本不对应客户端和服务端都会报相应的错误,对应关系请自行百度,本次测试的版本如下:springboot版本
归档
2018-11  12 2018-12  33 2019-01  28 2019-02  28 2019-03  32 2019-04  27 2019-05  33 2019-06  6 2019-07  12 2019-08  12 2019-09  21 2019-10  8 2019-11  15 2019-12  25 2020-01  9 2020-02  5 2020-03  16 2020-04  4 2020-06  1 2020-07  7 2020-08  13 2020-09  9 2020-10  5 2020-12  3 2021-01  1 2021-02  5 2021-03  7 2021-04  4 2021-05  4 2021-06  1 2021-07  7 2021-08  2 2021-09  8 2021-10  9 2021-11  16 2021-12  14 2022-01  7 2022-05  1 2022-08  3 2022-09  2 2022-10  2 2022-12  5 2023-01  3 2023-02  1 2023-03  4 2023-04  2 2023-06  3 2023-07  4 2023-08  1 2023-10  1 2024-02  1 2024-03  1 2024-04  1
标签
算法基础 linux 前端 c++ 数据结构 框架 数据库 计算机基础 储备知识 java基础 ASM 其他 深入理解java虚拟机 nginx git 消息中间件 搜索 maven redis docker dubbo vue 导入导出 软件使用 idea插件 协议 无聊的知识 jenkins springboot mqtt协议 keepalived minio mysql ensp 网络基础 xxl-job rabbitmq haproxy srs 音视频 webrtc javascript
目录
没有一个冬天不可逾越,没有一个春天不会来临。最慢的步伐不是跬步,而是徘徊,最快的脚步不是冲刺,而是坚持。