JoyLau's Blog

JoyLau 的技术学习与思考

添加多页面配置

  1. npm run eject
  2. 修改 webpack.config.js

entry 修改:
这里我加了一个 update.html 页面

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
entry: {
index: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
update: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + '/update.js',
].filter(Boolean),
},

output 修改

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
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name]bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
publicPath: publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},

注意修改其中的 filename

HtmlWebpackPlugin 修改:
新增一个 HtmlWebpackPlugin

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
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
chunks: ["index"]
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
chunks: ["update"],
filename: "update.html"
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),

在 public 目录里添加 update.html, 内容照抄 index.html 文件即可;
在 src 目录下添加 update.js 文件:

1
2
3
4
5
6
7
8
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Update from './page/Update';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<Update />, document.getElementById('root'));
serviceWorker.register();

之后, http://localhost:3000/update.html 即可访问; 如果想加个路径,直接修改 HtmlWebpackPlugin 里的 filename, 例如 filename: "index/update.html"
就可以 使用 http://localhost:3000/index/update.html 来访问

引入 src 目录以外的文件报错

例如需要引入 public 目录下的图片,就会报错,此时,注释掉

1
// new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),

这一行,重启即可.

步骤

创建 create-react-app-antd 项目

  1. git clone https://github.com/ant-design/create-react-app-antd
  2. npm install
  3. 将 webpack 所有内建的配置暴露出来, npm run eject, 如果发现错误,看下 package.json 里 eject 的脚本是不是为 react-scripts eject
  4. 修改 config-overrides.js
1
2
3
module.exports = function override(config, env) {
return config;
};
  1. 修改 webpack.config.js 里的 module.rules.oneOf 支持 css 和 less, 添加

    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
    {
    test: /\.(css|less)$/,
    use: [
    require.resolve('style-loader'),
    {
    loader: require.resolve('css-loader'),
    options: {
    importLoaders: 1,
    },
    },
    {
    loader: require.resolve('postcss-loader'),
    options: {
    // Necessary for external CSS imports to work
    // https://github.com/facebookincubator/create-react-app/issues/2677
    ident: 'postcss',
    plugins: () => [
    require('postcss-flexbugs-fixes'),
    autoprefixer({
    browsers: [
    '>1%',
    'last 4 versions',
    'Firefox ESR',
    'not ie < 9', // React doesn't support IE8 anyway
    ],
    flexbox: 'no-2009',
    }),
    ],
    },
    },
    {
    loader: require.resolve('less-loader'),
    options: { javascriptEnabled: true }
    },
    ],
    }
  2. 修改 start.js 注释掉下面代码关闭项目启动自动打开浏览器

1
// openBrowser(urls.localUrlForBrowser);
  1. package.json 添加 "homepage": "." ,防止打包后的静态文件 index.html 引入 css 和 js 的路径错误

  2. App.less 修改为 @import '~antd/dist/antd.less';

添加 electron

  1. package.json 添加 "main": "main.js", 和 electron 依赖
1
2
3
4
5
6
{
"main": "main.js",
"devDependencies": {
"electron": "^6.0.7"
}
}
  1. 创建 main.js,添加以下代码
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
const {app, BrowserWindow, Menu} = require('electron');

let win;

let windowConfig = {
width: 800,
height: 600,
title: "Joy Security",
};

let menuTemplate = [{
label: 'Joy Security',
submenu: [{
label: '退出',
role: 'quit'
}, {
label: `关于 ${windowConfig.title}`,
role: 'about'
}]
}];


app.on('ready', createWindow);

app.on('window-all-closed', () => {
app.quit();
});

app.on('activate', () => {
if (win == null) {
createWindow();
}
});


function createWindow() {
// 隐藏菜单栏,兼容 MAC
Menu.setApplicationMenu(Menu.buildFromTemplate([]));

win = new BrowserWindow(windowConfig);

win.loadURL('http://localhost:3000');

win.on('close', () => {
//回收BrowserWindow对象
win = null;
});

win.on('resize', () => {
// win.reload();
});

}

  1. package.json 更改脚本
1
2
3
4
5
6
7
{
"scripts": {
"react-start": "node scripts/start.js",
"eletron-start": "electron .",
"react-build": "node scripts/build.js",
}
}
  1. 启动时先 react-start 再 eletron-start 即可看到效果

