學習了前幾節,大家可能只是對Shiro有個大概的瞭解,其實,Shiro的重點及難點都在後面的博客中,接下來的這節我們來探討一下身份認證.
我們可以一起來看下身份認證流程,有個大概的思緒,在來一起寫代碼進行實現。
身份認證流程:
流程步驟(借鑑英文文檔翻譯):
1.首先調用Subject.login(token)進行登錄,其會自動委託給Security Manager,調用之前必須通過SecurityUtils. setSecurityManager()設置;
2.SecurityManager負責真正的身份驗證邏輯;它會委託給Authenticator進行身份驗證;
3.Authenticator纔是真正的身份驗證者,Shiro API中核心的身份認證入口點,此處可以自定義插入自己的實現;
4.Authenticator可能會委託給相應的AuthenticationStrategy進行多Realm身份驗證,默認ModularRealmAuthenticator會調用AuthenticationStrategy進行多Realm身份驗證;
5.Authenticator會把相應的token傳入Realm,從Realm獲取身份驗證信息,如果沒有返回/拋出異常表示身份驗證失敗了。此處可以配置多個Realm,將按照相應的順序及策略進行訪問。
流程大概就這個樣子,其實可以用更加通俗易懂的語言來描述更好。這些流程看源碼是最清楚的,所以我們可以針對源碼進行走一下,然後理解起來會更加的清楚明瞭。
代碼進行一一解釋
如下:
1.首先進行登錄:
currentUser.login(token);
2.SecurityManager一個大管家。
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
3.Authenticator核心的身份認證入口點。
/**
* Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.
*/
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
4.Authenticator可能會委託給相應的AuthenticationStrategy進行多Realm身份驗證。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {//單個
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {//多個
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
總而言之就是這樣:
1.獲取獲取當前的 Subject. 調用 SecurityUtils.getSubject();
2.判斷當前的用戶是否已經被認證. 即是否已經登錄. 調用 Subject 的 isAuthenticated() 。
3.若沒有認證, 則把用戶名和密碼封裝爲 UsernamePasswordToken 對象。
4. 執行登錄。調用 Subject 的login(token); 方法.
注:token指代是:AuthenticationToken
5.自定義 Realm 的方法, 從數據庫中獲取對應的記錄,並對密碼進行加密,來構建 AuthenticationInfo 對象並返回. 通常使用的實現類爲: SimpleAuthenticationInfo。
即:
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
實例結構:
實例代碼:
LoginController.java:
package com.yiyi.controller;
import javax.servlet.http.HttpServletRequest;
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.RequestMethod;
/**
* 登錄的Controller
* @author hanyiyi
*
*/
@Controller
@RequestMapping("/shiro")
public class LoginController {
/**
* 登錄方法
* @param request
* @return
*/
@RequestMapping(value="/login",method=RequestMethod.POST)
public String login(HttpServletRequest request){
//獲取用戶名和密碼
String username = request.getParameter("username");
String password=request.getParameter("password");
// 使用SecurityUtils.getSubject();來獲取當前的 Subject.
Subject currentUser = SecurityUtils.getSubject();
//判斷當前的用戶是否已經被認證。
if(!currentUser.isAuthenticated()){
//若沒被認證,則把用戶名和密碼封裝爲UsernamePasswordToken對象
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
token.setRememberMe(true);
try {
// 執行登錄.
currentUser.login(token);
}
// ... catch more exceptions here (maybe custom ones specific to your application?
// 所有認證時異常的父類.
catch (AuthenticationException ae) {
//unexpected condition? error?
System.out.println("登錄失敗---->"+ae.getMessage());
}
}
return "redirect:/index.jsp";
}
}
package com.yiyi.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.realm.AuthenticatingRealm;
public class MyRealm extends AuthenticatingRealm{
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//將AuthenticationToken對象轉換成UsernamePasswordToken對象
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//獲取UsernamePasswordToken中的用戶名
String username = upToken.getUsername();
//從數據庫中查詢 username 對應的用戶記錄
System.out.println("從數據庫中查找"+username+"的信息");
//若用戶的信息不存在,則拋出UnknownAccountException異常。
if("unknown".equals(username)){
throw new UnknownAccountException("用戶不存在");
}
//根據用戶的信息進行反饋,則拋出LockedAccountException異常。
if("han".equals(username)){
throw new LockedAccountException("用戶被鎖定");
}
//根據用戶的信息來封裝SimpleAuthenticationInfo對象。
//當前 realm 對象的 name
String realmName = getName();
//認證的實體信息。
Object principal = username;
//密碼
Object credentials="123456";
SimpleAuthenticationInfo info =new SimpleAuthenticationInfo(principal, credentials, realmName);
return info;
}
}
登錄的頁面:
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>登錄頁面</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
</head>
<body>
<h4>login page</h4>
<form action="shiro/login" method="post">
username:<input type="text" name="username"> <br/><br/>
password:<input type="password" name="password"><br/><br/>
<input type="submit" value="提交">
</form>
</body>
</html>
攔截器配置;
先讓shiro/login進行匿名訪問:
圖形化界面;
現在我們來執行下這個操作:
首先進入登錄頁面:
http://localhost:8080/Shiro-03/login.jsp
根據代碼,我們先輸入一個正確的用戶名和密碼:zhao 123456 這時候會進入到對應的頁面,假如我們在一次進行入到登錄頁面,隨意輸入個錯誤的密碼,這時候還是可以登錄上的,這個是爲什麼呢?
原因是:shiro的緩存起到了作用,這時候避免出現這樣的現象我們直接在applicationContext.xml中配置一個logou的攔截器即可。
index.jsp:
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>My JSP 'index.jsp' starting page</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
</head>
<body>
index jsp
<a href="shiro/logout">登出</a>
</body>
</html>
applicationContext.xml:
這其中還有很多重點需要一一解釋:
①:爲什麼自定義的Realm 爲什麼直接繼承AuthenticatingRealm呢?
②:SimpleAuthenticationInfo 這個對象中的參數都指代什麼呢?
③:shiro的密碼怎麼進行比對呢,怎麼進行加密呢?
這些都到下一節進行詳細的描述吧。
Ps:新手自學,哪裏不對還望指出,謝謝。