asp.net core 自定義 Policy 替換 AllowAnonymous 的行為

asp.net core 自定義 Policy 替換 AllowAnonymous 的行為

Intro

最近對我們的服務進行了改造,原本內部服務在內部可以匿名調用,現在增加了限制,通過 identity server 來管理 api 和 client,網關和需要訪問api的客戶端或api服務相互調用通過 client_credencial 的方式來調用,這樣一來我們可以清晰知道哪些 api 服務會被哪些 api/client 所調用,而且安全性來說更好。
為了保持後端服務的代碼更好的兼容性,希望能夠實現相同的代碼通過在 Startup 里不同的配置實現不同的 Authorization 邏輯,原來我們的服務的 Authorize 都是以 Authorize("policyName") 的形式來寫的,這樣一來我們只需要修改這個 Policy 的授權配置就可以了。對於 AllowAnonymous 就希望可以通過一種類似的方式來實現,通過自定義一個 Policy 來實現自己的邏輯

實現方式

將 action 上的 AllowAnonymous 替換為 Authorize("policyName"),在沒有設置 Authorize 的 controller 上增加 Authorize("policyName")

public class AllowAnonymousPolicyTransformer : IApplicationModelConvention
{
    private readonly string _policyName;

    public AllowAnonymousPolicyTransformer() : this("anonymous")
    {
    }

    public AllowAnonymousPolicyTransformer(string policyName) => _policyName = policyName;

    public void Apply(ApplicationModel application)
    {
        foreach (var controllerModel in application.Controllers)
        {
            if (controllerModel.Filters.Any(_ => _.GetType() == typeof(AuthorizeFilter)))
            {
                foreach (var actionModel in controllerModel.Actions)
                {
                    if (actionModel.Filters.Any(_ => _.GetType() == typeof(AllowAnonymousFilter)))
                    {
                        var allowAnonymousFilter = actionModel.Filters.First(_ => _.GetType() == typeof(AllowAnonymousFilter));
                        actionModel.Filters.Remove(allowAnonymousFilter);
                        actionModel.Filters.Add(new AuthorizeFilter(_policyName));
                    }
                }
            }
            else
            {
                if (controllerModel.Filters.Any(_ => _.GetType() == typeof(AllowAnonymousFilter)))
                {
                    var allowAnonymousFilter = controllerModel.Filters.First(_ => _.GetType() == typeof(AllowAnonymousFilter));
                    controllerModel.Filters.Remove(allowAnonymousFilter);
                }
                controllerModel.Filters.Add(new AuthorizeFilter(_policyName));
            }
        }
    }
}

public static class MvcBuilderExtensions
{
    public static IMvcBuilder AddAnonymousPolicyTransformer(this IMvcBuilder builder)
    {
        builder.Services.Configure<MvcOptions>(options =>
        {
            options.Conventions.Insert(0, new AllowAnonymousPolicyTransformer());
        });
        return builder;
    }

    public static IMvcBuilder AddAnonymousPolicyTransformer(this IMvcBuilder builder, string policyName)
    {
        builder.Services.Configure<MvcOptions>(options =>
        {
            options.Conventions.Insert(0, new AllowAnonymousPolicyTransformer(policyName));
        });
        return builder;
    }
}

