springboot + shiro 权限管理

本文介绍了一个基于SpringBoot 2.3.4和Shiro 1.6.0的权限管理系统,详细阐述了Shiro的核心组件,包括Subject、SecurityManager、SessionManager、Authentication和Authorization。流程涉及TokenFilter的自动登录逻辑,以及ShiroRealm中自定义的鉴权和认证方法。此外,还展示了ShiroConfig的配置,包括过滤链的设置和刷新。整个系统实现了用户权限的管理和刷新,确保了安全性。

springboot shiro 权限管理

版本 :

  • springboot 2.3.4-RELEASE
  • shiro 1.6.0

注: 此版本基于 ssm 与 shiro 下的权限管理_XGLLHZ的博客-优快云博客 文章(有些小问题,目前未修改),关联阅读便于理解。

核心组件 :

  • Subject 一般指用户
  • SecurityManager 安全管理器,管理所有 subject
  • SessionManager 会话管理器,管理用户登录后的 session
  • Authentication 认证
  • Authorization 鉴权
  • CacheManager 缓存管理器,管理缓存(缓存可自定义)

流程图 :
在这里插入图片描述

TokenFilter :

// token filter(请求先进入此拦截器)
public class TokenFilter extends AbstractFilter {
    
    private static final Logger logger = LogManager.getLogger(TokenFilter.class);
    private final SYSUserService userService;
    public TokenFilter(SYSUserService userService) {
        this.userService = userService;
    }

