开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

入职难题Git多人合作开发流程

发表于 2021-08-06

一、创建项目与管理

创建项目和管理项目都是管理账号需要做的事情,如果只是合作开发不进行管理,只需要浏览第二部分的内容即可。

1.创建项目

登录代码托管网站,点击添加项目,如下图所示:

填写相应的项目信息,如下图所示:

完成会生成项目的url,复制url后面会使用到,使用指令时需要注意每个项目的都不一样,如下图所示:

在本地创建项目文件,并创建项目说明文件“README.md”,如下图所示:

打开git执行如下命令操作
初始化git bash客户端,进入创建的项目文件夹执行如下命令(也可以想项目文件夹中右键打开,省去cd命令)

1
c复制代码git init

把文件添加到缓冲区,并添加注释信息

1
2
c复制代码git add README.md
git commit -m "first commit"

注:在 Linux 系统中,commit 信息使用单引号 ‘,Windows 系统,commit 信息使用双引号 “。
推送创建的仓库,其中url是之前复制的

1
2
c复制代码git remote add origin url
git push -u origin master

执行以上命令操作后,项目便创建成功了,如下图所示:

2.添加协作者

点击仓库设置,添加协作者,及协作者的操作权限,如下图所示:

这样简单的git项目就创建完成了。能访问到项目的协作者便可以开始项目的编写了。

3.合并请求管理

当有人发起合并请求时,会有相应的信息提醒,可以查看具体的请求说明,如下图所示:

查看明细后,如果觉得没问题后,点击合并请求即可完成代码的合并。如下图所示:

合并完成后,协作人员只需要拉取一下主分支的代码即更新本次更改的内容。

二、git仓库使用

1.派生主分支

登录协作者的账号即可使用相应的项目,如下图所示:

选择自己需要的项目并单击进入,此时便可以看到克隆的url,合作中不建议直接克隆主分支的项目,需要派生自己的分支,如下图所示:

派生完成后会发现项目的路径与主分支的不同,复制个人派生的url,如下图所示:

2.配置远程仓库

打开git bash 使用git clone url命令克隆分支仓库,其中url是个人派生出来的url

1
c复制代码git clone url

添加远程仓库fork的上游主库,其中rul是主分支的url

1
c复制代码git remote add upstream url

查看仓库的设置地址

1
c复制代码git remote -v

能看到origin和upstream的地址,则说明配置成功,如图所示:

到此仓库配置已经完成,接下来便可以进行开发了。

2.更新本地仓库

每次编写代码时,记得同步远程仓库到本地资源库,保证本地仓库和远程仓库的代码一直性

1
2
c复制代码git pull upstream master
git pull origin master

注意:其中origin是更新个人分支到本地仓库,upstream是更新主分支到本地资源库,因为个人分支的代码多数只能自己更改,一般情况下个人分支的代码和本地基本一致所以更新origin的频率会少一些。主要是主分支由于协作的人较多,代码变动很大。

3.提交代码

提交代码之前记得再次同步主分支的代码,也就是说执行以下步骤是记得使用git pull upstream master,这样能保证在合并时避免和主分支的代码产生冲突。
添加所有更新至本地缓存

1
c复制代码git add .

查看缓存区状态

1
c复制代码git status

提交到说明,便于版本管理

1
c复制代码git commit -m "提交说明"

提交到远程个人仓库(个人仓库名+分支名)

1
c复制代码git push origin master

这样已经完成代码的提交,提交完成后还需要将自己分支的代码合并到主分支。

4.代码合并

去远程管理仓库进入到个人分支,点击创建合并请求,如下图所示:

选择需要合并到的分支以及拉去代码的位置,如下图所示:

完成后点击创建合并请求并填写合并请求的说明已经更改代码的功能,便于管理员对代码进行管理。如
下图所示:

到此个人开发的流程已经完成了,最后只需要理员同意合并请求便可以在主分支看到个人更改的代码。

三、git其他指令

1.强制拉取覆盖

强制拉取个人分支,并覆盖本地仓库,主要用于自己删除本地文件后无法通过更新下载已删除的文件时使用,当然可以回滚至上一版本。

1
2
3
c复制代码git fetch --all
git reset --hard origin/master
git pull

2.本地指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码git config --list									#查看配置信息
git init #初始化仓库
git add 1.txt #添加文件至缓存
git add . #添加所有文件至缓存
git rm 1.txt #删除文件
git status #查看仓库状态
git commit –m "test" #提交说明
git rm 1.txt #删除文件
git commit -m “test” #删除相应的提交
git diff a.txt #查看a.txt文件更改的内容
git log #查看提交记录
git reset --hard HEAD^ #回滚上一个版本
git reset --hard HEAD~n #回滚n个版本
git xxx --help #查看指令帮助

3.本地仓库上传至远程仓库

1
2
3
4
c复制代码git pull origin master								#拉取远程主分支
git pull --rebase origin master #拉取本地分支
git push -u origin master #提交代码至个人分支
git push -u -f origin master #强制上传代码至个人分支

4.远程仓库指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码git clone url										#克隆仓库
git remote add #添加/关联一个远程仓库,默认名是origin
git remote remove origin #删除远程库的 origin 别名
git remote add upstream url #添加一个将被同步给fork远程的上游仓库
git fetch upstream #从上游仓库fetch分支和提交点,传送到本地,并会被存储在一个本地分支 upstream/master
git remote #查看远程库的别名
git remote –v #查看远程库的别名和仓库地址
git push origin master #把本地 master 分支推送到别名为 origin 的远程库
git branch #查看当前所有的分支,默认只有master 分支
git branch test #创建 test 分支
git branch –d test #删除 test 分支
git checkout test #从当前分支切换到 test 分支
git checkout –b dev #创建 dev 分支,并切换到 dev 分支上
git merge dev #在当前分支上合并 dev 分支
git merge upstream/master #把 upstream/master 分支合并到本地 master 上
git merge upstream/dev #把 upstream/dev 分支合并到本地 dev 上

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

线程安全的集合 线程安全的集合

发表于 2021-08-06

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

线程安全的集合

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

不安全的集合

日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的,举个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码package com.wangscaler.securecollection;
​
import java.util.*;
​
/**
* @author WangScaler
* @date 2021/8/5 15:25
*/
​
public class NotSafeCollection {
   public static void main(String[] args) {
​
       List<Integer> list = new ArrayList<>();
       int number = 3;
       Random random = new Random();
       for (int i = 0; i < number; i++) {
           new Thread(() -> {
               int randomNumber = random.nextInt(10) % 11;
               list.add(randomNumber);
          }, String.valueOf(i)).start();
      }
       while (Thread.activeCount() > 2) {
           Thread.yield();
      }
       list.forEach(System.out::println);
  }
}

你的本意可能是起三个线程去填充这个ArrayList集合,然而结果却总是超出意料,上述的代码执行的结果可能是null;null;3也可能是null;2;3,当然也有可能达到你的预期效果1,2,3。

为什么会出现这种情况呢?我们翻开源码

1
2
3
4
5
java复制代码public boolean add(E e) {
   ensureCapacityInternal(size + 1);  // Increments modCount!!
   elementData[size++] = e;
   return true;
}

但是三行代码的执行的时候字节码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码 0 aload_0
1 aload_0
2 getfield #284 <java/util/ArrayList.size : I>
5 iconst_1
6 iadd
7 invokespecial #309 <java/util/ArrayList.ensureCapacityInternal : (I)V>
10 aload_0
11 getfield #287 <java/util/ArrayList.elementData : [Ljava/lang/Object;>
14 aload_0
15 dup
16 getfield #284 <java/util/ArrayList.size : I>
19 dup_x1
20 iconst_1
21 iadd
22 putfield #284 <java/util/ArrayList.size : I>
25 aload_1
26 aastore
27 iconst_1
28 ireturn

在程序执行的时候,线程是交替执行的。我们上面的例子有三个线程,分别是线程1、线程2、线程3。

看字节码 putfield #284 <java/util/ArrayList.size : I>是在aastore之前的,也就是说size写回主内存是在数组写回之前的。所以就有可能出现线程1数组还没写进去的时候,线程2就开始执行了,此时size已经是加一之后的了,所以此时线程2将新值保存到数组里,也就出现了null;2;3的情况。

HashMap也是线程不安全的,同样HashSet也是,因为HashSet的底层就是HashMap,话不多说上源码

1
2
3
java复制代码public HashSet() {
   map = new HashMap<>();
}

那HashMap和HashSet的区别是啥呢?我们知道HashMap是键值对的形式,而HashSet的value值是固定的,源码如下。

1
2
3
4
java复制代码private static final Object PRESENT = new Object();
public boolean add(E e) {
   return map.put(e, PRESENT)==null;
}

HashMap除了线程不安全,在jdk8之前HashMap扩容的时候还会产生死链的情况,

如何解决?

1、遗留的安全集合

  • Vector用于ArrayList
  • HashTable用于HashMap

举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = new Vector<>(); 即可。为什么这个就可以解决问题呢?打开源码

1
2
3
4
5
6
java复制代码public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
}

是个同步方法,通过互斥锁使问题得到解决,但是在多线程中极大的影响效率,已经被弃用了。HashTable同样因为同步的问题,被弃用了。

2、Collections

通过Collections的修饰将不安全的集合变成安全的集合。

  • synchronizedList用于ArrayList
  • synchronizedMap用于HashMap
  • synchronizedSet用于HashSet

在原有不安全集合上包装了一个线程安全的类,来达到预期的效果。举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = Collections.synchronizedList(new ArrayList<>());即可解决。原理是什么呢?还是看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码SynchronizedList(List<E> list) {
   super(list);
   this.list = list;
}
public E get(int index) {
           synchronized (mutex) {return list.get(index);}
      }
       public E set(int index, E element) {
           synchronized (mutex) {return list.set(index, element);}
      }
       public void add(int index, E element) {
           synchronized (mutex) {list.add(index, element);}
      }
       public E remove(int index) {
           synchronized (mutex) {return list.remove(index);}
      }

在所有的方法上加了synchronized修饰,从而达到同步的效果。

3、JUC

  • Bloacking
+ ArrayBlockingQueue
+ LinkedBlockingQueue
+ LinkedBlockingDeque
+ ...
  • CopyOnWrite
