Shiro
Shiro
- 登陆、授权、拦截
 - 按钮权限控制
 
一、目标
- Maven+Spring+shiro
 - 自定义登陆、授权
 - 自定义拦截器
 - 加载数据库资源构建拦截链
 
使用总结:
1、需要设计的数据库:用户、角色、权限、资源
2、可以通过,角色,权限,两个拦截器同时确定是否能访问
3、角色与权限的关系,role1=permission1,permission2,多级的权限:sys:permission1,拥有高级权限同时用于低级权限。
4、perms[“permission1”] 为权限
5、拦截器机制介绍了拦截角色还是权限
6、角色与权限 是两个概念
7、权限-资源,一对一。资源分为上下级,因此权限分为父权限,子权限。创建资源的时候,创建权限。权限里资源的别名
8、角色-权限,一对多。角色里权限的别名
9、按钮是通过权限来控制的
10、防止有父级资源可以访问,子级资源不能访问的情况,不适用 sys:add 权限写法
二、代码
1、Pom.xml

 1     <properties>
 2         <spring.version>4.3.4.RELEASE</spring.version>
 3     </properties>
 4         <dependency>
 5             <groupId>junit</groupId>
 6             <artifactId>junit</artifactId>
 7             <version>4.9</version>
 8         </dependency>
 9         <dependency>
10             <groupId>commons-logging</groupId>
11             <artifactId>commons-logging</artifactId>
12             <version>1.1.3</version>
13         </dependency>
14         <dependency>
15             <groupId>org.apache.shiro</groupId>
16             <artifactId>shiro-core</artifactId>
17             <version>1.2.2</version>
18         </dependency>
19         <dependency>
20             <groupId>org.apache.shiro</groupId>
21             <artifactId>shiro-spring</artifactId>
22             <version>1.2.2</version>
23         </dependency>
24         <dependency>
25             <groupId>javax.servlet</groupId>
26             <artifactId>javax.servlet-api</artifactId>
27             <version>3.0.1</version>
28             <scope>provided</scope>
29         </dependency>
30         <dependency>
31             <groupId>org.springframework</groupId>
32             <artifactId>spring-web</artifactId>
33             <version>${spring.version}</version>
34         </dependency>
35         
36             
37             
38             
39         
40         
41             
42             
43             
44         
45         
46             
47             
48             
49         
50         
51             
52             
53             
54     

2、web.xml
Servlet拦截访问,使用注解更方便,需要删除项目中的servlet使用javax.servlet-api 3.0 包

 1 package com.cyd.shiro;
 2
 3 import java.io.IOException;
 4
 5 import javax.servlet.ServletException;
 6 import javax.servlet.annotation.WebServlet;
 7 import javax.servlet.http.HttpServlet;
 8 import javax.servlet.http.HttpServletRequest;
 9 import javax.servlet.http.HttpServletResponse;
10
11 import org.apache.shiro.SecurityUtils;
12 import org.apache.shiro.authc.AuthenticationException;
13 import org.apache.shiro.authc.IncorrectCredentialsException;
14 import org.apache.shiro.authc.UnknownAccountException;
15 import org.apache.shiro.authc.UsernamePasswordToken;
16 import org.apache.shiro.subject.Subject;
17 import org.apache.shiro.web.util.SavedRequest;
18 import org.apache.shiro.web.util.WebUtils;
19 import org.junit.Test;
20
21 @WebServlet(name = “loginServlet”, urlPatterns = “/loginController”)
22 public class LoginServlet extends HttpServlet {
23     @Override
24     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
25         req.getRequestDispatcher(“login.jsp”).forward(req, resp);
26     }
27
28     @Override
29     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
30         System.out.println(LoginServlet.class.toString());
31         String error = null;
32         String username = req.getParameter(“username”);
33         String password = req.getParameter(“password”);
34         Subject subject = SecurityUtils.getSubject();
35         UsernamePasswordToken token = new UsernamePasswordToken(username, password);
36         try {
37             subject.login(token);
38         } catch (UnknownAccountException e) {
39             error = “用户名/密码错误”;
40         } catch (IncorrectCredentialsException e) {
41             error = “用户名/密码错误”;
42         } catch (AuthenticationException e) {
43             // 其他错误,比如锁定,如果想单独处理请单独catch处理
44             error = “其他错误:” + e.getMessage();
45         }
46         if (error != null) {// 出错了,返回登录页面
47             req.setAttribute(“error”, error);
48             req.getRequestDispatcher(“login.jsp”).forward(req, resp);
49         } else {// 登录成功
50             //跳转到拦截登陆前的地址
51             SavedRequest request=WebUtils.getSavedRequest(req);
52             String url =request.getRequestURI();
53             req.getRequestDispatcher(url.substring(url.lastIndexOf(‘/‘))).forward(req, resp);
54         }
55     }
56
57 }

