Day14-基于Spring Security与JWT实现单点登录

236 阅读7分钟

Spring Security处理登录的流程

image.png

基于Spring Security与JWT实现单点登录

SSOSingle Sign On):单点登录,表现为在集群或分布式系统中,客户端只需要在某1个服务上登录成功,后续访问其它服务器都可以被识别身份。

使用JWT来表示用户的身份信息,本身就是支持单点登录的,因为各服务器端只需要有同样的解析JWT的程序即可!

当在csmall-passport中完成认证与权限后,可以将部分代码复制到csmall-product中,使得csmall-product中的许多请求也是需要通过认证才允许访问的,并且进行访问权限的控制!需要复制并调整的代码文件有:

  • 复制依赖项:spring-boot-starter-security / jjwt / fastjson

  • 复制配置文件中的自定义属性:csmall.jwt.secret-key / csmall.jwt.duration-in-minute

  • 复制LoginPrincipal

  • 复制ServiceCode,覆盖csmall-product中原有的文件

  • 复制GlobalExceptionHandler,覆盖csmall-product中原有的文件

  • 复制JwtAuthorizationFilter

  • 复制Spring Security配置类

    • 删除PasswordEncoder@Bean方法
    • 删除AuthenticationManager@Bean方法
    • 删除URL白名单中“管理员登录”的地址

Redis

关于Redis

Redis是一种基于内存的,使用K-V结构存储数据的NoSQL非关系型数据库。

提示:Redis也会占用磁盘空间,并自动将数据同步到磁盘中,所以,存入到Redis中的数据,即使重启电脑,再次开机时,Redis中仍有此前存入的数据。但是,Redis在读写过程中仍是基于内存的。

Redis的主要作用是缓存数据,通常,会将关系型数据库(例如MySQL等)中的数据读取出来,并写入到Redis中,后续,当需要获取数据时,将优先从Redis中获取,而不是从关系型数据库中获取!

由于Redis是基于内存的,读写效率远高于基于磁盘存储数据的关系型数据库,同时,Redis相比关系型数据库来说,单次查询耗时更短,所以,可以承受更加的查询访问量,并减少对关系型数据库的访问,可以起到“保护”关系型数据库的作用!

Redis的数据类型

Redis中的经典数据类型有5种:string / list / set / hash / z-set

  • 在Java语言中的简单数据类型,在Redis中对应的都是string类型

另外,还有:bitmap / hyperloglog / Geo / 流

Redis的常用命令

当登录Redis的客户端后(命令提示符变成127.0.0.1:6379>状态后),可以:

  • set KEY VALUE:存入数据,例如:set username root,如果反复使用同一个KEY执行此命令,后续存入的VALUE会覆盖前序存入的VALUE,相当于“修改数据”,如果使用的是此前从未使用过的KEY,则会新增数据

  • get KEY:取出数据,例如:get username,如果KEY存在,则取出对应的数据,如果KEY不存在,则返回(nil),相当于Java中的null

  • keys PATTERN:根据模式(PATTERN)获取KEY,例如:keys username,如果KEY存在,则返回,如果不存在,则返回(empty list or set),在PATTERN处可以使用星号(*)作为通配符,例如:keys username*可以返回当前Redis中所有以username作为前缀的KEY,返回的多个KEY在显示时是无序的,甚至,还可以使用keys *查询当前Redis中所有KEY

    • **注意:**在生产环境中,禁止使用此命令
  • del KEY [KEY ...]:删除指定KEY对应的数据,例如:del username,将返回删除了多少条数据

  • flushdb:清空当前数据库

更多命令可参考:www.cnblogs.com/antLaddie/p…

Redis中的List类型数据

在Redis中,List类型的数据是一个先进后出、后进先出的栈结构:

image.png

在学习Redis时,你应该把Redis中的List相像成一个在以上图示的基础上旋转了90度的栈!

在Redis中的List,可以从左侧进行压栈操作,例如:

image.png

也可以从右侧进行压栈操作,例如:

image.png

并且,从Redis中读取List数据时,都是从左至右读取,通常,为了更加符合平时使用列表的习惯,大多情况下会采取“从右侧压入数据”。

**注意:**在Redis中的List数据,每个元素都同时拥有2个下标,一个是从左至右、从0开始递增编号的,另一个是从右至左、从-1开始递减编号的!

image.png

后续,在读取List中的区间时,end表示的元素不可以是相对start更靠左的元素!

同时,-1始终是最后一个元素的下标,所以,当你需要读取整个列表的数据时,start0end-1

Redis编程

在Spring Boot项目中,实现Redis编程需要添加依赖项:

<!-- Spring Boot支持Redis编程 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在读写Redis中的数据时,主要使用RedisTemplate工具类的对象,通常,会使用配置类中的@Bean方法来配置这个类的对象,以便于需要读写Redis时可以直接自动装配此对象。

在项目的根包下创建config.RedisConfiguration类,并配置:

package cn.tedu.csmall.product.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
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.RedisSerializer;

import java.io.Serializable;