controller 中的代碼:

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly ILogger _logger;

    public ValuesController(ILogger<ValuesController> logger)
    {
        _logger = logger;
    }

    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        var msg = $"IsAuthenticated: {User.Identity.IsAuthenticated} ,UserName: {User.Identity.Name}";
        _logger.LogInformation(msg);
        return new string[] { msg };
    }

    // GET api/values/5
    [Authorize]
    [HttpGet("{id:int}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }
    // ...
}

Startup 中 ConfigureServices 配置:

var anonymousPolicyName = "anonymous";

services.AddAuthorization(options =>
{
    options.AddPolicy(anonymousPolicyName, builder => builder.RequireAssertion(context => context.User.Identity.IsAuthenticated));

    options.DefaultPolicy = new AuthorizationPolicyBuilder(HeaderAuthenticationDefaults.AuthenticationSchema)
        .RequireAuthenticatedUser()
        .RequireAssertion(context => context.User.GetUserId<int>() > 0)
        .Build();
});

services.AddMvc(options =>
    {
        options.Conventions.Add(new ApiControllerVersionConvention());
    })
    .AddAnonymousPolicyTransformer(anonymousPolicyName)
    ;

實現效果

訪問原來的匿名接口

userId 為0訪問原來的匿名接口

userId 大於0訪問原來的匿名接口

userId 為0訪問需要登錄的接口

userId 大於0訪問需要登錄的接口

More

注:按照上面的做法已經可以做到自定義 policy 代替 AllowAnonymous 的行為,但是原來返回的401,現在可能返回到就是 403 了

Reference

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

併發編程-硬件加持的CAS操作夠快么?

Talk is cheap

CAS(Compare And Swap),即比較並交換。是解決多線程并行情況下使用鎖造成性能損耗的一種機制,CAS操作包含三個操作數——內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論位置V的值是否等於A, 都將返回V原有的值。

CAS的含義是”我認為V的值應該是A,如果是,那我將V的值更新為B,否則不修改並告訴V的值實際是多少“

Show you my code

在單線程環境中分別使用無鎖,加鎖以及cas進行十組5億次累加運算,然後打印出平均耗時。

 /**
 * cas對比加鎖測試
 *
 * @author Jann Lee
 * @date 2019-11-21 0:12
 **/
public class CasTest {

    @Test
    public void test() {
        long times = 500_000_000;
        // 記錄耗時
        List<Long> elapsedTime4NoLock = new ArrayList<>(10);
        List<Long> elapsedTime4Synchronized = new ArrayList<>(10);
        List<Long> elapsedTime4ReentrantLock = new ArrayList<>(10);
        List<Long> elapsedTime4Cas = new ArrayList<>(10);

        // 進行10組試驗
        for (int j = 0; j < 10; j++) {
            // 無鎖
            long startTime = System.currentTimeMillis();
            for (long i = 0; i < times; i++) {
            }
            long endTime = System.currentTimeMillis();
            elapsedTime4NoLock.add(endTime - startTime);

            // synchronized 關鍵字(隱式鎖)
            startTime = endTime;
            for (long i = 0; i < times; ) {
                i = addWithSynchronized(i);
            }
            endTime = System.currentTimeMillis();
            elapsedTime4Synchronized.add(endTime - startTime);

            // ReentrantLock 顯式鎖
            startTime = endTime;
            ReentrantLock lock = new ReentrantLock();
            for (long i = 0; i < times; ) {
                i = addWithReentrantLock(i, lock);
            }
            endTime = System.currentTimeMillis();
            elapsedTime4ReentrantLock.add(endTime - startTime);

            // cas(AtomicLong底層是用cas實現)
            startTime = endTime;
            AtomicLong atomicLong = new AtomicLong();
            while (atomicLong.getAndIncrement() < times) {
            }
            endTime = System.currentTimeMillis();
            elapsedTime4Cas.add(endTime - startTime);
        }

        System.out.println("無鎖計算耗時: " + average(elapsedTime4NoLock) + "ms");
        System.out.println("synchronized計算耗時: " + average(elapsedTime4Synchronized) + "ms");
        System.out.println("ReentrantLock計算耗時: " + average(elapsedTime4ReentrantLock) + "ms");
        System.out.println("cas計算耗時: " + average(elapsedTime4Cas) + "ms");

    }

    /**
     * synchronized加鎖
     */
    private synchronized long addWithSynchronized(long i) {
        i = i + 1;
        return i;
    }

    /**
     * ReentrantLock加鎖
     */
    private long addWithReentrantLock(long i, Lock lock) {
        lock.lock();
        i = i + 1;
        lock.unlock();
        return i;
    }

    /**
     * 計算平均耗時
     */
    private double average(Collection<Long> collection) {
        return collection.stream().mapToLong(i -> i).average().orElse(0);
    }
}

從案例中我們可能看出在單線程環境場景下cas的性能要高於鎖相關的操作。當然,在競爭比較激烈的情況下性能可能會有所下降,因為要不斷的重試和回退或者放棄操作,這也是CAS的一個缺點所在,因為這些重試,回退等操作通常用開發者來實現。

CAS的實現並非是簡單的代碼層面控制的,而是需要硬件的支持,因此在不同的體系架構之間執行的性能差異很大。但是一個很管用的經驗法則是:在大多數處理器上,在無競爭的鎖獲取和釋放的”快速代碼路徑“上的開銷,大約是CAS開銷的兩倍。

為何CAS如此優秀

硬件加持,現代大多數處理器都從硬件層面通過一些列指令實現CompareAndSwap(比較並交換)同步原語,進而使操作系統和JVM可以直接使用這些指令實現鎖和併發的數據結構。我們可以簡單認為,CAS是將比較和交換合成是一個原子操作

JVM對CAS的支持, 由於Java程序運行在JVM上,所以應對不同的硬件體系架構的處理則需要JVM來實現。在不支持CAS操作的硬件上,jvm將使用自旋鎖來實現。

CAS的ABA問題

cas操作讓我們減少了鎖帶來的性能損耗,同時也給我們帶來了新的麻煩-ABA問題。

在線程A讀取到x的值與執行CAS操作期間,線程B對x執行了兩次修改,x的值從100變成200,然後再從200變回100;而後在線程A執行CAS操作過程中並未發現x發生過變化,成功修改了x的值。由於x的值100 ->200->100,所以稱之為ABA的原因。

魔高一尺道高一丈,解決ABA的問題目前最常用的辦法就是給數據加上“版本號”,每次修改數據時同時改變版本號即可。

Q&A

在競爭比較激烈的情況下,CAS要進行回退,重試等操作才能得到正確的結果,那麼CAS一定比加鎖性能要高嗎?

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?

jwt 實踐應用以及特殊案例思考

JSON Web Token 是 出的一份標準,使用 JSON 來傳遞數據,用於判定用戶是否登錄狀態。

jwt 之前,使用 session 來做用戶認證。

以下代碼均使用 javascript 編寫。

  • 原文鏈接:

session

傳統判斷是否登錄的方式是使用 session + token

token 是指在客戶端使用 token 作為用戶狀態憑證,瀏覽器一般存儲在 localStorage 或者 cookie 中。

session 是指在服務器端使用 redis 或者 sql 類數據庫,存儲 user_id 以及 token 的鍵值對關係,基本工作原理如下。

在服務器端使用 sessions 存儲鍵值對

const sessions = {
  "ABCED1": 10086,
  "CDEFA0": 10010
}

每次客戶端請求帶權限數據時攜帶 token,在服務器端根據 token 與 sessions 獲取 user_id, 完成認證過程

function getUserIdByToken (token) {
  return sessions[token]
}

如果存儲在 cookie 中就是經常聽到的 session + cookie 的登錄方案。其實存儲在 cookielocalStorage 甚至 IndexedDB 或者 WebSQL 各有利弊,核心思想一致。

關於 cookie 以及 token 優缺點,在 中有討論。

如果不使用 cookie,可以採取 localStorage + Authorization 的方式進行認證,更加無狀態化

// http 的頭,每次請求權限接口時,需要攜帶 Authorization Header
const headers = {
  Authorization: `Bearer ${localStorage.get('token')}`
}

推薦一個前端的存儲庫 ,使用 IndexedDBWebSQL 以及 IndexedDB 做鍵值對存儲。

無狀態登錄

session 需要在數據庫中保持用戶及token對應信息,所以叫 有狀態

試想一下,如何在數據庫中不保持用戶狀態也可以登錄。

第一種方法: 前端直接傳 user_id 給服務端

缺點也特別特別明顯,容易被用戶篡改成任意 user_id,權限設置形同虛設。不過思路正確,接着往下走。

改進: 對 user_id 進行對稱加密

服務端對 user_id 進行對稱加密后,作為 token 返回客戶端,作為用戶狀態憑證。比上邊略微強點,但由於對稱加密,選擇合適的算法以及密鑰比較重要

改進: 對 user_id 不需要加密,只需要進行簽名,保證不被篡改

這便是 jwt 的思想:user_id,加密算法和簽名組成 token 一起存儲到客戶端,每當客戶端請求接口時攜帶 token,服務器根據 token 解析出加密算法與 user_id 來判斷簽名是否一致。

Json Web Token

jwt 根據 HeaderPayload 以及 Signature 三個部分由 . 拼接而成。

Header

Header 由非對稱加密算法和類型組成,如下

const header = {
  // 加密算法
  alg: 'HS256',
  type: 'jwt'
}

Payload

Payload 中由 以及需要通信的數據組成。這些數據字段也叫 Claim

Registered Claim 中比較重要的是 "exp" Claim 表示過期時間,在用戶登錄時會設置過期時間。

const payload = {
  // 表示 jwt 創建時間
  iat: 1532135735,

  // 表示 jwt 過期時間
  exp: 1532136735,

  // 用戶 id,用以通信
  user_id: 10086
}

Signature

SignatureHeaderPayload 以及 secretOrPrivateKey 計算而成。secretOrPrivateKey 作為敏感數據存儲在服務器端,可以考慮使用 vault secret 或者 k8s secret

對於 secretOrPrivateKey,如果加密算法採用 HMAC,則為字符串,如果採用 RSA 或者 ECDSA,則為 PrivateKey。

// 由 HMACSHA256 算法進行簽名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)

// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign

從生成 jwt 規則可知客戶端可以解析出 payload,因此不要在 payload 中攜帶敏感數據,比如用戶密碼

校驗過程

在生成規則中可知,jwt 前兩部分是對 header 以及 payload 的 base64 編碼。

當服務器收到客戶端的 token 后,解析前兩部分得到 header 以及 payload,並使用 header 中的算法與 secretOrPrivateKey 進行簽名,判斷與 jwt 中攜帶的簽名是否一致。

帶個問題,如何判斷 token 過期?

應用

由上可知,jwt 並不對數據進行加密,而是對數據進行簽名,保證不被篡改。除了在登錄中可以用到,在進行郵箱校驗,圖形驗證碼和短信驗證碼時也可以用到。

圖形驗證碼

在登錄時,輸入密碼錯誤次數過多會出現圖形驗證碼。

圖形驗證碼的原理是給客戶端一個圖形,並且在服務器端保存與這個圖片配對的字符串,以前也大都通過 session 來實現。

可以把驗證碼配對的字符串作為 secret,進行無狀態校驗。

const jwt = require('jsonwebtoken')

// 假設驗證碼為字符驗證碼,字符為 ACDE,10分鐘失效
const token = jwt.sign({}, secrect + 'ACDE', { expiresIn: 60 * 10 })

const codeImage = getImageFromString('ACDE')

// 給前端的響應
const res = {
  // 驗證碼圖片的 token,從中可以校驗前端發送的驗證碼
  token,
  // 驗證碼圖片
  codeImage,
}

短信驗證碼與圖形驗證碼同理

郵箱校驗

現在網站在註冊成功後會進行郵箱校驗,具體做法是給郵箱發一個鏈接,用戶點開鏈接校驗成功。

// 把郵箱以及用戶id綁定在一起
const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 })

