Shiro-認證

0) 前言

上一篇[shiro-初體驗]中講解了Shiro的簡單用法, 實現了URL是否須要登陸訪問, 當未登陸訪問URL時自動跳轉至登陸頁.git

本篇主要講解在Shiro如何實現登陸處理. 先簡單說一下Shiro的登陸處理流程.github

Shiro的登陸處理是在authc過濾器中. authc會判斷若是是登陸請求會單獨處理, 所以登陸請求必需要配置成authc.數據庫

其中, 登陸請求包括兩個:apache

  • 訪問登陸: 進入登陸頁面
  • 提交登錄: 登陸頁點擊登陸按鈕發出的請求

Shiro判斷是不是登陸請求時認這兩個登陸請求必須是同一個地址, 而且GET爲訪問登陸頁, POST爲提交登陸安全

// 登陸請求(包括訪問登陸頁和提交登陸)
if (isLoginRequest(request, response)) {
    // 提交登陸
    if (isLoginSubmission(request, response)) {
        // 提交登陸, 執行Shiro的登陸邏輯
        return executeLogin(request, response);
    } else {
        // 訪問登陸請求, 繼續執行進入控制器
        return true;
    }
}
複製代碼

上一篇中, 咱們訪問登陸頁的請求爲/login.jsp, 直接訪問登陸JSP, 若是咱們在用POST訪問JSP顯然是不合理的使用JSP了.bash

所以, 咱們將登陸請求修改成/login, 在控制器中對GETPOST進行處理. 當修改了登陸請求地址時須要在Shiro配置一下app

// Shiro核心配置
shiroFilter(ShiroFilterFactoryBean) {
    // 登陸URL(包括請求登陸頁和提交登陸)
    // 自定義的登陸URL必須單獨設置
    loginUrl = "/login"
    
    // ....
}
複製代碼

相應的,在控制器中也增長兩個方法分別處理登陸請求和提交登陸jsp

// 處理請求登陸頁面
@GetMapping("/login")
public String toLogin() {
    return "/login";
}
 
// 處理提交登陸
@PostMapping("/login")
public String login() {
    System.out.println("處理提交登陸");
    return "/success";
}
複製代碼

登陸頁面: login.jspide

<form action="/login" method="POST">
	<input type="text" name="username" placeholder="用戶名" value="" />
	<input type="password" name="password" placeholder="密碼" value="" />
	<input type="submit" value="當即登陸" />
</form>
複製代碼

完成上述操做後啓動項目, 訪問/page/a時, 因爲未登陸Shiro會重定向至/login, 在登陸頁面輸入用戶名和密碼, 點擊當即登陸按鈕後會以POST方式提交至/login, Shiro就會處理本次登陸請求了.post

  • 用戶名和密碼的name必須爲username和password (Shiro會從Request中取這兩個參數名的值做爲用戶名和密碼)
  • 請求必須是POST, 請求地址必須和Shiro配置文件中的loginUrl保持一致.

那麼, 問題來了, Shiro怎麼知道輸入的用戶名和密碼是否正確呢?

答案必定是不知道, 所以, 須要咱們對用戶名和密碼進行驗證後將結果告訴Shiro. 那麼如何實現自定義驗證呢?

1) 自定義Realm

Shiro對Realm的定義: 一個能夠訪問系統安全相關信息(例如用戶, 角色, 權限等)的組件. 通俗的說, 就是在Realm實現寫查詢用戶, 角色, 權限等系統安全相關的數據的方法.

用戶信息通常會保存在數據庫中, 咱們能夠在Realm中經過登陸頁面傳遞的用戶名去數據庫查詢用戶, 將結果返回給Shiro.

然而Shiro並不知道用戶名和密碼是否正確, 因此提供了Realm組件, 讓咱們在Realm中查詢用戶相關信息並返回, Shiro根據Realm返回結果判斷是否登陸成功.

舉個例子

你在相親的時候要請女生吃飯, 你也不知道每次相親的女生喜歡吃什麼. 但針對每一個菜系都你準備好了相應的餐廳. 聰明的你準備了一個小盒子, 相親時讓女生把想吃的寫好放到盒子裏面, 而後你根據盒子裏面的內容到事先準備好的餐廳去吃飯. 至於女生是用鉛筆寫的, 仍是鋼筆寫的你根本不會關心, 你只關心女生想吃什麼.

