在40岁老架构师 尼恩的读者社区(50+)中,最近有小伙伴拿到了一线互联网企业如美团、极兔、有赞、希音、百度、网易、滴滴的面试资格,遇到一几个很重要的面试题:
近段时间,有小伙伴面试美团,说遇到一个去重 调优的面试题:
接口被狂刷10Wqps,怎么解决?
MYsql 设计的时候,如何高性能进行数据去重,也是调优的重点和难点,社区中,还遇到过大概的变种:
形式1:短信验证码接口,如何防刷?
形式2:登录注册入口被恶意调用攻击,该如何破解?
形式3:接口被恶意狂刷,怎么办?
形式4:…, 后面的变种很多,都会收入 《尼恩Java面试宝典》。
这里尼恩给大家 对数据去重的 调优,做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
也一并把这个 题目以及参考答案,收入咱们的《尼恩Java面试宝典》V75,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从文末获取,公号:技术自由圈。
恶意攻击者通常会通过自动化工具进行攻击,尤其是会针对一些高频接口、核心接口进行恶意的访问,恶意的攻击,比如:
接口被狂刷会带来很高的瞬时吞吐量,很容易超过1Wqps,甚至10WQPS。这样的超高并发,会导致系统的瞬时雪崩,严重的可能会导致线上系统 瘫痪。
主要包括:
当接口被恶意狂刷时,可以通过安全参数校验来防止这种攻击。安全参数校验是指在接口请求中添加一些校验参数,例如时间戳、随机字符串、签名等,来验证请求的合法性。这样可以防止攻击者通过恶意程序进行大量的请求攻击。
具体来说,可以通过以下步骤来实现安全参数校验:
timestamp=1622945123
。nonce=abc123
。nonce=abc123×tamp=1622945123
。nonce=abc123×tamp=1622945123
。nonce=abc123×tamp=1622945123&key=secret
。c0c3f9a2a4c4c4dcd6d5b7b2a2e4d7b1
。将签名值添加到接口请求中,例如:nonce=abc123×tamp=1622945123&signature=c0c3f9a2a4c4c4dcd6d5b7b2a2e4d7b1
。通过以上步骤,可以有效地防止接口被恶意狂刷的攻击。
理论上,哈希计算很难破解,但是如果攻击者知道了hash算法和盐,攻击者就有可能伪造出带有正确校验位的签名值,从而绕过Java接口的限流和安全机制。
因此,该方案主要适用于需要简单防范一些低强度攻击的场景,例如防范垃圾请求或非法爬虫等。
对于高强度攻击,建议采取更为复杂的验证策略,例如使用使用用户访问认证,资源访问授权、IP白名单、签名算法等。
使用 HTTPS 可以保护数据传输的安全性,可以防止恶意攻击者窃取数据。HTTPS 使用 SSL/TLS 协议对数据进行加密,可以确保数据在传输过程中不被篡改或窃取。
因此,在一些恶意狂刷的高频接口,比如短信验证码接口、登录注册入口等敏感区域使用 HTTPS 是必要的。
当然,尽量在客户端和服务端在全部通讯,都使用HTTPS协议进行加密,防止数据被窃听或篡改。
用户访问认证是指在系统中验证用户身份以授权其访问系统资源的过程。
用户访问认证是信息安全中非常重要的一环,可以保护系统免受未经授权的访问和攻击。
常见的用户访问认证方式包括用户名密码认证、双因素认证、证书认证等
这里来看最为简单的 用户访问认证: 用户名密码认证
用户名密码认证 的方式,要求用户提供 用户名和密码,换取 访问的令牌。
参考的代码如下:
@RequestMapping("/api/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password){
if(!checkUser(username, password)){
return "用户名或密码错误";
}
String token = getToken();
saveToken(token);
return token;
}
private boolean checkUser(String username, String password){
//校验用户是否合法
}
private String getToken(){
//生成token
}
private void saveToken(String token){
//保存token
}
在上述代码中,当用户调用login接口时,需要提供用户名和密码。
此时会进行用户校验,若校验失败则返回错误信息,否则生成token并保存,最终返回给用户。
生成Token的作用是为了在接口请求时验证用户身份。
具体来说,当用户第一次登录系统后,该接口可以根据用户信息生成一个Token字符串,并将其保存至服务端或客户端。
对于Java接口被恶意狂刷问题,Token的作用是防止非法请求。
如果Token验证失败,则返回错误信息并拦截该请求。
下面是使用拦截器进行令牌校验的示例代码:
// 鉴权拦截器
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !checkToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
return true;
}
private boolean checkToken(String token) {
// 验证Token是否合法,是否过期等
}
}
AuthInterceptor类是拦截器类,用于检查请求头中的Token是否合法。
如果Token验证失败,则返回401错误码并拦截该请求。
如果接口的安全性要求非常高,只有特定的用户才能访问。
或者说,如果遇到要对资源进行更细粒度的 防刷处理,可以对资源进行访问权限的管理和授权。 主要的策略有RBAC的机制。
主要的思路为:使用 访问控制策略 对资源权限进行 精准控制。场景的访问控制策略 为RBAC策略。
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常见的访问控制策略,它将用户分配到不同的角色中,每个角色具有一组权限,从而控制用户对系统资源的访问。
在 RBAC 中,管理员可以根据用户的职责和职位,将用户分配到适当的角色中,从而控制用户对系统资源的访问权限。RBAC 还可以提高系统的安全性和可管理性,减少权限管理的复杂性。
Shiro 是一个强大且易于使用的 RBAC 访问的安全框架,提供了身份验证、授权、加密、会话管理等安全功能。其中授权是 Shiro 的核心功能之一,它可以帮助我们实现资源访问授权。
在 Shiro 中,授权是通过授权信息和角色信息来实现的。授权信息是指哪些用户可以访问哪些资源,角色信息是指用户可以拥有哪些权限。
Shiro 中的授权流程如下:
在 Shiro 中,授权信息和角色信息可以通过配置文件或数据库来管理。我们可以在配置文件或数据库中定义哪些用户可以访问哪些资源,以及哪些角色可以拥有哪些权限。在程序运行时,Shiro 会从配置文件或数据库中读取授权信息和角色信息,并根据这些信息进行授权判断。
除了配置文件和数据库,Shiro 还提供了编程式授权方式,即通过编写代码来实现授权。这种方式可以实现更加灵活的授权,但需要开发人员自己编写授权逻辑。
下面是 Shiro 的使用入门步骤:
在 Maven 项目中,可以通过在 pom.xml 文件中添加以下依赖来引入 Shiro:
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.7.1version>
dependency>
在 Shiro 中,可以通过配置文件或编程方式来配置安全策略。下面是一个简单的 Shiro 配置文件示例:
[main]
# 定义一个默认的 Realm,用于认证和授权
myRealm = com.example.MyRealm
# 定义一个默认的加密算法
passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService = $passwordService
# 配置安全管理器
securityManager = org.apache.shiro.mgt.DefaultSecurityManager
securityManager.realm = $myRealm
# 配置加密器
securityManager.passwordService = $passwordService
securityManager.authenticator.passwordMatcher = $passwordMatcher
# 配置会话管理器
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager
# 配置缓存管理器
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
securityManager.cacheManager = $cacheManager
[users]
# 定义用户及其密码和角色
admin = admin, admin_role
user1 = password1, user_role1
user2 = password2, user_role2
[roles]
# 定义角色及其权限
admin_role = *
user_role1 = user:read, user:write
user_role2 = user:read
在 Shiro 中,Realm 是用于认证和授权的核心组件。可以通过实现 org.apache.shiro.realm.Realm 接口来创建自定义的 Realm,或者使用 Shiro 提供的现成的 Realm 实现。下面是一个简单的自定义 Realm 示例:
public class MyRealm implements Realm {
@Override
public String getName() {
return "myRealm";
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
if (!"admin".equals(username)) {
throw new UnknownAccountException("Unknown user");
}
if (!"password".equals(password)) {
throw new IncorrectCredentialsException("Incorrect password");
}
return new SimpleAuthenticationInfo(username, password, getName());
}
@Override
public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin_role");
info.addStringPermission("user:read");
info.addStringPermission("user:write");
return info;
}
}
在应用程序中,可以通过调用 Shiro 提供的 Subject 类的方法来进行认证和授权。下面是一个简单的示例:
// 获取当前用户的 Subject
Subject currentUser = SecurityUtils.getSubject();
// 创建一个用户名和密码的 Token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "password");
try {
// 进行认证
currentUser.login(token);
// 进行授权
if (currentUser.hasRole("admin_role")) {
System.out.println("User has admin role");
}
if (currentUser.isPermitted("user:read")) {
System.out.println("User has read permission");
}
if (currentUser.isPermitted("user:write")) {
System.out.println("User has write permission");
}
} catch (UnknownAccountException e) {
System.out.println("Unknown user");
} catch (IncorrectCredentialsException e) {
System.out.println("Incorrect password");
} catch (LockedAccountException e) {
System.out.println("Account is locked");
} catch (AuthenticationException e) {
System.out.println("Authentication error");
}
以上就是 Shiro 的入门使用步骤。当然,Shiro 还提供了很多其他的功能和配置选项,需要根据具体的应用场景进行选择和使用。
40岁老架构师尼恩提示:
和Shiro 类似,SpringSecurity 也是一个 资源访问授权的 框架。
原理都是类似的,大家精通一个,另一个也触类旁通了。
访问限流是一种常见的保护机制,用于控制对某个资源的访问速率,以防止过多的请求导致系统负载过高或崩溃。
访问限流包括两个维度:
维度一:访问限流策略
维度二:访问限流算法
在令牌桶算法中,系统会按照一定速率往令牌桶中添加令牌,每个令牌代表一个请求的访问权限。当请求到来时,系统会从令牌桶中取出一个令牌,如果令牌桶中没有令牌,则拒绝该请求。
在漏桶算法中,系统会按照一定速率从漏桶中释放请求,当请求到来时,如果漏桶中还有空余容量,则将该请求放入漏桶中,否则拒绝该请求。
接口被恶意狂,可以使用 基于 漏桶算法 + 基于用户限流的 综合性限流策略。
可以结合黑名单策略,对恶意用户进行有效人工管理。 如果用户被限流,甚至可以加入黑名单,封掉这个用户。
关于限流的详细内容,请查看尼恩的 深度笔记 :
限流深入解读:计数器、漏桶、令牌桶 三大算法的原理与实战(史上最全)
IP封禁是常见的网络安全措施,用于保护服务器免受恶意攻击。
IP封禁是指将某个IP地址列入黑名单,禁止其访问服务器。
在实际应用中,可以通过配置防火墙规则、使用反向代理服务器、使用专业的防火墙软件等方式来实现IP封禁和防刷。
也可以在应用层代码中,通过过滤器的方式,进行IP封禁
参考代码如下:
public class IpFilter extends OncePerRequestFilter {
private static final Set<String> IP_SET = new HashSet<>();
static {
IP_SET.add("192.168.1.100");
IP_SET.add("127.0.0.1");
//添加其他需要封禁的IP
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String ipAddress = request.getRemoteAddr();
if(IP_SET.contains(ipAddress)){
response.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
filterChain.doFilter(request, response);
}
}
在上述代码中,通过IpFilter过滤器来阻止特定的IP地址访问接口。其中,IP_SET为需要封禁的IP地址集合。
访问日志监控是一种常见的监控方式,用于监控网站、应用程序等的访问情况,可以帮助我们了解用户的行为和需求,以便做出相应的优化和改进。
常见的访问日志监控工具有 Apache 的 AccessLog、Nginx 的 AccessLog、ELK Stack 、Java 请求日志监控等。
这些工具、框架帮助我们收集、分析和可视化访问日志数据,从而更好地了解用户的需求和行为。
同时,我们也可以通过访问日志监控来检测和排查一些常见的安全问题,如 SQL 注入、XSS 攻击等。
监控访问日志可以帮助发现未经授权的访问请求。可以使用日志记录工具来记录每个请求的 IP 地址、时间戳和请求参数。
如果发现异常请求,可以及时采取措施,以防止攻击。
下面是一个参考的,进行响应记录的 过滤器。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
try {
log.info("Request Received. URI: {}", requestURI);
chain.doFilter(request, response);
} catch(Exception e) {
log.error("Exception occurred while processing request. URI: {}", requestURI, e);
throw e;
} finally {
log.info("Request Completed. URI: {} Response Status: {}", requestURI, httpResponse.getStatus());
}
}
在上述代码中,通过Filter过滤器来实现日志监控。
当请求进入时记录请求URI,当请求结束时记录响应状态码,如此可及时发现异常情况。
有了日志仅仅是第一步,还需要结合定时任务或者 流式计算工具,进行异步分析,甚至是离线分析。
异步分析或者离线分析的,最终得到恶意请求的用户或者ip,然后进行拉黑或者IP封禁。
如果服务器无法承受恶意攻击,可以通过升级硬件设备来增加服务器的承载能力。
例如,可以增加CPU或内存等硬件资源,降低服务器的响应时间。
当Java接口被恶意狂刷时,及时通知相关管理人员或安全团队是非常重要的。他们可以采取更加有效的措施,如封禁IP地址、加强认证机制等,从而保障接口的安全。
系统监控和预警通知是保持系统稳定和可靠性的重要手段。通常,我们需要对系统的各种指标进行监控和预警,如 CPU 使用率、内存使用率、磁盘空间、网络流量等等。当这些指标超过预设的阈值时,系统就会触发警报,并通知相关人员进行处理。
对于系统的 恶意狂刷,也可以基于 时序进行 统计和预警。
为了实现系统监控和预警通知,可以使用一些开源的工具,如 Prometheus、Grafana、Alertmanager 等等。
其中,Prometheus 是一个广泛使用的监控系统,它支持多种数据源,如本地文件、HTTP、JMX、SNMP 等等。
Grafana 则是一个可视化监控工具,它可以将 Prometheus 收集的数据进行可视化展示。
Alertmanager 则是一个通知管理器,它可以根据不同的警报级别和通知方式,将警报发送给不同的人员或团队。
结合 Prometheus +Grafana +Alertmanager , 可以对 Java接口被恶意狂刷 进行时序统计,一旦超过一定的阈值,比如 1分钟被狂刷 10W次,进行预警, 方便开发和运维进行防范。
接口被狂刷10Wqps,怎么破?其实是一个 架构问题。甚至是一个架构难题。
架构和高级开发不一样 , 架构的问题是open的、开发式的、没有标准答案的。高并发的架构之路,其实是充满了坎坷。 在做架构过程中,或者在转型过程中,如果遇到复杂的场景,确实不知道怎么做架构方案,确实找不到有底的方案,怎么办? 可以来找40岁老架构尼恩求助.
就在前几天,一个小伙伴遇到了一个 电商网站的黄金链路架构, 开始找不到思路,但是经过尼恩 10分钟语音指导,一下就豁然开朗。
so,大家如果遇到架构问题,甚至架构难题,可以找尼恩来交流,来求助。
《吃透8图1模板,人人可以做架构》
《10Wqps评论中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
尼恩 架构笔记、面试题 的PDF文件更新,▼请到下面【技术自由圈】公号取 ▼