SpringBoot使用自定義註解實現簡單參數加密解密(註解+HandlerMethodArgumentResolver)

来源:https://www.cnblogs.com/yellowgg/archive/2020/04/05/12635553.html
-Advertisement-
Play Games

SpringBoot 自定義註解 參數加密解密 HandlerMethodArgumentResolver ...


前言

我黃漢三又回來了,快半年沒更新博客了,這半年來的經歷實屬不易,
疫情當頭,本人實習的公司沒有跟員工共患難,直接辭掉了很多人。
作為一個實習生,本人也被無情開除了。所以本人又得重新準備找工作了。
算了,感慨一下,本來想昨天發的,但昨天是清明,哀悼時期,就留到了今天發。

話不多說,直接進入正題吧。這段時間本人在寫畢設,學校也遲遲沒有開學的消息,屬實難頂。
本來被開了本人只想回學校安度"晚年"算了,畢竟工作可以再找,但親朋好友以後畢業了就很少見了。
所以親們,一定要珍惜身邊的人噢。

因為這篇博文是現在本地typora上面寫好再放過博客園的,格式有點不統一
博客園的markdown編輯器還不是很好用,這點有點頭疼
還有一點是代碼格式問題,複製到markdown又變亂了
我哭了,本來就亂了,再加上博客篇幅的問題一擠壓,博文就亂完了
以後更文都用markdown了,所以關於排版的問題會越來越美化一下

通過本文讀者將可以學習到以下內容

  • 註解的簡單使用和解析
  • HandlerMethodArgumentResolver相關部分知識

起因

寫畢設,這周才把後臺搭好,還有小程式端還沒開始。如題目所說,用了SpringBoot做後端搭建。
然後也當然應用了RESTful風格,當本人有一個url是/initJson/{id}的時候,直接就把用戶ID傳過來了。
本人就想能不能在前端簡單把ID加密一下,起碼不能眼睜睜看著ID直接就傳到後端。雖然只是一個畢設,
但還是稍微處理一下吧,處理的話我選擇用Base64好了。
本人現在是想把前端傳的一些簡單參數,用密文傳到後端再解密使用,避免明文傳輸。
當然在真正的環境中,肯定是使用更好的方案的。這裡只是說有那麼一種思路或者說那麼一種場景。
給大家舉個例子之後可以拋磚引玉。

過程

1.前端

前端傳參的時候,加密

 // encode是Base64加密的方法,可以自己隨便整一個
 data.password = encode(pwd);
 data.username= encode(username);

這樣子前端傳過去就是密文了。

2.後端

當參數傳到後端之後,想要把密文解析回明文,然後接下來就是本文的主旨所在了。
解密的時候,本人一開始是在介面裡面解密的。

/**
  *  此時參數接受到的內容是密文
  */
String login(String username, String password) {
       username =  Base64Util.decode(username);
       password=  Base64Util.decode(password);
}

看起來也沒啥是吧,但是萬一參數很多,或者說介面多,難道要每個介面都這麼寫一堆解密的代碼嗎。
顯然還可以改造,怎麼做?本人想到了註解,或者說想用註解試試,這樣自己也能加深對註解的學習。

2.1 註解

註解這個東西,本人當時學習的時候還以為是怎麼起作用的,原來是可以自定義的(笑哭)。
我們在本文簡單瞭解下註解吧,如果有需要,後面本人可以更新一篇關於註解的博文。
或者讀者可以自行學習瞭解一下,說到這裡,本人寫博客的理由是,網上沒有,或者網上找到的東西跟本人需要的不一樣時才會寫博客。
有的話就不寫了,以免都是同樣的東西,所以本人更新的博客並不算多,基本很久才一篇。
但好像這樣想並不對,寫博客無論是什麼內容,不僅方便自己學習也可以方便他人,
所以以後應該更新頻率會好點吧希望。

回到正題,註解有三個主要的東西

  • 註解定義(Annotation)
  • 註解類型(ElementType)
  • 註解策略(RetentionPolicy)