// 在此鏈接校驗驗證碼
const link = `https://example.com/code=${code}`

無狀態 VS 有狀態

關於無狀態和有狀態,在其它技術方向也有對比,比如 React 的 stateLess component 以及 stateful component,函數式編程中的副作用可以理解為狀態,http 也是一個無狀態協議,需要靠 header 以及 cookie 攜帶狀態。

在用戶認證這裏,有無狀態是指是否依賴外部數據存儲,如 mysql,redis 等。

案例

思考以下幾個關於登錄的問題如何使用 session 以及 jwt 實現,來更加清楚 jwt 的使用場景

當用戶註銷時,如何使該 token 失效

因為 jwt 無狀態,不保存用戶設備信息,沒法單純使用它完成以上問題,可以再利用數據庫保存一些狀態完成。

  • session: 只需要把 user_id 對應的 token 清掉即可
  • jwt: 使用 redis,維護一張黑名單,用戶註銷時把該 token 加入黑名單,過期時間與 jwt 的過期時間保持一致。

如何允許用戶只能在一個設備登錄,如微信

  • session: 使用 sql 類數據庫,對用戶數據庫表添加 token 字段並加索引,每次登陸重置 token 字段,每次請求需要權限接口時,根據 token 查找 user_id
  • jwt: 假使使用 sql 類數據庫,對用戶數據庫表添加 token 字段(不需要添加索引),每次登陸重置 token 字段,每次請求需要權限接口時,根據 jwt 獲取 user_id,根據 user_id 查用戶表獲取 token 判斷 token 是否一致。另外也可以使用計數器的方法,如下一個問題。