背景

在群晖的 Docker 组件里添加了个人的私有仓库,发现却无法下载镜像….

分析

在 Docker 组件里添加新的仓库,并设置为使用仓库,发现在仓库里下载镜像总是失败,状态栏提示查看日志,可是在日志里总看不到东西

想了想,可能是新添加的 docker 私服是 http 的服务,而不是 https

方法

  1. 于是我使用 GateOne 组件进入 shell
  2. 使用命令 docker pull xxx:xxx, 发现报错 Get https://172.18.18.90:5000/v2/: http: server gave HTTP response to HTTPS client , 果然是这个问题
  3. 于是找到 Docker 组件的配置文件目录,在 /var/packages/Docker/etc 目录下,添加配置文件 daemon.json
1
2
3
{
"insecure-registries": ["domain:5000"]
}
  1. 重启 Docker 组件, 发现不起作用,在命令行下 pull 依然报错,可想配置文件错了
  2. 转眼看到一个可疑的配置文件 dockerd.json, 里面已经有一些配置了,于是就把配置写到这个里面
  3. 再重启,问题解决.可见群晖对于 docker 是做了一些改变的.

添加环境变量【Registry】

1
2
[Registry]
Root: HKCR; Subkey: "JOY-SECURITY"; ValueType: string; ValueData: "URL:JOY-SECURITY Protocol Handler"; Flags: uninsdeletekey

Root (必需的)

根键。必须是下列值中的一个:

HKCR (HKEY_CLASSES_ROOT)
HKCU (HKEY_CURRENT_USER)
HKLM (HKEY_LOCAL_MACHINE)
HKU (HKEY_USERS)
HKCC (HKEY_CURRENT_CONFIG)

Subkey (必需的)

子键名,可以包含常量。

ValueType

值的数据类型。必须是下面中的一个:

none
string
expandsz
multisz
dword
qword
binary

如果指定了 none (默认设置),安装程序将创建一个没有键值的键,在这种情况下,ValueData 参数将被忽略。
如果指定了 string,安装程序将创建一个字符串 (REG_SZ) 值。
如果指定了 expandsz,安装程序将创建一个扩展字符串 (REG_EXPAND_SZ) 值。
如果指定了 multisz,安装程序将创建一个多行文本 (REG_MULTI_SZ) 值。
如果指定了 dword,安装程序将创建一个32位整数 (REG_DWORD) 值。
如果指定了 qdword,安装程序将创建一个64位整数 (REG_QDWORD) 值。
如果指定了 binary,安装程序将创建一个二进制 (REG_BINARY) 值。

Flags

这个参数是额外选项设置。多个选项可以使用空格隔开。支持下面的选项:

createvalueifdoesntexist
当指定了这个标记,安装程序只在如果没有相同名字的值存在时创建值。如果值类型是 none,或如果你指定了 deletevalue 标记,这个标记无效。

deletekey
当指定了这个标记,安装程序在如果条目存在的情况下,先将尝试删除它,包括其中的所有值和子键。如果 ValueType 不是 none,那么它将创建一个新的键和值。

要防止意外,如果 Subkey 是空白的或只包含反斜框符号,安装时这个标记被忽略。

deletevalue
当指定了这个标记,安装程序在如果值存在的情况下,先将尝试删除值,如果 ValueType 是 none,那么在键不存在的情况下,它将创建键以及新值。

dontcreatekey
当指定了这个标记,如果键已经在用户系统中不存在,安装程序将不尝试创建键或值。如果键不存在,不显示错误消息。

一般来说,这个键与 uninsdeletekey 标记组合使用,在卸载时删除键,但安装时不创建键。

noerror
如果安装程序因任何原因创建键或值失败,不显示错误消息。

preservestringtype
这只在当 ValueType 参数是 string 或 expandsz 时适用。当指定这个标记,并且值不存在或现有的值不是 string 类型 (REG_SZ 或 REG_EXPAND_SZ),它将用 ValueType 指定的类型创建。如果值存在,并且是 string 类型,它将用先存在值的相同值类型替换。

uninsclearvalue
当卸载程序时,设置值数据为空字符 (类型 REG_SZ)。这个标记不能与 uninsdeletekey 标记组合使用。

