功能性的安全性保障:TOKEN鉴权校验

news2024/11/16 0:40:53

1. 引言

在软件开发过程中,确保系统的安全性是至关重要的一环。它不仅关乎保护用户数据的完整性和隐私性,也是维护系统稳定运行的基石。我认为,从宏观角度审视,软件开发的安全性保障主要可分为两大类:功能性的安全性保障和系统性的安全性校验。

功能性的安全性保障专注于应用程序层面,它着眼于那些直接影响用户数据和交互过程安全的特性。这些特性是构建用户信任和保障数据安全的关键。

而系统性的安全性校验则放眼于更为广阔的视角,它涵盖了整个系统架构和网络层面,确保从服务器到网络的每一环节都具备足够的防御能力,以抵御各种潜在的攻击。

在前文功能性的安全性保障:实现强制登录和密码加密功能,我们已经讨论了功能性的安全性保障中的两个核心议题:身份验证和密码加密。所以在本文中,我们将继续从功能性的安全性保障这个话题展开,通过对概念的细致解读、实施策略的全面分析,以及实际代码实现的展示,深入讨论其中的另外一个核心议题:TOKEN鉴权校验。

2. Token的定义

当我们准备使用Token进行鉴权校验时,首先得先了解Token是什么,我们才能扩展下去。这里简单讲下,所谓Token,是一种数据对象,通常由一组字符组成,主要用户身份验证和授权信息。通常所说的Token可以是多种形式,例如Session ID、Bearer Token、Access Token等。它们的安全性取决于如何生成、存储和传输。

3. JWT(Json Web Token):用户的身份和权限验证

在实际项目中,常用JWT作为Token来验证用户的身份和权限,确保请求的合法性。这种机制允许系统在不直接存储用户敏感信息的情况下,对用户进行身份验证和授权。

JSON Web Tokens (JWT) 是一种广泛采用的、用于在不同实体间安全传输信息的开放标准(RFC
7519)。它定义了一种紧凑且自包含的方式,允许通过JSON对象在通信双方之间传递安全性声明。这种机制不仅确保了信息的完整性和可验证性,还通过使用数字签名技术,保障了其安全性。

在JWT的框架下,令牌(Token)本身封装了一系列声明(Claims),这些声明可以被应用程序用来控制对资源的访问权限。这些声明包括但不限于用户的身份信息、权限级别以及令牌的有效期等。JWT的令牌通常采用JSON Web Signature (JWS) 进行签名,确保了其内容的不可篡改性和验证的可靠性。

3.1 JWT的构成

一个JWT由三部分组成,用点 . 分隔:

  • Header(头部):通常包含令牌的类型(即JWT)和所使用的签名算法(如HS256或RS256)。
  • Payload(负载):包含所谓的Claims(声明),它们是关于实体(通常是用户)和其他数据的声明。例如,用户ID、用户名、角色信息等。
  • Signature(签名):用于验证消息在传输过程中未被篡改,并且,对于使用私钥签名的令牌,还可以验证发送者的身份。

3.2 签名机制

特别地,JWT的签名过程常采用RSA公钥-私钥对机制。这种方式不仅提供了强大的安全保障,还使得JWT在各种应用场景中,如单点登录(SSO)、移动应用认证等,都能发挥其独特的优势。通过这种方式,JWT成为了一种理想的解决方案,用于发布和验证接入令牌(Access Tokens),从而在不同的服务和应用程序之间实现安全、高效的用户认证和授权。

3.3 过期时间

JWT通常包含一个过期时间(exp),这有助于限制Token的生命周期,增加安全性。其他类型的Token可能也需要设置过期时间,但它们的管理方式可能不同。

4. Spring Security:基于令牌的认证机制

另外,还有个话题我们不得不关注,那就是Spring Security。Spring Security是一个功能强大且高度可自定义的认证和访问控制框架,是Spring项目的一部分。它为JVM(Java虚拟机)语言提供了一套全面的安全服务,主要用于保护基于Spring的应用程序。