先來看看註解定義,很簡單

// 主要的就是 @interface  使用它定義的類型就是註解了,就跟class定義的類型是類一樣。
public @interface Base64DecodeStr {
    /**
     * 這裡可以放一些註解需要的東西
     * 像下麵這個count()的含義是解密的次數,預設為1次
     */
    int count() default 1;
}

然後再來看看註解類型

// 註解類型其實就是註解聲明在什麼地方
public enum ElementType {
    TYPE,               /* 類、介面(包括註釋類型)或枚舉聲明  */
    FIELD,              /* 欄位聲明(包括枚舉常量)  */
    METHOD,             /* 方法聲明  */
    PARAMETER,          /* 參數聲明  */
    CONSTRUCTOR,        /* 構造方法聲明  */
    LOCAL_VARIABLE,     /* 局部變數聲明  */
    ANNOTATION_TYPE,    /* 註釋類型聲明  */
    PACKAGE             /* 包聲明  */
}

// 這個Target就是這麼使用的
// 現在這個註解,本人希望它只能聲明在方法上還有參數上,別的地方聲明就會報錯
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface Base64DecodeStr {
    int count() default 1;
}

最後再來看看註解策略

public enum RetentionPolicy {
    SOURCE,  /* Annotation信息僅存在於編譯器處理期間,編譯器處理完之後就沒有該Annotation信息了*/
    CLASS,   /* 編譯器將Annotation存儲於類對應的.class文件中。預設行為  */
    RUNTIME  /* 編譯器將Annotation存儲於class文件中,並且可由JVM讀入 */
}

// 一般用第三個,RUNTIME,這樣的話程式運行中也可以使用
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Base64DecodeStr {
    int count() default 1;
}

到此為止,一個註解就定義好了。但是在什麼時候工作呢,這時我們就需要寫這個註解的解析了。
然後想想,定義這個註解的目的是,想直接在介面使用參數就是明文,所以應該在進入介面之前就把密文解密回明文並放回參數里。
這一步有什麼好辦法呢,這時候就輪到下一個主角登場了,它就是HandlerMethodArgumentResolver

2.2 HandlerMethodArgumentResolver

關於HandlerMethodArgumentResolver的作用和解析,官方是這麼寫的

/**
 * Strategy interface for resolving method parameters into argument values in
 * the context of a given request.
 * 翻譯了一下
 * 策略介面,用於在給定請求的上下文中將方法參數解析為參數值
 * @author Arjen Poutsma
 * @since 3.1
 * @see HandlerMethodReturnValueHandler
 */
public interface HandlerMethodArgumentResolver {

        /**
         * MethodParameter指的是控制器層方法的參數
         * 是否支持此介面
         * ture就會執行下麵的方法去解析
         */
	boolean supportsParameter(MethodParameter parameter);

        /**
         * 常見的寫法就是把前端的參數經過處理再複製給控制器方法的參數
         */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

所以這個介面,是很重要的,想想SpringMVC為何在控制器寫幾個註解,就能接收到參數,這個介面就是功不可沒的。
像常見的@PathVariable 就是用這個介面實現的。

本人的理解是,實現這個介面,就能在前端到後端介面之間處理方法和參數,所以剛好滿足上面的需求。
其實這個介面也是屬於SpringMVC源碼裡面常見的一個,讀者依然也可自行瞭解下,
目前本人還沒有準備要寫Spring讀源碼的文章,因為本人也還沒系統的去看過,或許以後本人看了就會更新有關博客。

繼續,有了這樣的介面就可以用來寫解析自定義註解了,細心的同學可以發現,在這裡寫註解解析,
那麼這個註解就只能是在控制層起作用了,在服務層甚至DAO層都用不了,所以如果想全局用的話,
本人想到的是可以用AOP切一下,把需要用到的地方都切起來就可以了。

實現HandlerMethodArgumentResolver介面來寫解析。

public class Base64DecodeStrResolver implements HandlerMethodArgumentResolver {