uninsdeletekey
当卸载程序时,删除整个键,包含其中的所有值和子键。这对于 Windows 自身使用的键明显不是一个好方法。你只能用于你的应用程序特有的键中。

为防止意外,安装期间如果 Subkey 空白或只包含反斜框符号,这个标记被忽略。

uninsdeletekeyifempty
当程序卸载时,如果这个键的内部没有值或子键,则删除这个键。这个标记可以与 uninsdeletevalue 组合使用

为防止意外,安装期间如果 Subkey 空白或只包含反斜框符号,这个标记被忽略。

uninsdeletevalue
当程序卸载时删除该值。这个标记不能与 uninsdeletekeyifempty 组合使用

注意: 在早于 1.1 的 Inno Setup 版本中,你可以使用这个标记连同数据类型 none,那么它的功能与“如果空则删除键”标记一样。这个方法已经不支持了。你必须使用 uninsdeletekeyifempty 标记实现。

添加环境变量【Code】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//添加环境变量
procedure CurStepChanged(CurStep: TSetupStep);
var
oldpath: String;
newpath: String;
ErrorCode: Integer;
begin
if CurStep = ssPostInstall then
begin
RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', oldPath);
newPath := oldPath + ';%JAVA_HOME%\bin\;';
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', newPath);
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'JAVA_HOME', ExpandConstant('{app}\java\jdk1.8.0_45'));
end;
end;

添加环境变量后记得在 setup 中配置 ChangesEnvironment=yes 通知其他应用程序从注册表重新获取环境变量

删除环境变量【Code】

1
2
3
4
5
6
7
8
9
10
11
12
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
oldpath: String;
newpath: String;
begin
if CurUninstallStep = usDone then
RegDeleteValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'JAVA_HOME');
RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'Path', oldPath);
StringChangeEx(oldPath, ';%JAVA_HOME%\bin\;', '', True);
newPath := oldPath;
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 'PATH', newPath);
end;

安装完成后执行脚本

1
2
[Run]
Filename: "{app}\service-install.bat"; Description: "{cm:LaunchProgram,{#StringChange('SERVICE_INSTALL', '&', '&&')}}"; Flags: shellexec postinstall waituntilterminated runascurrentuser

Parameters

程序的可选命令行参数,可以包含常量。

Flags

这个参数是额外选项设置。多个选项可以使用空格隔开。支持下面的选项:

32bit
Causes the {sys} constant to map to the 32-bit System directory when used in the Filename and WorkingDir parameters. This is the default behavior in a 32-bit mode install。

这个标记不能与 shellexec 组合使用。

64bit
Causes the {sys} constant to map to the 64-bit System directory when used in the Filename and WorkingDir parameters. This is the default behavior in a 64-bit mode install。

This flag can only be used when Setup is running on 64-bit Windows, otherwise an error will occur. On an installation supporting both 32- and 64-bit architectures, it is possible to avoid the error by adding a Check: IsWin64 parameter, which will cause the entry to be silently skipped when running on 32-bit Windows。

这个标记不能与 shellexec 组合使用。

hidewizard
如果指定了这个标记,向导将在程序运行期间隐藏。

nowait
如果指定了这个标记,它将在处理下一个 [Run] 条目前或完成安装前不等待进程执行完成。不能与 waituntilidle 或 waituntilterminated 组合使用。

postinstall
仅在 [Run] 段有效。告诉安装程序在安装完成向导页创建一个选择框,用户可以选中或不选中这个选择框从而决定是否处理这个条目。以前这个标记调用 showcheckbox。

如果安装程序已经重新启动了用户的电脑 (安装了一个带 restartreplace 标记的文件或如果 [Setup] 段的 AlwaysRestart 指令是 yes 引起的),选择框没有机会出现,因此这些条目不会被处理。

[Files] 段条目中的 isreadme 标记现在已被废弃。如果编译器带 isreadme 标记的条目,它将从 [Files] 段条目中忽略这个标记,并在 [Run] 段条目列表的开头插入一个生成的 [Run] 条目。这相生成的 [Run] 段条目运行自述文件,并带有 shellexec,skipifdoesntexist,postinstall 和 skipifsilent 标记。

