springboot3+springsecurity+redis 整合登录认证以及权限校验

news2024/11/17 5:54:38

1. 架构说明

整体架构如下(提供的对应的模块引入),围绕着springsecurity中的三大核心展开:

​ 1、Authentication:存储了认证信息,代表当前登录用户

​ 2、SeucirtyContext:上下文对象,用来获取Authentication

​ 3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

重点放在FilterChain的编写上,引入认证的和授权的Filter,并通过调用AuthenticationManager.authenticate和AuthenticationProvider中 写入的加密器、userdetailservice做认证

在这里插入图片描述

2. 依赖引入

由于需要在登录时查询数据库所以需要引入对数据库操作的依赖

<dependencies>
      <!--mybatis和springboot整合-->
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
      </dependency>
      <!--Mysql数据库驱动8 -->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
      </dependency>
      <!--persistence-->
      <dependency>
          <groupId>javax.persistence</groupId>
          <artifactId>persistence-api</artifactId>
      </dependency>
      <!--通用Mapper4-->
      <dependency>
          <groupId>tk.mybatis</groupId>
          <artifactId>mapper</artifactId>
      </dependency>
      <!--SpringBoot集成druid连接池-->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
      </dependency>
      <!--lombok-->
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.28</version>
          <scope>provided</scope>
      </dependency>
      <!--cloud_commons_utils-->
      <dependency>
          <groupId>com.simple.cloud</groupId>
          <artifactId>simpleCloud_api_commons</artifactId>
          <version>1.0-SNAPSHOT</version>
      </dependency>
      <!-- Spring Security依赖 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <scope>provided </scope>
      </dependency>

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
  </dependencies>

3. application.yml配置文件编写

spring:
  data:
    redis: # redis使用
      host: localhost
      port: 6379
      database: 0
      timeout: 1800000
      password:
      jedis:
        pool:
          max-active: 20 #最大连接数
          max-wait: -1    #最大阻塞等待时间(负数表示没限制)
          max-idle: 5    #最大空闲
          min-idle: 0     #最小空闲

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource 
    driver-class-name: com.mysql.cj.jdbc.Driver 
    # 注意修改为用户存在的数据库
    url: jdbc:mysql://localhost:3306/对应库名称?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: abc123

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

3. config核心配置文件

注意:security以及摒弃了之前继承 WebSecurityConfigurerAdapter 的方法,鼓励开发者自己写配置类将bean注入容器中,所以之前的springboot2的整合方法已经不好用啦,但是不用慌,大体上的实现还是一样的,毕竟只是换了一个壳子😁

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig{
}

3.1 加密器引入

自定义一个Md5 加密器

@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        return MD5Helper.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5Helper.encrypt(rawPassword.toString()));
    }
}

3.1.1 MD5Helper

public final class MD5Helper {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
}

3.1.2 config中注入依赖

@Bean
public PasswordEncoder passwordEncoder() {
    return new CustomMd5PasswordEncoder();
}

3.2 UserDetailServer 引入

直接使用Lambda表达式在config中注入

@Bean
public UserDetailsService userDetailsService(){
    // 调用 JwtUserDetailService实例执行实际校验
    return email -> {
        //实际上用email进行匹配 从数据库中获取
        SysUser authUser = sysUserMapper.getUserByEmail(email);

        if(null == authUser) {
            throw new UsernameNotFoundException("邮箱不存在!");
        }

        return new CustomUser(authUser, Collections.emptyList());
    };
}

基于mybatis实现对数据库的查找功能 —

3.2.1 sysUserMapper.java

public interface SysUserMapper extends Mapper<SysUser> {

    // 根据email获取用户
    SysUser getUserByEmail(String email);
}

