JoyLau's Blog

JoyLau 的技术学习与思考

说明

  1. 以前的笔记本是 windows7 的操作系统
  2. 6GB 内存
  3. 还剩 96G 硬盘
  4. 打算安装 Centos 7.2

材料

  1. U 盘一个(>= 8G)
  2. centos 镜像文件
  3. 刻录软件 UltraISO (官网直接下载试用版的即可)

安装过程

  1. 在 windows 系统下压缩出磁盘空间或者直接格式化一个盘出来用来装 centos ,注意盘的格式 要为fat32
  2. UltraISO 烧录镜像到U盘,U盘会被磁盘格式会改变且会被格式化
  3. 重启系统,以U盘启动
  4. 指定U盘安装
  5. 安装配置
  6. 等待进入系统

注意事项

  1. 在 U 盘启动的时候,在安装界面上会有三个选项,选择第一个 Install Centos,按tab键进行配置
  2. 找到U盘位置的方法: vmlinuz initrd=initrd.img linux dd
  3. 这个时候很容易找到 U 的device,记下来(我当时U的device是 sdb4),重启机器,在进入上一步的步骤
  4. 这时,将参数改为 : vmlinuz initrd=initrd.img inst.stage2=hd:/dev/sdb4 接下来等待即可
  5. 选择安装位置,下方一定要选择自定义分区
  6. 分区策略就选默认的,创建新的分区,分区的大小就按照默认分配的就好,不需要改变

WiFi 问题

因为笔记本自带了 wifi 模块,想着不要用网线扯来扯去的,直接用wifi连接网络就好了啊
一切都没想象的那么简单….

因为我之前安装的时候选择了最小化安装,进去系统后什么都没有,一起都用通过命令行来解决
第一次我是根据这篇文章的步骤来的 http://www.jb51.net/article/100300.htm
中间遇到了很多问题 network 服务不可用;systemctl restart network 也起不来,一直报错;ping 域名不通,ping ip 不通;ifconfig 命令不存在….总之一大堆问题
问题一个个解决,最后终于连上家里的wifi
后来重启了下,一切回到解放前
我去….
一顿惆怅
后来我装了个gnome图形界面,连上了wifi
在切换命令行使用,不使用图形界面,现在一切完好,而且内存占用空间大幅减少

WiFi 连接命令

  1. 设置NetworkManager自动启动
    chkconfig NetworkManager on
  2. 安装NetworkManager-wifi
    yum -y install NetworkManager-wifi
  3. 开启WiFi
    nmcli r wifi on
  4. 测试(扫描信号)
    nmcli dev wifi
    扫描不到可用 iw wlp8s0b1(网卡名称) scan | grep SSID 扫描一下
  5. 连接
    nmcli dev wifi connect password

注: 总结多次安装 centos 系统得出一个结论,如果在安装过程中选择打开 WiFi 并连接网络,系统安装完成后,会在 /etc/sysconfig/network-scripts/ 目录下生成一个 ifcfg-WiFi名称 文件和 keys-WiFi名称的密码文件,之后安装 NetworkManage-wifi 便可每次开机都能自动启动 WiFi,如果是装完系统在命令连接的话则每次重启后都需要自己手动连接 WiFi,这是扫描原因暂且不得而知

切换命令行和图形界面

1
2
systemctl set-default multi-user.target  //设置成命令模式
systemctl set-default graphical.target //设置成图形模式

关闭盖子不睡眠

vim /etc/systemd/logind.conf

HandlePowerKey     按下电源键后会触发的行为
HandleSleepKey      按下挂起键后会触发的行为
HandleHibernateKey   按下休眠键后会触发的行为
HandleLidSwitch     关闭笔记本盖子后会触发的行为

只需要把HandleLidSwitch选项设置为 HandleLidSwitch=lock

设置完成保存后运行 systemctl restart systemd-logind 命令才生效

恢复 Windows 启动项

windows 7、8/10 安装centos7双系统后,默认会将mbr改写成为grub2,而默认的centos7不识别windows 的ntfs分区,所以启动项没有windows。
可以用3条命令,即可将windows添加到grub2的启动项。

1
2
3
yum -y install epel-release
yum -y install ntfs-3g
grub2-mkconfig -o /boot/grub2/grub.cfg

重启

