1_springboot_shiro_jwt_多端认证鉴权_Shiro入门

1. Shiro简介

Shiro 是 Java 的一个安全框架,它相对比较简单。主要特性:
请添加图片描述

  • Authentication(认证):用户身份识别,通常被称为用户“登录”,即 “你是谁”
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。 即“你可以做什么“
  • Session Management(会话管理):特定于用户的会话管理,甚至在非web 应用中
  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用

还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点

  • Web支持: Shiro的Web支持API有助于保护Web应用程序
  • 缓存: 缓存是Apache Shiro API中的第一级,以确保安全操作保持快速和高效。
  • 并发性: Apache Shiro支持具有并发功能的多线程应用程序。
  • 测试: 存在测试支持,可帮助您编写单元测试和集成测试,并确保代码按预期得到保障。
  • 运行方式: 允许用户承担另一个用户的身份(如果允许)的功能
  • 记住我: 记住用户在会话中的身份。

注意: Shiro不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro

2. 核心概念

请添加图片描述
Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm

  • Subject: 当前用户
  • SecurityManager: 管理所有Subject,SecurityManager 是 Shiro 架构的核心,
  • Realms: 用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO

我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。请添加图片描述

3. 从 shiro-spring-boot-starter开始

Shiro 官方提供了 Spring Boot Starter. 目前最新版本是 2.0.0 . 下面使用了官方的例子,并对其进行改造,主要通过这个例子来理解Shiro的内部运行原理。

官方使用了 spring-boot-starter-thymeleaf 进行了服务端页面渲染,我的例子为了方便起见,将会全部返回JSON 格式的数据,不会返回页面。

3.1 新建SpringBoot项目

这里使用的SpringBoot 版本为 2.7.6 , JDK使用 JDK17 , shiro 以及 shiro-spring-boot-starter 版本均为 2.0.0 . 下面只给出重点代码,完整代码可以访问 github 获取完整项目.

引入 fastjson2和commons-lang3 是为了方便使用工具类

pom.xml

