参考文档-Redis快速入门指南-中文
参考文档-Redis 教程
一、启动并连接
本地启动一个redis
或者用redis cloud
免费账户,都可以。
1 | docker run --name CommonTrain -p 6379:6379 -itd redis:7.2 |
然后下载REDISINSIGHT。
二、支持的数据类型
- 字符串
string
- 哈希
hash
- 列表
list
- 集合
set
- 有序集合
sorted set
- 位图
bitmaps
- 基数统计
hyperLogLogs
参考文档-Redis快速入门指南-中文
参考文档-Redis 教程
本地启动一个redis
或者用redis cloud
免费账户,都可以。
1 | docker run --name CommonTrain -p 6379:6379 -itd redis:7.2 |
然后下载REDISINSIGHT。
string
hash
list
set
sorted set
bitmaps
hyperLogLogs
缓存击穿就是大量并发访问同一个热点数据,一旦这个热点数据缓存失效,则请求压力都来到数据库。
1 | @Override |
使用缓存预热,把数据提前放入缓存,然后根据过期时间,发布合理的定时任务,主动去更新缓存,让热点数据永不过期。
Redis之缓存穿透问题解决方案实践SpringBoot3+Docker
当一种请求,总是能越过缓存,调用数据库,就是缓存穿透。
比如当请求一个数据库没有的数据,那么缓存也不会有,然后就一直请求,甚至高并发去请求,对数据库压力会增大。
key
具有某种规则,那么可以对key增加校验机制,不符合直接返回。Redisson
布隆过滤器null
为value
,也插入redis
缓存,但设置较短的过期时间。docker-compose示例如下,redis.conf从这里下载
1 | redis: |
1 | <!-- redis --> |
1 | spring: |
要演示的代码很简单,就是一个携带courseId
请求过来,调用下面的service
函数,然后查询数据库。
1 | @Override |
当我们使用redis改造时,基本代码如下
1 | @Override |
我这里的id
没规则,所以加不了,跳过。
读取yaml
配置
1 | @Data |
配置RedissonClient
1 | @Slf4j |
把布隆过滤器加到service
,如下
1 | private RBloomFilter<String> bloomFilter; |
当数据库没有此数据,以null
为value
,也插入redis
缓存,但设置较短的过期时间。
1 | //后查询数据库 |
Redis之缓存穿透问题解决方案实践SpringBoot3+Docker
缓存雪崩,指大量的缓存失效,大量的请求又同时落在数据库。主要的一种诱因是key设置的过期时间都一样。
加锁,每次只让一个线程可以访问数据库,随后存入缓存。性能太差。
最简单有效的解决办法是设置不同的过期时间。比如
1 | int timeout = 10 + new Random().nextInt(20); |
使用缓存预热,把数据提前放入缓存,然后根据过期时间,发布合理的定时任务,主动去更新缓存。
缓存预热参考代码如下。
1 | @Component |
至于定时任务,可以使用xxl-job。具体使用方法,可以参考这个文章
Docker部署xxl-job调度器并结合SpringBoot测试
Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback
环境如下
Version | |
---|---|
SpringBoot | 3.2.1 |
spring-amqp | 3.1.1 |
RabbitMq | 3-management |
老版本的spring-amqp
在CorrelationData
上设置ConfirmCallback
。但是今天却突然发现correlationData.getFuture()
没有addCallback
函数了。
查询文档和帖子后,发现ConfirmCallback
和ReturnsCallback
都需要在RabbitTemplate
中设置,同时ConfirmCallback
中默认无法得到消息内容,如果想在ConfirmCallback
中把消息内容存到数据库等地方进行记录,怎么办呢?
参考手册
1 | @Slf4j |
在上面的类中继续添加RabbitTemplate
,并设置ConfirmCallback
和ReturnsCallback
。
1 | @Bean |
原始的CorrelationData
,目前已经无法从中获取消息内容,也就是说现在的ConfirmCallback
无法获取到消息的内容,因为设计上只关注是否投递到exchange
成功。如果需要在ConfirmCallback
中获取消息的内容,需要扩展这个类,并在发消息的时候,放入自定义数据。
1 | public class EnhancedCorrelationData extends CorrelationData { |
在EnhancedCorrelationData
把消息本身放进去,或者如果你有表记录消息,你可以只放入其id
。这样触发ConfirmCallback的时候,就可以获取消息内容。
1 | public void notifyPayResult() { |
微服务OAuth 2.1认证授权可行性方案(Spring Security 6)
Oauth2
停止维护,基于OAuth 2.1
和 OpenID Connect 1.0
的Spring Authorization Server
模块独立于SpringCloud
。
本文开发环境如下:
Version | |
---|---|
Java | 17 |
SpringCloud | 2023.0.0 |
SpringBoot | 3.2.1 |
Spring Authorization Server | 1.2.1 |
Spring Security | 6.2.1 |
mysql | 8.2.0 |
https://spring.io/projects/spring-security#learn
https://spring.io/projects/spring-authorization-server#learn
一个认证服务器(也是一个微服务),专门用于颁发JWT。
一个网关(也是一个微服务),用于白名单判断和JWT校验。
若干微服务。
本文的关键在于以下几点:
这里是官方文档https://spring.io/projects/spring-authorization-server#learn
基本上跟着Getting Started
写完就可以。
新建一个数据库xc_users
。
然后执行jar
里自带的三个sql
。
这一步官方并没有给出,大概因为可以使用内存存储,在简单demo省去了持久化。不建立数据库可能也是可行的,我没试过。
新建一个auth
模块,作为认证服务器。
1 | <dependency> |
1 | server: |
1 | @Configuration |
里面包含诸多内容,有来自Spring Security
的,也有来自的Spring Authorization Server
的。
UserDetailsService
的实例,用于检索用户进行身份验证。1 | @Bean |
1 | @Bean |
Spring Security
过滤器链1 | @Bean |
Spring Security
过滤器链。1 | @Bean |
1 | private JwtAuthenticationConverter jwtAuthenticationConverter() { |
RegisteredClientRepository
实例1 | @Bean |
1 | @Bean |
JwtDecoder
实例1 | @Bean |
Spring Authorization Server
的 AuthorizationServerSettings
实例1 | @Bean |
这里可以设置各种端点的路径,默认路径点开builder()即可看到,如下
1 | public static Builder builder() { |
这里我必须吐槽一下,qnmd /.well-known/jwks.json,浪费我一下午。获取公钥信息的端点现在已经替换成了/oauth2/jwks。
基本上跟着Getting Started
走就行。只不过端点的变动相较于Oauth2
很大,还有使用方法上不同。
在配置RegisteredClient
的时候,我们设置了三种GrantType
,这里只演示两种AUTHORIZATION_CODE
和CLIENT_CREDENTIALS
。
用浏览器打开以下网址,
1 | http://localhost:63070/auth/oauth2/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn |
对应oauth2/authorize
端点,后面的参数和当时设置RegisteredClient
保持对应就行。response_type
一定是code
。
进入到登陆表单,输入lisi
- 456
登陆。
选择all,同意请求。
url
被重定向到http://www.51xuecheng.cn
,并携带一个code
,这就是授权码。
1 | http://www.51xuecheng.cn/?code=9AexK_KFH1m3GiNBKsc0FU2KkedM2h_6yR-aKF-wPnpQT5USKLTqoZiSkHC3GUvt-56_ky-E3Mv5LbMeH9uyd-S1UV6kfJO6znqAcCAF43Yo4ifxTAQ8opoPJTjLIRUC |
使用apifox
演示,postman
,idea-http
都可以。
向localhost:63070/auth
服务的/oauth2/token
端点发送Post
请求,同时需要携带认证信息。
认证信息可以如图所填的方法,也可以放到Header
中,具体做法是将客户端ID和客户端密码用冒号(:)连接成一个字符串,进行Base64
编码放入HTTP
请求的Authorization
头部中,前缀为Basic
。比如Authorization: Basic bXlDbGllbnRJZDpteUNsaWVudFNlY3JldA==
得到JWT
不需要授权码,直接向localhost:63070/auth
服务的/oauth2/token
端点发送Post
请求,同时需要携带认证信息。
至于gateway
基础搭建步骤和gateway
管理的若干微服务本文不做指导。
相较于auth
模块(也就是Authorization Server
),gateway
的角色是Resource Server
。
1 | <dependency> |
在resource
下添加security-whitelist.properties
文件。
写入以下内容
1 | /auth/**=???? |
在全局过滤器中,加载白名单,然后对请求进行判断。
1 | @Component |
在yml
配置中添加jwk-set-uri
属性。
1 | spring: |
新建配置类,自动注入JwtDecoder
。
1 | @Configuration |
在全局过滤器中补全逻辑。
1 | @Component |
携带一个正确的JWT
向gateway
发送请求。
把JWT
写到Header
的Authorization
字段中,添加前缀Bearer
(用空格隔开),向gateway
微服务所在地址发送请求。
gateway日志输出。
颁发JWT都归一个认证服务器管理,校验JWT都归Gateway管理,至于授权,则由各个微服务自己定义。耦合性低、性能较好。
关于授权,可以接着这篇文章。
微服务OAuth 2.1认证授权Demo方案(Spring Security 6)
微服务OAuth 2.1认证授权Demo方案(Spring Security 6)
书接上文
微服务OAuth 2.1认证授权可行性方案(Spring Security 6)
三个微服务
auth
微服务作为认证服务器,用于颁发JWT
。gateway
微服务作为网关,用于拦截过滤。content
微服务作为资源服务器,用于校验授权。以下是授权相关数据库。
user
表示用户表role
表示角色表user_role
关联了用户和角色,表示某个用户是是什么角色。一个用户可以有多个角色menu
表示资源权限表。@PreAuthorize("hasAuthority('xxx')")
时用的就是这里的code
。permission
关联了角色和资源权限,表示某个角色用于哪些资源访问权限,一个角色有多个资源访问权限。 当我们知道userId
,我们就可以知道这个用户可以访问哪些资源,并把这些权限(也就是menu
里的code
字段)写成数组,写到JWT
的负载部分的authorities
字段中。当用户携带此JWT访问具有@PreAuthorize("hasAuthority('xxx')")
修饰的资源时,我们解析出JWT
中的authorities
字段,判断是否包含hasAuthority
指定的xxx
权限,以此来完成所谓的的”授权”。
1 | package com.xuecheng.auth.config; |
这里需要注意几点
BCryptPasswordEncoder
密码加密,在设置clientSecret
时需要手动使用密码编码器。jwtTokenCustomizer
解析UserDetails
然后往JWT
中添加authorities
字段,为了后面的授权。1 | package com.xuecheng.ucenter.service.impl; |
这里需要注意几点
username
就是前端/auth/login
的时候输入的账户名。myAuthService.execute(username)
不抛异常,就默认表示账户存在,此时将password
加入UserDetails
并返回,Spring Authorization Server
对比校验两个密码。myAuthService.execute(username)
根据username
获取用户信息返回,将用户信息存入withUsername
中,Spring Authorization Server
默认会将其加入到JWT
中。Spring Authorization Server
默认不会把authorities(permissions)
写入JWT
,需要配合OAuth2TokenCustomizer
手动写入。这样,auth
微服务颁发的JWT
,现在就会包含authorities
字段。示例如下
1 | { |
1 | @EnableWebFluxSecurity |
这里需要注意几点
oauth2.jwt(Customizer.withDefaults())
,但实际上基于远程auth
微服务开放的jwkSetEndpoint
配置的JwtDecoder
。.cors(cors -> cors.configurationSource(corsConfigurationSource()))
一次性处理CORS
问题。1 | @PreAuthorize("hasAuthority('xc_teachmanager_course_list')") |
使用了@PreAuthorize("hasAuthority('xc_teachmanager_course_list')")
修饰的controller
资源。
1 | @Configuration |
需要注意几点
@EnableMethodSecurity
让@PreAuthorize
生效gateway
一样,需要基于远程auth
微服务开放的jwkSetEndpoint
配置JwtDecoder
。JwtAuthenticationConverter
,让anyRequest().authenticated()
需要验证的请求,除了完成默认的JWT
验证外,还需要完成JwtAuthenticationConverter
指定逻辑。JwtAuthenticationConverter
中将JWT
的authorities
部分形成数组后写入GrantedAuthorities
,这正是spring security6
用于校验@PreAuthorize
的字段。1 | @Slf4j |
把JWT
的信息解析回XcUser
,相当于用户携带JWT
访问后端,后端可以根据JWT
获取此用户的信息。当然,你可以尽情的自定义,扩展。
当用户携带JWT
访问需要权限的资源时,现在可以正常的校验权限了。
RegisteredClient
时注册那么多redirectUri
是因为debug
了很久,才发现获取授权码和获取JWT
时,redirect_uri
参数需要一致。cors
问题,spring secuity6
似乎会一开始直接默认拒绝cors
,导致跨域请求刚到gateway
就寄了,到不了content
微服务,即使content
微服务配置了CORS
的处理方案,也无济于事。Gateway中Spring Security6统一处理CORS
使用了gateway
微服务作为整体的网关,并且整合了Spring Security6
;还有一个system微服务,作为被请求的资源,当浏览器向gateway
发送请求,请求system
资源时,遇到CORS
问题。
于是我在system
对应的controller
上加了@CrossOrigin
,无效;配置WebMvcConfigurer
,也无效。
后来发现,会不会是gateway
的spring security6
在一开始就拦截了CORS
跨域请求,导致根本走不到后面的system
配置。
查询了一波,果然如此。这里记录解决方法。
这是依赖
1 | <dependency> |
这是配置
1 | @EnableWebFluxSecurity |
需要注意的是,在gateway
的spring security
中处理了CORS
问题后,后续的system
什么的,就不需要再二次处理了。因为CORS
是一个浏览器的策略,只要处理一次,告诉浏览器我允许跨域,浏览器收到后就不再阻拦请求了。
微服务OAuth 2.1扩展额外信息到JWT并解析(Spring Security 6)
Version | |
---|---|
Java | 17 |
SpringCloud | 2023.0.0 |
SpringBoot | 3.2.1 |
Spring Authorization Server | 1.2.1 |
Spring Security | 6.2.1 |
mysql | 8.2.0 |
Spring Authorization Server
使用JWT
时,前两部分默认格式如下
1 | { |
现在我们要把用户信息也扩展到JWT
,最简便的方法就是将用户信息写成JSON
字符串替换sub
字段。其中用户信息由xc_user
数据库表存储。
注释掉原来的UserDetailsService
实例。新建一个实现类,如下
1 | @Component |
XcUser
为null
,返回null
,这里处理了用户不存在的情况。UserDetails
中,密码比对过程我们不关心,由框架完成。如果使用了加密算法,这里的password
应该是密文。withUsername
。这样框架生成JWT时就会把用户信息也放进去。是的,你没有猜错,UserDetails
只要返回密码框架就能比对成功,不需要再返回username
。
写一个工具类,通过Security ContextHolder.getContext()
上下文获取Authentication
然后解析JWT
。
1 | @Slf4j |
JwtAuthenticationToken
是Spring Authorization Server
的一个类,可以帮助我们解析JWT
。
从SecurityContextHolder.getContext().getAuthentication()
解析我们放进去的XcUser的方法不止一种,将其打印出来就可以看出,有多个地方包含了XcUser
,例如。
1 | { |
Spring Authorization Server Spring Security密码加密
以BCryptPasswordEncoder
举例。
直接将其注册成PasswordEncoder
的Bean
即可。
1 | @Bean |
使用了加密算法后,无论是RegisteredClient
的密码还是UserDetailsService
的密码,都会以密文的方式存储在服务器上。
但是前端输入的密码仍然是以明文
的方式出现并传到服务器,之后服务器会对明文进行相同的手段(指对同样的明文,密文相同)加密,比较两个密文是否一致。
1 | @Bean |
RegisteredClient
中clientSecret
仍然需要提供密文。
是因为,加密这个行为,只在服务器校验前端发送的明文时使用,至于对照物,则是代码中提供好的密文,所以这个需要提供密文。
对于UserDetailsService
也是,密码也需要提供现成的密文形式。
下面的代码中数据库保存的是password密文。
1 | @Component |