Spring Security 6 系列之九 - 集成JWT

之所以想写这一系列,是因为之前工作过程中使用Spring Security,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0,关键是其风格和内部一些关键Filter大改,导致在配置同样功能时,多费了些手脚,因此花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。

注意由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,所有代码都在spring-security-study项目上:https://github.com/forever1986/spring-security-study.git

目录

  • 1 JWT
    • 1.1 为什么使用JWT
    • 1.2 JWT格式
  • 2 Spring Security集成JWT
    • 2.1 项目新建
    • 2.2 返回异常处理类
    • 2.3 自定义基于数据库的用户
    • 2.4 Redis配置
    • 2.5 自定义登录和登出接口
    • 2.6 JWT配置
    • 2.7 SecurityConfig的配置
    • 2.8 测试

上两章中,我们讲了异常处理和前后端分离,其实都是为了本章做准备的。在系列6中我们说很多业务已经开始摒弃Session,而是采用JWT方式。这一章就从JWT开始讲,到最后实现一个Spring Security+JWT完整方案。

1 JWT

1.1 为什么使用JWT

最开始的前后端分离之间的交互是通过保持状态的Session方式,也就服务器通过生成Session信息保存在服务器中并返回给客户端,客户端(通常是浏览器)将信息存入Cookie里面,每次访问都是拿着Cookie访问(一般浏览器自动完成),服务器通过客户端传输的信息与服务器的Session进行比对验证。因为Session属于有状态的,因此有这么两个大缺点:

  • 缺点一:服务器要存储Session信息,如果并发量较大,对于服务器来说需要占用很大内存
  • 缺点二:Session信息默认存储在服务器上,这对于集群部署来说就不适用,还需要一个同一个存储的地方,比如Redis(系列6中已经讲过)

基于以上两个缺点,JWT就出现了。

  • JWT是一种无状态,也就是认证的时候,服务器给客户端一个token之后,服务器无需保留token
  • 由于无状态,因此解决集群部署问题,客户端携带token的访问,在任何服务器都可以鉴权
  • JWT包括3部分:header(声明和加密算法)、Payload(有效数据)、Signature(签名,是整个数据的认证信息,保证认证安全性和有效性)。

1.2 JWT格式

JWT 包含三部分数据:

Header:头部,通常头部有两部分信息:
声明类型(typ),这里是JWT
加密算法(alg),自定义

这部分会采用 Base64编码,得到数据。

Payload:载荷,就是设置想要存储的数据,你可以将部分信息放在token中,以下是规范中的内置对象:

  • iss (issuer):表示签发人
  • exp (expiration time):表示token过期时间
  • sub (subject):内容,可以使用json放置用户基本信息
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

这部分会采用 Base64编码,得到数据。

Signature:签名,是对数据进行认证的签名。根据前两步的数据+密钥 secret(密码可以是对称或非对称),通过 Header 中配置的加密算法(alg)进行验证。用于验证整个数据完整和可靠性。

以下是一个JWT数据,可以使用在线JWT解析工具解析:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJkMDk0MDBmODY5ZmY0MjgxOGYxOTFjZTE4ZDEwNjU2ZiIsInN1YiI6InVzZXI6NCIsImlzcyI6InNwcmluZy1zZWN1cml0eS1zdHVkeSIsImlhdCI6MTczNDQ4ODkzNywiZXhwIjoxNzM0NDg5MjM3fQ.X9V8DgX8ay8vQRLdA7tV8-MHKJxPv9e2gLqDs3Fpk9s

2 Spring Security集成JWT

代码参考lesson09子模块

本项目说明

  • 本项目依赖mysq和Redis,其中mysq中创建一个数据库,名为spring_security_study,创建用户表t_user(参考系列2)
  • 采用基于数据库用户的认证
  • 采用前后端分离,会自定义登录和登出接口,同时会屏蔽默认登录登出页面
  • 使用JWT生成token认证,因此会屏蔽默认Session管理,由于没有Session管理,因此我们需要自己做token管理。这里使用Redis存储token,同时自定义JwtAuthenticationTokenFilter过滤器,用于在访问时验证token,设置SecurityContext。

2.1 项目新建

1)新建lesson09子模块,其pom引入依赖如下:

<dependencies>
        <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>
        <!--Spring Boot 提供的 Security 启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
    </dependencies>

2)创建com.demo.lesson09包,并创建启动类SecurityLesson09Application

3)在controller包下,新增DemoController,用于测试

@RestController
public class DemoController {

    @GetMapping("/demo")
    public String demo() {
        return"demo";
    }

}

4)在resources下新增yaml,配置Mysql、redis和mybatis配置