對於這個需求,session 稍微簡單些,畢竟 jwt 也需要依賴數據庫。

如何允許用戶只能在最近五個設備登錄,如諸多播放器

  • session: 使用 sql 類數據庫,創建 token 數據庫表,有 id, token, user_id 三個字段,user 與 token 表為 1:m 關係。每次登錄添加一行記錄。根據 token 獲取 user_id,再根據 user_id 獲取該用戶有多少設備登錄,超過 5 個,則刪除最小 id 一行。
  • jwt: 使用計數器,使用 sql 類數據庫,在用戶表中添加字段 count,默認值為 0,每次登錄 count 字段自增1,每次登錄創建的 jwt 的 Payload 中攜帶數據 current_count 為用戶的 count 值。每次請求權限接口時,根據 jwt 獲取 count 以及 current_count,根據 user_id 查用戶表獲取 count,判斷與 current_count 差值是否小於 5

對於這個需求,jwt 略簡單些,而使用 session 還需要多維護一張 token 表。

如何允許用戶只能在最近五個設備登錄,而且使某一用戶踢掉除現有設備外的其它所有設備,如諸多播放器

  • session: 在上一個問題的基礎上,刪掉該設備以外其它所有的token記錄。
  • jwt: 在上一個問題的基礎上,對 count + 5,並對該設備重新賦值為新的 count。

如何显示該用戶登錄設備列表 / 如何踢掉特定用戶

  • session: 在 token 表中新加列 device
  • jwt: 需要服務器端保持設備列表信息,做法與 session 一樣,使用 jwt 意義不大

總結

從以上問題得知,如果不需要控制登錄設備數量以及設備信息,無狀態的 jwt 是一個不錯的選擇。一旦涉及到了設備信息,就需要對 jwt 添加額外的狀態支持,增加了認證的複雜度,此時選用 session 是一個不錯的選擇。

jwt 不是萬能的,是否採用 jwt,需要根據業務需求來確定。

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

小三通物流營運型態?

※快速運回,大陸空運推薦?

使用Spring安全表達式控制系統功能訪問權限

一、SPEL表達式權限控制

spring security 3.0開始已經可以使用spring Expression表達式來控制授權,允許在表達式中使用複雜的布爾邏輯來控制訪問的權限。Spring Security可用表達式對象的基類是SecurityExpressionRoot。

表達式函數 描述
hasRole([role]) 用戶擁有指定的角色時返回true (Spring security默認會帶有ROLE_前綴),去除前綴參考
hasAnyRole([role1,role2]) 用戶擁有任意一個指定的角色時返回true
hasAuthority([authority]) 擁有某資源的訪問權限時返回true
hasAnyAuthority([auth1,auth2]) 擁有某些資源其中部分資源的訪問權限時返回true
permitAll 永遠返回true
denyAll 永遠返回false
anonymous 當前用戶是anonymous時返回true
rememberMe 當前用戶是rememberMe用戶返回true
authentication 當前登錄用戶的authentication對象
fullAuthenticated 當前用戶既不是anonymous也不是rememberMe用戶時返回true
hasIpAddress('192.168.1.0/24')) 請求發送的IP匹配時返回true

部分朋友可能會對Authority和Role有些混淆。Authority作為資源訪問權限可大可小,可以是某按鈕的訪問權限(如資源ID:biz1),也可以是某類用戶角色的訪問權限(如資源ID:ADMIN)。當Authority作為角色資源權限時,hasAuthority(’ROLE_ADMIN’)與hasRole(’ADMIN’)是一樣的效果。

二、SPEL在全局配置中的使用

我們可以通過繼承WebSecurityConfigurerAdapter,實現相關的配置方法,進行全局的安全配置(之前的章節已經講過) 。下面就為大家介紹一些如何在全局配置中使用SPEL表達式。

2.1.URL安全表達式

config.antMatchers("/system/*").access("hasAuthority('ADMIN') or hasAuthority('USER')")
      .anyRequest().authenticated();

