JoyLau's Blog

JoyLau 的技术学习与思考

说明

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 了

问题解决

引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

SpringSecurity 配置类

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
@EqualsAndHashCode(callSuper = true)
@Data
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@ConfigurationProperties(prefix = "spring.security.ignore")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private List<String[]> marchers;

@Bean
public UserService userService(){
return new UserService();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable()
.csrf().disable()
.authorizeRequests()
// .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() //解决PreFlight请求问题
.anyRequest().authenticated()//其他请求必须授权后访问
.and()
.formLogin()
// .loginPage("/login")
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.permitAll()//登录请求可以直接访问
.and()
.logout()
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(new LogoutSuccess())
.permitAll()//注销请求可直接访问
.and()
.sessionManagement()
.invalidSessionStrategy(new InvalidSessionStrategyImpl())
.maximumSessions(-1).expiredSessionStrategy(expiredSessionStrategy())//配置并发登录,-1表示不限制
.sessionRegistry(sessionRegistry())
.and()
.and()
.exceptionHandling()
.authenticationEntryPoint(new UnauthenticatedEntryPoint())
.accessDeniedHandler(new AuthorizationFailure())
.and()
.addFilterBefore(new AuthorizationFilter(new AuthorizationMetadataSource(), new
AuthorizationAccessDecisionManager()), FilterSecurityInterceptor.class);

}

@Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider());
}

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

@Bean
public ExpiredSessionStrategyImpl expiredSessionStrategy(){
return new ExpiredSessionStrategyImpl();
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return SecurityUtils.getPasswordEncoder();
}

@Override
public void configure(WebSecurity web) {
for (String[] marcher : marchers) {
web.ignoring().antMatchers(marcher);
}
}

@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
/*不将UserNotFoundExceptions转换为BadCredentialsException*/
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(userService());
provider.setPasswordEncoder(passwordEncoder());
return provider;
}

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

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

自定义 userService

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
public class UserService implements UserDetailsService {
@Autowired
private WebSecurityConfig securityConfig;

@Autowired
private SysUserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
throw new UsernameNotFoundException("用户名不能为空!");
}
SysUser user = new SysUser();
user.setLoginName(username);
SysUser queryUser = userMapper.selectOne(user);
if (null == queryUser) {
throw new UsernameNotFoundException("用户 " + username + " 不存在!");
}
if (!queryUser.getPermissionIpList().contains("0.0.0.0") && !queryUser.getPermissionIpList().contains
(SecurityUtils.getRemoteAddress())) {
throw new InvalidIpAddrException("登录 IP 地址不合法");
}

return new SecurityUser(queryUser);
}

/**
* 重新授权
*/
public void reAuthorization(){
SecurityUser user = SecurityUtils.currentUser();
assert user != null;
String username = user.getUsername();
user.setRoles(userMapper.findRolesByName(username));
user.setMenus(userMapper.findMenusByName(username));
user.setFunctions(userMapper.findFunctionsByName(username));

List<GrantedAuthority> authorities = new ArrayList<>();
for (Function function : user.getFunctions()) {
for (String url : function.getFunctionUrl().split(",")) {
authorities.add(new SimpleGrantedAuthority(url));
}
}
user.setAuthorities(authorities.stream().distinct().collect(Collectors.toList()));
// 得到当前的认证信息
Authentication auth = SecurityUtils.getAuthentication();
// 生成新的认证信息
Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), authorities);
// 重置认证信息
SecurityContextHolder.getContext().setAuthentication(newAuth);

}


/**
* 根据用户名 将该用户登录的所有账户踢下线
* @param userNames userNames
*/
public void kickOutUser(String... userNames) {
SessionRegistry sessionRegistry = securityConfig.sessionRegistry();
for (Object o : sessionRegistry.getAllPrincipals()) {
SecurityUser user = (SecurityUser) o;
for (String username : userNames) {
if (user.getLoginName().equals(username)) {
for (SessionInformation sessionInformation : sessionRegistry.getAllSessions(user, false)) {
sessionInformation.expireNow();
}
}
}
}
}
}

