SpringBoot+Shiro

Shiro 是一款简单易用、功能强大的安全框架,帮助我们安全高效的构建企业级应用。

Shiro 能做什么

  • 认证:用户登录的认证
  • 权限:基于角色和权限的访问(url权限),以及颗粒化权限控制(按钮权限)
  • 加密技术:Shiro的crypto包中包含了一系列的易于理解和使用发加密、哈希(aka摘要)辅助类
  • session管理:可在web容器以及EJB容器中使用session,可扩展(例如我们可以通过重写sessionDao将session存储到数据库中)
  • RememberMe:基于cookie的记住我的服务

Shiro 常用组件介绍

image.png
  • Subject:Subject其实代表的就是当前正在操作的用户,只不过因为“User”一般指代人,但是一个“Subject”可以是人,也可以是任何的第三方系统,服务账号等任何其他正在和当前系统交互的第三方软件系统。
    所有的Subject实例都被绑定到一个SecurityManager,如果你和一个Subject交互,所有的交互动作都会别转换成Subject与SecurityManager的交互。
  • SecurityManager: Shiro的核心,他主要用于协调Shiro内部各种安全组件,不过我们一般不用太关心SecurityManager,对于应用程序开发者来说,主要还是使用Subject的API来处理各种安全验证逻辑。
  • Reaim:这是用于连接Shiro和客户系统的用户数据的桥梁。一旦Shiro真正需要访问各种安全相关的数据(比如使用用户账号来做用户身份验证以及权限验证)时,他总是通过调用系统配置的各种Realm来读取数据。
  • 关于Shiro的其余核心组件参Shiro 官网或Shiro的架构等文章。

Shiro 是如何工作的

简单来说,在Spring项目中

  1. Shiro会将他的所有组件注册到SecurityManager中。
  2. 再通过SecurityManager注册到ShiroFilterFactoryBean(这个类实现了Spring的BeanPostProcessor会预先加载)中。
  3. 最后以filter的形式注册到Spring容器(实现了Spring的FactoryBean,构造一个filter注册到Spring容器中),实现用户权限的管理。

Shiro 如何集成

  • shiro所需依赖


    org.apache.shiro
    shiro-core
    1.4.0


    org.apache.shiro
    shiro-spring
    1.4.0


    org.apache.shiro
    shiro-ehcache
    1.4.0



    com.github.theborakompanioni
    thymeleaf-extras-shiro
    2.0.0

  • ShiroConfig
@Configuration
public class ShiroConfig {

    private static final Logger log = LoggerFactory.getLogger(ShiroConfig.class);

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map chainDefinition = new LinkedHashMap<>();
        // 静态资源与登录请求不拦截
        chainDefinition.put("/js/**", "anon");
        chainDefinition.put("/css/**", "anon");
        chainDefinition.put("/img/**", "anon");
        chainDefinition.put("/layui/**", "anon");
        chainDefinition.put("/login", "anon");
        chainDefinition.put("/login.html", "anon");
        // 用户为授权通过认证 && 包含'admin'角色
        chainDefinition.put("/admin/**", "authc, roles[super_admin]");
        // 用户为授权通过认证或者RememberMe && 包含'document:read'权限
        chainDefinition.put("/docs/**", "user, perms[document:read]");
        // 用户访问所有请求 授权通过 || RememberMe
        chainDefinition.put("/**", "user");

        shiroFilter.setFilterChainDefinitionMap(chainDefinition);
        // 当 用户身份失效时重定向到 loginUrl
        shiroFilter.setLoginUrl("/login.html");
        // 用户登录后默认重定向请求
        shiroFilter.setSuccessUrl("/index.html");
        return shiroFilter;
    }

    @Bean
    public Realm realm() {
        ShiroRealm realm = new ShiroRealm();
        realm.setCredentialsMatcher(credentialsMatcher());
        realm.setCacheManager(ehCacheManager());
        return realm;
    }

    @Bean
    public CacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }

    @Bean
    public CredentialsMatcher credentialsMatcher() {
        AuthCredentialsMatcher credentialsMatcher = new AuthCredentialsMatcher(ehCacheManager());
        credentialsMatcher.setHashAlgorithmName(AuthCredentialsMatcher.HASH_ALGORITHM_NAME);
        credentialsMatcher.setHashIterations(AuthCredentialsMatcher.HASH_ITERATIONS);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        log.debug("--------------shiro已经加载----------------");
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setCacheManager(ehCacheManager());
        manager.setRealm(realm());
        manager.setRememberMeManager(rememberMeManager());
        return manager;
    }

    @Bean
    public RememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    @Bean
    public SimpleCookie rememberMeCookie(){
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * Shiro生命周期处理器:
     * 用于在实现了Initializable接口的Shiro bean初始化时调用Initializable接口回调(例如:UserRealm)
     * 在实现了Destroyable接口的Shiro bean销毁时调用 Destroyable接口回调(例如:DefaultSecurityManager)
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 启用shrio授权注解拦截方式,AOP式方法级权限检查
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * thymeleaf的shiro扩展
     *
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

}

