關於Shiro框架的學習(二)
前言
接上篇,關於Shiro框架的學習(一),這篇會記錄下Shiro整合Web、整合SSM的過程,之後就可以直接應用在專案的安全控制上。
關於整合Web
-
環境
Eclipse、MySQL、Tomcat8 -
準備工作
- 建立Dynamic Web工程:
-
修改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
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系列教材。-
使用基於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,desc_ varchar(100),constraint pk_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table permission ( id bigint auto_increment,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,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); INSERT INTO `user_role` VALUES (2,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,"/unauthorized"); return false; } } } } 複製程式碼
-
-
最後執行結果
在執行最後結果之前,先明確一下資料表中使用者的角色與許可權。
所以執行結果如下:
- 登入Object
- Object登入成功
- Object訪問addBlog
- Object訪問readBlog
- 登入Reader
- Reader訪問addBlog
- Reader訪問readBlog
結語
兩天時間學完Shiro並寫完了自己的學習筆記,從網上找資料學習到遇到各種坑,到差點偏離Shiro的方向,好在還是完成了Shiro的修煉,總算理解了這一系列驗證及授權的流程,之後應該將Shiro結合專案使用,並去理解其原理和工作流程。
參考資料:Shiro系列教材
歡迎大家訪問我的個人部落格:Object's Blog