3.2.2 sysUserMapper.xml 实现类

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.simple.cloud.mapper.SysUserMapper">
  <resultMap id="BaseResultMap" type="com.simple.cloud.entities.SysUser">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="email" jdbcType="VARCHAR" property="email" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="phone" jdbcType="VARCHAR" property="phone" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="is_deleted" jdbcType="TINYINT" property="isDeleted" />
  </resultMap>

  <!-- // 根据email获取用户
    SysUser getUserByEmail(String email);-->
  <select id="getUserByEmail" resultMap="BaseResultMap">
    SELECT * FROM system_user WHERE email = #{email} AND is_deleted = 0
  </select>
</mapper>

3.3 config中引入AuthenticationProvider

有了我们的加密器和userdetail后就可以引入AuthenticationProvider

在安全框架中,AuthenticationProvider 是一个接口或类,用于处理身份验证逻辑。它通常用于验证用户的身份信息,例如用户名和密码、令牌或其他认证凭据。

当用户尝试进行身份验证时,系统会将用户的认证请求传递给相应的 AuthenticationProvider。该提供者负责根据配置的规则和算法来验证用户提供的凭据是否有效。如果验证成功,AuthenticationProvider 通常会创建一个表示经过身份验证的主体(如用户)的 Authentication 对象,并返回该对象以供进一步处理。这个 Authentication 对象包含了主体的相关信息,如用户名、角色、权限等。

/**
 * 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
 *
 * @return
 */
@Bean
public AuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    // DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
    authProvider.setUserDetailsService(userDetailsService());
    // 设置密码编辑器
    authProvider.setPasswordEncoder(passwordEncoder());
    return authProvider;
}

3.4 AuthenticationManager

上面一点提到了Authentication ,其中包含了

​ 1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

​ 2、Credentials:用户凭证,一般是密码(在获取用户权限后会销毁,以保证安全,防止泄露)

​ 3、Authorities:用户权限

而在security中使用AuthenticationManager 来对其执行校验,所以需要引入相应的bean

/**
 * 登录时需要调用AuthenticationManager.authenticate执行一次校验
 *
 * @param config
 * @return
 * @throws Exception
 */
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

3.5 两个过滤器

在完成上面的配置之后就到了实际处理请求的filter了,我们知道springsecurity就是由一系列的filter构成,具体引入的如下两个

3.5.1 TokenLoginFilter 登录认证

本案例中用email , password做为登录的loginVo(email换成username也是可以的)

@Bean
public TokenLoginFilter authenticationTokenLoginFilter() {
		//这里通过上下文获取容器中的AuthenticationManager
     return new TokenLoginFilter(applicationContext.getBean(AuthenticationManager.class),redisTemplate);
 }
3.5.1.1 TokenLoginFilter 具体实现 继承UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是 Spring Security 中的一个过滤器,用于处理基于用户名和密码的身份验证请求。它通常与 AuthenticationProvider 一起使用(里面调用的就是刚刚定义的userdetailserivce),以验证用户提供的凭据是否有效。

当用户尝试通过用户名和密码进行身份验证时,系统会将认证请求传递给 UsernamePasswordAuthenticationFilter。该过滤器负责从请求中提取用户名和密码,并将它们封装成一个 Authentication 对象。然后,它会调用配置的 AuthenticationProvider 来验证这个 Authentication 对象。