3、Spring-shiro.xml

<beans xmlns=“http://www.springframework.org/schema/beans“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“ xmlns:context=“http://www.springframework.org/schema/context“ xmlns:util=“http://www.springframework.org/schema/util“ xsi:schemaLocation=“http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/util
    http://www.springframework.org/schema/util/spring-util-4.2.xsd"\>
<context:component-scan base-package\="com.cyd.shiro.\*"\></context:component-scan\>
<!-- Shiro的Web过滤器 \-->
<bean id\="shiroFilter" class\="com.cyd.shiro.ExtendShiroFilterFactoryBean"\>
    <property name\="securityManager" ref\="securityManager" />
    <property name\="loginUrl" value\="/login.jsp" />
    <!-- <property name="successUrl" value="/index.jsp" /> \-->
    <property name\="unauthorizedUrl" value\="/unauthorized.jsp" />
    <property name\="filters"\>
        <util:map\>
            <!-- <entry key="onperms" value-ref="URLPermissionsFilter" /> \-->
            <entry key\="onrole" value-ref\="ExtendRolesAuthorizationFilter" />
        </util:map\>
    </property\> 
    <property name\="filterChainDefinitions"\>
        <value\> /unauthorized.jsp = anon
            /logoutController=anon
            /login.jsp=authc
        </value\>
    </property\>
</bean\>
<!-- 安全管理器 \-->
<bean id\="securityManager" class\="org.apache.shiro.web.mgt.DefaultWebSecurityManager"\>
    <property name\="realm" ref\="myRealm" />
    <property name\="cacheManager" ref\="cacheManager" />
</bean\>
<!-- 自定义认证,授权 \-->
<bean id\="myRealm" class\="com.cyd.shiro.AdminRealm"\></bean\>
<!-- 注册ehcache,不然每次访问都要登陆 \-->
<bean id\="cacheManager" class\="org.apache.shiro.cache.ehcache.EhCacheManager"\>
    <property name\="cacheManagerConfigFile" value\="classpath:ehcache.xml" />
</bean\>
<!-- 自定义鉴权拦截器 \-->
<bean id\="URLPermissionsFilter" class\="com.cyd.shiro.URLPermissionsFilter" />
<bean id\="ExtendRolesAuthorizationFilter" class\="com.cyd.shiro.ExtendRolesAuthorizationFilter" />
</beans>

4、Ehcache.xml 缓存

<ehcache xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“ xsi:noNamespaceSchemaLocation=“../config/ehcache.xsd”>
<diskStore path=“java.io.tmpdir”/>
<defaultCache
        maxElementsInMemory=“10000” eternal=“false” timeToIdleSeconds=“600” timeToLiveSeconds=“600” overflowToDisk=“true” maxElementsOnDisk=“10000000” diskPersistent=“false” diskExpiryThreadIntervalSeconds=“120” memoryStoreEvictionPolicy=“LRU”
        />
</ehcache>

5、登陆Servlet

package com.cyd.shiro;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
@WebServlet(name = “loginServlet”, urlPatterns = “/loginController”)
public class LoginServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher(“login.jsp”).forward(req, resp);
    }
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    System.out.println(LoginServlet.class.toString());
    String error = null;
    String username = req.getParameter("username");
    String password = req.getParameter("password");
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        subject.login(token); 
    } catch (UnknownAccountException e) { 
        error = "用户名/密码错误";
    } catch (IncorrectCredentialsException e) {
        error = "用户名/密码错误";
    } catch (AuthenticationException e) {
        // 其他错误,比如锁定,如果想单独处理请单独catch处理
        error = "其他错误:" + e.getMessage();
    }
    if (error != null) {// 出错了,返回登录页面
        req.setAttribute("error", error);
        req.getRequestDispatcher("login.jsp").forward(req, resp);
    } else {// 登录成功
        //跳转到拦截登陆前的地址
        SavedRequest request=WebUtils.getSavedRequest(req);
        String url =request.getRequestURI();
        req.getRequestDispatcher(url.substring(url.lastIndexOf('/'))).forward(req, resp);
    }
}
}

6、自定义登陆、授权。
根据需求自定义登陆异常。从数据库查询出当前用户拥有的权限并授权

 1 package com.cyd.shiro;
 2
 3 import java.util.HashSet;
 4 import java.util.LinkedList;
 5 import java.util.List;
 6 import java.util.Set;
 7
 8 import org.apache.shiro.authc.AuthenticationException;
 9 import org.apache.shiro.authc.AuthenticationInfo;
