盘点认证框架 : 简单过一下 Shiro

总文档 :文章目录
Github : github.com/black-ant

一 . 前言

之前说了 SpringSecurity , 也说了 Pac4j , 后续准备把 Shiro 和 CAS 也完善进来 , Shiro 整个框架结构比较简单 , 这一篇也只是简单过一下 , 不深入太多.

1.1 基础知识

Shiro 的基础知识推荐看官方文档 Shiro Doc , 这里就简单的罗列一下

Shiro 具有很简单的体系结构 (Subject,SecurityManager 和 Realms) , 按照流程大概就是这样

ApplicationCode -->  Subject (Current User)
				       |
                     SecurityManager (Managers all Subject)
				       |
				   Realms

复制代码

Shiro 的基石

Shiro 自己内部定义了4个功能基石 , 分为身份验证、授权、会话管理和密码学

  • Authentication : 身份认证 , 证明用户身份的行为
  • Authorization : 访问控制的过程,即确定谁可以访问什么
  • Session Management : 管理特定于用户的会话,即使是在非 web 或 EJB 应用程序中
  • Cryptography : 使用加密算法来保证数据的安全,同时仍然易于使用

image.png

image.png

以及一些额外的功能点:

  • Web Support : Shiro 的 Web 支持 api 帮助简单地保护 Web 应用程序
  • Caching : 缓存是 Apache Shiro API 中的第一层,用于确保安全操作保持快速和高效
  • Concurrency : Apache Shiro 支持多线程应用程序及其并发特性
  • Run As : 允许用户假设另一个用户的身份的特性 (我理解这就是代办)
  • Remember Me : 记住我功能

补充隐藏概念:

  • Permission : 许可
  • Role : 角色

二 . 基本使用

Shiro 的使用对我而言第一感觉就是干净 , 你不需要像 SpringSecurity 一样去关注很多配置 ,关注很多Filter , 也不需要像 CAS 源码一样走了很多 WebFlow , 所有的认证都是由你自己去完成的.

2.1 配置类

@Configuration
public class shiroConfig {


    /**
     * 配置 Realm
     *
     * @return
     */
    @Bean
    public CustomRealm myShiroRealm() {
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    }

    /**
     * 权限管理,配置主要是Realm的管理认证
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        // logout url
        map.put("/logout", "logout");
        //对所有用户认证
        map.put("/**", "authc");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 注册 SecurityManager
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * AOP 注解冲突解决方式
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }
} 
复制代码

2.2 发起认证

Shiro 发起认证很简答 , 完全是手动发起

     Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                user.getUserName(),
                user.getPassword()
        );
        try {
            //进行验证,这里可以捕获异常,然后返回对应信息
            subject.login(usernamePasswordToken);
//            subject.checkRole("admin");
//            subject.checkPermissions("query", "add");
        } catch (UnknownAccountException e) {
            log.error("用户名不存在!", e);
            return "用户名不存在!";
        } catch (AuthenticationException e) {
            log.error("账号或密码错误!", e);
            return "账号或密码错误!";
        } catch (AuthorizationException e) {
            log.error("没有权限!", e);
            return "没有权限";
        }

复制代码

因为是完全手动发起的 , 所以在集成 Shiro 的时候毫无压力 , 可以自行在外层封装任何的接口 , 也可以在接口中做任何的事情.

2.3 校验逻辑

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private LoginService loginService;

    /**
     * @MethodName doGetAuthorizationInfo
     * @Description 权限配置类
     * @Param [principalCollection]
     * @Return AuthorizationInfo
     * @Author WangShiLin
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录用户名
        String name = (String) principalCollection.getPrimaryPrincipal();
        //查询用户名称
        User user = loginService.getUserByName(name);
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //添加权限
            for (Permissions permissions : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * @MethodName doGetAuthenticationInfo
     * @Description 认证配置类
     * @Param [authenticationToken]
     * @Return AuthenticationInfo
     * @Author WangShiLin
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        //获取用户信息
        String name = authenticationToken.getPrincipal().toString();
        User user = loginService.getUserByName(name);
        if (user == null) {
            //这里返回后会报出对应异常
            return null;
        } else {
            //这里验证authenticationToken和simpleAuthenticationInfo的信息
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName());
            return simpleAuthenticationInfo;
        }
    }
}

复制代码

LoginServiceImpl 也简单贴一下 , 就是从数据源中获取用户而已

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private PermissionServiceImpl permissionService;

    @Override
    public User getUserByName(String getMapByName) {
        return getMapByName(getMapByName);
    }

    /**
     * 模拟数据库查询
     *
     * @param userName 用户名
     * @return User
     */
    private User getMapByName(String userName) {

        // 构建 Role 1
        Role role = new Role("1", "admin", getAllPermission());
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(role);

        // 构建 Role 2
        Role role1 = new Role("2", "user", getSinglePermission());
        Set<Role> roleSet1 = new HashSet<>();
        roleSet1.add(role1);

        User user = new User("1", "root", "123456", roleSet);
        Map<String, User> map = new HashMap<>();
        map.put(user.getUserName(), user);

        User user1 = new User("2", "zhangsan", "123456", roleSet1);
        map.put(user1.getUserName(), user1);

        return map.get(userName);
    }