如果 AuthenticationProvider 验证成功,即用户名和密码匹配,那么 UsernamePasswordAuthenticationFilter 会创建一个表示经过身份验证的主体(如用户)的 Authentication 对象,并将其存储在安全上下文中。这样,后续的请求就可以访问到已认证的用户信息。

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/system/loginCon/login","POST"));
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getEmail(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        //获取当前用户
        CustomUser customUser = (CustomUser)auth.getPrincipal();
        //生成token
        String token = JWTHelper.createToken(customUser.getSysUser().getId(),
                customUser.getSysUser().getEmail());

        //获取当前用户权限数据,放到Redis里面 key:id   value:当前用户
        //设置90s过期 即是登录之后会立即请求用户信息调用info() 这里存入的数据只是为了再info()处做双向认证
        //上面的info后面会讲到 , 在具体的业务中请求用户信息包括了权限信息
        redisTemplate.opsForValue().set(customUser.getSysUser().getId()+"——BUSINESS_KEY",
                JSON.toJSONString(customUser.getSysUser()) , 90 , TimeUnit.SECONDS);

        //返回token给前端(或者登录请求的调用者)
        Map<String,Object> map = new HashMap<>();
        map.put("token",token);

        ResponseUtil.out(response, ResultData.success(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        ResponseUtil.out(response, ResultData.fail(ResultCodeEnum.RC401.getCode(), e.getMessage()));
    }
}

3.5.2 TokenAuthenticationFilter 权限认证

@Bean
public TokenAuthenticationFilter authenticationJwtTokenFilter() {
    return new TokenAuthenticationFilter(redisTemplate);
}
3.5.2.1 TokenAuthenticationFilter 具体实现 继承OncePerRequestFilter

OncePerRequestFilter是Spring框架中的一个抽象类,它用于确保在一次请求中只执行一次过滤操作。这个类主要用于实现自定义的过滤器,继承OncePerRequestFilter类并重写doFilterInternal方法来实现具体的过滤逻辑。

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/system/loginCon/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
        		// 放到security上下文中,有需要的地方就可以获取到
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            ResponseUtil.out(response, ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //请求头是否有token
        String token = request.getHeader("token");
        if(!StringUtils.isEmpty(token)) {
            String email = JWTHelper.getUsername(token);
            Long userId = JWTHelper.getUserId(token);

            if(!StringUtils.isEmpty(email)) {
                //当前用户信息放到ThreadLocal里面
                LoginUserInfoHelper.setUserId(userId);
                LoginUserInfoHelper.setEmail(email);

                //通过 userId 从redis获取权限数据
                //后续在info中放入 一开始从redis拿到的数据都为null
                List<String> authStringList = (List<String>) redisTemplate.opsForValue().get(userId+"_LOGIN_BUSINESS_STORAGE_");

                //把redis获取字符串权限数据转换要求集合类型 List<SimpleGrantedAuthority>
                if(authStringList != null) {
                    List<SimpleGrantedAuthority> authList = new ArrayList<>();
                    for (String val : authStringList) {
                        authList.add(new SimpleGrantedAuthority(val));
                    }
                    //由于授权信息必须是GrantedAuthority对象所以得转换一下
                    return new UsernamePasswordAuthenticationToken(email,null, authList);
                } else {
                    return new UsernamePasswordAuthenticationToken(email,null, new ArrayList<>());
                }
            }
        }
        return null;
    }
}

3.6 security过滤规则

有了上面两个过滤器后,我们应该以什么样的规则来使用呢? 这就引出config中最重要的一环 SecurityFilterChain的bean注入

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // 禁用basic明文验证
        .httpBasic().disable()
        // 前后端分离架构不需要csrf保护
        .csrf().disable()
        // 禁用默认登录页
        .formLogin().disable()
        // 禁用默认登出页
        .logout().disable()
        // 前后端分离是无状态的,不需要session了,直接禁用。
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                // 允许所有OPTIONS请求
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 允许直接访问授权登录接口
                .requestMatchers(HttpMethod.POST, "/system/loginCon/login").permitAll()
                // 允许 SpringMVC 的默认错误地址匿名访问
                .requestMatchers("/error").permitAll()
                // 允许任意请求被已登录用户访问,不检查Authority
                .anyRequest().authenticated())
        .authenticationProvider(authenticationProvider())
        // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
        .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
        // 登录过滤器
        .addFilter(authenticationTokenLoginFilter());

    return http.build();
}

这里就偷个懒些不写logout的实现😭

3.8 获取用户权限信息的方法info()