這裏我們定義了應用/person/*URL的範圍,只有擁有ADMIN或者USER權限的用戶才能訪問這些person資源。

2.2.安全表達式中引用bean

這種方式,比較適合有複雜權限驗證邏輯的情況,當Spring Security提供的默認表達式方法無法滿足我們的需求的時候。首先我們定義一個權限驗證的RbacService。

@Component("rbacService")
@Slf4j
public class RbacService {
    //返回true表示驗證通過
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        //驗證邏輯代碼
        return true;
    }
    public boolean checkUserId(Authentication authentication, int id) {
        //驗證邏輯代碼
        return true;
    }
}

對於”/person/{id}”對應的資源的訪問,調用rbacService的bean的方法checkUserId進行權限驗證,傳遞參數為authentication對象和person的id。該id為PathVariable,以#開頭表示。

config.antMatchers("/person/{id}").access("@rbacService.checkUserId(authentication,#id)")
      .anyRequest().access("@rbacService.hasPermission(request,authentication)");

三、 Method表達式安全控制

如果我們想實現方法級別的安全配置,Spring Security提供了四種註解,分別是@PreAuthorize , @PreFilter , @PostAuthorize 和 @PostFilter

3.1.開啟方法級別註解的配置

在Spring安全配置代碼中,加上EnableGlobalMethodSecurity註解,開啟方法級別安全配置功能。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

3.2 使用PreAuthorize註解

@PreAuthorize 註解適合進入方法前的權限驗證。只有擁有ADMIN角色才能訪問findAll方法。

@PreAuthorize("hasRole('ADMIN')")
List<Person> findAll();

3.3 使用PostAuthorize註解

@PostAuthorize 在方法執行后再進行權限驗證,適合根據返回值結果進行權限驗證。Spring EL 提供返回對象能夠在表達式語言中獲取返回的對象returnObject。下文代碼只有返回值的name等於authentication對象的name才能正確返回,否則拋出異常。

@PostAuthorize("returnObject.name == authentication.name")
Person findOne(Integer id);

3.4 使用PreFilter註解

PreFilter 針對參數進行過濾,下文代碼錶示針對ids參數進行過濾,只有id為偶數才能訪問delete方法。

//當有多個對象是使用filterTarget進行標註
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {

3.5 使用PostFilter 註解

PostFilter 針對返回結果進行過濾,特別適用於集合類返回值,過濾集合中不符合表達式的對象。

@PostFilter("filterObject.name == authentication.name")
List<Person> findAll();

期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

【其他文章推薦】

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

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

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

台灣寄大陸海運貨物規則及重量限制?

大陸寄台灣海運費用試算一覽表

EFCore批量操作,你真的清楚嗎

背景

EntityFramework Core有許多新的特性,其中一個重要特性便是批量操作。

批量操作意味着不需要為每次Insert/Update/Delete操作發送單獨的命令,而是在一次SQL請求中發送批量組合指令。

EFCore批量操作實踐

批處理是期待已久的功能,社區多次提出要求。現在EFCore支持開箱即用確實很棒,可以提高應用程序的性能和速度。

P1 對比實踐

下面以常見的批量插入為例,使用SQL Server Profiler 觀察實際產生並執行的SQL語句。

 還有一種關注EFCore產生的sql語句的方式,添加Nlog支持,關注Microsoft.EntityFrameworkCore.Database.Command 日誌

<logger name=”Microsoft.EntityFrameworkCore.Database.Command” minlevel=”Debug” writeTo=”sql” />

定義插入模型Category, 插入4個實體,這裏為什麼強調4,請留意下文。

    public class Category
    {
        public int Id { get; set; }
        public int CategoryID { get; set; }
        public string CategoryName { get; set; }
    }

/*EFCore 查看模型屬性,有Id使用id作為主鍵,
沒有Id,搜索public "{TableName}Id"作為主鍵,默認為int形主鍵設置標記列自增;
*/ info: Microsoft.EntityFrameworkCore.Database.Command[20100]
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [Categories] (
          [Id] int NOT NULL IDENTITY,
          [CategoryID] int NOT NULL,
          [CategoryName] nvarchar(max) NULL,
          CONSTRAINT [PK_Categories] PRIMARY KEY ([Id])
      );

using (var db = new BloggingContext())
{
  db.Categories.Add(new Category() { CategoryID = 1, CategoryName = “Clothing” });
  db.Categories.Add(new Category() { CategoryID = 2, CategoryName = “Footwear” });
  db.Categories.Add(new Category() { CategoryID = 3, CategoryName = “Accessories” });
  db.Categories.Add(new Category() { CategoryID = 4, CategoryName = “Accessories” });
  db.SaveChanges();
} 

當執行SaveChanges(), 日誌显示:

info: Microsoft.EntityFrameworkCore.Database.Command[20100]
      Executing DbCommand [Parameters=[@p0='1', @p1='Clothing' (Size = 4000), @p2='2', @p3='Footwear' (Size = 4000), @p4='3', @p5='Accessories' (Size = 4000), @p6='4', @p7='Accessories' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
      MERGE [Categories] USING (
      VALUES (@p0, @p1, 0),
      (@p2, @p3, 1),
      (@p4, @p5, 2),
      (@p6, @p7, 3)) AS i ([CategoryID], [CategoryName], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([CategoryID], [CategoryName])
      VALUES (i.[CategoryID], i.[CategoryName])
      OUTPUT INSERTED.[Id], i._Position
      INTO @inserted0;

      SELECT [t].[Id] FROM [Categories] t
      INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
      ORDER BY [i].[_Position];

從SQL Profiler追溯到的SQL:

exec sp_executesql N’SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Categories] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1),
(@p4, @p5, 2),
(@p6, @p7, 3)) AS i ([CategoryID], [CategoryName], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([CategoryID], [CategoryName])
VALUES (i.[CategoryID], i.[CategoryName])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;

SELECT [t].[Id] FROM [Categories] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];

‘,N’@p0 int,@p1 nvarchar(4000),@p2 int,@p3 nvarchar(4000),@p4 int,@p5 nvarchar(4000),@p6 int,@p7 nvarchar(4000)’,@p0=1,@p1=N’Clothing’,@p2=2,@p3=N’Footwear’,@p4=3,@p5=N’Accessories’,@p6=4,@p7=N’Accessories’

 如你所見,批量插入沒有產生4個獨立的語句,而是被組合為一個傳參存儲過程腳本(用列值作為參數);如果使用EF6執行相同的代碼,則在SQL Server Profiler中將看到4個獨立的插入語句 。

① 就性能和速度而言,EFCore批量插入更具優勢。 ② 若數據庫是針對雲部署,EF6運行這些查詢,還將產生額外的流量成本。

 經過驗證:EFCore批量更新、批量刪除功能,EFCore均發出了使用sp_executesql存儲過程+批量參數構建的SQL腳本。

 P2 深入分析

起關鍵作用的 sp_executesql存儲過程: 可以多次執行的語句或批處理 (可帶參)

-- Syntax for SQL Server, Azure SQL Database, Azure SQL Data Warehouse, Parallel Data Warehouse  
  
