Shiro源码分析④ :鉴权流程

一、前言

由于之前没有使用过 Shiro,最近开始使用,故对其部分流程和源码进行了阅读,大体总结了一些内容记录下来。本系列并不会完完全全分析 Shiro 的全部代码,仅把主(我)要(用)流(到)程(的) 简单分析一下。由于本系列大部分为个人内容理解 并且 个人学艺实属不精,故难免出现 “冤假错乱”。如有发现,感谢指正,不胜感激。


Shiro 源码分析全集:

  1. Shiro源码分析① :简单项目搭建
  2. Shiro源码分析② :AbstractShiroFilter
  3. Shiro源码分析③ :认证流程
  4. Shiro源码分析④ :鉴权流程

1. Filter的对应关系

首先我们需要知道,不同的过滤器名称对应什么过滤器,如下图
在这里插入图片描述

Shiro 默认的过滤器映射关系:

public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
}

如下图 :在这里,我们修饰的 /logout会使用名字为 logout的过滤器,即 LogoutFilter
同理 /shiro/login 会使用名字为 anon 的过滤器,即 AnonymousFilter。
在这里插入图片描述


文章到这,说明我们已经登录认证成功,这里开始访问 http://localhost:8081/shiro/admin。但是Shiro 是如何确定当前会话已经通过登录认证的呢?这就是本文需要讲解的内容。

二、鉴权流程

Shiro 的 Filter 有很多,我们这里举其中两个 Filter : FormAuthenticationFilter 和 RolesAuthorizationFilter 来做分析。
如下图:

  • /shiro/user 接口需要用户具有 admin 和 user 角色才可以访问,此时会使用 RolesAuthorizationFilter 过滤当前请求
  • /shiro/admin 接口只要登录验证通过就可以访问。此时会使用 FormAuthenticationFilter 过滤当前请求。
    在这里插入图片描述

1. FormAuthenticationFilter

FormAuthenticationFilter 的结构如下:
在这里插入图片描述
我们这里来看一看 FormAuthenticationFilter 继承链路,由于 FormAuthenticationFilterOncePerRequestFilter 子类,所以我们直接从 OncePerRequestFilter#doFilter 方法看起。
当请求到达时通过下面的调用链路我们到达了 AccessControlFilter#onPreHandle

=OncePerRequestFilter#doFilter 
=AdviceFilter#doFilterInternal 
=PathMatchingFilter#preHandle
=AccessControlFilter#onPreHandle

AccessControlFilter#onPreHandle 的代码如下:

	// AccessControlFilter#onPreHandle
    public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }

这个方法中的两个方法调用,决定了我们这个请求是否可以通过鉴权认证。

  • isAccessAllowed(request, response, mappedValue) : 判断请求是否可以通过,在这里面完成了鉴权操作。
  • onAccessDenied(request, response, mappedValue); :这里巧妙的用了 || 的执行顺序。当请求无法通过时,会调用该方法。当该方法返回true时,请求仍会通过。不过该方法被 FormAuthenticationFilter 重写了,进行了错误处理。

1.1 isAccessAllowed(request, response, mappedValue)

isAccessAllowed(request, response, mappedValue) 实际调用的是 AuthenticatingFilter#isAccessAllowed,其详细代码如下:

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    	// super.isAccessAllowed 返回true || (不是登录请求 && 宽容通过)
        return super.isAccessAllowed(request, response, mappedValue) ||
                (!isLoginRequest(request, response) && isPermissive(mappedValue));
    }
1.1.1 super.isAccessAllowed(request, response, mappedValue)

其中 super.isAccessAllowed(request, response, mappedValue) 调用的是 AuthenticationFilter#isAccessAllowed 方法,具体实现如下:

    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    	// 获取当前线程的Subject
        Subject subject = getSubject(request, response);
        // 这个状态是缓存在Session中的,现在被解析出来赋值给 Subject,所以如果通过登录验证则是true
        return subject.isAuthenticated();
    }
1.1.2 (!isLoginRequest(request, response) && isPermissive(mappedValue))

!isLoginRequest(request, response) 这里就不再解释,就是确定当前请求不是登录请求。
我们主要来看 isPermissive(mappedValue),其代码如下 :

	// org.apache.shiro.web.filter.authc.AuthenticatingFilter#isPermissive 中实现
	public static final String PERMISSIVE = "permissive";
 
    protected boolean isPermissive(Object mappedValue) {
        if(mappedValue != null) {
            String[] values = (String[]) mappedValue;
            return Arrays.binarySearch(values, PERMISSIVE) >= 0;
        }
        return false;
    }

可以看到,其逻辑就是判断mappedValue 是否包含 permissive ,如果包含则放行。