...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <!-- 2.0.0-->
    <version>${shiro-version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.47</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>
...

3.2 Shiro-Java Configuration

package com.qinyeit.shirojwt.demos.configuration;
@Configuration
public class ShiroConfiguration {
    @Bean
    public Realm realm() {
        TextConfigurationRealm realm = new TextConfigurationRealm();
        // 定义了admin 和 user 两个角色, admin 用户可以读写, user 用户只能读
        realm.setRoleDefinitions("""
                admin=read,write
                user=read
                """);
        // 定义了两个用户 joe.coder 和 jill.coder, joe.coder 用户可以读写, jill.coder 用户可以读
        realm.setUserDefinitions("""
                joe.coder=123,user
                jill.coder=456,admin
                """);
        // 开启了缓存,其实默认已经开启
        realm.setCachingEnabled(true);
        return realm;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        // 让Shiro框架拦截所有的请求
        chainDefinition.addPathDefinition("/**", "authc");
        return chainDefinition;
    }
}

配置中只配置了Realm和什么请求会被Shiro拦截器拦截。 这里使用了 shiro框架提供的 TextConfigurationRealm , 并定义了用户的定义和用户角色的定义。实际项目中这些数据都是存放到DB中的,此时就需要我们自定义 Realm。Shiro拦截器拦截了所有的请求,实际项目中可以根据需要对不需要认证的请求进行开放比如:

// 所有以/test开头的请求Shiro都不会处理
chainDefinition.addPathDefinition("/test/**", "anon");

3.3 Controller

  • HomeController.java
package com.qinyeit.shirojwt.demos.controller;
@RestController
@Slf4j
public class HomeController {
    @GetMapping("/")
    public Map<String, String> home() {
        // 现在将 subject理解成当前用户
        Subject subject = SecurityUtils.getSubject();
        // 用户凭证,简单理解成用户名
        PrincipalCollection principalCollection = subject.getPrincipals();
        String              name                = principalCollection.getPrimaryPrincipal().toString();
        // 当前用户登录成功后,它的session中都存放了哪些key
        String sessionKeys = subject.getSession().getAttributeKeys().toString();
        // 返回结果
        return Map.of("name", name, "sessionKeys", sessionKeys);
    }
}

当访问 “http://127.0.0.1:8080” 的时候会执行home()方法,如果用户没有通过认证会被shiro拦截

  • AuthenticateController.java

其中login() 方法在请求通过了Shiro 的 authc 过滤器,并且成功登录后,会调用它,在这个方法中获取了当前登录用户主体(Subject),并向客户端返回凭证。

package com.qinyeit.shirojwt.demos.controller;
@RestController
@Slf4j
public class AuthenticateController {

    @PostMapping("/login")
    public Map<String, String> login() {

        Subject subject = SecurityUtils.getSubject();
        // 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。
        PrincipalCollection principalCollection = subject.getPrincipals();
        Map<String, String> map                 = new HashMap<>();
        log.info("是否认证:{},当前登录用户主体信息:{}", subject.isAuthenticated(), principalCollection.getPrimaryPrincipal());
        map.put("name", principalCollection.getPrimaryPrincipal().toString());
        map.put("message", "登录成功");
        return map;
    }

    @PostMapping("/logout")
    public Map<String, String> logout() {

        Subject subject = SecurityUtils.getSubject();
        // 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。
        PrincipalCollection principalCollection = subject.getPrincipals();
        String              name                = principalCollection.getPrimaryPrincipal().toString();
        // 退出登录
        subject.logout();
        Map<String, String> resultMap = new HashMap<>();
        resultMap.put("name", name);
        resultMap.put("message", "退出登录成功");
        return resultMap;
    }
}

因为Shiro拦截了所有的请求,所以当发起"http://127.0.0.1:8080/logini" 请求,并携带用户名,密码 POST 提交后, Shiro首先会到 Realm中获取用户真实信息,然后调用匹配器 将提交的信息与真实信息进行匹配,即验证登录逻辑,在上面的配置中,这个匹配器 已经内置了

3.4 application.properties

# 告诉Shiro框架,哪个是登录的地址
shiro.loginUrl = /login

4. 开始测试

这里使用ApiFox 这个工具软件进行测试。首先进行正常的登录, 访问home,然后退出。

4.1 正常登录

请添加图片描述

响应:

{
    "name": "joe.coder",
    "message": "登录成功"
}

4.2 登录后访问home

请添加图片描述
响应:

{
    "sessionKeys": "[org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY, org.apache.shiro.web.session.HttpServletSession.HOST_SESSION_KEY, org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY]",
    "name": "joe.coder"
}

4.3 登录后退出

请添加图片描述

响应:

{
    "name": "joe.coder",
    "message": "退出登录成功"
}

4.4 错误密码登录

将密码修改为 1234 这个错误密码后进行登录,但是收到了这个响应:

请添加图片描述
意思是说发生了发生了重复的重定向。不光是错误登录请求会出现这个错误,所有的请求都会出现这个错误。

为什么会发生这个错误?

5. 错误分析

先看这段配置:

ShiroConfiguration.java

...
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    chainDefinition.addPathDefinition("/**", "authc");
    return chainDefinition;
}
...

所有的请求都会被Shiro 的 authc 拦截器拦截。Shiro框架定义了一些默认的拦截器,在 org.apache.shiro.web.filter.mgt.DefaultFilter 中,它是一个枚举:

public enum DefaultFilter {
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    authcBearer(BearerHttpAuthenticationFilter.class),
    ip(IpFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class),
    ...
}

可以看到authc 对应的类是org.apache.shiro.web.filter.authc.FormAuthenticationFilter

public class FormAuthenticationFilter extends AuthenticatingFilter {
    ...
    // 请求参数的名字在没有配置的情况下,是 username
    public static final String DEFAULT_USERNAME_PARAM = "username";
    // 密码请求参数名
    public static final String DEFAULT_PASSWORD_PARAM = "password";
	// 实例化的时候,设置了默认的登录URL,DEFAULT_LOGIN_URL是个常量,值为 /login.jsp
    public FormAuthenticationFilter() {
        setLoginUrl(DEFAULT_LOGIN_URL);
    }
    // 设置登录RUL
    @Override
    public void setLoginUrl(String loginUrl) {...}
	// 设置用户名参数名称
    public void setUsernameParam(String usernameParam) {...}
	
