日常记录-SpringBoot整合SpringSecurity(前后分离)+JWT+Redis

该博文不做原理的讲解,直接提供代码,实现SpringBoot整合SpringSecurity+JWT+Redis前后分离。

一、搭建项目

1、构建springboot项目

如果不会搭建springboot项目,可以参考我下面写的博文,如果会搭建springboot项目,直接跳过这步。

第一节:Idea父子项目创建
第二节:springboot整合Mybatis(入门)
第三节:springboot整合Mybatis(mapper的@Select)
第四节:springboot整合Mybatis(controller+service+mapper)完整过程
第五节:springboot整合Mybatis(声明式事务@Transactional)

2、导入依赖

核心依赖

<!--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>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>3.11.0</version>
</dependency>

<!-- fastjson -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.83</version>
</dependency>

其他依赖

<dependencies>

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

        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

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

        <!--简化get set等-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.25</version>
        </dependency>

        <!-- mybatis 支持 SprigBoot -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- mysql 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>

        <!--阿里巴巴数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.20</version>
        </dependency>

        <!--MyBatis Plus 的依赖包 ,比如可以直接使用他封装好的sql语句,selectOne等-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>

        <!--常用工具类 比如数字处理NumberUtils、字符串处理类StringUtils、日期类DateUtils-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

    </dependencies>

3、配置yml

security没有啥配置,主要是配置数据库、redis。mysql和redis就不做安装的教程了,自己去安装下就好,很简单的。

server:
  port: 8081

spring:
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://localhost:3306/study-demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

  # redis配置
  redis:
    host: localhost
    port: 6379
    #我没有给redis设置账号和密码
    #username: guest
    #password: guest


# 日志输出配置
logging:
  level:
    root: info


#jwt 自定义配置
Jwt:
  #jwt签名私钥
  secretKey: sdsdsd23232323

4、启动项目

看到输出Using generated security password:xxxx 就是Springboot整合SpringSecurity成功了
在这里插入图片描述
访问服务http://127.0.0.1:8081/ 这是我自己的ip和端口。你会看到跳转到SpringSecurity自带的页面了

  1. 默认账号 user
  2. 密码就是控制台输出的一串MD5 487eb5c9-0d7e-4a06-b9d5-1c0235b4ca2a

在这里插入图片描述

二、建立相关用户角色权限表

我为了方便,就只建了用户表、权限表和用户权限关联表

1、创建数据库的字符集和排序规则

在这里插入图片描述

2、用户表结构