sp_executesql [ @stmt = ] statement  
[   
  { , [ @params = ] N'@parameter_name data_type [ OUT | OUTPUT ][ ,...n ]' }   
     { , [ @param1 = ] 'value1' [ ,...n ] }  
]  

注意官方限制: 

The amount of data that can be passed by using this method is limited by the number of parameters allowed. SQL Server procedures can have, at most, 2100 parameters. Server-side logic is required to assemble these individual values into a table variable or a temporary table for processing.       // SQL存儲過程最多可使用2100個參數

P3 豁然開朗

SqlServer  sp_executesql存儲過程最多支持2100個批量操作形成的列值參數,所以遇到很大數量的批量操作,EFCore SqlProvider會幫我們將批量操作分塊傳輸, 

實際上EFCore 對於少於4個的批量命令,不會使用sp_executesql 存儲過程,我這邊自己根據官方驗證確實如此:

估摸着EFCore使用sp_executesql 也是有點耗資源的,對於小批量(小於4條的批量操作)依舊是產生單條sql。

// 同時EFCore開放了【配置關係型數據庫批量操作大小】
protected override void OnConfiguring(DbContextOptionsBuilder optionbuilder) { string sConnString
= @"Server=localhost;Database=EFSampleDB;Trusted_Connection=true;"; optionbuilder.UseSqlServer(sConnString , b => b.MaxBatchSize(1)); // 批量操作的SQL語句數量,也可設定為1禁用批量插入 }

總結

① EFCore 相比EF6,已經支持批量操作,能有效提高應用程序的性能

② EFCore的批量操作能力,由對應的DataBaseProvider支撐(Provider實現過程跟背後的存儲載體密切相關)

      –  對於小批量操作(當前EFCore默認MinBatchSize為4》),EFCore不會啟用sp_executesql

  - 大批量關注存儲過程sp_executesql ,存儲過程的列值參數最多2100 個,這個關鍵因素決定了在大批量操作的時候 依舊會被分塊傳輸。

③ 另外一個批量操作的方法,這裏也點一下:構造Rawsql【EFCore支持Rawsql】。

  sqlite不支持存儲過程,為完成批量插入提高性能,可採用此方案。

var insertStr = new StringBuilder();
insertStr.AppendLine("insert into ProfileUsageCounters (profileid,datetime,quota,usage,natureusage) values");
var txt = insertStr.AppendLine(string.Join(',', usgaeEntities.ToList().Select(x =>
{
       return $"({x.ProfileId},{x.DateTime},{x.Quota},{x.Usage},{x.NatureUsage})";
}).ToArray()));
await _context.Database.ExecuteSqlCommandAsync(txt.ToString());

+

+

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

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

前端小白在asp.net core mvc中使用ECharts

對於在瀏覽器中繪製圖形圖表,目前有較多的js類庫可以使用,如:ChartJS,Flot,canvasjs等,但是今天介紹的主角為國產圖表庫,並在apache孵化,就是大名鼎鼎的echarts。

前方高能【官方介紹】

特性 – Apache ECharts (incubating)

ECharts,一個使用 JavaScript 實現的開源可視化庫,可以流暢的運行在 PC 和移動設備上,兼容當前絕大部分瀏覽器(IE8/9/10/11,Chrome,Firefox,Safari等),底層依賴矢量圖形庫 ZRender,提供直觀,交互豐富,可高度個性化定製的數據可視化圖表。

豐富的可視化類型

ECharts 提供了常規的折線圖、柱狀圖、散點圖、餅圖、K線圖,用於統計的盒形圖,用於地理數據可視化的地圖、熱力圖、線圖,用於關係數據可視化的關係圖、treemap、旭日圖,多維數據可視化的平行坐標,還有用於 BI 的漏斗圖,儀錶盤,並且支持圖與圖之間的混搭。

除了已經內置的包含了豐富功能的圖表,ECharts 還提供了自定義系列,只需要傳入一個renderItem函數,就可以從數據映射到任何你想要的圖形,更棒的是這些都還能和已有的交互組件結合使用而不需要操心其它事情。

你可以在下載界面下載包含所有圖表的構建文件,如果只是需要其中一兩個圖表,又嫌包含所有圖表的構建文件太大,也可以在在線構建中選擇需要的圖表類型后自定義構建。

多種數據格式無需轉換直接使用

ECharts 內置的 dataset 屬性(4.0+)支持直接傳入包括二維表,key-value 等多種格式的數據源,通過簡單的設置 encode 屬性就可以完成從數據到圖形的映射,這種方式更符合可視化的直覺,省去了大部分場景下數據轉換的步驟,而且多個組件能夠共享一份數據而不用克隆。

為了配合大數據量的展現,ECharts 還支持輸入 TypedArray 格式的數據,TypedArray 在大數據量的存儲中可以佔用更少的內存,對 GC 友好等特性也可以大幅度提升可視化應用的性能。

實踐

1、 使用VS2019創建一個asp.net core web項目,選擇ASP.NET Core Web 應用程序,並使用Web應用程序(模型視圖控制器)模板

2、使用libman添加echarts庫

在解決方案資源管理器中選擇項目名稱->wwwroot->lib,在lib文件夾上右擊,選擇:添加->客戶端庫,彈出窗口輸入echarts,自動帶出最新的版本,由於使用了cdnjs,上面最新版為4.3.0,在其他的如:jsdelivr、unpkg上面最新版為4.5.0。不過使用jsdelivr、unpkg現在4.5.0版本失敗,所以選擇了cdnjs上面的4.3.0版本。

插曲:在彈出的客戶端庫輸入內容時,如果時中文輸入法,已輸入部分文字,切換為英文輸入法時,會導致vs2019重啟,所以在客戶端庫窗口輸入內容時一定要前切換到英文輸入法

3、添加一個action,名為Echarts,並添加相應的視圖

在視圖中添加如下代碼,idmychart的div用於放置echarts圖表

<div class="row">
<div class="col-md-12">
<div id="myechart" style="width: 100%; height: 500px;">

</div>
</div>
</div>

添加相關js的引用

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/echarts/echarts.js"></script>

4、使用Echarts

準備的json數據存放到了上面:
在引用的的js庫的下方編寫js代碼使用echarts显示圖表。
首先定義了一個結構,用於存放數據。

function Series(type, xdata, seriesData) {
        this.typeName = type;//相當於title
        this.xdata = xdata;//X軸數據
        this.seriesData = seriesData;//Y軸數據
    }

聲明一個charts實例。
> echartInstance.clear
清空當前實例,會移除實例中所有的組件和圖表。清空后調用 getOption 方法返回一個{}空對象。在多次查詢時需要用到清除上次添加的內容

var myEChart = echarts.init(document.getElementById('myechart'));
myEChart.clear();//用於清除已存在的內容

定義option

    var options = {
        title: {
            text: '數據曲線:',
            x: 'center'
        },
        
        legend: {
            data: [],
            x: 'left'
        },
        
        xAxis: {
            type: 'category'//必須
        },
        yAxis: {
            type: 'value',
            max: 90,
            min: -10
        },
        series: []
    }

設置myEChart的option

myEChart.setOption(options);

獲取json數據,經過處理后並將數據显示到myEChart

    $.get('/data.json').done(function (data) {

        //var aa = JSON.parse(data);
        var x = [];
        $.each(data.List,
            function (n, value) {
                var xd = [], sd = [];
                $.each(value.Data,
                    function (m, mValue) {
                        xd.push(mValue.SaveTime);
                        sd.push(mValue.WorkStationData);
                    });
                var s = new Series(value.TypeTitleName, xd, sd);
                x.push(s);
            });

        var xStart = data.Start;
        var xEnd = data.End;
        options.xAxis.data = x[0].xdata;
        options.xAxis.start = xStart;
        options.xAxis.end = xEnd;

        $.each(x,
            function (i, value) {
                options.title.text += value.typeName + " ";
                options.legend.data.push(value.typeName);
                options.series.push({
                    name: value.typeName,
                    type: 'line',
                    smooth: true,
                    data: value.seriesData
                });
            });
        myEChart.setOption(options);

    });
說明1

此段代碼是將json數據處理后,將x,y軸的數據分開存到到數組x中

        var x = [];
        $.each(data.List,
            function (n, value) {
                var xd = [], sd = [];
                $.each(value.Data,
                    function (m, mValue) {
                        xd.push(mValue.SaveTime);
                        sd.push(mValue.WorkStationData);
                    });
                var s = new Series(value.TypeTitleName, xd, sd);
                x.push(s);
            });
說明2

對option的xAxis軸的內容進行處理

var xStart = data.Start;
var xEnd = data.End;
options.xAxis.data = x[0].xdata;
options.xAxis.start = xStart;
options.xAxis.end = xEnd;
說明3

對option的titlelegendseries軸的內容進行處理

        $.each(x,
            function (i, value) {
                options.title.text += value.typeName + " ";
                options.legend.data.push(value.typeName);
                options.series.push({
                    name: value.typeName,
                    type: 'line',
                    smooth: true,
                    data: value.seriesData
                });
            });
###### 說明4
設置`myEChart`的option

myEChart.setOption(options);


### 5、運行程序
點擊“CTRL+F5”運行程序,切換到相應的action,可以看到圖表显示如下內容,所有的數據都正確的显示了。
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121084936137-183805478.png)


