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拦截了,需要放行

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

二、实现数据库用户登录

认证流程

在这里插入图片描述

### Spring Security 认证流程详解

基于 Spring Security 的用户认证流程,以下是详细分析:

1. **用户提交用户名和密码**:
   - 用户在登录界面输入用户名和密码,并提交给服务器。
   - `UsernamePasswordAuthenticationFilter` 捕获用户输入的认证信息。

2. **封装 Authentication 对象**:
   - `UsernamePasswordAuthenticationFilter` 将用户输入的用户名和密码封装成一个 `Authentication` 对象,这个对象此时只有用户名和密码,没有权限信息。

3. **调用 Authenticate 方法进行认证**:
   - 认证请求通过 `ProviderManager` 进行处理,它调用各个 `AuthenticationProvider``authenticate` 方法来验证用户身份。
   
4. **调用 DaoAuthenticationProvider 的 authenticate 方法进行认证**:
   - `ProviderManager` 通过 `DaoAuthenticationProvider``authenticate` 方法进行具体的认证逻辑。

5. **查询用户信息**:
   - `DaoAuthenticationProvider` 调用 `loadUserByUsername` 方法查询用户信息。
   - `InMemoryUserDetailsManager` 实现了 `UserDetailsService` 接口,它在内存中查找用户信息。

6. **返回 UserDetails 对象**:
   - `InMemoryUserDetailsManager` 根据用户名查询到对应的用户及其权限信息,并封装成 `UserDetails` 对象返回。

7. **密码验证**:
   - `DaoAuthenticationProvider` 使用 `PasswordEncoder` 对比 `UserDetails` 中的密码和 `Authentication` 中的密码是否匹配。

8. **设置权限信息**:
   - 如果密码匹配,`DaoAuthenticationProvider``UserDetails` 中的权限信息设置到 `Authentication` 对象中。

9. **返回 Authentication 对象**:
   - 认证成功后,`ProviderManager` 返回包含权限信息的 `Authentication` 对象。

10. **存储认证信息**:
   - 最后,`SecurityContextHolder.getContext().setAuthentication` 方法将认证信息存储到 `SecurityContextHolder` 中,以便后续使用。此时,整个认证流程结束,用户的信息和权限信息已经加载并保存到上下文中,供后续授权使用。

### 关键点总结:

- **UsernamePasswordAuthenticationFilter**:捕获用户输入并封装认证对象。
- **ProviderManager**:负责调用各个 `AuthenticationProvider` 进行认证。
- **DaoAuthenticationProvider**:具体的认证逻辑,包括查询用户和验证密码。
- **InMemoryUserDetailsManager**:在内存中查找用户信息的实现类。
- **PasswordEncoder**:负责密码的加密和匹配。
- **SecurityContextHolder**:存储认证后的用户信息和权限信息。

①:自定义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("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Collections.singletonList("*"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

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

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七@归七

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值