五.shiro,表單攔截器FormAuthenticationFilter
shiro有幾種預設的攔截器,authc,anno,roles,user等 authc就是FormAuthenticationFilter的例項
ShiroFilterFactoryBean的配置:
private Map<String, Filter> filters; <取名,攔截器地址>,可以自定義攔截器放在這
private Map<String, String> filterChainDefinitionMap; <url,攔截器名>哪些路徑會被此攔截器攔截到
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必須設定 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/index"); shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // private Map<String, Filter> filters; shiro有一些預設的攔截器 比如auth,它就是FormAuthenticationFilter表單攔截器 <取名,攔截器地址>,可以自定義攔截器放在這 //private Map<String, String> filterChainDefinitionMap; <url,攔截器名>哪些路徑會被此攔截器攔截到 // 攔截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的連結 順序判斷 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/ajaxLogin", "anon"); filterChainDefinitionMap.put("/focus/userlogin", "anon"); filterChainDefinitionMap.put("/swagger-ui.html#", "anon"); // 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問;user:remember me的可以訪問--> filterChainDefinitionMap.put("/fine", "user"); filterChainDefinitionMap.put("/focus/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); System.out.println("Shiro攔截器工廠類注入成功"); return shiroFilterFactoryBean; }
一。請求被authc攔截,如果狀態未登入,就會被跳到登入頁面,登入成功後,會繼續原請求頁面,除非原請求就是successurl,才去successurl
PathMatchingFilter是開濤講過得,匹配某url 攔截進行處理的攔截器,裡面有匹配url方法,preHandle,onPreHandle等方法。
AccessControlFilter重寫了onPreHandle
pathsMatch:該方法用於path與請求路徑進行匹配的方法;如果匹配返回true;
onPreHandle:在preHandle中,當pathsMatch匹配一個路徑後,會呼叫opPreHandler方法並將路徑繫結引數配置傳給mappedValue;然後可以在這個方法中進行一些驗證(如角色授權),如果驗證失敗可以返回false中斷流程;預設返回true;也就是說子類可以只實現onPreHandle即可,無須實現preHandle。如果沒有path與請求路徑匹配,預設是通過的(即preHandle返回true)
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
isAccessAllowed:請求是否被允許訪問,此方法在AccessControlFilter是抽象方法,被AuthenticatingFilter重寫
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
分別點進去看,呼叫父類AuthenticationFilter的方法判斷 當前使用者是否已認證過
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
並且判斷請求url是不是配置的loginurl
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}
回頭再看 onPrehandle,如果是一個沒認證過的請求,isAccessAlowed肯定是false,執行onAccessDefined方法,開濤也說過,此方法是請求未通過認證時執行的方法,按邏輯推理,請求未認證就跳轉到loginurl在這裡實現
這個方法在authc裡,onAccessDenied(用到這點:子類繼承父類,重寫了A方法,父類有個B方法,子類物件呼叫B方法,執行的是子類的A方法)
如果請求是登入請求,發起登入,如果不是就儲存原請求,並重定向到登入url ,saveRequestAndRedirectToLogin
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
看看saveRequestAndRedirectToLogin
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
redirectToLogin(request, response);
}
saveRequest
protected void saveRequest(ServletRequest request) {
WebUtils.saveRequest(request);
}
webUtils.saveRequest:其實是放到了session裡,key是SAVED_REQUEST_KEY
public static void saveRequest(ServletRequest request) {
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
HttpServletRequest httpRequest = toHttp(request);
SavedRequest savedRequest = new SavedRequest(httpRequest);
session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
}
那登入成功後怎麼跳轉到原請求頁面的?肯定是從session取出原請求,還是看authc的onAcessDefined方法
如果請求是loginurl並允許,就發起登入
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
}
executeLogin:同樣是 subject.login(token)
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
onLoginSuccess:authc重寫此方法
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request, ServletResponse response) throws Exception {
issueSuccessRedirect(request, response);
//we handled the success redirect directly, prevent the chain from continuing:
return false;
}
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}
找到session的的原請求,發起請求 public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
throws IOException {
String successUrl = null;
boolean contextRelative = true;
SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
successUrl = savedRequest.getRequestUrl();
contextRelative = false;
}
if (successUrl == null) {
successUrl = fallbackUrl;
}
if (successUrl == null) {
throw new IllegalStateException("Success URL not available via saved request or via the " +
"successUrlFallback method parameter. One of these must be non-null for " +
"issueSuccessRedirect() to work.");
}
WebUtils.issueRedirect(request, response, successUrl, null, contextRelative);
}
從session取出了那個key 並清空
public static SavedRequest getAndClearSavedRequest(ServletRequest request) {
SavedRequest savedRequest = getSavedRequest(request);
if (savedRequest != null) {
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.removeAttribute(SAVED_REQUEST_KEY);
}
return savedRequest;
}
到這就大致明白,authc 完成 認證通過後 轉發到原請求頁面的流程
二。SecurityUtils.getSubject
前面的AuthenticationFilter或其他過濾器也有不少是直接通過這個方法得到subject,然後通過subject.isAuthenticated 包括check許可權或角色,
就會想到是不是subject也儲存到session裡,登入成功一次後,以後的請求是如何判斷 使用者已認證通過呢?確實是這樣,shiro會在瀏覽器寫下coolie JSESSIONID
通過執行緒副本,
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}