SpringBoot整合SpringSecurity+jwt+knife4生成api接口(从零开始简单易懂)

一、准备工作

①:创建一个新项目

1.事先创建好一些包

在这里插入图片描述

②:引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <!--支持使用 JDBC 访问数据库 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--整合mybatis plus https://baomidou.com/-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!-- mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!--数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.16</version>
        </dependency>

        <!--引入hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>

        <!-- springboot security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--  redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- 图片验证码生成器-->
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <!-- 生成配置元数据-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 参数校验 如:@NotBlank(message = "name为必传参数") private String name;-->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
        <!-- 导入 knife4j生成接口文档-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

③:添加一个测试接口查看效果

1.TestController

@RestController
@Api(tags = "测试专用接口")
public class TestController {

    @GetMapping("hello")
    @ApiOperation("测试接口hello")
    public String hello(){
        return "您请求了一个测试接口-hello";
    }
}

2.启动查看效果访问http://localhost:8083/hello

  • 会自动跳到Springsecurity的登录页面(程序已经被SpringSecurity保护)
  • 没有配置用户名和密码时 默认用户user 密码 在控制台

在这里插入图片描述

3.登录成功可以看到(引入SpringSecurity测试成功)

在这里插入图片描述

④:创建工具类和统一响应类

01.工具类

1.创建Redis工具了

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    //============================String=============================  

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key 键
     * @param delta  要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key 键
     * @param delta  要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    //================================Map=================================  

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    //============================set=============================  

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    //===============================list=================================  

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束  0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    //================有序集合 sort set===================
    /**
     * 有序set添加元素
     *
     * @param key
     * @param value
     * @param score
     * @return
     */
    public boolean zSet(String key, Object value, double score) {
        return redisTemplate.opsForZSet().add(key, value, score);
    }

    public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
        return redisTemplate.opsForZSet().add(key, typles);
    }

    public void zIncrementScore(String key, Object value, long delta) {
        redisTemplate.opsForZSet().incrementScore(key, value, delta);
    }

    public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
        redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
    }

    /**
     * 获取zset数量
     * @param key
     * @param value
     * @return
     */
    public long getZsetScore(String key, Object value) {
        Double score = redisTemplate.opsForZSet().score(key, value);
        if(score==null){
            return 0;
        }else{
            return score.longValue();
        }
    }

    /**
     * 获取有序集 key 中成员 member 的排名 。
     * 其中有序集成员按 score 值递减 (从大到小) 排序。
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
    }

}

2.创建RedisConfig自定义key和value的序列化(避免出现乱码)

@Configuration
public class RedisConfig {
    
    @Bean
        // 定义 RedisTemplate Bean
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 创建 RedisTemplate 实例
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        
        // 配置 JSON 序列化器
        Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        redisSerializer.setObjectMapper(new ObjectMapper());
        
        // 设置键的序列化器为 StringRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置值的序列化器为 StringRedisSerializer
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        
        // 设置哈希键的序列化器为 StringRedisSerializer
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // 设置哈希值的序列化器为 StringRedisSerializer
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        
        return redisTemplate;
    }
}

3.Jwt工具类 创建jwt和校验jwt

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "coke.jwt")
public class JwtUtils {
   // JWT 过期时间(单位:秒)
   private long expire;
   
   // JWT 密钥,用于签名和验证
   private String secret;
   
   // JWT 头部字段,可自定义
   private String header;
   
   /**
    * 生成 JWT
    *
    * @param username 用户名
    * @return JWT 字符串
    */
   public String generateToken(String username) {
      // 获取当前时间
      Date nowDate = new Date();
      
      // 计算过期时间,当前时间 + 过期时长
      Date expireDate = new Date(nowDate.getTime() + expire);
      
      // 使用 JWT Builder 构建 JWT
      return Jwts.builder()
         .setHeaderParam("typ", "JWT") // 设置头部信息,通常为JWT
         .setSubject(username) // 设置主题,通常为用户名
         .setIssuedAt(nowDate) // 设置签发时间,即当前时间
         .setExpiration(expireDate) // 设置过期时间
         .signWith(SignatureAlgorithm.HS512, secret) // 使用HS512签名算法和密钥进行签名
         .compact();
   }
   
   /**
    * 解析 JWT 获取声明
    *
    * @param jwt JWT 字符串
    * @return JWT 中的声明部分
    */
   public Claims getClaimByToken(String jwt) {
      try {
         // 使用 JWT 解析器解析 JWT,并获取声明部分
         return Jwts.parser()
            .setSigningKey(secret) // 设置解析时的密钥,必须与生成时的密钥一致
            .parseClaimsJws(jwt)
            .getBody();
      } catch (Exception e) {
         // 解析失败,返回null
         return null;
      }
   }
   
   /**
    * 检查 JWT 是否过期
    *
    * @param claims JWT 中的声明部分
    * @return 是否过期
    */
   public boolean isTokenExpired(Claims claims) {
      // 检查过期时间是否在当前时间之前
      return claims.getExpiration().before(new Date());
   }

}

4.jwt工具类中读取了ym配置文件中的coke.jwt 配置如下

server:
  port: 8083
coke:
  jwt:
    header: Authorization
    expire: 604800 #7天,秒单位
    secret: ji8n3439n439n43ld9ne9343fdfer49h

02.统一响应类

1.Response

@Data
public class Response<T> {
    
    /**
     * 结果
     *
     * @mock true
     */
    private boolean success;
    
    /**
     * 状态码
     *
     * @mock 200
     */
    private int code;
    
    /**
     * 消息提示
     *
     * @mock 操作成功
     */
    
    private String msg;
    
    /**
     * 结果体
     *
     * @mock null
     */
    private T data;
    
    public Response () {
    
    }
    
    public Response (int code, Object status) {
        super();
        this.code = code;
        this.msg = status.toString();
        if (code == 1) {
            this.success = true;
        } else {
            this.success = false;
        }
    }
    