10 import org.apache.shiro.authc.AuthenticationToken;
11 import org.apache.shiro.authc.SimpleAuthenticationInfo;
12 import org.apache.shiro.authc.UnknownAccountException;
13 import org.apache.shiro.authz.AuthorizationInfo;
14 import org.apache.shiro.authz.SimpleAuthorizationInfo;
15 import org.apache.shiro.realm.AuthorizingRealm;
16 import org.apache.shiro.subject.PrincipalCollection;
17 import org.springframework.beans.factory.annotation.Autowired;
18
19 import com.cyd.helloworld.SysRoles;
20 import com.cyd.helloworld.SysUsers;
21 import com.cyd.shiro.admin.SysUsersService;
22
23 public class AdminRealm extends AuthorizingRealm {
24
25     @Autowired
26     private SysUsersService    sysusersservice;
27     // 认证登陆
28     @Override
29     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
30         System.out.println(“do doGetAuthenticationInfo”);
31         String username = (String) token.getPrincipal();
32         SysUsers user = sysusersservice.getSysUsers(username);
33         if (user == null) {
34             throw new UnknownAccountException();// 没找到帐号
35         }
36         SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(), // 用户名
37                 user.getPassWorld(), // 密码
38                 getName() // realm name
39         );
40         return authenticationInfo;
41     }
42
43     // 用户授权
44     @Override
45     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
46         System.out.println(“do doGetAuthorizationInfo”);
47         String username = (String)principals.getPrimaryPrincipal();
48         SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
49         //从数据库加载当前用户的角色,例如:[admin]
50         authorizationInfo.setRoles(new HashSet
51         //从数据库加载当前用户可以访问的资源,例如:[index.jsp, abc.jsp]
52         authorizationInfo.setStringPermissions(new HashSet
53
54         return authorizationInfo;
55     }
56 }

7、自定义拦截器。
重写拦截器是因为shiro 验证是否有权限访问是需要当前用户拥有拦截器链的所有权限。一般需求只需要拥有部分权限即可。
角色验证拦截,hasRole和hasAllRoles 验证是否有权限。

 1 package com.cyd.shiro;
 2
 3 import java.io.IOException;
 4 import java.util.Set;
 5
 6 import javax.servlet.ServletRequest;
 7 import javax.servlet.ServletResponse;
 8
 9 import org.apache.shiro.subject.Subject;
10 import org.apache.shiro.util.CollectionUtils;
11 import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
12
13 /**
14  * 通过角色验证权限
15  * @author chenyd
16  * 2017年11月21日
17  */
18 public class ExtendRolesAuthorizationFilter extends RolesAuthorizationFilter{
19
20     @Override
21     public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
22
23         System.out.println(ExtendRolesAuthorizationFilter.class.toString());
24         Subject subject = getSubject(request, response);
25         String[] rolesArray = (String[]) mappedValue;
26
27         if (rolesArray == null || rolesArray.length == 0) {
28             //no roles specified, so nothing to check - allow access.
29             return true;
30         }
31         //AbstractFilter
32         Set
33
34         boolean flag=false;
35         for(String role: roles){
36             if(subject.hasRole(role)){
37                 flag=true;
38                 break;
39             }
40         }
41         return flag;
42     }
43 }

url拦截校验,isPermitted和isPermittedAll验证是否有权限访问,

 1 package com.cyd.shiro;
 2
 3 import java.io.IOException;
 4
 5 import javax.servlet.ServletRequest;
 6 import javax.servlet.ServletResponse;
 7 import javax.servlet.http.HttpServletRequest;
 8
 9 import org.apache.shiro.subject.Subject;
10 import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
11 /**
12  * 通过字符串验证权限
13  * @author chenyd
14  * 2017年11月21日
15  */
16 public class URLPermissionsFilter extends PermissionsAuthorizationFilter {
17
18     /**
19      * mappedValue 访问该url时需要的权限
20      * subject.isPermitted 判断访问的用户是否拥有mappedValue权限
21      * 重写拦截器,只要符合配置的一个权限,即可通过
22      */
23     @Override
24     public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
25             throws IOException {
26         System.out.println(URLPermissionsFilter.class.toString());
27         Subject subject = getSubject(request, response);
28         // DefaultFilterChainManager
29         // PathMatchingFilterChainResolver
30         String[] perms = (String[]) mappedValue;
31         boolean isPermitted = false;
32         if (perms != null && perms.length > 0) {
33             for (String str : perms) {
34                 if (subject.isPermitted(str)) {
35                     isPermitted = true;
36                 }
37             }
38         }
39
40         return isPermitted;
41     }
42 }

