前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >shiro实战之常见问题整理

shiro实战之常见问题整理

作者头像
山行AI
发布2019-08-06 14:55:26
1.1K0
发布2019-08-06 14:55:26
举报
文章被收录于专栏:山行AI山行AI山行AI

结合shiro项目来聊一聊常见的问题

配置

  1. AjaxPermissionsAuthorizationFilter:
public class AjaxPermissionsAuthorizationFilter extends FormAuthenticationFilter {

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", ErrorEnum.E_20011.getErrorCode());
        jsonObject.put("msg", ErrorEnum.E_20011.getErrorMsg());
        PrintWriter out = null;
        HttpServletResponse res = (HttpServletResponse) response;
        try {
            res.setCharacterEncoding("UTF-8");
            res.setContentType("application/json");
            out = response.getWriter();
            out.println(jsonObject);
        } catch (Exception e) {
        } finally {
            if (null != out) {
                out.flush();
                out.close();
            }
        }
        return false;
    }

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        return super.onLoginSuccess(token, subject, request, response);
    }



    @Bean
    public FilterRegistrationBean registration(AjaxPermissionsAuthorizationFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }
}
  1. UserRealm:
public class UserRealm extends AuthorizingRealm {
    private Logger logger = LoggerFactory.getLogger(UserRealm.class);

    @Autowired
    private LoginService loginService;

    /**
     * 授权
     * @param principals
     * @return
     */
    @Override
    @SuppressWarnings("unchecked")
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Session session = SecurityUtils.getSubject().getSession();
        //查询用户的权限
        JSONObject permission = (JSONObject) session.getAttribute(Constants.SESSION_USER_PERMISSION);
        logger.info("permission的值为:" + permission);
        logger.info("本用户权限为:" + permission.get("permissionList"));
        //为当前用户设置角色和权限
        //授权
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addStringPermissions((Collection<String>) permission.get("permissionList"));
        return authorizationInfo;
    }

    /**
     * 认证
     *
     * 验证当前登录的Subject
     * LoginController.login()方法中执行Subject.login()时 执行此方法进行认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        String loginName = (String) authcToken.getPrincipal();
        // 获取用户密码
        String password = new String((char[]) authcToken.getCredentials());
        JSONObject user = loginService.getUser(loginName, password);
        if (user == null) {
            //没找到帐号
            throw new UnknownAccountException();
        }
        //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user.getString("username"),
                user.getString("password"),
                //ByteSource.Util.bytes("salt"), salt=username+salt,采用明文访问时,不需要此句
                getName()
        );
        //session中不需要保存密码
        user.remove("password");
        //将用户信息放入session中
        SecurityUtils.getSubject().getSession().setAttribute(Constants.SESSION_USER_INFO, user);
        return authenticationInfo;
    }
}
  1. ShiroConfiguration:
@Configuration
public class ShiroConfiguration {
    /**
     * Shiro的Web过滤器Factory 命名:shiroFilter
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        /*定义shiro过滤链  Map结构
         * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
         * anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
         * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
         /* 过滤链定义,从上向下顺序执行,一般将 / ** 放在最为下边:这是一个坑呢,一不小心代码就不好使了;
          authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 */
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login/auth", "anon");
        filterChainDefinitionMap.put("/login/logout", "anon");
        filterChainDefinitionMap.put("/error", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SubjectFactory subjectFactory(){
        SubjectFactory subjectFactory = new StatelessDefaultSubjectFactory();
        return subjectFactory;
    }

    /**
     * 不指定名字的话,自动创建一个方法名第一个字母小写的bean
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        //securityManager.setSubjectFactory(subjectFactory());
        return securityManager;
    }

    /**
     * Shiro Realm 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的
     */
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        return userRealm;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * 所以我们需要修改下doGetAuthenticationInfo中的代码;
     * )
     * 可以扩展凭证匹配器,实现 输入密码错误次数后锁定等功能,下一次
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //散列的次数,比如散列两次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        //storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * Shiro生命周期处理器
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
}

解析

com.ambition.config.shiro.UserRealm中的两个方法:

  1. 认证,登录时会进入方法为:com.ambition.config.shiro.UserRealm#doGetAuthenticationInfo
Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            currentUser.login(token);
            info.put("result", "success");
        } catch (AuthenticationException e) {
            info.put("result", "fail");
        }

在调用subject.login时会调用UserRealm的doGetAuthenticationInfo方法。

  1. 授权,登录成功后,每次操作时会进入,方法为:com.ambition.config.shiro.UserRealm#doGetAuthorizationInfo
  • 登录成功之后,每次来操作时会先去会进入org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo方法,然后先尝试从缓存中取,如果取不到会进入doGetAuthorizationInfo
  1. 关于缓存:

进入filter的流程:

  1. org.apache.shiro.web.filter.AccessControlFilter#onPreHandle:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
       return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
   }
  1. org.apache.shiro.web.filter.authc.AuthenticatingFilter#isAccessAllowed:
@Override
   protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
       return super.isAccessAllowed(request, response, mappedValue) ||
               (!isLoginRequest(request, response) && isPermissive(mappedValue));
   }
  1. org.apache.shiro.web.filter.authc.AuthenticationFilter#isAccessAllowed:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
       Subject subject = getSubject(request, response);
       return subject.isAuthenticated();
   }