    public Response (int code, String status, T result) {
        super();
        this.code = code;
        this.msg = status;
        this.data = result;
        if (code == 1) {
            this.success = true;
        } else {
            this.success = false;
        }
    }
    
    public static Response<?> ok() {
        return new Response<>(1, "success");
    }
    
    public static <T> Response<T> ok(T t) {
        return new Response<T>(1, "success", t);
    }
    
    public static Response<?> error(String status) {
        return new Response<>(500, status);
    }
    
    public static Response<?> error(int code, String status) {
        return new Response<>(code, status);
    }
}

2.添加一个常量类Const

public class Const {
    public final static String CAPTCHA_KEY = "captcha";
    public final static String Login_Key = "login";
}

⑤:数据库 数据准备

01.yml数据库配置

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://1.11.94.14:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: www

  thymeleaf:  # 是否使用springboot静态文件缓存  true 当修改静态文件需要重启服务器 false 浏览器端刷新就可以了
    cache: false
    check-template: true

  redis:
    host: 1.107.94.114
    password: www
    port: 6379

  mybatis-plus:
    mapper-locations: classpath*:/mapper/**Mapper.xml

02.添加数据

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
                            `id` bigint(20) NOT NULL AUTO_INCREMENT,
                            `parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
                            `name` varchar(64) NOT NULL,
                            `path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
                            `perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
                            `component` varchar(255) DEFAULT NULL,
                            `type` int(5) NOT NULL COMMENT '类型     0:目录   1:菜单   2:按钮',
                            `icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
                            `orderNum` int(11) DEFAULT NULL COMMENT '排序',
                            `created` datetime NOT NULL,
                            `updated` datetime DEFAULT NULL,
                            `statu` int(5) NOT NULL,
                            PRIMARY KEY (`id`),
                            UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', '0', '系统管理', '', 'sys:manage', '', '0', 'el-icon-s-operation', '1', '2021-01-15 18:58:18', '2021-01-15 18:58:20', '1');
INSERT INTO `sys_menu` VALUES ('2', '1', '用户管理', '/sys/users', 'sys:user:list', 'sys/User', '1', 'el-icon-s-custom', '1', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('3', '1', '角色管理', '/sys/roles', 'sys:role:list', 'sys/Role', '1', 'el-icon-rank', '2', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('4', '1', '菜单管理', '/sys/menus', 'sys:menu:list', 'sys/Menu', '1', 'el-icon-menu', '3', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('5', '0', '系统工具', '', 'sys:tools', null, '0', 'el-icon-s-tools', '2', '2021-01-15 19:06:11', null, '1');
INSERT INTO `sys_menu` VALUES ('6', '5', '数字字典', '/sys/dicts', 'sys:dict:list', 'sys/Dict', '1', 'el-icon-s-order', '1', '2021-01-15 19:07:18', '2021-01-18 16:32:13', '1');
INSERT INTO `sys_menu` VALUES ('7', '3', '添加角色', '', 'sys:role:save', '', '2', '', '1', '2021-01-15 23:02:25', '2021-01-17 21:53:14', '0');
INSERT INTO `sys_menu` VALUES ('9', '2', '添加用户', null, 'sys:user:save', null, '2', null, '1', '2021-01-17 21:48:32', null, '1');
INSERT INTO `sys_menu` VALUES ('10', '2', '修改用户', null, 'sys:user:update', null, '2', null, '2', '2021-01-17 21:49:03', '2021-01-17 21:53:04', '1');
INSERT INTO `sys_menu` VALUES ('11', '2', '删除用户', null, 'sys:user:delete', null, '2', null, '3', '2021-01-17 21:49:21', null, '1');
INSERT INTO `sys_menu` VALUES ('12', '2', '分配角色', null, 'sys:user:role', null, '2', null, '4', '2021-01-17 21:49:58', null, '1');
INSERT INTO `sys_menu` VALUES ('13', '2', '重置密码', null, 'sys:user:repass', null, '2', null, '5', '2021-01-17 21:50:36', null, '1');
INSERT INTO `sys_menu` VALUES ('14', '3', '修改角色', null, 'sys:role:update', null, '2', null, '2', '2021-01-17 21:51:14', null, '1');
INSERT INTO `sys_menu` VALUES ('15', '3', '删除角色', null, 'sys:role:delete', null, '2', null, '3', '2021-01-17 21:51:39', null, '1');
INSERT INTO `sys_menu` VALUES ('16', '3', '分配权限', null, 'sys:role:perm', null, '2', null, '5', '2021-01-17 21:52:02', null, '1');
INSERT INTO `sys_menu` VALUES ('17', '4', '添加菜单', null, 'sys:menu:save', null, '2', null, '1', '2021-01-17 21:53:53', '2021-01-17 21:55:28', '1');
INSERT INTO `sys_menu` VALUES ('18', '4', '修改菜单', null, 'sys:menu:update', null, '2', null, '2', '2021-01-17 21:56:12', null, '1');
INSERT INTO `sys_menu` VALUES ('19', '4', '删除菜单', null, 'sys:menu:delete', null, '2', null, '3', '2021-01-17 21:56:36', null, '1');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
                            `id` bigint(20) NOT NULL AUTO_INCREMENT,
                            `name` varchar(64) NOT NULL,
                            `code` varchar(64) NOT NULL,
                            `remark` varchar(64) DEFAULT NULL COMMENT '备注',
                            `created` datetime DEFAULT NULL,
                            `updated` datetime DEFAULT NULL,
                            `statu` int(5) NOT NULL,
                            PRIMARY KEY (`id`),
                            UNIQUE KEY `name` (`name`) USING BTREE,
                            UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('3', '普通用户', 'normal', '只有基本查看功能', '2021-01-04 10:09:14', '2021-01-30 08:19:52', '1');
INSERT INTO `sys_role` VALUES ('6', '超级管理员', 'admin', '系统默认最高权限,不可以编辑和任意修改', '2021-01-16 13:29:03', '2021-01-17 15:50:45', '1');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
                                 `id` bigint(20) NOT NULL AUTO_INCREMENT,
                                 `role_id` bigint(20) NOT NULL,
                                 `menu_id` bigint(20) NOT NULL,
                                 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('60', '6', '1');
INSERT INTO `sys_role_menu` VALUES ('61', '6', '2');
INSERT INTO `sys_role_menu` VALUES ('62', '6', '9');
INSERT INTO `sys_role_menu` VALUES ('63', '6', '10');
INSERT INTO `sys_role_menu` VALUES ('64', '6', '11');
INSERT INTO `sys_role_menu` VALUES ('65', '6', '12');
INSERT INTO `sys_role_menu` VALUES ('66', '6', '13');
INSERT INTO `sys_role_menu` VALUES ('67', '6', '3');
INSERT INTO `sys_role_menu` VALUES ('68', '6', '7');
INSERT INTO `sys_role_menu` VALUES ('69', '6', '14');
INSERT INTO `sys_role_menu` VALUES ('70', '6', '15');
INSERT INTO `sys_role_menu` VALUES ('71', '6', '16');
INSERT INTO `sys_role_menu` VALUES ('72', '6', '4');
INSERT INTO `sys_role_menu` VALUES ('73', '6', '17');
INSERT INTO `sys_role_menu` VALUES ('74', '6', '18');
INSERT INTO `sys_role_menu` VALUES ('75', '6', '19');
INSERT INTO `sys_role_menu` VALUES ('76', '6', '5');
INSERT INTO `sys_role_menu` VALUES ('77', '6', '6');
INSERT INTO `sys_role_menu` VALUES ('96', '3', '1');
INSERT INTO `sys_role_menu` VALUES ('97', '3', '2');
INSERT INTO `sys_role_menu` VALUES ('98', '3', '3');
INSERT INTO `sys_role_menu` VALUES ('99', '3', '4');
INSERT INTO `sys_role_menu` VALUES ('100', '3', '5');
INSERT INTO `sys_role_menu` VALUES ('101', '3', '6');

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
                            `id` bigint(20) NOT NULL AUTO_INCREMENT,
                            `username` varchar(64) DEFAULT NULL,
                            `password` varchar(64) DEFAULT NULL,
                            `avatar` varchar(255) DEFAULT NULL,
                            `email` varchar(64) DEFAULT NULL,
                            `city` varchar(64) DEFAULT NULL,
                            `created` datetime DEFAULT NULL,
                            `updated` datetime DEFAULT NULL,
                            `last_login` datetime DEFAULT NULL,
                            `statu` int(5) NOT NULL,
                            PRIMARY KEY (`id`),
                            UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', '123@qq.com', '广州', '2021-01-12 22:13:53', '2021-01-16 16:57:32', '2020-12-30 08:38:37', '1');
INSERT INTO `sys_user` VALUES ('2', 'test', '123456', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', 'test@qq.com', null, '2021-01-30 08:20:22', '2021-01-30 08:55:57', null, '1');

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
                                 `id` bigint(20) NOT NULL AUTO_INCREMENT,
                                 `user_id` bigint(20) NOT NULL,
                                 `role_id` bigint(20) NOT NULL,
                                 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('4', '1', '6');
INSERT INTO `sys_user_role` VALUES ('7', '1', '3');
INSERT INTO `sys_user_role` VALUES ('13', '2', '3');

⑥:创建根据用户名获取用户接口

1.实体类SysUser

@Data
@ApiModel(description = "用户实体类")
public class SysUser implements Serializable{

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("用户id,主键")
    private Long id;

    @NotBlank(message = "用户名不能为空")
    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("用户密码")
    private String password;

    @ApiModelProperty("头像")
    private String avatar;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @ApiModelProperty("邮箱")
    private String email;

    @ApiModelProperty("城市")
    private String city;

    @ApiModelProperty("最后登录时间")
    private LocalDateTime lastLogin;

    @ApiModelProperty("创建时间")
    private LocalDateTime created;
    
    @ApiModelProperty("更新时间")
    private LocalDateTime updated;

    @ApiModelProperty("用户状态")
    private Integer statu;
    
    @ApiModelProperty("用户权限")
    @TableField(exist = false)
    private List<String> auths;
}

2.创建SysUserMapper

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}

3.启动类上加@MapperScan("com.it.App.mapper")

在这里插入图片描述

4.创建SysUserService

public interface SysUserService {
    Response<?> getUserByName(String username);
}

5.创建SysUserServiceImpl

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    @Autowired
    private SysUserMapper sysUserMapper;
    @Override
    public Response<?> getUserByName(String username) {
        QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();
        QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);
        SysUser sysUser = sysUserMapper.selectOne(wrapper);
        // 是否查询到用户
        if (ObjectUtil.isNull(sysUser)){
            return Response.error("查无此人");
        }
        return Response.ok(sysUser);
    }
}

6.创建SysUserController

@RestController
@RequestMapping("/sys")
@Api(tags = "用户相关接口")
public class SysUserController {

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("/getUser")
    @ApiOperation("根据用户名获取用户")
    public Response<?> getUserByName(String username){
        return sysUserService.getUserByName(username);
    }

7.测试http://localhost:8083/sys/getUser?username=admin

在这里插入图片描述

  • 测试成功 说明我们mybatisPlus引入是没有问题的

⑦:配置Knife4j生成api文档在线测试

配置详情笔记:https://blog.csdn.net/cygqtt/article/details/134544894

注意:配置完成之后是访问不到的,因为被SpringSecurity拦截了,需要放行

如何放行:在下文 登录接口实现 里的 添加配置

二、实现数据库用户登录

认证流程

在这里插入图片描述

①:自定义UserDetailService

1.首先创建一个LoginUser实现UserDetails用于验证返回的数据

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginUser implements UserDetails {
    // 引入我们的sysUser实体类
    private SysUser sysUser;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        // 返回密码后将密码置空
        String password = sysUser.getPassword();
        sysUser.setPassword(null);
        return password;
    }

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

2.创建UserDetailServiceImpl实现UserDetailsService用于自定义登录

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 登录验证
        QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();
        QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);
        SysUser sysUser = sysUserMapper.selectOne(wrapper);
        // 是否查询到用户, 如果没有查询到永固抛出异常
        if (ObjectUtil.isNull(sysUser)){
           throw new RuntimeException("用户名或密码错误");
        }
        // TODO 权限验证
        // 将查询出来的用户封装成UserDetails返回
        return LoginUser.builder().sysUser(sysUser).build();
    }
}

3.创建SecurityConfig配置类 配置密码的加密方式

  • 如果不配置直接登录会报错There is no PasswordEncoder mapped for the id "null"意思就是说密码的加密方式为空
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 指定一个密码的加密方式
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4.虽然指定了加密方式但是数据库中的密码还是明文 所以要改成密文

  • 我们可以写一个测试类 将明文转换为密码 然后将密码存到数据库中
@SpringBootTest
@Slf4j
class ApplicationTests {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Test
    void getPwd() {
        String encode = bCryptPasswordEncoder.encode("123456");
        log.info("加密后的密文为: {}", encode);
    }

}

在这里插入图片描述

②:测试登录

1.登录

在这里插入图片描述

2.请求测试接口 http://localhost:8083/sys/getUser?username=admin

在这里插入图片描述

③:登录接口实现

01.添加配置

  • 在登录过程中 真正的认证逻辑还是交给SpringSecurity的,所以需要重写authenticationManagerBean()这个方法

  • 在登录时我们要放开登录接口,需要重写configure(HttpSecurity http)这个方法 指定放开的路径

1.配置类SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    public static final String[] URL_WHITELIST = {
            "/webjars/**",
            "/favicon.ico",

            "/sys/captcha",
            "/sys/login",
            "/sys/logout",
            "/swagger-resources/**",
            "/v2/api-docs",
            "/swagger-ui.html",
            "/webjars/**", // 放行knife4j生成的接口文档(/swagger-resources 和 /v2/api-docs 还有一些其他的资源路径, /swagger-ui.html、/webjars/** )
    };

    // 指定一个密码的加密方式
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    // 配置HttpSecurity,定义安全策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable() // 启用跨越支持,禁用CSRF保护
                .formLogin()

                .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll() // 设置白名单,允许访问的URL
                .antMatchers(String.valueOf(HttpMethod.OPTIONS), "/**").permitAll() // 放行OPTIONS请求: Swagger可能会发出OPTIONS请求,确保这个请求也被放行
                .anyRequest().authenticated()  // 其他所有请求需要身份验证

                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不会创建session
    }
}

02.登录接口

1.直接在SysUserController中添加登录方法即可

@PostMapping("/login")
@ApiOperation("用户登录")
public Response<?> login(@RequestBody SysUser sysUser){
    return sysUserService.login(sysUser);
}

2.SysUserService

Response<?> login(SysUser sysUser);

3.SysUserServiceImpl

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public Response<?> login(SysUser sysUser) {
        // AuthenticationManager 进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 如果认证没有通过 给出对应的提示
        if (ObjectUtil.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误!");
        }
        // 如果认证通过, 使用userId生成一个Jwt jwt存入到Response中返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        // 通过userId生成token
        String userId = loginUser.getSysUser().getId().toString();
        String token = jwtUtils.generateToken(userId);
        Map<Object, Object> map = MapUtil.builder().put("token", token).build();

        // 把完整的用户信息存入到redis中 统一的前缀 login  过期时间为10分钟
        String jsonString = objectMapper.writeValueAsString(loginUser);

        redisUtil.hset(Const.Login_Key,userId,jsonString,60*10);
        // 返回登录成功的结果
        return Response.ok(map);
    }

03.测试登录

  • 因为我们导入 knife4j 生成了接口文档所以可以使用knife4j发送请求测试

  • 访问:http://localhost:8083/doc.html

在这里插入图片描述

1.发送登录请求

在这里插入图片描述

在这里插入图片描述

④:token认证过滤器代码实现

01.创建token认证过滤器

1.JWTAuthenticationFilter

@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private ObjectMapper objectMapper;

    // 进行JWT校验的过滤操作
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 日志记录JWT校验过滤器的执行
        log.info("JWT校验过滤器执行");

        // 从请求头中获取JWT
        String token = request.getHeader("token");

        // 如果token为空,则放行,继续处理下一个过滤器
        if (StrUtil.isBlankOrUndefined(token)){
            chain.doFilter(request,response);
            return;
        }

        // token不为空 使用Jwt工具类 解析获取声明
        Claims claims = jwtUtils.getClaimByToken(token);

        // 如果 token异常 则抛出异常
        if (claims == null){
            throw new RuntimeException("Token异常");
        }
        // 如果 token已过期 则抛出异常
        if (jwtUtils.isTokenExpired(claims)){
            throw new RuntimeException("Token已过期");
        }

        // 从token中获取用户id
        String userId = claims.getSubject();
        // 从redis中获取用户的全部信息
        String loginUserStr = (String) redisUtil.hget(Const.Login_Key , userId);
        LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);
        SysUser sysUser = loginUser.getSysUser();
        // 日志记录正在登录的用户信息
        log.info("用户-{},正在登录!", sysUser.getUsername());

        // TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null,null);
        // 将认证信息设置到安全上下文中
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 继续处理请求
        chain.doFilter(request,response);
    }
}

2.将登录验证码校验过滤器加入到过滤器链中

  • SecurityConfig
    @Autowired
    private JWTAuthenticationFilter jwtAuthenticationFilter;
    .....
    // 配置HttpSecurity,定义安全策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable() // 启用跨越支持,禁用CSRF保护
                .formLogin()

                .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll() // 设置白名单,允许访问的URL
                .anyRequest().authenticated()  // 其他所有请求需要身份验证

                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不会创建session

        // 将登录验证码校验过滤器加入到过滤器链中
        http.addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
    }

02.测试登录

1.登录

在这里插入图片描述

  • redis中也存入了对象

在这里插入图片描述

2.携带token访问其他接口

在这里插入图片描述

⑤:登出接口实现

思路:退出登录时会携带token ==> 获取token中的用户id ==> 根据用户id 删除redis中存储的用户信息 ==>(如果有前台则登出成功后删除已缓存的token)

01. 登录接口实现

1.SysUserController

    @GetMapping("/logout")
    @ApiOperation("用户登出")
    public Response<?> logout(){
        return sysUserService.logout();
    }

2.SysUserService

    Response<?> logout();

3.SysUserServiceImpl

    @Override
    public Response<?> logout() {
        // 获取当前用户的认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 从认证信息中获取登录用户对象
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 如果登录用户为空,抛出异常,表示鉴权失败
        if (ObjectUtil.isNull(loginUser)) {
            throw new BaseException("鉴权失败!");
        }
        // 从Redis中删除用户登录信息
        String userId = loginUser.getSysUser().getId().toString();
        redisUtil.hdel(Const.Login_Key, userId);
        // 返回操作成功的响应
        return Response.ok("操作成功!");
    }

02.处理全局异常

1.创建BaseException

/**
 * @Author: Coke
 * @DateTime: 2023/11/23/9:53
 * @注释: 业务异常
 **/