+ CopyOnWriteArrayList对应ArrayList
+ CopyOnWriteArraySet用于HashSet,底层还是CopyOnWriteArrayList
  • Concurrent(推荐使用,弱一致性。)
+ ConcurrentHashMap用于HashMap


只能保证一个操作是原子的,比如先检查key在不在(get),不在再添加(put)两个操作无法保证原子性,应该使用computeIfAbsent()
+ ConcurrentSkipListMap
+ ConcurrentSkipListSet
+ ...

举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = new CopyOnWriteArrayList<>();

适合读多写少的场景。看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public boolean add(E e) {
   final ReentrantLock lock = this.lock;
   lock.lock();
   try {
       Object[] elements = getArray();
       int len = elements.length;
       Object[] newElements = Arrays.copyOf(elements, len + 1);
       newElements[len] = e;
       setArray(newElements);
       return true;
  } finally {
       lock.unlock();
  }
}

在写入的时候加锁,并复制一份,将新加入的写进新数组,最终把新数组写回原资源,从而保证数据的原子性。这个过程中只是给增加方法加锁,不影响读的操作。

结语

多线程中同步方法大大影响了工作效率,所以ConcurrentHashMap通过volatile结合自旋锁的方式,广受大家喜爱,同样也是面试官常问的题目之一,值得大家好好去读一下源码。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

内存分页工具类

发表于 2021-08-06

场景

自定义的内存分页工具类,遇到一些麻烦的查询功能时,数据库自带的排序方法可能无法满足,此时,可以先查询并过滤所有主数据后,进行其他必要的数据处理,然后再使用内存分页返回,满足工作所需。

工具代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
java复制代码import cn.hutool.core.util.ObjectUtil;

import java.util.ArrayList;
import java.util.List;

/**
* 内存分页工具类
* @author laozhou
* 2021年4月19日
*/
public class PageUtil
{
private PageUtil()
{
}

/** 默认-页码,从1开始 */
public static final int PAGE_OFFSET = 1;

/** 默认-页条数 */
public static final int PAGE_SIZE = 20;

/**
* 重新得到处理后的页码
* @param pageOffset 页码,从1开始
* @return
* 2021年4月19日
*/
private static Integer regainPageOffset(Integer pageOffset)
{
pageOffset = null == pageOffset || pageOffset <= 0 ? PAGE_OFFSET : pageOffset;
return pageOffset;
}

/**
* 重新得到处理后的页条数
* @param pageSize 页条数,大于0的整数
* @return
* 2021年4月19日
*/
private static Integer regainPageSize(Integer pageSize)
{
pageSize = null == pageSize || pageSize <= 0 ? PAGE_SIZE : pageSize;
return pageSize;
}

/**
* 分页方法,返回所有分页的数据集合
* @param totalList 待分页数据集合,不可为null
* @param pageSize 页条数 [小于等于0时则置为默认值: {@link com.bwss.common.utils.PageUtil#PAGE_SIZE}]
* @return
* 2021年4月19日
*/
public static <T> List<List<T>> backAllPageList(List<T> totalList, Integer pageSize)
{
pageSize = PageUtil.regainPageSize(pageSize);
int totalCount = totalList.size();
// 计算页码数
int totalPage = totalCount / pageSize + (totalCount % pageSize == 0 ? 0 : 1);
// 分页处理
List<List<T>> res = new ArrayList<>(totalCount);
for (int pageNo = 1; pageNo <= totalPage; pageNo++)
{
List<T> subList = PageUtil.pageList(totalList, pageNo, pageSize);
if (ObjectUtil.isEmpty(subList))
{
continue;
}
res.add(subList);
}

return res;
}

/**
* 分页方法,返回指定页的数据集合
* @param totalList 待分页数据集合,不可为null
* @param pageOffset 页码,从1开始 [小于等于0时则置为默认值: {@link com.bwss.common.utils.PageUtil#PAGE_OFFSET}]
* @param pageSize 页条数 [小于等于0时则置为默认值: {@link com.bwss.common.utils.PageUtil#PAGE_SIZE}]
* @return
* 2021年4月19日
*/
public static <T> List<T> pageList(List<T> totalList, Integer pageOffset, Integer pageSize)
{
pageOffset = PageUtil.regainPageOffset(pageOffset);
pageSize = PageUtil.regainPageSize(pageSize);

int startIndex = (pageOffset - 1) * pageSize;
int totalCount = totalList.size();
if (startIndex > totalCount)
{
return new ArrayList<>();
}
int endIndex = startIndex + pageSize;
if (endIndex > totalCount)
{
endIndex = totalCount;
}
List<T> page = totalList.subList(startIndex, endIndex);

return page;
}

/**
* 使用内存分页,返回固定分页结果对象[此处为booway框架分页o]
* @param totalList 待分页数据集合 [为 空 || null 则返回空数据分页结果对象]
* @param pageOffset 页码,从1开始 [小于等于0时则置为默认值: {@link com.bwss.common.utils.PageUtil#PAGE_OFFSET}]
* @param pageSize 页条数 [小于等于0时则置为默认值: {@link com.bwss.common.utils.PageUtil#PAGE_SIZE}]
* @param queryModel 查询对象模型,可为null
* @return
* 2021年4月19日
*/
/*public static <T> ResponseModel<T> backRespModelOfPage(List<T> totalList, Integer pageOffset, Integer pageSize,
T queryModel)
{
ResponseModel<T> res = null;
com.bw.framework.pagination.PageUtil pu = null;
List<T> page = null;
int totalCount = 0;

if (ObjectUtil.isEmpty(totalList))
{
pu = new com.bw.framework.pagination.PageUtil(pageOffset, pageSize, totalCount);
page = new ArrayList<>();
res = new ResponseModel<T>(pu, page, queryModel);

return res;
}

pageOffset = PageUtil.regainPageOffset(pageOffset);
pageSize = PageUtil.regainPageSize(pageSize);

totalCount = totalList.size();
pu = new com.bw.framework.pagination.PageUtil(pageOffset, pageSize, totalCount);
page = PageUtil.pageList(totalList, pageOffset, pageSize);
res = new ResponseModel<T>(pu, page, queryModel);

return res;
}*/

}

优缺

  • 内存分页相比普通分页,更消耗内存和时间,非必要不建议使用
  • 优点是能满足一些复杂的业务需求

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot整合Redis实现发布/订阅模式 附带R

发表于 2021-08-06

上一篇博客写了👉Docker搭建Redis Cluster 集群环境

我自己是认为对于每个知识点,光看了不操作是没有用的(遗忘太快…),多少得在手上用上几回才可以,才能对它加深印象。

昨天搭建了Redis Cluster 集群环境,今天就来拿它玩一玩Redis 发布/订阅模式吧

很喜欢一句话:”八小时内谋生活,八小时外谋发展“。

共勉.😁

地点:😂不知道
作者:L
@[TOC](SpringBoot 整合Redis集群配置 实现发布/订阅模式)

一、前言

其实光从代码层面上讲,其实没有什么变化,主要是变化是关于Redis的配置需要更改为集群配置而已,之前接触过redis的话,那么就只需要看一下redis集群配置文件即可了。

对redis实现发布/订阅感兴趣的话,那就可以接着看下去了哈。

发布/订阅模式 :所谓发布/订阅模式,其实就是和你关注微信公众号一样的意思。

举个例子:你订阅了两个微信公众号(一个叫青年湖南,一个叫央视新闻),假如我也订阅了青年湖南,某一天央视发布了一条新新闻,你能收到,我没有关注,则我不能收到。但是某周看青年大学习发布王冰冰叫你去学习时,你我都订阅了,就都可以收到。

二、前期准备

两份配置文件都有。

单机也是可以的,想一起搭集群玩的可以👉Docker搭建Redis Cluster 集群环境。

2.1、项目结构:

在这里插入图片描述

2.2、依赖的jar包

我这里是因为是习惯创建maven项目,然后将SpringBoot的版本抽出来,方便控制版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>

2.3 、yml配置文件

单机配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yml复制代码spring:
redis:
database: 0
port: 6379
host: localhost
password:
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 1024
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 10000
# 连接池中的最大空闲连接
max-idle: 200
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 10000

redis集群配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
yml复制代码server:
port: 8089
spring:
application:
name: springboot-redis
redis:
password: 1234
cluster:
nodes:
- IP地址:6379
- IP地址:6380
- IP地址:6381
- IP地址:6382
- IP地址:6383
- IP地址:6384
max-redirects: 3 # 获取失败 最大重定向次数
lettuce:
pool:
max-active: 1000 #连接池最大连接数(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接

#===========jedis配置方式=============================================
# jedis:
# pool:
# max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
# max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
# max-idle: 10 # 连接池中的最大空闲连接
# min-idle: 5 # 连接池中的最小空闲连接
#

三、编码

3.1、config层

redis配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
java复制代码import com.crush.ps.subscribe.AConsumerRedisListener;
import com.crush.ps.subscribe.BConsumerRedisListener;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis 配置类
* 1. 设置RedisTemplate序列化/返序列化
* 2. 监听消息
* @author cuberxp
* @since 1.0.0
* Create time 2020/1/23 0:06
*/
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {

@Autowired
AConsumerRedisListener aConsumerRedisListener;

@Autowired
BConsumerRedisListener bConsumerRedisListener;

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
//将消息侦听器添加到(可能正在运行的)容器中。 如果容器正在运行,则侦听器会尽快开始接收(匹配)消息。
// a 订阅了 topica、topicb 两个 频道
container.addMessageListener(aConsumerRedisListener, new PatternTopic("topica"));
container.addMessageListener(aConsumerRedisListener, new PatternTopic("topicb"));

// b 只订阅了 topicb 频道
container.addMessageListener(bConsumerRedisListener, new PatternTopic("topicb"));
return container;
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//设置value hashValue值的序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(
Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(om);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
//key hasKey的序列化
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

}

3.2、订阅者

我在这边写了两个订阅者,方便演示例子罢了。

