这篇文章主要是对工程中使用到的基本shiro的功能和配置进行一下总结,对源码的探索还是只停留在一些基本功能的实现调用链上,留一个坑,有时间把shiro的整体架构从源码级别深入了解一下。

shiro是轻量级的安全框架,但是提供的安全功能也比较有限,主要是

身份认证(Authentication

授权管理(Authorization

会话管理(Session Manager

加密功能(Cryptography

本项目主要用到的是身份认证和授权管理,会话管理,至于加密,本次没有做,但是会总结一下如何实现。使用shiro需要进行基础的配置,配置的内容包括:

  1. CacheManager用于缓存的管理,需要在配置文件中配置好缓存(这里是ehcache-shiro.xml)
  2. LifecycleBeanPostProcessor,正如其名字,是用于管理bean的声明周期
  3. DefaultAdvisorAutoProxyCreator,当ApplicationContext读如所有的Bean配置信息后,这个类将扫描上下文,寻找所有的Advistor(一个Advisor是一个切入点和一个通知的组成),将这些Advisor应用到所有符合切入点的Bean中,实现AOP。
  4. 装配一个自定义的realm,注入EhCacheManger,来管理缓存。Shiro 从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法
  5. 配置securityManger,使用DefaultWebSecurityManager,这里要设置好Realm和CacheManger
  6. 配置AuthorizationAttributeSourceAdvisor,权限管理的AOP支持,需要注入securityManger
  7. 身份认证最核心的部分是ShiroFilterFactoryBean,这里注入securityManger,并且设置好登录URL和认证成功跳转的URL,最后还需要配置一个访问控制链,使用LinkedHashMap,访问控制以键值对的形式表示(eg:put(“/css/**”,”anon”) 键表示URL,值表示什么身份可以访问),需要考虑到顺序,(最普遍的配置在最后)。
  8. 密码Hash的配置,通过配置一个HashedCredentialsMatcher来配置密码Hash,同时需要将Realm进行修改,userRealm,userRealm.setCredentialsMatcher(hashedCredentialsMatcher())
@Configuration
public class ShiroConfig{
    @Bean
    public EhCacheManager getEhCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
        return em;
    }
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }

    @Bean(name = "userRealm")
    public UserRealm userRealm(EhCacheManager cacheManager) {
        UserRealm userRealm = new UserRealm();
        userRealm.setCacheManager(cacheManager);
        return userRealm;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(UserRealm userRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(userRealm);
        defaultWebSecurityManager.setCacheManager(getEhCacheManager());
        return defaultWebSecurityManager;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/home");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 需要验证的写 authc 不需要的写 anon
        filterChainDefinitionMap.put("/css/**","anon");
        filterChainDefinitionMap.put("/js/**","anon");
        filterChainDefinitionMap.put("/img/**","anon");
        filterChainDefinitionMap.put("/druid/**","anon");
        filterChainDefinitionMap.put("/defaultKaptcha","anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/home","anon");
        filterChainDefinitionMap.put("/register", "anon");
        filterChainDefinitionMap.put("/file/**","anon");
        filterChainDefinitionMap.put("/admin/**","authc,roles[admin]");
        filterChainDefinitionMap.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }
}

再来看看自定义的Realm,继承了AuthorizingRealm类,这里主要需要重写两个方法,一个是doGetAuthenticationInfo,一个是doGetAuthorizationInfo,第一个方法是用来获取登录认证所需要的信息(获取用户名,密码等),第二个方法是用来获取权限控制所需要的信息(获取角色)

  1. 先来看看第一个方法的逻辑,返回值是一个AuthenticationInfo,这里的AuthenticationInfo主要记录的是SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());,这个principal是什么呢?这里的principal是从token中获取的,可以推断出得到的值应该是Username。
  2. 第二个方法,返回值是一个AuthorizationInfo,这的Info主要是配置好权限信息,也就是所说的角色info.setRoles(roles).
    public class UserRealm extends AuthorizingRealm {
    @Autowired
    private Usermapper usermapper;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        String principal = (String) token.getPrincipal();
        //使用Optional来解决nullException的问题,这里的usermapper.getUserByUsername
        User user = Optional.ofNullable(usermapper.getUserByUsername(principal)).orElseThrow(UnknownAccountException::new);
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
        Session session = SecurityUtils.getSubject().getSession();
        session.setAttribute("USER_SESSION", user);
        return authenticationInfo;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        Session session = SecurityUtils.getSubject().getSession();
        User user = (User) session.getAttribute("USER_SESSION");
        // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 用户的角色集合
        Set<String> roles = new HashSet<>();
        String[] roleList={"admin","normal"};
        roles.add(roleList[Integer.parseInt(user.getRole())-1]);
        info.setRoles(roles);
        return info;
    }

}

了解了配置和自己编写的Realm之后,我其实挺奇怪一点,这个身份验证的时候的调用链是怎么样的呢?登录逻辑核心部分就三行代码,新建了一个Subject,把传入的username和password当成一个token,用这个token 登录

 Subject sub = SecurityUtils.getSubject();
 UsernamePasswordToken token = new UsernamePasswordToken(username, password);
 sub.login(token);

我在sub.login(token)处打了一个断点,跟进去看一下。这里使用SecurityUtils.getSubject()得到的Subject是默认实现DelegatingSubject,来看看他的login方法,Subject subject = securityManager.login(this, token);,调用了securityManager的login方法,我们跟进看一下,由于这里我们在配置文件中配置了securityManger的bean,是一个DefaultWebSecurityManager,他继承了DefaultSecurityManager,这个login方法就是继承的。我们去看一下

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            throw ae; //propagate
        }
        Subject loggedIn = createSubject(token, info, subject);
        onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

这里的token是我们从用户请求登录时前端传回来的用户名和密码,调用authenticate方法进行验证,这个方法应该就是验证身份是否匹配。我们继续跟进这个方法,在AbstractAuthenticator中核心语句:info = doAuthenticate(token);,跟进doAuthenticate,默认应该是调用实现类ModularRealmAuthenticator的doAuthenticate方法。

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

这个类中有一个realms实例,包括我们自定义的realm,然后根据realm的个数调用对应的方法,这里我们的是单独的realm,所以调用doSingleRealmAuthentication,将realm和token传入,而在doSingleRealmAuthentication中又调用了getAuthenticationInfo,

AuthenticationInfo info = realm.getAuthenticationInfo(token);我们再次跟进,这里调用方法的具体实现是AuthenticatingRealm类,可以看出逻辑:

先从缓存中找,找到就直接返回,没有找到才调用doGetAuthenticationInfo(token),调用之后得到一个info,然后调用assertCredentialsMatch(token, info);来判断是否登录成功

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached, perform the lookup:
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        if (info != null) {
            assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }
        return info;
    }

总结

跟了这么多,我们可以简单认为,认证过程是根据用户post传入的token,取出其中的username,然后去doGetAuthenticationInfo,获得一个info对象,记录了user信息,然后比较user和传入的token中的对应的值。

这里也可以看出一点,当我们给shiro配置缓存的时候,在登录的时候缓存的其实是AuthenticationInfo这个信息。