以上基本是Spring项目集成Shiro的通用配置,下面针对上述的几个Bean聊一聊
1. ShiroFilterFactoryBean:用于定义请求的拦截规则,Shiro为我们默认提供了一些项,常用如下

  • anon : 请求不拦截
  • authc:要求用户必须认证通过
  • user:要求用户为记住我状态
  • roles[xxx]:要求用户必须满足xxx角色
  • perms[xxx]:要求用户必须满足xxx权限
    其实上述每一个都对应了一个Shiro过滤器
    image.png
  1. Realm:上面提到过Realm是用于连接Shiro和客户系统的用户数据的桥梁,我们通过实现AuthorizingRealm来提供用户认证和授权两个API
public class ShiroRealm extends AuthorizingRealm {

    private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);

    @Autowired
    @Lazy // 这里lazy 是有必要的, shiro组件会预先加载,导致依赖的bean 没有生成代理对象(AOP失效)
    private UserService userService;

    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executing doGetAuthenticationInfo", username));
        }

        User user = userService.getUserByUsername(username);

        if (user == null) {
            throw new UnknownAccountException();
        }

        if (Constant.IS_LOCK.equals(user.getIsLock())) {
            throw new LockedAccountException();
        }

        // ShiroUser 作为实际的 principal
        ShiroUser shiroUser = new ShiroUser();
        BeanUtils.copyProperties(user, shiroUser);

        // SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
        // principal 会被封装到 subject 中
        // shiro 默认会把我们的 credentials (也就是password) 和 token 中的作对比,所以我们可以不用做密码校验
        ByteSource salt = ByteSource.Util.bytes(user.getUsername());
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(shiroUser, user.getPassword(), salt, getName());

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executed doGetAuthenticationInfo", username));
        }

        return info;
    }

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        ShiroUser shiroUser = (ShiroUser) principalCollection.getPrimaryPrincipal();

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executing doGetAuthorizationInfo", shiroUser.getUsername()));
        }

        AuthorizationDTO authorizationDTO = userService.getRolesAndPermissions(shiroUser.getId());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(authorizationDTO.getRoleCodeSet());
        info.addStringPermissions(authorizationDTO.getPermissionCodeSet());

        if (log.isDebugEnabled()) {
            log.debug(String.format("user:%s executed doGetAuthorizationInfo", shiroUser.getUsername()));
        }

        return info;
    }

}
  • doGetAuthenticationInfo:认证方法,在执行subject.login(token);后,Shiro认证器会读取Realm中的该方法获取AuthenticationInfo对象(认证信息),包含principal(我们存储在shiro subject中的对象),credentials(密码)。
  • doGetAuthorizationInfo:授权方法,在需要校验用户访问权限的时候,Shiro授权器会读取Realm中的该方法获取AuthorizationInfo对象(授权信息)读取DB后,可以通过addRoles(roleCollection)和addStringPermissions(permCollection)设置当前用户的角色和权限。Shiro在拿到这个权限信息后,会去找缓存管理器。以当前subject的principal作为key缓存起来。
    3. CredentialsMatcher:密码匹配器,用于匹配doGetAuthenticationInfo方法返回的credentials和subject.login(token),时的token中的password是否一致。常用的实现有SimpleCredentialsMatcher(默认是该实现)、HashedCredentialsMatcher(该实现可以进行加密匹配)
    4. DefaultWebSecurityManger:如上述,用于协调Shiro内部各种安全组件,我们需要将我们扩展的bean注册到SecurityManager中
    5. RememberMeManager:开启该组件后使用记住服务,token中rememberMe为true时,登录成功之后会创建RememberMe cookie。

关于thymeleaf-extras-shiro

Shiro默认支持在jsp中使用shiro标签。但是想在thymeleaf中使用Shiro标签呢?
使用thymeleaf-extras-shiro 完美解决 thymeleaf颗粒化权限控制

你好, 

当前角色超级管理员

满足所有权限时显示
满足一个权限即可显示

你可能感兴趣的:(SpringBoot+Shiro)