用户实体类 SecurityUser

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
@Data
public class SecurityUser extends SysUser implements UserDetails{

/*角色*/
private List<SysRole> roles;

/*菜单*/
private List<Menu> menus;

/*功能权限*/
private List<Function> functions;

private Collection<? extends GrantedAuthority> authorities;

SecurityUser(SysUser user) {
this.setUserId(user.getUserId());
this.setGlbm(user.getGlbm());
this.setXh(user.getXh());
this.setLoginName(user.getLoginName());
this.setLoginPassword(user.getLoginPassword());
this.setPermissionIpList(user.getPermissionIpList());
this.setLatestLoginTime(user.getLatestLoginTime());
this.setTotalLoginCounts(user.getTotalLoginCounts());
this.setName(user.getName());
this.setCreateTime(user.getCreateTime());
this.setUpdateTime(user.getUpdateTime());
}


@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

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

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

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

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

@Override
public String getPassword() {
return super.getLoginPassword();
}

@Override
public String getUsername() {
return super.getLoginName();
}

@Override
public int hashCode() {
return this.getLoginName().hashCode();
}

@Override
public boolean equals(Object obj) {
return obj instanceof SecurityUser && ((SecurityUser) obj).getLoginName().equals(this.getLoginName());
}
}

授权Filter

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 class AuthorizationFilter extends AbstractSecurityInterceptor implements Filter {

private AuthorizationMetadataSource metadataSource;

public AuthorizationFilter(AuthorizationMetadataSource metadataSource, AuthorizationAccessDecisionManager
accessDecisionManager) {
this.metadataSource = metadataSource;
this.setAccessDecisionManager(accessDecisionManager);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}

private void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}


@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return metadataSource;
}

@Override
public void destroy() {
}

@Override
public void init(FilterConfig filterConfig) {
}
}

授权访问决策器

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
public class AuthorizationAccessDecisionManager implements AccessDecisionManager {

/**
* 认证用户是否具有权限访问该url地址
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
String url = ((FilterInvocation) object).getRequestUrl();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
SimpleGrantedAuthority authority = (SimpleGrantedAuthority) grantedAuthority;
if (matches(authority.getAuthority(), request)) {
return;
}
}
throw new AccessDeniedException("uri: " + url + ",无权限访问!");
}

/**
* 当前AccessDecisionManager是否支持对应的ConfigAttribute
*/
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

/**
* 当前AccessDecisionManager是否支持对应的受保护对象类型
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}

private boolean matches(String url, HttpServletRequest request) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
return matcher.matches(request);
}
}

授权元数据

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
public class AuthorizationMetadataSource implements FilterInvocationSecurityMetadataSource {

/**
* 加载 请求的url资源所需的权限
* @param object object
* @return Collection
* @throws IllegalArgumentException Exception
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String url = ((FilterInvocation) object).getRequestUrl();
Collection<ConfigAttribute> configAttributes = new ArrayList<>();
configAttributes.add(new SecurityConfig(url));
return configAttributes;
}

/**
* 会在启动时加载所有 ConfigAttribute 集合
* @return Collection
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

封装一些 Security 工具类

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
public class SecurityUtils {

public static Authentication getAuthentication(){
return SecurityContextHolder.getContext().getAuthentication();
}

/**
* 用户是否登录
* @return boolean
*/
public static boolean isAuthenticated(){
return getAuthentication() != null || !(getAuthentication() instanceof AnonymousAuthenticationToken);
}

/**
* 获取当前用户
* @return user
*/
public static SecurityUser currentUser(){
if (isAuthenticated()) {
return (SecurityUser) getAuthentication().getPrincipal();
}
return null;
}

/**
* 获取 webAuthenticationDetails
*/
private static WebAuthenticationDetails webAuthenticationDetails(){
return (WebAuthenticationDetails)getAuthentication().getDetails();
}

/**
* 获取session id
*/
public static String getSessionId(){
return webAuthenticationDetails().getSessionId();
}

/**
* 获取远程访问地址
*/
public static String getRemoteAddress(){
return webAuthenticationDetails().getRemoteAddress();
}

/**
* 获取密码编译器
* @return BCryptPasswordEncoder
*/
public static BCryptPasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder(4);
}

/**
* 根据明文加密 返回密文
* @param rawPassword 明文
* @return 密文
*/
public static String createPassword(String rawPassword){
return getPasswordEncoder().encode(rawPassword.trim());
}

/**
* 传入明文和密文 检查是否匹配
* @param rawPassword 明文
* @param encodedPassword 密文
* @return boolean
*/
public static boolean isMatching(String rawPassword,String encodedPassword){
return getPasswordEncoder().matches(rawPassword,encodedPassword);
}

}

主要的实现类都列举在内了,还有一些成功和失败的处理类,再次没有列举出来
因为该项目为构建纯restful风格的后台项目,这些成功或失败的处理类基本都是返回的http状态码