server:
  port: 8080
spring:
  # 配置数据源
  datasource:
  	type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/spring_security_study?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: root
    druid:
      initial-size: 5
      min-idle: 5
      maxActive: 20
      maxWait: 3000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: false
      filters: stat,wall,slf4j
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;socketTimeout=10000;connectTimeout=1200

  #redis配置
  data:
    redis:
      host: 127.0.0.1

mybatis-plus:
  global-config:
    banner: false
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.demo.lesson05.entity
  configuration:
    cache-enabled: false
    local-cache-scope: statement

2.2 返回异常处理类

1)增加result包和handler包,里面的类参考lesson07子模块,拷贝以下类:

  • IResultCode
  • Result
  • ResultCode
  • DemoAccessDeniedHandler : 授权异常处理
  • DemoAuthenticationEntryPoint :认证异常处理

2.3 自定义基于数据库的用户

1)不清楚的可以回顾lesson03子模块,这里做了一些小改动

2) 在entity包下新增LoginUserDetails类,自定义类为了能够Redis序列化,之前使用Security默认的User,在Redis反序列化会报错

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginUserDetails implements UserDetails {

    private TUser tUser;

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

    @Override
    public String getPassword() {
        return tUser.getPassword();
    }

    @Override
    public String getUsername() {
        return tUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

3)定义entity包下的TUser和对应mapper包下的TUserMapper

@Data
public class TUser {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String username;

    private String password;

    private String email;

    private String phone;

}
@Mapper
public interface TUserMapper {

    // 根据用户名,查询用户信息
    @Select("select * from t_user where username = #{username}")
    TUser selectByUsername(String username);

}

4)在service包下,建立JdbcUserDetailsServiceImpl(实现UserDetailsService),用于基于数据库查询用户

@Service
public class JdbcUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private TUserMapper tUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询自己数据库的用户信息
        TUser user = tUserMapper.selectByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException(username);
        }
        return new LoginUserDetails(user);
    }

}

2.4 Redis配置

1)在config包下,新增RedisConfiguration,用于Redis的序列化配置

@Configuration
public class RedisConfiguration {

    /**
     * 主要做redis配置。redis有2种不同的template(2种的key不能共享)
     * 1.StringRedisTemplate:以String作为存储方式:默认使用StringRedisTemplate,其value都是以String方式存储
     * 2.RedisTemplate:
     *    1)使用默认RedisTemplate时,其value都是根据jdk序列化的方式存储
     *    2)自定义Jackson2JsonRedisSerializer序列化,以json格式存储,其key与StringRedisTemplate共享,返回值是LinkedHashMap
     *    3)自定义GenericJackson2JsonRedisSerializer序列化,以json格式存储,其key与StringRedisTemplate共享,返回值是原先对象(因为保存了classname)
     */
    @Bean
    @ConditionalOnMissingBean({RedisTemplate.class})
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(factory);
        //本实例采用GenericJackson2JsonRedisSerializer
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    @ConditionalOnMissingBean({StringRedisTemplate.class})
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(factory);
        return template;
    }

}

2.5 自定义登录和登出接口

1)在entity包下定义LoginDTO类,用于前后端参数传输

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginDTO {

    private String username;
    private String password;
}

2)在service包下,定义LoginService接口以及实现类LoginServiceImpl,做登录和登出逻辑

public interface LoginService {

    Result<String> login(LoginDTO loginDTO);

    Result<String> logout();
}
@Service
public class LoginServiceImpl implements LoginService {

    // 注入AuthenticationManagerBuilder,用于获得authenticationManager
    @Autowired
    private AuthenticationManagerBuilder authenticationManagerBuilder;

    @Autowired
    private RedisTemplate redisTemplate;

    private static String PRE_KEY = "user:";

    @Override
    public Result<String> login(LoginDTO loginDTO) {
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        try {
            Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
            if(authentication!=null && authentication.isAuthenticated()){
                SecurityContextHolder.getContext().setAuthentication(authentication);
                LoginUserDetails user = (LoginUserDetails)authentication.getPrincipal();
                String subject = PRE_KEY + user.getTUser().getId();
                String token = JwtUtil.createToken(subject, 1000*60*5L);
                redisTemplate.opsForValue().set(subject, user, 1000*60*5L, TimeUnit.MILLISECONDS);
                return Result.success(token);
            }
        }catch (AuthenticationException e){
            return Result.failed(e.getLocalizedMessage());
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return Result.failed("认证失败");
    }

    @Override
    public Result<String> logout() {
        if(SecurityContextHolder.getContext().getAuthentication()!=null){
            LoginUserDetails user = (LoginUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if(user!=null){
                String key = PRE_KEY + user.getTUser().getId();
                redisTemplate.delete(key);
            }else {
                return Result.failed("登出失败,用户不存在");
            }
        }
        return Result.success("登出成功");
    }
}

3)在controller包下,新增LoginController ,对外发布登录和登出

@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public Result<String> login(@RequestBody LoginDTO loginDTO) {
        return loginService.login(loginDTO);
    }

