使用HMAC(Play 2.0)保護REST服務

我們有HTTPS,還需要什么?

當您談論基于REST的API的安全性時,人們通常會指向HTTPS。 借助HTTPS,您可以使用每個人都熟悉的方法輕松保護您的服務免遭窺視。 但是,當您需要更高級別的安全性或HTTPS不可用時,您需要替代方法。 例如,您可能需要跟蹤每個客戶對API的使用情況,或者需要確切地知道誰在進行所有這些調用。 您可以將HTTPS與客戶端身份驗證一起使用,但這將需要設置完整的PKI基礎結構以及一種安全的方式來標識您的客戶并交換私鑰。 與基于SOAP的WS-Security服務相比,我們沒有可用于REST的標準。

解決此問題的常用方法(Microsoft,Amazon,Google和Yahoo采用此方法)是通過基于客戶端與服務之間的共享機密對消息進行簽名。 請注意,使用這種方法,我們僅對數據進行簽名,而不對數據進行加密。 在這種情況下,我們所討論的簽名通常稱為基于哈希的消息認證代碼(簡稱HMAC)。 使用HMAC,我們根據已交換的密鑰為請求創建消息認證碼(MAC)。

在本文中,我將向您展示如何為基于Play 2.0的REST服務實現此算法。 如果您使用其他技術,則步驟將幾乎相同。

HMAC方案

對于客戶端,我將僅使用基于HTTPClient的簡單應用程序。 要實現這一點,我們必須采取以下步驟:

  1. 首先,我們需要與外部客戶端交換共享機密。 通常,這是由API提供程序使用電子郵件發送給客戶端的,或者提供程序具有一個您可以在其中查找共享密鑰的網站。 請注意,此機密僅在您和服務之間共享,每個客戶端將具有不同的共享機密。 這不是像公用密鑰那樣共享的東西,
  2. 為了確保客戶端和服務在同一內容上計算簽名,我們需要對要簽名的請求進行規范化。 如果我們不這樣做,則服務器可能會以與客戶端不同的方式解釋空格,并得出簽名無效的結論。
  3. 基于此規范化消息,客戶端使用共享機密創建HMAC值。
  4. 現在,客戶端已準備好將請求發送到服務。 他將HMAC值添加到標頭中,還將一些內容標識為用戶。 例如,用戶名或其他公共值。
  5. 當服務收到請求時,它將從標頭中提取用戶名和HMAC值。
  6. 根據用戶名,服務知道應該使用哪個共享密鑰對消息進行簽名。 例如,該服務將從某處的數據存儲中檢索此信息。
  7. 現在,服務以與客戶端相同的方式對請求進行規范化,并為其自身計算HMAC值。
  8. 如果來自客戶端的HMAC與從服務器計算出的HMAC相匹配,則您將知道消息的完整性得到保證,并且客戶端就是他所說的身份。 如果提供了錯誤的用戶名,或者使用了錯誤的機密來計算標題,則HMAC值將不匹配。

要實現HMAC,我們需要做什么? 在以下部分中,我們將研究以下主題。

  • 確定用于輸入的字段。
  • 創建可以計算此HMAC的客戶端代碼并添加相應的標頭
  • 創建基于Play 2.0的攔截器來檢查HMAC標頭

確定輸入字段

我們要做的第一件事是確定HMAC計算的輸入。 下表描述了我們將包括的元素:

領域 描述
HTTP方法 使用REST,我們執行的HTTP方法定義了服務器端的行為。 對特定URL的刪除與對該URL的GET處理不同。
內容MD5 此HTTP標頭是標準HTTP標頭。 這是請求正文的MD5哈希。 如果我們將此標頭包含在HMAC代碼生成中,則會獲得一個HMAC值,該值會隨著請求正文的更改而更改。
Content-Type標頭 進行REST調用時,Content-Type標頭是重要的標頭。 服務器可以根據媒體類型對請求做出不同的響應,因此應將其包含在HMAC中。
日期標題 我們還包括創建請求以計算HMAC的日期。 在服務器端,我們可以確保日期在傳輸中沒有更改。 除此之外,我們可以在服務器上添加消息過期功能。
路徑 由于URI標識REST中的資源,因此調用的URL的路徑部分也用于HMAC計算。

我們將包括的幾乎是來自請求的以下信息:

PUT /example/resource/1
Content-Md5: uf+Fg2jkrCZgzDcznsdwLg==
Content-Type: text/plain; charset=UTF-8
Date: Tue, 26 Apr 2011 19:59:03 CEST

可用于創建HMAC簽名的客戶端代碼

在下面,您可以看到我們用來調用受HMAC保護的服務的客戶端代碼。 這只是一個基于HTTPClient的快速客戶端,我們可以使用它來測試我們的服務。

public class HMACClient {private final static String DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss z";private final static String HMAC_SHA1_ALGORITHM = "HmacSHA1";private final static String SECRET = "secretsecret";private final static String USERNAME = "jos";private static final Logger LOG = LoggerFactory.getLogger(HMACClient.class);public static void main(String[] args) throws HttpException, IOException, NoSuchAlgorithmException {HMACClient client = new HMACClient();client.makeHTTPCallUsingHMAC(USERNAME);}public void makeHTTPCallUsingHMAC(String username) throws HttpException, IOException, NoSuchAlgorithmException {String contentToEncode = "{\"comment\" : {\"message\":\"blaat\" , \"from\":\"blaat\" , \"commentFor\":123}}";String contentType = "application/vnd.geo.comment+json";//String contentType = "text/plain";String currentDate = new SimpleDateFormat(DATE_FORMAT).format(new Date());HttpPost post = new HttpPost("http://localhost:9000/resources/rest/geo/comment");StringEntity data = new StringEntity(contentToEncode,contentType,"UTF-8");post.setEntity(data);String verb = post.getMethod();String contentMd5 = calculateMD5(contentToEncode);String toSign = verb + "\n" + contentMd5 + "\n"+ data.getContentType().getValue() + "\n" + currentDate + "\n"+ post.getURI().getPath();String hmac = calculateHMAC(SECRET, toSign);post.addHeader("hmac", username + ":" + hmac);post.addHeader("Date", currentDate);post.addHeader("Content-Md5", contentMd5);HttpClient client = new DefaultHttpClient();HttpResponse response = client.execute(post);System.out.println("client response:" + response.getStatusLine().getStatusCode());}private String calculateHMAC(String secret, String data) {try {SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_SHA1_ALGORITHM);Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);mac.init(signingKey);byte[] rawHmac = mac.doFinal(data.getBytes());String result = new String(Base64.encodeBase64(rawHmac));return result;} catch (GeneralSecurityException e) {LOG.warn("Unexpected error while creating hash: " + e.getMessage(), e);throw new IllegalArgumentException();}}private String calculateMD5(String contentToEncode) throws NoSuchAlgorithmException {MessageDigest digest = MessageDigest.getInstance("MD5");digest.update(contentToEncode.getBytes());String result = new String(Base64.encodeBase64(digest.digest()));return result;}
}

然后使用HMAC算法基于共享機密創建簽名。

private String calculateHMAC(String secret, String data) {try {SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_SHA1_ALGORITHM);Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);mac.init(signingKey);byte[] rawHmac = mac.doFinal(data.getBytes());String result = new String(Base64.encodeBase64(rawHmac));return result;} catch (GeneralSecurityException e) {LOG.warn("Unexpected error while creating hash: " + e.getMessage(), e);throw new IllegalArgumentException();}}

計算完HMAC值后,我們需要將其發送到服務器。 為此,我們提供了一個自定義標頭:

post.addHeader("hmac", username + ":" + hmac);

如您所見,我們還添加了用戶名。 服務器需要使用它來確定在服務器端使用哪個密鑰來計算HMAC值。 現在,當我們運行此代碼時,將執行一個簡單的POST操作,將以下請求發送到服務器:

POST /resources/rest/geo/comment HTTP/1.1[\r][\n]
hmac: jos:+9tn0CLfxXFbzPmbYwq/KYuUSUI=[\r][\n]
Date: Mon, 26 Mar 2012 21:34:33 CEST[\r][\n]
Content-Md5: r52FDQv6V2GHN4neZBvXLQ==[\r][\n]
Content-Length: 69[\r][\n]
Content-Type: application/vnd.geo.comment+json; charset=UTF-8[\r][\n]
Host: localhost:9000[\r][\n]
Connection: Keep-Alive[\r][\n]
User-Agent: Apache-HttpClient/4.1.3 (java 1.5)[\r][\n]
[\r][\n]
{"comment" : {"message":"blaat" , "from":"blaat" , "commentFor":123}}