环境准备

  • 项目整合 通用 mapper 和 pagehelper 插件,这部分以前有写过,略
  • 需要集成 mybatis 的 generator 插件,方便自动生成 实体类和 mapper 类,还可以生成xml,不过一般我们都不用 xml
  • baseMapper 需要继承 ExampleMapper 不过只需要继承 Mapper 就可以了,因为 Mapper 已经继承了 ExampleMapper

Example 的用法

首先需要说明一点 ,和 Example 使用相同的还有 Condition 类 该类继承自 Example,使用方法和 Example 完全一样,只是为了避免语义有歧义重命名的一个类,这里我们都用 Example 来说明

  • 创建 Example :
1
Example example = new Example(XXX.class);

其中构造方法为生成的 model 实体类,还有 2 个构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* 带exists参数的构造方法,默认notNull为false,允许为空
*
* @param entityClass
* @param exists - true时,如果字段不存在就抛出异常,false时,如果不存在就不使用该字段的条件
*/
public Example(Class<?> entityClass, boolean exists) {
...
}

/**
* 带exists参数的构造方法
*
* @param entityClass
* @param exists - true时,如果字段不存在就抛出异常,false时,如果不存在就不使用该字段的条件
* @param notNull - true时,如果值为空,就会抛出异常,false时,如果为空就不使用该字段的条件
*/
public Example(Class<?> entityClass, boolean exists, boolean notNull) {
...
}

然后可以对 example 的实体类的单表进行查询了

1
2
3
4
Example example = new Example(XXX.class);
example.createCriteria().andGreaterThan("id", 100).andLessThan("id",151);
example.or().andLessThan("id", 41);
List<XXX> list = mapper.selectByExample(example);

以上查询的条件是,查询 id 大于 100 并且小于 151 或者 id 小于 41 的记录

还可以写成 sql 的方式:

1
2
3
4
5
6
Example example = new Example(XXX.class);
example.createCriteria().andCondition("id > 100 and id <151 or id < 41");

// andCondition() 方法可以叠加使用,像这样
example.createCriteria().andCondition("id > 100 and id <151").orCondition("id <41");

andCondition() 有2中使用方法:
andCondition(String condition) : 手写条件,例如 “length(name)<5”
andCondition(String condition, Object value) : 手写左边条件,右边用value值,例如 “length(name)=” “5”
orCondition() 也是类似的

example 里有很多 mysql 常用的方法,使用方法和 elasticsearch 的 java api 很类似,这里列举几个

  • Set<String> selectColumns : 查询的字段
  • Set<String> excludeColumns : 排除的查询字段
  • Map<String, EntityColumn> propertyMap : 属性和列对应
  • andAllEqualTo : 将此对象的所有字段参数作为相等查询条件,如果字段为 null,则为 is null
  • andGreaterThan : and 条件 大于
  • andBetween : and 条件 between
  • andEqualTo : 将此对象的不为空的字段参数作为相等查询条件 还有一种有 value 参数的是 = 条件
  • andGreaterThanOrEqualTo : and 条件 》=

还有一些一看就知道意思的

  • andIn
  • andIsNotNull
  • andIsNull
  • andLessThan
  • andLessThanOrEqualTo
  • andNotLike

上面是以 and 条件举例 ,or的条件也是一样的

集成分页功能

我们知道 PageHelper.startPage(pageNum, pageSize); 可以对 后面的一个 select 进行分页
那么我们可以对 example 进行一个分页查询的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// 在baseMapper 里封装一个接口
PageInfo selectPageByExample(int pageNum, int pageSize, Object example);

//这样实现上面的接口
@Override
public PageInfo selectPageByExample(int pageNum, int pageSize, Object example) {
PageHelper.startPage(pageNum, pageSize);
List<T> list = selectByExample(example);
return new PageInfo<>(list);
}

//java 8 的lamda 用法
@Override
public PageInfo selectPageByExample(int pageNum, int pageSize, Object example) {
return PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(()->baseMapper.selectByExample(example));
}

前言

SpringBoot 默认有2种打包方式,一种是直接打成 jar 包,直接使用 java -jar 跑起来,另一种是打成 war 包,移除掉 web starter 里的容器依赖,然后丢到外部容器跑起来。

第一种方式的缺点是整个项目作为一个 jar,部署到生产环境中一旦有配置文件需要修改,则过程比较麻烦
linux 下可以使用 vim jar 包,找到配置文件修改后再保存
window 下需要使用 解压缩软件打开 jar 再找到配置文件,修改后替换更新