    @PostMapping("/logout")
    public Result<String> logout() {
        return loginService.logout();
    }

}

2.6 JWT配置

1)在utils包下面新建JwtUtil工具类,用来生成token和验证token。

这里采用HS256对称加密,密钥我们这里就直接写在代码中,有兴趣朋友可以改为如AES非对称加密算法

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000L; // 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "moo";

    public static String getUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    /**
     * 创建token
     */
    public static String createToken(String subject) {
        return getJwtBuilder(subject, null, getUUID());// 设置过期时间
    }

    /**
     * 创建token
     */
    public static String createToken(String subject, Long ttlMillis) {
        return getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
    }

    /**
     * 创建token
     */
    public static String createToken(String id, String subject, Long ttlMillis) {
        return getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
    }

    /**
     * 解析token
     */
    public static String parseJWT(String token) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody().getSubject();
    }

    private static String getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setId(uuid)            //唯一的ID
                .setSubject(subject)    // 主题  可以是JSON数据
                .setIssuer("spring-security-study")       // 签发者
                .setIssuedAt(now)       // 签发时间
                .signWith(signatureAlgorithm, secretKey)    //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate)
                .compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     */
    private static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

}

2)定义自己的Filter(JwtAuthenticationTokenFilter),通过该Filter进行token验证,并模拟设置SecurityContext。

我们知道访问资源最终在AuthorizationFilter过滤器会验证你的权限,是通过获取SecurityContext得到登录信息
但是使用前后端分离,且不使用Session管理,这就意味着登录之后,第二次请求Security会认为没有登录(这个可以去看看SecurityContextHolderFilter,会发现不再使用HttpSessionSecurityContextRepository)这也就意味着SecurityContext不会被设置
从Security Context原理中我们知道每个请求都是通过Filter过滤链验证,那么只需要我们在链中加入自己的认证,并设置SecurityContext,就能够达到一样的效果。
注意

  • 这里JwtAuthenticationTokenFilter 继承OncePerRequestFilter 而不是GenericFilterBean,只是为了保证每次请求只走一次JwtAuthenticationTokenFilter
  • JWT验证失败返回BadCredentialsException异常,这个在自定义异常处理讲过其原理,通过抛出BadCredentialsException可以被exceptionHandling处理
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 过滤login接口
        if("/login".equals(request.getRequestURI())){
            filterChain.doFilter(request, response);
            return;
        }

        // 从请求头获取token
        String token = request.getHeader("access_token");
        // 检查获取到的token是否为空或空白字符串。(判断给定的字符串是否包含文本)
        if (!StringUtils.hasText(token)) {
            // 如果token为空,则直接放行请求到下一个过滤器,不做进一步处理并结束当前方法,不继续执行下面代码。
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userAccount;
        try {
            userAccount = JwtUtil.parseJWT(token);
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadCredentialsException("token非法");
        }
        // 临时缓存中 获取 键 对应 数据
        Object object = redisTemplate.opsForValue().get(userAccount);
        LoginUserDetails loginUser = (LoginUserDetails)object;
        if (Objects.isNull(loginUser)) {
            throw new BadCredentialsException("用户未登录");
        }
        // 将用户信息存入 SecurityConText
        // UsernamePasswordAuthenticationToken 存储用户名 密码 权限的集合
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        // SecurityContextHolder是Spring Security用来存储当前线程安全的认证信息的容器。
        // 将用户名 密码 权限的集合存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }
}

2.7 SecurityConfig的配置

1)在config包下,配置SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth->auth
                        //允许/login访问
                        .requestMatchers("/login").permitAll().anyRequest().authenticated())
                // 异常处理
                .exceptionHandling(handling-> handling
                        .accessDeniedHandler(new DemoAccessDeniedHandler())
                        .authenticationEntryPoint(new DemoAuthenticationEntryPoint()))
                // 禁用csrf,因为登录和登出是post请求,csrf会屏蔽掉post请求
                .csrf(AbstractHttpConfigurer::disable)
                // 添加到过滤器链路中,确保在AuthorizationFilter过滤器之前
                .addFilterBefore(jwtAuthenticationTokenFilter, AuthorizationFilter.class)
                // 由于采用token方式认证,因此可以关闭session管理
                .sessionManagement(SessionManagementConfigurer::disable)
                // 禁用原来登录页面
                .formLogin(AbstractHttpConfigurer::disable)
                // 禁用系统原有的登出
                .logout(LogoutConfigurer::disable);
        return http.build();
    }
}