A消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
* @author crush
* MessageListener : Redis中发布的消息的侦听器。
*/
@Component
public class AConsumerRedisListener implements MessageListener {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/**
* @param message 传递过来的信息数据
* @param pattern 频道
*/
@Override
public void onMessage(Message message, byte[] pattern) {
doBusiness(message);
}

/**
* 打印 message body 内容
*
* deserialize 从给定的二进制数据反序列化一个对象。
* @param message
*/
public void doBusiness(Message message) {
Object value = redisTemplate.getValueSerializer().deserialize(message.getBody());
System.out.println("A==>consumer message: " + value.toString());
}

}

B消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码package com.crush.ps.subscribe;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
* @author crush
*/
@Component
public class BConsumerRedisListener implements MessageListener {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/**
* @param message 传递过来的信息数据
* @param pattern 频道
*/
@Override
public void onMessage(Message message, byte[] pattern) {
doBusiness(message);
}

/**
* 打印 message body 内容
*
* deserialize 从给定的二进制数据反序列化一个对象。
* @param message
*/
public void doBusiness(Message message) {
Object value = redisTemplate.getValueSerializer().deserialize(message.getBody());
System.out.println("B==>consumer message: " + value.toString());
}
}

3.3、AnnouncementMessage实体类

就是自己写的传递消息的实体类,(AnnouncementMessage 意思就是拿来模拟发布公布的实体类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

/**
* @author crush
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AnnouncementMessage implements Serializable {

private static final long serialVersionUID = 8632296967087444509L;

/**
* 公告信息id
*/
private String id;

/**
* 公告内容
*/
private String content;
}

四、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@SpringBootTest
public class SubscribeTest {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Test
public void testSubscribe() {
String achannel = "topica";
String bchannel = "topicb";

redisTemplate.convertAndSend(achannel, "hello world");

redisTemplate.convertAndSend(bchannel, new AnnouncementMessage("1", "模拟发通告"));
}
}

结果:

1
2
3
4
5
bash复制代码输出: 
A==>consumer message: hello world
A==>consumer message: AnnouncementMessage(id=1, content=模拟发通告)

B==>consumer message: AnnouncementMessage(id=1, content=模拟发通告)

因为A 消费者订阅两个频道,而B 消费者只订阅了一个频道,所以A 会多一条。

注 :测试时需要把主启动类也给启动起来,方便查看输出。(主启动自己写就好了,没有什么其他的注解,普普通通的)

五、自言自语

不知道大家学习是什么样的,博主自己的感觉就是学了的东西,要通过自己去梳理一遍,或者说是去实践一遍,我觉得这样子,无论是对于理解还是记忆,都会更加深刻。

如若有不足之处,请不啬赐教!!😁

有疑惑之处,也可以留言或私信,定会第一时间回复。👩‍💻

这篇文章就到这里啦,下篇文章再见。👉一篇文章用Redis 实现消息队列(还在写)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微双系统—Windows10上安装Linux的Ubuntu1

发表于 2021-08-06

这是一期Windows安装Linux子系统的教程,在为了练习Linux而安装它的虚拟机或实体机感到烦恼吗?在为Windows和Linux的交换代码文件感到麻烦吗?这期教程可以帮到你!它可以在Windows上运行绝大部分的Linux命令!可以尽情的在Windows上编写程序!

==安装后的美化和优化:==Windows10安装Terminal终端美化优化Linux的Ubuntu18.04子系统换字体背景换阿里云国内源

在这里插入图片描述

在 ==此电脑== 上面 右击 ==属性==
在这里插入图片描述

打开属性后 点击==控制面板主页==
在这里插入图片描述

打开控制面板主页后 在 ==查看方式== 选择 ==类别== 最后点击==程序==
在这里插入图片描述

打开程序后 点击 ==启用或关闭Windows功能==
在这里插入图片描述
打开 启用或关闭Windows功能后 往下拉 找到==Linux的Windows子系统== 选择它 确认后 等它加载完 再==重启电脑==
在这里插入图片描述
重启完后 点击==开始(windows建)== 在菜单里面下拉找到==微软商店== 打开它
在这里插入图片描述
在搜索里面 搜==wsl==它就会显示子系统出来 我这选 Ubuntu18.04子系统 选其它系统也可以看自己

在这里插入图片描述

那个版本都可以,这里推荐选择 /Ubuntu18.04/的子系统后等待下载完成
在这里插入图片描述
下载完后 点击启动 到黑色界面等待加载完成

在这里插入图片描述

加载完后,会提示==创建用户名==!然后==再创建密码==,创建密码的时候==按的密码是不会显示出来的==

在这里插入图片描述
命令可以执行了,到这子系统已经是完成了

在这里插入图片描述