简单来说,当用户尝试访问一个需要认证的网站或应用时,Spring Security会要求提供Token。只有提供了正确的Token,Spring Security才会让请求通过。

4.1 核心功能

  • 认证(Authentication)
    确认用户身份,通常通过用户名和密码,但也支持多种其他认证方法(如OAuth2、JWT、LDAP等)。
    提供内置的表单登录和HTTP基本认证。
  • 授权(Authorization,也称为访问控制)
    决定已认证的用户是否有权访问某些资源或执行某些操作。
    支持基于角色和权限的访问控制。
  • 保护Web应用程序
    通过声明式的URL访问配置,定义哪些URL需要认证,哪些URL可以匿名访问。
    支持防止常见的Web攻击,如CSRF(跨站请求伪造)、XSS(跨站脚本)等。
  • 方法级安全
    使用注解(如 @Secured, @PreAuthorize, @PostAuthorize)对方法调用进行访问控制。
    整合其他Spring项目
  • 无缝集成Spring Boot、Spring MVC、Spring Data等项目。

5. 代码实现:登录生成TOKEN,并使用TOKEN进行权限校验

5.1 前端代码实现

首先,我们基于之前开发的前端代码进行扩展,在主页创建一个头部导航栏、侧边栏。

<template>
  <el-container style="height: 100vh;">
    <el-header style="padding: 0; background-color:#545c64; height: 60px;">
      <TopNavbar/>
    </el-header>
    <el-container style="flex: 1; display: flex;">
      <SideNavbar />
      <el-main style="flex: 1; display: flex; justify-content: center; align-items: center;">
        <router-view class="my"></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
import TopNavbar from './TopNavbar.vue';
import SideNavbar from "@/views/SideNavbar.vue";

export default {
  name: 'HomePage',
  components: {
    SideNavbar,
    TopNavbar,
  }
};
</script>

<style>
.el-main {
  padding: 0;
}
</style>

头部导航栏主要定义两个按钮,分别是登录按钮和登出按钮,主要为了实现对TOKEN的创建和销毁。

<template>
    <div class="navbar">
        <div class="navbar-links">
            <div class="nav-item" v-if="true">
                <el-button @click="goToLogin">登录</el-button>
            </div>
          <div class="nav-item" v-if="true">
            <el-button @click="loginOut">退出</el-button>
          </div>
        </div>
    </div>
</template>  
  
<script >
import { ref } from 'vue';
export default {
  methods: {
    goToLogin() {
      this.$router.push({name: 'Login'});
    },
    loginOut() {
      fetch('http://localhost:8081/user/logout', {
        method: 'POST',
        headers: {
          Authorization: localStorage.getItem('token')
        }
      }).then(response => {
        if (response.ok) {
          localStorage.removeItem('token');
          this.$router.push('/login');
        }
      }).catch(error => {
        console.log(error);
      });
    }
  }
}
</script>
<style scoped>
.navbar {
  display: flex;
  justify-content: flex-end; /* 确保内容靠右对齐 */
  align-items: center; /* 垂直居中对齐 */
  width: 100%;
}

.navbar-links {
  display: flex;
  justify-content: flex-end; /* 确保按钮靠右对齐 */
}

.nav-item {
  margin-right: 15px; /* 可选:增加按钮之间的间距 */
  margin-top: 10px;
}
</style>

左侧导航菜单包含两个主项:“首页” 和 “我的”,其中"我的"菜单项下包含个人信息。实现用户界面与路由的集成。

<template>
  <el-aside style="width: 200px; background-color: #545c64;">
    <el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo"
             text-color="#fff" @select="handleSelect" :default-active="activeIndex">
      <el-sub-menu index="1">
        <template #title>
          <span>首页</span>
        </template>
      </el-sub-menu>
      <el-sub-menu index="2">
        <template #title>
          <span>我的</span>
        </template>
        <el-menu-item index="2-1" @click="navigateTo('my-page')">个人信息</el-menu-item>
      </el-sub-menu>
    </el-menu>
  </el-aside>