public class BaseException extends RuntimeException {

    public BaseException() {
    }

    public BaseException(String msg) {
        super(msg);
    }

}

2.创建GlobalExceptionHandler

/**
 * @Author: Coke
 * @DateTime: 2023/11/23/9:31
 * @注释: 全局异常处理器,处理项目中抛出的业务异常
 **/
@Slf4j
@RestControllerAdvice // 用于全局处理控制器层(Controller)的异常
public class GlobalExceptionHandler {
    
    /**
     * 捕获业务异常
     * @param e:  
     * @return Response<?>
     * @author: Coke
     * @DateTime: 2023/11/23 9:33
     */
    @ExceptionHandler(BaseException.class)
    public Response<?> exceptionHandler(BaseException e){
        log.error("异常信息:{}", e.getMessage());
        return Response.error(201,e.getMessage());
    }
}

3.将之前抛出的所有RuntimeException 改成BaseException

在这里插入图片描述

4.修改JWTAuthenticationFilter

  • 在过滤器中的异常 我们自定义的全局异常捕获只做用与Controller层以及控制层的调用链上 所以我们直接在filer中try catch 捕获然后直接response响应回去就好了 当然也可以做一个AOP的切面来捕获过滤器中的异常
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private ObjectMapper objectMapper;

    // 进行JWT校验的过滤操作
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 日志记录JWT校验过滤器的执行
        log.info("JWT校验过滤器执行");
        try {
            // 从请求头中获取JWT
            String token = request.getHeader("token");

            // 如果token为空,则放行,继续处理下一个过滤器
            if (StrUtil.isBlankOrUndefined(token)) {
                chain.doFilter(request, response);
                return;
            }

            // token不为空 使用Jwt工具类 解析获取声明
            Claims claims = jwtUtils.getClaimByToken(token);

            // 如果 token异常 则抛出异常
            if (claims == null) {
                throw new BaseException("Token异常");
            }
            // 如果 token已过期 则抛出异常
            if (jwtUtils.isTokenExpired(claims)) {
                throw new BaseException("Token已过期");
            }

            // 从token中获取用户id
            String userId = claims.getSubject();
            // 从redis中获取用户的全部信息
            String loginUserStr = (String) redisUtil.hget(Const.Login_Key, userId);
            if (ObjectUtil.isNull(loginUserStr)) {
                throw new BaseException("鉴权失败!请求重新登录。");
            }
            LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);
            SysUser sysUser = loginUser.getSysUser();
            // 日志记录正在登录的用户信息
            log.info("用户-{},正在登录!", sysUser.getUsername());

            // TODO 获取权限信息封装到Authentication中
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
            // 将认证信息设置到安全上下文中
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

            // 继续处理请求
            chain.doFilter(request, response);
        } catch (BaseException e) {
            // 捕获并处理异常
            log.error("JWT校验过滤器异常:{}", e.getMessage());
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            ServletOutputStream outputStream = response.getOutputStream();
            Response<?> result = Response.error(201, e.getMessage());
            outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
            outputStream.close();
        }
    }
}