    /**
     * 权限类型一
     */
    private Set<Permissions> getAllPermission() {
        Set<Permissions> permissionsSet = new HashSet<>();
        permissionsSet.add(permissionService.getPermsByUserId("1"));
        permissionsSet.add(permissionService.getPermsByUserId("2"));
        return permissionsSet;
    }

    /**
     * 权限类型二
     */
    private Set<Permissions> getSinglePermission() {
        Set<Permissions> permissionsSet1 = new HashSet<>();
        permissionsSet1.add(permissionService.getPermsByUserId("1"));
        return permissionsSet1;
    }

}

复制代码

LoginServiceImpl其实都可以不算是 Shiro 整个认证体系的一员 ,它只是做一个 User 管理的业务而已 , 那么剩下了干了什么?

  • 写了一个 API 接口
  • 准备了一个 Realm
  • 通过 Subject 发起认证
  • 在接口上标注相关的注解

整套流程下来 , 就是简单 , 便捷 , 很轻松的就集成了认证的功能.

三 . 源码

按照惯例 , 还是看一遍源码吧 ,我们按照四个维度来分析 :

  • 请求的拦截
  • 请求的校验
  • 认证的过程
  • 退出的过程

3.1 请求的拦截

这要从 ShiroFilterFactoryBean 这个类开始

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //....
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
    
    
C- ShiroFilterFactoryBean
    ?- 构建 ShiroFilterFactoryBean 时会为其配置一个 login 地址
    M- applyGlobalPropertiesIfNecessary : 配置全局属性
        - applyLoginUrlIfNecessary(filter);
        - applySuccessUrlIfNecessary(filter);
        - applyUnauthorizedUrlIfNecessary(filter);
        
    M- applyLoginUrlIfNecessary
        ?- 为 Filter 配置 loginUrl
        - String existingLoginUrl = acFilter.getLoginUrl();
        - acFilter.setLoginUrl(loginUrl)





// 这里对所有的 地址做了拦截
C01- PathMatchingFilter
    F- protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
        ?- 所有的path均会在这里处理
        - 拦截成功了会调用 isFilterChainContinued , 最终会调用 onAccessDenied -> M2_01
        
C02- FormAuthenticationFilter
    M2_01- onAccessDenied  
        - 
    
// 判断是否需要重定向
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            // 重定向到 login 页
            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }
    
// 当然还有已登录得逻辑 , 已登录是在上面之前判断得
C- AccessControlFilter
    M- onPreHandle
        ?- 该方法中会调用其他得 Filter 判断是否登录

// 例如这里就是 AuthenticationFilter 获取 Subject
C- AuthenticationFilter
    M- isAccessAllowed
         - Subject subject = getSubject(request, response);
         - return subject.isAuthenticated() && subject.getPrincipal() != null;
         

复制代码

整体的调用链大概是

  • OncePerRequestFilter # doFilter
  • AbstractShiroFilter # call
  • AbstractShiroFilter # executeChain
  • ProxiedFilterChain # doFilter
  • AdviceFilter # doFilterInternal
  • PathMatchingFilter # preHandle

最终会因为Filter 链 , 最终由 FormAuthenticationFilter 重定向出去

3.2 拦截的方式


按照我们的常规思路 , 拦截仍然是通过 Filter 来完成
    
    
C- AbstractShiroFilter
    M- doFilterInternal
        - final Subject subject = createSubject(request, response) : 通过 请求构建了一个 Subject
        - 调用 Subject 的 Callable 回调
            - updateSessionLastAccessTime(request, response);
            - executeChain(request, response, chain);
    M- executeChain
        - 执行 FilterChain 判断
        - 
        
// 这里往上追溯 , 可以看到实际上是一个 AOP 操作 : ReflectiveMethodInvocation
// 再往上就是 AopAllianceAnnotationsAuthorizingMethodInterceptor , 注意这里面是懒加载的
        
C03- AnnotationsAuthorizingMethodInterceptor : 通过 Interceptor 对方法进行拦截
	M3_01- assertAuthorized : 断言认证信息
        - 获取一个集合 Collection<AuthorizingAnnotationMethodInterceptor>
       	FOR- 循环 AuthorizingAnnotationMethodInterceptor -> PS301
        	- assertAuthorized  -> M3_05

C- AuthorizingAnnotationMethodInterceptor
	M3_05- assertAuthorized(MethodInvocation mi) 
        - ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi))
        	- 这里是获取 Method 上面的 Annotation , 再调用 assertAuthorized 验证 -> M5_01
        
 // 补充 : PS301 
TODO    

C05- RoleAnnotationHandler
    M5_01- assertAuthorized(Annotation a)
		- 如果不是 RequiresRoles , 则直接返回
		- getSubject().checkRole(roles[0]) -> M6_02
		- getSubject().checkRoles(Arrays.asList(roles));
			?- 注意 , 这里是区别 And 和 Or 将 roles 分别处理

复制代码

请求的逻辑 :

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享