</template>
<script>
export default {
  name: 'SideNavbar',
  data() {
    return {
      activeIndex: '2' // 默认激活的菜单项
    };
  },
  methods: {
    navigateTo(route) {
      this.$router.push({ name: route });
    },

    handleSelect(index, indexPath) {
      this.activeIndex = index;
    }
  }
};
</script>

接着定义一个个人信息页面,用于用户信息展示和头像上传功能。通过调用后端接口获取和上传数据,并使用 Element Plus 组件进行表单和消息提示的展示。这里目的是展示从本地存储中获取 token,将token作为接口携带参数,调用接口从服务器获取用户信息。如果 token 不存在,则通过 Element Plus 显示错误提示,提示用户需要登录,并重定向到登录页面。

<!-- MePage.vue -->
<template>
  <div class="me">
    <el-form :model="user" label-width="auto" style="max-width: 600px" enctype="multipart/form-data">
      <el-form-item label="头像">
        <el-upload
            class="avatar-uploader"
            :action="uploadUrl"
            accept="image/*"
            show-file-list="false"
            :before-upload="beforeUpload"
            :on-success="handleSuccess"
            :on-error="handleError"
            :on-change="handleFileChange"
        >
          <img v-if="imageUrl" style="height: 200px; width: 200px" alt="头像" class="avatar" :src="imageUrl"/>
          <el-icon class="avatar-uploader-icon" v-else-if="!islook"/>
        </el-upload>
      </el-form-item>
      <el-form-item label="用户名">
        <el-input v-model="user.username"/>
      </el-form-item>
      <el-form-item label="性别">
        <el-input v-model="user.sex"/>
      </el-form-item>
      <el-form-item label="年龄">
        <el-input v-model="user.age"/>
      </el-form-item>
      <el-form-item label="邮箱">
        <el-input v-model="user.mailbox"/>
      </el-form-item>
      <el-form-item label="个人简介">
        <el-input v-model="user.introduce" type="textarea"/>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import axios from 'axios';
import { ElMessage } from 'element-plus';

export default {
  name: 'MePage',
  data() {
    return {
      user: {
        userId: '',
        sex: '',
        age: '',
        mailbox: '',
        username: '',
        introduce: '',
        headPortrait: ''
      },
      uploadUrl: 'http://localhost:8081/upload/avatar',
      islook: false,
      imgUrl: '../assets/logo.png',
      imageUrl: ''
    }
  },
  components: {},
  created() {
    this.getuser();
  },
  methods: {
    getuser() {
      const token = localStorage.getItem('token');
      console.log('token:', token);
      if (!token) {
        ElMessage.error('请先登录');
        this.$router.push('/login');
      }
      axios.get('http://localhost:8081/user/getById?userId=123', {headers: {Authorization: `Bearer ${token}`}}).then(response => {
        console.log('服务器返回的数据:', response.data);
        this.user = response.data.body;
        this.imageUrl = this.user.headPortrait ? `${this.user.headPortrait}` : '@/assets/logo.png';
        console.log('图片 URL:', this.imageUrl);
        this.islook = true;
      }).catch(error => {
        console.log(error);
        ElMessage.error('获取用户信息失败');
      });
    },
    beforeUpload(file) {
      const isImage = file.type.startsWith('image/');
      if (!isImage) {
        ElMessage.error('只能上传图片文件!');
        return false;
      }
      return isImage;
    },
    handleSuccess(response) {
      console.log('图片 上传成功响应:', response);
      if (response.code === 200) {
        ElMessage.success('上传成功');
        console.log('上传成功');
        // 重新调用 getuser 方法以获取最新的头像地址
        this.getuser();
      } else {
        ElMessage.error('上传失败: ' + response.msg);
      }
    },
    handleError(err) {
      console.error('图片上传失败:', err);
      ElMessage.error('上传失败: ' + (err.message || '未知错误'));
    },
    handleFileChange(file) {
      // 更新显示的图片
      const fileURL = file.raw;
      this.imageUrl = URL.createObjectURL(fileURL);
    }
  }
}
</script>