    // 框架判断当前请求禁止访问后,将会调用这个方法
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 判断当前请求是否是登录请求
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                ...
                // 执行登录    
                return executeLogin(request, response);
            } else {
                ...
            }
        } else {
            // 重定向到登录页面
            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }
...
    // 登录成功后调用
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) throws Exception {
       // 重定向到登录成功的URL, 追踪源码发现默认的成功页面URL 为 "/"
        issueSuccessRedirect(request, response);
        //we handled the success redirect directly, prevent the chain from continuing:
        return false;
    }

    // 登录失败后调用
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        ...
        // 框架执行登录验证的时候会抛出异常,然后将异常信息保存到了request scope中
        setFailureAttribute(request, e);
        //login failed, let request continue back to the login page:
        return true;
    }
    ...
}
  • 第3-5行可以看到在我们没有配置的情况下,登录的请求参数就是 usernamepassword

  • 第 8 行 对应的是application.properties 中配置的 shiro.loginUrl=/login

  • 第 23 行,执行登录,实际上执行的是 subject.login(token) 方法,框架内部就会从 定义的 realm 中获取身份信息,然后调用匹配器进行匹配,如果没有匹配上就会抛出异常信息,接下来调用onLoginFailure() 方法,这个方法将异常信息保存到了request范围中,返回了true, 这样请求就会进入到要访问的Controller中。

    如果登录成功,则调用了onLoginSuccess() 这个方法,而这个方法将请求重定向到了 / 这个URL上了。

  • 第 30 行: 在访问被禁止后,会重定向到 /login 这个URL上, 而重新向 /login 发起请求后,又被禁止,重复重定向到 /login , 这样就造成了上面那个错误的产生。

6. 自定义FormAuthenticationFilter

弄清了为什么出错,问题就好解决了。因为现在前后端分离后,大多数情况下,后端都向前端返回JSON,而不应该进行重定向。

所以我们需要自己定义一个Filter,去继承FormAuthenticationFilter ,然后重写它的一些方法。最后再将这个Filter注册到Shiro框架中,覆盖 “authc” 这个过滤器

6.1 自定义Filter

AuthenticationFilter.java

package com.qinyeit.shirojwt.demos.shiro.filter;
...
@Slf4j
public class AuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {
    private void responseJsonResult(Map<String, ?> result, ServletResponse response) {
        if (response instanceof HttpServletResponse res) {
            res.setContentType("application/json;charset=UTF-8");
            res.setStatus(200);
            res.setCharacterEncoding("UTF-8");
            try {
                // 输出JSON 数据
                res.getWriter().write(JSON.toJSONString(result));
                res.getWriter().flush();
                res.getWriter().close();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            Map<String, ?> result = Map.of("code", 401, "msg", "未登录或登录已过期");
            responseJsonResult(result, response);
            //saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }

    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) throws Exception {
        // issueSuccessRedirect(request, response);
        // 登录成功直接放行,让请求到达Controller
        return true;
    }
}

6.2 注册Filter

这个配置的方法用到了 ShiroFilterFactoryBean 因为项目中使用的是shiro-spring-boot-web-starter 它的自动配置中已经配置了 ShiroFilterFactoryBean , 所以可以直接在这个方法中“注入” 进来

内置的Filter都存放在 ShiroFilterFactoryBean 中,所以只需要拿到ShiroFilterFactoryBean 中的filter Map集合,然后将新的filter put进去就完成了 Filter的替换。

ShiroConfiguration.java

@Configuration
public class ShiroConfiguration {
    ...
    @Bean
    public FilterRegistrationBean<AuthenticationFilter> customShiroFilterRegistration(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        FilterRegistrationBean<AuthenticationFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new AuthenticationFilter());
        AuthenticationFilter authcFilter = new AuthenticationFilter();
        // 设置登录请求的URL, application.properties 中的 loginUrl 配置项就可以去掉了
        authcFilter.setLoginUrl("/login");
        // 可以设置过滤器名称、顺序等属性
        //使用这个名称,覆盖掉内置的authc过滤器
        registration.setName("authc");
        // 设置过滤器执行顺序
        registration.setOrder(Integer.MAX_VALUE - 1);
        // 覆盖掉源有的 authc 过滤器
        shiroFilterFactoryBean.getFilters().put("authc", authcFilter);
        return registration;
    }
}

6.3 不登录直接访问Home

现在再来测试一下,不登录的情况下访问Home ,此时就不会再重定向了
在这里插入图片描述

返回:

{
    "msg": "未登录或登录已过期",
    "code": 401
}

7. 登录错误捕获

org.apache.shiro.web.filter.authc.FormAuthenticationFilter 中,定义的 onLoginFailure方法:

protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                 ServletRequest request, ServletResponse response) {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Authentication exception", e);
    }
    setFailureAttribute(request, e);
    //login failed, let request continue back to the login page:
    return true;
}