图标默认是不会显示到桌面的,可以在==开始(Windows建==里面来启动,或者拉一个快捷方式出来

在这里插入图片描述

好了,这期教程就到这了,现在的==字体颜色和背景==都很不好看所以下期出个==子系统美化和优化==

!
!
!

==后续来了:==Windows10安装Terminal终端美化优化Linux的Ubuntu20.04子系统换字体背景换阿里云国内源

!
!
!
!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Maven 多仓库和镜像配置

发表于 2021-08-06

因为之前maven配置的一直都是公司的私服仓库,今天 拉 JMH包发现拉不到,于是考虑配置多个仓库,可以满足工作以及日常开发需求,顺便梳理 mirrors 和 repository 的区别

maven 设置多个仓库

有两种不同的方式可以指定多个存储库的使用。第一种方法是在 POM 中指定要使用的存储库。这在构建概要文件内部和外部都支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<project>
...
<repositories>
<repository>
<id>my-repo1</id>
<name>your custom repo</name>
<url>http://jarsm2.dyndns.dk</url>
</repository>
<repository>
<id>my-repo2</id>
<name>your custom repo</name>
<url>http://jarsm2.dyndns.dk</url>
</repository>
</repositories>
...
</project>

另一种指定多个存储库的方法是在${user.home}/.m2/settings.xml或者 ${maven.home}/conf/settings.xml文件中 新建 profile信息 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
xml复制代码<settings>
...
<profiles>
// 第一个仓库地址
<profile>
<id>nexus</id>
<repositories>
<repository>
<id>my-repo2</id>
<name>your custom repo</name>
<url>http://jarsm2.dyndns.dk</url>
</repository>
</repositories>
</profile>
// 第二个仓库地址
<profile>
<id>aliyun</id>
<repositories>
<repository>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>

<activeProfiles>
<activeProfile>nexus</activeProfile>
<activeProfile>aliyun</activeProfile>
</activeProfiles>
...
</settings>

如果您在profiles 中指定 repository 存储库,需要激活该特定profiles,我们通过在 activeProfiles 中进行配置

你也可以通过执行以下命令来激活这个配置文件:

1
erlang复制代码mvn -Pnexus ...

正常maven 的settings.xml配置完成profiles之后,可以在idea中进行切换

设置镜像

镜像 相当于拦截机。它拦截 maven 对远程存储库的请求,将请求中的远程存储库地址重定向到镜像中配置的地址。它主要提供了一个方便的方式来切换远程仓库地址。例如,在公司工作时,使用电信网络,连接到电信仓库。当我回家的时候,是联通的网络。我想连接联通的仓库。我可以通过镜像配置将我的项目的仓库地址变成联通,而不是在特定的项目配置文件中逐个地改变地址

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<settings>
...
<mirrors>
<mirror>
<id>aliyun</id>
<name>Maven Repository Manager running on repo.mycompany.com</name>
<url>http://repo.mycompany.com/proxy</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
...
</settings>

配置说明:

•id: 镜像的唯一标识•mirrorOf: 指定镜像规则,什么情况下从镜像仓库拉取,•*: 匹配所有,所有内容都从镜像拉取•external:*: 除了本地缓存的所有从镜像仓库拉取idea•repo,repo1: repo 或者 repo1 ,这里的 repo 指的仓库 ID•*,!repo1: 除了 repo1 的所有仓库•name: 名称描述•url: 地址

示列: 针对aliyun 仓库进行设置镜像重定向到镜像中配置的地址

1
2
3
4
5
6
7
8
xml复制代码  <mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>aliyun</mirrorOf>
<name>ppd mirror</name>
<url>http://repo.mycompany.com/proxy</url>
</mirror>
</mirrors>

image-20210805154919762

这个时候会发现 虽然 repository 配置的是正确aliyun 地址,但是由于mirror镜像拦截的原因重定向新的url.

image-20210805155037900

mirrors 与profiles 设置repository的区别

mirror 与 repository 不同的是,假如配置同一个 repository 多个 mirror 时,相互之间是备份关系,只有当仓库连不上时才会切换到另

一个,而如果能连上但是找不到依赖时是不会尝试下一个 mirror 地址的

reference

•maven.apache.org/guides/mini…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java使用Aspose实现office文件转PDF(20

发表于 2021-08-06

最近一直在弄在线教学平台,然后就需要实现在线预览文件功能.之前资源库一直用的libreoffice实现Office转PDF.但是libreoffice是单线程的,作为一个在线教学平台显然是不靠谱的.所以今天分享另一个纯Java实现Office转PDF工具Aspose

  • 首先我们先引入响应的pom配置(由于默认maven仓库下不下来.文末提供下载地址)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>20.7</version>
<classifier>jdk17</classifier>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-slides</artifactId>
<version>20.7</version>
<classifier>jdk16</classifier>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-cells</artifactId>
<version>20.7</version>
</dependency>

采用工厂模式进行office系列转换

  • 新建接口而IFileConvert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface IFileConvert {

/**
* pdf文件后缀
*/
String FILE_PDF_SUFFIX = ".pdf";

default String getResultPath(String filePath, String fileSuffix){
// 什么将要返回的pdf路径
int i = filePath.lastIndexOf(".");
String path = filePath.substring(0, i);
return path + fileSuffix;
}

String fileToPdf(String filePath);
}
  • 实现WORD文件转换,新建WordsFileConvert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码public class WordsFileConvert implements IFileConvert {

private boolean getLicense() {
boolean result = false;
InputStream is = null;
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
is = loader.getResourceAsStream("license.xml");
License license = new License();
license.setLicense(is);
result = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

@Override
public String fileToPdf(String filePath) {
//去除水印
if (!getLicense()) {
return null;
}
String pdfPath = null;
FileOutputStream os = null;
try {
pdfPath = getResultPath(filePath, FILE_PDF_SUFFIX);
os = new FileOutputStream(pdfPath);
Document doc = new Document(filePath);
doc.save(os, SaveFormat.PDF);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return pdfPath;
}
}
  • 实现PPT文件转换,新建SlidesFileConvert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class SlidesFileConvert implements IFileConvert {

private boolean getLicense() {
boolean result = false;
InputStream is = null;
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
is = loader.getResourceAsStream("license.xml");
License license = new License();
license.setLicense(is);
result = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

@Override
public String fileToPdf(String filePath) {
if (!getLicense()) {
return null;
}
String pdfPath = null;
FileOutputStream os = null;
try {
pdfPath = getResultPath(filePath, FILE_PDF_SUFFIX);
os = new FileOutputStream(pdfPath);
Presentation pres = new Presentation(filePath);
pres.save(os, SaveFormat.Pdf);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return pdfPath;
}
}
  • 实现EXCEL文件转换,新建CellsFileConvert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class CellsFileConvert implements IFileConvert {

private boolean getLicense() {
boolean result = false;
InputStream is = null;
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
is = loader.getResourceAsStream("license.xml");
License license = new License();
license.setLicense(is);
result = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

@Override
public String fileToPdf(String filePath) {
if (!getLicense()) {
return null;
}
String pdfPath = null;
FileOutputStream os = null;
try {
pdfPath = getResultPath(filePath, FILE_PDF_SUFFIX);
os = new FileOutputStream(pdfPath);
Workbook workbook = new Workbook(filePath);
workbook.save(os, SaveFormat.PDF);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return pdfPath;
}
}
  • 在resources目录下新建license.xml文件
1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<License>
<Data>
<Products>
<Product>Aspose.Total for Java</Product>
</Products>
<EditionType>Enterprise</EditionType>
<SubscriptionExpiry>20991231</SubscriptionExpiry>
<LicenseExpiry>20991231</LicenseExpiry>
<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
</Data>
<Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>
</License>
  • 新建工厂枚举FileConvertEnum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码public enum FileConvertEnum {

PPT("PPT", new SlidesFileConvert()),
WORD("WORD", new WordsFileConvert()),
EXCEL("EXCEL", new CellsFileConvert()),
;

private String fileFormat;
private IFileConvert fileConvert;

FileConvertEnum(String fileFormat, IFileConvert fileConvert) {
this.fileFormat = fileFormat;
this.fileConvert = fileConvert;
}

public String getFileFormat() {
return fileFormat;
}

public IFileConvert getFileConvert() {
return fileConvert;
}

public static IFileConvert getFileConvert(String fileFormat) {
for (FileConvertEnum fileConvertEnum : values()) {
if (fileConvertEnum.fileFormat.equals(fileFormat)){
return fileConvertEnum.fileConvert;
}
}
return null;
}
}
  • 使用方式
1
2
3
4
java复制代码public static void main(String[] args) {
IFileConvert fileConvert = FileConvertEnum.getFileConvert("WORD");
String pdfUri = fileConvert.fileToPdf("/home/abelethan/Desktop/test.doc");
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot+SpringSecurity+Jwt权

发表于 2021-08-06
  1. 整体逻辑

  1. SpringSecurity认证的逻辑规则

启动项目时,SpringBoot自动检索所有带@Configuration的注解,所以就将我们的WebSecurityConfig给加载了,这个config中,我们需要在configure(AuthenticationManagerBuilder auth)方法中注册一个继承自UserDetailsService的接口,这个接口中只有一个方法,那就是使用username获取到数据库中用户信息并返回成UserDetail实体。这个方法需要我们按照我们的不同业务场景重写

WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
java复制代码
/**
* @description:
* @author: coderymy
* @create: 2020-10-01 13:54
* <p>
* 1\. 创建WebSecurityConfig 类继承WebSecurityConfigurerAdapter
* 2\. 类上加上@EnableWebSecurity,注解中包括@Configuration注解
* <p>
* WebSecurityConfigurerAdapter声明了一些默认的安全特性
* (1)验证所有的请求
* (2)可以使用springSecurity默认的表单页面进行验证登录
* (3)允许用户使用http请求进行验证
*/

/**
* 如何自定义认证
* 1\. 实现并重写configure(HttpSecurity http)方法,鉴权,也就是判断该用户是否有访问该api的权限
* <p>
* <p>
* 页面显示403错误,表示该用户授权失败(401代表该用户认证失败)前端可以使用返回的状态码来标识如何给用户展示
* 用2XX表示本次操作成功,用4XX表示是客户端导致的失败,用5XX表示是服务器引起的错误
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123");
System.out.println(encode);
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}

/**
* SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错
* 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功
*
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

//鉴权
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 1\. HttpSecurity被声明为链式调用
* 其中配置方法包括
* 1\. authorizeRequests()url拦截配置
* 2\. formLogin()表单验证
* 3\. httpBasic()表单验证
* 4\. csrf()提供的跨站请求伪造防护功能
*/
/**
* 2\. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的
* anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略
*/
http.authorizeRequests()
//这里指定什么样的接口地址的请求,需要什么样的权限 ANT模式的URL匹配器
.antMatchers("/select/**").hasRole("USER")//用户可以有查询权限
.antMatchers("/insert/**").hasRole("ADMIN")//管理员可以有插入权限权限
.antMatchers("/empower/**").hasRole("SUPERADMIN")//超级管理员才有赋权的权限
.antMatchers("/login/**").permitAll()//标识list所有权限都可以直接访问,即使不登录也可以访问。一般将login页面放给这个权限
.and()
.formLogin()
// .loginProcessingUrl("/login/user")//用来定义什么样的API请求时login请求
// .permitAll()//login请求需要是所有权限都可以的
.and().csrf().disable();

/**
* 将自定义的JWT过滤器加入configure中
*/
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
}

@Autowired
private MyUserDetailsService myUserDetailsService;

//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}

}

MyUserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码
@Service
public class MyUserDetailsService implements UserDetailsService {

@Resource
private UsersRepository usersRepository;

/**
* 其实这样就完成了认证的过程,能获取到数据库中配置的用户信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//获取该用户的信息
Users user = usersRepository.findByUsername(username);

if (user == null) {//用户不存在报错
throw new UsernameNotFoundException("用户不存在");
}

/**
* 将roles信息转换成SpringSecurity内部的形式,即Authorities
* commaSeparatedStringToAuthorityList可以将使用,隔开的角色列表切割出来并赋值List
* 如果不行的话,也可以自己实现这个方法,只要拆分出来就可以了
*/
//注意,这里放入Authorities中的信息,都需要是以Role_开头的,所以我们在数据库中配置的都是这种格式的。当我们使用hasRole做比对的时候,必须要是带Role_开头的。否则可以使用hasAuthority方法做比对
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));

return user;
}
}

其实如果去掉上面的将自定义的JWT过滤器加入到过滤链中的话,这个认证过程已经完成了。使用下面的代码就可以调用起整个认证程序。

核心代码

1
ini复制代码authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));

这一行就会将username和password放到认证程序中进行认证。

也就是需要我们自己也逻辑让他去触发这个代码的实现。就可以自动完成认证程序了。就会触发使用username获取到数据库用户信息,然后经过密码加密比对之后会将认证结果返回。

我们整合JWT其实也很简单,其实就是将JWT的登录部分的操作,使用过滤器封装,将该过滤器放到整个认证的过滤链中

  1. 自定义JWT过滤器的配置

SpringSecurity过滤器的配置无非以下几个条件

  1. 该过滤器过滤什么样的API请求(也就是说什么样的API请求会触发该过滤器执行)。配置被过滤的请求API
  2. 该过滤器做什么事
  3. 该过滤器执行成功以及执行失败的各种情况该怎么做
  4. 该过滤器执行的时机是什么样的,也就是在过滤链之前还是之后执行

先解决逻辑上以上三个问题的答案

  1. 我们需要拦截认证请求,肯定是形如xxx/login/xxx这种API接口的请求啦
  2. 这个过滤器会做什么事呢?
    1. 首先,我们需要进行用户名密码的基础验证,也就是合不合法
    2. 我们需要调用起SpringSecurity的默认认证程序
    3. 认证程序执行成功之后,我们需要按照用户的信息以JWT的规则生成一个JWTToken并将其放入response中返回回去
    4. 认证程序执行失败,也就是用户登录失败,我们也需要将返回信息封装起来返回给用户
  3. 执行成功,我们需要返回给用户JWTToken信息。执行失败,我们也要友好提示用户
  4. 如果排除其他的业务场景干扰,目前过滤链只有进行鉴权时候才使用。所以针对不同的业务场景,这个过滤器放的地方其实是不一样的。(之后我们的另一个JWTToken校验的过滤器应该需要在这个认证的过滤器之后(两个其实并不捕捉同样的APi所以不会依次执行。也不用太考虑这个问题))(我记得SpringCloud中的zuul网关的过滤器是可以自定义级别的。但是目前在SpringSecurity中尚未发现这种功能)

针对以上解答,下面用代码来做展示(ps:序号依次对应上面)

  1. 配置过滤器过滤地址
1
2
3
4
5
6
7
8
9
10
11
java复制代码    /**
* 下面是为了配置这个Manager
* 配置其拦截的API请求地址
*
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login/user");//这里指定什么样的API请求会被这个过滤器拦截

}
  1. 配置过滤器职能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
java复制代码    @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//这里是定义一个拦截器,在认证方面的拦截器,当请求的时候回拦截到这里面然后进行身份认证
//验证用户名密码是否正确之后
Authentication authenticate = null;
try {
System.out.println("InputStream:" + request.getInputStream());
UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//这个地方相当于封装一下请求,因为前台请求的是user.username="xxx"这种对象的形式。
//对于这个过滤器拦截的接口,去调用SpringSecurity默认的认证程序,也就是去进行SpringSecurity的认证
authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
return authenticate;
//如果返回成功,就进入successfulAuthentication。返回失败就进入unsuccessfulAuthentication
//可以通过下面的定义来让前端得到不同的返回从而向用户展示不同的效果
//TODO 这个地方还有点问题,如果密码错误了,就会报BadCredentialsException错误。需要看一下如果不让这么报错并让他进入到unsuccessfulAuthentication方法中
} catch (BadCredentialsException e) {//捕捉密码验证错误异常
log.info("密码错误");
try {
this.unsuccessfulAuthentication(request, response, e);
} catch (IOException ex) {
ex.printStackTrace();
} catch (ServletException ex) {
ex.printStackTrace();
}

} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//当身份验证通过之后,会进入这里,这里可以定义成功的返回
Users user = (Users) authResult.getPrincipal();//将principal中的信息转换成User对象

JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);

Map<String, Object> map = new HashMap<>();
map.put("username", user.getUsername());
map.put("password", user.getPassword());

String jwtToken = util.encode("tom", 30000, map);

response.addHeader("Authorizations", jwtToken);
user.setJwtToken(jwtToken);
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));
System.out.println(response.getHeaderNames());
// super.successfulAuthentication(request, response, chain, authResult);
//注意,不要使用默认的super来定义,否则上述会失效的
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//TODO 得看一下为啥会报这个错误
System.out.println("认证失败");

ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登录失败,账号密码错误")));
}
  1. 执行失败与成功,分别是2中的unsuccessfulAuthentication和successfulAuthentication方法
  2. 配置过滤链执行的位置
1
2
3
4
scss复制代码在WebSecurityConfig中的configure(HttpSecurity http)方法中

JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);

完成了以上的配置,前台就可以使用/login/user来进行登录操作了。登录成功会返回一个JSON对象来供前端判断成功与否

  1. 代码结果

全部代码奉上,随意写的注释有点多,不看的可以给删掉

  1. WebSecurityConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
java复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @description:
* @author: coderymy
* @create: 2020-10-01 13:54
* <p>
* 1\. 创建WebSecurityConfig 类继承WebSecurityConfigurerAdapter
* 2\. 类上加上@EnableWebSecurity,注解中包括@Configuration注解
* <p>
* WebSecurityConfigurerAdapter声明了一些默认的安全特性
* (1)验证所有的请求
* (2)可以使用springSecurity默认的表单页面进行验证登录
* (3)允许用户使用http请求进行验证
*/

/**
* 如何自定义认证
* 1\. 实现并重写configure(HttpSecurity http)方法,鉴权,也就是判断该用户是否有访问该api的权限
* <p>
* <p>
* 页面显示403错误,表示该用户授权失败(401代表该用户认证失败)前端可以使用返回的状态码来标识如何给用户展示
* 用2XX表示本次操作成功,用4XX表示是客户端导致的失败,用5XX表示是服务器引起的错误
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123");
System.out.println(encode);
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}

/**
* SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错
* 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功
*
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

//鉴权
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 1\. HttpSecurity被声明为链式调用
* 其中配置方法包括
* 1\. authorizeRequests()url拦截配置
* 2\. formLogin()表单验证
* 3\. httpBasic()表单验证
* 4\. csrf()提供的跨站请求伪造防护功能
*/
/**
* 2\. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的
* anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略
*/
http.authorizeRequests()
//这里指定什么样的接口地址的请求,需要什么样的权限 ANT模式的URL匹配器
.antMatchers("/select/**").hasRole("USER")//用户可以有查询权限
.antMatchers("/insert/**").hasRole("ADMIN")//管理员可以有插入权限权限
.antMatchers("/empower/**").hasRole("SUPERADMIN")//超级管理员才有赋权的权限
.antMatchers("/login/**").permitAll()//标识list所有权限都可以直接访问,即使不登录也可以访问。一般将login页面放给这个权限
.and()
.formLogin()
// .loginProcessingUrl("/login/user")//用来定义什么样的API请求时login请求
// .permitAll()//login请求需要是所有权限都可以的
.and().csrf().disable();

/**
* 将自定义过滤器加入configure中
*/
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());
http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);
}

