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,进入我们自定义的RealmgetAuthenticationInfo方法里,这又是一个模板方法实现在父类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,获取不到则调用自定义RealmdoGetAuthenticationInfo方法获取认证信息(模板方法模式),传入的仍然是上面的用户登录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中自定义RealmgetAuthenticationInfo方法逻辑返回从数据库获取、封装的SimpleAuthenticationInfo实例。

7.可以看到上面3以下的几步是层层深入到Realm里面的处理逻辑(有父类的有自定义子类的,也有调用的HashedCredentialsMatcher工具类等),现在开始走出Realm的逻辑,回到3的后半段,仍然是AbstractAuthenticator的实现类ModularRealmAuthenticatordoSingleRealmAuthentication方法:

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实例,最终回到抽象类AbstractAuthenticatorauthenticate方法,即我们开头的方法。

最终返回的就是自定义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。这个方法又是模板方法,继而向更外层返回到子类DefaultSecurityManagerlogin方法:

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);
}
}

这里充分说明我们封装的SubjectContextWeb环境上下文,包含了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实例。

向外继续返回到DefaultSecurityManagerlogin方法剩余部分:

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.执行ApplicationFilterChaindoFilter方法,里面是对每个Filter的链式调用:迭代每个Filter,执行其doFilter方法,这个方法的内部逻辑是:还没执行的就执行其内部的doFilterInterval方法,在这个方法里使用FilterChaindoFilter返回FilterChain;执行完的直接执行FilterChain的doFilter返回FilterChain。这样不断回到FilterChain继续迭代Filter(Filter数组游标++并判断是否到头),直到执行完最后一个Filter,其调用的FilterChain的doFilter方法进入else,结束,返回到这个Filter,再返回到调用它的FilterChain的doFilter方法,再返回到上一个Filter...这样反向层层返回到Filter和FilterChain的doFilter方法,直到整个责任链在最外层的ApplicationFilterChaindoFilter方法返回

3.其中责任链走到SpringMVCDelegatingFilterProxy时,这个代理Filter代理了一个名为delegatingFilterProxy的类,这就是在Spring中配置的ShiroFilter,这里在DelegatingFilterProxy的下面方法中:

protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}

走到ShiroAbstractShiroFilter,在其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,下面是走该类的一系列父类:

AdviceFilterdoFilterInternal方法,它是PathMatchingFilter的父类,在这个方法里接着走PathMatchingFilterpreHandle方法(模板方法模式),进行迭代路径匹配,这个路径是我们在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的父类AccessControlFilteronPreHandle方法,它继承了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方法,这个方法是上面AccessControlFilteronPreHandle方法调用的。

5.这样4中顺着onPreHandle的返回层层向上返回,最后又返回到3中,随着executeChain方法的返回,返回到ShiroAbstractShiroFilterdoFilterInternal方法后半段,最后按这个Filter前面的Filter责任链层层向外返回,逻辑结束。

 

待查:重定向跳转问题,Session问题,认证成功的token有效期配置和有效期内免登录逻辑原理。

 

posted @ 2018-10-19 14:58  free_wings  阅读(1426)  评论(0编辑  收藏  举报