03.测试

1.登录

  • 登录成功并且拿到了Token
    在这里插入图片描述
  • Redis中也存入了用户信息
    在这里插入图片描述

2.携带Token获取用户信息

在这里插入图片描述

  • 成功
    在这里插入图片描述

3.请求登出接口

  • 登出成功 并且Redis中的数据也被删除了
    在这里插入图片描述

4.再次携带Token获取用户信息

在这里插入图片描述

三、权限

①:权限实现

01.限制访问资源所需权限

1.SecurityConfig中开启全局方法安全

@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用全局方法安全

在这里插入图片描述

2.在controller接口上设置 访问接口所需要的权限

  • SysUserController
 @PreAuthorize("hasAuthority('sys:getUser')")

在这里插入图片描述

  • 为了测试我们在 TestController 接口上也加一个权限(不存在的权限)

在这里插入图片描述

02.封装权限信息

1.LoginUser

》

    // 权限
    private List<String> auths;

    // 定义一个新的权限集合
    List<SimpleGrantedAuthority> newAuths;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 如果 newAuths 为空 第一个进来需要转换 如果不是直接返回
        if (ObjectUtil.isNull(newAuths)){
            // 将String类型的权限转成SimpleGrantedAuthority类型
            newAuths = auths.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        }
        return newAuths;
    }