上例中的你至關於Shiro, 準備好各類餐廳至關於實現了各類登陸的邏輯, 小盒子就至關於Realm, 女生寫的紙條至關於實現了一個Realm, 紙條上的內容至關於查詢到的用戶信息. 至因而用鉛筆仍是鋼筆寫則至關於用戶信息獲取方式(數據庫,文件或其餘).

Shiro只關心返回的結果, 不會關心Realm查詢用戶信息的實現過程. 下面咱們來實現一個Realm

// 自定義查詢用戶信息的Realm
public class UserRealm extends AuthenticatingRealm {
 
    // 獲取用戶信息的方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 登陸用戶名
        // Shiro會將提交登陸傳入的用戶名和密碼封裝到UsernamePasswordToken中
        String username = ((UsernamePasswordToken) token).getUsername();
 
        // 根據用戶名從數據庫或其餘存儲中查詢用戶信息
        // 模擬數據庫查詢, 返回用戶信息
        User dbUser = getUser(username);
 
        // 用戶不存在,當返回null時Shiro會認爲用信息不存在
        if (dbUser == null) {
            return null;
        }
 
        // 將查詢到用戶信息返回給Shiro
        // 參數1: Shiro會將該參數做爲當前登陸用戶的信息保存,隨時可取
        // 參數2: 當前用戶的密碼,Shiro使用該參數和提交登陸傳遞的密碼進行判斷
        // 參數3: Realm名稱,暫不處理
        return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), "");
    }
 
}
複製代碼
  • 繼承AuthenticatingRealm並實現獲取用戶信息的doGetAuthenticationInfo方法
  • 根據用戶名查詢到用戶信息時返回Shiro須要的AuthenticationInfo對象(內置多種返回對象,稍後介紹)
  • 未查詢到用戶時返回null, 返回結果爲null時Shiro會按照用戶不存在進行處理, 本次登陸失敗
  • 密碼是否爭取判斷不在該方法中進行,Shiro會根據返回結果進行判斷,密碼正確時登陸成功. 錯誤時本次登陸失敗
  • Shiro不關心獲取用戶信息的方式, 不管是數據庫查詢仍是文件查詢,或是第三方接口,只要按照格式返回便可.
  • Shiro會將返回結果第一個參數對象保存,登陸成功後可經過Shiro的方法獲取登陸用戶的相關信息(例獲取登陸用戶ID等)

本例未鏈接數據庫, 模擬代碼:

// 模擬根據用戶名在數據庫查詢用戶信息
private User getUser(String username) {
    // 使用"atd681"做爲登陸密碼才能查到信息
    if (!"atd681".equals(username)) {
        return null;
    }
 
    User dbUser = new User();
    dbUser.setUserId(1L);
    dbUser.setUsername(username);
    dbUser.setPassword("123");
 
    return dbUser;
}
複製代碼
  • 有效登陸用戶名:atd681, 密碼:123, 用戶ID:1
  • 其他用戶名登陸失敗

2) 配置Realm

自定義Realm後須要告知Shiro哪一個Realm是查詢用戶信息的, 即將Realm配置到Shiro中

// 安全管理器
securityManager(DefaultWebSecurityManager) { 
    realm = ref("userRealm") 
}
 
// 定義Realm
userRealm(UserRealm)
複製代碼
  • 在Shiro配置文件中定義Realm
  • 將Realm配置到安全管理器securityManager中

啓動項目, 訪問/page/a, 未登陸時Shiro重定向至登陸頁面. 輸入atd681/123便可登陸成功並跳轉/page/a

3) 配置默認成功頁

當登陸成功後, Shiro會重定向到成功頁面

  • 當訪問其餘頁面(/page/a)跳轉至登陸時, 登陸成功會跳轉至目標頁面(/page/a)
  • 直接訪問登陸頁(無目標頁), 登陸成功後跳轉至默認成功頁

Shiro默認成功頁爲/, 可自定義默認成功頁

// Shiro核心配置
shiroFilter(ShiroFilterFactoryBean) {
    // 默認登陸成功後跳轉的頁面地址
    successUrl = "/index"
 
    // 其餘配置...
}
複製代碼

4) 處理登陸失敗