在 ShiroFilterFactoryBean 的配置中,我们可以通过下面的方式,来对某些请求进行一个宽容放行,这部分请求可以在不登录的情况下访问,此时mappedValue 就是 authc[]中的数组的值。
在这里插入图片描述

1.2.3 总结

总结起来,接口是否鉴权通过,有两种情况都可通过

  1. subject.isAuthenticated() 为true。这个是登录后会保存在Session 中的状态,其他请求发送过来会读取Session中的状态并写入到Subject中,从而实现了这个验证。
  2. 非登录请求 && 宽容放行,这个需要我们自己手动去配置,不再多说。

1.2 onAccessDenied(request, response, mappedValue);

当我们在 isAccessAllowed(request, response, mappedValue) 中校验没有通过时,便会调用 onAccessDenied 方法,而 FormAuthenticationFilter 重写了该方法,如下:

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    	// 判断是否是登录请求
        if (isLoginRequest(request, response)) {
        	// 判断请求为post请求
            if (isLoginSubmission(request, response)) {
            	// 重新执行登录方法
                return executeLogin(request, response);
            } else {
               // 否则返回true
                return true;
            }
        } else {
         	// 否则重定向到登录
            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }

	...
 	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 boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        if (log.isDebugEnabled()) {
            log.debug( "Authentication exception", e );
        }
        setFailureAttribute(request, e);
        //login failed, let request continue back to the login page:
        // 登录失败,返回到登录页面
        return true;
    }

这里我们来看看 executeLogin(request, response); 的实现如下:

	// org.apache.shiro.web.filter.authc.AuthenticatingFilter#executeLogin
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    	// 调用的是  FormAuthenticationFilter#createToken
    	// 会根据 request 中的username 和password,以及rememberMe、host 等信息封装成一个 UsernamePasswordToken
        AuthenticationToken token = createToken(request, response);
        if (token == null) {
          // ... 抛出异常
        }
        try {
        	// 重新执行登录逻辑
            Subject subject = getSubject(request, response);
            subject.login(token);
            // 登录成功执行
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
        	// 登录失败执行
            return onLoginFailure(token, e, request, response);
        }
    }

可以看到,onAccessDenied 方法完全用来处理鉴权失败的情况了,这里会将鉴权失败的请求重定向到登录页。

2. RolesAuthorizationFilter

RolesAuthorizationFilter 的调用链路和 FormAuthenticationFilter 相同,这里不再赘述,不同的是RolesAuthorizationFilter 重写了 isAccessAllowed 方法来完成对用户角色的验证。其实现如下:

public class RolesAuthorizationFilter extends AuthorizationFilter {

    //TODO - complete JavaDoc

    @SuppressWarnings({"unchecked"})
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
		// 获取当前 Subject
        Subject subject = getSubject(request, response);
        // 获取指定的角色列表,我们这里配的是 admin 和 user
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            //no roles specified, so nothing to check - allow access.
            return true;
        }

        Set<String> roles = CollectionUtils.asSet(rolesArray);
        // 判断当前用户是否包含所有的角色,这里会调用 Realm#doGetAuthorizationInfo 方法来获取用户所拥有角色和权限进行验证
        return subject.hasAllRoles(roles);
    }
}

3. 总结

简单画了一个时序图,如下:
请添加图片描述

整理一下整个流程:

  1. 当一个请求发送过来是,被 SpringShiroFilter 转发给合适的 Filter。如果是 /shiro/user 则交由 RolesAuthorizationFilter 处理,如果是 /shiro/admin 则交由 FormAuthenticationFilter 处理
  2. 如果转发给了 FormAuthenticationFilter ,则FormAuthenticationFilter 中的判断请求是否通过鉴权,通过鉴权有两种情况 : subject.isAuthenticated() 为true非登录请求 && 宽容放行
  3. 如果鉴权失败则交由 onAccessDenied 方法来处理,FormAuthenticationFilter 由于重写了 onAccessDenied 方法,将会将请求重定向到登录页。
  4. 如果转发给 RolesAuthorizationFilter,RolesAuthorizationFilter 重写了 isAccessAllowed 方法,会调用该方法判断是否允许访问当前请求,在这个方法中会对用户当前所用于角色做校验,满足则允许访问。

三、权限注解的实现

在Shiro 中,我们可以通过 @RequiresRoles@RequiresPermissions 等注解来进行一个更细致的角色权限的校验。具体的注解如下图。

在这里插入图片描述

在上面的代码分析中,我们并没有分析这些注解功能是如何实现的,下面就借由 @RequiresRoles 注解来进行分析。