    /**
     * 此拦截器工作流程:
     *      1、判断是否携带 token
     *      2、判断 token 是否过期
     *      3、判断用户登录是否过期(即 session 中是否存在用户信息)
     *      4、在 token 未过期且用户登录过期的情况刷新用户 session(系统内部自动登录)
     *
     * 此处做自动登录的目的:
     *      在 spring-shiro 中,用户登录信息是放在 session 中的,
     *      而 session 默认有效时间为半小时,也就是说默认情况下,
     *      用户每隔半小时就得登录一次,针对这个问题有两种解决方法:
     *          1、session 有效时长设置为无限长(缺点是当用户数量过大时会严重占用系统内存)
     *          2、利用 token 机制结合 session(有效时长可设两小时),当 token 有效 session
     *          失效时,刷新 session 中的用户信息,当 token 过期时提示用户重新登录
     *          
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws IOException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        logger.info("enter TokenFilter.doFilter() params = {}", servletRequest.toString());
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setContentType("application/json; charset=utf-8");
        String token = request.getHeader("token");

        // 若 token 为空则说明该请求为非法请求或不需要权限的请求
        if (StringUtils.isNotBlank(token)) {

            // 若 token 过期则返回响应体
            if (TokenUtil.checkToken(token) == 1) {
                ObjectMapper mapper = new ObjectMapper();
                PrintWriter writer = response.getWriter();
                writer.write(mapper.writeValueAsString(new APIResponse<>(ConstConfig.RE_LOGIN_EXPIRE_CODE,
                        ConstConfig.RE_LOGIN_EXPIRE_MSG)));
                writer.flush();
                writer.close();
                return;
            }

            // 若 token 未过期则判断内存中是否有此用户身份验证信息
            Object object = null;
            try {
                object = SecurityUtils.getSubject().getPrincipal();
            } catch (Exception e) {
                logger.error("The userInfo is overdue in session!");
            }

            // 若没有此用户身份验证信息则根据 token 获取用户信息并调用 shiro 中的 subject.login()
            // 类似于系统内部自动登录(刷新 session 中的用户信息)
            if (object == null) {

                String username = userService.getUsernameByToken(token);
                if (StringUtils.isBlank(username)) {
                    ObjectMapper mapper = new ObjectMapper();
                    PrintWriter writer = response.getWriter();
                    writer.write(mapper.writeValueAsString(new APIResponse<>(ConstConfig.RE_CHECK_TOKEN_ERROR_CODE,
                            ConstConfig.RE_CHECK_TOKEN_ERROR_MSG)));
                    writer.flush();
                    writer.close();
                    return;
                }

                SYSUserPo userPo = userService.getUserByUsername(username);
                if (userPo == null) {
                    ObjectMapper mapper = new ObjectMapper();
                    PrintWriter writer = response.getWriter();
                    writer.write(mapper.writeValueAsString(new APIResponse<>(ConstConfig.SERVER_EXCEPTION_CODE,
                            "User data does not exist")));
                    writer.flush();
                    writer.close();
                    return;
                }

                // 调用 shiro 中的登录方法
                UsernamePasswordToken usernamePasswordToken =
                        new UsernamePasswordToken(userPo.getUsername(), userPo.getPassword());
                Subject subject = SecurityUtils.getSubject();

                subject.login(usernamePasswordToken);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

ShiroRealm :

// 重写 shiro 父类中鉴权和认证的方法,以实现具体逻辑
public class ShiroRealm extends AuthorizingRealm {
    
    private static final Logger logger = LogManager.getLogger(ShiroRealm.class);
    private final SYSUserService userService;
    public ShiroRealm(SYSUserService userService) {
        this.userService = userService;
    }

    /**
     * 鉴权
     * 获取权限校验需要的信息(当前登录用户所具有的权限信息:权限、角色)
     * 调用时间: 1、默认情况下是每次请求资源(url)时都会调用;
     *          2、但可以将用户权限(资源)信息存放到缓存中,存放方式是在 subject.login()
     *              之后调用 subject.isPermitted("test")方法,在这个方法内部会调用下面的
     *              方法来获取用户权限(资源)信息,同时需要在 shiro 配置文件中配置相应的缓存管理器;
     *          3、放入缓存之后只在每次登录时调用;
     * 检验方式: shiro 的权限校验方式有两种,即基于权限(资源)、基于角色
     * 此项目采用基于权限(资源)的方式
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("enter ShiroRealm.doGetAuthorizationInfo() authority");

        // 获取登录成功的用户名
        // 这里会从 session 中获取(登录执行 subject.login 方法时会将用户信息放入 session)
        String username = (String) principalCollection.fromRealm(getName()).iterator().next();

        // 根据用户名获取该用户所具有的权限列表
        SYSUserPo sysUserPo = new SYSUserPo();
        sysUserPo.setUsername(username);
        List<SYSPermPo> permList = userService.listPermByUser(sysUserPo);
        List<String> perms = null;

        if (permList != null && permList.size() != 0) {
            perms = permList.stream().map(SYSPermPo::getPermUrl).collect(Collectors.toList());
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermissions(perms);

        return info;
    }

    /**
     * 认证
     * 获取身份验证需要的信息(数据库中的用户数据)
     * 调用时间: 登录时调用,即请求 /admin/user/login时(service 中 的 subject.login())
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        logger.info("enter ShiroRealm.doGetAuthenticationInfo() authenticate");

        // 用户登录提交的信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        if (StringUtils.isBlank(username)) {
            throw new GlobalException(ConstConfig.RE_NAME_OR_PASSWORD_ERROR_CODE,
                    ConstConfig.RE_NAME_OR_PASSWORD_ERROR_MSG);
        }

        // 根据用户名查询到的信息
        SYSUserPo userPo = userService.getUserByUsername(username);
        if (userPo == null) {
            throw new GlobalException(ConstConfig.SERVER_EXCEPTION_CODE, "User data does not exist");
        }

        return new SimpleAuthenticationInfo(userPo.getUsername(), userPo.getPassword(), getName());
    }
}

ShiroConfig :

// shiro config
@Component
public class ShiroConfig {

    public static final String PERMISSION_STRING = "perms[\"{0}\"]";
    private final SYSPermMapper permMapper;
    private final SYSUserService userService;
    public ShiroConfig(SYSPermMapper permMapper, SYSUserService userService) {
        this.permMapper = permMapper;
        this.userService = userService;
    }

    /**
     * 自定义实现 ShiroFilterFactoryBean
     *
     * 关于 shiro 中的 filterChains(过滤链)
     * 
     *      1、map 中的 key 表示要拦截的请求 url,value 表示处理此 url 所使用的拦截器,
     *          其中 value 的值可以是 shiro 提供的拦截器也可以是自定义拦截器
     *          
     *      2、value 的值可以为多个(以逗号隔开),表示该 url(key) 要被多个拦截器处理,
     *          其中执行顺序为 value 中的顺序,如: filterChains.put("/admin/user/list", "tokenFilter,authc")
     *          表示 /admin/user/list 请求会依次经过 tokenFilter(自定义)、authc 拦截器
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        // loginUrl 在前后端分离下,此处应为返回登录提示的接口 api
        // 即,当系统发现当前请求地址需要授权才可访问时返回登录提示
        factoryBean.setLoginUrl("/admin/user/login_code");

        // unAuthorizedUrl 在前后端分离下,此处应为鉴权失败后返回权限不足的接口 api
        // 即,当鉴权失败后返回权限不足提示
        factoryBean.setUnauthorizedUrl("/admin/user/authorizingFail");

        // 自定义拦截器 token 校验
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("tokenFilter", new TokenFilter(userService));
        factoryBean.setFilters(filters);

        Map<String, String> filterChains = new LinkedHashMap<>();   // 承载过滤链的变量

        // 从数据库中获取权限(资源)url 及其对应的 roleList
        // 将存在 roleList 的 url 加入到过滤链中,因为 sys_perm 表中放的是所有资源的 url,
        // 其中有些资源不需要权限就可以访问,所以不需要放到过滤链中
        // 凡是放到过滤链中的 url 都会被拦截
        List<SYSPermPo> list = permMapper.allUrlRole();
        if (list != null && list.size() != 0) {
            list.stream().peek(a -> {
                if (StringUtils.isNoneBlank(a.getPermUrl()) && !a.getRoleList().isEmpty()) {
                    filterChains.put(a.getPermUrl(), "tokenFilter,"
                            + MessageFormat.format(PERMISSION_STRING, a.getPermUrl()));
                }
            }).collect(Collectors.toList());
        }

        factoryBean.setFilterChainDefinitionMap(filterChains);

        return factoryBean;
    }

    @Bean
    public SecurityManager securityManager(ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }

    @Bean
    public ShiroRealm shiroRealm() {
        return new ShiroRealm(userService);
    }
}

shiro filter :

标识名称优先级说明对应类
anon匿名拦截器1不需要登录即可访问,一般用于静态资源AnonymousFilter
authc登录拦截器2需要登陆才可访问FormAuthenticationFilter
authcBasicHttpHttp拦截器3Http 拦截器 非常用类型BasicAuthenticationFilter
logout登出拦截器4用户登出拦截器 主要属性 redirectUrl 登出后重定向地址LogoutFilter
noSessionCreation不创建会话拦截器5没用过NoSessionCreationFilter
perms权限拦截器6验证用户是否有权限访问资源PermissionAuthenticationFilter
port端口拦截器7拦截端口,针对指定端口做出重定向PortFilter
restrest 风格拦截器8根据 rest 风格 url 构建权限 非常用HttpMethodPermissionFilter
roles角色拦截器9验证用户是否有角色访问资源RolesAuthorizationFilter
sslssl 拦截器10拦截非 https 请求,并跳转到 443SslFilter
user用户拦截器11用户认证通过或开启记住我功能SslFilter

RefreshFilterChains :

// shiro 过滤链是在服务启动时从数据库加载到内存中的
// 则当数据库权限数据发生变化时需要刷新内存中的过滤链
// 在添加修改角色权限相关方法中使用 refreshFilterChains.refreshFilterChains() 刷新过滤链
@Component
public class RefreshFilterChains {

    private static final Logger logger = LogManager.getLogger(RefreshFilterChains.class);
    public static final String PERMISSION_STRING = "perms[\"{0}\"]";
    private final ShiroFilterFactoryBean shiroFilterFactoryBean;
    private final SYSPermMapper permMapper;
    public RefreshFilterChains(ShiroFilterFactoryBean shiroFilterFactoryBean, SYSPermMapper permMapper) {
        this.shiroFilterFactoryBean = shiroFilterFactoryBean;
        this.permMapper = permMapper;
    }

    /**
     * 刷新过滤链(线程安全)
     */
    public void refreshFilterChains() {
        logger.info("refresh shiro filter chains");
        synchronized (shiroFilterFactoryBean) {
            AbstractShiroFilter filter;
            try {
                // 获取 shiro 拦截器实例
                filter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
                // 获取路径匹配过滤链解析器实例
                PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) filter.getFilterChainResolver();
                // 获取默认过滤链管理器
                DefaultFilterChainManager manager = (DefaultFilterChainManager) resolver.getFilterChainManager();

                // 清除过滤链
                manager.getFilterChains().clear();
                shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

                Map<String, String> filterChains = new LinkedHashMap<>();
                List<SYSPermPo> list = permMapper.allUrlRole();
                if (list != null && list.size() != 0) {
                    list.stream().peek(a -> {
                        if (StringUtils.isNoneBlank(a.getPermUrl()) && !a.getRoleList().isEmpty()) {
                            filterChains.put(a.getPermUrl(), "tokenFilter,"
                                    + MessageFormat.format(PERMISSION_STRING, a.getPermUrl()));
                        }
                    }).collect(Collectors.toList());
                }
                shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChains);

                // 重新生成过滤链
                Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
                if (!CollectionUtils.isEmpty(filterChainDefinitionMap)) {
                    filterChains.forEach((key, value) -> {
                        manager.createChain(key, value.replace(" ", ""));
                    });
                }

                logger.info("The refreshed filter chains is {}", filterChainDefinitionMap.toString());

            } catch (Exception e) {
                logger.error("Failed refresh shiro filter chains");
                e.printStackTrace();
            }
        }
    }
}

完结 撒花 庆祝

人生何处不相逢.mp3-娴公主

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红衣女妖仙

行行好,给点吃的吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值