- JavaMelody的身份认证
鉴于项目原因,接入了JavaMelody以及Jwt,由于Request header冲突的原因造成Jwt认证失败,大致出错代码如下:
String jwt = request.getHeader("Authorization");
if (!StringUtils.isEmpty(jwt)) {
return jwt;
}
然后查看了一下JavaMelody源码:
- 通过spring自动注册了一个拦截器:MonitoringFilter
@Bean(name = REGISTRATION_BEAN_NAME)
@ConditionalOnMissingBean(name = REGISTRATION_BEAN_NAME)
public FilterRegistrationBean monitoringFilter(JavaMelodyConfigurationProperties properties,
ServletContext servletContext) {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
// Create the monitoring filter and set its configuration parameters.
final MonitoringFilter filter = new MonitoringFilter();
filter.setApplicationType("Spring Boot");
// Wrap the monitoring filter in the registration bean.
registrationBean.setFilter(filter);
registrationBean.setAsyncSupported(true);
registrationBean.setName("javamelody");
registrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
// Set the initialization parameter for the monitoring filter.
for (final Entry<String, String> parameter : properties.getInitParameters().entrySet()) {
registrationBean.addInitParameter(parameter.getKey(), parameter.getValue());
}
// Set the URL patterns to activate the monitoring filter for.
registrationBean.addUrlPatterns("/*");
final FilterRegistration filterRegistration = servletContext
.getFilterRegistration("javamelody");
if (filterRegistration != null) {
// if webapp deployed as war in a container with MonitoringFilter already added by web-fragment.xml,
// do not try to add it again
registrationBean.setEnabled(false);
for (final Map.Entry<String, String> entry : registrationBean.getInitParameters()
.entrySet()) {
filterRegistration.setInitParameter(entry.getKey(), entry.getValue());
}
}
return registrationBean;
}
- 解析authorized-users以及allowed-addr-pattern标签:HttpAuth
public HttpAuth() {
super();
this.allowedAddrPattern = getAllowedAddrPattern();
this.authorizedUsers = getAuthorizedUsers();
}
private static Pattern getAllowedAddrPattern() {
if (Parameter.ALLOWED_ADDR_PATTERN.getValue() != null) {
return Pattern.compile(Parameter.ALLOWED_ADDR_PATTERN.getValue());
}
return null;
}
private static List<String> getAuthorizedUsers() {
// security based on user / password (BASIC auth)
final String authUsersInParam = Parameter.AUTHORIZED_USERS.getValue();
if (authUsersInParam != null && !authUsersInParam.trim().isEmpty()) {
final List<String> authorizedUsers = new ArrayList<String>();
// we split on new line or on comma
for (final String authUser : authUsersInParam.split("[\n,]")) {
final String authUserTrim = authUser.trim();
if (!authUserTrim.isEmpty()) {
authorizedUsers.add(authUserTrim);
LOG.debug("Authorized user: " + authUserTrim.split(":", 2)[0]);
}
}
return authorizedUsers;
}
return null;
}
authorized-users标签是设置登录账号以及密码的
allowed-addr-pattern标签是设置ip白名单的
拦截逻辑
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)
|| monitoringDisabled || !instanceEnabled) {
// si ce n'est pas une requête http ou si le monitoring est désactivé, on fait suivre
chain.doFilter(request, response);
return;
}
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;
if (httpRequest.getRequestURI().equals(getMonitoringUrl(httpRequest))) {
doMonitoring(httpRequest, httpResponse);
return;
}
if (!httpCounter.isDisplayed() || isRequestExcluded((HttpServletRequest) request)) {
// si cette url est exclue ou si le counter http est désactivé, on ne monitore pas cette requête http
chain.doFilter(request, response);
return;
}
doFilter(chain, httpRequest, httpResponse);
}
protected String getMonitoringUrl(HttpServletRequest httpRequest) {
if (monitoringUrl == null) {
monitoringUrl = httpRequest.getContextPath() + Parameters.getMonitoringPath();
}
return monitoringUrl;
}
//解析monitoring-path标签,自定义监控访问地址
public static String getMonitoringPath() {
final String parameterValue = Parameter.MONITORING_PATH.getValue();
if (parameterValue == null) {
return DEFAULT_MONITORING_PATH;//默认/monitoring
}
return parameterValue;
}
首先它会判断是否禁掉了监控,然后再判断当前拦截到的Request是否访问监控的,如果以上条件不满足的话将进行正常的访问后台服务并进行监控信息的统计
- 监控访问的权限判断
private void doMonitoring(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
throws IOException, ServletException {
if (isRumMonitoring(httpRequest, httpResponse)) {
return;
}
if (!isAllowed(httpRequest, httpResponse)) {
return;
}
final Collector collector = filterContext.getCollector();
final MonitoringController monitoringController = new MonitoringController(collector, null);
monitoringController.doActionIfNeededAndReport(httpRequest, httpResponse,
filterConfig.getServletContext());
if ("stop".equalsIgnoreCase(HttpParameter.COLLECTOR.getParameterFrom(httpRequest))) {
// on a été appelé par un serveur de collecte qui fera l'aggrégation dans le temps,
// le stockage et les courbes, donc on arrête le timer s'il est démarré
// et on vide les stats pour que le serveur de collecte ne récupère que les deltas
for (final Counter counter : collector.getCounters()) {
counter.clear();
}
if (!collector.isStopped()) {
LOG.debug(
"Stopping the javamelody collector in this webapp, because a collector server from "
+ httpRequest.getRemoteAddr()
+ " wants to collect the data itself");
filterContext.stopCollector();
}
}
}
protected boolean isAllowed(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
throws IOException {
return httpAuth.isAllowed(httpRequest, httpResponse);
}
public boolean isAllowed(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
throws IOException {
if (!isRequestAllowed(httpRequest)) {
LOG.debug("Forbidden access to monitoring from " + httpRequest.getRemoteAddr());
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden access");
return false;
}
if (!isUserAuthorized(httpRequest)) {
// Not allowed, so report he's unauthorized
httpResponse.setHeader("WWW-Authenticate", "BASIC realm=\"JavaMelody\"");
if (isLocked()) {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Unauthorized (locked)");
} else {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
return false;
}
return true;
}
private boolean isRequestAllowed(HttpServletRequest httpRequest) {
return allowedAddrPattern == null
|| allowedAddrPattern.matcher(httpRequest.getRemoteAddr()).matches();
}
private boolean isUserAuthorized(HttpServletRequest httpRequest) {
if (authorizedUsers == null) {
return true;
}
// Get Authorization header
final String auth = httpRequest.getHeader("Authorization");
if (auth == null) {
return false; // no auth
}
if (!auth.toUpperCase(Locale.ENGLISH).startsWith("BASIC ")) {
return false; // we only do BASIC
}
// Get encoded "user:password", comes after "BASIC "
final String userpassEncoded = auth.substring("BASIC ".length());
// Decode it
final String userpassDecoded = Base64Coder.decodeString(userpassEncoded);
final boolean authOk = authorizedUsers.contains(userpassDecoded);
return checkLockAgainstBruteForceAttack(authOk);
}
步骤如下:
1.判断是否在ip白名单中
2.从Request中获取key为Authorization的header项
3.判断开头是否是"BASIC ",然后通过Base64解码出账号密码,并判断权限用户列表中是否包含这个账号密码
4.如果没通过账户验证,则设置Response的header,WWW-Authenticate:BASIC realm=“JavaMelody”,并设置状态码为401(未认证)
####HTTP Basci身份认证机制
步骤1:用户访问受限资源
如下,用户访问受限资源 /protected_docs。请求报文如下:
GET /monitoring HTTP/1.1
Host: 127.0.0.1:3000
步骤2:服务端返回401要求身份认证
服务端发现 /protected_docs 为受限资源,于是向用户发送401状态码,要求进行身份认证。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm=JavaMelody
响应首部中,通过WWW-Authenticate告知客户端,认证的方案是basic。同时以realm告知认证的范围。
WWW-Authenticate: Basic realm=<需要保护资源的范围>
步骤3:用户发送认证请求
用户收到服务端响应后,填写用户名、密码,然后向服务端发送认证请求。
以下为请求报文。Authorization
请求首部中,包含了用户填写的用户名、密码。
GET /monitoring HTTP/1.1
Authorization: Basic bG9uZ3FpYW5nOjEyMzQ1Ng==
Authorization
首部的格式为Basic base64(userid:password)
。实际代码如下:
Buffer.from('longqiang:123456').toString('base64'); // bG9uZ3FpYW5nOjEyMzQ1Ng==
步骤4:服务端验证请求
服务端收到用户的认证请求后,对请求进行验证。验证包含如下步骤:
1.根据用户请求资源的地址,确定资源对应的realm。
2.解析 Authorization 请求首部,获得用户名、密码。
3.判断用户是否有访问该realm的权限。
4.验证用户名、密码是否匹配。
一旦上述验证通过,则返回请求资源。如果验证失败,则返回401要求重新认证,或者返回403(Forbidden)。
安全缺陷
Basic认证的安全缺陷比较明显,它通过明文传输用户的密码,这会导致严重的安全问题。
- 在传输层未加密的情况下,用户明文密码可被中间人截获。
- 明文密码一旦泄露,如果用户其他站点也用了同样的明文密码(大概率),那么用户其他站点的安全防线也告破。
关于上述问题的建议:
- 传输层未加密的情况下,不要使用Basic认证。
- 如果使用Basic认证,登录密码由服务端生成。
- 如果可能,不要使用Basic认证。
- 除了安全缺陷,Basic认证还存在无法吊销认证的情况。