Shiro源码分析
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有效期配置和有效期内免登录逻辑原理。