1. 程式人生 > >Shiro Web集成及攔截器機制

Shiro Web集成及攔截器機制

apach username www. 請求協議 機制 定向 stat mit target

Shiro與 Web 集

Shiro 提供了與 Web 集成的支持,其通過一個 ShiroFilter 入口來攔截需要安全控制的 URL,然後進行相應的控制,ShiroFilter 類似於如 Strut2/SpringMVC 這種 web 框架的前端控制器,其是安全控制的入口點,其負責讀取配置(如 ini 配置文件),然後判斷 URL 是否需要登錄 / 權限等工作。

ShiroFilter 入口

Shiro 1.2 及以後版本的配置方式

從 Shiro 1.2 開始引入了 Environment/WebEnvironment 的概念,即由它們的實現提供相應的 SecurityManager 及其相應的依賴。ShiroFilter 會自動找到 Environment 然後獲取相應的依賴。

<listener>
   <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

通過 EnvironmentLoaderListener 來創建相應的 WebEnvironment,並自動綁定到 ServletContext,默認使用 IniWebEnvironment 實現。

可以通過如下配置修改默認實現及其加載的配置文件位置:

<context-param>
   <param-name>shiroEnvironmentClass</param-name>
   <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
</context-param>
    <context-param>
        <param-name>shiroConfigLocations</param-name>
        <param-value>classpath:shiro.ini</param-value>
    </context-param>

shiroConfigLocations 默認是 “/WEB-INF/shiro.ini”,IniWebEnvironment 默認是先從 / WEB-INF/shiro.ini 加載,如果沒有就默認加載 classpath:shiro.ini。

與 Spring 集成

<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>

Web INI 配置

