前言 本章我们来学习 Shiro 集成 Spring,即在 Web 环境下如何使用 Shiro 来进行权限控制。
本章所需知识:
Shiro 认证 && 授权 Spring、SpringMVC 基础环境搭建 引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.0.7.RELEASE</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-all</artifactId > <version > 1.4.0</version > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <version > 1.2.17</version > </dependency > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-log4j12</artifactId > <version > 1.7.25</version > </dependency >
web.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <context-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring.xml, classpath:spring-shiro.xml </param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener > <servlet > <servlet-name > springmvc</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath:spring-web.xml</param-value > </init-param > </servlet > <servlet-mapping > <servlet-name > springmvc</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping >
比较常见的 Spring 配置,这里就不过多介绍了。我们需要 3 个配置文件,分别为 spring.xml
, spring-web.xml
, spring-shiro.xml
。
我们暂时只需要配置 spring-web.xml
, spring-shiro.xml
即可 (spring.xml
文件也需要创建,但不需要配置东西)。
spring-web.xml 1 2 3 4 5 6 7 8 9 10 11 12 <context:component-scan base-package ="im.zhaojun.controller" /> <mvc:annotation-driven > <mvc:message-converters > <bean class ="org.springframework.http.converter.StringHttpMessageConverter" > <constructor-arg value ="UTF-8" /> </bean > </mvc:message-converters > </mvc:annotation-driven > <mvc:default-servlet-handler />
spring-shiro.xml 之前我们都是手工 new
一个 DefaultSecurityManager
,但既然用到了 Spring,就将交由 Spring 容器来管理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <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 ="/index.jsp" /> <property name ="unauthorizedUrl" value ="/unauthorized.jsp" /> <property name ="filterChainDefinitions" > <value > /login.jsp = anon /login = anon /user.jsp = roles[user] /admin.jsp = roles[admin] /userList.jsp = perms[select] /** = authc </value > </property > </bean > <bean id ="securityManager" class ="org.apache.shiro.web.mgt.DefaultWebSecurityManager" > <property name ="realm" ref ="myRealm" /> </bean > <bean id ="myRealm" class ="im.zhaojun.realm.MyRealm" />
securityManager 和自定义 Realm 的配置很容易理解,但 shiroFilter 是个啥东西呢?
其实他是 Shiro 的权限过滤器,用来在 web 环境下对权限进行过滤,既然是一个 Filter,显然我们还需要在 web.xml 中增加 Shiro 的 Filter 配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 <filter > <filter-name > shiroFilter</filter-name > <filter-class > org.springframework.web.filter.DelegatingFilterProxy</filter-class > <init-param > <param-name > targetFilterLifecycle</param-name > <param-value > true</param-value > </init-param > </filter > <filter-mapping > <filter-name > shiroFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping >
注意:这里的 filter-name
一定要与 spring-shiro.xml
中我们配置的对应的 bean 的名称相同。在此示例中均为 :**shiroFilter
**。
这里还有一个 filterChainDefinitions
表示的是过滤器链,即从上到下以此判断,直到获取到当前请求资源的权限。
此处我们将 /login.jsp
和 /login
配置成 anon ,表示的是可以匿名访问 。
user.jsp
配置为 *roles[user]*, 表示的是需要 user 角色 可以访问。admin.jsp
配置为 *roles[admin]*, 表示的是需要 admin 角色 可以访问。
userList.jsp
配置为 *perms[select]*,表示的是需要 select 权限 才可访问。
/**
配置为 authc 表示的是所有页面都需要认证 (登录)后才可访问。
当然还有更多的权限通配符,以及自定义权限通配符,我们会在后面的章节讲到
前端页面 index.jsp 1 2 3 4 5 <html > <body > <h2 > Index Page</h2 > </body > </html >
login.jsp 1 2 3 4 5 6 7 8 9 10 11 <html > <body > <form action ="login" method ="post" > username : <input type ="text" name ="username" > <br > password : <input type ="password" name ="password" > <br > <input type ="submit" value ="Login" > </form > </body > </html >
user.jsp 1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html > <head > <title > User Page</title > </head > <body > <h1 > User Page</h1 > </body > </html >
admin.jsp 1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html > <head > <title > Admin Page</title > </head > <body > <h1 > Admin Page</h1 > </body > </html >
userList.jsp 1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html > <head > <title > UserList Page</title > </head > <body > <h1 > UserList Page</h1 > </body > </html >
后端代码 MyRealm 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package im.zhaojun.realm;import im.zhaojun.pojo.User;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 java.util.HashSet;import java.util.Set;public class MyRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) { System.out.println("MyRealm doGetAuthorizationInfo..." ); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Set<String> roles = new HashSet<>(); roles.add("admin" ); authorizationInfo.setRoles(roles); Set<String> permissions = new HashSet<>(); permissions.add("select" ); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("MyRealm doGetAuthenticationInfo..." ); String username = (String) authenticationToken.getPrincipal(); User user = selectUserByUserName(username); if (user == null ) { throw new UnknownAccountException("当前账户不存在" ); } return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), super .getName()); } private User selectUserByUserName (String username) { if ("zhao" .equals(username)) { return new User(username, "123456" ); } return null ; } }
当前只有一个用户,账户为 zhao
, 密码为 123456
。且拥有 admin
角色和 select
权限。
Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package im.zhaojun.controller;import im.zhaojun.pojo.User;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.subject.Subject;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller public class LoginController { @RequestMapping("login") @ResponseBody public String login (User user) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); try { subject.login(token); } catch (AuthenticationException e) { return e.getMessage(); } return "login success" ; } }
登录成功,返回 login success
,登录失败则返回对应的异常信息。
测试 过滤器基本测试 启动 web 服务,默认情况下会访问 index.jsp
,但我们启动启动项目后自动跳转到了 login.jsp
,且手动访问 index.jsp
页面也会自动跳转到 login.jsp
。
原因是因为我们在 filterChainDefinitions
过滤器链中仅为 login.jsp
与 login
配置了可匿名访问,而 index.jsp
这上述配置中的是需要认证后才可访问。
由此可见 filterChainDefinitions
,过滤器链是正常的。
认证/登录测试 认证失败 先来试试用户名错误 (使用 admin
, 123456
来进行登录),返回结果为 当前账户不存在
,因为我们在 Realm 中抛出了 UnknownAccountException
异常,并设置了相应的 message,所以在 controller 中捕获到了异常并返回给了页面。
再来试试密码错误 (使用 zhao
, 123456
来进行登录),返回结果为 Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - zhao, rememberMe=false] did not match the expected credentials.
。
认证成功 使用正常的账号密码进行登录,返回结果为 login success
。
由此可见认证成功和失败的功能是正常的。
授权测试 角色 我们在过滤器链中关于角色的配置有:/user.jsp = roles[user]
和 /admin.jsp = roles[admin]
。下面测试,假设以账号是 zhao
为例 (具备 user
角色):
未登录情况下:访问 user.jsp
会跳转到登录页面。 已登录 zhao
且具备相应的角色:可以正常访问 user.jsp
。 已登录 zhao
且未具备相应的角色:访问 admin.jsp
会跳转到 unauthorized.jsp
。 权限 我们在过滤器链中关于权限的配置有:/userList.jsp = perms[select]
。下面测试,假设以账号是 zhao
为例 (具备 select
权限):
未登录情况下:访问 userList.jsp
会跳转到登录页面。 已登录 zhao
且具备相应的权限:可以正常访问 userList.jsp
。 已登录 zhao
且未具备相应的权限:和不具备角色一样会跳转到 unauthorized.jsp
。 缓存测试 可能细心的朋友会注意到我在 MyRealm
中的 doGetAuthorizationInfo()
和 doGetAuthenticationInfo()
分别加了一条输出语句,会有以下效果:
认证时会触发 doGetAuthenticationInfo
中的输出语句,证明每次登陆都会调用该方法。
授权时同样会触发 doGetAuthorizationInfo
中的输出语句,且每次授权也需要调用此方法一次。
那么我们来考虑一下缓存问题:
认证就不用说了,涉及到的查询就一条,根据用户名返回用户信息即可,没必要进行缓存。
但授权却比较麻烦,因为授权时我们一般都会去调用数据库来查询其用户所拥有的角色和权限,往往这都会涉及到多表查询。所以我们是否可以将授权数据缓存起来呢?应该如何进行缓存?缓存后角色或权限数据修改了怎么办?这里留下一个悬念,后面的章节中我们会讲到 Shiro 的缓存模块来完善缓存功能。
本章代码地址:https://github.com/zhaojun1998/Premission-Study/tree/master/Permission-Shiro-05/