同样是配置文件,但与application.yml有所不同
spring: cloud: config: (name)url
,从而配置bootstrap.yml的位置)加盐:是指给每个用户随机生成一个字符串,需要和password进行结合
加密:md5由于每次对相同的明文生成的密文相同,因此对加盐后的密码再进行md5加密DigestUtils.md5DigestAsHex(password.getBytes()
JWT是token的一种具体实现方式
token用于身份验证阶段,由服务端签发给客户端;客户端再次登录时可将token加入请求头Header中,服务器可验证token,验证成功即可向客户端返回请求的数据。
可使用拦截器Interceptor
或过滤器Filter
拦截请求,然后使用HttpServletRequest.addHeader(String name,String value)
将token加入请求头
示例:
request.addHeader("Authorization", "Bearer " + token);
//创建拦截器对象
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = "your_token_value"; // 替换为实际的Token值
request.addHeader("Authorization", "Bearer " + token);
return true;
}
}
//在配置类中注册拦截器
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**");
}
}
//创建过滤器对象
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = "your_token_value"; // 替换为实际的Token值
request.addHeader("Authorization", "Bearer " + token);
filterChain.doFilter(servletRequest, servletResponse);
}
}
//在配置类中注册过滤器
@Configuration
public class AppConfig {
@Bean
public FilterRegistrationBean<TokenFilter> tokenFilter() {
FilterRegistrationBean<TokenFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TokenFilter());
registrationBean.addUrlPatterns("/**");
return registrationBean;
}
}
服务端则可使用request.getHeader("name")
获取请求头中的对应字段信息进而解析token
上述Filter基于FilterRegistrationBean实现,另一种方法是直接在Filter的实现类上加上
@WebFilter(urlPatterns = "/**")
的注解,省去了编写配置类的操作
- Interceptor是Spring框架提供的一种机制,它可以直接与Spring的上下文和生命周期集成。
- Filter是Java Servlet规范提供的一种机制,它是在Servlet容器级别进行请求和响应的过滤。
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码(可理解为一种加密方式)后用.进行连接形成最终传输的字符串
.
进行连接形成最终传输的字符串{
"alg": "HS256",
"typ": "JWT"
}
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
这些预定义的字段并不要求强制使用。
除以上默认字段外,还可以自定义私有字段,一般会把包含用户信息的数据放到payload中
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}
默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息
HMAC SHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象
java-jwt
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.10.3version>
dependency>
public class JWTTest {
@Test
public void testGenerateToken(){
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 10);
String token = JWT.create()
.withHeader(new HashMap<>()) // Header
.withClaim("userId", 21) // Payload
.withClaim("userName", "baobao")
.withExpiresAt(calendar.getTime()) // 过期时间
.sign(Algorithm.HMAC256("!34ADAS")); // 签名用的secret
System.out.println(token);
}
}
@Test
public void testResolveToken(){
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!34ADAS")).build();
// 解析指定的token
DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImJhb2JhbyIsImV4cCI6MTU5OTkyMjUyOCwidXNlcklkIjoyMX0.YhA3kh9KZOAb7om1C7o3vBhYp0f61mhQWWOoCrrhqvo");
// 获取解析后的token中的payload信息
Claim userId = decodedJWT.getClaim("userId");
Claim userName = decodedJWT.getClaim("userName");
System.out.println(userId.asInt());
System.out.println(userName.asString());
// 输出超时时间
System.out.println(decodedJWT.getExpiresAt());
}
创建一个类,实现Ordered
和GlobalFilter
接口,并重写需要的方法
getOrder()
:设置优先级,值越小,优先级越高Mono filter(ServerWebExchange exchange,GatewayFilterChain chain)
示例代码:
public class AuthFilter implements Ordered,GlobalFilter{
@Override
public int getOrder(){
return 0;
}
@Override
public Mono<void> filter(ServerWebExchange exchange,GatewayFilterChain chain){
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//2.判断是否是登录
if(request.getURI().getPath().contains("/login")){
//放行
return chain.filter(exchange);
}
//3.获取token
//这里是需要在登录的时候将token加入请求头中
String token = request.getHeaders().getFirst("token");
// 4.判断token是否存在
if(StringUtils.isBlank(token){
response.setStatusCode(HttpStatus.UNAUTHORIZED); //该状态码为401
return response.setComplete();
}
// 5.判断token是否有效
// TODO(这里需要根据token获取Claim,再根据Claim中的字段判断token是否有效)
// 如果token无效,就类似步骤4的处理;如果有效就接步骤6放行
// 6.放行
return chain.filter(exchange);
}
}
模板引擎
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
FreeMarker的优势:性能好,强大的模板语言、轻量
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
server:
port: 8881 #服务端口
spring:
application:
name : test-freemarker #指定服务名
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为o表示立即检查,如果时间大于o会有缓存不方便进行模板测试
suffix : .ftl #指定Freemarker模板文件的后缀名(FreeMarker的默认配置是.ftlh);html等文件格式也支持
template-loader-path: classpath:/templates # 这也是FreeMarker的默认路径
resources
包下创建templates
目录,作为freemarker
的默认模板存放目录templates
下创建01-basic.ftl
模板DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello world !title>
head>
<body>
<b>普通文本String展示:b><br><br>
Hello ${name} <br>
<hr>
<b>对象Student中的数据展示:b><br/>
姓名: ${stu.name}<br/>
年龄: ${stu.age}
<hr>
body>html>
其中的${}
内容为插值表达式,后续需要替换为需要的值
freemarker作为springmvc一种视图格式,默认情况下SpringMVC支持freemarker视图格式。
代码如下:
@GetMapping("basic")
public String test(Model model) {
//1.纯文本形式的参数
model.addAttribute("name", "freemarker");
//2.实体类相关的参数
Student student = new Student();student.setName("小明");
student.setAge(18);
model.addAttribute("stu", student);
return "01-basic";
}
因为在导入FreeMarker的依赖后,其在spring.factories中完成了对FreeMarker的自动配置
而FreeMaker基于SpringMVC,因此Controller能够自动获取Model并通过返回值构建对应的视图
public class FreeMarker{
//自动注入的是freemarker中的Configuration
@Autowired
private Configuration configuration
public void test(){
Template template = configuration.getTemplate("xxx.ftl")
/**
* 合成方法
* 两个参数
* 第一个参数:模型数据
* 第二个参数:输出流
*/
template.process(getData() ,new Filewriter("d:/xxx.html"));
}
private Map getData(){
Map map = new HashMap();
//这里将步骤5中的Model改为Map
//即用Map存数据,然后和模板中的内容进行转换
return map;
}
}
<#-- -->
${}
<# >...#>
示例:<#list>...#list>
<#list ListName as Entity>
<tr>
<td>${Entity_index}td> <#--获取元素下标,第一个元素为0-->
<td>${Entity.property1}td>
<td>${Entity.property2}td>
tr>
#list>
${MapName['KeyName'].property}
即 使用model中attribute设置的Map名称获取到map对象,然后使用['KeyName']
即在[]
中添加键名称来获取Value值,如果Value是类,则可类似地获取属性。${MapName.KeyName.property}
<#list MapName?keys as key>#list>
,即将所有的key封装为list<#if>
<#else>
#if>
freemarker中1个等于和2个等于作用相同
尽量用lt和gt代替小于和大于,其他符号相同
在变量后接两个问号,可判断该元素是否为空,比如ListName??
在变量后接叹号再接值,能设定空值情况下的默认值,比如${name!'------'}
,name为空返回'------'
内建函数语法格式:变量+?+函数名称
示例:
${ListName?size}
${today?datetime}
${today?string("yyyy年MM月")}
分布式文件系统,容易实现扩容
优点:
①拉取镜像
docker pull minio/minio
②运行容器
docker run -p 9000:9000 --name minio -d --restart=always -e "MINO_ACCESS_KEY=minio" -e "MINTO_SECRET_KEY=minio123" -v /home/data:/data -v /home/config:/root/.minio minio/minio server /data
③访问minio:http://192.168.133.128:9000
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
<version>7.1.0version>
dependency>
try {
FileInputStream fileInputStream = new FileInputStream("D:\\xxx.html");
//1.获取minio的链接信息创建一个minio的客户端
//其中包含用户名,密码,地址
MinioClient minioClient = MinioClient.builder().credentials("minio", "minio123").endpoint("http://192.168.133.128:9000").build();
//2.上传
//这里其实也就是需要提供3个字符串类型的参数,对应bucketName、objectName、filePath、contentType
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("xxx.html") //文件名称
.contentType("text/html") //文件类型
.bucket("BucketName") //桶名称 与minio管理界面创建的桶一致即可
.stream(fileInputStream,fileInputstream.available(), -1).build();
minioClient.putObject(putObjectArgs);
//访问路径
System.out.println("http://192.168.133.128:9000/BucketName/xxx.html");
}catch (Exception e) {
e.printStackTrace();
}
想从Minio获取文件操作类似,调用MinioClient的getObject()方法即可,参数少一个contentType
获取到的文件存到FileOutputStream中,也是getObject()方法的一个参数
在这里面也可以将其中的链接信息抽成配置文件
minio:
accessKey: minio
secretKey: minio123
bucket: BucketName
endpoint: http://192.168.133.128:9000
readPath: http://192.168.133.128:9000
然后写一个实体类,用@ConfigurationProperties(prefix = "minio")
去获取配置信息即可
@TableId(value = "id", type = IdType.ID_WORKER)
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.rainbow.model.article.pojos
global-config:
datacenter-id: 1
workerId: 1
@Async
注解@EnableAysnc
注解@Async
注解过的方法,然后将该方法交给线程池进行异步处理@Configuration
public class RabbitMQConfig {
// 定义普通队列
@Bean
public Queue delayQueue() {
return QueueBuilder.durable("delay.queue") // 队列名称
.withArgument("x-dead-letter-exchange", "dead-letter-exchange") // 设置死信交换器
.withArgument("x-dead-letter-routing-key", "dead-letter-routing-key") // 设置死信路由键
.withArgument("x-message-ttl", 5000) // 设置消息的TTL(单位:毫秒)
.build();
}
// 定义死信队列
@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable("dead-letter.queue")
.build();
}
// 定义死信交换器
@Bean
public DirectExchange deadLetterExchange() {
return new DirectExchange("dead-letter-exchange");
}
// 绑定死信队列和死信交换器
@Bean
public Binding deadLetterBinding(Queue deadLetterQueue, DirectExchange deadLetterExchange) {
return BindingBuilder.bind(deadLetterQueue)
.to(deadLetterExchange)
.with("dead-letter-routing-key");
}
}
该配置中,delay.queue的消息TTL到后,将被发送到dead-letter-exchange绑定的dead-letter.queue中,然后只需要监听dead-letter.queue即可
3. redis
zset数据类型:利用去重有序的特点去实现延迟任务。
可以按时间顺序将任务进行排序,然后根据当前时间去获取队列中的任务进行处理
在对数据修改时并不加锁,而是在最后存入数据库时检查版本号,如果版本号和预期相同则完成修改。
@Version
注解@Bean
public MybatisPlusInterceptor optimisticLockerInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new optimisticLockerInnerInterceptor());
return interceptor;
}
redis的sexnx
(SET if not exists)命令在指定的key不存在时,为key设置指定的值。
//方式一
RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();
Boolean result = connection.set(name.getBytes(),
token-getBytes(),
Expiration.from(expire,TimeUnit.MILLISECONDS),
RediSstringCommands.SetOption.SET_IF_ABSENT); //NX
//方式二
private static final String LOCK_KEY = "mylock";
private static final long LOCK_EXPIRE_TIME = 30000; // 锁的过期时间,单位:毫秒
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean acquireLock() {
// 使用 SETNX 命令尝试获取锁
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "locked");
if (lockAcquired != null && lockAcquired) {
// 设置锁的过期时间,防止锁在持有者崩溃时一直存在
redisTemplate.expire(LOCK_KEY, LOCK_EXPIRE_TIME);
return true; // 获取到了锁
}
return false; // 锁已被其他进程持有
}
public void releaseLock() {
// 释放锁,使用 DEL 命令删除锁的键
redisTemplate.delete(LOCK_KEY);
}
原因是在com.baomidou.mybatisplus.extension.service
这个包下的getOne()方法:T getOne(Wrapper
其中第二个参数用于决定是否在查询到多个结果时抛出异常,默认情况是true
解决办法:用list()方法去替代,然后根据需要去获取元素,如果无特殊要求获取第一个元素就行
但使用mybatis-plus相关方法时,需要判断返回值是否为null,为null需要额外处理