1.入口類:AbstractAuthenticator
用戶輸入的登錄信息經過其authenticate方法:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
} else {
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = this.doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable var8) {
AuthenticationException ae = null;
if (var8 instanceof AuthenticationException) {
ae = (AuthenticationException)var8;
}
if (ae == null) {
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " + "error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, var8);
if (log.isWarnEnabled()) {
log.warn(msg, var8);
}
}
try {
this.notifyFailure(token, ae);
} catch (Throwable var7) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. Please check your AuthenticationListener implementation(s). Logging sending exception and propagating original AuthenticationException instead...";
log.warn(msg, var7);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
this.notifySuccess(token, info);
return info;
}
}
其中的token包含用戶輸入的登錄信息,如果是用戶名/密碼登錄,這里是UsernamePasswordToken,其屬性:username(這里測試賬號是admin)、password(這里是111111)、rememberMe(記住我,前台勾選,默認false)、host
2.在上面的方法里,進入內層doAuthenticate方法(傳入上面的用戶登錄token),這個方法是子類ModularRealmAuthenticator實現的方法(1中是模板設計模式,抽象父類聲明,子類實現):
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
這里是根據認證類型選擇單Realm還是多Realm認證,進入不同的方法。這里進入前者,即單Realm認證。
無論那種認證,這里最終返回一個AuthenticationInfo實例,為 類型
3.在上面的方法里,進入doSingleRealmAuthentication方法,傳入了Realm和上面的用戶登錄token,其中Realm是Realm鏈中的一個,是我們自定義的一個Realm:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
} else {
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
} else {
return info;
}
}
}
從這里開始,我們要進入自定義Realm的邏輯。這里,我們自定義的Realm完整如下:
/** * Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng) * <p> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.stylefeng.guns.core.shiro; import cn.stylefeng.guns.core.shiro.service.UserAuthService; import cn.stylefeng.guns.core.shiro.service.impl.UserAuthServiceServiceImpl; import cn.stylefeng.guns.modular.system.model.User; import cn.stylefeng.roses.core.util.ToolUtil; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.HashSet; import java.util.List; import java.util.Set; public class ShiroDbRealm extends AuthorizingRealm { /** * 登錄認證 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { UserAuthService shiroFactory = UserAuthServiceServiceImpl.me(); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; User user = shiroFactory.user(token.getUsername()); ShiroUser shiroUser = shiroFactory.shiroUser(user); return shiroFactory.info(shiroUser, user, super.getName()); } /** * 權限認證 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { UserAuthService shiroFactory = UserAuthServiceServiceImpl.me(); ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal(); List<Integer> roleList = shiroUser.getRoleList(); Set<String> permissionSet = new HashSet<>(); Set<String> roleNameSet = new HashSet<>(); for (Integer roleId : roleList) { List<String> permissions = shiroFactory.findPermissionsByRoleId(roleId); if (permissions != null) { for (String permission : permissions) { if (ToolUtil.isNotEmpty(permission)) { permissionSet.add(permission); } } } String roleName = shiroFactory.findRoleNameByRoleId(roleId); roleNameSet.add(roleName); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(permissionSet); info.addRoles(roleNameSet); return info; } /** * 設置認證加密方式 */ @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher(); md5CredentialsMatcher.setHashAlgorithmName(ShiroKit.hashAlgorithmName); md5CredentialsMatcher.setHashIterations(ShiroKit.hashIterations); super.setCredentialsMatcher(md5CredentialsMatcher); } }
4.在3中的方法里,繼續傳入token,進入我們自定義的Realm的getAuthenticationInfo方法里,這又是一個模板方法,實現在父類AuthenticatingRealm中,AuthenticatingRealm是AuthorizingRealm的父類,而我們的自定義Realm繼承AuthorizingRealm:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = this.getCachedAuthenticationInfo(token);//這是從緩存獲取,這里為null
if (info == null) {
info = this.doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
this.cacheAuthenticationInfoIfPossible(token, info);//這是放入緩存
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
這里先從緩存獲取認證信息,這里為null,獲取不到則調用自定義Realm的doGetAuthenticationInfo方法獲取認證信息(模板方法模式),傳入的仍然是上面的用戶登錄token,這樣又進入到了我們的自定義Realm方法實現中:
/** * 登錄認證 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { UserAuthService shiroFactory = UserAuthServiceServiceImpl.me(); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; User user = shiroFactory.user(token.getUsername()); ShiroUser shiroUser = shiroFactory.shiroUser(user); return shiroFactory.info(shiroUser, user, super.getName()); }
我們的Realm獲取認證信息的方式:
a.從Spring容器獲取我們自定義實現的業務類,該類注入了數據庫用戶操作的Mapper(繼承了MyBatis-Plus的BaseMapper接口)
b.使用自定義業務類的數據庫Mapper,根據用戶登錄token中的用戶名到數據庫獲取數據庫對應的用戶信息
c.根據用戶表roleId字段,關聯查詢用戶對應角色(集合)信息
d.調用了業務類自定義的如下方法:
@Override public SimpleAuthenticationInfo info(ShiroUser shiroUser, User user, String realmName) { String credentials = user.getPassword(); // 密碼加鹽處理 String source = user.getSalt();//數據庫存儲原始鹽值 ByteSource credentialsSalt = new Md5Hash(source);//轉換后使用的哈希鹽值 return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName); }
這里使用了鹽值加密,獲取了該用戶數據庫存儲的原始鹽值,調用Shiro框架的Md5Hash方法獲取了哈希鹽值。
使用數據庫用戶信息(Object類型,這里是自定義ShiroUser)、數據庫存儲的哈希后的密碼、哈希鹽值、realmName(自定義Realm全路徑名加上一個並發登錄的原子整形自增值,這里是自定義Realm直接調用的super.getName())生成了一個Shiro框架要求的一個SimpleAuthenticationInfo對象返回
5.自定義Realm執行結束,向外回到4的后面部分繼續執行,調用了下面方法:
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = this.getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
傳入的是用戶登錄token和我們上面使用自定義Realm獲取數據庫用戶信息,並進行封裝的SimpleAuthenticationInfo實例(接口為AuthenticationInfo )
注意這里獲取到的CredentialsMatcher是我們上面自定義的Realm中的第三個@Override方法setCredentialsMatcher設置進去的,是可以自定義設置的,設置了哈希算法(這里為MD5)和哈希迭代次數(這里為1024),這里為HashedCredentialsMatcher實例。
6.上面方法中的if判斷里,又進入下面方法:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenHashedCredentials, accountCredentials);
}
實現類是HashedCredentialsMatcher,這里面的三個方法:
hashProvidedCredentials:最終通過
new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
獲取一個SimpleHash(接口是Hash)實例。
其中參數分別為哈希算法(這里是MD5)、登錄token中用戶輸入的原始密碼(字符數組格式,這里是[1,1,1,1,1,1])、哈希鹽值、哈希迭代次數(這里設置成了1024),這幾個參數都是自定義可配。這里最終獲取的是用戶輸入密碼通過哈希算法、哈希鹽值生成的哈希密碼信息。
getCredentials:最終獲取的是用戶數據庫哈希密碼信息。
equals:比較上述兩個信息
最終返回到doCredentialsMatch,又返回到5中的assertCredentialsMatch方法。如果驗證失敗,返回IncorrectCredentialsException異常,提示(密碼憑證不匹配)信息,否則驗證成功,接着4中自定義Realm的getAuthenticationInfo方法邏輯返回從數據庫獲取、封裝的SimpleAuthenticationInfo實例。
7.可以看到上面3以下的幾步是層層深入到Realm里面的處理邏輯(有父類的有自定義子類的,也有調用的HashedCredentialsMatcher工具類等),現在開始走出Realm的邏輯,回到3的后半段,仍然是AbstractAuthenticator的實現類ModularRealmAuthenticator的doSingleRealmAuthentication方法:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
} else {
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
} else {
return info;
}
}
}
返回自定義Realm返回的SimpleAuthenticationInfo實例,繼而到外面的doAuthenticate方法:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
繼續返回自定義Realm返回的SimpleAuthenticationInfo實例,最終回到抽象類AbstractAuthenticator的authenticate方法,即我們開頭的方法。
最終返回的就是自定義Realm返回的AuthenticationInfo的實現類SimpleAuthenticationInfo的實例,實例包括用戶自定義ShiroUser信息(principals,可有多個自定義用戶信息實體)、哈希密碼(credentials)、哈希鹽值(credentialsSalt)信息。
8.最終返回到的是外層的類AuthenticatingSecurityManager,繼承了RealmSecurityManager,最終實現的是SecurityManager.這里是調用了AuthenticatingSecurityManager的下面方法:
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
返回的就是上面幾步獲取認證的AuthenticationInfo。這個方法又是模板方法,繼而向更外層返回到子類DefaultSecurityManager的login方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = this.authenticate(token);
} catch (AuthenticationException var7) {
AuthenticationException ae = var7;
try {
this.onFailedLogin(token, ae, subject);
} catch (Exception var6) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6);
}
}
throw var7;
}
Subject loggedIn = this.createSubject(token, info, subject);
this.onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
這里傳入的參數Subject里面有request信息,token仍然是用戶輸入登錄信息(這里是用戶名、密碼),上面的所有過程並沒有Subject參與,而是token.最終登錄成功后,是使用token,登錄成功返回的AuthenticationInfo,和上面這個包含request信息的Subject,封裝了另外一個Subject:
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
SubjectContext context = this.createSubjectContext();
context.setAuthenticated(true);
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return this.createSubject(context);
}
這個方法使用SubjectContext封裝認證成功信息,再把帶request的Subject設置其中,最后使用這個context調用下面方法:
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = this.copy(subjectContext);
context = this.ensureSecurityManager(context);
context = this.resolveSession(context);
context = this.resolvePrincipals(context);
Subject subject = this.doCreateSubject(context);
this.save(subject);
return subject;
}
這里的前三個方法最終都是獲取DefaultSubjectContext(它是的SubjectContext子類)對應的SecurityManager,Session,Principals組件進行確認
最后的doCreateSubject方法最終調用到類DefaultWebSubjectFactory(父類是DefaultSubjectFactory,實現了SubjectFactory接口)的下面方法:
public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
} else {
WebSubjectContext wsc = (WebSubjectContext)context;
SecurityManager securityManager = wsc.resolveSecurityManager();
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated();
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager);
}
}
這里充分說明我們封裝的SubjectContext是Web環境上下文,包含了request信息和認證成功后的信息、Session等信息。這里重新拿到這些信息,最終用這些信息生成了一個WebDelegatingSubject實例進行返回,它是一個代理類,最終實現了Subject接口。
回到上面的doCreateSubject方法,進而回到createSubject方法,返回的就是這個WebDelegatingSubject實例:
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = this.copy(subjectContext);
context = this.ensureSecurityManager(context);
context = this.resolveSession(context);
context = this.resolvePrincipals(context);
Subject subject = this.doCreateSubject(context);
this.save(subject);
return subject;
}
繼續走save方法,這里將包含已認證用戶信息的Subject放入Session中。
向外繼續回到
createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing)
方法,返回的是這個WebDelegatingSubject實例。
向外繼續返回到DefaultSecurityManager的login方法剩余部分:
Subject loggedIn = this.createSubject(token, info, subject);
this.onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
onSuccessfulLogin方法是處理rememberMe相關信息。
最終返回到:
9.DelegatingSubject(實現了Subject接口)的login方法:
public void login(AuthenticationToken token) throws AuthenticationException {
this.clearRunAsIdentitiesInternal();
Subject subject = this.securityManager.login(this, token);
String host = null;
PrincipalCollection principals;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject)subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals != null && !principals.isEmpty()) {
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 = this.decorate(session);
} else {
this.session = null;
}
} else {
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);
}
}
看來SecurityManager的login方法是這里調用的,返回了認證成功的WebDelegatingSubject實例后,這里繼續向下執行就是DelegatingSubject實例的一些簡單的賦值邏輯了,也就是把上面返回的認證成功的WebDelegatingSubject實例信息給到這個DelegatingSubject。
10.之后這個DelegatingSubject實例返回給我們自定義的@Controller類。原來我們是在自定義的@Controller類中調用了Shiro的
SecurityUtils.getSubject();
來獲取的9中DelegatingSubject的Web實例WebDelegatingSubject,執行了上面的login操作。
此后我們自定義的@Controller類的邏輯就是使用返回的WebDelegatingSubject中的用戶認證信息設置Session,最終重定向到主頁。
登錄Controller完整的登錄方法如下:
/** * 點擊登錄執行的動作 */ @RequestMapping(value = "/login", method = RequestMethod.POST) public String loginVali() { String username = super.getPara("username").trim(); String password = super.getPara("password").trim(); String remember = super.getPara("remember"); //驗證驗證碼是否正確 if (KaptchaUtil.getKaptchaOnOff()) { String kaptcha = super.getPara("kaptcha").trim(); String code = (String) super.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY); if (ToolUtil.isEmpty(kaptcha) || !kaptcha.equalsIgnoreCase(code)) { throw new InvalidKaptchaException(); } } Subject currentUser = ShiroKit.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password.toCharArray()); if ("on".equals(remember)) { token.setRememberMe(true); } else { token.setRememberMe(false); } currentUser.login(token); ShiroUser shiroUser = ShiroKit.getUser(); super.getSession().setAttribute("shiroUser", shiroUser); super.getSession().setAttribute("username", shiroUser.getAccount()); LogManager.me().executeLog(LogTaskFactory.loginLog(shiroUser.getId(), getIp())); ShiroKit.getSession().setAttribute("sessionFlag", true); return REDIRECT + "/"; }
可以看到我們自己使用用戶傳入的登錄信息封裝了UsernamePasswordToken,並使用它來進行了Shiro的登錄流程。
進一步研究提示:
1.SecurityUtils.getSubject()原理(和配置的Shiro怎樣整合、獲取Web環境及request有關)。
2.Session配置和獲取相關,分布式Session問題。
3.rememberMe原理和配置。
補充:Shiro集成到Web(Tomcat、SpringMVC、Spring、Spring Boot)
1.請求不同的Servlet,其過濾器鏈可能不同(每個過濾器配置了過濾規則),所以每次Web容器使用createFilterChain方法為每個Servlet調用創建FilterChain(Java原生,這里實例為ApplicationFilterChain),包含該Servlet每個配置的Filter(Java原生),形成責任鏈調用(這里集成SpringMVC,所以Servlet類型為DispatcherServlet,即SpringMVC的前端控制器),即從一個FilterMap里匹配出一個和當前請求url對應的Filter集合加入到創建的FilterChain當中
2.執行ApplicationFilterChain的doFilter方法,里面是對每個Filter的鏈式調用:迭代每個Filter,執行其doFilter方法,這個方法的內部邏輯是:還沒執行的就執行其內部的doFilterInterval方法,在這個方法里使用FilterChain的doFilter返回FilterChain;執行完的直接執行FilterChain的doFilter返回FilterChain。這樣不斷回到FilterChain繼續迭代Filter(Filter數組游標++並判斷是否到頭),直到執行完最后一個Filter,其調用的FilterChain的doFilter方法進入else,結束,返回到這個Filter,再返回到調用它的FilterChain的doFilter方法,再返回到上一個Filter...這樣反向層層返回到Filter和FilterChain的doFilter方法,直到整個責任鏈在最外層的ApplicationFilterChain的doFilter方法返回
3.其中責任鏈走到SpringMVC的DelegatingFilterProxy時,這個代理Filter代理了一個名為delegatingFilterProxy的類,這就是在Spring中配置的Shiro的Filter,這里在DelegatingFilterProxy的下面方法中:
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
走到Shiro的AbstractShiroFilter,在其doFilterInternal方法里,執行:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
Subject subject = this.createSubject(request, response);
subject.execute(new Callable() {
public Object call() throws Exception {
AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
AbstractShiroFilter.this.executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException var8) {
t = var8.getCause();
} catch (Throwable var9) {
t = var9;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException)t;
} else if (t instanceof IOException) {
throw (IOException)t;
} else {
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
}
這里的request類型變換為ShiroHttpServletRequest,調用的createSubject方法如下:
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return (new Builder(this.getSecurityManager(), request, response)).buildWebSubject();
}
這里獲取到配置的SecurityManager配置了一個WebDelegatingSubject返回,這個類原理流程可以參照上文的源碼分析
4.接着executeChain方法內開始走Shiro內部配置的Filter(實際是又創建了一個ProxiedFilterChain進行內部轉發,最后回到Web容器創建的ApplicationFilterChain),這里走我們配置的自定義的OAuth2Filter,該類繼承了AuthenticatingFilter,下面是走該類的一系列父類:
AdviceFilter的doFilterInternal方法,它是PathMatchingFilter的父類,在這個方法里接着走PathMatchingFilter的preHandle方法(模板方法模式),進行迭代路徑匹配,這個路徑是我們在Shiro中自定義配置的攔截路徑,比如配置為"anon"的路徑不需要認證就可訪問,其他均需走oauth2,也就是下面我們以此名字配置的子類OAuth2Filter,配置如下:
@Bean("shiroFilter")//2.用來初始化DelegatingFilterProxy來向Web容器注冊Shiro的Filter,攔截所有請求
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);//設置了SecurityManager
//oauth過濾
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());//3.Shiro級過濾器,攔截帶token的請求,獲取請求token,封裝到OAuth2Token(實現了AuthenticationToken),繼續傳遞給Realm處理接口
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");//以上匿名訪問
filterMap.put("/**", "oauth2");//4.所有其他,轉到oauth2認證,名字和上面配置的oauth2的filter名字匹配
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
這里如果不是anon管理的路徑,則在preHandle方法里面,繼續走我們的OAuth2Filter的父類:走AccessControlFilter的onPreHandle方法,它繼承了PathMatchingFilter:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return this.isAccessAllowed(request, response, mappedValue) || this.onAccessDenied(request, response, mappedValue);
}
這個方法里走的兩個方法,則是我們OAuth2Filter中覆蓋的兩個方法,執行前一個返回true則不再執行后一個,否則執行后一個,我們定義的OAuth2Filter完整如下:
package io.renren.modules.sys.oauth2; import com.google.gson.Gson; import io.renren.common.utils.HttpContextUtils; import io.renren.common.utils.R; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpStatus; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * oauth2過濾器 * * @author chenshun * @email sunlightcs@gmail.com * @date 2017-05-20 13:00 */ public class OAuth2Filter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { //獲取請求token String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isBlank(token)){ return null; } return new OAuth2Token(token); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){ return true; } return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //獲取請求token,如果token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if(StringUtils.isBlank(token)){ HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token")); httpResponse.getWriter().print(json); return false; } return executeLogin(request, response); } @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); try { //處理登錄失敗的異常 Throwable throwable = e.getCause() == null ? e : e.getCause(); R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage()); String json = new Gson().toJson(r); httpResponse.getWriter().print(json); } catch (IOException e1) { } return false; } /** * 獲取請求的token */ private String getRequestToken(HttpServletRequest httpRequest){ //從header中獲取token String token = httpRequest.getHeader("token"); //如果header中不存在token,則從參數中獲取token if(StringUtils.isBlank(token)){ token = httpRequest.getParameter("token"); } return token; } }
這里如果沒登錄過,都是走后一個方法,即onAccessDenied方法,這里是走我們自定義的onAccessDenied方法,該方法最后調用了executeLogin方法,表明任何未認證請求都被導向登錄邏輯,該方法是父類AuthenticatingFilter的方法:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.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);
} else {
try {
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(token, var5, request, response);
}
}
}
這個方法里走的就是Shiro的登錄邏輯,使用的是我們自定義的Shiro配置,其中Realm這里走的是我們自定義的OAuth2Realm,這個邏輯最上面已經分析過。
這樣最后走出executeLogin方法,再走出我們的onAccessDenied方法,這個方法是上面AccessControlFilter的onPreHandle方法調用的。
5.這樣4中順着onPreHandle的返回層層向上返回,最后又返回到3中,隨着executeChain方法的返回,返回到Shiro的AbstractShiroFilter的doFilterInternal方法后半段,最后按這個Filter前面的Filter責任鏈層層向外返回,邏輯結束。
待查:重定向跳轉問題,Session問題,認證成功的token有效期配置和有效期內免登錄邏輯原理。
