天天看点

若依非分离版-第六十一章:Spring boot shiro安全框架

作者:源码解析

shiro简介

Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了

Shiro架构

从外部来看Shiro,即从应用程序角度的来观察如何使用Shiro完成工作

Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者

若依非分离版-第六十一章:Spring boot shiro安全框架

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色

Realm:Shiro从Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource

导入shiro依赖

若依非分离版-第六十一章:Spring boot shiro安全框架

shiro配置信息

在application.yml文件,路径如下图

若依非分离版-第六十一章:Spring boot shiro安全框架
# Shiro
shiro:
  user:
    # 登录地址
    loginUrl: /login
    # 权限认证失败地址
    unauthorizedUrl: /unauth
    # 首页地址
    indexUrl: /index
    # 验证码开关
    captchaEnabled: true
    # 验证码类型 math 数字计算 char 字符验证
    captchaType: math
  cookie:
    # 设置Cookie的域名 默认空,即当前访问的域名
    domain:
    # 设置cookie的有效访问路径
    path: /
    # 设置HttpOnly属性
    httpOnly: true
    # 设置Cookie的过期时间,天为单位
    maxAge: 30
    # 设置密钥,务必保持唯一性(生成方式,直接拷贝到main运行即可)Base64.encodeToString(CipherUtils.generateNewKey(128, "AES").getEncoded()) (默认启动生成随机秘钥,随机秘钥会导致之前客户端RememberMe Cookie无效,如设置固定秘钥RememberMe Cookie则有效)
    cipherKey:
  session:
    # Session超时时间,-1代表永不过期(默认30分钟)
    expireTime: 30
    # 同步session到数据库的周期(默认1分钟)
    dbSyncPeriod: 1
    # 相隔多久检查一次session的有效性,默认就是10分钟
    validationInterval: 10
    # 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
    maxSession: -1
    # 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
    kickoutAfter: false
  rememberMe:
    # 是否开启记住我
    enabled: true           

shiro配置代码详解

1. 加入 Ehcache 缓存管理器

CacheManager缓存控制器,来管理如用户,角色,权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

EhCacheManager (com.ruoyi.framework.config.ShiroConfig)

若依框架中缓存配置使用 EhCacheManager ,配置文件为 ehcache-shiro.xml 。

/**
 * 缓存管理器 使用Ehcache实现
 */
@Bean
public EhCacheManager getEhCacheManager()
{
    net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.getCacheManager("ruoyi");
    EhCacheManager em = new EhCacheManager();
    if (StringUtils.isNull(cacheManager))
    {
        em.setCacheManager(new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream()));
        return em;
    }
    else
    {
        em.setCacheManager(cacheManager);
        return em;
    }
}           
/**
 * 返回配置文件流 避免ehcache配置文件一直被占用,无法完全销毁项目重新部署
 */
protected InputStream getCacheManagerConfigFileInputStream()
{
    String configFile = "classpath:ehcache/ehcache-shiro.xml";
    InputStream inputStream = null;
    try
    {
        inputStream = ResourceUtils.getInputStreamForPath(configFile);
        byte[] b = IOUtils.toByteArray(inputStream);
        InputStream in = new ByteArrayInputStream(b);
        return in;
    }
    catch (IOException e)
    {
        throw new ConfigurationException(
                "Unable to obtain input stream for cacheManagerConfigFile [" + configFile + "]", e);
    }
    finally
    {
        IOUtils.closeQuietly(inputStream);
    }
}           

2. 加入自定义Realm

Realm域,Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager要验证用户身份, 那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以有1个或多个Realm,我们一般在应用中都需要实现自己的Realm

若依框架中也实现了自己的 Realm (com.ruoyi.framework.shiro.realm.UserRealm) ,在自定义Realm中主要重写了两个方法 doGetAuthorizationInfo (授权) ,doGetAuthenticationInfo (登录认证) 。

并且将 Realm 加入了缓存管理器中 (com.ruoyi.framework.config.ShiroConfig) 。

/**
 * 自定义Realm
 */
@Bean
public UserRealm userRealm(EhCacheManager cacheManager)
{
    UserRealm userRealm = new UserRealm();
    userRealm.setAuthorizationCacheName(Constants.SYS_AUTH_CACHE);
    userRealm.setCacheManager(cacheManager);
    return userRealm;
}           

其中清理所有用户授权信息缓存的调用时机为 更新菜单或者角色信息,直接删除所有用户的授权信息,点解任意接口的时候会进行授权信息的获取,而这时的授权信息的最新的,无需用户登出再登录操作。

3. 加入 SecurityManager 安全管理器

Subject主体,代表了当前的“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫, 机器人等;即一个抽象概念;所有Subject都绑定到SercurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者

SecurityManage安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject; 可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互

安全管理器 SecurityManager (com.ruoyi.framework.config.ShiroConfig)

/**
 * 安全管理器
 */
@Bean
public SecurityManager securityManager(UserRealm userRealm)
{
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 设置realm.
    securityManager.setRealm(userRealm);
    // 记住我
    securityManager.setRememberMeManager(rememberMe ? rememberMeManager() : null);
    // 注入缓存管理器;
    securityManager.setCacheManager(getEhCacheManager());
    // session管理器
    securityManager.setSessionManager(sessionManager());
    return securityManager;
}           