第二种方式的缺点是需要依赖外部容器,这无非多引入了一部分,很多时候我们很不情愿这么做

spring boot 项目启动时 指定配置有2种方式:一种是启动时修改配置参数,像 java -jar xxxx.jar –server.port=8081 这样;另外一种是 指定外部配置文件加载,像 java -jar xxxx.jar -Dspring.config.location=applixxx.yml这样

目标

我们希望打包成 tomcat 或者 maven 那样的软件包结构,即

--- bin
    --- start.sh
    --- stop.sh
    --- restart.sh
    --- start.bat
    --- stop.bat
    --- restart.bat
--- boot
    --- xxxx.jar
--- lib
--- conf
--- logs
--- README.md
--- LICENSE

就像这样
Assembly-Package

  • bin 目录放一些我们程序的启动停止脚本
  • boot 目录放我们自己的程序包
  • lib 目录是我们程序的依赖包
  • conf 目录是项目的配置文件
  • logs 目录是程序运行时的日志文件
  • README.md 使用说明
  • LICENSE 许可说明

准备

  • maven-jar-plugin : 打包我们写的程序包和所需的依赖包,并指定入口类,依赖包路径和classpath路径,其实就是在MANIFEST.MF这个文件写入相应的配置
  • maven-assembly-plugin : 自定义我们打包的文件目录的格式

pom.xml 配置

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
<build>
<plugins>
<!--<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<mainClass>com.ahtsoft.AhtsoftBigdataWebApplication</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>../lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Class-Path>../conf/resources/</Class-Path>
</manifestEntries>
</archive>
<excludes>
<exclude>static/**</exclude>
<exclude>*.yml</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
  1. 将 spring boot 默认的打包方式 spring-boot-maven-plugin 去掉,使用现在的打包方式
  2. maven-jar-plugin 配置中,制定了入口类,addClasspath 配置将所需的依赖包单独打包,依赖包打的位置在lib目录底下,在MANIFEST.MF这个文件写入相应的配置
  3. 配置了 classpath 在 /conf/resources/ ,这个和后面的 assembly.xml 要相对应
  4. 我单独把spring boot 的配置文件 yml文件 和 静态资源目录 static 单独拎了出来,在我们的源码包中并没有打进去,而是交给 assembly.xml 来单独打到一个独立的文件 conf文件下
  5. 这也是照应了 前面为什么要设置 classpath 为 /conf/resources/

下面重要的是 assembly.xml 配置文件了,这个文件才是把我们的程序打成标准的目录结构

assembly.xml

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
<assembly>
<id>assembly</id>
<formats>
<format>tar.gz</format>
</formats>
<baseDirectory>${project.artifactId}-${project.version}/</baseDirectory>

<files>
<file>
<source>target/${project.artifactId}-${project.version}.jar</source>
<outputDirectory>boot/</outputDirectory>
<destName>${project.artifactId}-${project.version}.jar</destName>
</file>
</files>

<fileSets>
<fileSet>
<directory>./</directory>
<outputDirectory>./</outputDirectory>
<includes>
<include>*.txt</include>
<include>*.md</include>
</includes>
</fileSet>
<fileSet>
<directory>src/main/bin</directory>
<outputDirectory>bin/</outputDirectory>
<includes>
<include>*.sh</include>
<include>*.cmd</include>
</includes>
<fileMode>0755</fileMode>
</fileSet>
<fileSet>
<directory>src/main/resources/static</directory>
<outputDirectory>conf/resources/static/</outputDirectory>
<includes>
<include>*</include>
</includes>
</fileSet>
<fileSet>
<directory>src/main/resources</directory>
<outputDirectory>conf/resources</outputDirectory>
<includes>
<include>*.properties</include>
<include>*.conf</include>
<include>*.yml</include>
</includes>
</fileSet>
</fileSets>

<dependencySets>
<dependencySet>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<scope>runtime</scope>
<includes>
<include>*:*</include>
</includes>
<excludes>
<exclude>${groupId}:${artifactId}</exclude>
<exclude>org.springframework.boot:spring-boot-devtools</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>
  • 将最终的程序包打成 tar.gz ,当然也可以打成其他的格式如zip,rar等,fileSets 里面指定我们源码里的文件和路径打成标准包相对应的目录
  • 需要注意的是,在最终的依赖库 lib 下 去掉我们的程序和开发时spring boot的热部署依赖 spring-boot-devtools,否则的会出问题
  • 代码里的启动和停止脚本要赋予权限,否则在执行的时候可能提示权限的问题
0%