前面的filter中多次提到这个info方法,那具体实现就放在这里啦,主要的目的是获取到权限信息,而前端调用info接口也可以获取到相应的登录用户信息,前端做他的权限控制,后端也做权限控制双重保障
由于前面在登录成功后TokenLoginFiltersuccessfulAuthentication方法向redis中放入了数据(90s过期时间)理想情况下,前端登录请求一成功,返回状态码200,此时会直接调用info方法再次发送请求,请求相应的用户具体信息(菜单按钮权限等用于渲染页面)而后端会把相应的按钮权限放入redis中,后续有请求匹配就会查找是否有相应的权限来决定是否执行

 /**
  * 获取用户信息
  * @return
  */
 @PostMapping("/info")
 public ResultData info(HttpServletRequest request){
     String token = request.getHeader("token");

     //获取用户id
     Long userId = JWTHelper.getUserId(token);
     //根据id从redis中获取当前用户
     SysUser sysUser = JSON.parseObject((String) redisTemplate.opsForValue().get(userId + "——BUSINESS_KEY"), SysUser.class);

     if(sysUser == null)
         return ResultData.fail(ResultCodeEnum.RC401.getCode(), "当前用户未登录,请登录后再试");

     //获取用户的角色
     List<SysRole> sysRoles = sysUserService.selectAllByUserId(userId);
     sysUser.setRoleList(sysRoles);

     //根据id获取所有菜单列表
     List<RouterVo> routerList = sysMenuService.getAllRouterListByUserId(userId);

     //根据id获取所有按钮列表
     List<String> permsList = sysMenuService.getAllMenuListByUserId(userId);

     //map中插入相应的值
     Map<String, Object> map = new HashMap<>();
     map.put("routers",routerList);
     map.put("buttons",permsList);
     map.put("roles",sysUser.getRoleList());
     map.put("name",sysUser.getName());

     // 存放权限信息到redis中 , springsecurity通过 userId 做为key获取权限列表
     redisTemplate.opsForValue().set(sysUser.getId()+"_LOGIN_BUSINESS_STORAGE_", permsList);

     return ResultData.success(map);
 }

