最近有需求需要对单点登录(cas3.5),用户次数和时间进行限制,例如ghf用户在1分钟内密码错误次数超过三次禁止登录,锁定1分钟,网上很多说配置throttleInterceptor,但最后发现是对频率的限制,以下是参数解释:
thresholdRate = (double) failureThreshold / (double) failureRangeInSeconds
failureRangeInSeconds 失败范围时间 :多少秒内失败算一次;例如10,表示 10秒为一个周期,这个周期内失败次数累加,超过10秒,重新开始;同时也表示锁定时间。
总结来说即在一秒内用户错误登录的频率大于failureThreshold/failureRangeInSeconds时候,就会被阻止。显然根本实现不了需求,看到有文章说使用日志审计功能可以,特意进行了修改,基本完成需求,仍然有小问题,有时间优化以下吧!
参考文章:https://blog.csdn.net/zhurhyme/article/details/37535697
总体思路是,用户登录日志插入CAS_LOG表中,用户每登录一次就记录一条数据,登录成功失败记录信息不一致,在程序内部查询最近几分钟(例如1分钟)内几(例如3次)次登录失败的次数,超过规定次数跳转到提示页面。修改后时间、次数、表名都可以配置。因为cas默认日志审计默认是COM_AUDIT_TRAIL表,不符合公司项目默认建表规范,需要修改。
以下是修改过程:
注释第一部分是在内存中进行限制的方法,不满足需求,第二部分是cas原生的日志审计功能,需要修改。最后是代码部分是真正的配置信息,注意红色部分。
MyInspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter" p:failureRangeInSeconds="60" p:failureThreshold="3" p:tableName="CAS_LOG">
同时修改WEB-INF/cas-servlet.xml,中
将登录过滤信息加入其中。
MyJdbcAuditTrailManager" >
建表语句,这是oracle的:
create table CAS_LOG
(
aud_user VARCHAR2(100) not null,
aud_client_ip VARCHAR2(15) not null,
aud_server_ip VARCHAR2(15) not null,
aud_resource VARCHAR2(100) not null,
aud_action VARCHAR2(100) not null,
applic_cd VARCHAR2(5) not null,
aud_date TIMESTAMP(6) not null
)
tablespace LOG
pctfree 10
initrans 1
maxtrans 255
storage
(
initial 64K
next 1M
minextents 1
maxextents unlimited
);
-- Add comments to the table
comment on table CAS_LOG
is 'cas 登录记录表';
-- Add comments to the columns
comment on column CAS_LOG.aud_user
is '审计用户';
comment on column CAS_LOG.aud_client_ip
is '客户端ip';
comment on column CAS_LOG.aud_server_ip
is '服务端ip';
comment on column CAS_LOG.aud_resource
is '资源';
comment on column CAS_LOG.aud_action
is '审计状态';
comment on column CAS_LOG.applic_cd
is 'APPLIC_CD';
comment on column CAS_LOG.aud_date
is '时间戳';
说明:由于项目cas服务器还进行了返回更过信息的修改:登录cas本来只返回用户名,现在返回其他信息,包括角色、权限、菜单信息等,造成登录后用户日志用户名部分变成立了,audit:unknown;用户日志没办法确定用户信息,所有对JdbcAuditTrailManager(inspektr-support-spring-1.0.7.GA.jar)进行了修改(蓝色部分),如果你没有问题不需要修改。
修改好jar包下载地址:https://download.csdn.net/download/guohangfei001/11005804
需要修改2个jar包中内容,cas-server-webapp-support-4.0.0.jar、inspektr-support-spring-1.0.7.GA.jar,(没有说明部分问题不需要修改),修改说明
cas-server-webapp-support-4.0.0.jar中的
InspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter修改为:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.jasig.cas.web.support; import com.github.inspektr.audit.AuditActionContext; import com.github.inspektr.audit.AuditPointRuntimeInfo; import com.github.inspektr.audit.AuditTrailManager; import com.github.inspektr.common.web.ClientInfo; import com.github.inspektr.common.web.ClientInfoHolder; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; public class MyInspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter extends AbstractThrottledSubmissionHandlerInterceptorAdapter { private static final String DEFAULT_APPLICATION_CODE = "CAS"; private static final String DEFAULT_AUTHN_FAILED_ACTION = "AUTHENTICATION_FAILED"; private static final String INSPEKTR_ACTION = "THROTTLED_LOGIN_ATTEMPT"; private final AuditTrailManager auditTrailManager; private final JdbcTemplate jdbcTemplate; private String applicationCode = "CAS"; private String authenticationFailureCode = "AUTHENTICATION_FAILED"; private String tableName = "CAS_LOG"; public MyInspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter(AuditTrailManager auditTrailManager, DataSource dataSource) { this.auditTrailManager = auditTrailManager; this.jdbcTemplate = new JdbcTemplate(dataSource); } //这是执行cas登录判断的关键部分,返回true,跳转到异常提示页面,flase正常执行。其中有对用户ip的限制,还有时间的判断,具体的可以对比InspektrThrottledSubmissionByIpAddressAndUsernameHandlerInterceptorAdapter进行分析。 protected boolean exceedsThreshold(HttpServletRequest request) { String query = "SELECT AUD_DATE FROM " + this.tableName + " WHERE AUD_CLIENT_IP = ? AND AUD_USER = ? " + "AND AUD_ACTION = ? AND APPLIC_CD = ? AND AUD_DATE >= ? ORDER BY AUD_DATE DESC"; String userToUse = this.constructUsername(request, this.getUsernameParameter()); Calendar cutoff = Calendar.getInstance(); Listfailures = this.jdbcTemplate.query(query, new Object[]{request.getRemoteAddr(), userToUse, this.authenticationFailureCode, this.applicationCode, new Date(cutoff.getTimeInMillis() - (long)(this.getFailureRangeInSeconds() * 1000))}, new int[]{12, 12, 12, 12, 93}, new RowMapper protected void recordSubmissionFailure(HttpServletRequest request) { } protected void recordThrottle(HttpServletRequest request) { super.recordThrottle(request); String userToUse = this.constructUsername(request, this.getUsernameParameter()); ClientInfo clientInfo = ClientInfoHolder.getClientInfo(); AuditPointRuntimeInfo auditPointRuntimeInfo = new AuditPointRuntimeInfo() { private static final long serialVersionUID = 1L; public String asString() { return String.format("%s.recordThrottle()", this.getClass().getName()); } }; AuditActionContext context = new AuditActionContext(userToUse, userToUse, "THROTTLED_LOGIN_ATTEMPT", this.applicationCode, new Date(), clientInfo.getClientIpAddress(), clientInfo.getServerIpAddress(), auditPointRuntimeInfo); this.auditTrailManager.record(context); } public final void setApplicationCode(String applicationCode) { this.applicationCode = applicationCode; } public final void setAuthenticationFailureCode(String authenticationFailureCode) { this.authenticationFailureCode = authenticationFailureCode; } protected String constructUsername(HttpServletRequest request, String usernameParameter) { String username = request.getParameter(usernameParameter); return "[username: " + (username != null ? username : "") + "]"; } public void setTableName(String tableName) { this.tableName = tableName; } } 下面是对inspektr-support-spring-1.0.7.GA.jar,的修改(你可能不需要),日志审计实际上是开启了另一个线程进行插入日志:修改() { public Timestamp mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getTimestamp(1); } }); return failures.size() >= this.getFailureThreshold(); }
JdbcAuditTrailManager为:MyJdbcAuditTrailManager:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.github.inspektr.audit.support;
import com.github.inspektr.audit.AuditActionContext;
import com.github.inspektr.audit.AuditTrailManager;
import com.github.inspektr.common.Cleanable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
public class MyJdbcAuditTrailManager extends SimpleJdbcDaoSupport implements AuditTrailManager, Cleanable, DisposableBean {
private static final String INSERT_SQL_TEMPLATE = "INSERT INTO %s (AUD_USER, AUD_CLIENT_IP, AUD_SERVER_IP, AUD_RESOURCE, AUD_ACTION, APPLIC_CD, AUD_DATE) VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String DELETE_SQL_TEMPLATE = "DELETE FROM %s %s";
private static final int DEFAULT_COLUMN_LENGTH = 100;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@NotNull
private final TransactionTemplate transactionTemplate;
@NotNull
@Size(
min = 1
)
private String tableName = "COM_AUDIT_TRAIL";
@Min(50L)
private int columnLength = 100;
@NotNull
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private boolean defaultExecutorService = true;
private WhereClauseMatchCriteria cleanupCriteria = new NoMatchWhereClauseMatchCriteria();
private HttpServletRequest httpServletRequest;
public MyJdbcAuditTrailManager(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void record(AuditActionContext auditActionContext) {
this.executorService.execute(new MyJdbcAuditTrailManager.LoggingTask(auditActionContext, this.transactionTemplate, this.columnLength));
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public void setCleanupCriteria(WhereClauseMatchCriteria criteria) {
this.cleanupCriteria = criteria;
}
public void setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
this.defaultExecutorService = false;
}
public void setColumnLength(int columnLength) {
this.columnLength = columnLength;
}
public void destroy() throws Exception {
if (this.defaultExecutorService) {
this.executorService.shutdown();
}
}
public void clean() {
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
String sql = String.format("DELETE FROM %s %s", MyJdbcAuditTrailManager.this.tableName, MyJdbcAuditTrailManager.this.cleanupCriteria);
List> params = MyJdbcAuditTrailManager.this.cleanupCriteria.getParameterValues();
MyJdbcAuditTrailManager.this.logger.info("Cleaning audit records with query " + sql);
MyJdbcAuditTrailManager.this.logger.debug("Query parameters: " + params);
int count = MyJdbcAuditTrailManager.this.getSimpleJdbcTemplate().update(sql, params.toArray());
MyJdbcAuditTrailManager.this.logger.info(count + " records deleted.");
}
});
}
protected class LoggingTask implements Runnable {
private final AuditActionContext auditActionContext;
private final TransactionTemplate transactionTemplate;
private final int columnLength;
public LoggingTask(AuditActionContext auditActionContext, TransactionTemplate transactionTemplate, int columnLength) {
this.auditActionContext = auditActionContext;
this.transactionTemplate = transactionTemplate;
this.columnLength = columnLength;
}
public void run() {
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
String userId_L = LoggingTask.this.auditActionContext.getResourceOperatedUpon().trim();
String userId = userId_L.length() < 23 ? "username" : "[username: " + userId_L.substring(23, userId_L.length() - 10) + "]";
String resource = LoggingTask.this.auditActionContext.getResourceOperatedUpon().length() <= LoggingTask.this.columnLength ? LoggingTask.this.auditActionContext.getResourceOperatedUpon() : LoggingTask.this.auditActionContext.getResourceOperatedUpon().substring(0, LoggingTask.this.columnLength);
String action = LoggingTask.this.auditActionContext.getActionPerformed().length() <= LoggingTask.this.columnLength ? LoggingTask.this.auditActionContext.getActionPerformed() : LoggingTask.this.auditActionContext.getActionPerformed().substring(0, LoggingTask.this.columnLength);
MyJdbcAuditTrailManager.this.getSimpleJdbcTemplate().update(String.format("INSERT INTO %s (AUD_USER, AUD_CLIENT_IP, AUD_SERVER_IP, AUD_RESOURCE, AUD_ACTION, APPLIC_CD, AUD_DATE) VALUES (?, ?, ?, ?, ?, ?, ?)", MyJdbcAuditTrailManager.this.tableName), new Object[]{userId, LoggingTask.this.auditActionContext.getClientIpAddress(), LoggingTask.this.auditActionContext.getServerIpAddress(), resource, action, LoggingTask.this.auditActionContext.getApplicationCode(), LoggingTask.this.auditActionContext.getWhenActionWasPerformed()});
}
});
}
}
}
红色部分是查入日志的关键信息部分,userid就是AUD_USER字段,我修改成了[username: ghf]。问题也出现在这里,登录失败的时候可以获得用户名ghf,但是成功的时候变成了一串长字符串,所有log信息变成了一下图片,不过用来判断登录没有问题了
现在已经实现登录次数、时间,根据ip地址、用户信息进行限制了,问题是时间的限制部分其实不是很完善,需要优化。