8、加载数据库资源构建拦截器链

 1 package com.cyd.shiro;
 2
 3 import java.util.Map;
 4
 5 import org.apache.shiro.config.Ini;
 6 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
 7 import org.apache.shiro.util.CollectionUtils;
 8 import org.apache.shiro.web.config.IniFilterChainResolverFactory;
 9 import org.springframework.beans.factory.annotation.Autowired;
10
11 import com.cyd.shiro.admin.SysUsersService;
12
13 public class ExtendShiroFilterFactoryBean extends ShiroFilterFactoryBean{
14
15     @Autowired
16     private SysUsersService    sysusersservice;
17     //PathMatchingFilter
18     @Override
19     public void setFilterChainDefinitions(String definitions) {
20         //数据库中获取权限,{/index.jsp=authc,onrole[“admin2”,”admin”], /abc.jsp=authc,onrole[“admin2”,”admin”]}
21         Map<String, String> otherChains = sysusersservice.getFilterChain();
22         Ini ini = new Ini();
23         ini.load(definitions);
24         Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
25         if (CollectionUtils.isEmpty(section)) {
26             section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
27         }
28         section.putAll(otherChains);
29         setFilterChainDefinitionMap(section);
30     }
31
32 }

三、 学习笔记
1、INI文件配置

[users] #提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2
zhang=123,admin
[roles] #提供了角色及权限之间关系的配置,角色=权限1,权限2
admin=index.jsp
[urls] #配置拦截器链,/** 为拦截器链名称(filterChain),authc,roles[admin],perms[“index.jsp”]拦截器列表名
/login.jsp=anon
/loginController=anon
/unauthorized.jsp=anon
/**=authc,roles[admin],perms[“index.jsp”]

2、拦截器链
Shiro的所有拦截器链名定义在源码DefaultFilter中。
anon
例子/admins/**=anon 没有参数,表示可以匿名使用。
authc
例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
roles
例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,
并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[“admin,guest”],
每个参数通过才算通过,相当于hasAllRoles()方法。
perms
例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,
例如/admins/user/**=perms[“user:add:*,user:modify:*“],当有多个参数时必须每个参数都通过才通过,
想当于isPermitedAll()方法。
rest
例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,
其中method为post,get,delete等。
port
例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString, 其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
authcBasic
例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl
例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user
例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
注:anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
3、拦截器链源码类关系图
 
 
① NameableFilter有一个name属性,定义每一个filter的名字。
② OncePerRequestFilter保证客户端请求后该filter的doFilter只会执行一次。
doFilterInternal非常重要,在shiro整个filter体系中的核心方法及实质入口。另外,shiro是通过在request中设置一个该filter特定的属性值来保证该filter只会执行一次的。
③ AdviceFilter中主要是对doFilterInternal做了更细致的切分。
springmvc中的Interceptor,doFilterInternal会先调用preHandle做一些前置判断,如果返回false则filter链不继续往下执行,
④ AccessControlFilter中的对onPreHandle方法做了进一步细化。
isAccessAllowed方法和onAccessDenied方法达到控制效果。这两个方法都是抽象方法,由子类去实现。到这一层应该明白。isAccessAllowed和onAccessDenied方法会影响到onPreHandle方法,而onPreHandle方法会影响到preHandle方法,而preHandle方法会达到控制filter链是否执行下去的效果。所以如果正在执行的filter中isAccessAllowed和onAccessDenied都返回false,则整个filter控制链都将结束,不会到达目标方法(客户端请求的接口),而是直接跳转到某个页面(由filter定义的,将会在authc中看到)。
⑤ FormAuthenticationFiltershiro提供的登录的filter,
saveRequestAndRedirectToLogin保存request并拦截到登陆页面,登陆成功后可从WebUtils.getSavedRequest(req);中取出。
四、未实现的功能
- 动态URL权限控制。当修改权限时,重新加载拦截器链。
 - 密码加密
 - 记住我
 - 在线人数控制
 - 集成验证码
 
五、参考链接
- spring mvc整合shiro登录 权限验证 http://blog.csdn.net/rongku/article/details/51336424
 - Shiro(4)默认鉴权与自定义鉴权 http://blog.csdn.net/zhengwei223/article/details/9981741
 - 拦截器机制-跟我学shiro http://jinnianshilongnian.iteye.com/blog/2025656
 - shiro Filter–拦截器源码解释 https://www.cnblogs.com/yoohot/p/6085830.html
 - 动态URL http://blog.csdn.net/shadowsick/article/details/39001273
 - 重写shirofilterbean方式加载数据库资源权限 http://blog.csdn.net/qq_18333833/article/details/70243620