@Autowired
private MyUserDetailsService myUserDetailsService;

//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}

}
  1. JWTAuthenticationFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
java复制代码import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huanong.avatarma.basic.entity.Users;
import com.huanong.avatarma.basic.model.vo.UserDto;
import com.huanong.avatarma.common.util.JwtUtil;
import com.huanong.avatarma.common.util.ResponseUtil;
import com.huanong.avatarma.common.util.ResultUtil;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* @description: jwt拦截器
* 这个定义完成之后,想要生效还需要将其加入到过滤链中
* @author: coderymy
* @create: 2020-10-02 09:36
**/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

/**
* 验证用户名密码是否正确之后,生成一个token,并将token返回客户端
*
* @param request
* @param response
* @return
* @throws AuthenticationException
*/

private String secretKey = "ILoveDanChaoFan";

private AuthenticationManager authenticationManager;

/**
* 下面是为了配置这个Manager
* 配置其拦截的API请求地址
*
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login/user");//这里指定什么样的API请求会被这个过滤器拦截

}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//这里是定义一个拦截器,在认证方面的拦截器,当请求的时候回拦截到这里面然后进行身份认证
//验证用户名密码是否正确之后
Authentication authenticate = null;
try {
System.out.println("InputStream:" + request.getInputStream());
UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//这个地方相当于封装一下请求,因为前台请求的是user.username="xxx"这种对象的形式。
//对于这个过滤器拦截的接口,去调用SpringSecurity默认的认证程序,也就是去进行SpringSecurity的认证
authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));
return authenticate;
//如果返回成功,就进入successfulAuthentication。返回失败就进入unsuccessfulAuthentication
//可以通过下面的定义来让前端得到不同的返回从而向用户展示不同的效果
//TODO 这个地方还有点问题,如果密码错误了,就会报BadCredentialsException错误。需要看一下如果不让这么报错并让他进入到unsuccessfulAuthentication方法中
} catch (BadCredentialsException e) {//捕捉密码验证错误异常
log.info("密码错误");
try {
this.unsuccessfulAuthentication(request, response, e);
} catch (IOException ex) {
ex.printStackTrace();
} catch (ServletException ex) {
ex.printStackTrace();
}

} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//当身份验证通过之后,会进入这里,这里可以定义成功的返回
Users user = (Users) authResult.getPrincipal();//将principal中的信息转换成User对象

JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);

Map<String, Object> map = new HashMap<>();
map.put("username", user.getUsername());
map.put("password", user.getPassword());

String jwtToken = util.encode("tom", 30000, map);

response.addHeader("Authorizations", jwtToken);
user.setJwtToken(jwtToken);
ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));
System.out.println(response.getHeaderNames());
// super.successfulAuthentication(request, response, chain, authResult);
//注意,不要使用默认的super来定义,否则上述会失效的
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//TODO 得看一下为啥会报这个错误
System.out.println("认证失败");

ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error("登录失败,账号密码错误")));
}

}
  1. MyUserDetailsService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码import com.huanong.avatarma.basic.dao.UsersRepository;
import com.huanong.avatarma.basic.entity.Users;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @description:
* @author: coderymy
* @create: 2020-10-01 15:55
**/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UsersRepository usersRepository;
/**
* 其实这样就完成了认证的过程,能获取到数据库中配置的用户信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获取该用户的信息
Users user = usersRepository.findByUsername(username);
if (user == null) {//用户不存在报错
throw new UsernameNotFoundException("用户不存在");
}
/**
* 将roles信息转换成SpringSecurity内部的形式,即Authorities
* commaSeparatedStringToAuthorityList可以将使用,隔开的角色列表切割出来并赋值List
* 如果不行的话,也可以自己实现这个方法,只要拆分出来就可以了
*/
//注意,这里放入Authorities中的信息,都需要是以Role_开头的,所以我们在数据库中配置的都是这种格式的。当我们使用hasRole做比对的时候,必须要是带Role_开头的。否则可以使用hasAuthority方法做比对
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
  1. Users
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
typescript复制代码@Data
@ToString
@Entity
@Table(name = "users")
public class Users implements UserDetails {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String password;

private boolean enable;

private String roles;

private Date createDate;

private Date modifyDate;

@Transient//这个注解可以帮助在entity中添加表中没有的字段
private List<GrantedAuthority> authorities;

@Transient
private String jwtToken;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//这个本身是对应roles字段的,但是因为结构不一致,所以重新创建一个,后续补充这部分
return this.authorities;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}

@Override
public boolean isEnabled() {
return this.enable;
}
}
  1. Result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码import lombok.Data;
import lombok.ToString;

/**
* @description:
* @author: coderymy
* @create: 2020-10-02 13:24
**/
@Data
@ToString
public class Result {

private Integer code;

private String msg;

private Object data;

}
  1. JwtUtil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
java复制代码/**
* @description:
* @author: coderymy
* @create: 2020-10-02 09:24
**/
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import java.util.*;

/*
* 总的来说,工具类中有三个方法
* 获取JwtToken,获取JwtToken中封装的信息,判断JwtToken是否存在
* 1\. encode(),参数是=签发人,存在时间,一些其他的信息=。返回值是JwtToken对应的字符串
* 2\. decode(),参数是=JwtToken=。返回值是荷载部分的键值对
* 3\. isVerify(),参数是=JwtToken=。返回值是这个JwtToken是否存在
* */
public class JwtUtil {
//创建默认的秘钥和算法,供无参的构造方法使用
private static final String defaultbase64EncodedSecretKey = "badbabe";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;

public JwtUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}

private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;

public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}