runascurrentuser
如果指定了这个标记,the spawned process will inherit Setup/Uninstall’s user credentials (typically, full administrative privileges)。

This is the default behavior when the postinstall flag is not used。

这个标记不能与 runasoriginaluser 组合使用。

runasoriginaluser
仅在 [Run] 段有效。If this flag is specified and the system is running Windows Vista or later, the spawned process will execute with the (normally non-elevated) credentials of the user that started Setup initially (i.e., the “pre-UAC dialog” credentials)。

This is the default behavior when the postinstall flag is used。

If a user launches Setup by right-clicking its EXE file and selecting “Run as administrator”, then this flag, unfortunately, will have no effect, because Setup has no opportunity to run any code with the original user credentials. The same is true if Setup is launched from an already-elevated process. Note, however, that this is not an Inno Setup-specific limitation; Windows Installer-based installers cannot return to the original user credentials either in such cases。

这个标记不能与 runascurrentuser 组合使用。

runhidden
如果指定了这个标记,它将在隐藏窗口中运行程序。请在执行一个要提示用户输入的程序中不要使用这个标记。

runmaximized
如果指定了这个标记,将在最大化窗口运行程序或文档。

runminimized
如果指定了这个标记,将在最小化窗口运行程序或文档。

shellexec
如果 Filename 不是一个直接可执行文件 (.exe 或 .com 文件),这个标记是必需的。当设置这个标记时,Filename 可以是一个文件夹或任何已注册的文件类型 – 包括 .hlp,.doc 等。该文件将用用户系统中与这个文件类型关联的应用程序打开,与在资源管理器双击文件的方法是相同的。

按默认,当使用 shellexec 标记时,将不等待,直到生成的进程终止。
如果你需要,你必须添加标记 waituntilterminated。注意,如果新进程未生成,它不能执行也将不等待 – 例如,文件指定指定为一个文件夹。

skipifdoesntexist
如果这个标记在 [Run] 段中指定,如果 Filename 不存在,安装程序不显示错误消息。

如果这个标记在 [UninstallRun] 段中指定,如果 Filename 不存在,卸载程序不显示“一些元素不能删除”的警告。

在使用这个标记时, Filename 必须是一个绝对路径。

skipifnotsilent
仅在 [Run] 段有效。告诉安装程序如果安装程序未在后台运行则跳过这个条目。

skipifsilent
仅在 [Run] 段有效。告诉安装程序如果安装程序在后台运行则跳过这个条目。

unchecked
仅在 [Run] 段有效。告诉安装程序初始为不选中选择框。如果用户希望处理这个条目,可以通过选取选择框执行。如果 postinstall 标记未同时指定,这个标记被忽略。

waituntilidle
如果指定了这个标记,它将在未输入期间等待,直到进程等待用户输入,而不是等待进程终止。(调用 WaitForInputIdle Win32 函数。) 不能与 nowait 或 waituntilterminated 组合使用。

waituntilterminated
如果指定这个标记,将等待到进程完全终止。注意这是一个默认动作 (也就是你不需要指定这个标记),除非你使用了 shellexec 标记,在这种情况下,如果你要等待,需要指定这个标记。不能与 nowait 或 waituntilidle 组合使用。