这里需要对Spring Aop 有一定程度的源码了解,如果不了解,建议先阅读完 Spring源码分析十一:@Aspect方式的AOP上篇 - @EnableAspectJAutoProxy


看到这种注解,就猜测是Aop 是实现。在Shiro配置类中也发现了猫腻,配置了DefaultAdvisorAutoProxyCreator 和 AuthorizationAttributeSourceAdvisor 两个,


    /**
     * 这里指定了动态代理的方式使用了 Cglib
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启注解支持,包括 RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class。
     * 和Aop相同的逻辑,通过注入 Advisor 来增强一些类的和方法
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

这里尤其是 AuthorizationAttributeSourceAdvisor ,这货是Advisor 子类,必然是用来增强类的。
Advisor 简单解释:Advisor 中包含 PointCut 和 Advice 。其中 PointCut 代表切点,代表要增强的点,Advice 中编写了具体的增强实现。Spring在启动时会通过自动代理创建器去扫描所有的Advisor 实现类,并在加载每个Bean的时候判断Advisor 是否适用于当前Bean,如果适用,则会通过Advice 来创建该Bean的增强代理。

1. AuthorizationAttributeSourceAdvisor

Shiro 借助 AuthorizationAttributeSourceAdvisor 实现了权限注解的功能。
下面我们来AuthorizationAttributeSourceAdvisor 的部分内容,如下:

public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {

    private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);
	// 这里声明了需要增强的注解,被这些注解修饰的类或方法会被增强代理
    private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
            new Class[] {
                    RequiresPermissions.class, RequiresRoles.class,
                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
            };
    /**
     * Create a new AuthorizationAttributeSourceAdvisor.
     */
    public AuthorizationAttributeSourceAdvisor() {
    	// 设置增强点,即具体的增强策略实现在 AopAllianceAnnotationsAuthorizingMethodInterceptor 中
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
    }
    ...
	// 校验是否可以代理当前类或者方法。返回true,则说明 AuthorizationAttributeSourceAdvisor  可以增强代理该类
    public boolean matches(Method method, Class targetClass) {
        Method m = method;
		// 如果方法 被 AUTHZ_ANNOTATION_CLASSES  中的注解修饰则返回true
        if ( isAuthzAnnotationPresent(m) ) {
            return true;
        }

        //The 'method' parameter could be from an interface that doesn't have the annotation.
        //Check to see if the implementation has it.
        if ( targetClass != null) {
            try {
            	// 获取实现类的方法,method可能是接口的方法,而实现类的方法可能被注解修饰
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                // 校验实现类或者实现类的方法是否被注解修饰
                return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);
            } catch (NoSuchMethodException ignored) {
            
            }
        }

        return false;
    }
}

这里面我们可以简单理解,如果是被 AUTHZ_ANNOTATION_CLASSES 修饰的方法 或者类,就是需要增强的点,其增强具体实现在 AopAllianceAnnotationsAuthorizingMethodInterceptor 中。下面我们进入 AopAllianceAnnotationsAuthorizingMethodInterceptor 中一探究竟。

1.1. AopAllianceAnnotationsAuthorizingMethodInterceptor

public class AopAllianceAnnotationsAuthorizingMethodInterceptor
        extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor {

    public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
        List<AuthorizingAnnotationMethodInterceptor> interceptors =
                new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);

        //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
        //raw JDK resolution process.
        AnnotationResolver resolver = new SpringAnnotationResolver();
        //we can re-use the same resolver instance - it does not retain state:
        // 添加不同注解的拦截器,
        // 针对 @RequiresRoles
        interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
        // 针对 @RequiresPermissions
        interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
        // 针对 @RequiresAuthentication
        interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
        // 针对 @RequiresUser
        interceptors.add(new UserAnnotationMethodInterceptor(resolver));
        // 针对 @RequiresGuest
        interceptors.add(new GuestAnnotationMethodInterceptor(resolver));

        setMethodInterceptors(interceptors);
    }
	...
}

这里可以看到针对不同的注解,Shiro 使用了不同的 过滤器来进行操作,这里我们以 @RequiresRoles 注解为例,对 RoleAnnotationMethodInterceptor 进行简单的分析

1.1. RoleAnnotationMethodInterceptor

RoleAnnotationMethodInterceptor 本身并没有什么实现。其全部实现交给了 RoleAnnotationHandler。
在这里插入图片描述