2.UserDetailServiceImpl

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 登录验证
        QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();
        QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);
        SysUser sysUser = sysUserMapper.selectOne(wrapper);
        // 是否查询到用户, 如果没有查询到永固抛出异常
        if (ObjectUtil.isNull(sysUser)){

           throw new BaseException("用户名或密码错误");
        }
        // TODO 权限验证
        // 先将权限写死
        ArrayList<String> auths = new ArrayList<>(Arrays.asList("sys:getUser", "sys:addUser", "sys:delUser"));
        // 将查询出来的用户封装成UserDetails返回
        return LoginUser.builder().sysUser(sysUser).auths(auths).build();
    }

在这里插入图片描述

3.JWTAuthenticationFilter

   // TODO 获取权限信息封装到Authentication中
            Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser, null, authorities);

在这里插入图片描述

03.测试

1.修改RedisTemplate键和值的序列化

在这里插入图片描述

这里解释一下为什么要注销掉

  • 首先我们指定的是值的序列化器为 StringRedisSerializer 所以我们存的值要转成String类型,这样我们可以清楚的看懂存的是什么

  • 其次我们从redis中获取到String类型的值后还要转成对象(问题就在这里平常对象当然没问题,但是我们今天存了这个类型的字段List<SimpleGrantedAuthority> newAuths;注意:SimpleGrantedAuthority没有无参构造方法

  • 然而字符串转对象调用的就是无参构造(所以会报错)

  • 最后 干脆我们直接存Redis中的值为对象好了

所以我们需要改动两个地方

    1. SysUserServiceImpl加粗样式
    1. JWTAuthenticationFilter
      在这里插入图片描述

2.首先登录然后拿到Token

在这里插入图片描述

3.携带Token获取用户信息(有这个权限可以获取到)

在这里插入图片描述

4.携带Token请求Hello接口(没有hello的权限,不能访问)

在这里插入图片描述

②:基于数据库的权限实现

01.介绍

在这里插入图片描述

1.看一下流程就明白了

在这里插入图片描述

02.新增一些测试接口

1.SysUserController

  • 由于测试我们直接返回即可(重点在权限验证上)
    @PostMapping("/user/save")
    @ApiOperation("添加用户")
    @PreAuthorize("hasAuthority('sys:role:save')")
    public Response<?> userSave(){
        return Response.ok("新增用户成功!");
    }

    @PostMapping("/user/update")
    @ApiOperation("修改用户")
    @PreAuthorize("hasAuthority('sys:role:update')")
    public Response<?> updateSave(){
        return Response.ok("更新用户成功!");
    }

    @GetMapping("/user/delete")
    @ApiOperation("删除用户")
    @PreAuthorize("hasAuthority('sys:role:delete')")
    public Response<?> deleteSave(){
        return Response.ok("删除用户成功!");
    }

03.查询SQL实现

1.SysUserMapper

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

    @Select("select sm.perms\n" +
            "from sys_user su\n" +
            "         join sys_user_role sur on sur.user_id = su.id\n" +
            "         join sys_role sr on sur.role_id = sr.id\n" +
            "         join sys_role_menu srm on sr.id = srm.role_id\n" +
            "         join sys_menu sm on srm.menu_id = sm.id\n" +
            "where su.id = #{userId}")
    List<String> getMenuByUserId(Long userId);
}

