Spring Security+jwt+redis+自定义认证逻辑 权限控制

news2024/12/24 11:40:46

Spring Security+jwt+redis+自定义认证逻辑 权限控制

1.拦截访问基本思路

在这里插入图片描述

2.创建数据库表:角色表(应该6个表,这里只用用户表代替角色表)、权限表、路径表、角色-权限表、权限-路径表

/*
SQLyog Professional v12.14 (64 bit)
MySQL - 5.7.40 : Database - assist_silkworm_db
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`assist_silkworm_db` /*!40100 DEFAULT CHARACTER SET latin1 */;

USE `assist_silkworm_db`;

/*Table structure for table `t_path` */
# 路径表
DROP TABLE IF EXISTS `t_path`;

CREATE TABLE `t_path` (
  `path_id` bigint(19) NOT NULL COMMENT '路径表id',
  `path` varchar(200) NOT NULL COMMENT '路径名称',
  `gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`path_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

/*Data for the table `t_path` */

insert  into `t_path`(`path_id`,`path`,`gmt_created`,`gmt_modified`) values 

(1607234202607067138,'/**','2023-02-23 12:07:42','2023-02-23 12:12:47');

/*Table structure for table `t_permission` */

DROP TABLE IF EXISTS `t_permission`;
# 权限表
CREATE TABLE `t_permission` (
  `permission_id` bigint(19) NOT NULL COMMENT '主键ID',
  `permission` varchar(255) NOT NULL COMMENT '权限',
  `gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`permission_id`),
  UNIQUE KEY `role` (`permission`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `t_permission` */

insert  into `t_permission`(`permission_id`,`permission`,`gmt_created`,`gmt_modified`) values 

(1607234202607067138,'ROLE_BIG_ADMIN','2023-02-23 12:10:07','2023-02-23 12:10:07');

/*Table structure for table `t_permission_path` */
# 权限路径表
DROP TABLE IF EXISTS `t_permission_path`;

CREATE TABLE `t_permission_path` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `permission_id` bigint(19) NOT NULL COMMENT '权限Id',
  `path_id` bigint(19) NOT NULL COMMENT '权限对应的路径',
  `gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

/*Data for the table `t_permission_path` */

insert  into `t_permission_path`(`id`,`permission_id`,`path_id`,`gmt_created`,`gmt_modified`) values 

(1,1607234202607067138,1607234202607067138,'2023-02-23 12:13:59','2023-02-23 12:13:59');

/*Table structure for table `t_user` */
# 用户表
DROP TABLE IF EXISTS `t_user`;

CREATE TABLE `t_user` (
  `user_id` bigint(19) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(100) NOT NULL COMMENT '账号',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_del` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1607234202607067139 DEFAULT CHARSET=utf8;

/*Data for the table `t_user` */

insert  into `t_user`(`user_id`,`username`,`password`,`gmt_created`,`gmt_modified`,`is_del`) values 

(1607234202607067138,'admin','$2a$10$GE0hWDRIksPXpZCDtTEFP.8EKi25OQ8PPvc6Q14YzSyzpkkQzDPxW','2022-12-26 12:37:50','2023-02-23 12:09:05',0);

/*Table structure for table `t_user_permission` */

DROP TABLE IF EXISTS `t_user_permission`;
# 用户权限表
CREATE TABLE `t_user_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(19) NOT NULL COMMENT '用户Id',
  `permission_id` bigint(19) NOT NULL COMMENT '权限Id',
  `gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

/*Data for the table `t_user_permission` */

insert  into `t_user_permission`(`id`,`user_id`,`permission_id`,`gmt_created`,`gmt_modified`) values 

(2,1607234202607067138,1607234202607067138,'2023-02-23 12:13:06','2023-02-23 12:13:06');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

3.导入依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.4</version>
</dependency>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J2aKwzdu-1677162858470)(SpringSecurity%20+%20JWT-%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6.assets/image-20230223222442753.png)]

###4.创建实现UserDestails的实现类

package com.rfos.assistsilkworm.config.security;

import com.rfos.assistsilkworm.pojo.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author hjt
 * @date 2022/10/4 20:55
 * @description
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetailsDTO implements UserDetails {

    private User user;

    private List<String> permissionList;

    private Collection<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        for (String permission : permissionList) {
            //拥有的权限名
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
            grantedAuthorityList.add(simpleGrantedAuthority);
        }
        setAuthorities(grantedAuthorityList);
        return grantedAuthorityList;
    }

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

    @Override
    public String getUsername() {
        return user.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;
    }
}


UserDetails类:

UserDetails(位于org.springframework.security.core.userdetails包下)主要和用户信息有关的接口,该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication中去

public interface UserDetails extends Serializable {
	
	// 权限
	// 用户的权限集, 默认需要添加ROLE_ 前缀
    Collection<? extends GrantedAuthority> getAuthorities();
	// 密码
	// 用户的加密后的密码, 不加密会使用{noop}前缀
    String getPassword();
	// 用户名
    String getUsername();
	// 帐户未过期
    boolean isAccountNonExpired();
    // 帐户未锁定
    boolean isAccountNonLocked();
	// 凭证是否过期
    boolean isCredentialsNonExpired();
	// 用户是否可用
    boolean isEnabled();
}

角色表等各个表对应的实体类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ToXl9gvR-1677162858471)(SpringSecurity%20+%20JWT-%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6.assets/image-20230223222708838.png)]

package com.rfos.assistsilkworm.pojo;

import com.baomidou.mybatisplus.annotation.*;

import java.util.Date;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * <p>
 * 
 * </p>
 *
 * @author rfos
 * @since 2023-02-23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("t_user")
@ApiModel(value="User对象", description="")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主键")
      @TableId(value = "user_id", type = IdType.ASSIGN_ID)
    private Long userId;

    @ApiModelProperty(value = "账号")
    private String username;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty("创建时间")//创建时间由SQL完成
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreated;

    @ApiModelProperty("更新时间")//更新时间由数据库完成
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;

    @ApiModelProperty(value = "是否删除")
    private Boolean isDel;


}
package com.rfos.assistsilkworm.pojo;

import com.baomidou.mybatisplus.annotation.*;

import java.util.Date;
import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import springfox.documentation.annotations.ApiIgnore;

/**
 * <p>
 * 
 * </p>
 *
 * @author rfos
 * @since 2023-02-23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("t_path")
@ApiModel(value="Path对象", description="")
public class Path implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "路径表id")
      @TableId(value = "path_id", type = IdType.ASSIGN_ID)
    private Long pathId;

    @ApiModelProperty(value = "路径名称")
    private String path;

    @ApiModelProperty("创建时间")//创建时间由SQL完成
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreated;

    @ApiModelProperty("更新时间")//更新时间由数据库完成
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;


}

package com.rfos.assistsilkworm.pojo;

import com.baomidou.mybatisplus.annotation.*;

import java.util.Date;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * <p>
 * 
 * </p>
 *
 * @author rfos
 * @since 2023-02-23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("t_permission")
@ApiModel(value="Permission对象", description="")
public class Permission implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主键ID")
      @TableId(value = "permission_id", type = IdType.ASSIGN_ID)
    private Long permissionId;

    @ApiModelProperty(value = "权限")
    private String permission;

    @ApiModelProperty("创建时间")//创建时间由SQL完成
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreated;

    @ApiModelProperty("更新时间")//更新时间由数据库完成
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;


}

package com.rfos.assistsilkworm.pojo;

import com.baomidou.mybatisplus.annotation.*;

import java.util.Date;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * <p>
 * 
 * </p>
 *
 * @author rfos
 * @since 2023-02-23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("t_permission_path")
@ApiModel(value="PermissionPath对象", description="")
public class PermissionPath implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主键ID")
      @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "权限Id")
    private Long permissionId;

    @ApiModelProperty(value = "权限对应的路径")
    private Long pathId;

    @ApiModelProperty("创建时间")//创建时间由SQL完成
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreated;

    @ApiModelProperty("更新时间")//更新时间由数据库完成
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;


}

package com.rfos.assistsilkworm.pojo;

import com.baomidou.mybatisplus.annotation.*;

import java.util.Date;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * <p>
 * 
 * </p>
 *
 * @author rfos
 * @since 2023-02-23
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("t_user_permission")
@ApiModel(value="UserPermission对象", description="")
public class UserPermission implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主键ID")
      @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "用户Id")
    private Long userId;

    @ApiModelProperty(value = "权限Id")
    private Long permissionId;

    @ApiModelProperty("创建时间")//创建时间由SQL完成
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreated;

    @ApiModelProperty("更新时间")//更新时间由数据库完成
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;


}


5.创建自定义认证逻辑-UserDetailsService的实现类-UserDetailServiceImpl类

UserDetailsService接口

package org.springframework.security.core.userdetails;
public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

UserDetailServiceImpl类

package com.rfos.assistsilkworm.config.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rfos.assistsilkworm.pojo.Permission;
import com.rfos.assistsilkworm.pojo.User;
import com.rfos.assistsilkworm.pojo.UserPermission;
import com.rfos.assistsilkworm.service.PermissionService;
import com.rfos.assistsilkworm.service.UserPermissionService;
import com.rfos.assistsilkworm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author hjt
 * @date 2022/10/5 11:24
 * @description
 */
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
    //用户业务
    @Autowired
    private UserService userService;
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private UserPermissionService userPermissionService;

    /*
    重写方法:
    username: 需要其数据的用户的用户名
    UsernameNotFoundException: 找不到用户的异常
    */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //获取用户并判断是否存在
        User user = userService.getByName(username);
        if(ObjectUtils.isEmpty(user)){
            return null;
        }
        //封装用户信息至对应的认证类中
        UserDetailsDTO userDetailsDTO = new UserDetailsDTO();
        userDetailsDTO.setUser(user);
        //TODO 获取角色对应的权限
        List<UserPermission> userPermissions = userPermissionService.list(new QueryWrapper<UserPermission>().eq("user_id", user.getUserId()));
        //TODO 获取权限id对应的所有权限
        List<String> pathList = new ArrayList<>();
        for (UserPermission userPermission : userPermissions) {
            List<Permission> permissionList = permissionService.list(new QueryWrapper<Permission>().eq("permission_id", userPermission.getPermissionId()));
            pathList.addAll(permissionList.stream().map(data-> data.getPermission()).collect(Collectors.toList()));
        }
        userDetailsDTO.setPermissionList(pathList);
        return userDetailsDTO;
    }

}

6.JwtUtil工具类说明

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

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

    public static String getUUID() {
        //随机生成token字符串
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder 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()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);//设置过期时间
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

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

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

7.,创建过滤器JwtAuthenticationTokenFilter类拦截指定路径验证访问信息

OncePerRequestFilter:继承OncePerRequestFilter用于继承实现并在每次请求时只执行一次过滤

package com.rfos.assistsilkworm.config.security;

import com.alibaba.fastjson.JSONObject;
import com.rfos.assistsilkworm.config.redis.RedisUtils;
import com.rfos.assistsilkworm.config.security.UserDetailsDTO;
import com.rfos.assistsilkworm.constant.CommonConstants;
import com.rfos.assistsilkworm.enums.ResultCode;
import com.rfos.assistsilkworm.exception.ApiException;
import com.rfos.assistsilkworm.util.IPAddrUtil;
import com.rfos.assistsilkworm.util.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;


/**
 * @date 2022/10/4
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtils redisUtil;
    //保证一次请求只调用一次doFilterInternal方法
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("用户访问requestURI={},ip={}", request.getRequestURI(), IPAddrUtil.getIpAddress(request));
        //获取请求头中的token
        String token = request.getHeader(CommonConstants.TOKEN_HEAD);
        //如果没有token则为第一次访问,过滤器放行-将请求转发给过滤器链上下一个对象
        if (!StringUtils.hasText(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        log.info("获取到请求的token:\n"+token);
        String userId;
        try {
            Claims claims = JwtUtils.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new ApiException(ResultCode.FAIL_AUTHORITY);
        }
        //从redis中获取用户信息
        Object obj = redisUtil.get(CommonConstants.TOKEN_HEAD + ":" + userId);
        if (Objects.isNull(obj)) {
            throw new ApiException(ResultCode.EMPTY_USER);
        }
        //获取认证对象的信息
        UserDetailsDTO userDetailsDTO = JSONObject.parseObject(obj.toString(), UserDetailsDTO.class);
        log.info("用户访问requestURI={},username={},ip={}", request.getRequestURI(), userDetailsDTO.getUsername(), IPAddrUtil.getIpAddress(request));
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        log.info("获取权限对应的所有权限:\n{}",userDetailsDTO.getAuthorities());
        /*
        UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
        所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),
        然后生成的Authentication会被交由AuthenticationManager来进行管理
        而AuthenticationManager管理一系列的AuthenticationProvider,
        而每一个Provider都会通UserDetailsService和UserDetail来返回一个
        以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication
        */
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetailsDTO, null, userDetailsDTO.getAuthorities());
        //将用户信息保存在安全上下文中
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }

}

8.创建自定义的UrlFilterInvocationSecurityMetadataSource类实现FilterInvocationSecurityMetadataSource接口

FilterInvocationSecurityMetadataSource:拦截url判断用户url的访问权限是否包含该用户的权限

package com.rfos.assistsilkworm.config.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rfos.assistsilkworm.pojo.Path;
import com.rfos.assistsilkworm.pojo.PermissionPath;
import com.rfos.assistsilkworm.service.PathService;
import com.rfos.assistsilkworm.service.PermissionPathService;
import com.rfos.assistsilkworm.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.security.access.SecurityConfig;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * @author hjt
 * @Package com.kdcloud.srd.security
 * @date 2022/10/5 20:14
 * @description 获取url匹配的用户权限
 *
 */
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    //没有权限
    private static final String ROLE_NONE = "ROLE_NONE";
    @Autowired
    private PermissionPathService permissionPathService;
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private PathService pathService;
    //路径匹配类
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    //开放的路径
    private final static String[] OPEN_PATH_LIST = new String[]{
            "/**",
            "/static/**",
            "/templates/**",
            "/index","/login",
            "/admin/login",
            "/home/**",
            "/user/login",
            "/swagger-ui.html",
            "/swagger-resources/**",
            "/*/api-docs",
            "/webjars/**",
            "/v2/**",
            "/api/**",
            "/actuator/**"
    };
    //filterInvocation:获取该请求
    @Override
    public Collection<ConfigAttribute> getAttributes(Object requestObj) throws IllegalArgumentException {
        FilterInvocation filterInvocation = (FilterInvocation) requestObj;
        String requestUrl = filterInvocation.getRequestUrl();
        System.out.println("请求路径url:"+requestUrl);
        //开放部分路径
        //静态资源不拦截
        if(isMatcherAllowedRequest(filterInvocation)){
            return null;
        }
        // 数据库中所有url
        List<Path> pathList = pathService.list();
        pathList.forEach(value-> System.out.println(value.getPath()));
        for (Path path : pathList) {
            // 获取该url所对应的权限
            boolean match = antPathMatcher.match(path.getPath(), requestUrl);
            System.out.println(match);
            if (match) {
                System.out.println("匹配的路径url\n:"+path);
                //获取路径对应的所有权限
                List<PermissionPath> permissionPaths = permissionPathService.list(new QueryWrapper<PermissionPath>().eq("path_id", path.getPathId()));
                List<String> permissionPathList = new ArrayList<>();
                if (!CollectionUtils.isEmpty(permissionPaths)){
                    for (PermissionPath permissionPath : permissionPaths) {
                        String roleName = permissionService.getById(permissionPath.getPermissionId()).getPermission();
                        permissionPathList.add(roleName);
                    }
                }

                // 能访问url对应角色权限信息
                System.out.println("能访问url对应角色权限信息:"+permissionPathList);
                return SecurityConfig.createList(permissionPathList.stream().toArray(String[]::new));
            }
        }
        // 如果数据中没有找到相应url资源则为非法访问
        return  SecurityConfig.createList(ROLE_NONE);
    }

    /**
     * 判断当前请求是否在允许请求的范围内
     * @param fi 当前请求
     * @return 是否在范围中
     */
    private boolean isMatcherAllowedRequest(FilterInvocation fi){
        return allowedRequest().stream().map(AntPathRequestMatcher::new)
                .filter(requestMatcher -> requestMatcher.matches(fi.getHttpRequest()))
                .toArray().length > 0;
    }
    /**
     * @return 定义允许请求的列表
     */
    private List<String> allowedRequest(){
        return Arrays.asList(OPEN_PATH_LIST);
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 表示返回的对象是否支持校验
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return FunctionalInterface.class.isAssignableFrom(aClass);
    }

}

9.决策访问处理器:创建自定义的JwtAccessDeniedManager实现AccessDeniedManager接口

package com.rfos.assistsilkworm.config.security;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @author hjt
 * @date 2022/10/5 23:29
 * @description决策访问处理器:创建自定义的JwtAccessDeniedManager实现AccessDeniedManager接口
 */
@Component
public class JwtAccessDeniedManager implements AccessDecisionManager {
    /**
     * auth:认证信息
     * collection:角色信息
     */
    @Override
    public void decide(Authentication auth, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> auths = auth.getAuthorities();
        for (ConfigAttribute configAttribute : collection) {
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
                    && auth instanceof UsernamePasswordAuthenticationToken) {
                return;
            }
            for (GrantedAuthority authority : auths) {
                if (configAttribute.getAttribute().equals(authority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

10.创建Spring Security的配置类securityConfig

package com.rfos.assistsilkworm.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启注解匹配
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    //自动装配自定义逻辑
    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;
    //过滤器JwtAuthenticationTokenFilter
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    //用户请求处理过程中遇到认证异常时,被ExceptionTranslationFilter用于开启特定认证方案(authentication schema)的认证流程
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    //拒绝访问处理
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    //决策处理器
    @Autowired
    private JwtAccessDeniedManager jwtAccessDeniedManager;
    //请求访问url处理过滤器
    @Autowired
    private UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;

    //密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    //加密密码
    public static void main(String[] args) {
        SpringSecurityConfig springSecurityConfig = new SpringSecurityConfig();
        PasswordEncoder passwordEncoder = springSecurityConfig.passwordEncoder();
        String encode = passwordEncoder.encode("admin");
        System.out.println("加密后的密码:"+encode);

    }
    //配置访问路径


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //请求需要经过权限认证
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                //自定义的元数据源和权限决策配置
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O obj) {
                        obj.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                        obj.setAccessDecisionManager(jwtAccessDeniedManager);
                        return obj;
                    }
                });
        //配置异常处理器 => 自定义的
        http.exceptionHandling()
                //配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
        //允许跨域
        http.cors();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用自己的UserDetailsService
        auth.userDetailsService(userDetailsService);
    }

}

11.创建登录接口,生成封装验证信息

package com.rfos.assistsilkworm.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.rfos.assistsilkworm.common.CommonResult;
import com.rfos.assistsilkworm.dto.LoginDTO;
import com.rfos.assistsilkworm.pojo.User;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author rfos
 * @since 2022-11-02
 */
public interface UserService extends IService<User> {

    /**
     * 管理员登录验证
     * @param loginDTO
     * @return
     */
    CommonResult login(LoginDTO loginDTO);

    User getByName(String username);
}

package com.rfos.assistsilkworm.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.util.StringUtil;
import com.rfos.assistsilkworm.common.CommonResult;
import com.rfos.assistsilkworm.config.redis.RedisUtils;
import com.rfos.assistsilkworm.config.security.UserDetailsDTO;
import com.rfos.assistsilkworm.constant.CommonConstants;
import com.rfos.assistsilkworm.constant.RedisConstants;
import com.rfos.assistsilkworm.dto.LoginDTO;
import com.rfos.assistsilkworm.enums.ResultCode;
import com.rfos.assistsilkworm.exception.ApiException;
import com.rfos.assistsilkworm.mapper.UserMapper;
import com.rfos.assistsilkworm.pojo.User;
import com.rfos.assistsilkworm.service.UserService;
import com.rfos.assistsilkworm.util.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author rfos
 * @since 2022-11-02
 */
@Service
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public CommonResult login(LoginDTO loginDTO) {
        //封装用户认证信息
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
            throw new ApiException("用户名或密码错误");
        }
        //使用username生成token令牌
        UserDetailsDTO userDetailsDTO = (UserDetailsDTO) authenticate.getPrincipal();
        String userId = String.valueOf(userDetailsDTO.getUser().getUserId());
        String jwt = JwtUtils.createJWT(userId);
        log.info("使用username生成token:\n"+jwt);
        //将认证信息authenticate存入redis
        log.info("authenticate存入redis:\n{}",userDetailsDTO);
        redisUtils.set(CommonConstants.TOKEN_HEAD +":"+ userId, JSONObject.toJSONString(userDetailsDTO));
        return CommonResult.success(jwt);
    }

    @Override
    public User getByName(String username) {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        if(StringUtil.isNotEmpty(username)){
            userQueryWrapper.eq("username",username);
            userQueryWrapper.eq("is_del", CommonConstants.IS_NOT_DEL);
        }
        User user = userMapper.selectOne(userQueryWrapper);
        return user;
    }


}

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

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

相关文章

Leetcode第450题删除二叉搜索树中的结点|C语言

题目&#xff1a; 给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤…

一个跟蘑菇结缘的企业老板

记得那是一个很久以前的一家公司了董事长办公室里中的大型盆栽里面长了一个蘑菇董事长认为是祥瑞每天都会浇水后来一个新来的保洁阿姨以为杂草啥的给他掰掉扔垃圾桶了董事长第二天来浇水的时候发现没了就问谁动了他的蘑菇问道之后就跑到楼道大垃圾桶那里把蘑菇找回来种在花盆里…

“点工”的觉悟,5年时间从7K到24K的转变,我的测试道路历程~

2015年7月我从一个90%以上的人都不知道的二本院校毕业&#xff08;新媒体专业&#xff09;&#xff0c;凭借自学的软件测试&#xff08;点点点&#xff09;在北京找到了一份月薪7000的工作&#xff0c;在当时其实还算不错&#xff0c;毕竟我的学校起点比较差&#xff0c;跟大部…

python学习笔记——csv文件

目录 一、csv文件和Excel文件区别 二、手动转换&#xff08;文本与列表&#xff09; ①普通的写(列表嵌套转成文本的表格形式) ②普通的读&#xff08;文本的表格形式转成列表嵌套&#xff09; 二、csv库-读 1、CSV库-读-reader() 2、CSV库-读-DictReader() 三、csv库-写 …

基于YOLO的酸枣病虫害检测识别实践

在我前面的博文中对于农作物病虫害的检测识别已经做过了&#xff0c;不过那个主要是针对水稻的&#xff0c;文章如下&#xff1a;《基于yolov5的轻量级水稻虫害目标检测项目实践》感兴趣的话可以自行移步阅读。这里主要是针对酸枣常见的几种病虫害检测检测识别&#xff0c;首先…

苹果电脑如何录屏?3个方法,轻松学会

苹果电脑是很多创作者、视频制作人和经常工作用户的选择&#xff0c;但是如何在苹果电脑上录制高质量的屏幕视频呢&#xff1f;苹果电脑如何录屏&#xff1f;本文将介绍3种不同的方法&#xff0c;帮助小伙伴轻松学会如何在苹果电脑上录制屏幕视频。 方法一&#xff1a;使用Mac自…

假设检验的基本思想

假设检验 首先了解参数估计&#xff0c;比如有服从正态分布的数据集X∼N(μ,σ2)X\sim N(\mu,\sigma^{2})X∼N(μ,σ2)&#xff0c;我们希望根据样本x1,...xnx_{1},...x_{n}x1​,...xn​估计出参数μ,σ\mu,\sigmaμ,σ&#xff0c;这些参数可以是一个具体值&#xff0c;也可以…

【C++】Windows动态库【.DLL文件】制作方法总结

如题&#xff0c;我们本篇介绍如何制作DLL&#xff0c;将代码类中的方法以接口的形式暴露出来给exe程序使用。会涉及类厂创建方法实例、声明DLL接口、.def文件的使用等。 目录 一、DLL介绍 二、C制作DLL文件 2.1 DLL端 2.2 调用端 三、DLL导出类方法 四、COM技术制作DLL…

扎心话题 | 设计院背后的潜规则你知道吗?

大家好&#xff0c;我是建模助手。 大家都知道&#xff0c;在过去的2022年经济是真难&#xff01;以小编所在的广东为例&#xff0c;全年GDP增长仅1.9%。 这个数据足以呈现一个社会现象——不仅消费力咔咔下降&#xff0c;各行各业更有不同程度地嗝屁。这其中也包括一些设计院…

只要一直向前定能到达远方,社科院与杜兰大学金融管理硕士项目为你注入动力

在人生这条道路上&#xff0c;我们很远的路要走&#xff0c;不管前方是否平坦&#xff0c;我们只要坚持前向&#xff0c;终将抵达远方。一路上我们付出很多&#xff0c;也收获很多。想要变得更强大&#xff0c;就要不断优化自身&#xff0c;积攒更多的能量&#xff0c;社科院与…

Flask入门(10):Flask使用SQLAlchemy

目录11.SQLAlchemy11.1 简介11.2 安装11.3 基本使用11.4 连接11.5 数据类型11.6 执行原生sql11.7 插入数据11. 8 删改操作11.9 查询11.SQLAlchemy 11.1 简介 SQLAlchemy的是Python的SQL工具包和对象关系映射&#xff0c;给应用程序开发者提供SQL的强大功能和灵活性。它提供了…

浅析无人值守+智慧巡检变电站安全管控系统设计方案

一、项目背景 安全是电力生产的基石&#xff0c;确保电网安全和人身安全&#xff0c;是电网企业安全工作的出发点和落脚点。 随着智能信息化技术应用越来越广泛&#xff0c;智能信息化现场安全管理是近年来基于智能安全巡检技术下发展起来的现场作业安全管理新技术。 变电站运…

【机器学习】朴素贝叶斯算法

朴素贝叶斯&#xff08;Naive Bayes&#xff09;是经典的机器学习算法之一&#xff0c;也是为数不多的基于概率论的分类算法。由于朴素贝叶斯计算联合概率&#xff0c;所以朴素贝叶斯模型属于生成式模型。经典应用案例包括&#xff1a;文本分类、垃圾邮件过滤等。 1.贝叶斯公式…

rust 安装

rust 安装一、需要一个c的环境二、配置环境变量三、开始安装一、需要一个c的环境 安装Visual Studio 二、配置环境变量 Rust需要安装两个东西&#xff0c;一个是rustup&#xff0c;一个是cargo。所以你需要设置两个环境变量来分别指定他们的安装目录。 通过RUSTUP_HOME指定…

滤波算法:经典卡尔曼滤波

卡尔曼滤波实质上就是基于观测值以及估计值二者的数据对真实值进行估计的过程。预测步骤如图1所示&#xff1a; ​图1 卡尔曼滤波原理流程图 假设我们能够得到被测物体的位置和速度的测量值 ​&#xff0c;在已知上一时刻的最优估计值 ​以及它的协方差矩阵 的条件下&#xff…

ChatGPT热潮背后,金融行业大模型应用路在何方?——金融行业大模型应用探索

ChatGPT近两个月以来不断引爆热点&#xff0c;对人工智能应用发展的热潮前所未有地高涨&#xff0c;ChatGPT所代表的大模型在语义理解、多轮交互、内容生成中所展现的突出能力令人惊喜。而人工智能技术在金融行业的落地应用仍然面临挑战&#xff0c;虽然已经让大量宝贵的人力从…

易基因|ChIP-seq等组学研究鉴定出结直肠癌的致癌超级增强子:Nature子刊

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。超级增强子&#xff08;Super enhancer&#xff09;是一类包含多个普通增强子的大簇&#xff0c;主要富集高密度的转录因子、辅助因子及增强子相关表观修饰位点。与普通增强子相比&#xf…

canal实时同步mysql数据到elasticsearch(部署,配置,测试)

这里写目录标题简介工作原理MySQL主备复制原理canal 工作原理canal 使用流程环境搭建环境使用版本mysql配置修改配置创建从库权限账号创建测试数据库创建测试数据表elasticsearch配置创建索引建立映射canal的下载部署下载canal配置服务端 canal-deployer配置客户端canal-adapte…

Keysight E5061B网络分析仪

Keysight E5061B&#xff08;安捷伦&#xff09;网络分析仪可在 5 Hz 至 3 GHz 的宽频率范围内提供多功能的高性能网络分析。E5061B 提供了 ENA 系列共有的出色射频性能&#xff0c;还提供了成熟的 LF&#xff08;低频&#xff09;网络测量功能&#xff1b;包括带有内置 1 Mohm…

【Vue学习】Vue基本使用

1. 模板语法 插值&#xff1a;使用双大括号进行数据的插值&#xff0c;包括文本、JS表达式。动态属性&#xff1a;可以使用模板字符串。如果使用标签使用了v-html指令&#xff0c;那么标签中的子元素会被引入的html代码覆盖掉&#xff0c;同时也会存在xss风险。 2. compute…