前言
接上篇,关于Shiro框架的学习(一),这篇会记录下Shiro整合Web、整合SSM的过程,之后就可以直接应用在项目的安全控制上。
关于整合Web
-
环境
Eclipse、MySQL、Tomcat8
-
准备工作
-
创建Dynamic Web工程:
使用到的类
准备User实体类、ShiroDao类、DatabaseRealm类,这三个类在上一篇文章中已经提及,这里不再重复赘述。数据库
沿用上一篇博文中未加密的数据库,数据库脚本上一篇已提及,同样不再重复贴代码了。-
jar包:
本次需要用到的jar包主要有如下几个
-
-
修改shiro.ini配置文件和web.xml配置文件
配置文件中指定了寻找DatabaseRealm的方法、指定了每个页面需要什么角色和权限、指定了如果没有权限将会跳转到哪个页面。
[main] #使用数据库进行验证和授权 databaseRealm=com.shiro.DatabaseRealm securityManager.realms=$databaseRealm #当访问需要验证的页面,但是又没有验证的情况下,跳转到login.jsp authc.loginUrl=/login.jsp #当访问需要角色的页面,但是又不拥有这个角色的情况下,跳转到noroles.jsp roles.unauthorizedUrl=/noRoles.jsp #当访问需要权限的页面,但是又不拥有这个权限的情况下,跳转到noperms.jsp perms.unauthorizedUrl=/noPerms.jsp #users,roles和perms都通过前面知识点的数据库配置了 [users] #urls用来指定哪些资源需要什么对应的授权才能使用 [urls] #doLogout地址就会进行退出行为 /doLogout=logout #login.jsp,noroles.jsp,noperms.jsp 可以匿名访问 /login.jsp=anon /noroles.jsp=anon /noperms.jsp=anon #阅读博客,需要登录后才可以查看 /readBlog.jsp=authc #新增博客不仅需要登录,而且要拥有 blogManager 角色才可以操作 /addBlog.jsp=authc,roles[blogManager] #删除博客,不仅需要登录,而且要拥有 deleteBlog 权限才可以操作 /deleteBlog.jsp=authc,perms["deleteBlog"]
web.xml配置了加载shiro.ini的配置
在<web-app>
中配置<listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <context-param> <param-name>shiroEnvironmentClass</param-name> <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value> <!-- 默认先从/WEB-INF/shiro.ini,如果没有找classpath:shiro.ini --> </context-param> <context-param> <param-name>shiroConfigLocations</param-name> <param-value>classpath:shiro.ini</param-value> </context-param> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
-
Servlet
新建一个LoginServlet,负责控制登录验证。
@WebServlet(name = "loginServlet", urlPatterns = "/login") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); String password = request.getParameter("password"); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(name, password); try { subject.login(token); //通过subject获取session Session session=subject.getSession(); session.setAttribute("subject", subject); response.sendRedirect(""); }catch (AuthenticationException e) { request.setAttribute("error", "验证失败"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }
-
Jsp前台页
- index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> </head> <body> <div class="workingroom"> <div class="loginDiv"> <c:if test="${empty subject.principal}"> <a href="login.jsp">登录</a><br> </c:if> <c:if test="${!empty subject.principal}"> <span class="desc">你好,${subject.principal},</span> <a href="doLogout">退出</a><br> </c:if> <a href="readBlog.jsp">查看博客</a><span class="desc">(登录后才可以查看) </span><br> <a href="addBlog.jsp">新增博客</a><span class="desc">(要有博客管理员角色, Reader是读者,Object是博客管理员) </span><br> <a href="deleteBlog.jsp">删除博客</a><span class="desc">(要有删除订单权限, Object有该权限) </span><br> </div> </body> </html>
- login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> <div class="errorInfo">${error}</div> <form action="login" method="post"> 账号: <input type="text" name="name"> <br> 密码: <input type="password" name="password"> <br> <br> <input type="submit" value="登录"> <br> <br> <div> <span class="desc">账号:Object 密码:123456 角色:blogManager</span><br> <span class="desc">账号:Reader 密码:654321 角色:reader</span><br> </div> </form> </div>
- readBlog.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> readBlog.jsp ,能进来,就表示已经登录成功了 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
- addBlog.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> addBlog.jsp,能进来<br>就表示拥有 blogManager 角色 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
- deleteBlog.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> deleteBlog.jsp ,能进来,就表示有deleteBlog权限 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
- noRoles.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> 角色不匹配 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
- noPerms.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> 权限不足 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
- style.css(页面样式)
span.desc{ margin-left:20px; color:gray; } div.workingroom{ margin:200px auto; width:400px; } div.workingroom a{ display:inline-block; margin-top:20px; } div.loginDiv{ text-align: left; } div.errorInfo{ color:red; font-size:0.65em; }
-
测试
打开tomcat服务器,在浏览器url输入:localhost:8080/ShiroWeb
登录Object
点击各功能:
登录Reader后点击各功能:
除了ReadBlog可以进以外,其余都失败
关于整合SSM
-
使用@RequireRoles注解
核心:
这种方式只需要在Controller映射页面的方法上加上@RequireRoles("需要的权限")就可以轻松控制页面所需的权限了,但是在真实开发中,如果权限改变,那么你就需要一直去修改源代码,这样显然不合适,所以这种方式一带而过,可以在这里了解:[Shiro系列教材](http://how2j.cn/k/shiro/shiro-plan/1732.html)。
-
使用基于URL配置权限
这种方式主要是不用在每个页面的映射上都加所需的权限,而是动态地将角色信息和权限信息写入数据库,再读取数据库,看页面是否需要拦截,访问页面需要什么权限等。
-
从零搭建
首先先修改表结构DROP DATABASE IF EXISTS shiro; CREATE DATABASE shiro DEFAULT CHARACTER SET utf8; USE shiro; drop table if exists user; drop table if exists role; drop table if exists permission; drop table if exists user_role; drop table if exists role_permission; create table user ( id bigint auto_increment, name varchar(100), password varchar(100), salt varchar(100), constraint pk_users primary key(id) ) charset=utf8 ENGINE=InnoDB; create table role ( id bigint auto_increment, name varchar(100), desc_ varchar(100), constraint pk_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table permission ( id bigint auto_increment, name varchar(100), desc_ varchar(100), url varchar(100), constraint pk_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; create table user_role ( id bigint auto_increment, uid bigint, rid bigint, constraint pk_users_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table role_permission ( id bigint auto_increment, rid bigint, pid bigint, constraint pk_roles_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB;
实际上上面这段代码就是对原来的表新增了几个字段,新增字段如下:
permission:desc_、url
role:desc_插入表数据:
INSERT INTO `permission` VALUES (1,'addblog','新增博客','/addBlog'); INSERT INTO `permission` VALUES (2,'readerBlog','阅读博客','/readBlog'); INSERT INTO `role` VALUES (1,'blogManager','博客管理员'); INSERT INTO `role` VALUES (2,'reader','读者'); INSERT INTO `role_permission` VALUES (1,1,1); INSERT INTO `role_permission` VALUES (2,2,2); INSERT INTO `user` VALUES (1,'Object','a7d59dfc5332749cb801f86a24f5f590','e5ykFiNwShfCXvBRPr3wXg=='); INSERT INTO `user` VALUES (2,'Reader','43e28304197b9216e45ab1ce8dac831b','jPz19y7arvYIGhuUjsb6sQ=='); INSERT INTO `user_role` VALUES (1,2,2); INSERT INTO `user_role` VALUES (2,1,1);
配置文件web.xml
在web.xml文件中要配置四块内容,分别是,中文过滤器、Spring相关、MVC相关、Shiro过滤器相关。
先说说shiro过滤器
<!-- Shiro--> <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>
shiro过滤器默认拦截所有请求
Spring相关:主要有两个,一个是Spring整合Mybatis,一个是Spring整合Shiro
<!-- spring --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext.xml, classpath:applicationContext-shiro.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
MVC相关:
就是日常的 MVC配置<!-- spring mvc --> <servlet> <servlet-name>mvc-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- spring mvc--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMVC.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>mvc-dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
- Spring中关于Shiro的配置:
1.SecurityManager:Shiro的核心安全管理类。
<!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="databaseRealm" /> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean>
2.HashedCredentialsMatcher:密码匹配器,可散列
<!-- 密码匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean>
3.LifecycleBeanPostProcessor:保证了Shiro内部lifecycle函数的执行
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
4.ShiroFilterFactoryBean:shiro的过滤器工厂类
<!-- url过滤器 --> <bean id="urlPathMatchingFilter" class="com.shiro.filter.URLPathMatchingFilter"/> <!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 调用我们配置的权限管理器 --> <property name="securityManager" ref="securityManager" /> <!-- 配置我们的登录请求地址 --> <property name="loginUrl" value="/login" /> <!-- 如果您请求的资源不再您的权限范围,则跳转到/403请求地址 --> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- 退出 --> <property name="filters"> <util:map> <entry key="logout" value-ref="logoutFilter" /> <entry key="url" value-ref="urlPathMatchingFilter" /> </util:map> </property> <!-- 权限配置 --> <property name="filterChainDefinitions" ref="filterChainDefinitions"/> </bean> <!-- 退出过滤器 --> <bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter"> <property name="redirectUrl" value="/index" /> </bean>
5.FilterChainDefinitions:配置可从数据库中读取页面权限的LinkedHashMap
<bean id="filterChainDefinitions" factory-bean="filterChainDefinitionsFactory" factory-method="buildFilterChainDefinitionMap"></bean> <bean id="filterChainDefinitionsFactory" class="com.shiro.entity.FilterChainDefinitions"> </bean>
6.会话相关
<!-- 会话ID生成器 --> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" /> <!-- 会话Cookie模板 关闭浏览器立即失效 --> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid" /> <property name="httpOnly" value="true" /> <property name="maxAge" value="-1" /> </bean> <!-- 会话DAO --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="sessionIdGenerator" ref="sessionIdGenerator" /> </bean> <!-- 会话验证调度器,每30分钟执行一次验证 ,设定会话超时及保存 --> <bean name="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler"> <property name="interval" value="1800000" /> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 会话管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 全局会话超时时间(单位毫秒),默认30分钟 --> <property name="globalSessionTimeout" value="1800000" /> <property name="deleteInvalidSessions" value="true" /> <property name="sessionValidationSchedulerEnabled" value="true" /> <property name="sessionValidationScheduler" ref="sessionValidationScheduler" /> <property name="sessionDAO" ref="sessionDAO" /> <property name="sessionIdCookieEnabled" value="true" /> <property name="sessionIdCookie" ref="sessionIdCookie" /> </bean>
7.自定义Realm
<bean id="databaseRealm" class="com.shiro.service.impl.DatabaseRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> </bean>
- 核心代码:
因为这是基于url的权限管理,所以,就不会再有ini配置文件了(实际上配置到Spring中),Realm直接从数据库中获取用户的信息,给用户做验证和授权操作。
最核心的代码还是自定义Realm:
public class DatabaseRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private PermissionService permissionService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //获取用户名 String userName = (String) principalCollection.getPrimaryPrincipal(); //从数据库中获取角色和权限 Set<String> permissions = permissionService.getStringPermissionByName(userName); Set<String> roles = roleService.listPermissionURLs(userName); //建立简单授权对象 SimpleAuthorizationInfo s = new SimpleAuthorizationInfo(); //设置权限和角色 s.setStringPermissions(permissions); s.setRoles(roles); //授权 return s; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken t = (UsernamePasswordToken) token; String userName = token.getPrincipal().toString(); User user = userService.getUser(userName); String passwordInDB = user.getPassword(); System.out.println(passwordInDB); System.out.println(t.getPassword()); String salt = user.getSalt(); //做用户验证 SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt), getName()); return a; } }
Subject会将用户的信息交给上述Realm做验证和授权,做验证很好理解,登录即验证,但是要怎么判断一个url是否要做权限验证呢?
在PermissionService中有两个方法,分别是needIntercepter
和listPermissionURLs
,第一个方法的作用是判断一个url是否要验证,如果权限表中有这个url,则需要进行授权,如果没有则直接放行,listPermissionURLs是判断一个用户有权访问的所有url@Override public boolean needInterceptor(String requestURI) { // TODO Auto-generated method stub List<Permission> permissionList = permissionDao.listPermission(); for(Permission p : permissionList) { if(p.getUrl().equals(requestURI)) { return true; } } return false; } @Override public Set<String> listPermissionURLs(String userName) { // TODO Auto-generated method stub Set<String> result = new HashSet<>(); List<Permission> permissions = permissionDao.queryPermissionByUsername(userName); for(Permission p : permissions) { result.add(p.getUrl()); } return result; }
于是,在过滤器和Realm中就可以调用这两个方法判断是否需要授权和进行授权了。
过滤器类:public class URLPathMatchingFilter extends PathMatchingFilter { @Autowired PermissionService permissionService; @Override protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { String requestURI = getPathWithinApplication(request); System.out.println("requestURI:" + requestURI); Subject subject = SecurityUtils.getSubject(); // 如果没有登录,就跳转到登录页面 if (!subject.isAuthenticated()) { WebUtils.issueRedirect(request, response, "/login"); return false; } // 看看这个路径权限里有没有维护,如果没有维护,一律放行(也可以改为一律不放行) boolean needInterceptor = permissionService.needInterceptor(requestURI); if (!needInterceptor) { return true; } else { //如果有维护判断是否有权限访问(进入Realm进行授权) if (subject.isPermitted(requestURI)) return true; else { UnauthorizedException ex = new UnauthorizedException("当前用户没有访问路径 " + requestURI + " 的权限"); subject.getSession().setAttribute("ex", ex); WebUtils.issueRedirect(request, response, "/unauthorized"); return false; } } } }
-
-
最后运行结果
在运行最后结果之前,先明确一下数据表中用户的角色与权限。
所以运行结果如下:
-
登录Object
、
-
Object登录成功
-
Object访问addBlog
-
Object访问readBlog
-
登录Reader
-
Reader访问addBlog
-
Reader访问readBlog
结语
两天时间学完Shiro并写完了自己的学习笔记,从网上找资料学习到遇到各种坑,到差点偏离Shiro的方向,好在还是完成了Shiro的修炼,总算理解了这一系列验证及授权的流程,之后应该将Shiro结合项目使用,并去理解其原理和工作流程。
参考资料:Shiro系列教材
欢迎大家访问我的个人博客:Object's Blog