<style scoped>
.me {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
  height: 200px;
  width: 200px
}

.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}
</style>

路由这里我们要改下配置,在主页面下配置子页面路径为 '/my-page'

const routes = [
    {
        path: '/',
        name: 'HomePage',
        component: HomePage,
        meta: {requiresAuth: true}, // 添加元信息,表示需要认证
        children: [
            {
                path: '/my-page',
                name: 'my-page',
                component: MePage,
            }
        ]
    },
    {
        path: '/login',
        name: 'Login',
        component: Login
    },
    {
        path: '/register',
        name: 'Register',
        component: Register
    },
    {
        path: '/:pathMatch(.*)*',
        redirect: '/login' // 捕获所有未定义的路由并重定向到登录页面
    }
];

Login.vue组件也需要改下,若登录成功,则存储 TOKEN 到本地并跳转到主页;若登录失败,则显示相应的错误提示,捕获异常以便出错时给予用户反馈。

async login() {
      try {
        const params = new URLSearchParams();
        params.append('username', this.form.username);
        params.append('password', this.form.password);

        const response = await axios.post('http://127.0.0.1:8081/user/login', params);
        console.log("login response:", response);
        if (response.data.code === 200) {
          this.$message.success('登录成功');
          console.log("登录成功,获取到的TOKEN是: " + JSON.stringify(response.data.body))
          localStorage.setItem('token', response.data.body);
          this.$router.push('/');
        } else {
          if (response.data.extension && response.data.extension.error) {
            this.$message.error('登录失败: ' + response.data.extension.error);
            console.log(response.data.extension.error);
          } else {
            this.$message.error('登录失败');
            console.log('未知错误');
          }
        }
      } catch (error) {
        console.log(error);
        this.$message.error('登录失败');
      }
    }

5.2后端功能实现

首先引入jwt和Spring Security的依赖

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

接着我们定义一个JWT工具类,用于生成 JWT Token、验证JWT Token以及反向解析Token,获取用户名。

/**
 * 功能描述:JWT工具类
 **/
public class JwtUtils {

    private static final String SECRET = "USER_JWT_SECRET";

    /**
     * 功能描述:根据用户名生成JWT token
     *
     * @param username 用户名
     * @return {@code String }
     */
    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, SECRET)
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24 小时过期
                .compact();
    }

    /**
     * 功能描述: 验证JWT token是否有效
     *
     * @param token Jwt Token
     * @return boolean
     */
    public static boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 功能描述: 从JWT token中获取用户名
     *
     * @param token Jwt Token
     * @return {@code String }
     */
    public static String getUsername(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

定义好JwtUtils工具类后,预期是在前端请求后端时,校验是否传入Token以及传入Token的有效性。我们可以在每个接口定义Token校验,当时这样子无疑增加了工作量和代码复杂度,所以这里我使用AOP的形式,利用注解的形式,在接口被请求之前,自动去校验Token。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenListener {
}
@Aspect
@Component
public class JwtAspect {

    @Before("@annotation(com.example.interestplfm.annotation.TokenListener) || @within(com.example.interestplfm.annotation.TokenListener)")
    public void validateToken() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = request.getHeader("Authorization");

        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        assert response != null;
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            // 抛出异常,返回401状态码
            if (!JwtUtils.validateToken(token)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                throw new IllegalArgumentException("用户未成功登录,请先登录!");
            }
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            throw new IllegalArgumentException("用户未成功登录,请先登录!");
        }

    }
}

最后,我们在控制层实现用户相关接口,并对必要的接口进行Token权限校验。