CREATE TABLE `sys_user` (
  `user_id` int(8) NOT NULL AUTO_INCREMENT,
  `account` varchar(32) DEFAULT NULL COMMENT '账号',
  `user_name` varchar(32) DEFAULT NULL COMMENT '用户名',
  `password` varchar(64) DEFAULT NULL COMMENT '用户密码',
  `last_login_time` datetime DEFAULT NULL COMMENT '上一次登录时间',
  `enabled` tinyint(1) DEFAULT '1' COMMENT '账号是否可用。默认为1(可用)',
  `account_not_expired` tinyint(1) DEFAULT '1' COMMENT '是否过期。默认为1(没有过期)',
  `account_not_locked` tinyint(1) DEFAULT '1' COMMENT '账号是否锁定。默认为1(没有锁定)',
  `credentials_not_expired` tinyint(1) DEFAULT NULL COMMENT '证书(密码)是否过期。默认为1(没有过期)',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';

3、权限表结构

CREATE TABLE `sys_permission` (
  `permission_id` int(8) NOT NULL,
  `permission_code` varchar(32) DEFAULT NULL,
  `permission_name` varchar(32) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';

4、用户与权限关联表结构

CREATE TABLE `sys_user_permission_relation` (
  `user_permission_relation_id` int(8) NOT NULL,
  `user_id` int(8) DEFAULT NULL,
  `permission_id` int(8) DEFAULT NULL,
  PRIMARY KEY (`user_permission_relation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户权限关联表';

5、插入测试数据

INSERT INTO `sys_user` VALUES (1, '888', '小张', '$2a$10$2mO7/KcswzO3SQU7TX3fiOfkypjdOn3tLBezV/tf2IJXdQu1BpxK2', '2023-08-16 09:45:53', 1, 1, 1, 1, '2023-08-09 17:49:20', '2023-08-09 17:49:22');

INSERT INTO `sys_permission` VALUES (1, 'sys:queryUser', '查询用户', '/getUser');

INSERT INTO `sys_user_permission_relation` VALUES (1, 1, 1);

三、创建实体类和Mapper

1、SysUser

用户实体类


import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

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

@Data
@Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。
@TableName("sys_user")
public class SysUser implements Serializable {
    
    

    private static final long serialVersionUID = 915478504870211231L;

    @TableId(value = "user_id", type = IdType.ID_WORKER)
    private Integer userId;

    //账号
    private String account;

    //用户名
    private String userName;

    //用户密码
    private String password;

    //上一次登录时间
    private Date lastLoginTime;

    //账号是否可用。默认为1(可用)
    private Boolean enabled;

    //是否过期。默认为1(没有过期)
    private Boolean accountNotExpired;

    //账号是否锁定。默认为1(没有锁定)
    private Boolean accountNotLocked;

    //证书(密码)是否过期。默认为1(没有过期)
    private Boolean credentialsNotExpired;

    //创建时间
    private Date createTime;

    //修改时间
    private Date updateTime;

}

用户mapper


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysUser;//这是你自己实体类放的路径,记得修改下


public interface SysUserMapper extends BaseMapper<SysUser> {
    
    


}

2、SysPermission

权限实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。
@TableName("sys_permission")
public class SysPermission implements Serializable {
    
    

    @TableId(value = "permission_id", type = IdType.ID_WORKER)
    private Integer permissionId;

    private String permissionCode;

    private String permissionName;

    private String url;
}

权限mapper


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysPermission;//这是你自己实体类放的路径,记得修改下
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface SysPermissionMapper extends BaseMapper<SysPermission> {
    
    


    /**
     * 通过用户id查询用户的权限数据
     * @param userId
     * @return
     */
    @Select({
    
    "<script>"+
            " SELECT p.* FROM"+
            " sys_user u"+
            " LEFT JOIN sys_user_permission_relation r ON u.user_id = r.user_id"+
            " LEFT JOIN sys_permission p on r.permission_id = p.permission_id"+
            " WHERE u.user_id = #{userId}"+
            "</script>"

    })
    List<SysPermission> selectPermissionList(@Param("userId") Integer userId);
}

3、SysUserPermissionRelation

用户与权限关联实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。
@TableName("sys_user_permission_relation")
public class SysUserPermissionRelation  {
    
    

    @TableId(value = "user_permission_relation_id", type = IdType.ID_WORKER)
    private Integer userPermissionRelationId;

    private Integer userId;

    private Integer permissionId;
}

用户与权限关联mapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysUserPermissionRelation;//这是你自己实体类放的路径,记得修改下

public interface SysUserPermissionRelationMapper extends BaseMapper<SysUserPermissionRelation> {
    
    

}

4、配置@MapperScan

千万别忘记配置@MapperScan,不然找不到mapper,StudyApplication 是我启动类的名称。


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.study.test.web.mapper")//这要修改为你自己mapper放的路径
public class StudyApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(StudyApplication.class, args);
    }

}

这些工作完成后,记得重启下项目看下有没有成功,不成功就需要自己调试下了

四、Redis配置

这里的redis主要是为了实现SpringSecurity整合JWT,实现redis的token登录。

1、RedisConfig

package com.study.test.common.config;//这是我存放redisconfig的路径

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

//RedisConfig 配置类
@Configuration
public class RedisConfig {
    
    


    //解决redis可视化乱码问题,方便调试查找问题
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    
    
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        redisTemplate.setKeySerializer(stringRedisSerializer); // key的序列化类型

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }


}

2、RedisUtil

redis工具,解决使用redis代码重复问题

package com.study.test.common.utils;//这是我存放redis工具的路径

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
@Order(-1)
public final class RedisUtil {
    
    

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return 0
     */

    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表中放入数据,如果不存在将创建
     * 0
     *
     * @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 到 -代表所有值
     * @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 0
     */
    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 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推
     * @return 0
     */
    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 0
     */
    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 0
     */
    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;
        }
    }
}

新增完redis的配置后记得重启项目看看有没有问题

五、统一全局返回格式与处理系统异常

1、统一全局返回格式

ApiCode

package com.study.test.common.api;//这是我存放ApiCode 的目录


public enum ApiCode {
    
    

    SUCCESS(200, "成功"),

    SYSTEM_ERROR(500, "操作失败"),

    NOT_FOUND(404,"未找到该资源");

    private final int code;
    private final String msg;

    ApiCode(final int code, final String msg) {
    
    
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
    
    
        return code;
    }

    public String getMsg() {
    
    
        return msg;
    }

}

ApiResult

package com.study.test.common.api;//这是我存放ApiResult的路径


import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class ApiResult<T> implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    //状态码
    private int code;

    //返回数据
    private T data;

    //结果信息
    private String message;

    //时间字符串
    private String time;

    private ApiResult(){
    
    

    }

    //定义成功的构造器
    private ApiResult(T data){
    
    
        this.code = ApiCode.SUCCESS.getCode();
        this.message = ApiCode.SUCCESS.getMsg();
        this.data = data;
        this.time = LocalDateTime.now().toString();

    }

    private ApiResult(ApiCode apiCode){
    
    
        this.code = apiCode.getCode();
        this.message = apiCode.getMsg();
        this.time = LocalDateTime.now().toString();
    }

    private ApiResult(int code,String msg){
    
    
        this.code = code;
        this.message = msg;
        this.time = LocalDateTime.now().toString();
    }

    private ApiResult(ApiCode apiCode,T data){
    
    
        this.code = apiCode.getCode();
        this.message = apiCode.getMsg();
        this.data = data;
        this.time = LocalDateTime.now().toString();
    }

    /**
     * 成功的时候调用
     * @param data
     * @return
     * @param <T>
     */
    public static <T> ApiResult<T> success(T data){
    
    
        return new ApiResult(data);
    }

    /**
     * 根据状态返回结果
     * @param apiCode
     * @return
     * @param <T>
     */
    public static <T> ApiResult<T> build(ApiCode apiCode){
    
    
        return new ApiResult(apiCode);
    }

    /**
     * 根据code和msg返回结果
     * @param code
     * @param msg
     * @return
     * @param <T>
     */
    public static <T> ApiResult<T> build(int code,String msg){
    
    
        return new ApiResult(code,msg);
    }

    /**
     * 根据状态和数据返回结果
     * @param apiCode
     * @param data
     * @return
     * @param <T>
     */
    public static <T> ApiResult<T> build(ApiCode apiCode,T data){
    
    
        return new ApiResult(apiCode,data);
    }


    /**
     * 返回异常结果
     * @param code
     * @param msg
     * @return
     * @param <T>
     */
    public static <T> ApiResult<T> error(int code,String msg){
    
    
        return new ApiResult(code,msg);
    }
}



2、全局系统异常处理

1、自定义异常类

package com.study.test.common.exception;//这是我异常类存放的目录


import com.study.test.common.api.ApiCode;

import lombok.Data;


/**
 * 自定义异常类
 */
@Data
public class BusinessException extends RuntimeException{
    
    

    private int code;

    private String msg;


    public BusinessException(ApiCode apiCode) {
    
    
        super(apiCode.getMsg());
        this.code = apiCode.getCode();
        this.msg = apiCode.getMsg();
    }


}



2、捕获全局异常

package com.study.test.common.exception;//这是我存放全局异常的目录

import com.study.test.common.api.ApiCode;
import com.study.test.common.api.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    

    //自定义异常
    @ExceptionHandler(BusinessException.class)
    public ApiResult systemExceptionHandler(BusinessException e) {
    
    
        log.error("BusinessException全局异常:{}",e);
        return ApiResult.error(e.getCode(), e.getMsg());
    }

    //系统异常
    @ExceptionHandler(Exception.class)
    public ApiResult exceptionHandler(Exception e) {
    
    
        log.error("Exception全局异常:{}",e);
        return ApiResult.error(ApiCode.SYSTEM_ERROR.getCode(), e.getMessage());
    }


}



记得重启看看有没有问题

六、工具类

1、JwtUtils工具类

package com.study.test.common.utils;//这是我存放jwt工具类的目录

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;


@Component
public class JwtUtils {
    
    
    private static String secretKey;

    private static Integer amount = 1800;//jwt的过期周期/秒 默认30分钟

    @Value("${Jwt.secretKey}")
    public void secretKey(String secretKey) {
    
    
        JwtUtils.secretKey =  secretKey;
    }


    /**
     * 创建token
     * @param payloadMap 存储的内容,自定义,一般是用户id
     * @return
     */
    public static String generateToken(Map<String, String> payloadMap) {
    
    

        HashMap headers = new HashMap();

        JWTCreator.Builder builder = JWT.create();

        //定义jwt过期时间
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, amount);


        //payload
        payloadMap.forEach((k, v) ->{
    
    
            builder.withClaim(k, v);
        });


        // 生成token
        String token = builder.withHeader(headers)//header
                //.withClaim("second",amount)//jwt的过期周期/秒,可以用于jwt快过期的时候自动刷新
                .withExpiresAt(instance.getTime())//指定令牌的过期时间
                .sign(Algorithm.HMAC256(secretKey));//签名


        return token;
    }


    /**
     * 校验token是否合法
     * @param token
     * @return
     */
    public static DecodedJWT verifyToken(String token) {
    
    

        /*
        如果有任何验证异常,此处都会抛出异常
        SignatureVerificationException 签名不一致异常
        TokenExpiredException 令牌过期异常
        AlgorithmMismatchException 算法不匹配异常
        InvalidClaimException 失效的payload异常
        */
        DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);



        return decodedJWT;
    }

    /**
     * 获取token信息
     * @param token
     * @return
     */
    public static DecodedJWT getTokenInfo(String token) {
    
    
        DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
        return decodedJWT;
    }

    /**
     * 获取token信息方法
     */
    /*public static Map<String, Claim> getTokenInfo(String token) {

        return JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token).getClaims();
    }*/
}


2、WebUtils工具类

package com.study.test.common.utils;//这是我存放web工具类的目录

import javax.servlet.http.HttpServletResponse;

public class WebUtils {
    
    
    public static String rednerString(HttpServletResponse response, String content) {
    
    
        try{
    
    
            response.setStatus(200);
            response.setContentType("application/json;charset=utf-8");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().print(content);
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
        return null;
    }
}

七、跨域配置

package com.study.test.common.config;//这是我存放跨域的目录


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

//跨越请求配置类
@Configuration
public class CorsConfig {
    
    

    private CorsConfiguration buildConfig() {
    
    
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //  你需要跨域的地址  注意这里的 127.0.0.1 != localhost
        // * 表示对所有的地址都可以访问
        corsConfiguration.addAllowedOrigin("*");  // 1
        //  跨域的请求头
        corsConfiguration.addAllowedHeader("*"); // 2
        //  跨域的请求方法
        corsConfiguration.addAllowedMethod("*"); // 3
        //加上了这一句,大致意思是可以携带 cookie
        //最终的结果是可以 在跨域请求的时候获取同一个 session
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }
    @Bean
    public CorsFilter corsFilter() {
    
    

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //配置 可以访问的地址
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }

}

八、弄完上这些后,前期项目的准备工作就完成了

参考下即可
在这里插入图片描述

九、整合SpringSecurity

1、SpringSecurity认证异常工具类

AuthExceptionUtil

package com.study.test.security.utils;//我存放AuthExceptionUtil的目录


import com.study.test.common.api.ApiResult;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.AuthorizationServiceException;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.csrf.CsrfException;

//认证异常工具类
public class AuthExceptionUtil {
    
    

    public static ApiResult getErrMsgByExceptionType(AuthenticationException e) {
    
    

        if (e instanceof LockedException) {
    
    

            return ApiResult.error(1100,"账户被锁定,请联系管理员!");

        } else if (e instanceof CredentialsExpiredException) {
    
    
            return ApiResult.error(1105,"用户名或者密码输入错误!");

        }else if (e instanceof InsufficientAuthenticationException) {
    
    
            return ApiResult.error(403,"请登录!");

        } else if (e instanceof AccountExpiredException) {
    
    
            return ApiResult.error(1101, "账户过期,请联系管理员!");

        } else if (e instanceof DisabledException) {
    
    
            return ApiResult.error(1102, ("账户被禁用,请联系管理员!"));

        } else if (e instanceof BadCredentialsException) {
    
    
            return ApiResult.error(1105, "用户名或者密码输入错误!");

        }else if (e instanceof AuthenticationServiceException) {
    
    

            return ApiResult.error(1106, "认证失败,请重试!");
        }

        return ApiResult.error(1200, e.getMessage());
    }

    public static ApiResult getErrMsgByExceptionType(AccessDeniedException e) {
    
    

        if (e instanceof CsrfException) {
    
    

            return ApiResult.error(-1001, "非法访问跨域请求异常!");
        } else if (e instanceof CsrfException) {
    
    

            return ApiResult.error(-1002,"非法访问跨域请求异常!");
        } else if (e instanceof AuthorizationServiceException) {
    
    

            return ApiResult.error(1101, "认证服务异常请重试!");
        }else if (e instanceof AccessDeniedException) {
    
    

            return ApiResult.error(4003, "权限不足不允许访问!");
        }

        return ApiResult.error(1200, e.getMessage());
    }

}


自定义授权失败异常处理类

package com.study.test.security.service;//我存放的目录

import com.study.test.common.utils.WebUtils;
import com.study.test.security.utils.AuthExceptionUtil;

import com.alibaba.fastjson.JSON;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

//自定义授权失败异常处理类
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    

        System.out.println("AccessDeniedHandler:暂无权限");
        WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(accessDeniedException)));

    }
}

自定义认证失败异常处理类

package com.study.test.security.service;//我存放的目录

import com.study.test.common.utils.WebUtils;
import com.study.test.security.utils.AuthExceptionUtil;

import com.alibaba.fastjson.JSON;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

//自定义认证失败异常处理类
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException authenticationException) throws IOException, ServletException {
    
    
        System.out.println("AuthenticationEntryPoint:用户未登录");
        WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(authenticationException)));
    }
}

重启下看看有没有问题,security的自定义异常就处理完了,接下来就是如何实现自定义登录了

2、前后分离自定义认证

  1. Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
  2. AuthenticationManager接口:定义了认证Authentication的方法。
  3. UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
  4. UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

UserDetails(用户信息)

我们自定义一个用户信息,要实现security的UserDetails

package com.gzgs.security.web.security.entity;//我存放的路径

import com.alibaba.fastjson.annotation.JSONField;
import com.gzgs.security.web.entity.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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


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

    //用户信息
    private SysUser user;

    //用户权限
    private List<String> permissions;

    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LogUser(SysUser user,List<String> permissions){
    
    

        this.user = user;
        this.permissions = permissions;

    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        // 将权限信息封装成 SimpleGrantedAuthority
        if (authorities != null) {
    
    
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = this.permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());

        return authorities;

    }

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

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

    @Override
    public boolean isAccountNonExpired() {
    
    
        return user.getAccountNotExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return user.getAccountNotLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return user.getCredentialsNotExpired();
    }

    @Override
    public boolean isEnabled() {
    
    
        return user.getEnabled();
    }
}

UserDetailsService(重写loadUserByUsername方法)

在loadUserByUsername方法写自己的登录逻辑,这里面涉及到查数据库

package com.study.test.security.service;//这是我的目录

import com.study.test.security.entity.LogUser;
import com.study.test.web.entity.SysPermission;
import com.study.test.web.entity.SysUser;
import com.study.test.web.mapper.SysPermissionMapper;
import com.study.test.web.mapper.SysUserMapper;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

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

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    


    @Autowired
    private SysPermissionMapper sysPermissionMapper;

    @Autowired
    private SysUserMapper userMapper;



    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    

        //需要构造出 org.springframework.security.core.userdetails.User 对象并返回


        System.out.println("用户名:"+username);
        if (username == null || "".equals(username)) {
    
    
            throw new RuntimeException("用户不能为空");
        }

        //根据用户名查询用户
        SysUser user = userMapper.selectOne(new QueryWrapper<SysUser>().eq("account", username));
        if (user == null) {
    
    
            throw new RuntimeException("用户不存在");
        }



        List<String> permissionsList = new ArrayList<>();

        if (user != null) {
    
    
            //获取该用户所拥有的权限
            List<SysPermission> sysPermissions = sysPermissionMapper.selectPermissionList(user.getUserId());

            // 声明用户授权
            sysPermissions.forEach(sysPermission -> {
    
    
                permissionsList.add(sysPermission.getPermissionCode());

            });
        }

        //返回用户信息
        return new LogUser(user,permissionsList);

    }


    //这是加密的算法,把加密后的密码update你用户表的数据库用户的密码上
    public static void main(String[] args) {
    
    
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123456");
        System.out.println(encode);
    }


}


JwtAuthenticationFilter(登录校验)

package com.study.test.security.filter;



import com.study.test.common.exception.BusinessException;
import com.study.test.common.utils.JwtUtils;
import com.study.test.common.utils.RedisUtil;
import com.study.test.security.entity.LogUser;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;


import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    
    @Autowired
    private RedisUtil redisUtil;

    //每次请求都会执行这个方法
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws BusinessException, ServletException, IOException {
    
    

        // 获取Headers上的token,我命名为token
        String token = request.getHeader("token");

        System.out.println("doFilterInternal:"+token);


        if (StringUtils.isEmpty(token)) {
    
    
            // token不存在 放行 并且直接return 返回
            filterChain.doFilter(request, response);
            return;
        }


        // 解析token
        String userId = null;

        try {
    
    
            DecodedJWT tokenInfo = JwtUtils.verifyToken(token);

            //token过期时间
            Date expiresAt = tokenInfo.getExpiresAt();
            SimpleDateFormat ymdhms = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            System.out.println("token过期时间:"+ymdhms.format(expiresAt));

            //其实这里后端可以做token是否快过期的处理,然后返回新的token给前端
            //或者新写一个刷新tokena接口给前端,让前端自己刷新



            userId = tokenInfo.getClaim("userId").asString();

        } catch (Exception e) {
    
    

            if(e instanceof TokenExpiredException){
    
    
                throw new RuntimeException("登录已过期!");
            }else {
    
    
                throw new RuntimeException("token非法");
            }


        }

        // 获取userid 从redis中获取用户信息
        String redisKey = "login:" + userId;
        LogUser loginUser = (LogUser)redisUtil.get(redisKey);
        if (Objects.isNull(loginUser)) {
    
    
            throw new RuntimeException("用户未登录");
        }

        //将用户信息存入到SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }
}

SecurityConfig(核心)

package com.study.test.security.config;//我的目录



import com.study.test.security.filter.JwtAuthenticationFilter;
import com.study.test.security.service.AccessDeniedHandlerImpl;
import com.study.test.security.service.AuthenticationEntryPointImpl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//@EnableWebSecurity //因为我引入了spring-boot-starter-security,所以不用@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    


    //将authenticationManager注入容器中,再自定义登录接口中获取进行认证
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;



    //注入加密方式--后面就会使用这种方式进行对密码的对比(明文与密码的对比是否匹配)
    // 而不使用默认的密码验证
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }


    //配置放行的规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

        http.csrf().disable() // 关闭csrf验证(防止跨站请求伪造攻击)由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问
        // 不通过session 获取SecurityContext(基于Token不需要session)
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
         //开启权限拦截
        .authorizeRequests()
        // 允许登录接口匿名访问
        .antMatchers("/sysUser/login", "/sysUser/test","/test/**").anonymous()
        .antMatchers("/**.html","/js/**","/css/**","/img/**").permitAll()//放行静态资源
        // 其他请求都需要认证
        .anyRequest().authenticated();

        //将jwtAuthenticationTokenFilter过滤器注入到UsernamePasswordAuthenticationFilter过滤器之前
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        // 认证授权异常自定义处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)//自定义认证失败异常处理类
                .accessDeniedHandler(accessDeniedHandler);//自定义授权失败异常处理类


        // 禁用缓存
        http.headers().cacheControl();

        // 跨域请求配置
        http.cors();
    }



}


你再重新启动的时候,就看不到控制台输出密码了
在这里插入图片描述
我的项目结构
在这里插入图片描述

自定义登录,登出

自定义登录的传参loginUserParam

package com.study.test.web.param;//我的目录

import lombok.Data;

@Data
public class LoginUserParam {
    
    

    //用户名
    private String userName;

    //用户密码
    private String password;
}

LogService

package com.study.test.web.service;//我的目录


import com.study.test.common.api.ApiResult;
import com.study.test.web.param.LoginUserParam;

public interface LogService {
    
    

    ApiResult login(LoginUserParam param);

    ApiResult logOut();
}

LogServiceImpl

package com.study.test.web.service.impl;//我的目录


import com.study.test.common.api.ApiResult;
import com.study.test.common.utils.JwtUtils;
import com.study.test.common.utils.RedisUtil;
import com.study.test.security.entity.LogUser;
import com.study.test.web.param.LoginUserParam;
import com.study.test.web.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Service
public class LogServiceImpl implements LogService {
    
    

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private AuthenticationManager authenticationManager;


    @Override
    public ApiResult login(LoginUserParam param) {
    
    

        // 1 获取AuthenticationManager 对象 然后调用 authenticate() 方法
        // UsernamePasswordAuthenticationToken 实现了Authentication 接口
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(param.getUserName(), param.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //2 认证没通过 提示认证失败
        if (Objects.isNull(authenticate)) {
    
    
            throw new RuntimeException("认证失败用户信息不存在");
        }


        //认证通过 使用userid 生成jwt token令牌
        LogUser loginUser = (LogUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getUserId().toString();

        Map<String, String> payloadMap = new HashMap<>();
        payloadMap.put("userId", userId);
        payloadMap.put("userName", loginUser.getUser().getUserName());
        payloadMap.put("token", JwtUtils.generateToken(payloadMap));

        boolean resultRedis = redisUtil.set("login:" + userId, loginUser);

        if(!resultRedis){
    
    
            throw new RuntimeException("redis连接不上,登录失败");
        }


        return ApiResult.success(payloadMap);
    }

    @Override
    public ApiResult logOut() {
    
    
        // 1 获取 SecurityContextHolder 中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LogUser loginUser = (LogUser)authentication.getPrincipal();
        //2 删除redis 中的缓存信
        String key = "login:"+loginUser.getUser().getUserId().toString();
        redisUtil.del(key);
        return ApiResult.success("退出成功!");

    }

}

SysUserController

package com.study.test.web.controller;



import com.study.test.common.api.ApiResult;
import com.study.test.web.param.LoginUserParam;
import com.study.test.web.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/sysUser")
public class SysUserController {
    
    

    @Autowired
    private LogService logService;


    /**
     * 自定义登录
     * @param param 登录传参
     * @return
     */
    @PostMapping("/login")
    public ApiResult login(@RequestBody LoginUserParam param) {
    
    

        return logService.login(param);

    }


    /**
     * 自定义登出
     * @return
     */
    @PostMapping("/logOut")
    public ApiResult logOut() {
    
    

        return logService.logOut();

    }





}

3、登出登入Postman测试

我自定义登录的请求为/sysUser/login 所以你得看看有没有把这个请求放行了,不然会提示无权限访问

在这里插入图片描述

登入
在这里插入图片描述
登出失败
在这里插入图片描述
登出成功
在这里插入图片描述

4、自定义security权限校验方法

配置自定义权限校验方法

package com.study.test.security.handler;//我的目录


import com.study.test.security.entity.LogUser;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

//自定义security权限校验方法
@Component("syex")
public class SecurityPermissionsExpression {
    
    
    public boolean hasAuthority(String authority){
    
    
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LogUser loginUser = (LogUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

到这里,security的配置基本结束了,下面是我的目录结构
在这里插入图片描述
我的权限【sys:queryUser】
在这里插入图片描述

在控制层配置权限

package com.study.test.web.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/testPreAuthorize")
public class TestPreAuthorizeController {
    
    


    @PostMapping("/hello")
    // 只有sys:queryUser 权限才能访问
    //@PreAuthorize("hasAuthority('sys:queryUser')") //这是没有自定义权限校验方法的默认写法
    @PreAuthorize("@syex.hasAuthority('sys:queryUser')")
    public String hello(){
    
    

        return "hello";
    }
    
    @PostMapping("/hello2")
    // 只有sys:queryUser2 权限才能访问
    @PreAuthorize("@syex.hasAuthority('sys:queryUser2')")
    public String hello2(){
    
    

        return "hello2";
    }
}

Postman测试权限

在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq407995680/article/details/132312399