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

news2024/11/18 7:38:03

一、准备工作

①:创建一个新项目

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攻击也就不用担心了。

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

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

相关文章

时间敏感网络TSN的车载设计实践: 802.1Qbv协议

▎概述 IEEE 802.1Qbv[1]是TSN系列协议中备受关注的技术之一&#xff0c;如图1所示&#xff0c;它定义了一种时间感知整形器&#xff08;Time Aware Shaper&#xff0c;TAS&#xff09;&#xff0c;支持Qbv协议的交换机可以按照配置好的门控列表来打开/关闭交换机出口队列&…

某上市证券公司:管控文件交换行为 保护核心数据资产

客户简介 某上市证券公司成立于2001年&#xff0c;经营范围包括&#xff1a;证券经纪、证券投资咨询、证券承销与保荐、证券自营等。经过多年发展&#xff0c;在北京、上海、深圳、重庆、杭州、厦门等国内主要中心城市及甘肃省内各地市设立了15家分公司和80余家证券营业部。20…

字符串函数的模拟实现(strlen,strcpy,strcat,strcmp,strstr)(图文并茂,清晰易懂)

目录 1. strlen函数2. strcpy函数3. strcat函数4. strcmp函数5. strstr函数 个人专栏&#xff1a; 《零基础学C语言》 1. strlen函数 strlen函数&#xff08;Get string length&#xff09;的功能是求字符串长度 使用注意事项&#xff1a; 字符串以 ‘\0’ 作为结束标志&…

如何预防数据泄露?六步策略帮您打造企业信息安全壁垒

大家好&#xff01;我是恒小驰&#xff0c;今天我想和大家聊聊一个非常重要的话题——如何预防数据泄露。在这个数字化的时代&#xff0c;数据已经成为了我们生活中不可或缺的一部分。然而&#xff0c;随着数据的价值日益凸显&#xff0c;数据泄露的风险也随之增加。企业应该如…

windows电脑定时开关机设置

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

Lora学习资料汇总

目录 LoRa联盟 Semtech lora网关供应商: LoRaMAC API文档 论坛 开发板 主流技术对比分析 LoRa网络距离模拟测试方法 LoRa应用 Lora LoraWAN教程 LoRa联盟 LoRa联盟&#xff1a;LoRaWAN规范的制定组织 https://www.lora-alliance.org/ LoRa技术白皮书&#xff1a;htt…

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

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

SAP指针Field-Symbols:<FS>用法及实例

指针Field-Symbols:用法 内部字段定义 : FIELD-SYMBOLS: [TYPE>] 一、在ABAP编程中使用非常广泛&#xff0c;类似于指针&#xff0c;可以指代任何变量。 当不输入时&#xff0c;继承赋给它的变量的所有属性 当输入时&#xff0c;赋给它的变量必须与同类型。 举个简…

一文带你了解多文件混淆加密

目录 &#x1f512; 一文带你了解 JavaScript 多文件混淆加密 ipaguard加密前 ipaguard加密后 &#x1f512; 一文带你了解 JavaScript 多文件混淆加密 JavaScript 代码多文件混淆加密可以有效保护源代码不被他人轻易盗取。虽然前端的 JS 无法做到纯粹的加密&#xff0c;但通…

Grails 启动

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

技术部工作职能规划分析

前言 技术部的职能。以下是一个基本的框架,其中涵盖了技术部在公司中的关键职能和子职能。 主要职能 技术部门的主要职能分为以下几个板块: - 技术规划与战略: 制定技术规划和战略,与业务团队合作确定技术需求。 研究和预测技术趋势,引领公司在技术创新和数字化转型方…

外网讨论疯了的神秘模型Q*(Q-Star)究竟是什么?OpenAI的AGI真的要来了吗 | 详细解读

大家好&#xff0c;我是极智视界&#xff0c;欢迎关注我的公众号&#xff0c;获取我的更多前沿科技分享 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码和资源下载&#xff0c;链接&#xff1a;https://t.zsxq.com/0aiNxERDq 这几天&#xff0c;…

分布式篇---第三篇

系列文章目录 文章目录 系列文章目录前言一、什么是补偿事务?二、消息队列是怎么实现的?三、那你说说Sagas事务模型前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。…

c语言新龟兔赛跑

以下是一个使用C语言编写的新的龟兔赛跑游戏&#xff1a; #include <stdio.h>#include <stdlib.h>#include <time.h>int main() { int distance, turtle_speed, rabbit_speed, turtle_time, rabbit_time, rabbit_lead; srand(time(NULL)); // 随机数种…

F盘满了变成红色怎么清理?这4个简单方法记得收藏!

“因为我电脑的磁盘比较多&#xff0c;我通常会把一些比较重要的文件放在F盘中。但是很奇怪&#xff0c;我的F盘用着用着就满成红色了&#xff0c;这该怎么办呢&#xff1f;应该怎么进行清理呢&#xff1f;” 我们在使用电脑时都会发现&#xff0c;电脑上有很多的磁盘。我们可以…

企业数字化转型转什么?怎么转?这份攻略请收好

目录 -01-数字化转型“是什么” -02-数据驱动推动企业数字化转型 -03-企业数字化转型的行动路线图 数字化转型&#xff0c;转什么&#xff1f;怎么转&#xff1f;这些问题仍在困扰不少企业&#xff0c;也是每个企业转型升级不得不思考的重要问题。对此&#xff0c;中关村数字…

SQL语句的用法

目录 关系统型数据库相 联系类型 数据库的正规化分析 第一范式&#xff1a;1NF 范式主要就是减产冗余 第二范式&#xff1a;2NF 第三范式&#xff1a;3NF 字符串(char,varchar,text) char和varchar的比较&#xff1a; 修饰符&#xff0c; 主键&#xff0c;唯一键 常见…

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

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

EMG肌肉电信号处理合集(二)

本文主要展示常见的肌电信号特征的提取说明。使用python 环境下的Pysiology计算库。 目录 1 肌电信号第一次burst的振幅&#xff0c; getAFP 函数 2 肌电信号波长的标准差计算&#xff0c;getDASDV函数 3 肌电信号功率谱频率比例&#xff0c;getFR函数 4 肌电信号直方图…

视频录制怎么弄?这里有一份超全攻略!

视频录制已成为一项常见任务&#xff0c;无论是为了保存在线学习资料&#xff0c;还是为了记录游戏精彩瞬间&#xff0c;它都可以轻松实现&#xff0c;可是您知道视频录制怎么弄吗&#xff1f;本文将介绍两种视频录制的方法&#xff0c;我们将分步骤详细说明&#xff0c;让您轻…