2.UserDetailServiceImpl

        // 根据 用户id 从数据库中查询权限
        List<String> auths = sysUserMapper.getMenuByUserId(sysUser.getId());

在这里插入图片描述

③:测试

01.使用admin用户测试

  • 测试结果:有权限都可以访问

1.登录获取到token

在这里插入图片描述

2.测试新增用户接口

在这里插入图片描述

3.测试修改用户接口

在这里插入图片描述

4.测试删除用户接口

在这里插入图片描述

02.使用test用户测试

  • 测试结果:没有权限都不可以访问

1.登录获取到token

在这里插入图片描述

  • 登录成功后redis中就有两个用户信息了

在这里插入图片描述

2.测试新增用户接口

在这里插入图片描述

3.测试修改用户接口

在这里插入图片描述

4.测试删除用户接口

在这里插入图片描述

四、自定义异常处理(完善)

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的jso,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslation Filter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException:然后调用AuthenticationEntryPoint)对象的方法去进行异常处
理。

如果是授权过程中出现的异常会被封装成AccessDeniedException?然后调用*AccessDeniedHandler**对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

①:自定义实现类

1.授权失败异常处理 (AccessDeniedHandlerImpl)

/**
 * @Author: Coke
 * @DateTime: 2023/11/23/16:38
 * @注释: 授权失败异常处理
 **/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        ServletOutputStream outputStream = response.getOutputStream();
        Response<?> result = Response.error(HttpStatus.FORBIDDEN.value(), "您权限不足!");
        outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

2.认证失败异常处理 (AuthenticationEntryPointImpl)

/**
 * @Author: Coke
 * @DateTime: 2023/11/23/16:34
 * @注释: 认证失败异常处理
 **/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        ServletOutputStream outputStream = response.getOutputStream();
        Response<?> result = Response.error(HttpStatus.UNAUTHORIZED.value(), "用户认证失败!请重新登录");
        outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

3.修改JWTAuthenticationFilter

  • 之前我们是在JWTAuthenticationFilter中使用try – catch 捕获的异常然后处理的现在不需要了
  • 删除try – catch 处理异常的代码
  • 抛出的异常BaseException改成RuntimeException

修改后的代码如下

@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private RedisUtil redisUtil;

    // 进行JWT校验的过滤操作
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 日志记录JWT校验过滤器的执行
        log.info("JWT校验过滤器执行");
        // 从请求头中获取JWT
        String token = request.getHeader("token");

        // 如果token为空,则放行,继续处理下一个过滤器
        if (StrUtil.isBlankOrUndefined(token)) {
            chain.doFilter(request, response);
            return;
        }

        // token不为空 使用Jwt工具类 解析获取声明
        Claims claims = jwtUtils.getClaimByToken(token);

        // 如果 token异常 则抛出异常
        if (claims == null) {
            throw new RuntimeException("Token异常");
        }
        // 如果 token已过期 则抛出异常
        if (jwtUtils.isTokenExpired(claims)) {
            throw new RuntimeException("Token已过期");
        }

        // 从token中获取用户id
        String userId = claims.getSubject();
        // 从redis中获取用户的全部信息
        LoginUser loginUser = (LoginUser) redisUtil.hget(Const.Login_Key, userId);
        if (ObjectUtil.isNull(loginUser)) {
            throw new RuntimeException("鉴权失败!请求重新登录。");
        }
