Daily record-SpringBoot integrates SpringSecurity (front and rear separation) + JWT + Redis

SpringBoot integrates SpringSecurity

This blog post does not explain the principle, but directly provides the code to realize the separation of SpringBoot's integration of SpringSecurity+JWT+Redis before and after.

1. Build the project

1. Build the springboot project

If you don’t know how to build a springboot project, you can refer to the blog post I wrote below. If you can build a springboot project, just skip this step.

Section 1: Idea parent-child project creation
Section 2: springboot integration Mybatis (getting started)
Section 3: springboot integration Mybatis (@Select of mapper)
Section 4: springboot integration Mybatis (controller+service+mapper) complete process
Section 5 : springboot integrates Mybatis (declarative transaction @Transactional)

2. Import dependencies

core dependencies

<!--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>

other dependencies

<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. Configure yml

There is no configuration for security, mainly to configure the database and redis. There are no installation tutorials for mysql and redis, just install it yourself, it's very simple.

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. Start the project

Seeing the output Using generated security password: xxxx means that Springboot has successfully integrated SpringSecurity.
insert image description here
Access service http://127.0.0.1:8081/ This is my own ip and port. You will see the jump to the page that comes with SpringSecurity

  1. Default account user
  2. The password is a string of MD5 487eb5c9-0d7e-4a06-b9d5-1c0235b4ca2a output by the console

insert image description here

2. Create a table of relevant user roles and permissions

For convenience, I only built the user table, permission table and user permission association table

1. Create the character set and collation of the database

insert image description here

2. User table structure

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. Permission table structure

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. User and permission association table structure

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 test data

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);

3. Create entity classes and Mapper

1、SysUser

User Entity Class


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;

}

user mapper


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


public interface SysUserMapper extends BaseMapper<SysUser> {
    
    


}

2、SysPermission

permission entity class

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;
}

permission 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

User and permission associated entity class

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;
}

User and permission association mapper

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

public interface SysUserPermissionRelationMapper extends BaseMapper<SysUserPermissionRelation> {
    
    

}

4. Configure @MapperScan

Don't forget to configure @MapperScan, otherwise the mapper cannot be found, and StudyApplication is the name of my startup class.


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);
    }

}

After these tasks are completed, remember to restart the project to see if it is successful. If it is not successful, you need to debug it yourself

Four, Redis configuration

The redis here is mainly to realize the integration of JWT with Spring Security and realize the token login of redis.

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 tool to solve the problem of duplication of code using 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;
        }
    }
}

After adding the redis configuration, remember to restart the project to see if there is any problem

5. Unify the global return format and handle system exceptions

1. Unify the global return format

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. Global system exception handling

1. Custom exception class

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. Capture global exceptions

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());
    }


}



Remember to restart to see if there is any problem

6. Tools

1. JwtUtils tool class

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 tool class

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;
    }
}

7. Cross-domain configuration

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);
    }

}

8. After finishing these, the preparatory work for the preliminary project is completed

Just refer to
insert image description here

9. Integrate Spring Security

1. Spring Security authentication exception tool class

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());
    }

}


Custom authorization failure exception handling class

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)));

    }
}

Custom authentication failure exception handling class

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)));
    }
}

Restart to see if there is any problem, the security custom exception is processed, the next step is how to implement custom login

2. Separate front and back custom authentication

  1. Authentication interface: Its implementation class represents the user currently accessing the system and encapsulates user-related information.
  2. AuthenticationManager interface: defines the authentication authentication method.
  3. UserDetailsService interface: the core interface for loading user-specific data. It defines a method to query user information based on username.
  4. UserDetails interface: Provides core user information. The user information obtained and processed through the UserDetailsService according to the username should be encapsulated into a UserDetails object and returned. Then encapsulate this information into the Authentication object.

UserDetails (user information)

We customize a user information to implement 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 (override loadUserByUsername method)

Write your own login logic in the loadUserByUsername method, which involves checking the database

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 (login verification)

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 (Core)

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();
    }



}


When you restart again, you will not see the console output password
insert image description here
My project structure
insert image description here

custom login, logout

Custom login parameter 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. Log out and log in Postman test

My custom login request is /sysUser/login, so you have to check if you have allowed this request, otherwise you will be prompted for unauthorized access

insert image description here


insert image description here
Failed to login and
insert image description here
logout Successfully logged out
insert image description here

4. Customize the security permission verification method

Configure a custom permission verification method

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);
    }
}

At this point, the security configuration is basically over. The following is my directory structure and
insert image description here
my permissions [sys:queryUser]
insert image description here

Configure permissions at the control layer

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 test permissions

insert image description here

insert image description here

Guess you like

Origin blog.csdn.net/qq407995680/article/details/132312399