我们这里需要找到 invoke 方法。invoke 方法用来调用真实的被代理的方法,所有的增强实现也基于此。 RoleAnnotationMethodInterceptor 继承了 AuthorizingAnnotationMethodInterceptor,invoke方法的实现在 AuthorizingAnnotationMethodInterceptor#invoke 中,如下

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
    	// 校验是否有权限
        assertAuthorized(methodInvocation);
        // 调用方法
        return methodInvocation.proceed();
    }

	...
	// 校验是否有足够的权限
   public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
        try {
        	// 调用Handler 的 assertAuthorized 方法来进行校验。这里调用的自然就是 RoleAnnotationHandler
            ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
        }
        catch(AuthorizationException ae) {
			// ... 抛出异常
        }         
    }

上面的代码中,调用了Handler也进行校验,这里调用的handler 即为 RoleAnnotationHandler。下面我们来看RoleAnnotationHandler#assertAuthorized 方法实现如下:

    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (!(a instanceof RequiresRoles)) return;
		// 获取 @RequiresRoles  注解上标注的角色
        RequiresRoles rrAnnotation = (RequiresRoles) a;
        String[] roles = rrAnnotation.value();
		// 进行角色校验
        if (roles.length == 1) {
            getSubject().checkRole(roles[0]);
            return;
        }
        // 针对 Logical.AND 和 Logical.OR 的校验
        if (Logical.AND.equals(rrAnnotation.logical())) {
            getSubject().checkRoles(Arrays.asList(roles));
            return;
        }
        if (Logical.OR.equals(rrAnnotation.logical())) {
            // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
            boolean hasAtLeastOneRole = false;
            for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;
            // Cause the exception if none of the role match, note that the exception message will be a bit misleading
            if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);
        }
    }

这里我们看到,校验的实现在 getSubject().checkRole 中。由于调用链路太长,这里就不再追踪, getSubject().checkRole 方法最终会调用 CustomRealm#doGetAuthorizationInfo,并根据doGetAuthorizationInfo 方法的返回值来进行角色的校验。具体调用链路如下:
在这里插入图片描述

四、总结

至此,我们可以总结出Shiro 的整个执行流程如下:

  1. 用户登录,通过AbstractShiroFilter 。AbstractShiroFilter 创建一个Subject绑定当前请求线程,并将请求分发给合适的过滤器处理。(这里由于是登录请求,我们选择放行,anno对应的过滤器是 AnonymousFilter,会直接放行 ):
    在这里插入图片描述

  2. 我们会在登录请求中调用 Subject subject = SecurityUtils.getSubject(); 获取到的 Subject就是上一步AnonymousFilter 中绑定到当前线程的Subject。
    在这里插入图片描述

  3. 随后执行 subject.login(usernamePasswordToken);。会通过我们自定义的 Realm (CustomRealm) 进行认证和鉴权的操作(鉴权操作 doGetAuthorizationInfo 方法并非一定执行,需要权限时才会执行),认证成功后,将会创建一个Session来保存认证后的结果信息,同时将SessionId写入到客户端Cookies 中。

  4. 当我们进行其他请求时,此时请求会携带Cookies 过来,首先还是会经过 AbstractShiroFilter ,在 AbstractShiroFilter 中Shiro 解析Cookies 中的SessionId,从而获取到Session,再将Session中保存的认证结果信息解析保存到Subject中,再将Subject绑定到当前线程。由于在Session中保存了这次会话已经通过验证的信息,所以 FormAuthenticationFilter(这里用FormAuthenticationFilter 来举例) 会直接通过认证。

  5. 此时我们的其他请求已经通过了AbstractShiroFilter。便可以开始处理请求,请求处理结束后,会将请求后的信息和原先的信息在Session 中进行合并,简单来说就是新的信息覆盖旧的缓存。


以上:内容部分参考
https://www.cnblogs.com/xxbiao/p/11485851.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Shiro分布式鉴权是指在分布式系统中使用Shiro进行身份验证和授权的过程。对于这个问题,可以参考引用和引用中提供的相关文档和示例代码来实现Shiro分布式鉴权的功能。首先,可以查看jsets-shiro-spring-boot-starter项目详情以了解如何在Spring Boot项目中集成Shiro。并且可以参考jsets-shiro-demo示例源码来了解如何在具体项目中应用Shiro进行鉴权。另外,可以查看使用说明来了解jsets-shiro-spring-boot-starter的具体使用方法。引用中提到了一种防止重放攻击的方法,通过将token的ID放入缓存(如redis、memcached)进行阅后即焚或销毁缓存来确保token只能使用一次。这是一种常见的防止重放攻击的解决方案。通过这些参考资料,你可以更好地理解和实践Shiro分布式鉴权的方案。123 #### 引用[.reference_title] - *1* *2* [shiro jwt 构建无状态分布式鉴权体系](https://blog.csdn.net/wj596/article/details/84914572)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *3* [分布式鉴权之JWT整合shiro](https://blog.csdn.net/weixin_43769053/article/details/107643606)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值