2.8 测试

1)直接访问:http://127.0.0.1:8080/demo 会给出一个无权限的返回Json数据
在这里插入图片描述
2)访问登录接口:http://127.0.0.1:8080/login
在这里插入图片描述

3)再次访问:http://127.0.0.1:8080/demo ,注意加入第二步中的token
在这里插入图片描述

4)访问登出:http://127.0.0.1:8080/logout,注意加入第二步中的token
在这里插入图片描述
5)再次:http://127.0.0.1:8080/demo ,注意加入第二步中的token
在这里插入图片描述

结语:至此,我们就完整实现了基于JWT的Spring Security认证过程。这实际上已经接近生产使用情况。到目前为止,我们了解了认证、授权、前后端分离、JWT等内容,接下来,我们将会讲述Spring Security更高级的一些功能。

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

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

相关文章

【Go】context标准库

文章目录 1. 概述1.1 什么是 Context1.2 设计原理1.3 使用场景1.4 Context 分类核心:Context接口2. 源码解读4个实现emptyCtxTODO 和 BackgroundcancelCtxWithCancelcancelCtx.propagateCancel 构建父子关联parentCancelCtx 获取父上下文中的内嵌cancelCtxcanceltimerCtxWithT…

Windows和Linux安全配置和加固

一.A模块基础设施设置/安全加固 A-1.登录加固 1.密码策略 a.最小密码长度不少于8个字符&#xff0c;将密码长度最小值的属性配置界面截图。 练习用的WindowsServer2008,系统左下角开始 > 管理工具 > 本地安全策略 > 账户策略 > 密码策略 > 密码最小长度&#…

webrtc-internals调试工具

Google 的 Chrome&#xff08;87 或更高版本&#xff09;WebRTC 内部工具是一套内置于 Chrome 浏览器中的调试工具; webrtc-internals 能够查看有关视频和音频轨道、使用的编解码器以及流的一般质量的详细信息。这些知识对于解决音频和视频质量差的问题非常有帮助。 webrtc-int…

MT6765核心板_MTK6765安卓核心板规格参数_联发科MTK模块开发

MTK6765安卓核心板是基于联发科高效八核处理器平台开发的一款强大硬件解决方案。这款核心板的核心是采用12纳米工艺打造的MTK6765 CPU&#xff0c;具备四个主频高达2.3GHz的CORTEX-A53核心和四个主频为1.8GHz的CORTEX-A53核心&#xff0c;提供了卓越的处理性能。用户可以根据需…

Linux Shell 脚本编程基础知识篇—shell 运算命令详解

ℹ️大家好&#xff0c;我是练小杰&#xff0c;本文继续Linux shell脚本编程的基础知识内容&#xff0c;接着讲算术运算命令的详细操作~~ 复习&#xff1a;【shell简介以及基本操作】 更多Linux 相关内容请点击&#x1f449;“Linux专栏”~ 文章目录 let运算命令的用法let 的高…

Nginx单向链表 ngx_list_t

目录 基本概述 数据结构 接口描述 具体实现 ngx_list_create ngx_list_init ngx_list_push 使用案例 整理自 nginx 1.9.2 源码 和 《深入理解 Nginx&#xff1a;模块开发与架构解析》 基本概述 Nginx 中的 ngx_list_t 是一个单向链表容器&#xff0c;链表中的每一个节…

KVM虚拟机管理脚本

思路&#xff1a; 在/opt/kvm下创建一个磁盘文件&#xff0c;做差异镜像&#xff0c;创建一个虚拟机配置文件&#xff0c;做虚拟机模版 [rootnode01 ~]# ls /opt/kvm/ vm_base.qcow2 vm_base.xml创建虚拟机的步骤&#xff1a;首先创建虚拟机的差异镜像&#xff0c;然后复制虚…

芯片Tapeout power signoff 之IR Drop Redhawk Ploc文件格式及其意义

数字IC后端工程师在芯片流程最后阶段都会使用redhawk或voltus进行设计的IR Drop功耗signoff分析。必须确保静态&#xff0c;动态ir drop都符合signoff标准。 在做redhawk ir drop分析前&#xff0c;我们需要提供一个redhawk ploc供电点坐标。 数字IC设计后端实现前期预防IR D…

流批一体向量化计算引擎 Flex 在蚂蚁的探索和实践