/*
*这里就是产生jwt字符串的地方
* jwt字符串包括三个部分
* 1\. header
* -当前字符串的类型,一般都是“JWT”
* -哪种算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,没有什么变化
* 2\. payload
* 一般有四个最常见的标准字段(下面有)
* iat:签发时间,也就是这个jwt什么时候生成的
* jti:JWT的唯一标识
* iss:签发人,一般都是username或者userId
* exp:过期时间
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();

JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2\. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
.setIssuedAt(new Date(nowMillis))//1\. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
.setSubject(iss)//3\. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4\. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
builder.setExpiration(exp);
}
return builder.compact();
}

//相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
//也就是拿到荷载部分所有的键值对
public Claims decode(String jwtToken) {

// 得到 DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(base64EncodedSecretKey)
// 设置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}

//判断jwtToken是否合法
public boolean isVerify(String jwtToken) {
//这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持该算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常
//判断合法的标准:1\. 头部和荷载部分没有篡改过。2\. 没有过期
return true;
}

public static void main(String[] args) {
JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
//以tom作为秘钥,以HS256加密
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);

String jwtToken = util.encode("tom", 30000, map);

System.out.println(jwtToken);
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}
  1. ResponseUtil、ResultUtil、repository等不做展示。需要的可以联系我,这个是基础性的东西
  2. 依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang 并发编程实战——协程、管道、select用法

发表于 2021-08-06

在阅读本文前,我希望你有一定的Go语言基础,以及一部分关于协程的使用经验。
本文旨在帮助你使用高级并发技巧,其主要包含了以下几个部分:goroutine的基本用法;使用chan来实现多个goroutine之间的通信;使用select关键字来处理超时等。


术语 解析
goroutine 指协程,比线程要更轻量级
chan/channel 指管道,多用于多个 goroutine 之间通信

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func boring(msg string) {
for i := 0; ; i++ {
fmt.Println(msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}

func main() {
go boring("boring!")

fmt.Println("I'm listening")
time.Sleep(2 * time.Second)
fmt.Println("You're boring. I'm leaving")
}
1
2
3
4
5
6
7
8
rust复制代码I'm listening
boring! 0
boring! 1
boring! 2
boring! 3
boring! 4
boring! 5
You're boring. I'm leaving

上述这段代码有两个部分,boring方法负责向控制台输出当前的循环次数,main方法的第一行为这个方法开启了一个协程,也就是说,main方法不会等待boring方法执行完毕。main方法在输出I'm listening后,进入为期2秒的睡眠,随后唤醒结束main函数。由于main函数结束会带来整个程序的结束,所以开启的boring协程也会结束。
不过,上述的例子只是一个简单的演示。实际上,协程之间、协程与主进程之间是需要通信的,这能够帮助我们完成更为复杂的应用。

Go 管道的用法

一个简单的使用方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码func boring(msg string, c chan string) {
for i := 0; ; i++ {
// 发送信息给管道 (hannel / chan)
// 同时,它也在等待管道的消费者消费完成
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}

func main() {
c := make(chan string) // 初始化一个管道
go boring("boring!", c)

for i := 0; i < 5; i++ {
// `<-c` 等待 `boring` 方法给它发送值,如果一直没有收到,那么会被阻塞在这一步
fmt.Printf("You say: %q\n", <-c)
}
fmt.Println("You're boring. I'm leaving")
}
1
2
3
4
5
6
python复制代码You say: "boring! 0"
You say: "boring! 1"
You say: "boring! 2"
You say: "boring! 3"
You say: "boring! 4"
You're boring. I'm leaving

上述方法简单说就是boring方法在给管道c发送数据,并且等待另一头,也就是main方法来消费。由于管道中只能够存在一个数据,所以main方法和boring方法在某些程度上是交替运行的。但实际上不完全是,以main方法来说,接受到管道的数据后可以直接进行下一步,而不需要继续等待。

【知识点】Chan的概念

在Go语言中,通道是goroutine与另一个goroutine通信的媒介,并且这种通信是无锁的。换句话说,通道是一种允许一个goroutine将数据发送到另一个goroutine的技术。默认情况下,通道是双向的,这意味着goroutine可以通过同一通道发送或接收数据,如下图所示:
go channel
在Go语言中,除了chan string这样的写法能够使用读写功能双向管道外,还可以创建出单向管道,如<-chan string只能从管道中读取数据,而chan<- string只能够向管道中写入数据。

【案例讲解】两个线程输出数据

通过两个管道实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
go复制代码// `boring` 是一个返回管道的方法,该管道用于和 `boring` 方法通信
// `<-chan string` 意味着只能够从管道里面接受 String 数据,而不能够向该管道发送数据
func boring(msg string) <-chan string {
c := make(chan string)
// 现在在 boring 方法中开启协程,并在这个协程中向管道发送数据
go func() {
for i := 0; i < 10; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
close(c) // 要记得关闭协程
}()
return c
}

func main() {
joe := boring("Joe")
ahn := boring("Ahn")

// 必须要按照顺序输出 joe 和 ahn
for i := 0; i < 10; i++ {
fmt.Println(<-joe)
fmt.Println(<-ahn)
}

fmt.Println("You're both boring. I'm leaving")
}

这段代码会让代码按照boring("Joe")、boring("Ahn")这样交替输出。虽然说是能够交替输出数据,但这个本质上不是通过线程之间的通信实现,为此下面会进行一点改造。

合并管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
go复制代码// `boring` 是一个返回管道的方法,该管道用于和 `boring` 方法通信
// `<-chan string` 意味着只能够从管道里面接受 String 数据,而不能够向该管道发送数据
func boring(msg string) <-chan string {
c := make(chan string)
go func() {
for i := 0; i < 10; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
close(c)
}()
return c
}

func fanIn(cs ...<-chan string) <-chan string {
c := make(chan string)
for _, ci := range cs { // spawn channel based on the number of input channel
go func(cv <-chan string) {
for {
c <- <-cv // 把 cs 里面的每个管道都接到主管道 c 上
}
}(ci)
}
return c
}

func main() {
c := fanIn(boring("Joe"), boring("Ahn"))

for i := 0; i < 20; i++ {
fmt.Println(<-c)
}
fmt.Println("You're both boring. I'm leaving")
}

现在我们可以从两个方法内的协程获取到数据,虽然不能够保证交替输出数据(在这里是随机的),下面,我们使用管道来让多个进程之间开始通信。

协程间通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
go复制代码type Message struct {
str string // 真正要传输的数据
wait chan bool //
}

func fanIn(inputs ...<-chan Message) <-chan Message {
c := make(chan Message)
for i := range inputs {
input := inputs[i]
go func() {
for {
c <- <-input
}
}()
}
return c
}

// `boring` 是一个返回管道的方法,该管道用于和 `boring` 方法通信
func boring(msg string) <-chan Message {
c := make(chan Message)
waitForIt := make(chan bool)
go func() {
for i := 0; ; i++ {
c <- Message{
str: fmt.Sprintf("%s %d", msg, i),
wait: waitForIt, // 将管道注入到返回值中,用于协程之间的通信
}
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)

// 协程需要在这里等到接收到信息才能够继续执行后面的逻辑
<-waitForIt
}

}()
return c
}

func main() {
// merge 2 channels into 1 channel
c := fanIn(boring("Joe"), boring("Ahn"))

for i := 0; i < 5; i++ {
msg1 := <-c // 等到从管道中接受数据
fmt.Println(msg1.str)
msg2 := <-c
fmt.Println(msg2.str)

// 由于 boring 协程中需要等待 wait 信号才能继续执行,所以这一步能够保证两个协程都能够输出一次数据
msg1.wait <- true // main 协程允许 boring 协程继续执行任务
msg2.wait <- true // main 协程允许 boring 协程继续执行任务
}
fmt.Println("You're both boring. I'm leaving")
}

【案例讲解】设定超时等待时间

一个简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
go复制代码// `boring` 是一个返回管道的方法,该管道用于和 `boring` 方法通信
func boring(msg string) <-chan string {
c := make(chan string)
go func() {
for i := 0; ; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
}

}()
return c
}

func main() {
c := boring("Joe")

// timeout 的类型为:`<-chan Time`
timeout := time.After(5 * time.Second) // 这个方法会在 5 秒钟向 timeout 管道写入数据
for {
select {
case s := <-c:
fmt.Println(s)
case <-timeout:
fmt.Println("You talk too much.")
return
}
}
}

通过select能够保证在时间到达之后,执行case 2来结束程序。如果刚好二者一起到达,那么会随机执行一个case,在这里case最多可能会执行一次,但不一定来得及输出结果。

【知识点】select 解析

select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
select 随机执行一个可运行的 case。如果没有 case 可运行,那么会执行 default 里的操作,如果没有 default,那么它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

【实战】模拟Google搜索服务

Web页面中,搜索是一个很常见的功能,多数情况下,我们会使用一个微服务来搭建一个搜索服务,如ElasticSearch就是一个单独的服务。在这里,我们不会真的模拟一个ES来处理,反之,我们用一个随机延时的函数来代替它。由于搜索的时间不能够保证,有时候会很快,但有时候也会慢,不管是因为搜索本身就需要时间还是由于IO的耗时。
在这个案例中,我们将会循序渐进来告诉你如何更好的利用goroutine和chan来处理这个问题。除此以外,这里还使用了函数式编程技巧,如果你对这个还不太熟悉,可以先了解一些相关的知识再来继续阅读。

Google搜索1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
go复制代码type Result string
type Search func(query string) Result

var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)

func fakeSearch(kind string) Search {
return func(query string) Result {
time.Sleep(100 * time.Millisecond)
return Result(fmt.Sprintf("%s result for %q\n", kind, query))
}
}

// 它会依次调用Web,Image和Video,并将它们附加到结果中返回
func Google(query string) (results []Result) {
results = append(results, Web(query))
results = append(results, Image(query))
results = append(results, Video(query))
}

func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
results := Google("golang")
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed)
}
1
2
3
4
5
6
sql复制代码[
web result for "golang"
image result for "golang"
video result for "golang"
]
331.1909ms

现在,我们可以从Google方法中获取到结果,但是这一步还远远不够,我们希望调用搜索服务有一个时间上线,如果超时,那么相关的结果就不要了,返回现在已经有的数据。但在此之前,我们还发现Google方法中,三个查询是顺序调用的,只有前者返回了结果,才能够执行后面的逻辑,这也是为什么返回的时间这么长的原因。基于此,我们首先要将搜索的结果更改为并发执行的。

并发搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
go复制代码type Result string
type Search func(query string) Result

var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)

func fakeSearch(kind string) Search {
return func(query string) Result {
time.Sleep(100 * time.Millisecond)
return Result(fmt.Sprintf("%s result for %q\n", kind, query))
}
}

func Google(query string) []Result {
c := make(chan Result)

// 搜索的结果都会返回到管道 c 中
go func() {
c <- Web(query)
}()
go func() {
c <- Image(query)
}()
go func() {
c <- Video(query)
}()

var results []Result
for i := 0; i < 3; i++ {
results = append(results, <-c)
}

return results
}

func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
results := Google("golang")
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed)
}
1
2
3
4
5
6
sql复制代码[
image result for "golang"
web result for "golang"
video result for "golang"
]
109.5769ms

可以看到,现在的搜索结果只需要100+ms的时间了,这说明三次搜索的确是并发执行的。

更进一步:加上超时时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
go复制代码type Result string
type Search func(query string) Result

var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)

func fakeSearch(kind string) Search {
return func(query string) Result {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) // 将搜索时间更改为随机值
return Result(fmt.Sprintf("%s result for %q\n", kind, query))
}
}

func Google(query string) []Result {
c := make(chan Result)

go func() {
c <- Web(query)
}()
go func() {
c <- Image(query)
}()
go func() {
c <- Video(query)
}()

var results []Result

// 在这里,timeout 会在 50 毫秒后接收到管道传来的信息
timeout := time.After(50 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case r := <-c:
results = append(results, r)
case <-timeout: // 在 timeout 接收到信息后,将会结束搜索,并将结果直接返回
fmt.Println("timeout")
return results
}
}

return results
}

func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
results := Google("golang")
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed)
}

结果一:部分逻辑超时

1
2
3
bash复制代码timeout
[video result for "golang"]
61.2052ms

结果二:所有逻辑都没有超时

1
2
3
4
5
6
sql复制代码[
web result for "golang"
image result for "golang"
video result for "golang"
]
28.1629ms

结语

至此,我已经告诉你一些高级并发编程技巧,但高阶技巧远不止这些,希望这些技巧能给你一些帮助,并带来一些思考。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

还觉得Shiro难(下)? 四、授权 五、Shiro密码加密

发表于 2021-08-06

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

四、授权

4.1、授权概述

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
系统中的授权功能就是为用户分配相关的权限,只有当用户拥有相应的权限后,才能访问对应的资源。
如果系统中无法管理用户的权限,那么将会出现客户信息泄露,数据被恶意篡改等问题,所以在绝大多数的应用中,我们都会有权限管理模块。一般基于角色的权限控制管理有以下三个子模块:
  1. 用户管理
  2. 角色管理
  3. 权限管理

授权流程

4.2、 关键对象

**授权可简单理解为who对what(which)进行How操作:**


`Who,即主体(Subject)`,主体需要访问系统中的资源。


`What,即资源(Resource)`,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括`资源类型`和`资源实例`,比如`商品信息为资源类型`,类型为t01的商品为`资源实例`,编号为001的商品信息也属于资源实例。


`How,权限/许可(Permission)`,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

4.3、授权流程

image-20200521230705964

4.4、授权方式

4.4.1、基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
1
2
3
java复制代码if(subject.hasRole("admin")){//主体具有admin角色
//操作什么资源
}

4.4.2、基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
1
2
3
4
5
6
java复制代码if(subject.isPermission("user:find:*")){ //对用户模块的所有用户有查询权限
//对01用户进行修改
}
if(subject.isPermission("user:*:01")){ //user模块下的所有用户有所有权限
//对01用户进行修改
}

4.5、权限字符串

​ 权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

例子:

  • 用户创建权限:user:create,或user:create:*
  • 用户修改实例001的权限:user:update:001
  • 用户实例001的所有权限:user:*:001

4.6、Shiro授权的实现方式

4.6.1、编程式

1
2
3
4
5
6
java复制代码Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}

4.6.2、注解式

1
2
3
4
java复制代码@RequiresRoles("admin")
public void hello() {
//有权限
}

4.6.3、标签式

1
2
3
4
5
jsp复制代码JSP标签:在JSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf/FreeMarker 中使用shiro需要额外集成!

4.7、基于ini的授权

4.7.1、编写ini文件

1
2
3
4
5
6
7
8
9
ini复制代码#用户的身份、凭据、角色
[users]
zhangsan=555,hr,seller
xiaolin=123,hr

#角色与权限信息
[roles]
hr=employee:list,employee:delete
seller=customer:list,customer:save

4.7.2、编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码@Test
public void testAuthor(){
//创建Shiro的安全管理器,是shiro的核心
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//加载shiro.ini配置,得到配置中的用户信息(账号+密码)
IniRealm iniRealm = new IniRealm("classpath:shiro-author.ini");
securityManager.setRealm(iniRealm);
//把安全管理器注入到当前的环境中
SecurityUtils.setSecurityManager(securityManager);
//无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
Subject subject = SecurityUtils.getSubject();
System.out.println("认证状态:"+subject.isAuthenticated());
//创建令牌(携带登录用户的账号和密码)
UsernamePasswordToken token = new UsernamePasswordToken("dafei","666");
//执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
//登出
//subject.logout();
//System.out.println("认证状态:"+subject.isAuthenticated());

//判断用户是否有某个角色
System.out.println("role1:"+subject.hasRole("role1"));
System.out.println("role2:"+subject.hasRole("role2"));

//是否同时拥有多个角色
System.out.println("是否同时拥有role1和role2:"+subject.hasAllRoles(Arrays.asList("role1", "role2")));

//check开头的是没有返回值的,当没有权限时就会抛出异常
subject.checkRole("hr");

//判断用户是否有某个权限
System.out.println("user:delete:"+subject.isPermitted("user:delete"));
subject.checkPermission("user:delete");
}

4.7.3、自定义Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码public class EmployeeRealm extends AuthorizingRealm {
//提供认证信息
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//暂且使用假数据来模拟数据库中真实的账号和密码
Employee employee = new Employee();
employee.setName("admin");
employee.setPassword("1");
//获取token中需要登录的账号名
String username = (String)token.getPrincipal();
//如果账号存在,则返回一个 AuthenticationInfo 对象
if(username.equals(employee.getName())){
return new SimpleAuthenticationInfo(
employee,//身份对象,与主体subject绑定在一起的对象,暂时无用但后续要用
employee.getPassword(),//该账号真正的密码,传给shiro做密码校验的
this.getName()//当前 Realm 的名称,暂时无用,不需纠结
);
}
return null;
}
//提供授权信息
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//登录成功用户对象
Employee employee = (Employee)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 根据登录用户的id查询到其拥有的所有角色的编码,这里进行模拟数据库
List<String> roleSns = Arrays.asList("hr","manager","seller");
// 将用户拥有的角色添加到授权信息对象中,供 Shiro 权限校验时使用
info.addRoles(roleSns);
// 根据登录用户的 id 查询到其拥有的所有权限表达式,这里进行模拟数据库
List<String> expressions = Arrays.asList("employee:list","employee:save");
// 将用户拥有的权限添加到授权信息对象中,供 Shiro 权限校验时使用
info.addStringPermissions(expressions);
return info;
}
}

4.8、SSM整合Shiro认证

在开发中,我们一般使用注解来完成授权操作, 我们需要使用 Shiro 自身提供的一套注解来完成。

4.8.1、贴注解

在 Controller 的方法上贴上 Shiro 提供的权限注解(@RequiresPermissions,@RequiresRoles)
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码  // 说明需要有这个权限才可以访问这个方法 
@RequiresPermissions(value = "employee:list")
@RequestMapping("list")
public String listAll(@ModelAttribute("qo") EmployeeQueryObject employeeQueryObject, Model model,
DepartmentQueryObject departmentQueryObject) {
PageInfo<Employee> pageInfo = employeeService.selectForPage(employeeQueryObject);
List<Department> departments = departmentService.selectAll();
List<Role> roles = roleService.selectAll();
model.addAttribute("departments", departments);
model.addAttribute("roles", roles);
model.addAttribute("pageInfo", pageInfo);
return "employee/list";
}

4.8.2、配置注解扫描

当扫描到 Controller 中有使用 @RequiresPermissions 注解时,会使用cglib动态代理为当前 Controller 生成代理对象,增强对应方法,进行权限校验
1
2
3
4
5
6
xml复制代码<!-- <aop:config/> 会扫描配置文件中的所有advisor,并为其创建代理 -->
<aop:config />
<!-- Pointcut advisor通知器, 会匹配所有加了shiro权限注解的方法 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

4.8.3、修改自定义Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 创建info对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 获取当前登录主体对象
//Subject subject = SecurityUtils.getSubject();
// 获取主体的身份对象(这里获取的对象与认证方法doGetAuthenticationInfo返回的SimpleAuthenticationInfo对象的第一个参数是同一个对象)
Subject subject = SecurityUtils.getSubject();
Employee employee = (Employee) subject.getPrincipal();
// 判断是否是超级管理员
if (employee.getAdmin()) {
info.addRole("admin");
// 给他所有的权限
info.addStringPermission(" *:* ");
} else {
// 通过员工id拿到所有的角色
List<Role> roles = employeeService.selectRolesById(employee.getId());
for (Role role : roles) {
info.addRole(role.getSn());
}
//通过员工id查询出所有的权限
List<String> permissionByEmployeeId = employeeService
.getPermissionByEmployeeId(employee.getId());
info.addStringPermissions(permissionByEmployeeId);
}
return info;
}

4.8.4、配置自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码 @ExceptionHandler(AuthorizationException.class)
public String BussinessException(AuthorizationException exception, HttpServletResponse response,
HandlerMethod method) throws IOException {
exception.printStackTrace(); //方便开发的时候找bug
//如果原本控制器的方法是返回jsonresult数据,现在出异常也应该返回jsonresult
//获取当前出现异常的方法,判断是否有ResponseBody注解,有就代表需要返回jsonresult
if (method.hasMethodAnnotation(ResponseBody.class)) {
try {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(new ResponseResult(false, "没有权限操作")));
} catch (IOException e1) {
e1.printStackTrace();
}
return null;
}
//如果原本控制器的方法是返回视图页面,现在也应该返回视图页面
return "common/nopermission";
}

4.8.5、Shiro标签集成FreeMarker

在前端页面上,我们通常可以根据用户拥有的权限来显示具体的页面,如:用户拥有删除员工的权限,页面上就把删除按钮显示出来,否则就不显示删除按钮,通过这种方式来细化权限控制。我们需要使用Shiro标签来进行控制。要能够实现上面的控制,需要使用 Shiro 中提供的相关标签。

4.8.5.1、拓展FreeMarker标签

前端页面我们选择的是freemarker,**而默认 freemarker 是不支持 shiro 标签的**,所以需要对其功能做拓展,可以理解为注册 shiro 的标签,达到在freemarker 页面中使用的目的。
1
2
3
4
5
6
7
8
9
10
java复制代码public class ShiroFreeMarkerConfig extends FreeMarkerConfigurer {

@Override
public void afterPropertiesSet() throws IOException, TemplateException {
//继承之前的属性配置,这不能省
super.afterPropertiesSet();
Configuration cfg = this.getConfiguration();
cfg.setSharedVariable("shiro", new ShiroTags());//注册shiro 标签,这里可以换成自己想要其他的标签,但是一般建议是用shiro
}
}

4.8.5.2、修改mvc.xml中的配置

在mvc.xml 中把以前的FreeMarkerConfigurer修改成我们自定义的MyFreeMarkerConfig类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码  <!-- 注册 FreeMarker 配置类 -->
<bean class="cn.linstudy.shiro.ShiroFreeMarkerConfig">
<!-- 配置 FreeMarker 的文件编码 -->
<property name="defaultEncoding" value="UTF-8"/>
<!-- 配置 FreeMarker 寻找模板的路径 -->
<property name="templateLoaderPath" value="/WEB-INF/views/"/>
<property name="freemarkerSettings">
<props>
<!-- 兼容模式 ,配了后不需要另外处理空值问题,时间格式除外 -->
<prop key="classic_compatible">true</prop>
<!-- 数字格式化 , 不会有,字符串的 -->
<prop key="number_format">0.##</prop>
</props>
</property>
</bean>

4.8.5.3、常用标签

4.8.5.3.1、authenticated标签
authenticated标签里面囊括的表示的是已经通过了认证了的用户才会显示的前端界面
1
html复制代码<@shiro.authenticated> </@shiro.authenticated>
4.8.5.3.2、notAuthenticated标签
与authenticated标签相对立,表示为认证通过的用户。
1
html复制代码<@shiro.notAuthenticated></@shiro.notAuthenticated>
4.8.5.3.3、principal 标签
principal 标签表示的是输出当前用户的信息。,通常可以用来输出登录用户的用户名。
1
html复制代码<@shiro.principal property="name" />
4.8.5.3.4、hasRole 标签
hasRole表示验证当前用户是否拥有某些角色。
1
html复制代码<@shiro.hasRole name="admin">Hello admin!</@shiro.hasRole>
4.8.5.3.5、hasAnyRoles 标签
hasAnyRoles标签表示验证当前用户是否拥有这些角色中的任何一个,角色之间逗号分隔。
1
html复制代码<@shiro.hasAnyRoles name="admin,user">Hello admin</@shiro.hasAnyRoles>
4.8.5.3.6、hasPermission 标签

hasPermission 标签表示验证当前用户是否拥有该权限。

1
html复制代码<@shiro.hasPermission name="department:delete">删除</@shiro.hasPermission>

4.8.6、重构权限加载方法

这里要说的是一种思想,我们在项目中可能会遇到需要加载项目中加了@RequiresPermissions注解的权限,就会有一个类似加载权限的按钮。

image-20210325114035329

但是我们发现好像Shiro的@RequiresPermissions注解并没有提供name属性给我们,仅仅只有value属性,那么我们需要另辟蹊径来完成这个需求。
Shiro的@RequiresPermissions注解有两个属性:
  1. value属性:这个属性是一个数组,也就是说一个请求映射方法可以运行配置多个权限。多个权限之间用逗号隔开
1
java复制代码value={"employee:list","employee:delete", }
  1. logical 属性:该属性根据配置属性值对当前用户是否有权限访问请求映射方法进行限制,他有两个值:

Logical.AND: 必须同时拥有value配置所有权限才允许访问。
Logical.OR:只需要拥有value配置所有权限中一个即可允许访问。

我们可以约定@RequiresPermissions 注解中的value属性值(数组)中第一位为权限表达式, 第二位为权限名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码  // shiro注解无法使用name属性,所以约定,value中第一个位置的值是权限表达式,第二个位置的值是权限名称.
// 但是 logical 的值必须是Logical.OR
@RequiresPermissions(value = {"employee:list", "员工列表"}, logical = Logical.OR)
@RequestMapping("list")
public String listAll(@ModelAttribute("qo") EmployeeQueryObject employeeQueryObject, Model model,
DepartmentQueryObject departmentQueryObject) {
PageInfo<Employee> pageInfo = employeeService.selectForPage(employeeQueryObject);
List<Department> departments = departmentService.selectAll();
List<Role> roles = roleService.selectAll();
model.addAttribute("departments", departments);
model.addAttribute("roles", roles);
model.addAttribute("pageInfo", pageInfo);
return "employee/list";
}
同时修改reload方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码 public void reload() {
//一次性的把所有的权限信息查出来.
List<Permission> permissionsOnDB = permissionMapper.selectAll();
// 拿到所有方法
Map<RequestMappingInfo, HandlerMethod> handlerMethods = rmhm.getHandlerMethods();
// 遍历所有方法
for (HandlerMethod method : handlerMethods.values()) {
// 判断方法上是否有RequiresPermissions注解
RequiresPermissions annotation = method.getMethodAnnotation(RequiresPermissions.class);
if (annotation != null) {
// 如果不为空。说明是贴了注解的
// 数组的第一个元素表示的是权限表达式
String expression = annotation.value()[0];
// 数组的第二个元素表示的是权限的名称
String name = annotation.value()[1];
// 在存入数据库之前先判断以下,如果数据库中没有才插入
if (!permissionsOnDB.contains(expression)) {
Permission permission = new Permission();
permission.setName(name);
permission.setExpression(expression);
permissionMapper.insert(permission);
}
}
}
}

五、Shiro密码加密

加密的目的是从系统数据的安全考虑,如,用户的密码,如果我们不对其加密,那么所有用户的密码在数据库中都是明文,只要有权限查看数据库的都能够得知用户的密码,这是非常不安全的。所以,只要密码被写入磁盘,任何时候都不允许是明文, 以及对用户来说非常机密的数据,我们都应该想到使用加密技术,这里我们采用的是 MD5+盐+散列次数来进行加密。

如何实现项目中密码加密的功能:

  1. 添加用户的时候,对用户的密码进行加密
  2. 登录时,按照相同的算法对表单提交的密码进行加密然后再和数据库中的加密过的数据进行匹配

5.1、MD5+盐加密

MD5 加密的数据如果一样,那么无论在什么时候加密的结果都是一样的,所以,相对来说还是不够安全,但是我们可以对数据加“盐”。同样的数据加不同的“盐”之后就是千变万化的,因为我们不同的人加的“盐”都不一样。这样得到的结果相同率也就变低了。


**盐一般要求是固定长度的字符串,且每个用户的盐不同。**


可以选择用户的唯一的数据来作为盐(账号名,身份证等等),注意使用这些数据作为盐要求是不能改变的,假如登录账号名改变了,则再次加密时结果就对应不上了。

5.2、Md5Hash()

Md5Hash()这个方法有三个参数,第一个参数表示需要加密的密码的明文,第二个参数表示加密时所需要的盐,第三个参数表示散列次数(加密几次),这样可以保证加密后的密文很难恢复和破解。

5.3、注册用户(密码加密)

在添加用户的时候,需要对用户的密码进行加密。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RequestMapping("checkUsername")
@ResponseBody
public ResponseResult checkUsername(String username, String password, HttpSession session) {
Employee employee = employeeService.selectByUsername(username);
if (employee == null) {
// 进行MD5密码加密
Md5Hash md5Hash = new Md5Hash(password, username, 1024);
EmployeeInsertVO employeeVO = new EmployeeInsertVO(username, username, md5Hash.toString(),
session.getAttribute("EMAIL_IN_SESSION").toString(), false, true);
employeeService.regsiter(employeeVO);
return new ResponseResult(true, "注册成功");
} else {
return new ResponseResult(false, "用户名已存在");
}
}

5.4、登录

在登录时, 先对前端传过来的密码进行与注册相同的相同算法的加密,再传给shiro进行认证处理即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码try {
// 对传进来的密码进行加密
Md5Hash md5Hash = new Md5Hash(password, username, 1024);
UsernamePasswordToken token = new UsernamePasswordToken(username, md5Hash.toString());
SecurityUtils.getSubject().login(token);
Employee employee = employeeMapper.selectByUsername(username);
return employee;
} catch (UnknownAccountException e) {
throw new CarBussinessException("用户名错误");
} catch (IncorrectCredentialsException e) {
throw new CarBussinessException("密码错误");
}
}

六、Shiro集成EhCache

6.1、Cache是什么

Cache是缓存,他是**计算机内存中一段数据** ,他的作用是 **用来减轻DB的访问压力,从而提高系统的查询效率。**

image-20200530090656417

6.2、使用缓存的原因

我们在进行Debug的时候,我们会发现,一旦请求到需要权限控制的方法的时候,每请求一次他都会去调用自定义Realm中的 doGetAuthorizationInfo 方法获取用户的权限信息,这个时候对数据库造成的访问压力是十分大的,而且用户登陆后,授权信息一般很少变动,所以我们可以在第一次授权后就把这些授权信息存到缓存中,下一次就直接从缓存中获取,避免频繁访问数据库。

6.3、集成EhCache

6.3.1、引入依赖

1
2
3
4
5
6
xml复制代码<!--引入shiro和ehcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>

6.3.2、添加缓存配置文件

1
2
3
4
5
6
7
8
9
xml复制代码<ehcache>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
配置文件属性详解:

maxElementsInMemory: 缓存对象最大个数。

**eternal **:对象是否永久有效,一但设置了,timeout 将不起作用。

timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。

timeToLiveSeconds:对象存活时间,指对象从创建到失效所需要的时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,默认是 0,也就是对象存活时间无穷大。

memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。

缓存策略一般有3种:

  1. 默认LRU(最近最少使用,距离现在最久没有使用的元素将被清出缓存)。
  2. FIFO(先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉)。
  3. LFU(较少使用,意思是一直以来最少被使用的,缓存的元素有一个hit 属性(命中率),hit 值最小的将会被清出缓存)。

6.3.2、配置缓存管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--注册自定义数据源-->
<property name="realm" ref="employeeRealm"/>
<!--注册缓存管理器-->
<property name="cacheManager" ref="cacheManager"/>
</bean>

<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!-- 设置配置文件 -->
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…576577578…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%