Spring Security处理登录的流程
基于Spring Security与JWT实现单点登录
SSO(Single 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类型的数据是一个先进后出、后进先出的栈结构:
在学习Redis时,你应该把Redis中的List相像成一个在以上图示的基础上旋转了90度的栈!
在Redis中的List,可以从左侧进行压栈操作,例如:
也可以从右侧进行压栈操作,例如:
并且,从Redis中读取List数据时,都是从左至右读取,通常,为了更加符合平时使用列表的习惯,大多情况下会采取“从右侧压入数据”。
**注意:**在Redis中的List数据,每个元素都同时拥有2个下标,一个是从左至右、从0开始递增编号的,另一个是从右至左、从-1开始递减编号的!
后续,在读取List中的区间时,end表示的元素不可以是相对start更靠左的元素!
同时,-1始终是最后一个元素的下标,所以,当你需要读取整个列表的数据时,start为0,end为-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:1、category:item:1 - 品牌列表:
brand:list、category:list