/**
 * Redis配置类
 *
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Slf4j
@Configuration
public class RedisConfiguration {

    public RedisConfiguration() {
        log.debug("创建配置类对象:RedisConfiguration");
    }

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.json());
        return redisTemplate;
    }

}

关于string、List、Set类型数据的基本访问:

@Slf4j
@SpringBootTest
public class RedisTest {

    // 提示:当写入的数据包含“非ASCII码字符”时,在终端窗口中无法正常显示,是正常现象,也并不影响后续读取数据
    // opsForValue():返回ValueOperations对象,只要是对Redis中的string进行操作,都需要此类型对象的API
    // opsForList():返回ListOperations对象,只要是对Redis中的list进行操作,都需要此类型对象的API
    // opsForSet():返回SetOperations对象,只要是对Redis中的set进行操作,都需要此类型对象的API
    @Autowired
    RedisTemplate<String, Serializable> redisTemplate;

    // 存入字符串类型的值
    @Test
    void set() {
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        opsForValue.set("username", "好好学习");
        log.debug("向Redis中存入Value类型(string)的数据,成功!");
    }

    // 读取字符串类型的值
    @Test
    void get() {
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        String key = "username";
        Serializable value = opsForValue.get(key);
        log.debug("从Redis中读取Value类型(string)数据,Key={},Value={}", key, value);
    }

    // 可以存入对象
    @Test
    void setObject() {
        Brand brand = new Brand();
        brand.setId(1L);
        brand.setName("大米");

        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        opsForValue.set("brand1", brand);
        log.debug("向Redis中存入Value类型(string)的数据,成功!");
    }

    // 读取到的对象的类型就是此前存入时的类型
    @Test
    void getObject() {
        try {
            ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
            String key = "brand1";
            Serializable value = opsForValue.get(key);
            log.debug("从Redis中读取Value类型(string)数据,完成!");
            log.debug("Key={},Value={}", key, value);

            log.debug("Value的类型:{}", value.getClass().getName());
            Brand brand = (Brand) value;
            log.debug("可以将Value转换回此前存入时的类型!");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    // 如果Key并不存在,则读取的结果为null
    @Test
    void getEmpty() {
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        String key = "EmptyKey";
        Serializable value = opsForValue.get(key);
        log.debug("从Redis中读取Value类型(string)数据,Key={},Value={}", key, value);
    }

    // 使用keys命令对应的API
    @Test
    void keys() {
        String pattern = "username*";
        Set<String> keys = redisTemplate.keys(pattern); // keys *
        log.debug("根据模式【{}】查询Key,结果:{}", pattern, keys);
    }

    // 删除指定的Key的数据
    @Test
    void delete() {
        String key = "age";
        Boolean result = redisTemplate.delete(key);
        log.debug("根据Key【{}】删除数据,结果:{}", key, result);
    }

    // 批量删除指定的Key的数据,与删除某1个数据的方法相同,只是参数列表不同
    @Test
    void deleteBatch() {
        Set<String> keys = new HashSet<>();
        keys.add("username");
        keys.add("username1");
        keys.add("username2");

        Long count = redisTemplate.delete(keys);
        log.debug("根据Key【{}】批量删除数据,删除的数据的数量:{}", keys, count);
    }

    // 向Redis中存入List类型的数据
    // 注意:反复执行相同的代码,会使得同一个List中有多份同样的数据
    @Test
    void rightPush() {
        List<Album> albumList = new ArrayList<>();
        for (int i = 1; i <= 8; i++) {
            Album album = new Album();
            album.setId(i + 0L);
            album.setName("测试相册-" + i);
            albumList.add(album);
        }

        ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
        String key = "album:lists";
        for (Album album : albumList) {
            opsForList.rightPush(key, album);
        }
        log.debug("向Redis中写入List类型的数据,完成!");
    }

    // 从Redis中取出List类型的数据列表
    @Test
    void range() {
        String key = "albums";
        long start = 0L;
        long end = -1L;

        ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
        List<Serializable> serializableList = opsForList.range(key, start, end);
        log.debug("从Redis中读取Key【{}】的List数据,数据量:{}", key, serializableList.size());
        for (Serializable serializable : serializableList) {
            log.debug("{}", serializable);
        }
    }

    // 从Redis中读取List的长度
    @Test
    void size() {
        String key = "albums";

        ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
        Long size = opsForList.size(key);
        log.debug("从Redis中读取Key【{}】的List的长度,结果:{}", key, size);
    }

    // 向Redis中存入Set类型的数据
    // Set中的元素必须是唯一的,如果反复添加,后续的添加并不会成功
    @Test
    void add() {
        String key = "albumItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Long add = opsForSet.add(key, "album:item:100");
        log.debug("向Redis中存入Set类型的数据,结果:{}", add);
    }

    // 向Redis中批量存入Set类型的数据
    @Test
    void addBatch() {
        String key = "brandItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Long add = opsForSet.add(key, "brand:item:1", "brand:item:2", "brand:item:3");
        log.debug("向Redis中存入Set类型的数据,结果:{}", add);
    }

    // 从Redis中取出Set类型的数据集合
    @Test
    void members() {
        String key = "albumItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Set<Serializable> members = opsForSet.members(key);
        log.debug("从Redis中读取Key【{}】的Set数据,数据量:{}", key, members.size());
        for (Serializable serializable : members) {
            log.debug("{}", serializable);
        }
    }

    // 从Redis中读取Set的长度
    @Test
    void sizeSet() {
        String key = "albumItemKeys";

        SetOperations<String, Serializable> opsForSet = redisTemplate.opsForSet();
        Long size = opsForSet.size(key);
        log.debug("从Redis中读取Key【{}】的Set的长度,结果:{}", key, size);
    }

}

关于Key的格式

在绝大多数Redis的可视化工具(例如Another Redis Desktop Manager)中,会自动处理Key中的冒号(:),将多个前缀相同的Key放在相同的“文件夹”中!

其中,冒号(:)是默认的建议的分隔符号,但并不一定必须使用冒号,也可以改为其它符号!

注意:使用冒号作为分隔符,也只是为了更加方便的使用相关的可视化工具,对Redis的数据读写并没有什么影响!

另外,Key的定义,应该是多层级的,并且,应该保证同类的数据一定具有相同的组成部分,不同类的数据一定与其它数据能明确的区分开来!

例如:

  • 每个品牌数据:brand:item:1category:item:1
  • 品牌列表:brand:listcategory:list