最小化安装时配置静态 IP 地址

  1. vim /etc/sysconfig/network-scripts/ifcfg-网络接口名称,默认第一个是网络接口名称

  2. 修改以下红色标注的配置

    TYPE=Ethernet

    BOOTPROTO=static

    DEFROUTE=yes

    IPV4_FAILURE_FATAL=no

    IPV6INIT=yes

    IPV6_AUTOCONF=yes

    IPV6_DEFROUTE=yes

    IPV6_FAILURE_FATAL=no

    NAME=eno16777736

    UUID=9e8d604f-d991-4aa2-88a3-4c679e6f139c

    DEVICE=eno16777736

    ONBOOT=yes

    PEERDNS=yes

    PEERROUTES=yes

    IPV6_PEERDNS=yes

    IPV6_PEERROUTES=yes

    HWADDR=B8:70:F4:24:61:A7 #MAC地址

    IPADDR=192.168.10.29 #静态IP

    GATEWAY=192.168.10.1 #默认网关

    NETMASK=255.255.255.0 #子网掩码

    DNS1=61.132.163.68 #DNS配置

  3. 重启网络服务 :service network restart

时间同步

更新时间 2018-08-01 16:36:09
系统安装 ntpdate
查看本地时间: date
本地时间与服务器时间同步 : ntpdate ntp1.aliyun.com
查看bois时间: hwclock
将本地时间写入到bois时间内: hwclock -w

img

  • val可以将变量申明是final类型。
  • @NonNull注解能够为方法或构造函数的参数提供非空检查。
  • @Cleanup注解能够自动释放资源。
  • @Getter/@Setter注解可以针对类的属性字段自动生成Get/Set方法。
  • @ToString注解,为使用该注解的类生成一个toString方法,默认的toString格式为:ClassName(fieldName= fieleValue ,fieldName1=fieleValue)。
  • @EqualsAndHashCode注解,为使用该注解的类自动生成equals和hashCode方法。
  • @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor,这几个注解分别为类自动生成了无参构造器、指定参数的构造器和包含所有参数的构造器。
  • @Data注解作用比较全,其包含注解的集合 @ToString, @EqualsAndHashCode,所有字段的 @Getter和所有非final字段的 @Setter, @RequiredArgsConstructor。
  • @Builder注解提供了一种比较推崇的构建值对象的方式。
  • @Synchronized注解类似Java中的Synchronized 关键字,但是可以隐藏同步锁

官网地址: https://www.projectlombok.org/features/all

说明

  1. Filebeat 版本为 5.3.0
    之所以使用 beats 家族的 Filebeat 来替代 Logstash 是因为 Logstash 实在太消耗资源了(服务器资源充足的土豪请无视)
    在官网下载 Logstash 有 89M,而 Filebeat 才8.4M,由此可见一斑
    Logstash 可以配置 jvm 参数,经过我本身的调试,内存分配小了,启动很慢有时根本起不来,分配大了,其他服务就没有资源了
    所有说对于配置低的服务器,选择 Filebeat 是最好的选择了,而且现在 Filebeat 已经开始替代 Logstash 了
  2. 依然需要修改 nginx 的日志格式

nginx.config

更改日志记录的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
log_format json '{ "@timestamp": "$time_iso8601", '
'"time": "$time_iso8601", '
'"remote_addr": "$remote_addr", '
'"remote_user": "$remote_user", '
'"body_bytes_sent": "$body_bytes_sent", '
'"request_time": "$request_time", '
'"status": "$status", '
'"host": "$host", '
'"request": "$request", '
'"request_method": "$request_method", '
'"uri": "$uri", '
'"http_referrer": "$http_referer", '
'"body_bytes_sent":"$body_bytes_sent", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"http_user_agent": "$http_user_agent" '
'}';

access_log /var/log/nginx/access.log json;

filebeat.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#=========================== Filebeat prospectors =============================

filebeat.prospectors:

- input_type: log