    private static final transient Logger log = LogUtils.getExceptionLogger();

    /**
     * 如果參數上有自定義註解Base64DecodeStr的話就支持解析
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Base64DecodeStr.class) 
                || parameter.hasMethodAnnotation(Base64DecodeStr.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        /**
         * 因為這個註解是作用在方法和參數上的,所以要分情況
         */
        int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
                ? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
                : parameter.getParameterAnnotation(Base64DecodeStr.class).count();
        /**
         * 如果是實體類參數,就把前端傳過來的參數構造成一個實體類
         * 在系統中本人把所有實體類都繼承了BaseEntity
         */
            if (BaseEntity.class.isAssignableFrom(parameter.getParameterType())) {
                Object obj = parameter.getParameterType().newInstance();
                webRequest.getParameterMap().forEach((k, v) -> {
                    try {
                        BeanUtils.setProperty(obj, k, decodeStr(v[0], count));
                    } catch (Exception e) {
                        log.error("參數解碼有誤", e);
                    }
                });
                // 這裡的return就會把轉化過的參數賦給控制器的方法參數
                return obj;
                // 如果是非集合類,就直接解碼返回
            } else if (!Iterable.class.isAssignableFrom(parameter.getParameterType())) {
                return decodeStr(webRequest.getParameter(parameter.getParameterName()), count);
            }
        return null;
    }

    /**
     * Base64根據次數恢復明文
     *
     * @param str   Base64加密*次之後的密文
     * @param count *次
     * @return 明文
     */
    public static String decodeStr(String str, int count) {
        for (int i = 0; i < count; i++) {
            str = Base64.decodeStr(str);
        }
        return str;
    }
}

然後註冊一下這個自定義的Resolver。
這裡就不用配置文件註冊了

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
    //region 註冊自定義HandlerMethodArgumentResolver
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(base64DecodeStrResolver());
    }

    @Bean
    public Base64DecodeStrResolver base64DecodeStrResolver() {
        return new Base64DecodeStrResolver();
    }
    //endregion
}

在控制器層使用註解。

/**
 * 先試試給方法加註解
 */
@Base64DecodeStr 
public void login(@NotBlank(message = "用戶名不能為空")  String username,
                   @NotBlank(message = "密碼不能為空") String password) {
            System.out.println(username);
            System.out.println(password);
    }

看看效果

  • 前端傳值
  • 後端接收

至此整個功能上已經實現了,我們來看下關鍵api

// 這個就是一個參數,控制層的方法參數
MethodParameter parameter
    // 常用方法
    hasMethodAnnotation()  是否有方法註解
    hasParameterAnnotation()  是否有參數註解
    getMethodAnnotation()  獲取方法註解(傳入Class可以指定)
    getParameterAnnotation() 獲取參數註解(傳入Class可以指定)
    getParameterType()  獲取參數類型


// 這個可以理解為是前端傳過來的東西,裡面可以拿到前端傳過來的密文,也就是初始值,沒有被處理過的
NativeWebRequest webRequest
    // 常用方法 其實這幾個都是同一個 基於map的操作
    getParameter()  
    getParameterMap()
    getParameterNames()
    getParameterValues()

2.3 深入探討

上面的例子是註解在方法上的,接下來試試註解在參數上。

/**
 * 註解一個參數
 */
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr  String username,
                   @NotBlank(message = "密碼不能為空") String password) {
            System.out.println(username);
            System.out.println(password);
}
/*****************輸出******************************/
username
WTBkR2VtTXpaSFpqYlZFOQ==

/**
 * 註解兩個參數
 */
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr  String username,
                   @NotBlank(message = "密碼不能為空") @Base64DecodeStr String password) {
            System.out.println(username);
            System.out.println(password);
}
/*****************輸出******************************/
username
password