登陸成功後並無執行到控制器中的處理POST登陸的方法. 輸入atd681之外的帳號或輸入錯誤密碼會致使登陸失敗, 卻會執行控制器中的處理POST登陸的方法. 爲何呢???

Shiro的登陸邏輯:

  • 訪問登陸頁面時, Shiro不處理, 進入控制器
  • 登陸成功後, 直接重定向至成功頁面(不進入控制器)
  • 登陸失敗時, 進入控制器處理, 由控制器決定登陸失敗頁面

登陸失敗時, Shiro用異常表示失敗緣由, 並將失敗緣由保存在Request中, key爲shiroLoginFailure, Shiro登陸邏輯中會拋出以下異常:

  • 用戶不存在: org.apache.shiro.authc.UnknownAccountException
  • 密碼不正確: org.apache.shiro.authc.IncorrectCredentialsException

同時內置了以下異常, 方便用戶自行驗證時拋出:

  • 無效的用戶: org.apache.shiro.authc.DisabledAccountException
  • 鎖定的用戶: org.apache.shiro.authc.LockedAccountException
  • 失敗數過多: org.apache.shiro.authc.ExcessiveAttemptsException
  • 用戶已登陸: org.apache.shiro.authc.ConcurrentAccessException

登陸失敗時能夠根據異常在頁面中顯示相應的錯誤提示信息, 本例登陸失敗時返回登陸頁並顯示錯誤信息

<!-- 有登陸錯誤信息時,根據異常顯示對應的提示信息 -->
<c:if test="${shiroLoginFailure != null}">
	<c:if test="${shiroLoginFailure == 'org.apache.shiro.authc.UnknownAccountException'}">用戶不存在</c:if>
	<c:if test="${shiroLoginFailure == 'org.apache.shiro.authc.IncorrectCredentialsException'}">密碼不正確</c:if>
</c:if>
<!-- 無登陸錯誤時 -->
<c:if test="${shiroLoginFailure == null}">你訪問的頁面須要先進行登陸</c:if>
 
<form action="/login" method="post">
	<input type="text" name="username" placeholder="用戶名" value="" />
	<input type="password" name="password" placeholder="密碼" value="" />
	<input type="submit" value="當即登陸" />
</form>
複製代碼

5) 登出

配置登出URL使用logout過濾器便可. Shiro登出後默認重定向至登陸頁.

// Shiro核心配置
shiroFilter(ShiroFilterFactoryBean) {
    // 配置URL規則
    // 有請求訪問時Shiro會根據此規則找到對應的過濾器處理
    filterChainDefinitionMap = [
        "/page/n" : "anon", // /page/n不須要登陸便可訪問
        "/logout" : "logout", // 登出使用logout過濾器
        "/**": "authc" // 其他全部頁面須要認證(authc爲認證過濾器)
    ]
 
    // 其餘配置 ....   
}
複製代碼

如登出後自定義重定向頁面, 須要在配置文件中手動定義logout過濾器(未定義時Shiro會經過Spring自動加載)

// 手動定義Logout過濾器
// 未定義時Shiro會經過Spring自動加載
logout(LogoutFilter){
    redirectUrl = "/logout_success.jsp"
}
複製代碼

同時, 必須配置logout_success.jsp不須要登陸也能夠訪問(anon), 若是不配置, 登出後進入logout_success.jsp不須要時會被Shiro攔截(此時未登陸)並重定向至登陸(登陸成功後會重定向至logout_success.jsp)

"/logout_success.jsp" : "anon", // 登出成功頁不須要認證
複製代碼

6) 獲取登陸用戶信息

1) 自定義Realm中提到獲取的登陸用戶信息在登陸成功後會被Shiro保存. Shiro提供了能夠獲取登陸用戶信息的方法.

@RequestMapping("/page/a")
public String toPageA(ModelMap map) {
    // Shiro提供的獲取當前登陸用戶信息的靜態方法
    // 用戶信息對象爲在Realm中保存的對象
    User user = (User) SecurityUtils.getSubject().getPrincipal();
    // 獲取用戶ID,用戶名
    map.put("userId", user.getUserId());
    map.put("userName", user.getUsername());
 
    return "/page_a";
}
複製代碼

獲取到的用戶對象必須和在Realm中返回SimpleAuthenticationInfo對象中第一個參數一致

7) 示例代碼

至此, 基於Shiro認證的示例配置完成.

相關文章
相關標籤/搜索