若依框架中 SecurityManager 主要参数有 Realm、RememberMe、CacheManager、SessionManager 等。

4. SessionManager、SessionDAO、SessionFactory

SessionManager如果写过Servlet就应该知道Session的概念,Session需要有人去管理它的生命周期,这个组件就是SessionManager

SessionDAODAO大家都用过,数据库访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,也可以写入缓存,以提高性能

SessionManager (com.ruoyi.framework.config.ShiroConfig)

/**
 * 会话管理器
 */
@Bean
public OnlineWebSessionManager sessionManager()
{
    OnlineWebSessionManager manager = new OnlineWebSessionManager();
    // 加入缓存管理器
    manager.setCacheManager(getEhCacheManager());
    // 删除过期的session
    manager.setDeleteInvalidSessions(true);
    // 设置全局session超时时间
    manager.setGlobalSessionTimeout(expireTime * 60 * 1000);
    // 去掉 JSESSIONID
    manager.setSessionIdUrlRewritingEnabled(false);
    // 定义要使用的无效的Session定时调度器
    manager.setSessionValidationScheduler(SpringUtils.getBean(SpringSessionValidationScheduler.class));
    // 是否定时检查session
    manager.setSessionValidationSchedulerEnabled(true);
    // 自定义SessionDao
    manager.setSessionDAO(sessionDAO());
    // 自定义sessionFactory
    manager.setSessionFactory(sessionFactory());
    return manager;
}           

SessionFactory (com.ruoyi.framework.config.ShiroConfig)

/**
 * 自定义sessionFactory会话
 */
@Bean
public OnlineSessionFactory sessionFactory()
{
    OnlineSessionFactory sessionFactory = new OnlineSessionFactory();
    return sessionFactory;
}
           

自定义sessionFactory会话 OnlineSessionFactory (com.ruoyi.framework.shiro.session.OnlineSessionFactory)

主要是从请求中获取需要的信息保存到自定义对象 OnlineSession 中。

SessionDAO (com.ruoyi.framework.config.ShiroConfig)

/**
 * 自定义sessionDAO会话
 */
@Bean
public OnlineSessionDAO sessionDAO()
{
    OnlineSessionDAO sessionDAO = new OnlineSessionDAO();
    return sessionDAO;
}
           

OnlineSessionDAO (com.ruoyi.framework.shiro.session.OnlineSessionDAO) 针对自定义的ShiroSession的db操作。

5. 设置 Shiro 过滤器配置

/**
     * Shiro过滤器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
    {
        CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 身份认证失败,则跳转到登录页面的配置
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // 权限认证失败,则跳转到指定页面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        // Shiro连接约束配置,即过滤链的定义
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 对静态资源设置匿名访问
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/ruoyi.png**", "anon");
        filterChainDefinitionMap.put("/html/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/ajax/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/ruoyi/**", "anon");
        filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
        // 退出 logout地址,shiro去清除session
        filterChainDefinitionMap.put("/logout", "logout");
        // 不需要拦截的访问
        filterChainDefinitionMap.put("/login", "anon,captchaValidate");
        // 注册相关
        filterChainDefinitionMap.put("/register", "anon,captchaValidate");
        // 系统权限列表
        // filterChainDefinitionMap.putAll(SpringUtils.getBean(IMenuService.class).selectPermsAll());

        Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
        filters.put("onlineSession", onlineSessionFilter());
        filters.put("syncOnlineSession", syncOnlineSessionFilter());
        filters.put("captchaValidate", captchaValidateFilter());
        filters.put("kickout", kickoutSessionFilter());
        // 注销成功,则跳转到指定页面
        filters.put("logout", logoutFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // 所有请求需要认证
        filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }           

限制同一用户多设备登录

/**
     * 同一个用户多设备登录限制
     */
    public KickoutSessionFilter kickoutSessionFilter()
    {
        KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
        kickoutSessionFilter.setCacheManager(getEhCacheManager());
        kickoutSessionFilter.setSessionManager(sessionManager());
        // 同一个用户最大的会话数,默认-1无限制;比如2的意思是同一个用户允许最多同时两个人登录
        kickoutSessionFilter.setMaxSession(maxSession);
        // 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序
        kickoutSessionFilter.setKickoutAfter(kickoutAfter);
        // 被踢出后重定向到的地址;
        kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
        return kickoutSessionFilter;
    }           

原理:不同设备登录的时候会产生不同的sessionId,但是subject是一样的,通过统计sessionId的个数来判断用户登录了多少个不同设备

若依非分离版-第六十一章:Spring boot shiro安全框架

未来计划

1、ruoyi非分离版本拆解

2、ruoyi-vue-pro:讲解工作流

3、ruoyi-vue-pro:支付模块,电商模块

4、基于ruoyi-vue-pro项目开发

5、JEECG低代码开发平台

请关注我,本星球会持续推出更多的开源项目代码解析,如有更好的意见请留言回复或者私信。

#头条创作挑战赛#