安装前卸载旧版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function InitializeSetup(): boolean;
var
bRes: Boolean;
ResultStr: String;
ResultCode: Integer;
begin
if RegQueryStringValue(HKLM, 'SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{4AA89D60-9EB2-4A69-B73E-67E3AC22CF8E}_is1', 'UninstallString', ResultStr) then
begin
MsgBox('检测到系统之前安装过本程序,即将卸载低版本!', mbInformation, MB_OK);
ResultStr := RemoveQuotes(ResultStr);
bRes := Exec(ResultStr, '/silent', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if bRes and (ResultCode = 0) then begin
result := true;
Exit;
end else
MsgBox('卸载低版本失败!', mbInformation, MB_OK);
result:= false;
Exit;
end;
result := true;
end;

检测服务是否存在并删除

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
function DeleteService(strExeName: String): Boolean;
var
ErrorCode: Integer;
bRes: Boolean;
strCmdFind: String;
strCmdDelete: String;
begin
strCmdFind := Format('/c sc query "%s"', [strExeName]);
strCmdDelete := Format('/c sc stop "%s" & sc delete "%s"', [strExeName, strExeName]);
bRes := ShellExec('open', ExpandConstant('{cmd}'), strCmdFind, '', SW_HIDE, ewWaitUntilTerminated, ErrorCode);
if bRes and (ErrorCode = 0) then begin
if MsgBox('检测到 ' + strExeName + ' 服务存在,需要删除,是否继续?', mbConfirmation, MB_YESNO) = IDYES then begin
bRes := ShellExec('open', ExpandConstant('{cmd}'), strCmdDelete, '', SW_HIDE, ewWaitUntilTerminated, ErrorCode);
if bRes and (ErrorCode = 0) then begin
MsgBox('服务 '+strExeName+' 删除成功!', mbInformation, MB_OK);
result := true;
Exit;
end else
MsgBox('删除失败,请手动删除服务 ' + strExeName, mbError, MB_OK);
result := false;
Exit;
end else
result := false;
Exit;
end;
MsgBox('服务 '+strExeName+' 不存在!', mbInformation, MB_OK);
result := true;
end;

执行

执行 git config credential.helper store

或者在 .gitconfig 添加

1
2
[credential]
helper = store

示例代码,10后抛出超时错误,并且取消子线程任务的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> {
....
}
);

try {
return future.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
future.cancel(true);
executorService.shutdown();
return new ArrayList<>();
}

背景

最近做了一个小 demo,需要使用到 spring security,于是就把以前写过的 spring security 的代码直接 copy 过来用了,没想到却出现了问题…..

问题

小 demo 直接使用 spring boot 构建,前后端不分离,于是自己写的登录界面,在 spring security 里配置好 loginPage 后,发现只要打开登录页就会无限重定向到登录页,其他任何请求都是如此

配置

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
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()//其他请求必须授权后访问
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/login")
.permitAll();//登录请求可以直接访问
}

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()).withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN");
}

@Bean
public SessionRegistry sessionRegistry(){
return new SessionRegistryImpl();
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}


@Bean
public AuthenticationSuccess authenticationSuccessHandler(){
return new AuthenticationSuccess();
}

@Bean
public AuthenticationFailureHandler authenticationFailureHandler(){
return new AuthenticationFailure();
}
}

分析

一开始的直觉告诉我,登录页的请求 “/“ 没有认证,而没有认证的请求会重定向到登录页,也就还是 “/“,于是就造成了重定向

于是我先添加请求 “/“, 不进行认证即可访问,也就是上面配置的 .antMatchers("/").permitAll()

重启后发现不起作用,依旧无限重定向

然而可怕的是控制台没有打印任何日志….

一下子懵逼了,不知如何解决….

冷静下来分析后—

我是这样解决的

打开 debug 日志

配置 spring security 的日志级别

1
2
3
logging:
level:
org.springframework.security: debug

启动时看到日志截取如下

1
2
3
4
5
6
7
8
9
2019-08-19 15:19:06.628 DEBUG 19133 --- [           main] edFilterInvocationSecurityMetadataSource : Adding web access control expression 'permitAll', for ExactUrl [processUrl='/?error']
2019-08-19 15:19:06.631 DEBUG 19133 --- [ main] edFilterInvocationSecurityMetadataSource : Adding web access control expression 'permitAll', for ExactUrl [processUrl='/login']
2019-08-19 15:19:06.631 DEBUG 19133 --- [ main] edFilterInvocationSecurityMetadataSource : Adding web access control expression 'permitAll', for ExactUrl [processUrl='/']
2019-08-19 15:19:06.631 DEBUG 19133 --- [ main] edFilterInvocationSecurityMetadataSource : Adding web access control expression 'permitAll', for Ant [pattern='/']
2019-08-19 15:19:06.632 DEBUG 19133 --- [ main] edFilterInvocationSecurityMetadataSource : Adding web access control expression 'authenticated', for any request
2019-08-19 15:19:06.646 DEBUG 19133 --- [ main] o.s.s.w.a.i.FilterSecurityInterceptor : Validated configuration attributes
2019-08-19 15:19:06.648 DEBUG 19133 --- [ main] o.s.s.w.a.i.FilterSecurityInterceptor : Validated configuration attributes