[main]
\#默認是/login.jsp
authc.loginUrl=/login
roles.unauthorizedUrl=/unauthorized
perms.unauthorizedUrl=/unauthorized
[users]
zhang=123,admin
wang=123
[roles]
admin=user:*,menu:*
[urls]
/login=anon
/unauthorized=anon
/static/**=anon
/authenticated=authc
/role=authc,roles[admin]
/permission=authc,perms["user:create"]

其中最重要的就是 [urls] 部分的配置,其格式是: “url = 攔截器 [參數],攔截器[參數]”;即如果當前請求的 url 匹配[urls] 部分的某個 url 模式,將會執行其配置的攔截器。比如 anon 攔截器表示匿名訪問(即不需要登錄即可訪問);authc 攔截器表示需要身份認證通過後才能訪問;roles[admin]攔截器表示需要有 admin 角色授權才能訪問;而 perms["user:create"]攔截器表示需要有 “user:create” 權限才能訪問。

url 模式使用 Ant 風格模式
Ant 路徑通配符支持?、、,註意通配符匹配不包括目錄分隔符 “/”:
?:匹配一個字符,如”/admin?” 將匹配 / admin1,但不匹配 / admin 或 / admin2;
\:匹配零個或多個字符串,如 / admin * 將匹配 / admin、/admin123,但不匹配 / admin/1;
**\:匹配路徑中的零個或多個路徑
,如 / admin/** 將匹配 / admin/a 或 / admin/a/b。

url 模式匹配順序

url 模式匹配順序是按照在配置中的聲明順序匹配,即從頭開始使用第一個匹配的 url 模式對應的攔截器鏈。如:

/bb/**=filter1
/bb/aa=filter2
/**=filter3

如果請求的 url 是 “/bb/aa”,因為按照聲明順序進行匹配,那麽將使用 filter1 進行攔截。

攔截器將在下一節詳細介紹。接著我們來看看身份驗證、授權及退出在 web 中如何實現。

身份驗證(登錄)

首先配置需要身份驗證的 url

/authenticated=authc
/role=authc,roles[admin]
/permission=authc,perms["user:create"]

即訪問這些地址時會首先判斷用戶有沒有登錄,如果沒有登錄默會跳轉到登錄頁面,默認是 / login.jsp,可以通過在 [main] 部分通過如下配置修改:

authc.loginUrl=/login

Shiro 攔截器機制

攔截器機制

攔截器介紹

Shiro 使用了與 Servlet 一樣的 Filter 接口進行擴展;所以如果對 Filter 不熟悉可以參考《Servlet 3.1 規範》http://www.iteye.com/blogs/subjects/Servlet-3-1了解 Filter 的工作原理。首先下圖是 Shiro 攔截器的基礎類圖:

技術分享圖片

1、NameableFilter
NameableFilter 給 Filter 起個名字,如果沒有設置默認就是 FilterName;還記得之前的如 authc 嗎?當我們組裝攔截器鏈時會根據這個名字找到相應的攔截器實例;

2、OncePerRequestFilter
OncePerRequestFilter 用於防止多次執行 Filter 的;也就是說一次請求只會走一次攔截器鏈;另外提供 enabled 屬性,表示是否開啟該攔截器實例,默認 enabled=true 表示開啟,如果不想讓某個攔截器工作,可以設置為 false 即可。

3、ShiroFilter
ShiroFilter 是整個 Shiro 的入口點,用於攔截需要安全控制的請求進行處理,這個之前已經用過了。

4、AdviceFilter
AdviceFilter 提供了 AOP 風格的支持,類似於 SpringMVC 中的 Interceptor:

  • preHandler:類似於 AOP 中的前置增強;在攔截器鏈執行之前執行;如果返回 true 則繼續攔截器鏈;否則中斷後續的攔截器鏈的執行直接返回;進行預處理(如基於表單的身份驗證、授權)
  • postHandle:類似於 AOP 中的後置返回增強;在攔截器鏈執行完成後執行;進行後處理(如記錄執行時間之類的);
  • afterCompletion:類似於 AOP 中的後置最終增強;即不管有沒有異常都會執行;可以進行清理資源(如接觸 Subject 與線程的綁定之類的);

5、PathMatchingFilter

PathMatchingFilter 提供了基於 Ant 風格的請求路徑匹配功能及攔截器參數解析的功能,如“roles[admin,user]”自動根據“,”分割解析到一個路徑參數配置並綁定到相應的路徑:

pathsMatch:該方法用於 path 與請求路徑進行匹配的方法;如果匹配返回 true;
onPreHandle:在 preHandle 中,當 pathsMatch 匹配一個路徑後,會調用 opPreHandler 方法並將路徑綁定參數配置傳給 mappedValue;然後可以在這個方法中進行一些驗證(如角色授權),如果驗證失敗可以返回 false 中斷流程;默認返回 true;也就是說子類可以只實現 onPreHandle 即可,無須實現 preHandle。如果沒有 path 與請求路徑匹配,默認是通過的(即 preHandle 返回 true)。

6、AccessControlFilter

AccessControlFilter 提供了訪問控制的基礎功能;比如是否允許訪問/當訪問拒絕時如何處理等:

isAccessAllowed:表示是否允許訪問;mappedValue 就是[urls]配置中攔截器參數部分,如果允許訪問返回 true,否則 false;

onAccessDenied:表示當訪問拒絕時是否已經處理了;如果返回 true 表示需要繼續處理;如果返回 false 表示該攔截器實例已經處理了,將直接返回即可。

onPreHandle 會自動調用這兩個方法決定是否繼續處理:

boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

另外 AccessControlFilter 還提供了如下方法用於處理如登錄成功後/重定向到上一個請求:

void setLoginUrl(String loginUrl) //身份驗證時使用,默認/login.jsp
String getLoginUrl()
Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject 實例
boolean isLoginRequest(ServletRequest request, ServletResponse response)//當前請求是否是登錄請求
void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當前請求保存起來並重定向到登錄頁面
void saveRequest(ServletRequest request) //將請求保存起來,如登錄成功後再重定向回該請求
void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登錄頁面

比如基於表單的身份驗證就需要使用這些功能。

到此基本的攔截器就完事了,如果我們想進行訪問訪問的控制就可以繼承 AccessControlFilter;如果我們要添加一些通用數據我們可以直接繼承 PathMatchingFilter。

基於表單登錄攔截器

之前我們已經使用過 Shiro 內置的基於表單登錄的攔截器了,此處自己做一個類似的基於表單登錄的攔截器。

public class FormLoginFilter extends PathMatchingFilter {
    private String loginUrl = "/login.jsp";
    private String successUrl = "/";
    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        if(SecurityUtils.getSubject().isAuthenticated()) {
            return true;//已經登錄過
        }
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if(isLoginRequest(req)) {
            if("post".equalsIgnoreCase(req.getMethod())) {//form表單提交
                boolean loginSuccess = login(req); //登錄
                if(loginSuccess) {
                    return redirectToSuccessUrl(req, resp);
                }
            }
            return true;//繼續過濾器鏈
        } else {//保存當前地址並重定向到登錄界面
            saveRequestAndRedirectToLogin(req, resp);
            return false;
        }
    }
    private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        WebUtils.redirectToSavedRequest(req, resp, successUrl);
        return false;
    }
    private void saveRequestAndRedirectToLogin(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        WebUtils.saveRequest(req);
        WebUtils.issueRedirect(req, resp, loginUrl);
    }
    private boolean login(HttpServletRequest req) {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        try {
            SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
        } catch (Exception e) {
            req.setAttribute("shiroLoginFailure", e.getClass());
            return false;
        }
        return true;
    }
    private boolean isLoginRequest(HttpServletRequest req) {
        return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));
    }
}

onPreHandle 主要流程:

  1. 首先判斷是否已經登錄過了,如果已經登錄過了繼續攔截器鏈即可;
  2. 如果沒有登錄,看看是否是登錄請求,如果是 get 方法的登錄頁面請求,則繼續攔截器鏈(到請求頁面),否則如果是 get 方法的其他頁面請求則保存當前請求並重定向到登錄頁面;
  3. 如果是 post 方法的登錄頁面表單提交請求,則收集用戶名 / 密碼登錄即可,如果失敗了保存錯誤消息到 “shiroLoginFailure” 並返回到登錄頁面;
  4. 如果登錄成功了,且之前有保存的請求,則重定向到之前的這個請求,否則到默認的成功頁面。

shiro.ini 配置

[filters]
formLogin=com.github.zhangkaitao.shiro.chapter8.web.filter.FormLoginFilter
[urls]
/test.jsp=formLogin
/login.jsp=formLogin

任意角色授權攔截器

Shiro 提供 roles 攔截器,其驗證用戶擁有所有角色,沒有提供驗證用戶擁有任意角色的攔截器。

public class AnyRolesFilter extends AccessControlFilter {
    private String unauthorizedUrl = "/unauthorized.jsp";
    private String loginUrl = "/login.jsp";
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        String[] roles = (String[])mappedValue;
        if(roles == null) {
            return true;//如果沒有設置角色參數,默認成功
        }
        for(String role : roles) {
            if(getSubject(request, response).hasRole(role)) {
                return true;
            }
        }
        return false;//跳到onAccessDenied處理
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if (subject.getPrincipal() == null) {//表示沒有登錄,重定向到登錄頁面
            saveRequest(request);
            WebUtils.issueRedirect(request, response, loginUrl);
        } else {
            if (StringUtils.hasText(unauthorizedUrl)) {//如果有未授權頁面跳轉過去
                WebUtils.issueRedirect(request, response, unauthorizedUrl);
            } else {//否則返回401未授權狀態碼
                WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
            }
        }
        return false;
    }
} 

流程:

  1. 首先判斷用戶有沒有任意角色,如果沒有返回 false,將到 onAccessDenied 進行處理;
  2. 如果用戶沒有角色,接著判斷用戶有沒有登錄,如果沒有登錄先重定向到登錄;
  3. 如果用戶沒有角色且設置了未授權頁面(unauthorizedUrl),那麽重定向到未授權頁面;否則直接返回 401 未授權錯誤碼。

shiro.ini 配置

[filters]
anyRoles=com.github.zhangkaitao.shiro.chapter8.web.filter.AnyRolesFilter
[urls]
/test.jsp=formLogin,anyRoles[admin,user]
/login.jsp=formLogin

默認攔截器

Shiro 內置了很多默認的攔截器,比如身份驗證、授權等相關的。默認攔截器可以參考 org.apache.shiro.web.filter.mgt.DefaultFilter 中的枚舉攔截器:

默認攔截器名

攔截器類

說明(括號裏的表示默認值)

身份驗證相關的

authc

org.apache.shiro.web.filter.authc

.FormAuthenticationFilter

基於表單的攔截器;如 “`/**=authc`”,如果沒有登錄會跳到相應的登錄頁面登錄;主要屬性:usernameParam:表單提交的用戶名參數名( username); passwordParam:表單提交的密碼參數名(password); rememberMeParam:表單提交的密碼參數名(rememberMe); loginUrl:登錄頁面地址(/login.jsp);successUrl:登錄成功後的默認重定向地址; failureKeyAttribute:登錄失敗後錯誤信息存儲 key(shiroLoginFailure);

authcBasic

org.apache.shiro.web.filter.authc

.BasicHttpAuthenticationFilter

Basic HTTP 身份驗證攔截器,主要屬性: applicationName:彈出登錄框顯示的信息(application);

logout

org.apache.shiro.web.filter.authc

.LogoutFilter

退出攔截器,主要屬性:redirectUrl:退出成功後重定向的地址(/); 示例 “/logout=logout”

user

org.apache.shiro.web.filter.authc

.UserFilter

用戶攔截器,用戶已經身份驗證 / 記住我登錄的都可;示例 “/**=user”

anon

org.apache.shiro.web.filter.authc

.AnonymousFilter

匿名攔截器,即不需要登錄即可訪問;一般用於靜態資源過濾;示例 “/static/**=anon”

授權相關的

roles

org.apache.shiro.web.filter.authz

.RolesAuthorizationFilter

角色授權攔截器,驗證用戶是否擁有所有角色;主要屬性: loginUrl:登錄頁面地址(/login.jsp);unauthorizedUrl:未授權後重定向的地址;示例 “/admin/**=roles[admin]”

perms

org.apache.shiro.web.filter.authz

.PermissionsAuthorizationFilter

權限授權攔截器,驗證用戶是否擁有所有權限;屬性和 roles 一樣;示例 “/user/**=perms["user:create"]”

port

org.apache.shiro.web.filter.authz

.PortFilter

端口攔截器,主要屬性:port(80):可以通過的端口;示例 “/test= port[80]”,如果用戶訪問該頁面是非 80,將自動將請求端口改為 80 並重定向到該 80 端口,其他路徑 / 參數等都一樣

rest

org.apache.shiro.web.filter.authz

.HttpMethodPermissionFilter

rest 風格攔截器,自動根據請求方法構建權限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)構建權限字符串;示例 “/users=rest[user]”,會自動拼出“user:read,user:create,user:update,user:delete” 權限字符串進行權限匹配(所有都得匹配,isPermittedAll);

ssl

org.apache.shiro.web.filter.authz

.SslFilter

SSL 攔截器,只有請求協議是 https 才能通過;否則自動跳轉會 https 端口(443);其他和 port 攔截器一樣;

其他

noSessionCreation

org.apache.shiro.web.filter.session

.NoSessionCreationFilter

不創建會話攔截器,調用 subject.getSession(false) 不會有什麽問題,但是如果 subject.getSession(true) 將拋出 DisabledSessionException 異常;

Shiro Web集成及攔截器機制