# Paths that should be crawled and fetched. Glob based paths.
paths:
- /var/log/nginx/*access*.log
json.keys_under_root: true
json.overwrite_keys: true

#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
# Array of hosts to connect to.
hosts: ["ip:port","ip:port"]
index: "filebeat_server_nginx_%{+YYYY-MM}"

这里面需要注意的是
json.keys_under_root: 默认这个值是FALSE的,也就是我们的json日志解析后会被放在json键上。设为TRUE,所有的keys就会被放到根节点
json.overwrite_keys: 是否要覆盖原有的key,这是关键配置,将keys_under_root设为TRUE后,再将overwrite_keys也设为TRUE,就能把filebeat默认的key值给覆盖了

还有其他的配置
json.add_error_key:添加json_error key键记录json解析失败错误
json.message_key:指定json日志解析后放到哪个key上,默认是json,你也可以指定为log等。

说白了,差别就是,未配置前elasticsearch的数据是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"_index": "filebeat_server_nginx_2018-05",
"_type": "log",
"_id": "AWM9sVOkCcRcg0IPg399",
"_version": 1,
"_score": 1,
"_source": {
"@timestamp": "2018-05-08T03:00:17.544Z",
"beat": {
"hostname": "VM_252_18_centos",
"name": "VM_252_18_centos",
"version": "5.3.0"
},
"input_type": "log",
"json": {},
"message": "{ "@timestamp": "2018-05-08T11:00:11+08:00", "time": "2018-05-08T11:00:11+08:00", "remote_addr": "113.16.251.67", "remote_user": "-", "body_bytes_sent": "403", "request_time": "0.000", "status": "200", "host": "blog.joylau.cn", "request": "GET /img/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90.png HTTP/1.1", "request_method": "GET", "uri": "/img/\xE7\xBD\x91\xE6\x98\x93\xE4\xBA\x91\xE9\x9F\xB3\xE4\xB9\x90.png", "http_referrer": "http://blog.joylau.cn/css/style.css", "body_bytes_sent":"403", "http_x_forwarded_for": "-", "http_user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36" }",
"offset": 7633,
"source": "/var/log/nginx/access.log",
"type": "log"
}
}

配置后,是这样的:

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
{
"_index": "filebeat_server_nginx_2018-05",
"_type": "log",
"_id": "AWM9rjLd8mVZNgvhdnN9",
"_version": 1,
"_score": 1,
"_source": {
"@timestamp": "2018-05-08T02:56:50.000Z",
"beat": {
"hostname": "VM_252_18_centos",
"name": "VM_252_18_centos",
"version": "5.3.0"
},
"body_bytes_sent": "12576",
"host": "blog.joylau.cn",
"http_referrer": "http://blog.joylau.cn/",
"http_user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"http_x_forwarded_for": "-",
"input_type": "log",
"offset": 3916,
"remote_addr": "60.166.12.138",
"remote_user": "-",
"request": "GET /2018/03/01/JDK8-Stream-Distinct/ HTTP/1.1",
"request_method": "GET",
"request_time": "0.000",
"source": "/var/log/nginx/access.log",
"status": "200",
"time": "2018-05-08T10:56:50+08:00",
"type": "log",
"uri": "/2018/03/01/JDK8-Stream-Distinct/index.html"
}
}

这样看起来就很舒服了

启动 FileBeat

进入 Filebeat 目录

1
nohup sudo ./filebeat -e -c filebeat.yml >/dev/null 2>&1 & 

更新

nginx 的日志里含有中文的话,会将中文转为 Unicode 编码,如果不转的话,加入 escape=json 参数就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
log_format json escape=json '{ "@timestamp": "$time_iso8601", '
'"time": "$time_iso8601", '
'"remote_addr": "$remote_addr", '
'"remote_user": "$remote_user", '
'"body_bytes_sent": "$body_bytes_sent", '
'"request_time": "$request_time", '
'"status": "$status", '
'"host": "$host", '
'"request": "$request", '
'"request_method": "$request_method", '
'"uri": "$uri", '
'"http_referrer": "$http_referer", '
'"body_bytes_sent":"$body_bytes_sent", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"http_user_agent": "$http_user_agent" '
'}';

access_log /var/log/nginx/access.log json;

说明

logstash 需要和 nginx 部署到一台机器
需要修改 nginx 的日志格式

nginx.config

更改日志记录的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
log_format json '{ "@timestamp": "$time_iso8601", '
'"time": "$time_iso8601", '
'"remote_addr": "$remote_addr", '
'"remote_user": "$remote_user", '
'"body_bytes_sent": "$body_bytes_sent", '
'"request_time": "$request_time", '
'"status": "$status", '
'"host": "$host", '
'"request": "$request", '
'"request_method": "$request_method", '
'"uri": "$uri", '
'"http_referrer": "$http_referer", '
'"body_bytes_sent":"$body_bytes_sent", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"http_user_agent": "$http_user_agent" '
'}';

access_log /var/log/nginx/access.log json;

log-file.config

input 里添加 file 类型

1
2
3
4
5
6
7
8
9
10
input {
file {
path => "/var/log/nginx/access.log"
codec => "json"
start_position => "beginning"
type => "server_nginx"
tags => ["nginx"]
}
}

说明

  1. Elasticsearch, Logstash,Kibana 版本都是5.3.0
  2. SpringBoot 集成 ELK,实际上指的就是 SpringBoot 与 Logstash 的整合
  3. Elasticsearch 负责数据的存储,Logstash 负责数据的接受和数据的发送,相当于一个中转站,Kibana 负责数据的展示,查询
  4. SpringBoot 项目是我们产生日志并且需要存储和分析的项目
  5. SpringBoot 我还是使用的默认的 logback 日志系统,当然也可以采用 log4j,不过我还是比较喜欢 logback,性能好,配置少,有颜色

Elasticsearch 集群搭建

Logstash 安装

  1. 官网下载 Logstash
  2. 解压
  3. 添加配置文件 log.config
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
input {
tcp {
host => "192.168.10.78"
type => "dev"
tags => ["spring-boot"]
port => 4560
codec => json_lines
}

tcp {
host => "192.168.10.78"
type => "server"
tags => ["spring-boot"]
port => 4561
codec => json_lines
}

tcp {
host => "192.168.10.78"
type => "work_dev"
tags => ["boot"]
port => 4568
codec => json_lines
}
}

filter {

}

output {
if[type] == "work_dev" {
elasticsearch {
hosts => ["ip:9268"]
index => "logstash_%{type}_%{+YYYY-MM}"
}
} else {
elasticsearch {
hosts => ["http://192.168.10.232:9211"]
index => "logstash_%{type}_%{+YYYY-MM}"
}
}
}

总的来说,配置文件里由 input,filter,output,这里我没有特别复杂的需求,filter就没有配置
我这里有三个input,但是都是 tcp 类型的
意思配置了三个input,分别监听192.168.10.78(就是安装logstash的机器)的4560,4561,和4568端口,有数据发送过来的话就进行output处理
这里我配置了3个type,这个type也就是elasticsearch里索引的type,并且该type可作为参数在output里判断进行不同的处理
codec 是的对日志数据进行处理的插件,这里是 json_lines
所以需要安装插件

1
sh bin/logstash-plugin install logstash-codec-json_lines

elasticsearch:hosts es的http地址和端口
index 是创建的索引名
如果要配置索引模板的话,可以添加以下配置
manage_template => true
template_name => “template_name”
template_overwrite => true
template => “/usr/local/path.json”

配置好了,我们检验下配置文件是否正确

1
sh /app/logstash-5.3.0/bin/logstash -f /app/logstash-5.3.0/config/log.config -t

没有问题的话就可启动了,后台启动的就用 nohup

1
sh /app/logstash-5.3.0/bin/logstash -f /app/logstash-5.3.0/config/log.config

启动成功的话,9600端口可以获取到 logstash 的相关信息

SpringBoot 集成 Logstash

  1. 添加依赖:
1
2
3
4
5
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.1</version>
</dependency>
  1. 添加配置 logstash 文件
    在 resources 下直接添加 logback.xml 文件即可
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>ip:4568</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<root level="INFO">
<appender-ref ref="LOGSTASH" />
</root>

</configuration>

这里我是使用的是 SpringBoot 自带的 logback 日志
SpringBoot 默认会读取 resources 目录下的 logback.xml 作为配置文件,别问我怎么知道的(我特地查看了源码:org.springframework.boot.logging.logback.LogbackLoggingSystem,”logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml”这些文件检测到都会读取其中的配置的)
配置文件里我只配置了 一个Appender,就是net.logstash.logback.appender.LogstashTcpSocketAppender,用来输出日志到logstash的,并且级别是 INFO
destination 指的就是 logstash 的地址
encoder 就配置LogstashEncoder不要变
再把 SpringBoot默认的配置引入base.xml

好了,SpringBoot 集成 Logstash 完毕

注 :后来我想用 javaConfig 去配置 SpringBoot和Logstash,不过没有成功,哪位大佬看到这个信息,可以给我留言下怎么配置
xml,也很方便,打包部署后可以作为配置文件修改

那么,这个时候启动项目,elasticsearch里面就会看到有新的索引数据了

Kibana 安装

  1. 其实 Kibana 非必须安装,只是用来统计数据和查询数据的,用来提供一个可视化的界面
  2. 下载 Kibana
  3. 修改配置文件 kibana.yml
    server.port: 5668
    server.host: “0.0.0.0”
    elasticsearch.url: “http://localhost:9268
  4. 后台启动
  5. 访问kibana的地址即可

说明

  1. 机器三台
  2. 彼此间内网不同,公网可通(因为这个问题花费了很长时间,配置文件里有我的理解说明)
  3. 机器配置很低,需要调节jvm参数来优化
  4. elasticsearch 版本为 5.3.0

elasticsearch.yml

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
cluster.name: joylau-es
node.name: joylau
# 有资格作为主节点
node.master: true
# 节点存储数据
node.data: true
# 绑定的ip地址
# 这里原来默认的是 network.host,如果配置了network.host,则一下2个配置的属性都为network.host的值
# 集群中各个节点内网不通,集群搭建不起来的原因就在这里,我也是查阅了大量资料,花费了好长时间,才搞明白
# 绑定地址,这里配置任何ip都能访问
network.bind_host: 0.0.0.0
# 这里配置该节点的公网IP地址,在集群启动时就不会使用默认内网地址寻找策略,就会以配置的公网地址来寻找该节点
network.publish_host: ip
#
# Set a custom port for HTTP:
#
http.port: 9268
transport.tcp.port: 9368
#
# For more information, consult the network module documentation.
# 集群的各个节点配置
discovery.zen.ping.unicast.hosts: ["ip1:9368", "ip2:9368", "ip3:9368"]
#
# Prevent the "split brain" by configuring the majority of nodes (total number of master-eligible nodes / 2 + 1):
# 上句话的意思是采取过半原则的策略配置节点数,为了防止“脑裂”情况,数量建议为 (节点总数/2) + 1
# 我的理解就是最少有多少个节点的时候开始选取主节点,这里我配置的1,比如说我现在有3个几点,其中一个节点的网络断了
# 如果配置的2 的话,那么有2个节点的会投票选取主节点,成为一个集群,剩下的那个节点无法选取主节点而挂了
# 如果配置的 1 的话,剩下的那个节点就会自己成为主节点而单独成为一个集群,这样就有2个集群了
# 说了这么多,大致的意思就是这样,我是这么理解的
#
discovery.zen.minimum_master_nodes: 1

剩下没贴出配置的都是默认配置

依照改配置,在各个节点上修改节点名称及network.publish_host,要保证集群名称一样就可以了。

jvm.options

主要配置
-Xms1400m
-Xmx1400m

我这里的机器是2G的运存,经过我的反复调试,能给出elasticsearch最大的内存空间就是1400m了,给多了跑步起来,给少了有不能完全发挥elasticsearch的性能优势
机器差,没办法
还有一点注意的是初始化内存大小个最大内存大小的配置数值要是一样的,否则会启动出错

GarageBand,这个是系统上的模拟乐器,一般都使用不到

1
2
3
rm -rf /Library/Application\ Support/GarageBand
rm -rf /Library/Application\ Support/Logic
rm -rf /Library/Audio/Apple\ Loops

但是有些系统文件显示占用的空间很大,该怎么看呢

1
du -sh *

这个命令用来查看根目录下,所有文件的大小分布

比如,我的电脑 Library 文件路径最大

那就在进入 Library 文件路径,再执行 du -sh *

直至找到占用内存最大的文件,然后结合实际情况,进行删减

问题描述

在 SpringSecurity 中,我想配置一个关于session并发的控制,于是我是这样配置的

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.invalidSessionStrategy(new InvalidSessionStrategyImpl())
.maximumSessions(-1).expiredSessionStrategy(expiredSessionStrategy())//配置并发登录,-1表示不限制
.sessionRegistry(sessionRegistry());
}

上下文的配置我在此省略了

这里设置 maximumSessions 为 -1,表示不限制同一账号登录的客户端数

session过期后执行的逻辑是进入我自定义的类 expiredSessionStrategy() 中

因为我是构建的 rest 服务,所以我是返回的 http 状态码

1
2
3
4
5
6
7
8
public class ExpiredSessionStrategyImpl implements SessionInformationExpiredStrategy {

@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
event.getResponse().sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
JSONObject.toJSONString(MessageBody.failure(405,"not login or login has been expired")));
}
}

在这里,问题就来了

我测试的时候,把 -1 改成了 1,之后登录同一个用户,后面登录的用户会把前面一个已经登录的用户挤下线,就是说之前登录的那个用户的session 会过期

就是说他所在的页面再发送任何请求的话会收到我返回的 405 状态码

在这里是没问题的

问题就在发完一个请求后,在发一个请求,在浏览器的 network 上会看到发出的请求会被重定向的 /login 请求上

后续再发任何请求都会被重定向到 /login 上

问题思考

为什么会出现这样的情况呢?

为什么会第一个请求会收到405的状态码,后续的请求会被重定向到 /login 呢?

通过 debug 断点,我定位到过滤器的前置执行方法 beforeInvocation() 上

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
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();

if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);

if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}

if (debug) {
logger.debug("Public object - authentication not attempted");
}

publishEvent(new PublicInvocationEvent(object));

return null; // no further work post-invocation
}

if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}

if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}

Authentication authenticated = authenticateIfRequired();

// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));

throw accessDeniedException;
}

if (debug) {
logger.debug("Authorization successful");
}

if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}

// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);

if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}

// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}

SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);

// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}

问题出在了 SecurityContextHolder.getContext().getAuthentication() == null

getAuthentication() 为 null,于是进入了credentialsNotFound(),抛出了 AuthenticationCredentialsNotFoundException 异常

确实,在控制台上也能看到抛出的异常信息

问题深入

AuthenticationCredentialsNotFoundException 是 AuthenticationException 异常的子类

不仅仅是 AuthenticationCredentialsNotFoundException 还有其他很多异常都是异常的子类

既然抛出了异常,猜测肯定是被某个处理器给处理了而且处理的默认机制是重定向到 /login

于是继续搜索 SpringSecurity 异常处理器

我找到的答案是 ExceptionTranslationFilter

ExceptionTranslationFilter 是Spring Security的核心filter之一,用来处理AuthenticationException和AccessDeniedException两种异常(由FilterSecurityInterceptor认证请求返回的异常)

ExceptionTranslationFilter 对异常的处理是通过这两个处理类实现的,处理规则很简单:

规则1. 如果异常是 AuthenticationException,使用 AuthenticationEntryPoint 处理
规则2. 如果异常是 AccessDeniedException 且用户是匿名用户,使用 AuthenticationEntryPoint 处理
规则3. 如果异常是 AccessDeniedException 且用户不是匿名用户,如果否则交给 AccessDeniedHandler 处理。

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
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);

sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);

sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);

accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}

我们这里的异常是 AuthenticationException ,紧接着就找 sendStartAuthentication() 方法

1
2
3
4
5
6
7
8
9
10
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}

上面的方法是先保存请求,之后执行 authenticationEntryPoint.commence(request, response, reason), 再深入来看

默认实现 commence 接口的是 LoginUrlAuthenticationEntryPoint 类

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
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

String redirectUrl = null;

if (useForward) {

if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}

if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);

if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}

RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

dispatcher.forward(request, response);

return;
}
}
else {
// redirect to login page. Use https if forceHttps true

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

}

redirectStrategy.sendRedirect(request, response, redirectUrl);
}

我们看到了 redirectUrl = buildRedirectUrlToLoginPage(request, response, authException)

这下总算是知道了为什么会重定向了 /login 请求了

问题解决

知道问题的原因了,解决问题就很简单了,重新实现 commence 接口,返回http 状态码就可以了,于是加上这样的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.invalidSessionStrategy(new InvalidSessionStrategyImpl())
.maximumSessions(-1).expiredSessionStrategy(expiredSessionStrategy())//配置并发登录,-1表示不限制
.sessionRegistry(sessionRegistry())
.and()
.and()
.exceptionHandling()
.authenticationEntryPoint(new UnauthenticatedEntryPoint())
.accessDeniedHandler(new AuthorizationFailure());
}
1
2
3
4
5
6
7
8
public class UnauthenticatedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
if (!response.isCommitted()) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,"未认证的用户:" + authException.getMessage());
}
}
}

再次重试,发现会返回 405状态码了,不会在重定向到 /login 了

问题解决

0%