HTTP监测Spring Boot运行情况
上一篇文章中对Spring Boot的部署和启动进行了简单介绍:Spring Boot项目的部署运行。在Spring Boot项目运行后,Spring提供了一系列的端点来监测项目的运行状态,下面对Spring默认使用的actuator监测过程进行说明。
开启项目中的HTTP监测
1. 引入HTTP监测依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2. HTTP监测的默认端点和配置情况
默认请求路径:http://localhost:8080/actuator
由于监测的端点信息中有可能包含敏感信息,因此默认情况下,Spring Boot仅开启info和health两个端点,其余的并不暴露。health的端点情况如下:
3. 配置Spring Boot可以暴露的端点
为了让Spring Boot暴露出多个端点,可以在application.properties配置文件中进行配置:
# spring boot http监测配置
# 配置默认暴露的端点,*表示所有端点,也可以设置端点名称(info,health,beans等)
management.endpoints.web.exposure.include=*
# 配置不暴露的端点(env)
management.endpoints.web.exposure.exclude=env
重新启动后,在查看actuator页面则会出现除env以外的所有节点链接,并且beans原默认的信息也可以进行查看。
Spring Actuator提供的各个监测端点如下:
查看敏感信息
虽然使用上述的方法可以将需要的端点信息暴露出来,但对于应用来说是很危险的,所以需要使用Spring Security来配置用户名和角色,来解决这些敏感信息的访问权限问题。
1. 引入spring boot security依赖
<!-- 配置Spring Security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在Spring Boot中加入上述依赖后,就会直接开启Spring Security的认证机制,但使用默认用户(user)的系统生成密码不方便且无法满足这里的需要。所以下面对系统中可以使用的用户名和角色进行设置。
2. 配置用户名和角色
在Spring Boot中继承WebSecurityConfigurerAdapter类,即可以自定义Spring Security中的用户名和角色,并设置不同角色可以访问的URL。在继承WebSecurityConfigurerAdapter类中,重写如下两个configure方法。
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码编码器
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 使用内存存储
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("admin")
.password(passwordEncoder.encode("admin"))
.roles("USER", "ADMIN")
.and()
.withUser("yitian")
.password(passwordEncoder.encode("yitian"))
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/actuator/**").hasRole("ADMIN")
.antMatchers("/web/index").hasRole("USER")
.anyRequest().permitAll()
.and()
.formLogin()
.and()
.logout()
.logoutUrl("/signOut")
.logoutSuccessUrl("/logoutResult"); // 这里的响应controller位于WebSecurityConfiguration
}
}
第一个configure方法中,使用内存存储的方式设置了两个用户,admin用户具有USER和ADMIN两个权限,yitian用户只有USER权限,并分别为两个用户设置了相应的登录密码。(这里为了方便使用了内存存储的方式,也可以使用数据库存储的方式来结合数据表进行用户和权限的创建和分配)
第二个configure方法中,是为不同的ROLE来划分可以访问的URL。authorizeRequests方法表示设置哪些需要签名验证的请求,并且可以将不用的请求权限赋予不同的角色。antMatchers为配置请求的路径,hasRole为访问相应路径需要的ROLE。这里既是将/actuator/**下的所有监测路径权限设置为ADMIN,也就是只用admin用户可以访问该路径下的监控端点,yitian用户无法访问。进而,将/web/index注解设置为URER角色可以访问。然后anyRequest表示除了antMatchers方法设置的URL之外的其他所有URL,permitALL表示这些路径允许没有签名的情况下也可以访问。
此外,and为连接语句,formLogin表示这里使用Spring Security中默认的Login页面,logout表示用户登出,并设置登出的请求路径为/signOut,这样Spring会对这个请求进行拦截,来进行登出的处理,并清除Session等操作。logoutSuccessUrl为设置登出成功后请求页面的URL。这里的/logoutResult的URL Mapping被设置在了WebMvcConfiguration类中,代码如下,当然也可以使用一个controller来实现该映射。
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
// ...
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 为登出设置相应页面
registry.addViewController("logoutResult").setViewName("logoutResult");
}
}
相应的logoutResult.jsp页面为:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Logout Success</title>
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/icon.css">
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.min.js"></script>
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.easyui.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
</script>
</head>
<body>
<h2>您已经登出了系统</h2>
<div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>
上面设置了logout之后成功的跳转页面,但spring security对设置的登出请求的拦截需要其实POST请求,也就是无法直接在浏览器中输入URL来进行登出。所以需要设置一个登出页面,使用POST来请求/signOut,登出页面logout.jsp如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>User logout</title>
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/icon.css">
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.min.js"></script>
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.easyui.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
</script>
</head>
<body>
<form action="/signOut" method="POST">
<P><input type="submit" value="退出登录"/></P>
<input type="hidden" id="${_csrf.parameterName}" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
<div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>
注意,表单中有一个隐藏的input,其中携带了csrf的token,这是为了防止csrf攻击进行的验证。后面内容也有对csrf的相关设置。
同样还需要映射到该logout.jsp页面的controller,简单如下:
@Controller
@RequestMapping("/app")
public class ApplicationController {
@GetMapping("/logout")
public String logout() {
return "logout";
}
}
这样就实现了为/actuator监测端点设置admin用户才可以访问的权限,防止其中的敏感信息可以被随意访问。并可以完成用户的登录和登出功能。
3. 访问测试
重启Spring Boot项目后,尝试打开/web/index主页,会被login页面拦截需要输入用户名和密码来登录:
这里使用yitian和admin用户均可以登录,如果使用yitian用户则在后续请求/actuator页面是会被阻止,使用admin用户则可以成功打开。下面分yitian用户进行登录后,请求/actuator的结果如下:
在尝试使用admin用户重新登录之前,需要对yitian用户进行登出,浏览器访问登出页面:/app/logout,然后点击页面中的登出按钮进行登出:
登出成功后会得到下面的logoutResult页面:
这样在重新点击返回首页(/web/index),就可以重新进行登录,下面使用admin用户进行登录:
可以看到此时/actuator请求可以正常访问。
使用shutdown端点关闭服务
shutdown端点是一个特殊的端点,可以对服务进行关闭。所以该端点默认是不提供的,需要在application.properties配置文件中进行如下配置来开启该节点:
# 启动shutdown端点
management.endpoint.shutdown.enabled=true
配置好后,重启Spring Boot项目既可以使用该端点了。但该端点是一个POST请求,所以无法使用浏览器来访问它,可以在JSP中使用javascript来进行访问。定义shutdown.jsp页面如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Application Shutdown</title>
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/icon.css">
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.min.js"></script>
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.easyui.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(header, token);
});
$("#submit").click(function () {
<%--alert("${_csrf.token}");--%>
$.post({
url: "/actuator/shutdown",
success: function (result) {
if (result != null || result.message != null) {
alert(result.message);
return;
}
alert("关闭Spring Boot应用失败!");
}
})
})
})
</script>
</head>
<body>
<input id="submit" name="submit" type="button" value="关闭应用" />
<div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>
映射到该shutdown.jsp页面的对应Controller如下:
@Controller
@RequestMapping("/app")
public class ApplicationController {
@GetMapping("/shutdown")
public String shutdown() {
return "shutdown";
}
}
所以在项目启动后,通过/app/shutdown请求打开shutdown.jsp页面,其中包含一个按钮,来调用/actuator/shutdown POST请求关闭项目服务。
需要注意的是:由于目前项目开启了spring security,所以会存在CSRF权限问题,根据Spring文档的内容,在进行请求是加入如下内容既可以解决该问题:
<meta name="_csrf" content="${_csrf.token}"/> <meta name="_csrf_header" content="${_csrf.headerName}"/> <script type="text/javascript"> $(document).ready(function () { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function (e, xhr, options) { xhr.setRequestHeader(header, token); }); $("#submit").click(function () { // your request }) }) </script>
或者
configure(HttpSecurity http)
方法中追加http.csrf().disable();来关闭CSRF
自定义监测端点实现数据库连接监测
Spring除了上述的默认端点之外,也允许自定义端点用于服务状态的监控。下面创建数据库连接的监测端点作为示例,代码如下:
@Component
@Endpoint(id = "dbcheck", enableByDefault = true)
public class DataBaseConnectionEndpoint {
private static final String DRIVER = "com.mysql.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
/**
* 一个端点只能存在一个ReadOperation标注的方法,表示一个HTTP GET请求
*/
@ReadOperation
public Map<String, Object> check() {
Connection connection = null;
Map<String, Object> resultMap = new HashMap<>();
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(url, username, password);
resultMap.put("success", true);
resultMap.put("message", "数据库连接成功!");
} catch (ClassNotFoundException | SQLException e) {
resultMap.put("success", false);
resultMap.put("message", e.getMessage());
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return resultMap;
}
}
上述代码中有如下点需要关注:
@Endpoint注解用于定义自定义的端点,该注解会同时开启JMX和WEB监控(之前的监控内容均为WEB监控),如果只提供JMX监控使用@JmxEndpoint注解,只提供Web监控使用@WebEndpoint注解。该注解中的id为该端点的标识名称,并使用enableByDefault设置为默认开启的状态。
@ReadOperation注解指明该方法是一个读操作(对应于HTTP的GET请求),该注解只能在端点中出现一次,否则Spring会出现异常。相应的注解还有,@WriteOperation和@DeleteOperation,前者代表POST请求后者对应DELETE请求。
编写完成上述的自定义端点后,需要在配置文件中对其进行配置:
# 开启自定义数据库检查端点
management.endpoint.dbcheck.enabled=true
这样在重新运行项目之后,就可以在 http://localhost:8080/actuator返回的页面中看到该请求,如下:
点击后得到的结果如下:
自定义服务器万维网Health指标
1. Actuator的默认health指标
Actuator默认提供了如下的Health指标,但这些健康指标默认不会进行展示,需要开发者进行配置进行开启。
要展示这些健康指标,需要在application.properties文件中进行如下配置:
# 签名认证后展示,never:不展示,always:每次都展示
management.endpoint.health.show-details=when_authorized
然后重新使用admin登录后,可以得到详细的health信息:
若想关闭其中的一个health监测,在application.properties配置文件中添加如下配置即可:
# 关闭db健康指标
management.health.db.enabled=false
2. 自定义互联网访问健康指标
继承AbstarctHealthIndicator类并将其注入到Ioc容器中,即可以实现自定义的health监控:
@Component
public class WwwHealthIndicator extends AbstractHealthIndicator {
private final static String BAIDU_HOST = "www.baidu.com";
private final static int TIME_OUT = 3000;
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Boolean status = ping();
if (status) {
builder.withDetail("message", "当前服务器可以访问互联网").up();
} else {
builder.withDetail("message", "当前服务器无法访问互联网").outOfService();
}
}
private Boolean ping() throws Exception {
try {
// 本地运行的话,应该是无法访问互联网
return InetAddress.getByName(BAIDU_HOST).isReachable(TIME_OUT);
} catch (Exception e) {
return false;
}
}
}
重新启动Spring Boot项目,查看health端点既可以看到如下的自定义监测指标WWW:
这里用于是本地IDEA运行,所以是无法访问互联网的,因此状态为OUT_OF_SERVICE.