SpringCloud?Gateway實(shí)現(xiàn)API接口加解密
接口范圍
所有GET請(qǐng)求 白名單除外
body 體 是 application_json 和 application_json_utf8 的 POST請(qǐng)求 白名單除外
POST url傳參也支持 白名單除外
啟用禁用/版本
后端提供獨(dú)立接口(或者現(xiàn)有接口)查詢是否需要啟用加密功能(如果后端啟用了,前端請(qǐng)求被攔截修改為為啟用,接口也無法訪問回報(bào)解密錯(cuò)誤),此接口明文傳輸
請(qǐng)求頭增加一個(gè)加密版本字段,標(biāo)識(shí)當(dāng)前的加密算法版本:crypto-version: 1.0.0
加密算法
考慮到全局加密,使用AES加密方式性能更高
加密字符串:原始數(shù)據(jù) > AES加密后的字節(jié)數(shù)組 > Base64編碼處理
解密字符串:Base64密文 > AES密文 -> 原始字符串
AES加密細(xì)節(jié):
aesKey:32/16 位由后端同一生成
iv:aesKey
mode:CBC
padding:pkcs7
js例子
//加密 static encryptAES(data, key) { const dataBytes = CryptoJS.enc.Utf8.parse(data); const keyBytes = CryptoJS.enc.Utf8.parse(key); const encrypted = CryptoJS.AES.encrypt(dataBytes, keyBytes, { iv: keyBytes, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return CryptoJS.enc.Base64.stringify(encrypted.ciphertext); }
報(bào)文格式
GET
url:/app/xx/xx?xx=1
加密處理
秘鑰:xxxxxxxxxxxxxxxx
加密文本:{"xx":1}
密文:xq4YR89LgUs4V5N5juKgW5hIsiOsCxBOwzX632S8NV4=
加密后的請(qǐng)求
/app/xx/xx?data=xq4YR89LgUs4V5N5juKgW5hIsiOsCxBOwzX632S8NV4=
POST
url:/app/xx/xx/xxx
json body:
{"xxx1":"111","xxx2":"huawei","xxx3":"789","xxx4":101,"xxx5":2}
加密處理
秘鑰:xxxxxxxxxxxxxxxx
加密文本:
{"xxx1":"111","xxx2":"huawei","xxx3":"789","xxx4":101,"xxx5":2}
密文:1oUTYvWfyaeTJ5/wJTVBqUv0Dz0IAUQTZtxSKY9WLZZl8pILP2Sozk5yOYg9I1WTvzgbbGRDGcWV1ASpYykyS1Fq5cT8s3aLXQ6NMo0AaMOC9L0aVpR863qWso5O8aG3
加密后的請(qǐng)求*
json body:
{
"data": "1oUTYvWfyaeTJ5/wJTVBqUv0Dz0IAUQTZtxSKY9WLZZl8pILP2Sozk5yOYg9I1WTvzgbbGRDGcWV1ASpYykyS1Fq5cT8s3aLXQ6NMo0AaMOjt4G9dK0WwhMGZofYuBKmdF27R8Qkr3VtZvjadtvBazJurITyE7hFcr43nlHSL5E="
}
POST url傳參 和GET格式一致
網(wǎng)關(guān)實(shí)現(xiàn)細(xì)節(jié)代碼
基于GlobalFilter 接口包裝請(qǐng)求request和響應(yīng)response,先列出關(guān)鍵代碼,完整代碼見文末
filter過濾器請(qǐng)求配置和請(qǐng)求方式分發(fā)
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (!cryptoProperties.isEnabled()) { return chain.filter(exchange); } ServerHttpRequest request = exchange.getRequest(); //校驗(yàn)請(qǐng)求路徑跳過加密 String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange); String path = exchange.getRequest().getURI().getPath(); if (isSkip(path) || isSkip(originalRequestUrl)) { return chain.filter(exchange); } HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); //后期算法升級(jí)擴(kuò)展,暫時(shí)只判斷是否相等 if (!cryptoProperties.getCryptoVersion().equals(headers.getFirst(cryptoProperties.getCryptoVersionHeader()))) { return Mono.error(new CryptoException("加密版本不支持")); } if (request.getMethod() == HttpMethod.GET) { return this.handleGetReq(exchange, chain); } else if (request.getMethod() == HttpMethod.POST && (contentType == null || MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType))) { return this.handlePostReq(exchange, chain); } else { return chain.filter(exchange); } }
Get請(qǐng)求參數(shù)解密包裝 ServerHttpRequestDecorator
//構(gòu)造查詢參數(shù)Map MultiValueMap<String, String> map = buildMultiValueMap(dataJson); //新的解密后的uri ServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map); //新的解密后的uri request ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) { @Override public MultiValueMap<String, String> getQueryParams() { return map; } };
post請(qǐng)求參數(shù)解密包裝 ServerHttpRequestDecorator
//構(gòu)造一個(gè)請(qǐng)求包裝 final MultiValueMap<String, String> finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap); ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) { @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); return httpHeaders; } //處理post url傳參解密 @Override public MultiValueMap<String, String> getQueryParams() { if (queryParamsDecrypt) { return finalQueryParamMap; } return super.getQueryParams(); } @Override public Flux<DataBuffer> getBody() { //注意: 這里需要buffer一下,拿到完整報(bào)文后再map解密 return super.getBody().buffer().map(buffer -> { DataBuffer joinDataBuffer = dataBufferFactory.join(buffer); byte[] content = new byte[joinDataBuffer.readableByteCount()]; joinDataBuffer.read(content); DataBufferUtils.release(joinDataBuffer); String decryptData = new String(content, StandardCharsets.UTF_8); log.info("post decryptData: {}", decryptData); if (!queryParamsDecrypt && StringUtils.isEmpty(decryptData)) { throw new CryptoException("參數(shù)格式錯(cuò)誤"); } else { JSONObject dataJsonObj = JSON.parseObject(decryptData); if (!queryParamsDecrypt && !dataJsonObj.containsKey(cryptoProperties.getParamName())) { throw new CryptoException("參數(shù)格式錯(cuò)誤"); } byte[] bytes = AesUtil.decryptFormBase64(dataJsonObj.getString(cryptoProperties.getParamName()), cryptoProperties.getAesKey()); return dataBufferFactory.wrap(Objects.requireNonNull(bytes)); } });
GET/POST返回值加密處理CryptoServerHttpResponseDecorator
class CryptoServerHttpResponseDecorator extends ServerHttpResponseDecorator { final DataBufferFactory bufferFactory; boolean isPass = false; public CryptoServerHttpResponseDecorator(ServerHttpResponse delegate) { super(delegate); bufferFactory = delegate.bufferFactory(); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = super.getHeaders(); //同一個(gè)請(qǐng)求此處有可能調(diào)用多次,先重置為false isPass = false; if (headers.getContentType() != null && !MediaType.APPLICATION_JSON.equals(headers.getContentType()) && !MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) { //相應(yīng)體ContentType只處理json isPass = true; } else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) { //添加version響應(yīng)頭 headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion()); } return headers; } //調(diào)用 writeWith 和 writeAndFlushWith 判斷: NettyWriteResponseFilter // application/json;charset=UTF-8 走這里 @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { if (body instanceof Flux && !isPass) { Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body; return super.writeWith(fluxBody.buffer().map(dataBuffer -> { DataBuffer joinDataBuffer = bufferFactory.join(dataBuffer); byte[] content = new byte[joinDataBuffer.readableByteCount()]; joinDataBuffer.read(content); DataBufferUtils.release(joinDataBuffer); Map<String, String> data = new HashMap<>(1); data.put(cryptoProperties.getParamName(), AesUtil.encryptToBase64(content, cryptoProperties.getAesKey())); return bufferFactory.wrap(JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8)); })); } return super.writeWith(body); } // StreamingMediaType類型:application/stream 和 application/stream+json 走這里 @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return super.writeAndFlushWith(body); } }
完整CryptoFilter實(shí)現(xiàn)
package org.xx.xx.gateway.filter; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Lists; import io.netty.buffer.ByteBufAllocator; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.xx.xx.gateway.props.CryptoProperties; import org.xx.xx.gateway.provider.RequestProvider; import org.xx.xx.gateway.provider.ResponseProvider; import org.xx.xx.gateway.util.AesUtil; import org.xx.xx.gateway.util.StringPool; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.util.AntPathMatcher; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * CryptoFilter * * @author lizheng * @version 1.0 * @date 2022/3/11 上午10:57 */ @Slf4j @RequiredArgsConstructor @Configuration @ConditionalOnProperty(value = "gateway.crypto.enabled", havingValue = "true", matchIfMissing = true) public class CryptoFilter implements GlobalFilter, Ordered { private final AntPathMatcher antPathMatcher = new AntPathMatcher(); private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); private final CryptoProperties cryptoProperties; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (!cryptoProperties.isEnabled()) { return chain.filter(exchange); } ServerHttpRequest request = exchange.getRequest(); //校驗(yàn)請(qǐng)求路徑跳過加密 String originalRequestUrl = RequestProvider.getOriginalRequestUrl(exchange); String path = exchange.getRequest().getURI().getPath(); if (isSkip(path) || isSkip(originalRequestUrl)) { return chain.filter(exchange); } HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); //后期算法升級(jí)擴(kuò)展,暫時(shí)只判斷是否相等 if (!cryptoProperties.getCryptoVersion().equals(headers.getFirst(cryptoProperties.getCryptoVersionHeader()))) { return Mono.error(new CryptoException("加密版本不支持")); } if (request.getMethod() == HttpMethod.GET) { return this.handleGetReq(exchange, chain); } else if (request.getMethod() == HttpMethod.POST && (contentType == null || MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType))) { return this.handlePostReq(exchange, chain); } else { return chain.filter(exchange); } } class CryptoServerHttpResponseDecorator extends ServerHttpResponseDecorator { final DataBufferFactory bufferFactory; boolean isPass = false; public CryptoServerHttpResponseDecorator(ServerHttpResponse delegate) { super(delegate); bufferFactory = delegate.bufferFactory(); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = super.getHeaders(); //同一個(gè)請(qǐng)求此處有可能調(diào)用多次,先重置為false isPass = false; if (headers.getContentType() != null && !MediaType.APPLICATION_JSON.equals(headers.getContentType()) && !MediaType.APPLICATION_JSON_UTF8.equals(headers.getContentType())) { //相應(yīng)體ContentType只處理json isPass = true; } else if (!headers.containsKey(cryptoProperties.getCryptoVersionHeader())) { //添加version響應(yīng)頭 headers.add(cryptoProperties.getCryptoVersionHeader(), cryptoProperties.getCryptoVersion()); } return headers; } //調(diào)用 writeWith 和 writeAndFlushWith 判斷: NettyWriteResponseFilter // application/json;charset=UTF-8 走這里 @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { if (body instanceof Flux && !isPass) { Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body; return super.writeWith(fluxBody.buffer().map(dataBuffer -> { DataBuffer joinDataBuffer = bufferFactory.join(dataBuffer); byte[] content = new byte[joinDataBuffer.readableByteCount()]; joinDataBuffer.read(content); DataBufferUtils.release(joinDataBuffer); Map<String, String> data = new HashMap<>(1); data.put(cryptoProperties.getParamName(), AesUtil.encryptToBase64(content, cryptoProperties.getAesKey())); return bufferFactory.wrap(JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8)); })); } return super.writeWith(body); } // StreamingMediaType類型:application/stream 和 application/stream+json 走這里 @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return super.writeAndFlushWith(body); } } @SneakyThrows private Mono<Void> handlePostReq(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String paramData = request.getQueryParams().getFirst(cryptoProperties.getParamName()); MultiValueMap<String, String> queryParamMap = new LinkedMultiValueMap<>(); final boolean queryParamsDecrypt = !StringUtils.isEmpty(paramData); if (queryParamsDecrypt) { String dataJson; try { //AES解密 dataJson = AesUtil.decryptFormBase64ToString(paramData, cryptoProperties.getAesKey()); } catch (Exception e) { log.error("請(qǐng)求參數(shù)解密異常: ", e); return cryptoError(exchange.getResponse(), "請(qǐng)求參數(shù)解密異常"); } //構(gòu)造查詢參數(shù)Map queryParamMap = buildMultiValueMap(dataJson); //新的解密后的uri request request = this.buildNewServerHttpRequest(request, queryParamMap); } //構(gòu)造一個(gè)請(qǐng)求包裝 final MultiValueMap<String, String> finalQueryParamMap = new LinkedMultiValueMap<>(queryParamMap); ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) { @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); return httpHeaders; } @Override public MultiValueMap<String, String> getQueryParams() { if (queryParamsDecrypt) { return finalQueryParamMap; } return super.getQueryParams(); } @Override public Flux<DataBuffer> getBody() { //注意: 這里需要buffer,拿到完整報(bào)文后再map解密 return super.getBody().buffer().map(buffer -> { DataBuffer joinDataBuffer = dataBufferFactory.join(buffer); byte[] content = new byte[joinDataBuffer.readableByteCount()]; joinDataBuffer.read(content); DataBufferUtils.release(joinDataBuffer); String decryptData = new String(content, StandardCharsets.UTF_8); log.info("post decryptData: {}", decryptData); if (!queryParamsDecrypt && StringUtils.isEmpty(decryptData)) { throw new CryptoException("參數(shù)格式錯(cuò)誤"); } else { JSONObject dataJsonObj = JSON.parseObject(decryptData); if (!queryParamsDecrypt && !dataJsonObj.containsKey(cryptoProperties.getParamName())) { throw new CryptoException("參數(shù)格式錯(cuò)誤"); } byte[] bytes = AesUtil.decryptFormBase64(dataJsonObj.getString(cryptoProperties.getParamName()), cryptoProperties.getAesKey()); return dataBufferFactory.wrap(Objects.requireNonNull(bytes)); } }); } }; return chain.filter(exchange.mutate() .request(decorator) .response(new CryptoServerHttpResponseDecorator(exchange.getResponse())) .build()); } @SneakyThrows private Mono<Void> handleGetReq(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); if (request.getQueryParams().isEmpty()) { // get無參數(shù) 不走參數(shù)解密 return chain.filter(exchange.mutate() .request(request) .response(new CryptoServerHttpResponseDecorator(exchange.getResponse())) .build()); } String paramData = request.getQueryParams().getFirst(cryptoProperties.getParamName()); if (StringUtils.isEmpty(paramData)) { //有參數(shù)但是密文字段不存在 throw new CryptoException("參數(shù)格式錯(cuò)誤"); } String dataJson; try { //AES解密 dataJson = AesUtil.decryptFormBase64ToString(paramData, cryptoProperties.getAesKey()); } catch (Exception e) { log.error("請(qǐng)求參數(shù)解密異常: ", e); return cryptoError(exchange.getResponse(), "請(qǐng)求參數(shù)解密異常"); } //構(gòu)造查詢參數(shù)Map MultiValueMap<String, String> map = buildMultiValueMap(dataJson); //新的解密后的uri ServerHttpRequest newHttpRequest = this.buildNewServerHttpRequest(request, map); //新的解密后的uri request ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(newHttpRequest) { @Override public MultiValueMap<String, String> getQueryParams() { return map; } }; return chain.filter(exchange.mutate() .request(decorator) .response(new CryptoServerHttpResponseDecorator(exchange.getResponse())) .build()); } private MultiValueMap<String, String> buildMultiValueMap(String dataJson) { JSONObject jsonObject = JSON.parseObject(dataJson); MultiValueMap<String, String> map = new LinkedMultiValueMap<>(jsonObject.size()); for (String key : jsonObject.keySet()) { map.put(key, Lists.newArrayList(jsonObject.getString(key))); } return map; } private ServerHttpRequest buildNewServerHttpRequest(ServerHttpRequest request, MultiValueMap<String, String> params) throws URISyntaxException { StringBuilder queryBuilder = new StringBuilder(); for (String key : params.keySet()) { queryBuilder.append(key); queryBuilder.append(StringPool.EQUALS); queryBuilder.append(params.getFirst(key)); queryBuilder.append(StringPool.AMPERSAND); } queryBuilder.deleteCharAt(queryBuilder.length() - 1); //經(jīng)過測(cè)試只覆蓋 ServerHttpRequest的getQueryParams路由分發(fā)之后,無法攜帶過去新的參數(shù),所以這里需要構(gòu)造一個(gè)新的解密后的uri URI uri = request.getURI(); URI newUri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), queryBuilder.toString(), uri.getFragment()); //構(gòu)造一個(gè)新的ServerHttpRequest return request.mutate().uri(newUri).build(); } private boolean isSkip(String path) { for (String pattern : cryptoProperties.getSkipPathPattern()) { if (antPathMatcher.match(pattern, path)) { return true; } } return false; } private Mono<Void> cryptoError(ServerHttpResponse resp, String msg) { resp.setStatusCode(HttpStatus.UNAUTHORIZED); resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); String result = JSON.toJSONString(ResponseProvider.unAuth(msg)); DataBuffer buffer = resp.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8)); return resp.writeWith(Flux.just(buffer)); } @Override public int getOrder() { return -200; } }
以上就是SpringCloud Gateway實(shí)現(xiàn)API接口加解密的詳細(xì)內(nèi)容,更多關(guān)于SpringCloud Gateway接口加解密的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringCloud?Gateway讀取Request?Body方式
- SpringCloud?Gateway之請(qǐng)求應(yīng)答日志打印方式
- springcloud gateway網(wǎng)關(guān)服務(wù)啟動(dòng)報(bào)錯(cuò)的解決
- SpringCloud中Gateway實(shí)現(xiàn)鑒權(quán)的方法
- 解決SpringCloud Gateway配置自定義路由404的坑
- springboot集成springCloud中g(shù)ateway時(shí)啟動(dòng)報(bào)錯(cuò)的解決
- SpringCloud?Gateway實(shí)現(xiàn)請(qǐng)求解密和響應(yīng)加密的過程解析
相關(guān)文章
springboot驗(yàn)證碼生成以及驗(yàn)證功能舉例詳解
登錄注冊(cè)是大部分系統(tǒng)需要實(shí)現(xiàn)的基本功能,同時(shí)也會(huì)對(duì)登錄驗(yàn)證增加需求,下面這篇文章主要給大家介紹了關(guān)于springboot驗(yàn)證碼生成以及驗(yàn)證功能的相關(guān)資料,需要的朋友可以參考下2023-04-04springboot+mybatis-plus基于攔截器實(shí)現(xiàn)分表的示例代碼
本文主要介紹了springboot+mybatis-plus基于攔截器實(shí)現(xiàn)分表,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11jar包運(yùn)行一段時(shí)間后莫名其妙掛掉線上問題及處理方案
這篇文章主要介紹了jar包運(yùn)行一段時(shí)間后莫名其妙掛掉線上問題及處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09java結(jié)合keytool如何實(shí)現(xiàn)非對(duì)稱加密與解密詳解
這篇文章主要給大家介紹了關(guān)于java結(jié)合keytool如何實(shí)現(xiàn)非對(duì)稱加密與解密的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08Java GZip 基于內(nèi)存實(shí)現(xiàn)壓縮和解壓的方法
這篇文章主要介紹了Java GZip 基于內(nèi)存實(shí)現(xiàn)壓縮和解壓的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08Java中transient關(guān)鍵字的詳細(xì)總結(jié)
本文要介紹的是Java中的transient關(guān)鍵字,transient是短暫的意思。對(duì)于transient 修飾的成員變量,在類的實(shí)例對(duì)象的序列化處理過程中會(huì)被忽略,感興趣的朋友可以參考閱讀2023-04-04使用Spring Boot快速構(gòu)建基于SQLite數(shù)據(jù)源的應(yīng)用
為了提供一個(gè)單包易部署的服務(wù)器應(yīng)用,考慮使用Spring Boot,因?yàn)槠浼闪薃pache Tomcat,易于運(yùn)行,免去絕大部分了服務(wù)器配置的步驟2017-08-08Sping?Security前后端分離兩種實(shí)戰(zhàn)方案
這篇文章主要介紹了Sping?Security前后端分離兩種方案,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03