到目前為止,貼出上面完整的代碼,到目前為止,你已經可以使用echarts處理圖表問題了:

### 6、功能擴展

###### 6.1 圖表左右空白較多
為了解決圖表中左右空白較多的問題,可以在options中添加如下內容:
    grid: {
        show: true,
        borderColor: '#19507c',
        x: 50,
        x2: 50,
        y: 40
    },
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121084959477-981010092.png)

###### 6.2 鼠標滑過圖表显示當前時間的各項數據內容
上面的截圖,我們使用數據劃過圖表時,不能現在當前時間上各項數據,在options中添加如下代碼可以實現鼠標滑過显示各項數據的功能。
    tooltip: {
        trigger: 'axis',
        axisPointer: {
            type: 'cross',
            animation: false,
            label: {
                backgroundColor: '#505765'
            }
        }
    },
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121085018478-908000477.png)


###### 6.3 添加相關功能按鈕
使用如下設置,添加了3個按鈕,分別為:區域縮放,區域縮放還原和還原。
區域縮放:可以在圖標上按住鼠標左鍵,用鼠標滑定一段區域,显示滑定區域的數據,相當於區域放大
區域縮放還原:還原到初始狀態
還原:就是還原功能
    toolbox: {
        feature: {
            dataZoom: {
                yAxisIndex: 'none'
            },
            restore: {},
            saveasImage: {}
        }
    },
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121085036835-316142579.png)


###### 6.4 區域縮放
dataZoom 組件 用於區域縮放,從而能自由關注細節的數據信息,或者概覽數據整體,或者去除離群點的影響。
    dataZoom: [
        {
            show: true,
            realtime: true,
            start: 80,
            end: 100
        },
        {
            type: 'inside',
            realtime: true,
            start: 65,
            end: 100
        }
    ],
