RabbitMQ身份验证、授权、访问控制
概述
不同的用户只能访问特定的虚拟主机。他们在每个虚拟主机中的权限也可以被限制。
RabbitMQ支持两种主要的身份验证机制,以及几种身份验证和授权后端。
专用术语和定义
身份验证和授权经常被混淆或交替使用。这是错误的,在RabbitMQ中,两者是分开的。为了简单起见,我们将身份验证定义为“确定用户是谁”,而授权定义为“确定用户可以做什么和不可以做什么”。
基础
客户端使用RabbitMQ特性来连接它。每个连接都有一个经过身份验证的关联用户。它还以一个虚拟主机为目标,用户必须对该虚拟主机拥有一组特定的权限。
用户凭据、目标虚拟主机和(可选的)客户端证书在连接启动时指定。
有一对默认的凭据,称为默认用户。默认情况下,该用户只能用于主机-本地连接。使用它的远程连接将被拒绝。
生产环境不应该使用默认用户,而应该使用生成的凭证创建新用户帐户。
默认虚拟主机和用户
当服务器第一次开始运行时,检测到它的数据库没有被初始化或已经被删除,它会用以下资源初始化一个新的数据库:
- 一个名为/(斜杠)的虚拟主机,
- 一个名为guest的用户,默认密码为guest,被授予对/虚拟主机的完全访问权
建议使用生成的用户名和密码对新用户进行预配置,或者删除guest用户,或者至少将其密码更改为合理安全的生成值,这样就不会被公众所知。
身份验证:你说你是谁?
应用程序连接到RabbitMQ后,在执行操作之前,必须进行身份验证,即呈现并证明自己的身份。有了这个身份,RabbitMQ节点就可以查找它的权限,并授权访问诸如虚拟主机、队列、exchanges等资源。
验证客户端身份的两种主要方法是用户名/密码对和X.509证书。用户名/密码对可以用于各种验证凭据的身份验证后端。
未通过身份验证的连接将被关闭,并在服务器日志中显示一条错误消息。
要使用X.509证书验证客户端连接,必须启用内置插件rabbitmq-auth-mechanism-ssl,并且客户端必须配置为使用EXTERNAL机制。使用这种机制,任何客户端提供的密码都将被忽略。
"guest"用户只能从本地主机连接
默认情况下,guest用户被禁止从远程主机连接;它只能通过loopback接口(即localhost)连接。这适用于与协议无关的连接。任何其他用户(默认情况下)都不会受到这种限制。
在生产系统中,解决这个问题的推荐方法是创建一个新用户或一组具有访问必要虚拟主机权限的用户。这可以通过 CLI tools、HTTP API或定义导入来实现。
这是通过配置文件中的loopback_users项配置的。
可以通过将loopback_users配置设置为none来允许guest用户从远程主机连接。
一个简单的RabbitMQ配置文件,允许客户端远程连接,如下所示:
# DANGER ZONE!
#
# allowing remote connections for default user is highly discouraged
# as it dramatically decreases the security of the system. Delete the user
# instead and create a new one with generated secure credentials.
loopback_users = none
管理用户和权限
用户和权限可以使用CLI工具和定义导入(下面将介绍)进行管理。
开始之前:Shell转义和生成的密码
生成复杂的密码是一种常见的安全实践,通常涉及非字母数字字符。这种做法在RabbitMQ用户中非常适用。
shell (bash、zsh等)解释某些字符(!, ?, &, ^, ", ', *, ~和其他)作为控制字符。
当rabbitmqctl add_user
、rabbitmqctl change_password
以及其他接受密码的命令在命令行中指定密码时,这些控制字符必须在shell中进行适当的转义。如果转义不恰当,命令将会失败,或者RabbitMQ CLI工具将从shell接收到不同的值。
在生成将在命令行上传递的密码时,长(比如,40到100个字符)的字母数字值和一组非常有限的符号(例如:,=)是最安全的选项。
当用户是通过HTTP API创建而不使用shell(例如curl)时,控制字符限制不适用。但是,根据所使用的编程语言,可能需要不同的转义规则。
添加用户
添加用户使用rabbitmqctl add_user
。它有多种方式指定密码:
# will prompt for password, only use this option interactively
rabbitmqctl add_user "username"
# Password is provided via standard input.
# Note that certain characters such as $, &, &, #, and so on must be escaped to avoid
# special interpretation by the shell.
echo '2a55f70a841f18b97c3a7db939b7adc9e34a0f1b' | rabbitmqctl add_user 'username'
# Password is provided as a command line argument.
# Note that certain characters such as $, &, &, #, and so on must be escaped to avoid
# special interpretation by the shell.
rabbitmqctl add_user 'username' '2a55f70a841f18b97c3a7db939b7adc9e34a0f1b'
在Windows上,rabbitmqctl变成rabbitmqctl.bat, shell转义是不同的:
# password is provided as a command line argument
rabbitmqctl.bat add_user 'username' '9a55f70a841f18b97c3a7db939b7adc9e34a0f1d'
列出用户
rabbitmqctl list_users
rabbitmqctl.bat list_users
输出可以更改为JSON:
rabbitmqctl list_users --formatter=json
rabbitmqctl.bat list_users --formatter=json
删除用户
rabbitmqctl delete_user 'username'
rabbitmqctl.bat delete_user 'username'
授予用户权限
使用rabbitmqctl set_permissions
命令为虚拟主机中的用户授予权限。
先添加vhost
rabbitmqctl add_vhost "custom-vhost"
# First ".*" for configure permission on every entity
# Second ".*" for write permission on every entity
# Third ".*" for read permission on every entity
rabbitmqctl set_permissions -p "custom-vhost" "username" ".*" ".*" ".*"
# First ".*" for configure permission on every entity
# Second ".*" for write permission on every entity
# Third ".*" for read permission on every entity
rabbitmqctl.bat set_permissions -p 'custom-vhost' 'username' '.*' '.*' '.*'
在虚拟主机中撤销用户权限
# Revokes permissions in a virtual host
rabbitmqctl clear_permissions -p "custom-vhost" "username"
# Revokes permissions in a virtual host
rabbitmqctl.bat clear_permissions -p 'custom-vhost' 'username'
多虚拟主机操作
每个rabbitmqctl
权限管理操作都是针对单个虚拟主机的。大容量操作必须脚本化,虚拟主机列表来自rabbitmqctl list_vhosts ——silent
:
# Assumes a Linux shell.
# Grants a user permissions to all virtual hosts.
for v in $(rabbitmqctl list_vhosts --silent); do rabbitmqctl set_permissions -p $v "a-user" ".*" ".*" ".*"; done
播种(预创建)用户和权限
生产环境通常需要预先配置(种子)许多虚拟主机、用户和用户权限。
这可以通过以下几种方式实现:
- 使用CLI Tools
- 在节点引导时导出和导入定义(推荐)
- 覆盖配置文件中的默认凭据
CLI Tools
请参阅用户管理一节。
节点启动时导入
这个过程包括以下步骤:
- 使用CLI工具设置一个临时节点,并创建必要的虚拟主机、用户、权限等
- 将定义导出到定义文件
- 删除文件中不相关的部分
- 配置节点在节点启动时或之后导入文件
要了解更多信息,请参见定义指南中的节点启动时导入定义。
节点启动后导入定义
参见importing definitions after node boot
覆盖默认用户凭据
可以使用两个配置选项来覆盖默认用户凭据。这个用户将只在第一次节点启动时创建,因此它们必须在第一次启动之前存在于配置文件中。
# default is "guest", and its access is limited to localhost only.
# See https://www.rabbitmq.com/access-control.html#default-state
default_user = a-user
# default is "guest"
default_pass = 768a852ed69ce916fa7faa278c962de3e4275e5f
和rabbitmq.conf
中的所有值一样,#字符作为注释的开头,所以在生成的凭证中必须避免使用这个字符。
默认用户凭据也可以加密。这需要使用高级配置文件advanced.config。配置值加密将详细介绍此主题。
授权:权限如何工作
当RabbitMQ客户端建立与服务器的连接并进行身份验证时,它会指定一个虚拟主机来进行操作。此时将执行第一级访问控制,服务器检查用户是否具有访问虚拟主机的权限,否则将拒绝连接请求。
资源,如交换和队列,是特定虚拟主机中的命名实体;相同的名称在每个虚拟主机中表示不同的资源。当对资源执行某些操作时,执行第二级访问控制。
RabbitMQ区分了对资源的配置操作、写操作和读操作。配置操作创建或销毁资源,或改变其行为。写操作将消息注入到资源中。而读操作则从资源中检索消息。
为了对资源执行操作,用户必须被授予相应的权限。下表显示了执行权限检查的所有AMQP命令对什么类型的资源需要什么权限。
AMQP 0-9-1 Operation | configure | write | read | |
---|---|---|---|---|
exchange.declare | (passive=false) | exchange | ||
exchange.declare | (passive=true) | |||
exchange.declare | (with AE) | exchange | exchange (AE) | exchange |
exchange.delete | exchange | |||
queue.declare | (passive=false) | queue | ||
queue.declare | (passive=true) | |||
queue.declare | (with DLX) | queue | exchange (DLX) | queue |
queue.delete | queue | |||
exchange.bind | exchange (destination) | exchange (source) | ||
exchange.unbind | exchange (destination) | exchange (source) | ||
queue.bind | queue | exchange | ||
queue.unbind | queue | exchange | ||
basic.publish | exchange | |||
basic.get | queue | |||
basic.consume | queue | |||
queue.purge | queue |
权限在每个vhost的基础上表示为三个正则表达式——分别用于配置、写和读。对于名称匹配正则表达式的所有资源,用户都被授予相应的操作权限。
为了方便,RabbitMQ在执行权限检查时,将AMQP 0-9-1的默认exchange的空名映射为'amq.default'。
正则表达式'^$',即只匹配空字符串,覆盖所有资源,有效地阻止用户执行任何操作。内置的AMQP 0-9-1资源名以amq作为前缀。服务器生成的名称以amq.gen作为前缀。
例如,'(amq.gen.*|amq.default)$'允许用户访问服务器生成的名称和默认exchange。空字符串,"是'$'的同义词,以完全相同的方式限制权限。
RabbitMQ可能会缓存每个连接或每个通道的访问控制检查结果。因此,对用户权限的更改只有在用户重新连接时才会生效。
关于如何设置访问控制的详细信息,请参见用户管理部分以及rabbitmqctl的手册页面。
用户标签和管理用户界面访问
除了上面提到的权限之外,用户还可以拥有与其关联的标记。目前,只有管理UI访问是由用户标签控制的。
标签使用rabbitmqctl管理。默认情况下,新创建的用户没有设置任何标记。
请参考插件管理指南来了解更多关于什么标签被支持,以及它们如何限制管理UI访问。
Topic授权
自3.7.0版本起,RabbitMQ支持主题交换的主题授权。发布到topic exchange的消息的routing key会在强制发布授权时被考虑在内(例如,在RabbitMQ默认的授权后端,routing key会与一个正则表达式匹配,以决定消息是否可以被路由到下游)。主题授权以STOMP和MQTT等协议为目标,它们围绕主题进行结构设计,并在底层使用topic exchanges。
主题授权是针对发布者的现有检查之上附加的一层。将消息发布到主题类型的exchange 将经过basic.publish
和路由键检查。如果前者拒绝访问,则永远不会调用后者。
也可以对主题消费者强制执行主题授权。请注意,对于不同的协议,它的工作方式是不同的。主题授权的概念只对面向主题的协议(如MQTT和STOMP)有意义。例如,在AMQP 0-9-1中,消费者从队列中消费,因此应用标准资源权限。此外,对于AMQP 0-9-1,如果有,AMQP 0-9-1 topic exchange和队列/交换之间的绑定路由键将根据配置的主题权限进行检查。关于RabbitMQ如何处理主题授权的更多信息,请参阅STOMP和MQTT文档指南。
当使用默认授权后端时,如果没有定义主题权限,发布到topic exchange或消费主题总是被授权的(在一个新的RabbitMQ安装中就是这样)。有了这个授权后端,主题授权是可选的:你不需要白名单任何exchanges。因此,要使用主题授权,您需要为一个或多个exchanges选择并定义主题权限。详细信息请参见rabbitmqctl手册页面。
内部(默认)授权后端支持权限模式中的变量扩展。支持username、vhost、client_id三个变量。注意,client_id只适用于MQTT。例如tonyg是连接的用户,则权限^{username}-.*
扩展为^tonyg-.*
。
如果使用了不同的授权后端(例如LDAP、HTTP、AMQP),请参考这些后端文档。
如果使用自定义的授权后端,则通过实现rabbit_authz_backend
行为的check_topic_access
回调来强制主题授权。
可替代的身份验证和授权后端
身份验证和授权是可插拔的。插件可以提供的实现
- authentication ("authn") backends
- authorisation ("authz") backends
插件可以同时提供这两种功能。例如,internal、LDAP和HTTP后端就是这样做的。
有些插件,例如Source IP range one,只提供一个授权后端。身份验证应该由内部数据库、LDAP等来处理。
结合后端
可以使用auth_backends配置键为authn或authz使用多个后端。当使用多个身份验证后端时,链中的后端返回的第一个正的结果被认为是最终结果。不应将其与混合后端混淆(例如,使用LDAP进行身份验证,使用内部后端进行授权)。
下面的例子配置RabbitMQ只使用internal 后端(并且是默认的):
# rabbitmq.conf
#
# 1 here is a backend name. It can be anything.
# Since we only really care about backend
# ordering, we use numbers throughout this guide.
#
# "internal" is an alias for rabbit_auth_backend_internal
auth_backends.1 = internal
上面的例子使用了rabbit_auth_backend_internal的别名internal。以下别名可用:
internal
forrabbit_auth_backend_internal
ldap
forrabbit_auth_backend_ldap
(from the LDAP plugin)http
forrabbit_auth_backend_http
(from the HTTP auth backend plugin)amqp
forrabbit_auth_backend_amqp
(from the AMQP 0-9-1 auth backend plugin)dummy
forrabbit_auth_backend_dummy
当使用第三方插件时,需要提供完整的模块名称。
下面的例子配置RabbitMQ使用LDAP后端进行认证和授权。内部数据库将不被参考:
auth_backends.1 = ldap
这将首先检查LDAP,如果用户不能通过LDAP认证,然后返回到内部数据库:
auth_backends.1 = ldap
auth_backends.2 = internal
和上面一样,但是会回到HTTP后端:
# rabbitmq.conf
#
auth_backends.1 = ldap
# uses module name instead of a short alias, "http"
auth_backends.2 = rabbit_auth_backend_http
# See HTTP backend docs for details
auth_http.user_path = http://my-authenticator-app/auth/user
auth_http.vhost_path = http://my-authenticator-app/auth/vhost
auth_http.resource_path = http://my-authenticator-app/auth/resource
auth_http.topic_path = http://my-authenticator-app/auth/topic
下面的例子配置RabbitMQ使用内部数据库进行认证,使用source IP range backend进行授权:
# rabbitmq.conf
#
auth_backends.1.authn = internal
# uses module name because this backend is from a 3rd party
auth_backends.1.authz = rabbit_auth_backend_ip_range
下面的例子配置RabbitMQ使用LDAP后端进行认证,使用内部后端进行授权:
# rabbitmq.conf
#
auth_backends.1.authn = ldap
auth_backends.1.authz = internal
下面的例子相当高级。它将首先检查LDAP。如果在LDAP中找到用户,那么将根据LDAP检查密码,并根据内部数据库执行后续的授权检查(因此LDAP中的用户也必须存在于内部数据库中,但不需要在那里设置密码)。如果用户在LDAP中没有找到,那么第二次尝试仅使用内部数据库:
# rabbitmq.conf
#
auth_backends.1.authn = ldap
auth_backends.1.authz = internal
auth_backends.2 = internal
用户认证机制
RabbitMQ支持多种SASL认证机制。服务器中内置了三种这样的机制:PLAIN、AMQPLAIN和RABBIT-CR-DEMO,还有一个——EXTERNAL——可以作为插件使用。
插件可以提供更多的身份验证机制。有关通用插件开发的更多信息,请参阅插件开发指南。
内置的身份验证机制
Mechanism | Description |
---|---|
PLAIN | SASL PLAIN 验证。在RabbitMQ服务器和客户端中,默认是启用的,其他大多数客户端也是默认的。 |
AMQPLAIN | PLAIN的非标准版本,用于向后兼容。该功能在RabbitMQ服务器中默认启用。 |
EXTERNAL | 身份验证使用带外机制进行,例如x509证书对等验证、客户端IP地址范围或类似的机制。这种机制通常由RabbitMQ插件提供。 |
RABBIT-CR-DEMO | 演示challenge-response认证的非标准机制。该机制具有与PLAIN等价的安全性,在RabbitMQ服务器中默认不启用。 |
服务器中的配置
rabbit应用程序中的配置变量auth_mechanisms决定了为连接的客户端提供哪些已安装的机制。这个变量应该是一个与机制名称对应的原子列表,例如默认情况下['PLAIN', 'AMQPLAIN']。服务器端列表被认为没有任何特定的顺序。请参见配置文件文档。
客户端机制配置
应用程序必须选择使用不同的身份验证机制,如EXTERNAL。
java中的配置
Java客户端默认情况下不使用javax.security.sasl包,因为这在非oracle的jdk上是不可预测的,而在Android上完全没有。有一个rabbitmq特有的SASL实现,通过SaslConfig接口配置。提供了一个DefaultSaslConfig类,使SASL配置在通常情况下更加方便。提供了一个类JDKSaslConfig来充当到javax.security.sasl的桥梁。
ConnectionFactory.getSaslConfig()和ConnectionFactory.setSaslConfig(SaslConfig)是与身份验证机制交互的主要方法。
身份验证故障排除
服务器日志将包含关于身份验证失败的条目:
2019-03-25 12:28:19.047 [info] <0.1613.0> accepting AMQP connection <0.1613.0> (127.0.0.1:63839 -> 127.0.0.1:5672)
2019-03-25 12:28:19.056 [error] <0.1613.0> Error on AMQP connection <0.1613.0> (127.0.0.1:63839 -> 127.0.0.1:5672, state: starting):
PLAIN login refused: user 'user2' - invalid credentials
2019-03-25 12:28:22.057 [info] <0.1613.0> closing AMQP connection <0.1613.0> (127.0.0.1:63839 -> 127.0.0.1:5672)
使用X.509证书进行身份验证的连接上的身份验证失败将以不同的方式记录。详细信息请参见TLS故障处理指南。
rabbitmqctl authenticate_user
可以用来测试用户名密码对的认证:
rabbitmqctl authenticate_user "a-username" "a/password"
如果身份验证成功,它将退出,代码为0。如果出现故障,将使用非零的退出码,并将打印失败错误消息。
rabbitmqctl authenticate_user
将使用CLI-to-node通信连接,试图通过内部API端点验证用户名/密码对。假定连接是可信的。如果不是这样,它的流量可以使用TLS加密。
根据AMQP 0-9-1规范,认证失败应该导致服务器立即关闭TCP连接。然而,RabbitMQ客户端可以选择使用AMQP 0-9-1的认证失败通知扩展来接收更具体的通知。现代客户端库对用户透明地支持这种扩展:不需要配置,身份验证失败将导致特定编程语言或环境中使用的可见的返回错误、异常或其他沟通问题的方式。
授权故障排除
rabbitmqctl list_permissions
可以用来检查用户在给定虚拟主机中的权限:
rabbitmqctl list_permissions --vhost /
# => Listing permissions for vhost "/" ...
# => user configure write read
# => user2 .* .* .*
# => guest .* .* .*
# => temp-user .* .* .*
rabbitmqctl list_permissions --vhost gw1
# => Listing permissions for vhost "gw1" ...
# => user configure write read
# => guest .* .* .*
# => user2 ^user2 ^user2 ^user2
服务器日志将包含操作授权失败的条目。例如,用户对虚拟主机没有任何权限:
2019-03-25 12:26:16.301 [info] <0.1594.0> accepting AMQP connection <0.1594.0> (127.0.0.1:63793 -> 127.0.0.1:5672)
2019-03-25 12:26:16.309 [error] <0.1594.0> Error on AMQP connection <0.1594.0> (127.0.0.1:63793 -> 127.0.0.1:5672, user: 'user2', state: opening):
access to vhost '/' refused for user 'user2'
2019-03-25 12:26:16.310 [info] <0.1594.0> closing AMQP connection <0.1594.0> (127.0.0.1:63793 -> 127.0.0.1:5672, vhost: 'none', user: 'user2')
授权失败(权限违规)也会被记录:
2019-03-25 12:30:05.209 [error] <0.1627.0> Channel error on connection <0.1618.0> (127.0.0.1:63881 -> 127.0.0.1:5672, vhost: 'gw1', user: 'user2'), channel 1:
operation queue.declare caused a channel exception access_refused: access to queue 'user3.q1' in vhost 'gw1' refused for user 'user2'
获得帮助并提供反馈
如果你对本指南的内容或其他任何与RabbitMQ相关的话题有任何疑问,不要犹豫,可以在RabbitMQ邮件列表中询问他们。
RabbitMQ身份验证、授权、访问控制的更多相关文章
- 从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credenti ...
- 关于WEB Service&WCF&WebApi实现身份验证之WebApi篇
之前先后总结并发表了关于WEB Service.WCF身份验证相关文章,如下: 关于WEB Service&WCF&WebApi实现身份验证之WEB Service篇. 关于WEB S ...
- .Net身份验证概述
一直以来,所有的系统基本都会有用户的登陆验证过程,整个过程其实也不难理解,就是对于cookie的解析.微软的.Net平台围绕用户身份验证授权也有好几个版本了,从早期的Membership到Identi ...
- 无法在web服务器上启动调试。调试失败,因为没有启用集成windows身份验证
----注意:以管理员身份运行VS C#中ASP.NET Web应用程序编译时的错误:无法在web服务器上启动调试.调试失败,因为没有启用集成windows身份验证. 解决:打开IIS,在IIS里查看 ...
- C# 实现身份验证之WebApi篇
今天再来总结关于如何实现WebApi的身份验证,以完成该系列所有文章,WebApi常见的实现方式有:FORM身份验证.集成WINDOWS验证.Basic基础认证.Digest摘要认证 第一种:FOR ...
- django使用email进行身份验证(转载)
版权所有,转载请注明出处:http://guangboo.org/2013/03/27/authentication-using-email-in-django django自带的验证功能免去了我们的 ...
- Net Core Identity 身份验证:注册、登录和注销 (简单示例)
一.前言 一般我们自己的系统都会用自己设置的一套身份验证授权的代码,这次用net core的identity来完成简单的注册.登录和注销. 二.数据库 首先就是创建上下文,我这里简单的建了Users和 ...
- Shiro身份验证及授权(二)
一.Shiro 身份验证 身份验证的步骤: 收集用户身份 / 凭证,即如用户名 / 密码: 调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationExcepti ...
- ASP.NET WEBAPI 的身份验证和授权
定义 身份验证(Authentication):确定用户是谁. 授权(Authorization):确定用户能做什么,不能做什么. 身份验证 WebApi 假定身份验证发生在宿主程序称中.对于 web ...
- ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇
在前一篇文章中,我介绍了ASP.NET Identity 基本API的运用并创建了若干用户账号.那么在本篇文章中,我将继续ASP.NET Identity 之旅,向您展示如何运用ASP.NET Ide ...
随机推荐
- vue调接口导出表格
props:{ form:{ type:Object, default:()=>{} }, indexNum:{ ...
- HFS~HTTP File Server 2.4rc2 20191231
后台,打卡,这有的 电脑 PC 浏览器 打开 安卓平台,浏览器,打开,界面
- TP5--路由
路由定义采用\think\Route类,通常是在应用的路由配置文件application/route.php进行注册 use think\Route; //路由规则 // Route::rule('/ ...
- TP5--数据库基本操作
/** * 插入数据 * 执行成功返回影响数据的条数,执行失败返回false */ //添加一条数据 $data = [ 'name'=>'wangwu', 'pwd'=>123456 ] ...
- goland使用go mod模式
使用go mod之后,想要在goland中有代码提示,有两种方式,一种是使用gopath下的goimport工具,另一种是使用gomod自身的管理工具 我是用的是非gopath的方式,每次新建项目后总 ...
- 【小记】如果 golang 内存不够了怎么办
在看 redis 1.0 源码时,总会看到需要申请内存的地方,如果申请不到需要大的内存就会返回 NULL,然后在调用层抛出 oom. 比如 listDup 中在复制特殊 value 或者加入新节点时都 ...
- loj6851
(CF1761D Tester Solution in Chinese) 定义 \(L(v)=\log_2\operatorname{lowbit}(v+1)\):也就是说,\(L(v)\) 是 \( ...
- vue仿QQ聊天室|vue聊天实例,直播聊天室
图片压缩 百亿站点 基于vue2.0+vue-cli+vuex+vue-router+webpack+es6+wcPop等技术开发的仿微信聊天界面|仿微信聊天室vue-chatRoom,实现了微信聊天 ...
- echart一个框里放三个饼图案例
效果图: 代码: function(chartOption){ chartOption = $nps$.deepCopyTo({}, chartOption); var chartDataList_r ...
- 点击div实现选中效果
先上一份效果图.原来的checked多选框还是存在的,我只不过隐藏了,让他的整个div的范围都是可以点击的,右上角三个点是可以删除当前元素,左下角的播放按钮可以点击播放语音,主要是利用z-index把 ...