关于org.apache.shiro.web.filter.authc.AuthenticatingFilter#onLoginSuccess的用法见 org.apache.shiro.web.filter.authc.AuthenticatingFilter#executeLogin方法:

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
       AuthenticationToken token = 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);
       }
       try {
           Subject subject = getSubject(request, response);
           subject.login(token);
           return onLoginSuccess(token, subject, request, response);
       } catch (AuthenticationException e) {
           return onLoginFailure(token, e, request, response);
       }
   }

而executeLogin方法被调用的地方在org.apache.shiro.web.filter.authc.FormAuthenticationFilter#onAccessDenied:

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
       if (isLoginRequest(request, response)) {
           if (isLoginSubmission(request, response)) {
               if (log.isTraceEnabled()) {
                   log.trace("Login submission detected.  Attempting to execute login.");
               }
               return executeLogin(request, response);
           } else {
               if (log.isTraceEnabled()) {
                   log.trace("Login page view.");
               }
               //allow them to see the login page ;)
               return true;
           }
       } else {
           if (log.isTraceEnabled()) {
               log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
                       "Authentication url [" + getLoginUrl() + "]");
           }

           saveRequestAndRedirectToLogin(request, response);
           return false;
       }
   }

而onAccessDenied方法在com.ambition.config.shiro.AjaxPermissionsAuthorizationFilter中被重写了:

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("code", ErrorEnum.E_20011.getErrorCode());
    jsonObject.put("msg", ErrorEnum.E_20011.getErrorMsg());
    PrintWriter out = null;
    HttpServletResponse res = (HttpServletResponse) response;
    try {
        res.setCharacterEncoding("UTF-8");
        res.setContentType("application/json");
        out = response.getWriter();
        out.println(jsonObject);
    } catch (Exception e) {
    } finally {
        if (null != out) {
            out.flush();
            out.close();
        }
    }
    return false;
}

这样写的时候onLoginSuccess方法就不会被回调了。

常见问题:

  1. com.ambition.config.shiro.UserRealm#doGetAuthorizationInfo走好几遍,注掉:
//    @Bean
//    @DependsOn({"lifecycleBeanPostProcessor"})
//    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
//        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
//        advisorAutoProxyCreator.setProxyTargetClass(true);
//        return advisorAutoProxyCreator;
//    }

恢复正常。

这个是问题可以看一下之前的推文-spring源码之apo proxy。简单讲来有两点:

  • 在org.springframework.aop.config.AopConfigUtils#registerOrEscalateApcAsRequired中:
@Nullable
    private static BeanDefinition registerOrEscalateApcAsRequired(
            Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

        if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
            BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
            if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
                int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
                int requiredPriority = findPriorityForClass(cls);
                if (currentPriority < requiredPriority) {
                    apcDefinition.setBeanClassName(cls.getName());
                }
            }
            return null;
        }

        RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
        beanDefinition.setSource(source);
        beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
        beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
        return beanDefinition;
    }

会将AnnotationAwareAspectJAutoProxyCreator进行BeanDefinition注册。

  • 第二点是如果没有配置spring.aop.proxy-target-class时会默认置为true。这里简单讲来主要是因为在org.springframework.boot.autoconfigure.aop.AopAutoConfiguration中配置的:
@Configuration
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class",
            havingValue = "true", matchIfMissing = true)
    public static class CglibAutoProxyConfiguration {

    }

这样在org.springframework.aop.config.AopConfigUtils#forceAutoProxyCreatorToUseClassProxying:

public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
        if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
            BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
            definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
        }
    }

    public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
        if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
            BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
            definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
        }
    }

这样就会使用AnnotationAwareAspectJAutoProxyCreator进行bean代理。如果再使用如下代码自定义creator:

//    @Bean
//    @DependsOn({"lifecycleBeanPostProcessor"})
//    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
//        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
//        advisorAutoProxyCreator.setProxyTargetClass(true);
//        return advisorAutoProxyCreator;
//    }

就会有两个proxyCreator,而proxyCreator也是BeanPostProcessor,所以会对bean产生多个代理。

关于这个问题可以看下:https://jinnianshilongnian.iteye.com/blog/1894465

  1. shiro自定义过滤器不执行onLoginSuccess方法 有一种说法是filter中的loginUrl的值与filterChainDefinitions中的authc对应的路径值不同引起的,但根据上面对filter执行流程的分析发现,可能是重写onAccessDenied方法引起的。
  2. 自定义realm时可以重写supports方法来对支持的token做限制:
public boolean supports(AuthenticationToken token) {  
        //仅支持StatelessToken类型的Token
        return token instanceof StatelessToken;  
    }

参考

  1. https://segmentfault.com/a/1190000014479154?utm_source=index-hottest
  2. https://jinnianshilongnian.iteye.com/blog/1894465
  3. https://blog.csdn.net/weixin_44632986/article/details/90516225
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 配置
  • 解析
    • com.ambition.config.shiro.UserRealm中的两个方法:
      • 进入filter的流程:
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档