//            LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);
        SysUser sysUser = loginUser.getSysUser();
        // 日志记录正在登录的用户信息
        log.info("用户-{},正在登录!", sysUser.getUsername());

        // TODO 获取权限信息封装到Authentication中
        Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities);
        // 将认证信息设置到安全上下文中
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 继续处理请求
        chain.doFilter(request, response);
    }
}

②:配置给SpringSecurity

1.SecurityConfig

在这里插入图片描述

    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    // 配置异常处理器(认证异常和授权异常)
    http.exceptionHandling()
         // 配置 认证异常处理器
         .authenticationEntryPoint(authenticationEntryPoint)
         // 配置授权异常处理器
         .accessDeniedHandler(accessDeniedHandler);

③:测试

1.登录给出错误密码

在这里插入图片描述

2.使用Test用户登录后访问新增用户接口(没有这个权限)

在这里插入图片描述

五、跨域

浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTP请求时必须遵守同源策略,否则就是跨域的HTP请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。

所以我们就要处理一下,让前端能进行跨域请求。

①:先对SpringBoot配置,允许跨域请求

1.创建CorsConfig

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings (CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
              // 设置允许跨域的域名
              .allowedOriginPatterns("*")
              // 是否允许cookie
              .allowCredentials(true)
              // 这是允许的请求方式
              .allowedMethods("GET","POST","DELETE","PUT")
              //设置允许的header属性
              .allowedHeaders("*")
              // 跨域允许时间
              .maxAge(3600);
    }
}

②:开启SpringSecurity的跨域访问

在这里插入图片描述

六、其他权限校验方法

我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。SpringSecurityi还为我们提供了其它方法

例如:hasAnyAuthority,hasRole,hasAnyRole,等。

这里我们先不急着去介绍这些方法,我们先去理解nasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。

hasAuthority)方法实际是执行到了SecurityExpressionRoot的nasAuthority,大家只要断点调试既可知道它内部的校验原理。

它内部其实是调用authenticationl的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

    @GetMapping("hello2")
    @ApiOperation("测试接口hello多个权限")
    @PreAuthorize("hasAnyAuthority('hello','sys:role:save')")
    public String hello2(){
        return "您请求了一个测试接口-hello多个权限";
    }

hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上RoLE后再去比较。所以这种情况下要用用户对应的权限也要有ROLE这个前缀才可以。

hasAnyRole有任意的角色就可以访问。它内部也会把我们传入的参数拼接上RoLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。

①:自定义权限校验

1.com.it.App.expression.MyExpressionRoot(自己定义权限校验)

/**
 * @Author: Coke
 * @DateTime: 2023/11/24/9:00
 * @注释: 自定义权限校验
 **/
@Component("MyEx") // 自定义一下容器中Bean的名字
public class MyExpressionRoot {
    public boolean hasAuthority(String authority){
        // 获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> auths = loginUser.getAuths();
        // 判断用户权限集合中是否存在authority
        return auths.contains(authority);
    }
}

2.使用自己定义的权限校验

    @GetMapping("hello3")
    @ApiOperation("自定义权限校验")
    @PreAuthorize("@MyEx.hasAuthority('hello')") // 在SPEL表达式中使用@MyEx相当于获取容器中bean的名字未MyEx的对象。
    public String hello3(){
        return "您请求了一个测试接口-hello自定义权限校验";
    }

②:基于配置的权限校验

.antMatchers("/user/save").hasAuthority("sys:role:save") // 访问 /user/save接口 必须要拥有sys:role:save权限

在这里插入图片描述

七、CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

https://blog.csdn.net/freeking101/article/details/86537087

SpringSecurity去防l止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

我们可以发现cSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

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

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

相关文章

Whatweb简单使用

目录 简介 安装 debian/ubtuntu redhat/centos 特性 使用 常用参数如下&#xff1a; whatweb -v whatweb --version whatweb -i 1.txt whatweb -v www.baidu.com 扫描等级 whatweb -a 4 www.baidu.com 扫描网段 whatweb --no-errors -t 255 192.168.71.0/24 导出…

Word怎么看字数?简单教程分享!

“我在写文章时&#xff0c;总是想看看写了多少字。但是我发现我的Word无法看到字数。在Word中应该怎么查看字数呢&#xff1f;请帮帮我&#xff01;” Word是一个广泛使用的文档编辑工具。在我们编辑文章时&#xff0c;如果想查看写了多少字&#xff0c;也是可以轻松完成的。 …

黑马点评笔记 redis实现优惠卷秒杀

文章目录 难题全局唯一IDRedis实现全局唯一Id 超卖问题问题解决方案乐观锁问题 一人一单 难题 要解决优惠卷秒杀的问题我们要考虑到三个个问题&#xff0c;全局唯一ID&#xff0c;超卖问题&#xff0c;一人一单。 全局唯一ID 用户抢购时&#xff0c;就会生成订单并保存到同一…

LeetCode.283移动零(双指针)

LeetCode.283移动零 1.问题描述2.解题思路3.代码 1.问题描述 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1…

便携式水污染物监测设备的招标参数有哪些

便携式多参数水质检测仪参数特点&#xff1a; 便携式多参数水质检测仪参数特点&#xff1a; 便携式多参数水质快速测定仪&#xff0c;根据国家检测标准&#xff08;G B &#xff09; 及环境部检测标准(H J &#xff09;研发生产&#xff0c;本仪器具有检测快速、操作简单、测试…