可見註解在參數上也能用,接下來再來看看,同時註解在方法上和參數上,想一下。
假設方法上的註解優先,參數上的註解其次,會不會被解析兩次,
也就是說,密文先被方法註解解析成明文,然後之後被參數註解再次解析成別的東西。

/**
 * 註解方法 註解參數
 */
@Base64DecodeStr
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr  String username,
                   @NotBlank(message = "密碼不能為空") @Base64DecodeStr String password) {
            System.out.println(username);
            System.out.println(password);
}
/*****************輸出******************************/
username
password

輸出的是正確的明文,也就是說上面的假設不成立,讓我們康康是哪裡的問題。

回想一下,在解析的時候,我們都是用的webRequest的getParameter,而webRequest裡面的值是從前端拿過來的,
所以decodeStr解密都是對前端的值解密,當然會返回正確的內容(明文),所以即使是方法註解先解密了,它解密的是前端的值,
然後再到屬性註解,它解密的也是前端的值,不會出現屬性註解解密的內容是方法註解解密出來的內容。
從這點來看,確實是這麼一回事,所以即使方法註解和參數註解一起用也不會出現重覆解密的效果。

但是,這隻是一個原因,一開始本人還沒想到這個,然後就好奇打了斷點追蹤下源碼。

@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
                 // 獲取參數的resolver,參數的定位是控制器.方法.參數位置 ,所以每個parameter都是唯一的
                 // 至於重載的啊,不知道沒試過,你們可以試下,XD
		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
		if (resolver == null) {
		  throw new IllegalArgumentException("Unsupported parameter type [" +
		  parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
		}
		  return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
                // argumentResolverCache是一個緩存,map,
                // 從這裡可以看出,每個控制器方法的參數都會被緩存起來,
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                            // 調用supportsParameter看看是否支持
	                    if (resolver.supportsParameter(parameter)) {
				result = resolver;
                                // 一個參數可以有多個resolver 
	        		this.argumentResolverCache.put(parameter, result);
				break;
				}
			}
		}
		return result;          
}


所以問題再細化一點,當我們同時註解方法和參數的時候,會調用幾次getArgumentResolver()呢,
為了便於觀察,本人將註解傳不同的參數。
在那之前,先放點小插曲,就是在調試的時候發現的問題

/**
 * 註解方法
 */
@Base64DecodeStr( count = 10)
public void login(@NotBlank(message = "用戶名不能為空") String username,
                   @NotBlank(message = "密碼不能為空")  String password) {
            System.out.println(username);
            System.out.println(password);
}

進去前

parameter是獲取不到方法上這個自定義註解的。
當代碼往下走,走到supportsParameter的時候

此時又有了,無語。
什麼原因本人暫時沒找到。

言歸正傳,我們繼續調試

/**
 * 註解方法 註解全部參數
 */
@Base64DecodeStr( count = 30)
public void login(@NotBlank(message = "用戶名不能為空") @Base64DecodeStr(count = 10)  String username,
                   @NotBlank(message = "密碼不能為空") @Base64DecodeStr(count =20) String password) {
            System.out.println(username);
            System.out.println(password);
}

看看是先走方法註解還是參數註解。

  • 第一次進來

    可以看到是第一個參數username
  • 第二次進來

    依然是第一個參數username
  • 第三次進來

    看到是第二個參數password
  • 第四次進來

    也是第二個參數password

所以可以看到,根本就沒有走方法註解,或者說方法註解會走兩次,參數註解一個一次,所以總共四次,這也沒問題。
這是怎麼回事呢。要是不走方法註解,那方法註解怎麼會生效呢,後面我找到了原因

  /**
   * 原來是因為這裡,雖然不是因為方法註解進來的,但是這裡優先取的是方法註解的值,
   * 所以如果想讓屬性註解優先的話這裡改一下就行
   */
  int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
                ? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
                : parameter.getParameterAnnotation(Base64DecodeStr.class).count();

所以真相大白了,如果方法註解和屬性註解同時加上的話,會執行四次getArgumentResolver(),
其中只會調用兩次supportsParameter(),因為每個參數第二次都直接從map取到值了就不再走supportsParameter()了。