4. 最终版config配置类

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig{
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private ApplicationContext applicationContext;
    @Resource
    private SysUserMapper sysUserMapper;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new CustomMd5PasswordEncoder();
    }

    @Bean
    public TokenAuthenticationFilter authenticationJwtTokenFilter() {
        return new TokenAuthenticationFilter(redisTemplate);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用basic明文验证
            .httpBasic().disable()
            // 前后端分离架构不需要csrf保护
            .csrf().disable()
            // 禁用默认登录页
            .formLogin().disable()
            // 禁用默认登出页
            .logout().disable()
            // 前后端分离是无状态的,不需要session了,直接禁用。
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                    // 允许所有OPTIONS请求
                    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    // 允许直接访问授权登录接口
                    .requestMatchers(HttpMethod.POST, "/system/loginCon/login").permitAll()
                    // 允许 SpringMVC 的默认错误地址匿名访问
                    .requestMatchers("/error").permitAll()
                    // 允许任意请求被已登录用户访问,不检查Authority
                    .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider())
            // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
            .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilter(authenticationTokenLoginFilter());

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        // 调用 JwtUserDetailService实例执行实际校验
        return email -> {
            //实际上用email进行匹配 从数据库中获取
            SysUser authUser = sysUserMapper.getUserByEmail(email);

            if(null == authUser) {
                throw new UsernameNotFoundException("邮箱不存在!");
            }

            return new CustomUser(authUser, Collections.emptyList());
        };
    }

    /**
     * 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
     *
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
        authProvider.setUserDetailsService(userDetailsService());
        // 设置密码编辑器
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    /**
     * 登录时需要调用AuthenticationManager.authenticate执行一次校验
     *
     * @param config
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public TokenLoginFilter authenticationTokenLoginFilter() {
        return new TokenLoginFilter(applicationContext.getBean(AuthenticationManager.class),redisTemplate);
    }
}

到此结束👍希望对各位能有帮助

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

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

相关文章

爬虫学习--5.xpath数据解析

xpath是XML路径语言&#xff0c;它可以用来确定xml文档中的元素位置&#xff0c;通过元素路径来完成对元素的查找。HTML就是XML的一种实现方式&#xff0c;所以xpath是一种非常强大的定位方式。 基本概念 XPath&#xff08;XML Path Language&#xff09;是一种XML的查询语言…

计算机网络-----总结UDP的报文结构

UDP报文结构 UDP报文结构: 有报头和载荷组成 源端口: 发送端的端口号 目的端口: 接收端的端口号 数据报长度 : UDP报文的数据长度 包括报头和载荷 校验和: 检测UDP报文在传输过程中是否损坏, 用于数据完整性校验 说明: UDP报头一共八个字节 因此, 每个部分(源端口, 目的端…

sql Server2015安装——参考的教程

1.sql Server安装包来自&#xff1a;https://mp.weixin.qq.com/s/Pe_YbWw_MgwjzzZhQWIYfA 2.需要的替换文件和补丁&#xff1a;https://blog.csdn.net/Auspicious_air/article/details/108315154 https://blog.csdn.net/m0_60477996/article/details/126748477 3.安装manger…

php开发-个人博客项目文件操作类编辑器上传下载删除读写

特地整了个软件 这就舒服了 文件操作类的开发 文件的任意上传&#xff0c;下载&#xff0c;读取&#xff0c;删除操作等 1.文件上传类-任意文件上传 分为三类 1&#xff0c;代码自主编写的 先写一个html的上传表单&#xff0c;这个网上搜索就有 标题看着不够明确啊&#…

php168 6.0.1变量覆盖

php168 6.0.1变量覆盖 -》 远程代码执行漏洞 前言:敢说全网最详细是因为我做一半我做不下去了&#xff0c;我心态做崩溃了&#xff0c;然后上网找漏洞成因&#xff0c;我找不到&#xff0c;然后又搞了好久&#xff0c;真的心态崩了&#xff0c;到最后灰盒测试的时候我可能PHP版…

【论文阅读】Spectral–Spatial Attention Network for Hyperspectral Image Classification

Spectral–Spatial Attention Network for Hyperspectral Image Classification 论文地址摘要&#xff1a;1. 简介1.1.动机1.2.贡献 2. 相关作品2.1.双向递归网络RNN2.2.CNN2.3. Attention Mechanism 3. 方法3.1 Attention with RNN for Spectral Classification3.2&#xff0e…

【转载】数字化工厂规划蓝图报告

制造业进入到全新的数字化时代&#xff0c;需要构建新型智能工厂、数字化工厂与智能车间以助力传统产业智能制造升级&#xff0c;将新一代信息技术贯穿到设计、工艺、生产、物流等各个环节。目的是完善创新体系、提升产品质量、推行绿色制造、增强核心竞争力、发展现代化客户体…

4月功能更新 | 知识库新增微信分享图配置,丰富模板一键应用

4月HelpLook带来了更多知识库模板资源&#xff0c;直观的文章版本对比和微信分享图配置等功能&#xff0c;更多功能更新等你往下挖...... 目录 知识库管理功能升级 1. 知识库的全局替换支持替换标题2. 知识库支持配置微信分享图3. 知识库链接支持生成二维码和下载4. 后台站点栏…

计算机中GPU快不行的几个标志,看下有没有你遇到的

GPU是处理图形密集型任务的主要组件。尽管它非常耐用,但它最终会磨损并开始失效。在到达生命的终结之前,它通常会显示出即将发生故障的迹象,需要及时修复或更换。本指南详细介绍了这些标志。 在我们开始之前 在深入研究GPU故障的迹象之前,重要的是要承认,下面提到的一些…

来!给我讲讲分库分表!

你好面试官&#xff0c;在我的理解中分库分表分为四个类型&#xff0c;垂直分表、垂直分库、水平分表、水平分库。接下来我会对这几个名词谈谈我的理解&#xff0c;具体如下&#xff1a; 垂直分表 1&#xff09;简单来说就是将原本的一张表切割成多张表。举个例子&#xff1a…

身份证OCR识别接口如何对接

身份证OCR识别接口又叫身份证识别API接口、身份证正反面文字识别接口&#xff0c;指的是传入身份证照片&#xff0c;精准识别静态身份证图像上的文字信息&#xff0c;如果传的是正面照片只返回正面信息&#xff0c;如果传的是反面只返回反面信息。那么身份证OCR识别接口如何对接…

SD-WAN助力企业实现多分支互联

随着企业规模的扩大和业务的多样化&#xff0c;多分支互联已成为许多行业不可或缺的网络架构。SD-WAN&#xff08;软件定义广域网&#xff09;作为一种创新的网络解决方案&#xff0c;以其独特的优势&#xff0c;正在助力企业实现更高效、智能和安全的多分支互联。 一、SD-WAN的…

应用层协议之 DNS 协议

DNS 就是一个域名解析系统。域名就是网址&#xff0c;类似于 www.baidu.com。网络上的服务器想要访问它&#xff0c;就得需要它对应的 IP 地址&#xff0c;同时&#xff0c;每个域名对对应着一个 / N个 IP 地址&#xff08;即对应多台服务器&#xff09;。 因此&#xff0c;为了…

解决uniapp软键盘弹起导致页面fixed定位元素被顶上去

在移动端开发中通常导航栏需要固定在页面的最顶端&#xff0c;但当页面中有输入框且dom元素较多时&#xff0c;点击输入框弹出软键盘会促使导航栏往上移 正常情况如图一所示&#xff0c;软键盘弹起如图二所示 图一 图二 解决办法 1&#xff09;给输入框添加 :adjust-position…

本地搭建hydra服务用go以验证oidc流程

目录 1、docker搭建hydra&#xff0c;环境配置&#xff1a; 2、搭建完成后服务调用&#xff1a; 2.1保证服务正常启动&#xff1a; 2.2 通过postman调用&#xff0c;获取client_id&#xff1a; 2.3 通过client_id&#xff0c;实现oauth2/auth调用 3. 通过go语言实现oidc验…

一套C语言开发的 PACS影像系统源码 PACS系统的基本概念、系统业务流程

PACS系统基本概念 PACS&#xff0c;全称 Picture Archiving and Communication Systems&#xff0c;中文意为影像归档和通信系统。它是应用于医院影像科室的一种系统&#xff0c;主要任务是把日常产生的各种医学影像&#xff08;包括核磁&#xff0c;CT&#xff0c;超声&#…

怎么将文字做成二维码?文本活码在线的生成技巧

文本类型的活码该如何来制作呢&#xff1f;通过二维码来展示文字信息是现在很常用的一种方式&#xff0c;包括物品、建筑、人员等方面的信息&#xff0c;都可以用生成二维码后让其他人通过扫码了解自己需要的信息。那么文本活码的制作需要几步操作呢&#xff1f;下面就教大家使…

CPU的星际穿越——“三维”解析“二维”之谜

文章目录 写在前面为什么三维的CPU能执行二维的指令二维指令是三维机器的抽象而已计算机所有东西都是三维的降维抽象没有软件没有指令二维到三维的总结操作系统的重塑 写在前面 以下是自己关于CPU为何能执行指令的迷惑的抽丝破茧的解答—— 困扰我的一个的问题之CPU的星际穿越…

RV1106点亮1.44寸SPI接口tftlcd

最近入手了一块微雪的幸狐RV1106微型Linux开发板&#xff0c;具体型号为Luckfox Pico Max&#xff0c;这是一款集成ARM Cortex-A7/RISC-V MCU/NPU/ISP等处理器。 根据微雪官网的wiki入门指导测试了一下&#xff0c;功能一切正常&#xff0c;感觉很nice&#xff0c;这款板子真的…

kubernate 基本概念

一 K8S 是什么&#xff1f; K8S 全称&#xff1a;Kubernetes 作用&#xff1a; 用于自动部署、扩展和管理“容器化&#xff08;containerized&#xff09;应用程序”的开源系统。 可以理解成 K8S 是负责自动化运维管理多个容器化程序&#xff08;比如 Docker&#xff09;的…