@RestController
@RequestMapping("/user")
@CrossOrigin(origins = "http://localhost:8080")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public RestResult<String> register(@RequestParam String username, @RequestParam String password) {
        if (userService.registerUser(username, password)) {
            return RestResult.build("User registered successfully");
        } else {
            Map<String, Object> extension = new HashMap<>();
            extension.put("error", "User already exists");
            return RestResult.buildFailure(extension);
        }
    }

    @PostMapping("/login")
    public RestResult<String> login(@RequestParam String username, @RequestParam String password) {
        User user = userService.verifyPassword(username, password);
        if (user != null) {
            String token = JwtUtils.generateToken(user.getUsername());
            return RestResult.build(token);
        } else {
            Map<String, Object> extension = new HashMap<>();
            extension.put("error", "Invalid username or password");
            return RestResult.buildFailure(extension);
        }
    }

    @GetMapping("/getById")
    @TokenListener
    public RestResult<User> uploadPicture(@RequestParam("userId") Long userId) {
        return RestResult.build(userService.selectByUserId(userId));
    }

    @PostMapping("/logout")
    public void logout(HttpServletRequest request, HttpServletResponse response) {
        // 清空当前用户的Token
        response.setHeader("Authorization", "");
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

一切准备就绪,让我们来验证下功能是否达到预期吧。

哦吼~通过点击登录,可以发现并没有反应,一看控制台,提示前端访问后端跨域请求被阻止。 这时候可能有人就疑问了,明明在控制层已经设置了@CrossOrigin(origins = "http://localhost:8080"),允许前端的跨域请求,可为何现在失效了呢?
在这里插入图片描述

5.3 Spring Security 的坑

其实,这是由于Spring Security引起的问题,当我们在项目集成了Spring Security功能后,基本会遇到一些常见的“坑”或需要注意的问题。比如说:

  1. 出现无法登录问题
  2. 静态资源请求被拦截
  3. 前端访问后端的请求被拦截
  4. 出现跨域问题
  5. 认证和授权逻辑混乱
  6. 角色和权限的误用

就比如上面跨域的情况,实际上是因为默认情况下,Spring Security启用了CSRF保护,所以在使用REST API时,可能就会导致问题。

要解决这个问题也很简单,可以根据具体情况决定是否禁用CSRF保护,或为特定API端点配置CSRF保护。这里我们定义了一个Spring Security配置类SecurityConfig,配置URL访问权限,允许特定路径或用户进行访问。同时启用CORS配置,允许来自特定域的请求,并允许所有HTTP方法和头部信息,同时允许发送Cookie。也在配置中明确允许访问静态资源。

package com.example.interestplfm.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig  extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").permitAll() // 所有以 /user/ 开头的路径都允许匿名访问
                .antMatchers("/upload/**").permitAll() // 所有以 /upload/ 开头的路径都允许匿名访问
                .antMatchers("/static/**").permitAll() // 允许静态资源访问
                .antMatchers("/css/**", "/js/**", "/image/**").permitAll() // 允许特定静态资源目录访问
                .anyRequest().authenticated()
                .and().formLogin()
                .and().logout()
                .logoutUrl("/logout"); // 配置注销URL
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("http://localhost:8080"); // 允许的前端域
        configuration.addAllowedMethod("*"); // 允许的HTTP方法
        configuration.addAllowedHeader("*"); // 允许的头部信息
        configuration.setAllowCredentials(true); // 允许发送Cookie

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

配置完后,我们再次验证下:可以发现,可以正常登录成功,且可以跳转到个人信息页面进行头像上传和用户信息展示,图片也正常显示,同时点击退出清空了Token后,直接通过输入URL路径请求接口的形式,会直接提示登录失败并回到登录页面。

至此,我们的预期效果已全部实现。
在这里插入图片描述

6. 总结

TOKEN鉴权校验作为一种行之有效的安全机制,确保了用户请求的合法性,为用户身份和权限验证提供了坚实的基础。通过使用JWT(JSON Web Tokens)加密用户信息,结合Spring Security进行细粒度的权限认证,我们可以为用户数据提供双重保障,从而实现更高级别的安全性。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

昇思MindSpore 应用学习-DCGAN生成漫画头像-CSDN

日期 心得 昇思MindSpore 应用学习-DCGAN生成漫画头像&#xff08;AI代码学习&#xff09; DCGAN生成漫画头像 在下面的教程中&#xff0c;我们将通过示例代码说明DCGAN网络如何设置网络、优化器、如何计算损失函数以及如何初始化模型权重。在本教程中&#xff0c;使用的动…

Python从0到100(四十六):实现管理员登录及测试功能

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

-XX:MaxDirectMemorySize和-Dio.netty.maxDirectMemory区别

-XX:MaxDirectMemorySize是java运行参数&#xff0c;用户控制java程序可以使用的最大直接内存&#xff08;堆外/本地&#xff09;&#xff1b; -Dio.netty.maxDirectMemory是netty运行参数&#xff0c;用户控制netty程序可以使用的最大直接内存&#xff08;堆外/本地&#xff…

SQLynx数据库管理工具

背景&#xff1a;业主对网络安全要求比较高&#xff0c;不提供VPN等远程工具&#xff0c;也不能开放3306端口到互联网。那怎么样运维数据库就是个难题&#xff1f;找到了SQLynx这个可以网页访问的数据库管理工具&#xff0c;给大家分享一下。 1.介绍 SQLynx原名SQL Studio&…

如何优化 Selenium 和 BeautifulSoup 的集成以提高数据抓取的效率?

摘要 在互联网时代&#xff0c;数据的价值日益凸显。对于电商网站如京东&#xff0c;其商品信息、用户评价等数据对于市场分析、产品定位等具有重要意义。然而&#xff0c;由于这些网站通常使用 JavaScript 动态生成内容&#xff0c;传统的爬虫技术难以直接获取到完整数据。本…

Vue 实现电子签名并生成签名图片

目录 前言项目结构代码实现 安装依赖创建签名画布组件生成签名图片 总结相关阅读 1. 前言 电子签名在现代Web应用中越来越普遍&#xff0c;例如合同签署、确认表单等。本文将介绍如何使用Vue.js实现一个简单的电子签名功能&#xff0c;并将签名生成图片。 2. 项目结构 项…

基于 LlamaIndex 构建自己的 RAG 知识库

创建虚拟环境用于运行 运行 InternLM 的基础环境&#xff0c;命名为 llamaindex conda create -n llamaindex python3.10 查看存在的环境 conda env list 激活刚刚创建的环境 conda activate llamaindex 安装基本库pytorch,torchvision ,torchaudio,pytorch-cuda 并指定通道&…

动态代理更改Java方法的返回参数(可用于优化feign调用后R对象的统一处理)

动态代理更改Java方法的返回参数&#xff08;可用于优化feign调用后R对象的统一处理&#xff09; 需求原始解决方案优化后方案1.首先创建AfterInterface.java2.创建InvocationHandler处理代理方法3. 调用 实际运行场景拓展 需求 某些场景&#xff0c;调用别人的方法&#xff0…

手机空号过滤批量查询的意义及方法

手机空号过滤批量查询是现代营销和通信管理中常用的技术手段&#xff0c;旨在通过批量处理手机号码&#xff0c;筛选出活跃号码和空号等无效号码&#xff0c;以提高营销效率和减少不必要的通信成本。以下是关于手机空号过滤批量查询的详细解答&#xff1a; 一、手机空号过滤批…

3dsMax 设置近平面削减,靠近模型之后看不到模型,看很小的模型放大看不到

3dsMax 设置近平面削减&#xff0c;靠近模型之后看不到模型&#xff0c;看很小的模型放大看不 问题展示 解决办法_1 把这两个东西最上面的拖拽到最上面&#xff0c;最下面的拖拽到最下面。 解决办法_2 勾选视口裁剪 把这两个东西最上面的拖拽到最上面&#xff0c;最下面的…

华为ensp中ISIS原理与配置(超详细)

isis原理与配置 8-20字节&#xff1b; 地址组成&#xff1a;area id&#xff0c;system id&#xff0c;set三部分组成&#xff1b; system id占6个字节&#xff1b;sel占一个&#xff0c;剩下的为area id区域号&#xff1b; system id 唯一&#xff0c; 一般将router id 配…

opengl 写一个3D立方体——计算机图形学编程 第4章 管理3D图形数据 笔记

计算机图形学编程&#xff08;使用OpenGL和C&#xff09; 第4章 管理3D图形数据 笔记 数据处理 想要绘制一个对象&#xff0c;它的顶点数据需要发送给顶点着色器。通常会把顶点数据在C端放入 一个缓冲区&#xff0c;并把这个缓冲区和着色器中声明的顶点属性相关联。 初始化立…

Python中高效处理大数据的几种方法

随着数据量的爆炸性增长&#xff0c;如何在Python中高效地处理大数据成为了许多开发者和数据科学家的关注焦点。Python以其简洁的语法和丰富的库支持&#xff0c;在数据处理领域占据了重要地位。本文将介绍几种在Python中高效处理大数据的常用方法。 目录 1. 使用Pandas进行数…

基于STM32的逻辑分析仪

文章目录 一、逻辑分析仪体验1、使用示例1.1 逻辑分析仪1.2 开源软件PulseView 2、核心技术2.1 技术方案2.2 信号采集与存储2.3 数据上传 3、使用逻辑分析仪4、 SourceInsight 使用技巧4.1新建工程4.2 设置工程名及工程数据目录4.3 指定源码目录4.4 添加源码4.5 同步文件4.6 操…

为RTEMS Raspberrypi4 BSP添加SPI支持

为RTEMS Raspberrypi4 BSP添加SPI支持 主要参考了dev/bsps/shared/dev/spi/cadence-spi.c RTEMS 使用了基于linux的SPI框架&#xff0c;SPI总线驱动已经在内核中实现。在这个项目中我需要实习的是 RPI4的SPI主机控制器驱动 SPI在RTEMS中的实现如图&#xff1a; 首先需要将S…

25.x86游戏实战-理解发包流程

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

江科大/江协科技 STM32学习笔记P9-11

文章目录 OLED1、OLED硬件main.c EXTI外部中断1、中断系统2、中断执行流程图3、STM32中断4、中断地址的作用5、EXTI6、EXTI基本结构7、AFIO复用IO口8、EXTI框图或门和与门 9、旋转编码器介绍10、硬件电路 OLED 1、OLED硬件 SCL和SDA是I2C的通信引脚&#xff0c;需要接在单片机…

java包装类型缓存简单探究-Integer为例

文章目录 包装类型缓存自动装箱与valueOf感悟结语 包装类型缓存 包装类型缓存是什么 本文以常用的Integer包装类为例做一个探索&#xff0c;感兴趣可以用类似方法查看其他包装类。 我们都知道它会缓存 -128到127之间的整数Integer对象。 结论大伙都知道。那么我们今天就来探究…

【Android】安卓四大组件之广播知识总结

文章目录 动态注册使用BroadcastReceiver监听Intent广播注册Broadcast Receiver 静态注册自定义广播标准广播发送广播定义广播接收器注册广播接收器 有序广播修改发送方法定义第二个广播接收器注册广播接收器广播截断 使用本地广播实践-强制下线使用ActivityCollector管理所有活…

ubuntu那些ppa源在哪

Ubuntu中的 PPA 终极指南 - UBUNTU粉丝之家 什么是PPA PPA 代表个人包存档。 PPA 允许应用程序开发人员和 Linux 用户创建自己的存储库来分发软件。 使用 PPA&#xff0c;您可以轻松获取较新的软件版本或官方 Ubuntu 存储库无法提供的软件。 为什么使用PPA&#xff1f; 正如…