![](https://img2018.cnblogs.com/blog/1746998/201911/1746998-20191121085050551-185052507.png)


添加完4項擴展后,完整代碼如下:

“`
給個關注:

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

【其他文章推薦】

※專營大陸空運台灣貨物推薦

台灣空運大陸一條龍服務

打破松下壟斷,特斯拉擬向Samsung SDI採購車用電池

美商特斯拉(Tesla)所生產的電動車所使用之車用蓄電池,至目前仍幾乎都由日本松下(Panasonic)供應,幾乎形成壟斷狀況。日媒報導,特斯拉計畫拓展電池採購對象,目前已與韓商Samsung SDI展開最終協調。

《日經中文網》一篇報導指出,特斯拉為供應需求日漸成長的純電動車所需的蓄電池,計畫推行多樣化採購對象,且與Samsung SDI的合作,目前已到了最終階段。據報導,Samsung SDI的純電動車用鋰電池已送至位於美國加州的特斯拉開發基地,但由於數量較大,因此被認為已到了正式採用前的試驗階段,而非產品試驗用品。

松下所供應的電動車用蓄電池在質、量兩方面一直占有市場優勢,因此佔據電動車市場供應的大宗。松下與特斯拉於2015年在美國內華達州展開的Gigafactory建設計畫仍在持續,且有部分產能陸續投產,預計將在今年七月底初步完工。

Gigafactory原先規劃的產能為2020年時50GWh,但由於特斯拉還發表了儲能設備Powerpack 和Powerwall ,且市場對Tesla Model 3的反應熱烈,使特斯拉客戶對鋰電池的需求大增,因此有擴增Gigafactory產量的計畫。

而目前供應純電動車用鋰電池的廠商,除了松下、Samsung SDI之外,另一主要廠商為韓國LG Chem,但供應量不大。

(照片來源:Samsung SDI)

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

※試算大陸海運運費!

現代計畫為旗下捷恩斯品牌增添一款豪華純電動車

據路透社報導,韓國現代汽車公司高級副總裁Manfred Fitzgerald稱,公司計畫為旗下捷恩斯品牌增添一款豪華純電動車,擴充該豪華品牌的產品線。

Fitzgerald在釜山車展期間向路透社記者表示,由於捷恩斯對現代來講起著推進作用,而且很顯然,電動車必須佔據著重要地位,因此我們確定將推出豪華純電動車。然而,該副總裁並未透露新車推出時間等詳細資訊。

Fitzgerald認為純電動車將是汽車行業的“未來車型。”2015年11月,現代汽車宣佈將原先的高檔子品牌Genesis設立為獨立的全球豪華品牌,此後為了樹立捷恩斯的形象,該公司先後從賓利挖來後者的設計總監呂克•東克沃爾克(Luc Donckerwolke)、外飾設計師Sangyup Lee和前蘭博基尼設計總監Manfred Fitzgerald。

捷恩斯也被現代賦以眾望,被期望增加消費者對高檔車的需求,並借助更高的價位提升不斷減少的利潤。

呂克•東克沃爾克加盟現代後任職現代設計中心高級副總裁,其表示“捷恩斯現在最主要的目標是提高與其他品牌的辨識度,不與後者有設計上的重合。”

現代在一份聲明中表示,計畫在2017年上半年推出捷恩斯G80中大型車的柴油版。在釜山車展上亮相的汽油版G80是該品牌下的第二款車型,將於7月在韓國上市。捷恩斯旗下第一款車型為G90。

Fitzgerald稱,電動車將為捷恩斯這個新品牌增添更多的推進系統,將説明該品牌在豪華車市場獲得成功。

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

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

台灣快遞大陸的貨運公司有哪些呢?

挪威擬修法禁售汽油車

做為全球電動車市佔率最高的國家,挪威政府對於推行採用電動車不遺餘力。外媒報導,挪威打算進一步修法,目標是在2025年時達成全面禁售汽油車。

挪威目前有24%的汽車屬電動車,去年登記在案的電動車超過五萬輛,且首都奧斯陸是全球電動車密度最高的城市。據外媒《獨立報》、《Dagens Naeringsliv》等媒體報導,挪威朝野四大黨的國會議員已達成立法共識,計畫在2025年前全面禁售或者大量減產汽油車;若計畫順利,挪威將成為全球第一個全面改用電動車的國家。

但由於挪威高度依賴石油產業,此決議是否成行、若成行後會對國家產業帶來何種衝擊,仍在評估當中。

對於這項提案,特斯拉(Tesla)執行長Elon Musk在推特上表示:「這國家太讚了!」若挪威正式通過這項法案,將在國際間產生示範效果,再次推高電動車的接受度。

Just heard that Norway will ban new sales of fuel cars in 2025. What an amazingly awesome country. You guys rock!!

— Elon Musk (@elonmusk)

 

(首頁圖片來源:)

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

哈雷五年內推出電動機車

經典重型機車品牌哈雷終於將正式邁向電動車行列?媒體報導,哈雷高級副總裁Sean Cumming在受訪時透漏,哈雷將在五年內推出一款貨真價實的哈雷電動機車。

《癮科技》報導哈雷曾在2014年以Project LiveWire為名推出一款電動機車原型車,行駛續航力只有96公里左右;續航力差強人意的原因是,為了兼顧車體的美觀而無法安裝體積過於龐大的電池。

Sean Cumming在受訪時表示,公司會在五年內推出電動機車,但並未透漏更多細節。由於哈雷機車車體較大,或許就能安裝電容量更大的電池;未來電池的能量密度也會更高,續航力問題也許能獲得解決。

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?