2019-08-19 15:34:24.451 INFO 22575 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@32c6d164, org.springframework.security.web.context.SecurityContextPersistenceFilter@390a7532, org.springframework.security.web.header.HeaderWriterFilter@5ebf776c, org.springframework.security.web.authentication.logout.LogoutFilter@523ade68, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@652f26da, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7d49a1a0, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@3a12f3e7, org.springframework.security.web.session.SessionManagementFilter@54ae1240, org.springframework.security.web.access.ExceptionTranslationFilter@3c62f69a, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2b3242a5]

可以看到 “/“ 添加成功了,可是为什么好像没有生效呢?

错误信息

继续往下走,刷新登录页,发现控制台打印了错误信息如下“

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
2019-08-19 15:21:16.813 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : / at position 1 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2019-08-19 15:21:16.815 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 2 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2019-08-19 15:21:16.816 DEBUG 19133 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2019-08-19 15:21:16.817 DEBUG 19133 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2019-08-19 15:21:16.822 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2019-08-19 15:21:16.825 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 4 of 10 in additional filter chain; firing Filter: 'LogoutFilter'
2019-08-19 15:21:16.825 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/logout', GET]
2019-08-19 15:21:16.825 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/'; against '/logout'
2019-08-19 15:21:16.826 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/logout', POST]
2019-08-19 15:21:16.826 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'GET /' doesn't match 'POST /logout'
2019-08-19 15:21:16.826 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/logout', PUT]
2019-08-19 15:21:16.826 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'GET /' doesn't match 'PUT /logout'
2019-08-19 15:21:16.826 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher : Trying to match using Ant [pattern='/logout', DELETE]
2019-08-19 15:21:16.827 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'GET /' doesn't match 'DELETE /logout'
2019-08-19 15:21:16.827 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher : No matches found
2019-08-19 15:21:16.827 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 5 of 10 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
2019-08-19 15:21:16.827 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher : Request 'GET /' doesn't match 'POST /login'
2019-08-19 15:21:16.828 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 6 of 10 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
2019-08-19 15:21:16.828 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.s.HttpSessionRequestCache : saved request doesn't match
2019-08-19 15:21:16.829 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 7 of 10 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
2019-08-19 15:21:16.832 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 8 of 10 in additional filter chain; firing Filter: 'SessionManagementFilter'
2019-08-19 15:21:16.833 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 9 of 10 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
2019-08-19 15:21:21.085 DEBUG 19133 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy : / at position 10 of 10 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
2019-08-19 15:21:21.440 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /; Attributes: [permitAll]
2019-08-19 15:21:21.450 DEBUG 19133 --- [nio-8080-exec-2] o.s.s.w.a.ExceptionTranslationFilter : Authentication exception occurred; redirecting to authentication entry point

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:379) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:223) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:124) ~[spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:119) ~[spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:170) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.StandardContextValve.__invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:41002) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587) [tomcat-embed-core-9.0.22.jar:9.0.22]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.22.jar:9.0.22]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135) [na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) [na:na]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.22.jar:9.0.22]
at java.base/java.lang.Thread.run(Thread.java:844) [na:na]

看到2个关键的错误信息:

  1. Authentication exception occurred; redirecting to authentication entry point
  2. An Authentication object was not found in the SecurityContext

意思是认证异常,重定向到认证入口点,异常的原因是在 SecurityContext 没有找到认证信息对象

排查

根据错误信息,我先到 ExceptionTranslationFilter 类中去查看问题出在什么地方

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
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

try {
chain.doFilter(request, response);

logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);

if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}

if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}

// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}

ExceptionTranslationFilter 没有什么逻辑,都是对异常的处理, 然后直接进入下一个过滤器了,

那么这时我们就需要了解 spring security 的过滤器链的顺序

我们来看最开始打印的 debug 的日志信息,我整理一下,有如下顺序:

WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor

这是spring security 的默认过滤器链,完整的过滤器链可以通过查看源码详细看到, 在类 FilterComparator 中:

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
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
order.next());
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(SwitchUserFilter.class, order.next());
}

回到原来的问题, ExceptionTranslationFilter 过滤器后是 FilterSecurityInterceptor 过滤器

再来看 FilterSecurityInterceptor 的源码

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
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}

InterceptorStatusToken token = super.beforeInvocation(fi);