在Scala中實現/播放

到目前為止,我們已經看到客戶需要做什么才能為我們提供正確的標題。 服務提供商通常會提供多種語言的特定庫,用于處理消息簽名的詳細信息。 但是,正如您所看到的,手工完成并不困難。 現在,讓我們看一下服務器端,在此我們將scala與Play 2.0框架一起使用,以檢查提供的標頭是否包含正確的信息。 有關設置正確的Scala環境以測試此代碼的更多信息,請參閱我以前在scala上的帖子( http://www.smartjava.org/content/play-20-akka-rest-json-and-dependencies )。

首先要做的是設置正確的路由以支持此POST操作。 我們在conf / routes文件中執行此操作

POST /resources/rest/geo/comment   controllers.Application.addComment

這是基本的Play功能。 對/ resource / rest / geo / comment URL的所有POST調用都將傳遞到指定的控制器。 讓我們看一下該操作的樣子:

def addComment() = Authenticated {(user, request) => {// convert the supplied json to a comment objectval comment = Json.parse(request.body.asInstanceOf[String]).as[Comment]// pass the comment object to a service for processingcommentService.storeComment(comment)println(Json.toJson(comment))Status(201)}}

現在,它變得更加復雜了。 如您在上面的清單中所見,我們定義了一個addComment操作。 但是,與其直接定義這樣的動作,不如:

def processGetAllRequest() = Action {val result = service.processGetAllRequest;Ok(result).as("application/json");}

我們改為這樣定義它:

def addComment() = Authenticated {(user, request) => {

我們在這里所做的是創建一個復合動作http://www.playframework.org/documentation/2.0/ScalaActionsComposition )。 因為Scala是一種功能語言,所以我們可以輕松地做到這一點。 您在此處看到的“已認證”引用只是對簡單函數的簡單引用,該函數以另一個函數作為參數。 在“已驗證”功能中,我們將檢查HMAC簽名。 您可以將其讀為使用批注,但現在無需任何特殊構造。 因此,我們的HMAC檢查是什么樣的。

import play.api.mvc.Action
import play.api.Logger
import play.api.mvc.RequestHeader
import play.api.mvc.Request
import play.api.mvc.AnyContent
import play.api.mvc.Result
import controllers.Application._
import java.security.MessageDigest
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import org.apache.commons.codec.binary.Base64
import play.api.mvc.RawBuffer
import play.api.mvc.Codec/*** Obejct contains security actions that can be applied to a specific action called from* a controller.*/
object SecurityActions {val HMAC_HEADER = "hmac"val CONTENT_TYPE_HEADER = "content-type"val DATE_HEADER = "Date"val MD5 = "MD5"val HMACSHA1 = "HmacSHA1"/*** Function authenticated is defined as a function that takes as parameter* a function. This function takes as argumens a user and a request. The authenticated* function itself, returns a result.** This Authenticated function will extract information from the request and calculate* an HMAC value.***/def Authenticated(f: (User, Request[Any]) => Result) = {// we parse this as tolerant text, since our content type// is application/vnd.geo.comment+json, which isn't picked// up by the default body parsers. Alternative would be// to parse the RawBuffer manuallyAction(parse.tolerantText) {request =>{// get the header we're working withval sendHmac = request.headers.get(HMAC_HEADER);// Check whether we've recevied an hmac headersendHmac match {// if we've got a value that looks like our header case Some(x) if x.contains(":") && x.split(":").length == 2 => {// first part is username, second part is hashval headerParts = x.split(":");val userInfo = User.find(headerParts(0))// Retrieve all the headers we're going to use, we parse the complete // content-type header, since our client also does thisval input = List(request.method,calculateMD5(request.body),request.headers.get(CONTENT_TYPE_HEADER),request.headers.get(DATE_HEADER),request.path)// create the string that we'll have to signval toSign = input.map(a => {a match {case None => ""case a: Option[Any] => a.asInstanceOf[Option[Any]].getcase _ => a}}).mkString("\n")// use the input to calculate the hmacval calculatedHMAC = calculateHMAC(userInfo.secret, toSign)// if the supplied value and the received values are equal// return the response from the delegate action, else return// unauthorizedif (calculatedHMAC == headerParts(1)) {f(userinfo, request)} else {Unauthorized}}// All the other possibilities return to 401 case _ => Unauthorized}}}}/*** Calculate the MD5 hash for the specified content*/private def calculateMD5(content: String): String = {val digest = MessageDigest.getInstance(MD5)digest.update(content.getBytes())new String(Base64.encodeBase64(digest.digest()))}/*** Calculate the HMAC for the specified data and the supplied secret*/private def calculateHMAC(secret: String, toEncode: String): String = {val signingKey = new SecretKeySpec(secret.getBytes(), HMACSHA1)val mac = Mac.getInstance(HMACSHA1)mac.init(signingKey)val rawHmac = mac.doFinal(toEncode.getBytes())new String(Base64.encodeBase64(rawHmac))}
}

那是很多代碼,但是其中大多數將很容易理解。 “ calculateHMAC”和“ calculateMD5”方法只是圍繞Java功能的基本scala包裝器。 該類內的文檔應該足以了解正在發生的事情。 但是,我確實想在這段代碼中突出幾個有趣的概念。 首先是方法簽名:

def Authenticated(f: (User, Request[Any]) => Result) = {

這意味著Authenticated方法本身將另一個方法(或函數,如果要調用該方法)作為參數。 如果回頭看我們的路線目標,您會發現我們只是這樣做:

def addComment() = Authenticated {(user, request) => ...

現在,當調用此“已認證”方法時會發生什么? 我們要做的第一件事是檢查HMAC標頭是否存在并且格式正確:

val sendHmac = request.headers.get(HMAC_HEADER);sendHmac match {// if we've got a value that looks like our header case Some(x) if x.contains(":") && x.split(":").length == 2 => {...}// All the other possibilities return to 401 case _ => Unauthorized

我們通過對HMAC標頭使用匹配來實現。 如果它包含正確格式的值,則我們將處理標頭并以與客戶端相同的方式計算HMAC值。 如果不是,則返回401。如果HMAC值正確,則使用以下代碼將其委托給提供的函數:

if (calculatedHMAC == headerParts(1)) {f(userInfo, request)} else {Unauthorized}

就是這樣。 使用此代碼,您可以輕松地使用HMAC來檢查郵件在傳輸過程中是否已更改,以及您的客戶是否真正為您所知。 如您所見,非常簡單。 只是Play 2.0中有關JSON使用情況的一小部分便條。 如果您查看操作代碼,則可以看到我使用了標準的JSON功能:

def addComment() = Authenticated {(user, request) => {// convert the supplied json to a comment objectval comment = Json.parse(request.body.asInstanceOf[String]).as[Comment]// pass the comment object to a service for processingcommentService.storeComment(comment)println(Json.toJson(comment))Status(201)}}

首先,我們使用'json.parse'將接收到的JSON解析為'comment'類,然后存儲注釋,并將命令對象轉換回字符串值。 不是最有用的代碼,但它很好地演示了Play 2.0提供的一些JSON功能。 為了從JSON轉換為對象并再次返回,使用了一種稱為“隱式轉換”的方法。 我不會在細節上過多介紹,但是可以在這里找到很好的解釋: http : //www.codecommit.com/blog/ruby/implicit-conversions-more-powerful-t… 。 這里發生的是JSON.parse和Json.toJson方法在Comment類上尋找特定的方法。 如果無法在此處找到它,它將在其范圍內查找特定的操作。 要查看此方法如何用于JSON解析,讓我們看一下Comment類及其配套對象:

import play.api.libs.json.Format
import play.api.libs.json.JsValue
import play.api.libs.json.JsObject
import play.api.libs.json.JsString
import play.api.libs.json.JsNumber
import play.api.libs.json.JsArrayobject Comment {implicit object CommentFormat extends Format[Comment] {def reads(json: JsValue): Comment = {val root = (json \ "comment")Comment((root \ "message").as[String],(root \ "from").as[String],(root \ "commentFor").as[Long])}def writes(comment: Comment): JsValue = {JsObject(List("comment" ->JsObject(Seq("message" -> JsString(comment.message),"from" -> JsString(comment.message),"commentFor" -> JsNumber(comment.commentFor)))))}}}case class Comment(message: String, from: String, commentFor: Long) {}

您在此處看到的是,在伴隨對象中,我們創建了一個新的“格式”對象。 現在,與“ Comment”類一起使用時,JSON操作將使用此對象中的“讀取”和“寫入”操作來進行JSON轉換。 非常強大的功能,盡管有些神奇;-)有關在此示例中使用的Scala / Play環境的更多信息,請參見我以前的文章:
http://www.smartjava.org/content/play-20-akka-rest-json-and-dependencies
http://www.smartjava.org/content/using-querulous-scala-postgresql

參考:來自Smart Java博客的JCG合作伙伴 Jos Dirksen 使用HMAC(Play 2.0)保護REST服務 。


翻譯自: https://www.javacodegeeks.com/2012/04/dzoneprotect-rest-service-using-hmac.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/373004.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/373004.shtml
英文地址,請注明出處:http://en.pswp.cn/news/373004.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

安裝卡主_智能溫室四周玻璃的安裝學問還這么多

智能玻璃溫室大棚是指頂部及四周以玻璃為覆蓋材料的尖頂溫室大棚,玻璃溫室大棚這幾年的流行是由于紋絡型溫室頂部陽光板問題的抗老化方面容易出現問題。因此很多客戶為了種植獲得更高的透光率,更長的使用年限,因而多選擇全玻璃溫室大棚。那么…

String類詳解(1)

首先String是一個類。  1,實例化String類方法。 1)直接賦值:String name"haha"; 2)通過關鍵字:String namenew String("haha"); 2,String類的數據比較。 首先回顧一下,基礎數據的比較…

第六章 計算機網絡與i教案,大學計算機基礎教案第6章計算機網絡基礎與應用.docx...

廣東第二師范學院計算機科學系教案課程名稱計算機基礎I課程代碼111012003課程類型公必√□ 專必□ 專選□ 公選□授課方式講授□ 實踐□案例討論□ 上機√□考核方式考試□√ 考查□上機□√ 論文□教學總學時數16學分數1學時分配課堂講授 2 學時;實踐課 14 學時教材…

分享性能優化問題

談談性能優化問題 代碼層面:避免使用css表達式,避免使用高級選擇器,通配選擇器。 緩存利用:緩存Ajax,使用CDN,使用外部js和css文件以便緩存,添加Expires頭,服務端配置Etag&#xff0…

使用Scala,Play和Akka連接到RabbitMQ(AMQP)

在本文中,我們將研究如何從Scala連接到RabbitMQ,以便可以從應用程序中支持AMQP協議。 在此示例中,我將使用Play Framework 2.0作為容器(有關更多信息,請參閱我在該主題上的其他文章 )在其中運行應用程序&am…

阿爾法貝塔閥原理_圖總結 - 阿爾法個貝塔 - 博客園

一.思維導圖二.概念筆記圖的存儲結構1. 鄰接矩陣定義:設圖G有n (n大于等于1) 個頂點,則鄰接矩陣是一個n階方陣。當矩陣中的 [i,j] !0(下標從1開始) ,代表其對應的第i個頂點與第j個頂點是連接的特點無向圖的鄰接矩陣是對稱矩陣,n個頂點的無向圖…

WebApi Post 后臺無法獲取參數的解決方案

事件回放: 之前一段時間,公司里前端用的Angularjs 發送http請求也是用的ng的組件,后臺是.Net的WebApi 前端 var data {PArgs: {PageIndex: 0,PageSize: 8,RowsCount: 0} };$http.post("/Api/Test/ABC", data).success(function (d…

南京大學計算機系周小莉,周會群

媒體報道:南京大學周會群:用計算機聰明地做實驗Q《中國教育網絡》A周會群Q:南京大學的高性能計算中心非常特殊,分布在物理,化學、天文、地球科學四個不同的學科中,為什么采取這種模式?A&#xf…

不要慫,就是GAN (生成式對抗網絡) (五):無約束條件的 GAN 代碼與網絡的 Graph...

GAN 這個領域發展太快,日新月異,各種 GAN 層出不窮,前幾天看到一篇關于 Wasserstein GAN 的文章,講的很好,在此把它分享出來一起學習:https://zhuanlan.zhihu.com/p/25071913。相比 Wasserstein GAN &#…

用于MyBatis CRUD操作的Spring MVC 3控制器

到目前為止,我們已經為域類“ User ”創建了CRUD數據庫服務,并且還將MyBatis配置與Spring Configuration文件集成在一起。 接下來,我們將使用Spring MVC創建一個網頁,以使用MyBatis CRUD服務對數據庫執行操作。 使用MyBatis 3創建…

2pin接口耳機_拆解報告:雷柏首款真無線耳機XS200

-----我愛音頻網拆解報告第185篇-----雷柏是一家歷史悠久的鼠標和鍵盤廠商,截至目前,雷柏(rapoo)總共出了四款耳機,此前曾推出過三款藍牙耳機, 分別是S500 藍牙立體聲麥克風耳機,S200 藍牙立體聲麥克風耳機&#xff0c…

html表單中陰影,html5中input表單加邊框,陰影效果.doc

文檔介紹:CSS:input:focus{border-color:#99;}獲取焦點時改變顏色focus能同時改變寬度長度背景色…….form,p(margin-bottom:30px;margin-left:20px;).shadow,.one,.two,.three,.four,.five,.six( height:50px; width:280px; border:C;).shadow( -moz-box-shadow:C;…

帶有GSON和抽象類的JSON

經過多年使用org.json庫在Java中支持JSON數據交換格式后,我已切換到Google Gson 。 org.json是一個較低級的庫,因此您必須創建JSONObject,JSONArray,JSONString等…并執行其他低級工作。 Gson簡化了這項工作。 它提供了簡單的toJs…

深入理解javascript原型和閉包(3)——prototype原型

轉載,原文地址http://www.cnblogs.com/wangfupeng1988/p/3978131.html 既typeof之后的另一位老朋友! prototype也是我們的老朋友,即使不了解的人,也應該都聽過它的大名。如果它還是您的新朋友,我估計您也是javascript的…

python 溫度 符號_Python通過小實例入門學習---1.0(溫度轉換)

1.安裝Python 3 下載地址: Welcome to Python.org?www.python.org 2.“溫度轉換”實例:攝氏度--->華氏度 / 華氏度--->攝氏度 TempConvert.py TempStr = input("請輸入帶有符號的溫度值:") if TempStr[-1] in ["f","F"]:C = (eval(Tem…

mysql 修改root密碼

1.找到配置文件my.ini ,然后將其打開,可以選擇用記事本打開 C:\Program Files (x86)\MySQL\MySQL Server 5.0 2.打開后,搜索mysqld關鍵字,找到后,在mysqld下面添加skip-grant-tables,保存退出。 PS&#x…

聯想計算機CDROM啟動,聯想電腦光驅啟動問題?

1、開機按del鍵或f2進入bios設置(不同主板按鍵不一樣,一般是DEL,也可能是F2,可以參考下主板說明),將計算機的啟動模式調成從光盤啟動。也就是從cdrom啟動,根據主板的不同,bios設置有所差異(一般是&#xff…

沒有J2EE容器的JNDI和JPA

我們希望通過盡可能簡單的設置來測試一些JPA代碼。 計劃僅使用Java和Maven,不使用應用程序服務器或其他J2EE容器。 我們的JPA配置需要兩件事才能成功運行: 數據庫來存儲數據, JNDI訪問數據庫。 這篇文章分為兩個部分。 第一部分顯示了如何…

string 大小寫轉換

STL的algorithm庫確實給我們提供了這樣的便利&#xff0c;使用模板函數transform可以輕松解決這個問題&#xff0c;開發人員只需要提供一個函數對象&#xff0c;例如將char轉成大寫的toupper函數或者小寫的函數tolower函數。 transform原型&#xff1a; 1 #include <string&…

linux服務器上svn的log_如何在 Centos 8 / RHEL 8 上安裝和配置 VNC 服務器 | Linux 中國...

在 Centos 8 和 RHEL 8 系統中&#xff0c;默認未安裝 VNC 服務器&#xff0c;它需要手動安裝。在本文中&#xff0c;我們將通過簡單的分步指南&#xff0c;介紹如何在 Centos 8 / RHEL 8 上安裝 VNC 服務器。-- Pradeep KumarVNC(虛擬網絡計算Virtual Network Computing)服務器…