結束

至此我們完成了本次從前端到後端的旅途。
簡單總結一下。

  • 註解
    • 定義:@interface
    • 類型:TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE
    • 策略:SOURCE,CLASS,RUNTIME
  • HandlerMethodArgumentResolver
    • 作用:像攔截器一樣,在前端到後端中間的關卡
    • 兩個方法
      • supportsParameter:是否支持使用該Resolver
      • resolveArgument:Resolver想要做的事

然後關於註解解析部分也不夠完善,比如如果參數是集合類型的話應該怎麼處理,這都是後續了。

本篇內容都是本人真實遇到的問題並記錄下來,從開始想要加密加密參數到想辦法去實現這個功能,
這麼一種思路,希望能給新人一點啟示,當然本人本身也還需要不斷學習,不然都找不到工作了,我只能邊忙畢設邊擠時間複習了。
人一惆悵話就多了,嘿嘿,不啰嗦了,現在是夜裡兩點,準備睡了。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 小計劃 四月抓基礎 五月二刷重點知識,加點項目鞏固(大概三個項目的樣子) 六月抓面試,沖秋招。 四月博客會更以下內容 VUE 全套搭建小米商城(更註重前端) 三種 node.js 方式搭建個人博客(更註重後端) 深入解讀 ES6 系列 再學 JavaScript ES(6-10) 大話 HTTP ... ...
  • 路由(vue-router) 現在的應用都流行SPA應用(single page application) 傳統的項目大多使用多頁面結構,需要切換內容的時候我們往往會進行單個HTML文件的跳轉,這個時候網路、性能影響,瀏覽器會出現不定時間的空白界面,用戶體驗不好 單頁面應用就是用戶通過某些操作地址欄 ...
  • 父組件給子組件傳值 // 父<div id = "app"> <my-content></my-content></div> // 子<template id="content"> <div class="content"> 這裡是內容區域 {{ msg }} </div></template> 在 ...
  • 其實將整個網頁全局變色,無非就是三種,css直接設置,添加svg濾鏡,通過js遍歷所有標簽更改顏色,於是 1、css 直接編輯樣式,然後在需要應用的地方設置class 1 .gray { 2 -webkit-filter: grayscale(100%); 3 -moz-filter: graysc ...
  • 圖解Java設計模式之職責鏈模式 學校OA系統的採購審批項目 :需求是 傳統方案解決OA系統審批,傳統的設計方案 職責鏈模式基本介紹 職責鏈模式解決OA系統採購審批 職責鏈模式在SpringMVC框架應用的源碼 職責鏈模式的註意事項和細節 學校OA系統的採購審批項目 :需求是 採購員採購教學器材1) ...
  • 中台MongoDB應用目前數據倉庫與大數據不足數據中台以打通部門或數據孤島的統一數據平臺為基礎,構建統一數據資產體系,並以API服務方式為全渠道業務 分析+應用,提供即時交付能力的企業級數據架構金融行業技術需求技術產品關係型資料庫數據倉庫大數據NOSQL與非結構數據MongoDb存儲的優勢,多模數據... ...
  • 什麼是單例模式 單例模式(Singleton Pattern)是一個比較簡單的模式,實際應用很廣泛,比如 Spring 中的Bean實例就是一個單例對象。 文章首發於作者微信公眾號【碼猿技術專欄】設計模式:單例模式,原創不易,喜歡的支持一下!!!! 定義:確保某一個類 只有一個實例,而且自行實例化並 ...
  • 本文主要介紹 Web Scraping 的基本原理,基於Python語言,大白話,面向可愛的小白(\^ \^)。 易混淆的名稱: 很多時候,大家會把,在網上獲取Data的代碼,統稱為“爬蟲”, 但實際上,所謂的“爬蟲”,並不是特別準確,因為“爬蟲”也是分種的, 常見的“爬蟲”有兩種: 1. 網路爬蟲 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...