我们自定义的Filter中,并没有重写这个方法,可以看到这个方法将异常信息放到了request范围中后返回的是true,这样在登录失败的情况下就会进入到Controller中,所以要捕获异常信息,可以在Controller中进行。

在Controller中只需要将Request范围中放入的异常类的名字获取到,就可以知道登录究竟出了什么样的错误。

AuthenticateController.java

package com.qinyeit.shirojwt.demos.controller;
import com.qinyeit.shirojwt.demos.shiro.filter.AuthenticationFilter;
...
@RestController
@Slf4j
public class AuthenticateController {

    @PostMapping("/login")
    public Map<String, String> login(HttpServletRequest req) {
        Subject             subject = SecurityUtils.getSubject();
        Map<String, String> map     = new HashMap<>();
        if (subject.isAuthenticated()) {
            // 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。
            PrincipalCollection principalCollection = subject.getPrincipals();
            log.info("是否认证:{},当前登录用户主体信息:{}", subject.isAuthenticated(), principalCollection.getPrimaryPrincipal());
            map.put("name", principalCollection.getPrimaryPrincipal().toString());
            map.put("message", "登录成功");
        } else {
            String exceptionClassName = (String) req.getAttribute(AuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
            log.error("signinError:{}", exceptionClassName);
            String error = null;
            if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
                error = "用户名/密码错误";
            } else if (IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
                error = "用户名/密码错误";
            } else if (ExcessiveAttemptsException.class.getName().equals(exceptionClassName)) {
                error = "登录次数过多";
            } else if (exceptionClassName != null) {
                error = "其他错误:" + exceptionClassName;
            }
            map.put("message", error);
        }
        return map;
    }

   ...
}

8. 小结

通过这个例子,我们大概知道了Shiro的整个认证流程。

  1. 请求被我们指定的 authc 拦截器拦截,这个拦截器是默认的FormAuthenticationFilter ,它在禁止访问的时候,进行了重定向,所以我们对它进行了改写,直接向客户端响应数据,不再进行重定向。
  2. 如果提交的是登录请求,则会调用 subject.login(token) 进行登录,登录的时候从 配置的 realm 中获取用户的凭证,然后再用匹配器对提交的数据和从realm中获取的凭证进行匹配,如果匹配成功则登录成功。如果抛出异常则登录失败,将异常类信息保存到Request范围中,进入Controller,这样Controller中就可以捕获错误信息,然后响应给客户端。
  3. 自定义的过滤器要进行注册。因为要替换掉默认的 authc 过滤器,所以在注册过滤器的时候注入了 ShiroFilterFactoryBean , 它负责管理所有的过滤器,在Springboot 的自动配置中,它已经被创建好了放入到Spring Bean 容器中了。

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 1_springboot_shiro_jwt_多端认证鉴权_Shiro入门 分支上.