PHP预约上门回收废品系统的代码披露

PHP预约上门回收废品系统的代码披露 <?phpnamespace app\admin\controller;class Code {public function getTopDomainhuo(){error_reporting(0);$host $_SERVER["HTTP_HOST"];$matchstr "[^\\.]\\.(?:(" . $host . ")|\\w{2}|((" . $ho…

解决LocalDateTime传输前端为时间的数组

问题出现如下&#xff1a; 问题出现原因&#xff1a; 默认序列化情况下会使用SerializationFeature.WRITE_DATES_AS_TIMESTAMPS。使用这个解析时就会打印出数组。 解决方法&#xff1a; 我在全文搜索处理方法总结如下&#xff1a; 1.前端自定义函数来书写 ,cols: [[ //表头{…

【黑马甄选离线数仓day03_数仓设计和维度域开发】

1. 数仓基本概念 1- 什么是数据仓库呢? 存储数据的仓库, 主要用于存储过去历史发生过的数据,面向主题, 对数据进行统计分析的操作, 从而能够对未来提供决策支持 2- 数据仓库最大的特点是什么呢? 数据仓库既不生产数据, 也不消耗数据, 数据来源于各个数据源 3- 数据仓库的四…

知行之桥EDI系统HTTP签名验证

本文简要概述如何在知行之桥EDI系统中使用 HTTP 签名身份验证&#xff0c;并将使用 CyberSource 作为该集成的示例。 API 概述 首字母缩略词 API 代表“应用程序编程接口”。这听起来可能很复杂&#xff0c;但真正归结为 API 是一种允许两个不同实体相互通信的软件。自开发以…

Android WorldWind加载shapefile格式文件形成三维效果

目录 1 前言2 实现思路3 绘制Polygons4 读取shapefile文件5 加载立体模型6 问题1 前言 在项目中有时会加载shapefile格式的数据,要形成三维立体效果。但是查看worldwind NASA官网,在worldwind android的使用教程中并没用加载shapefile格式的教程,然后源码中也没有开发加载s…

Grails 启动

Grails系列 Grails项目启动 文章目录 Grails系列Grails一、项目创建二、可能的问题1.依赖下载2.项目导入到idea失败3.项目导入到idea后运行报错 Grails Grails是一款基于Groovy语言的Web应用程序框架&#xff0c;它使用了许多流行的开源技术&#xff0c;如Spring Framework、…

synchronized的轻量级锁居然不会自旋?

《Java并发编程的艺术》中说到「如果失败&#xff0c;表示其他线程竞争锁&#xff0c;当前线程便尝试使用自旋来获取锁」&#xff0c;并且下文所配的流程图中明确表示自旋失败后才会升级为重量级锁&#xff0c;但《深入理解Java虚拟机》又说「如果出现两条以上的线程争用同一个…

Python潮流周刊#1:如何系统地自学Python?

这里记录每周值得分享的 Python 及通用技术内容&#xff0c;部分内容为英文&#xff0c;已在小标题注明。&#xff08;本期标题取自其中一则分享&#xff0c;不代表全部内容都是该主题&#xff0c;特此声明。&#xff09; 文章&教程 1、编程语言的错误处理模式 文章讨论…

HR8825 STEP/DIR 微步进电机驱动芯片

HR8825是一种内置步进表的集成微步进电机驱动器&#xff0c;为打印机、扫描仪和其它自动化设备提供处理方案。HR8825提供一种带有裸L焊盘的TSSOP-28封装&#xff0c;能改进散热性能&#xff0c;且是无铅产品&#xff0c;引脚框采用100&#xff05;无锡电镀。 内部的同步整流控制…

计算机毕业设计项目选题推荐(免费领源码)java+springboot+mysql 城市房屋租赁管理系统01855

摘 要 本论文主要论述了如何使用springboot 城市房屋租赁管理系统 &#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构JAVA技术&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述城市房屋租赁管理系统的当前背景以及…

罗拉ROLA告诉你美国IP代理如何获取?适用于哪些场景?

美国代理IP可以是静态&#xff08;不会改变&#xff09;或动态&#xff08;周期性更改&#xff09;&#xff0c;并且可以由专业的代理服务提供商提供。不同的代理IP服务提供商可能提供不同类型的代理&#xff0c;包括数据中心代理、住宅代理和移动代理&#xff0c;以满足不同用…

干货!ERP软件如何帮助企业实现信息化管理?

ERP即企业资源规划&#xff08;Enterprise Resource Planning&#xff09;系统&#xff0c;其核心是物料的追踪流转。而在物料追踪流转的基础上&#xff0c;又衍生出一系列各类资源计划的管理和追踪。因此ERP发展成为一款集成各类资源计划&#xff0c;也就是集成企业核心业务流…

C++学习之路(二)C++如何实现一个超简单的学生信息管理系统?C++示例和小项目实例

这个示例实现了一个简单的学生信息管理系统。它包括了学生类的定义&#xff0c;可以添加学生信息、显示所有学生信息&#xff0c;将学生信息保存到文件并从文件加载信息。通过这个示例&#xff0c;你可以了解到如何使用类、函数和文件操作来构建一个基本的信息管理系统。 一个简…

windows电脑定时开关机设置

设置流程 右击【此电脑】>【管理】 【任务计划程序】>【创建基本任务】 gina 命令 查看 已经添加的定时任务从哪看&#xff1f;这里&#xff1a; 往下滑啦&#xff0c;看你刚才添加的任务&#xff1a;

云原生技术演进之路-(云技术如何一步步演进的,云原生解决了什么问题?)

云技术如何一步步演进的&#xff1f; 云原生解决了什么问题&#xff1f; 物理设备 电脑刚被发明的时候&#xff0c;还没有网络&#xff0c;每个电脑&#xff08;PC&#xff09;&#xff0c;就是一个单机。 这台单机&#xff0c;包括CPU、内存、硬盘、显卡等硬件。用户在单机…