• Springboot集成Shiro


    Shiro提供身份验证、授权、企业会话管理和加密等功能。

    1、添加依赖:

    		<!-- shiro spring. -->
    		<dependency>
    			<groupId>org.apache.shiro</groupId>
    			<artifactId>shiro-core</artifactId>
    			<version>1.2.2</version>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.shiro</groupId>
    			<artifactId>shiro-spring</artifactId>
    			<version>1.2.2</version>
    		</dependency>
    		<!-- shiro ehcache -->
    		<dependency>
    			<groupId>org.apache.shiro</groupId>
    			<artifactId>shiro-ehcache</artifactId>
    			<version>1.2.2</version>
    		</dependency>
    		<!-- shiro-thymeleaf 2.0.0-->
    		<dependency>
    			<groupId>com.github.theborakompanioni</groupId>
    			<artifactId>thymeleaf-extras-shiro</artifactId>
    			<version>2.0.0</version>
    		</dependency>
    

    2、在src/main/resources添加config文件夹,创建ehcache-shiro.xml文件,用于权限缓存:

    <?xml version="1.0" encoding="UTF-8"?>
    <!--<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"-->
            <!--xsi:noNamespaceSchemaLocation="ehcache.xsd">-->
    <ehcache name="es">
        <diskStore path="java.io.tmpdir"/>
    
        <!--
           name:缓存名称。
           maxElementsInMemory:缓存最大数目
           maxElementsOnDisk:硬盘最大缓存个数。
           eternal:对象是否永久有效,一但设置了,timeout将不起作用。
           overflowToDisk:是否保存到磁盘,当系统当机时
           timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
           timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
           diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
           diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
           diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
           memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
            clearOnFlush:内存数量最大时是否清除。
             memoryStoreEvictionPolicy:
                Ehcache的三种清空策略;
                FIFO,first in first out,这个是大家最熟的,先进先出。
                LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
                LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
        -->
    
        <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="0"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120" />
        <cache 
            name="user"
            maxEntriesLocalHeap="2000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="0"
            overflowToDisk="false"
            statistics="true"/>
            
         <!-- 登录记录缓存锁定1小时 -->
        <cache 
            name="passwordRetryCache"
            maxEntriesLocalHeap="2000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="0"
            overflowToDisk="false"
            statistics="true" />
    </ehcache>
    

    3、实现自定义的ShiroRealm.java类:

    package com.example.demo.realm;
    
    import com.example.demo.entity.Menu;
    import com.example.demo.entity.Role;
    import com.example.demo.entity.User;
    import com.example.demo.service.MenuService;
    import com.example.demo.service.RoleService;
    import com.example.demo.service.UserService;
    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.apache.shiro.util.ByteSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.StringUtils;
    
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    /**
     * 在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO.
     * Shiro的认证过程最终会交由Realm执行,这时会调用Realm的getAuthenticationInfo(token)方法。
     * 该方法主要执行以下操作:
     *
     * 检查提交的进行认证的令牌信息
     * 根据令牌信息从数据源(通常为数据库)中获取用户信息
     * 对用户信息进行匹配验证。
     * 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
     * 验证失败则抛出AuthenticationException异常信息。而在我们的应用程序中要做的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(),重写获取用户信息的方法。
     */
    public class ShiroRealm extends AuthorizingRealm {
    
        @Autowired
        private UserService userService;
        @Autowired
        private RoleService roleService;
        @Autowired
        private MenuService menuService;
    
        /**
         * 验证用户身份
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String userName = (String) token.getPrincipal();
            String password = new String((char[]) token.getCredentials());
    
            //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
            User user = this.userService.findByName(userName);
    
            //这里校验了,CredentialsMatcher就不需要了,如果这里不校验,调用CredentialsMatcher校验
            if (user == null) {
                throw new UnknownAccountException("用户名或密码错误!");
            }
            if (!password.equals(user.getPassword())) {
                throw new IncorrectCredentialsException("用户名或密码错误!");
            }
            if ("0".equals(user.getEnabled())) {
                throw new LockedAccountException("账号已被锁定,请联系管理员!");
            }
            //也可以在此处更新最后登录时间(或在登录方法实现)
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
    //        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()),getName()); ////salt=username+salt
            return info;
        }
    
        /**
         * 授权用户权限
         * 授权的方法是在碰到<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 principalCollection) {
            //获取用户
            User user = (User) SecurityUtils.getSubject().getPrincipal();
    //        User user = (User) principalCollection.getPrimaryPrincipal();
    //        User user=(User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();//获取session中的用户
            SimpleAuthorizationInfo info =  new SimpleAuthorizationInfo();
    
            List<Role> roles=this.roleService.findRolesByUserId(user.getId());
            //获取用户角色
            Set<String> roleSet = new HashSet<String>();
            for (Role role:roles) {
                roleSet.add(role.getName());
            }
            info.setRoles(roleSet);
    
            List<Menu> menus=this.menuService.findMenusByUserId(user.getId());
            //获取用户权限
            Set<String> permissionSet = new HashSet<String>();
            for (Menu menu:menus) {
                if(!StringUtils.isEmpty(menu.getPermission())) { //权限为空会异常,Caused by: java.lang.IllegalArgumentException: Wildcard string cannot be null
                    CollectionUtils.mergeArrayIntoCollection(menu.getPermission().split(","), permissionSet);
                }
            }
            info.setStringPermissions(permissionSet);
            return info;
        }
    
        /**
         * 待补充:
         * shiro+redis集成,避免每次访问有权限的链接都会去执行MyShiroRealm.doGetAuthenticationInfo()方法来查询当前用户的权限,因为实际情况中权限是不会经常变得,这样就可以使用redis进行权限的缓存。
         * 实现shiro链接权限的动态加载,之前要添加一个链接的权限,要在shiro的配置文件中添加filterChainDefinitionMap.put(“/add”, “roles[100002],perms[权限添加]”),这样很不方便管理,一种方法是将权限使用数据库进行加载,另一种是通过init配置文件的方式读取。
         * Shiro 自定义权限校验Filter定义,及功能实现。
         * Shiro Ajax请求权限不满足,拦截后解决方案。这里有一个前提,我们知道Ajax不能做页面redirect和forward跳转,所以Ajax请求假如没登录,那么这个请求给用户的感觉就是没有任何反应,而用户又不知道用户已经退出了。
         * 在线显示,在线用户管理(踢出登录)。
         * 登录注册密码加密传输。
         * 记住我的功能。关闭浏览器后还是登录状态。
         */
    }
    

    4、如有需要实现自定义的密码校验CredentialsMatcher.java:

    package com.example.demo.realm;
    
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
    
    /**
     * shiro中唯一需要程序员编写的两个类:类ShiroRealm完成根据用户名去数据库的查询,并且将用户信息放入shiro中,供第二个类调用.CredentialsMatcher,完成对于密码的校验.其中用户的信息来自ShiroRealm类
     */
    public class CredentialsMatcher extends SimpleCredentialsMatcher {
    
        @Override
        public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
            UsernamePasswordToken utoken=(UsernamePasswordToken) token;
            //获得用户输入的密码:(可以采用加盐(salt)的方式去检验)
            String inPassword = new String(utoken.getPassword());
            //获得数据库中的密码
            String dbPassword=(String) info.getCredentials();
            //进行密码的比对
            return this.equals(inPassword, dbPassword);
        }
        
    }
    

    5、根据需要,选择创建ShiroSessionListener.java:

    package com.example.demo.listener;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.SessionListener;
    
    public class ShiroSessionListener implements SessionListener{
    
    	private final AtomicInteger sessionCount = new AtomicInteger(0);
    	
    	@Override
    	public void onStart(Session session) {
    		sessionCount.incrementAndGet();
    	}
    
    	@Override
    	public void onStop(Session session) {
    		sessionCount.decrementAndGet();
    		
    	}
    
    	@Override
    	public void onExpiration(Session session) {
    		sessionCount.decrementAndGet();
    	}
    }
    
    

    6、根据需要创建自定义Filter:

    package com.example.demo.filter;
    
    import java.io.IOException;
    import java.util.Set;  
      
    import javax.servlet.ServletRequest;  
    import javax.servlet.ServletResponse;  
    import javax.servlet.http.HttpServletRequest;  
      
    import org.apache.shiro.subject.Subject;  
    import org.apache.shiro.util.StringUtils;  
    import org.apache.shiro.web.filter.authz.AuthorizationFilter;  
    import org.apache.shiro.web.util.WebUtils;  
    import org.springframework.beans.factory.annotation.Autowired;
      
    /** 
     * @Type MyFilter.java 
     * @Desc 用于自定义过滤器,过滤用户请求是否被授权 ,MyFilter是用于过滤需要权限校验的请求
     */  
    public class MyFilter extends AuthorizationFilter {
        @SuppressWarnings("unchecked")  
        @Override  
        protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object arg2) throws Exception {
            HttpServletRequest request = (HttpServletRequest) req;  
            //获取请求路径  
            String path = request.getServletPath();  
            Subject subject = getSubject(req, resp);  
            if (null != subject.getPrincipals()) {  
                //根据session中存放的用户权限,比对路径,如果拥有该权限则放行  
                Set<String> userPrivileges = (Set<String>) request.getSession()  
                        .getAttribute("USER_PRIVILEGES");  
                if (null != userPrivileges && userPrivileges.contains(path)) {  
                    return true;  
                }  
            }  
            return false;  
        }  
      
        /** 
         * 会话超时或权限校验未通过的,统一返回401,由前端页面弹窗提示 
         */  
        @Override  
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
            if (isAjax((HttpServletRequest) request)) {  
                WebUtils.toHttp(response).sendError(401);  
            } else {  
                String unauthorizedUrl = getUnauthorizedUrl();  
                if (StringUtils.hasText(unauthorizedUrl)) {  
                    WebUtils.issueRedirect(request, response, unauthorizedUrl);  
                } else {  
                    WebUtils.toHttp(response).sendError(401);  
                }  
            }  
      
            return false;  
        }  
      
        private boolean isAjax(HttpServletRequest request) {  
            String header = request.getHeader("x-requested-with");  
            if (null != header && "XMLHttpRequest".endsWith(header)) {  
                return true;  
            }  
            return false;  
        }  
    } 
    
    package com.example.demo.filter;
    
    import java.io.IOException;
    import javax.servlet.ServletRequest;  
    import javax.servlet.ServletResponse;  
    import javax.servlet.http.HttpServletRequest;
    import org.apache.shiro.subject.Subject;  
    import org.apache.shiro.util.StringUtils;  
    import org.apache.shiro.web.filter.authz.AuthorizationFilter;  
    import org.apache.shiro.web.util.WebUtils;
    /** 
     * @Type LoginFilter.java 
     * @Desc 用于自定义过滤器,过滤用户请求时是否是登录状态 loginFilter主要是覆盖了自带的authc过滤器,让未登录的请求统一返回401
     */  
    public class LoginFilter extends AuthorizationFilter {
        @Override  
        protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object arg2) throws Exception {
            Subject subject = getSubject(req, resp);  
            if (null != subject.getPrincipals()) {  
                return true;  
            }  
            return false;  
        }  
      
        /** 
         * 会话超时或权限校验未通过的,统一返回401,由前端页面弹窗提示 
         */  
        @Override  
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
            if (isAjax((HttpServletRequest) request)) {  
                WebUtils.toHttp(response).sendError(401);  
            } else {  
                String unauthorizedUrl = getUnauthorizedUrl();  
                if (StringUtils.hasText(unauthorizedUrl)) {  
                    WebUtils.issueRedirect(request, response, unauthorizedUrl);  
                } else {  
                    WebUtils.toHttp(response).sendError(401);  
                }  
            }
            return false;  
        }  
      
        private boolean isAjax(HttpServletRequest request) {  
            String header = request.getHeader("x-requested-with");  
            if (null != header && "XMLHttpRequest".endsWith(header)) {  
                return true;  
            }  
            return false;  
        }  
    }  
    

    6、创建Shiro配置类,可用shiro-conf.xml代替:

    package com.example.demo.config;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.LinkedHashMap;
    import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
    import com.example.demo.listener.ShiroSessionListener;
    import com.example.demo.realm.CredentialsMatcher;
    import org.apache.shiro.cache.MemoryConstrainedCacheManager;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.cache.ehcache.EhCacheManager;
    import com.example.demo.realm.ShiroRealm;
    import org.apache.shiro.codec.Base64;
    
    import org.apache.shiro.session.SessionListener;
    import org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler;
    import org.apache.shiro.session.mgt.SessionManager;
    import org.apache.shiro.session.mgt.ValidatingSessionManager;
    import org.apache.shiro.session.mgt.eis.*;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    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.mgt.SecurityManager;
    import org.apache.shiro.web.servlet.SimpleCookie;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    //import org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler
    
    
    /**
     * shiro的配置类,需要注意一点filterChainDefinitionMap必须是LinkedHashMap因为它必须保证有序
     * @author Administrator
     *
     */
    @Configuration
    public class ShiroConfig {
    
    	//授权缓存管理器
    	@Bean
    	public EhCacheManager getEhCacheManager() {
    		EhCacheManager em = new EhCacheManager();
    		em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
    		return em;
    	}
    
    	/**
    	 * 使用上面的Ehcache或下面的shiro自带的内存缓存实现
    	 */
    //	@Bean
    //	public MemoryConstrainedCacheManager getMemoryConstrainedCacheManager() {
    //		return new MemoryConstrainedCacheManager();
    //	}
    
    	/**
    	 * ShiroFilterFactoryBean 处理拦截资源文件问题。
    	 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
    	 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
    
    	 *
    	 * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
    	 * 3、部分过滤器可指定参数,如perms,roles
    	 *
    	 *     <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    	 *			<property name="securityManager" ref="securityManager" />
    	 *			<!-- 配置登录页 -->
    	 *			<property name="loginUrl" value="/login.jsp" />
    	 *			<!-- 配置登录成功后的页面 -->
    	 *			<property name="successUrl" value="/list.jsp" />
    	 *			<property name="unauthorizedUrl" value="/unauthorized.jsp" />
    	 *			<property name="filterChainDefinitions">
    	 *				<value>
    	 *					<!-- 静态资源允许访问 -->
    	 *					<!-- 登录页允许访问 -->
    	 *					/login.jsp = anon
    	 *					/test/login = anon
    	 *					/user/delete = perms["delete"]
    	 *					/logout = logout
    	 *					<!-- 其他资源都需要认证 -->
    	 *					 /** = authc
    	 *				</value>
    	 *			</property>
    	 *		</bean>
    	 */
    	/**
    	 * Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行
    	 * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截,Shiro对基于Spring的Web应用提供了完美的支持
    	 * @param securityManager
    	 * @return
    	 */
    	@Bean(name="shiroFilter")
    	public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
    		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    		// 必须设置 SecurityManager,Shiro的核心安全接口
    		shiroFilterFactoryBean.setSecurityManager(securityManager);
    		// 配置登录的url,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面(源码)
    		shiroFilterFactoryBean.setLoginUrl("/login"); //这是后台的/控制器
    		// 登录成功后要跳转的链接,本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码了
    		shiroFilterFactoryBean.setSuccessUrl("/index"); //这是Index.html页面
    		// 未授权界面;配置不会被拦截的链接 顺序判断
    		shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); ////这里设置403并不会起作用
    
    // 有自定义拦截器就放开 wangzs(源码)
    //		//自定义拦截器
    //		LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
    //		//限制同一帐号同时在线的个数。
    //		//filtersMap.put("kickout", kickoutSessionControlFilter());
    //		shiroFilterFactoryBean.setFilters(filtersMap);
    
    		// 配置访问权限,权限控制map.Shiro连接约束配置,即过滤链的定义,
    		LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    		// 配置不会被拦截的链接 顺序判断
    		// value值的'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
    		// anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
    		// authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
    		// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
    		// filterChainDefinitionMap.put("/logout", "logout");
    		// 从数据库获取动态的权限
    		// filterChainDefinitionMap.put("/add", "perms[权限添加]"); /userList=roles[admin],需要有admin这个角色,如果没有此角色访问此URL会返回无授权页面,或authc,perms[user:list]
    		//       <!-- 需要权限为add的用户才能访问此请求-->
            //        /user=perms[user:add]
    		//       <!-- 需要管理员角色才能访问此页面 -->
            //        /user/add=roles[admin]或roles[admin],perms[user:add]
    		// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
    		// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
    		//logout这个拦截器是shiro已经实现好了的。
    		// 从数据库获取
            /*List<SysPermissionInit> list = sysPermissionInitService.selectAll();
    
            for (SysPermissionInit sysPermissionInit : list) {
                filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
                        sysPermissionInit.getPermissionInit());
            }*/
    
    		/**
    		 * 可自定义过滤器,比如myFilter替代authc
    		 * <bean id="myFilter" class="com.cmcc.hygcc.comm.shiro.MyFilter"></bean>
    		 * <property name="filters">
    		 * 		<map>
    		 * 			<entry key="myFilter" value-ref="myFilter" />
    		 * 				<!-- 覆盖authc过滤器,使得未登录的ajax请求返回401状态 -->
    		 * 			<entry key="authc" value-ref="loginFilter" />
    		 * 		</map>
    		 * </property>
    		 *
    		 * /**=myFilter
    		 */
    		//静态资源允许访问//登录页允许访问,一个URL可以配置多个Filter,使用逗号分隔,当设置多个过滤器时,全部验证通过,才视为通过,部分过滤器可指定参数,如perms,roles
    //		filterChainDefinitionMap.put("/login.html*", "anon"); //表示可以匿名访问,*表示参数如?error等
    		filterChainDefinitionMap.put("/login", "anon");
    		filterChainDefinitionMap.put("/unauthorized", "anon");
    		filterChainDefinitionMap.put("/css/**", "anon");
    		filterChainDefinitionMap.put("/js/**", "anon");
    		filterChainDefinitionMap.put("/fonts/**", "anon");
    		filterChainDefinitionMap.put("/img/**", "anon");
    		filterChainDefinitionMap.put("/druid/**", "anon");
    		filterChainDefinitionMap.put("/user/regist", "anon");
    		filterChainDefinitionMap.put("/gifCode", "anon");
    		filterChainDefinitionMap.put("/logout", "logout"); //logout是shiro提供的过滤器
    		filterChainDefinitionMap.put("/user/delete", "perms["user:delete"]"); //此时访问/user/delete需要delete权限,在自定义Realm中为用户授权。
    		//其他资源都需要认证
    		filterChainDefinitionMap.put("/**", "authc");
    
    		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    
    		return shiroFilterFactoryBean; //Shiro拦截器工厂类注入成功
    	}
    
    	//配置核心安全事务管理器
    	@Bean(name="securityManager")
    	public SecurityManager securityManager(){
    		DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
    		// 设置realm.
    		securityManager.setRealm(shiroRealm()); //如果方法加上参数@Qualifier("shiroRealm") ShiroRealm shiroRealm,可以直接securityManager.setRealm(shiroRealm);
    		//注入记住我管理器;
    		securityManager.setRememberMeManager(rememberMeManager());
    		// 自定义缓存实现 可使用redis
    //		securityManager.setCacheManager(cacheManager());
    		securityManager.setCacheManager(getEhCacheManager()); //缓存管理器
    		// 自定义session管理 可使用redis
    		securityManager.setSessionManager(sessionManager());
    		return securityManager;
    	}
    
    	//Shiro生命周期处理器
    	@Bean(name = "lifecycleBeanPostProcessor")
    	public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    		return new LifecycleBeanPostProcessor();
    	}
    
    	//配置自定义的权限登录器,本段不需要配置自定义的密码比较器,可以换成下面的~~里面的写法,需要创建自定义的密码比较器CredentialsMatcher.java
    	@Bean //必须,身份认证realm; (这个需要自己写,账号密码校验;权限等)
    	public ShiroRealm shiroRealm(){
    		ShiroRealm shiroRealm = new ShiroRealm();
    		return shiroRealm;
    	}
    
    //需要这种方式就放开
    	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    //	//配置自定义的权限登录器
    //	@Bean(name="shiroRealm")
    //	public ShiroRealm shiroRealm(@Qualifier("credentialsMatcher") CredentialsMatcher matcher) {
    //		ShiroRealm shiroRealm=new ShiroRealm();
    //		shiroRealm.setCredentialsMatcher(matcher);
    //		return shiroRealm;
    //	}
    //	//配置自定义的密码比较器
    //	@Bean(name="credentialsMatcher")
    //	public CredentialsMatcher credentialsMatcher() {
    //		return new CredentialsMatcher();
    //	}
    //	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    	/**
    	 * shiro的密码比较器
    	 * @return
    	 */
    //	@Bean
    //	public HashedCredentialsMatcher hashedCredentialsMatcher() {
    //		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    //		hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
    //		hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
    //		return hashedCredentialsMatcher;
    //	}
    
    
    
    	/** //必须
    	 * cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe,自定义
    	 * @return
    	 */
    	public SimpleCookie rememberMeCookie() {
    		//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
    		SimpleCookie cookie = new SimpleCookie("rememberMe");
    		//<!-- 记住我cookie生效时间30天 ,单位秒;-->
    		cookie.setHttpOnly(true);
    		cookie.setMaxAge(86400);
    		return cookie;
    	}
    
    	/** //必须
    	 * cookie管理对象;记住我功能,rememberMe管理器
    	 * @return
    	 */
    	public CookieRememberMeManager rememberMeManager() {
    		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    		cookieRememberMeManager.setCookie(rememberMeCookie());
    		//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位) //3AvVhmFLUs0KTA3Kprsdag==
    		cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
    		return cookieRememberMeManager;
    	}
    
    	/**
    	 * 使用shiro注解为用户授权 1. 在shiro-config.xml开启shiro注解(硬编码,修改权限码很麻烦)
    	 * 在方法上配置注解@RequiresPermissions("xxx:yyy")
    	 * <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
    	 * <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    	 * 		<property name="securityManager" ref="securityManager"/>
    	 * </bean>
    	 * 编程方式实现用户权限控制
    	 *     Subject subject = SecurityUtils.getSubject();
    	 *     if(subject.hasRole("admin")){
    	 *     		//有权限
    	 *		}else{
    	 *			//无权限
    	 *		}
    	 * @return AOP式方法级权限检查,DefaultAdvisorAutoProxyCreator用来扫描上下文,寻找所有的Advistor(通知器),将这些Advisor应用到所有符合切入点的Bean中。
             * LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean
             * 生命周期的目的。
    	 */
    	@Bean
    	@DependsOn({"lifecycleBeanPostProcessor"})
    	public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    		DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    		advisorAutoProxyCreator.setProxyTargetClass(true);
    		return advisorAutoProxyCreator;
    	}
    	// AOP式方法级权限检查
    	@Bean
    	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { //@Qualifier("securityManager") SecurityManager manager
    		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    		return authorizationAttributeSourceAdvisor;
    	}
    
    	@Bean //必须(thymeleaf页面使用shiro标签控制按钮是否显示) //未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect
    	public ShiroDialect shiroDialect() {
    		return new ShiroDialect();
    	}
    
    	//SessionManager和SessionDAO可以不配置,会话DAO
    	@Bean
    	public SessionDAO sessionDAO() {
    		MemorySessionDAO sessionDAO = new MemorySessionDAO();
    		return sessionDAO;
    	}
    
    	/**
    	 * sessionDao的方法2
    	 * @return
    	 */
    //	@Bean //
    //	public SessionIdGenerator sessionIdGenerator() {
    //		return new JavaUuidSessionIdGenerator();
    //	}
    //	@Bean
    //	public SessionDAO sessionDAO() {
    //		EnterpriseCacheSessionDAO cacheSessionDAO=new EnterpriseCacheSessionDAO();
    //		cacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
    //		cacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
    //		return cacheSessionDAO;
    //	}
    
    	//// 会话管理器,设定会话超时及保存
    	@Bean
    	public SessionManager sessionManager() {
    		DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    		Collection<SessionListener> listeners = new ArrayList<SessionListener>();
    		listeners.add(new ShiroSessionListener());
    		sessionManager.setSessionListeners(listeners);
    		sessionManager.setGlobalSessionTimeout(1800000); //全局会话超时时间(单位毫秒),默认30分钟
    		sessionManager.setSessionDAO(sessionDAO());
    		sessionManager.setDeleteInvalidSessions(true);
    		sessionManager.setSessionValidationSchedulerEnabled(true);
    		//定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话
    		sessionManager.setSessionValidationInterval(1800000);
    //		sessionManager.setSessionValidationScheduler(executorServiceSessionValidationScheduler());
    		sessionManager.setSessionIdCookieEnabled(true);
    		sessionManager.setSessionIdCookie(rememberMeCookie());
    		return sessionManager;
    	}
    	//会话验证调度器,每30分钟执行一次验证
    	@Bean(name="sessionValidationScheduler")
    	public ExecutorServiceSessionValidationScheduler executorServiceSessionValidationScheduler() {
    		ExecutorServiceSessionValidationScheduler sessionValidationScheduler=new ExecutorServiceSessionValidationScheduler();
    		sessionValidationScheduler.setInterval(1800000);
    		sessionValidationScheduler.setSessionManager((ValidatingSessionManager)sessionManager());
    		return sessionValidationScheduler;
    	}
    }
    

    7、未授权的设置不生效,需要添加未授权异常处理:

    package com.example.demo.resolver;
    
    import org.apache.shiro.mgt.SecurityManager;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
    
    import java.util.Properties;
    
    /**
     * 定制的异常处理类
     */
    
    //    private void applyUnauthorizedUrlIfNecessary(Filter filter) {
    //        String unauthorizedUrl = getUnauthorizedUrl();
    //        if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) {
    //            AuthorizationFilter authzFilter = (AuthorizationFilter) filter;
    //            //only apply the unauthorizedUrl if they haven't explicitly configured one already:
    //            String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl();
    //            if (existingUnauthorizedUrl == null) {
    //                authzFilter.setUnauthorizedUrl(unauthorizedUrl);
    //            }
    //        }
    //    }
    
    //shiro默认过滤器(10个)
    //        anon -- org.apache.shiro.web.filter.authc.AnonymousFilter
    //        authc -- org.apache.shiro.web.filter.authc.FormAuthenticationFilter
    //        authcBasic -- org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
    //        perms -- org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
    //        port -- org.apache.shiro.web.filter.authz.PortFilter
    //        rest -- org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
    //        roles -- org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
    //        ssl -- org.apache.shiro.web.filter.authz.SslFilter
    //        user -- org.apache.shiro.web.filter.authc.UserFilter
    //        logout -- org.apache.shiro.web.filter.authc.LogoutFilter
    @Configuration
    public class DzExceptionResolver {
        /**
         * shiro中unauthorizedUrl不起作用,这是因为shiro源代码private void applyUnauthorizedUrlIfNecessary(Filter filter)中判断了filter是否为AuthorizationFilter,只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl设置后不起作用。
         * 解决方法:在shiro配置文件中添加(异常全路径做key,错误页面做value)
         * <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
         *       <property name="exceptionMappings">
         *          <props>
         *               <prop key="org.apache.shiro.authz.UnauthorizedException">/403</prop>
         *          </props>
         *       </property>
         *   </bean>
         */
        @Bean
        public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver() {
            SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
            Properties properties=new Properties();
            properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized");
            simpleMappingExceptionResolver.setExceptionMappings(properties);
            return simpleMappingExceptionResolver;
        }
    
        /**
         * 相当于调用SecurityUtils.setSecurityManager(securityManager)
         * @param securityManager
         * @return
         */
        @Bean
        public MethodInvokingFactoryBean getMethodInvokingFactoryBean(@Qualifier("securityManager")SecurityManager securityManager) {
            MethodInvokingFactoryBean methodInvokingFactoryBean=new MethodInvokingFactoryBean();
            methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
            methodInvokingFactoryBean.setArguments(securityManager);
            return methodInvokingFactoryBean;
        }
    }
    
    

    8、登录和退出Controller:

    package com.example.demo.controller;
    
    import com.example.demo.entity.User;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.authz.UnauthorizedException;
    import org.apache.shiro.crypto.hash.SimpleHash;
    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.util.ByteSource;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    import java.util.Properties;
    
    @Controller
    public class LoginController {
    
        @RequestMapping(value = "/login",method = RequestMethod.GET)
        public String login() {
            return "login";
        }
    
        /**
         * 和shiro框架的交互完全通过Subject这个类去交互,用它完成登录,注销,获取当前的用户对象等操作
         * login请求调用subject.login之后,shiro会将token传递给自定义realm,此时realm会先调用doGetAuthenticationInfo(AuthenticationToken authcToken )登录验证的方法,验证通过后会接着调用 doGetAuthorizationInfo(PrincipalCollection principals)获取角色和权限的方法(授权),最后返回视图。
         * 当其他请求进入shiro时,shiro会调用doGetAuthorizationInfo(PrincipalCollection principals)去获取授权信息,若是没有权限或角色,会跳转到未授权页面,若有权限或角色,shiro会放行,此时进入真正的请求方法……
         * @param username
         * @param password
         * @param model
         * @param session
         * @return
         */
        @RequestMapping(value = "/login",method = RequestMethod.POST)
    //    public String loginUser(String username,String password,boolean remeberMe,HttpSession session) {
        public String loginUser(HttpServletRequest request, String username, String password, Model model, HttpSession session) {
    //        password=new SimpleHash("md5", password, ByteSource.Util.bytes(username.toLowerCase() + "shiro"),2).toHex();
    //        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password,remeberMe);
            UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password);
            Subject subject = SecurityUtils.getSubject();
            try {
                subject.login(usernamePasswordToken);   //完成登录
                User user=(User) subject.getPrincipal();
                //更新用户登录时间,也可以在ShiroRealm里面做
                session.setAttribute("user", user);
                model.addAttribute("user",user);
                return "index";
            } catch(Exception e) {
                String exception = (String) request.getAttribute("shiroLoginFailure");
                //logger.info("登录失败从request中获取shiro处理的异常信息,shiroLoginFailure:就是shiro异常类的全类名");
                model.addAttribute("msg",e.getMessage());
                return "login";//返回登录页面
            }
        }
    
        @RequestMapping("/logout")
        public String logout(HttpSession session,Model model) {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
    //        session.removeAttribute("user");
            model.addAttribute("msg","安全退出!");
            return "login";
        }
    }
    

    9、权限测试Controller:

    package com.example.demo.controller;
    
    import com.example.demo.entity.User;
    import com.example.demo.service.UserService;
    import org.apache.shiro.authz.annotation.RequiresPermissions;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class IndexController {
        @Autowired
        private UserService userService;
    
        @RequestMapping("/index")
        public User index() {
            return userService.getUserById(1);
        }
    
        @RequiresPermissions("test")
        @RequestMapping("/test")
        public String test() {
            return "ok";
        }
    }
    

    10、项目用的Thymeleaf,要在页面使用shiro标签,首先添加依赖:

    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-thymeleaf</artifactId>
    		</dependency>
    		<!-- shiro-thymeleaf 2.0.0-->
    		<dependency>
    			<groupId>com.github.theborakompanioni</groupId>
    			<artifactId>thymeleaf-extras-shiro</artifactId>
    			<version>2.0.0</version>
    		</dependency>
    

    11、application.properties配置thymeleaf:

    #thymeleaf的配置是去掉页面缓存(开发环境)和html的校验
    spring.thymeleaf.cache=false
    spring.thymeleaf.mode=LEGACYHTML5
    

    12、在shiro的configuration中配置:

    @Bean
    public ShiroDialect shiroDialect() {
           return new ShiroDialect();
    }
    
    

    13、在html中加入xmlns:

    <html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
    

    14、使用标签。例如:

    <span shiro:authenticated="true" >
            <span>欢迎您:<span th:text="${userInfo.realName}"></span></span>
    </span>
    

    15、标签说明:

    1、用户没有身份验证时显示相应信息,即游客访问信息:
    <shiro:guest>内容</shiro:guest>
    2、用户已经身份验证/记住我登录后显示相应的信息:
    <shiro:user>内容</shiro:user>
    3、用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的:
    <shiro:authenticated>内容</shiro:authenticated>
    4、显示用户身份信息,通常为登录帐号信息,默认调用Subject.getPrincipal()获取,即Primary Principal:
    <shiro:principal/>
    5、用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证,与guest标签的区别是,该标签包含已记住用户。:
    <shiro:notAuthenticated>内容</shiro:notAuthenticated> 
    6、<shiro:principal type="java.lang.String"/>
    相当于Subject.getPrincipals().oneByType(String.class)。 
    7、<shiro:principal property="username"/> 
    相当于((User)Subject.getPrincipals()).getUsername()。  
    8、如果当前Subject有角色将显示body体内容:
    <shiro:hasRole name="角色名">内容</shiro:hasRole>
    9、<shiro:hasAnyRoles name="角色名1,角色名2…">内容</shiro:hasAnyRoles>
    如果当前Subject有任意一个角色(或的关系)将显示body体内容。
    10、<shiro:lacksRole name="角色名">内容</shiro:lacksRole>
    如果当前Subject没有角色将显示body体内容。
    11、<shiro:hasPermission name="权限名">内容</shiro:hasPermission>
    如果当前Subject有权限将显示body体内容。 
    12、<shiro:lacksPermission name="权限名">内容</shiro:lacksPermission>
    如果当前Subject没有权限将显示body体内容。
    

    补充:

    1).需要在templates下创建login.html、index.html、unauthorized.html页面。
    2).自定义Filter,ShiroSessionListener和自定义密码校验以及ShiroConfig中的配置项,按需添加即可。
    
  • 相关阅读:
    [haoi2011]向量
    [haoi2008]硬币购物
    noi2001 [食物链]并查集p1697
    [haoi2012]容易题(数论+容斥的思想)
    p1620田忌赛马(贪心+模拟)
    p1368[扑街]广场铺砖
    [zjoi2010]网络扩容
    是时候写一篇总结了
    P1630 求和
    魔术棋子
  • 原文地址:https://www.cnblogs.com/kibana/p/8953566.html
Copyright © 2020-2023  润新知