本章节对FormAuthenticationFilter 进行了定制改造,下一章节将会对TextConfigurationRealm和匹配器进行定制,替换掉 TextConfigurationRealm , TextConfigurationRealm 使用了默认的凭证匹配器,下一章节也会对它进行改造。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/450739.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

webpack-dev-server5.0+ 版本问题

webpack-dev-server版本选择 在使用webpack-dev-server搭建新项目时&#xff0c;需要依赖node 和webpack以及webpack-cli 这是需要注意各个应用之间的版本问题 通过npm官网查看webpack-dev-server使用的版本依赖对象 先看package.json&#xff0c;可以看到当前的版本 再找到依…

YOLOv9使用训练好的权重检测目标

打开yolov9-main\detect.py文件 1修改为训练后权重文件的位置 2改为要检测图片的位置 3修改成数据集的yaml文件 运行detect.py文件并解决报错 打开报错文件yolov9-main\utils\general.py,在prediction = prediction[0]后边加上[0] 继续运行detect.py,成功检测 存在问题 当…

9个免费游戏后端平台

在这篇文章中&#xff0c;您将看到 九个免费的游戏服务平台提供商&#xff0c;这可以帮助您开始在线多人游戏&#xff0c;而无需预先投入大量资金。 每个提供商都有非常独特的功能&#xff0c;因此成本应该只是决定时要考虑的方面之一。 我还从低预算项目的角度对免费提供商进…

如何在Ubuntu系统部署DbGate数据库管理工具并结合cpolar内网穿透远程访问

文章目录 1. 安装Docker2. 使用Docker拉取DbGate镜像3. 创建并启动DbGate容器4. 本地连接测试5. 公网远程访问本地DbGate容器5.1 内网穿透工具安装5.2 创建远程连接公网地址5.3 使用固定公网地址远程访问 本文主要介绍如何在Linux Ubuntu系统中使用Docker部署DbGate数据库管理工…

WordPress供求插件API文档:获取市场类型

请注意&#xff0c;该文档为&#xff1a; WordPress供求插件&#xff1a;一款专注于同城生活信息发布的插件-CSDN博客文章浏览阅读396次&#xff0c;点赞6次&#xff0c;收藏5次。WordPress供求插件&#xff1a;sliver-urban-life 是一款专注于提供同城生活信息发布与查看的插件…

CSS伪类与常用标签属性整理与块级、行级、行级块标签(文本,背景,列表,透明,display)

目录 文本 color&#xff1a;字体颜色 font-size&#xff1a;字体大小​编辑 front-family&#xff1a;字体 text-align&#xff1a;文本对齐 text-decoration:line-through&#xff1a;定义穿过文本下的一条线 text-decoration:underline&#xff1a;定义文本下的一条线…

mysqld.exe运行时,提示缺少msvcr100.dll,msvcp100.dll文件,导致mysql安装失败或mysql服务无法启动

mysqld.exe运行时&#xff0c;提示缺少msvcr100.dll&#xff0c;msvcp100.dll文件&#xff0c;导致mysql安装失败或无法启动 msvcr100.dll&#xff0c;msvcp100.dll时VC2010的动态链接库。 1、下载地址 https://www.microsoft.com/zh-cn/download/details.aspx?id26999&wd…

027—pandas 不同分类每天指定取值的比例

前言 本例我们将进行分组计算&#xff0c;分组后得到一个堆叠数据&#xff0c;并对堆叠数据解除堆叠&#xff0c;最后再按要求格式化为百分数样式。 此类操作会经常发生在业务数据透视场景下&#xff0c;一般都会有 Excel 来操作完成&#xff0c;今天我们使用 Python 的 panda…

钉钉扫码登录,sdk问题

别问我为啥会写这玩意。因为有人问到了 1.钉钉扫码登录&#xff0c;网上代码一大堆&#xff0c;但是小同学在抄的时候突然问我&#xff0c;为啥jar包倒入不了。pom添加了&#xff0c;镜像也是阿里的&#xff0c;还是不行 下载了包&#xff0c;按这个放啊发去操作就好了 1.先…