编者按&#xff1a;Flex是蚂蚁数据部自研的一款流批一体的向量化引擎&#xff0c;Flex是Fink和Velox的全称&#xff0c;也是Flexible的前缀&#xff0c;被赋予了灵活可插拔的寓意。本文将重点从向量化技术背景、Flex架构方案和未来规划三个方面展开论述。 作者介绍&#xff1a;…

Pytorch | 利用I-FGSSM针对CIFAR10上的ResNet分类器进行对抗攻击

Pytorch | 利用I-FGSSM针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集I-FGSSM介绍I-FGSSM代码实现I-FGSSM算法实现攻击效果 代码汇总ifgssm.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器&#xff1a; Pytorch | 从零构建AlexNet对CIFAR10进行分类 Pytorch…

全面Kafka监控方案:从配置到指标

文章目录 1.1.监控配置1.2.监控工具1.3.性能指标系统相关指标GC相关指标JVM相关指标Topic相关指标Broker相关指标 1.4.性能指标说明1.5.重要指标说明 1.1.监控配置 开启JMX服务端口&#xff1a;kafka基本分为broker、producer、consumer三个子项&#xff0c;每一项的启动都需要…

HTML制作一个普通的背景换肤案例2024版

一&#xff0c;完整的代码&#xff1a; <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>换肤</t…

《计算机组成及汇编语言原理》阅读笔记:p86-p115

《计算机组成及汇编语言原理》学习第 6 天&#xff0c;p86-p115 总结&#xff0c;总计 20 页。 一、技术总结 1.if statement 2.loop 在许多编程语言中&#xff0c;有类种循环&#xff1a;一种是在程序开头检测条件(test the condition),另一种是在程序末尾检测条件。 3.C…

CSS(三)盒子模型

目录 Content Padding Border Margin 盒子模型计算方式 使用 box-sizing 属性控制盒子模型的计算 所有的HTML元素都可以看作像下图这样一个矩形盒子&#xff1a; 这个模型包括了四个区域&#xff1a;content&#xff08;内容区域&#xff09;、padding&#xff08;内边距…

MySQL外键类型与应用场景总结:优缺点一目了然

前言&#xff1a; MySQL的外键简介&#xff1a;在 MySQL 中&#xff0c;外键 (Foreign Key) 用于建立和强制表之间的关联&#xff0c;确保数据的一致性和完整性。外键的作用主要是限制和维护引用完整性 (Referential Integrity)。 主要体现在引用操作发生变化时的处理方式&…

MySQL从入门到入土---MySQL表的约束 (内含实践)---详细版

目录 引入&#xff1a; null 与not null default&#xff1a; comment列描述 &#xff1a; not null 和 default&#xff1a; zerofill &#xff1a; 主键&#xff1a;primary key 复合主键&#xff1a; 自增长:auto_increment 唯一键&#xff1a;unique key 外键&a…

基于NodeMCU的物联网窗帘控制系统设计

最终效果 基于NodeMCU的物联网窗帘控制系统设计 项目介绍 该项目是“物联网实验室监测控制系统设计&#xff08;仿智能家居&#xff09;”项目中的“家电控制设计”中的“窗帘控制”子项目&#xff0c;最前者还包括“物联网设计”、“环境监测设计”、“门禁系统设计计”和“小…

Webpack在Vue CLI中的应用

webpack 作为目前最流行的项目打包工具&#xff0c;被广泛使用于项目的构建和开发过程中&#xff0c;其实说它是打包工具有点大材小用了&#xff0c;我个人认为它是一个集前端自动化、模块化、组件化于一体的可拓展系统&#xff0c;你可以根据自己的需要来进行一系列的配置和安…

java日志框架:slf4j、jul(java.util.logging)、 log4j、 logback

SLF4J--抽象接口 SLF4J (Simple Logging Facade for Java) 是一个为各种 Java 日志框架提供简单统一接口的库。它的主要目的是将应用程序代码与具体的日志实现解耦&#xff0c;使得在不修改应用程序代码的情况下&#xff0c;可以轻松地切换不同的日志框架。 jul-to-slft4j.ja…

命令行之巅:Linux Shell编程的至高艺术(中)

文章一览 前言一、输入/输出及重定向命令1.1 输入/输出命令1.1.1 read命令1.1.2 echo命令 1.2 输入/输出重定向1.3 重定向深入讲解1.4 Here Document1.4.1 /dev/null 文件 二、shell特殊字符和命令语法2.1 引号2.1.1 双引号2.1.2 单引号2.1.3 倒引号 2.2 注释、管道线和后台命令…