try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}

super.afterInvocation(token, null);
}
}

这里的看的主要方法是 invoke(fi)

通过调试看到, 问题出在 beforeInvocation(fi) 方法里:

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);
}
}

继续调试,问题定位在了 if (SecurityContextHolder.getContext().getAuthentication() == null)

也就打印出了日志 An Authentication object was not found in the SecurityContext,这也就对的上号了

继续分析

为什么 SecurityContext 里的 Authentication 会为空呢?

据官方文档解释 spring security 默认是会有匿名的 Authentication 的啊

一想到这里,马上看下配置,原来是我禁用了匿名用户, .anonymous().disable() 怪不得这样。。。。

可是一想,为什么以前项目这么配置就没有出现这个问题呢???

对比发现,以前的项目是前后端分离的,不需要配置 loginPage, 而且登录成功和登录失败都是返回状态码和错误信息的,和我的这个小 demo 不一样,这个是前后端不分离的,需要做页面的跳转

为什么如此

这时搞清楚之后,我把 .anonymous().disable() 注释掉再重启,刷新下页面,果然登录页出来了,问题不再了

那么为什么会这样呢???

我仔细分析了一下, 看下注释掉配置会有说明不同

首先从过滤器链来看, 这里我不再贴日志信息了, 过滤器链整理如下:

WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor

对比发现,过滤器链里多了一个过滤器 AnonymousAuthenticationFilter,来看看 AnonymousAuthenticationFilter 做了什么事情

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
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));

if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}

chain.doFilter(req, res);
}

protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));

return auth;
}

看到了关键信息 createAuthenticationsetAuthentication, 那么在后续的过滤器链中就有了认证信息,不再报错了

这也就是为什么解决了这个问题的原因所在

重定向的原因

至于为什么会无限的重定向到登录页,还得再回过头来看 ExceptionTranslationFilter 类,这里有个处理异常的方法

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
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(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"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);
}
}
}

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);
}

通过调试, 发现进入了 sendStartAuthentication 方法,继续调试,进入 authenticationEntryPoint.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 通过调试发现就是 "/"

于是无限重定向的原因也清楚了。

总结

解决方式有 2 种:

  1. 上面所说的注释掉 .anonymous().disable()
  2. 配置 webSecurity.ignoring().antMatchers("/")
1
2
3
4
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/");
}

这种方法一般是配置系统静态资源用,配置的请求根本不会进入 spring security 的过滤器链,直接放行,
.antMatchers("/").permitAll() 是会进入 spring security 的过滤器链的,这是 2 者的主要区别
结合实际情况,第二种方式不是太好,建议第一种方式。

背景

我这款硕美科耳机是 2015 年入手的,到现在已经 4 年多了,日常使用中汗水已经腐蚀了耳机的皮套和头悬梁的皮套
但是耳机本身是没有任何问题的,只是用起来经常掉皮,我并不想重新再买一个
于是我决定在淘宝上买些配件
把原来腐蚀掉的皮套给换掉

材料

就下面 2 个皮套和一个头悬梁
材料

原来的模样

下面是我耳机没有更换前的模样,掉皮,平时我都是用纸巾包一层在戴到头上使用



动手

  1. 先硬撕掉耳机保护套,撕不掉用剪子剪掉

  2. 撕掉之后使用扁平的螺丝刀沿着边缘将卡口翘出来,像下面这样

  3. 之后将皮套套到卡口的圈中,注意皮套上的一圈洞和卡口上的突起相对应

  4. 将套好的一只耳机沿着之前的翘起的卡口位置在按到耳机架上,另一只耳机也是这样操作

  5. 接下来就是头悬梁的皮套的安装了,这个比较麻烦

  6. 先用小号螺丝刀打开悬梁 2 边的塑料小盒,露出 2 边的钢丝

  7. 用剪刀剪断钢丝,再翘起钢丝边缘的卡扣,把钢丝取出来,另一边也是这样操作

  8. 把新的悬梁换上,把钢丝穿过悬梁的小盒子,在穿进耳机架里面

  9. 打开送的小袋子里面的四个铜帽,用钳子夹紧到耳机架边缘伸出来的钢丝末端


  10. 另一边也是相同的方式安装好

更换完成

这是更换完成后的模样

0%