1.Datax数据同步之Windows下,mysql数据同步至另一个mysql数据库

目录 前言步骤操作大纲步骤明细其他问题 前言 Datax是什么&#xff1f; DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台&#xff0c;实现包括 MySQL、SQL Server、Oracle、PostgreSQL、HDFS、Hive、HBase、OTS、ODPS 等各种异构数据源之间高效的数据同步功能。准备…

Hand 3D相关

看到一个不错的文献总结网址&#xff0c;如下 GitHub - SeanChenxy/Hand3DResearch 涉及的内容如下图&#xff1a;

基于SSM框架的民族文化传承与乡村扶贫网站设计与实现【附项目源码】分享

民族文化传承与乡村扶贫网站设计与开发: 源码地址&#xff1a;https://download.csdn.net/download/qq_41810183/88842794 一、引言 随着信息技术的飞速发展&#xff0c;互联网已成为文化传播与经济发展的重要平台。为了有效传承和弘扬民族文化&#xff0c;同时推动乡村地区…

【Linux】Shell编程【一】

shell是一个用 C 语言编写的程序&#xff0c;它是用户使用 Linux 的桥梁。Shell 既是一种命令语言&#xff0c;又是一种程序设计语言。 Shell 是指一种应用程序&#xff0c;这个应用程序提供了一个界面&#xff0c;用户通过这个界面访问操作系统内核的服务。 Shell属于内置的…

Docker上部署LPG(loki+promtail+grafana)踩坑复盘

Docker上部署LPG&#xff08;lokipromtailgrafana&#xff09;踩坑复盘 声明网上配置部署踩坑多机采集 声明 参考掘金文章&#xff1a;https://juejin.cn/post/7008424451704356872 版本高的用docker compose命令&#xff0c;版本低的用docker-compose 按照文章描述&#xff0c…

应急响应-Webshell-典型处置案例

网站后台登录页面被篡改 事件背景 在2018年11月29日4时47分&#xff0c;某网站管理员发现网站后台登录页面被篡改&#xff0c;“中招”服务器为windows系统&#xff0c;应采用java语言开发&#xff0c;所使用的中间件为Tomcat。 事件处置 Webshell排查 利用D盾对网站目录进…

C++_包装器

目录 1、包装器的用法 2、包装器的类型 3、包装器的作用 4、包装成员函数 5、bind&#xff08;绑定&#xff09; 5.1 bind的用法 5.2 bind减少参数个数 结语 前言&#xff1a; C11的包装器&#xff0c;总称为function包装器&#xff0c;而包装器又称适配器…

模块化机房的成本效益分析

在当今日益数字化的商业环境中&#xff0c;数据中心的建设和运营成本成为企业关注的重点。模块化机房以其独特的优势&#xff0c;不仅满足了快速部署和高效能的需求&#xff0c;还显著降低了总体成本。本文将深入探讨模块化机房在不同方面带来的成本效益。 1.预制模块化设计 …

如何解决由触发器导致 MySQL 内存溢出?

由触发器导致得 OOM 案例分析过程和解决方式。 作者&#xff1a;龚唐杰&#xff0c;爱可生 DBA 团队成员&#xff0c;主要负责 MySQL 技术支持&#xff0c;擅长 MySQL、PG、国产数据库。 爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编…

如何用 RAG 技术玩转文档问答?Milvus × 网易有道 QAnything 为你揭秘!

过去一年&#xff0c;RAG 在技术层面发展迅速&#xff0c;为向量数据库赛道添了一把火。RAG 和向量数据库的结合&#xff0c;能够有效解决幻觉、时效性差、专业领域知识不足等阻碍大模型应用的核心问题。 不久前&#xff0c;网易有道开源了自研的 RAG 引擎 QAnything。用户的任…

如何下载安装chromium浏览器

下载安装chromium浏览器去这个网站下载&#xff1a; CNPM Binaries Mirror 参考链接&#xff1a;手写 Puppeteer&#xff1a;自动下载 Chromium - 知乎