今年国庆前夕接手一个外部项目,说是要保障接口数据安全,数据安全相对容易些,接口安全嘛emmmmm, 这个要考虑加解密算法、白名单之类的问题了。于是打算今天搞一期接口安全为题的成长之路番外篇。

为什么要考虑接口安全

想必各位大佬们写一个非核心的业务接口不用花很多时间精力吧,但大家有没有考虑过一个问题:我们在开发一些系统,尤其是对公、对客系统时(如小程序、公众号等),一旦接口数据被劫持、篡改甚至恶意非法远程调用接口,那么后果将不堪设想。这时候就要仔细考虑一下接口的安全问题了。

接口安全问题有哪些

  1. 接口访问权限问题,我所了解的有登录访问权限、角色访问权限和行为访问权限

登录访问权限:
所谓的登录访问权限是指只有认证过的用户才可以有权访问接口,拒绝未认证用户的访问
常见的解决方式为shiro、spring security 框架,一劳永逸解决登录访问问题。
不常见的解决方式为aop、拦截器自定义登录检验服务模块。

角色访问权限:
所谓的角色访问权限是指只有相关的角色才有权访问,角色信息在用户登录认证之后就可获取,也就是说不同角色的用户登录后就被赋予了不同的角色访问权限。就比如说可以给某一系统设置超级管理员和普通员工两个角色。
根据粒度不同解决方式分为注解+aop和拦截器形式【上面两种访问权限如何实现的先挖个坑,下期来填】
行为访问权限:
所谓的行为访问权限是指限制某一角色进行某一行为的操作。

  1. 接口数据安全问题,分为传入参数安全问题和返回参数安全问题。

传入参数安全问题:
传入参数一般是指请求参数,一般是指请求体参数
@RequestMapping("test") public ResponseData test(@RequestBody String param1) { .... }
这里的param1就是传入参数
返回参数
返回参数一般是指接口返回前端的参数,如
public ResponseData test(@RequestBody String param1) { .... }
data: {errcode:'200',data:{}, errmsg:'插入成功!'}
这里的data就是返回参数
处理此类问题最好的办法就是进行前后端加解密。

  1. 接口访问ip地址问题

为防止出现DDOS分布式攻击,防止出现代理访问的情况

接口数据安全问题加密方式

  1. AES 对称加密(生成密钥形式进行加解密,加密密钥和解密密钥为同一把)
    特点:加解密效率高,适合加解密数据量大的内容,但安全性不高,无法认证。
  2. RSA 非对称加密 (根据算法生成公钥、私钥,公钥加密,私钥解密)
    特点:加解密效率低【因为公私钥长度较长且为模运算】,适合加解密数据量小的内容,安全性高,具备认证功能。
  3. AES + RSA 混合加密(先用AES 对明文进行加密,再用RSA公钥对AES密钥进行加密,返回密文和RSA加密后密钥)
    特点:结合了方法1、2的优点,较好的规避了方法1、2的缺点

介绍AES+RSA算法的实现原理

注:rsa 公钥加密,rsa私钥解密

AES+RSA算法后端代码

借鉴了一下这位大佬的代码
https://blog.csdn.net/qq_36360181/article/details/111871712
rsa加解密算法代码


import com.alibaba.fastjson.JSONObject;
import com.zygswo.test1.common.exception.MyException;
import lombok.extern.slf4j.Slf4j;import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** rsa 加解密工具类* @author 章瑜亮*/
@Slf4j
public class RsaEncryptUtil<T> {/*** 加密算法*/private static final String ALGO_NAME = "RSA";/*** RSA密钥长度必须是64的倍数,在512~65536之间。默认是1024*/public static final int KEY_SIZE = 2048;/*** 算法默认为RSA/NONE/PKCS1Padding,未验证*/public static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";/*** 加密请求返回对象的公钥*/private static final String PUBLIC_KEY = "";/*** 解密请求返回对象的私钥(放在配置文件中)*/private static final String PRIVATE_KEY = "";/*** 注意只能生成一次* @return 私钥公钥密钥对*/private static Map<String,String> generateKeyPairs() {try {KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGO_NAME);generator.initialize(KEY_SIZE);  //rsa长度KeyPair keyPair = generator.generateKeyPair();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();Map<String, String> map = new ConcurrentHashMap<>();map.put("public_key", Base64.getEncoder().encodeToString(publicKey.getEncoded()));map.put("private_key",Base64.getEncoder().encodeToString(privateKey.getEncoded()));return map;} catch (NoSuchAlgorithmException e) {e.printStackTrace();log.error(e.getMessage());}return null;}/*** 给AES密钥解密,再调用AES解密* @param privatekey 解密用的rsa私钥* @param encodedText 密文* @param aesKeyEncrypt rsa加密后的AES密钥* @return 解密后的明文*/public static String decryptionRsaAndAes(String privatekey, String encodedText,String aesKeyEncrypt){// 解密String aesKeyDecrypt = decodeString(privatekey, aesKeyEncrypt);//解密后aes密钥String data = AesEncryptUtil.decryptStringBySecret(encodedText, aesKeyDecrypt);return data;}/*** 调用AES加密明文(JSON OBJECT),再用RSA算法加密AES密钥* @param data 明文* @return 解密后的明文*/public static <T> Map<String, String> encryptionRsaAndAes(T data) {//加密明文log.info("-------------------data=" + JSONObject.toJSONString(data) + "---------------------");String encodedText = AesEncryptUtil.encryptString(JSONObject.toJSONString(data), AesEncryptUtil.SYMMETRY_ENCRYPT.AES);log.info("-------------------encodedText=" + encodedText + "---------------------");// 加密后aes密钥String aesKeyEncrypt = encodeString(PUBLIC_KEY, AesEncryptUtil.DEFAULT_SECRET);//解密后aes密钥Map<String, String> map = new ConcurrentHashMap<>();map.put("encodedText", encodedText);map.put("aesKeyEncrypt",aesKeyEncrypt);return map;}/*** 生成公私钥的算法(只生成一次)* @param args 参数*/
//    public static void main(String[] args) {//        Map<String,String> keyPairs = generateKeyPairs();
//        String publicKey = keyPairs.get("public_key");
//        String privateKey = keyPairs.get("private_key");
//        log.info("----------------publicKey = " + publicKey + "-------------------");
//        log.info("----------------privateKey = " + privateKey + "-------------------");
//        String encodedStr = encodeString(publicKey, "123456");
//        log.info("----------------base64Str = " + encodedStr + "-------------------");
//        String result = decodeString(privateKey, encodedStr);
//        log.info("----------------final result = " + result + "-------------------");
//    }/*** 加密算法* @param needEncodeText 需要加密的字符串* @param publicKey 加密密钥(公钥)*/private static String encodeString(String publicKey, String needEncodeText) {try {Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);//将字符串转PublicKey对象// bytes 为base64处理后的字节码,所以要先base64Encoder一下byte[] data = Base64.getDecoder().decode(publicKey.getBytes());X509EncodedKeySpec spec = new X509EncodedKeySpec(data);KeyFactory fact = KeyFactory.getInstance(ALGO_NAME);PublicKey publicKey1 = fact.generatePublic(spec);//初始化加密工具cipher.init(Cipher.ENCRYPT_MODE, publicKey1);//将需要加密的字串转字节码byte[] bytes = needEncodeText.getBytes();// 加密byte[] resultBytes = cipher.doFinal(bytes);//转base64(不清楚是否需要判断为空)return Base64.getEncoder().encodeToString(resultBytes);} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException |InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {e.printStackTrace();log.error(e.getMessage());throw new MyException(e.getMessage());}}/*** 解密算法* @param needDecodeText 需要解密的字符串* @param privateKey 解密密钥(私钥)*/private static String decodeString(String privateKey, String needDecodeText) {try {Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);//将字符串转PublicKey对象// bytes 为base64处理后的字节码,所以要先base64Encoder一下byte[] data = Base64.getDecoder().decode(privateKey.getBytes());PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(data);KeyFactory fact = KeyFactory.getInstance(ALGO_NAME);PrivateKey privateKey1 = fact.generatePrivate(spec);//初始化加密工具cipher.init(Cipher.DECRYPT_MODE, privateKey1);//将需要加密的字串转字节码//base64转bytebyte[] bytes = Base64.getDecoder().decode(needDecodeText.getBytes());// 解密byte[] resultBytes = cipher.doFinal(bytes);return resultBytes == null ? null : new String(resultBytes);} catch (Exception e) {e.printStackTrace();log.error(e.getMessage());throw new MyException("解密失败,请检查加解密密钥");}}
}

aes加解密算法代码


import com.zygswo.test1.common.exception.MyException;
import lombok.extern.slf4j.Slf4j;import javax.crypto.*;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;/*** aes加密*/
@Slf4j
public class AesEncryptUtil {//DES算法要有一个随机数源,因为Random是根据时间戳生成的有限随机数,比较容易破解,所以在这里使用SecureRandomprivate static SecureRandom secureRandom = new SecureRandom();//AES默认加密key, 长度固定为16位,代表128bit的密钥static final String DEFAULT_SECRET = "自定义加密key";//字符集private static final String CHARSET = "UTF-8";/*** 功能描述: <br>* 〈字符串解密〉<密钥参数>*/public static String decryptStringBySecret(String source, String secret) {try {byte[] sourceBytes = Base64.getDecoder().decode(source);byte[] secretBytes = secret.getBytes(CHARSET);byte[] encryptBytes = decrypt(sourceBytes, secretBytes, SYMMETRY_ENCRYPT.AES);return new String(encryptBytes, CHARSET);} catch (Exception e) {e.printStackTrace();throw new MyException("解密失败,请检查加解密密钥");}}/*** 功能描述: <br>* @param source 要加密的文本* @param encryptType 加密类型(AES,DES)* 〈字符串加密〉*/public static String encryptString(String source, SYMMETRY_ENCRYPT encryptType) {try {byte[] sourceBytes = source.getBytes(CHARSET);byte[] secretBytes = DEFAULT_SECRET.getBytes(CHARSET);byte[] encryptBytes = encrypt(sourceBytes, secretBytes, encryptType);return Base64.getEncoder().encodeToString(encryptBytes);} catch (InvalidKeyException | NoSuchAlgorithmException | UnsupportedEncodingException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) {e.printStackTrace();throw new MyException(e.getMessage());}}/*** 功能描述: <br>* 〈字符串解密〉*/public static String decryptString(String source, SYMMETRY_ENCRYPT encryptType) {try {byte[] sourceBytes = Base64.getDecoder().decode(source);byte[] secretBytes = DEFAULT_SECRET.getBytes(CHARSET);byte[] encryptBytes = decrypt(sourceBytes, secretBytes, encryptType);return new String(encryptBytes, CHARSET);} catch (InvalidKeyException | NoSuchAlgorithmException | UnsupportedEncodingException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) {e.printStackTrace();throw new MyException(e.getMessage());}}/*** 功能描述: <br>* 〈使用原始密钥数据转换为SecretKey对象〉*/private static SecretKey getSecretKey(byte[] keyBytes, String type) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {if ("DES".equals(type)) {// 创建一个密匙工厂SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(type);// 创建一个DESKeySpec对象DESKeySpec dks = new DESKeySpec(keyBytes);// 将DESKeySpec对象转换成SecretKey对象return keyFactory.generateSecret(dks);}return new SecretKeySpec(keyBytes, type);}/*** 功能描述: <br>* 〈DES加密〉*/private static byte[] encrypt(byte[] contentArray, byte[] keyArray, SYMMETRY_ENCRYPT encryptType) throws InvalidKeyException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, InvalidKeySpecException {return symmetryDE(contentArray, keyArray, Cipher.ENCRYPT_MODE, encryptType);}/*** 功能描述: <br>* 〈DES解密〉*/private static byte[] decrypt(byte[] encryptArray, byte[] keyArray, SYMMETRY_ENCRYPT encryptType) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidKeySpecException {return symmetryDE(encryptArray, keyArray, Cipher.DECRYPT_MODE, encryptType);}/*** 功能描述: <br>* 〈Cipher 加密解密操作〉*/private static byte[] symmetryDE(byte[] contentArray, byte[] keyArray, int mode, SYMMETRY_ENCRYPT encryptType) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException {//获取密钥对象SecretKey secretKey = getSecretKey(keyArray, encryptType.getType());//获取真正执行加/解密操作的CipherCipher cipher = Cipher.getInstance(encryptType.getEncrypt());//用密匙初始化Cipher对象cipher.init(mode, secretKey, secureRandom);//执行加/解密操作return cipher.doFinal(contentArray);}/*** 功能描述: <br>* 〈加密方式〉** @author Blare* @date 2019/12/11*/enum SYMMETRY_ENCRYPT {AES("AES", "AES/ECB/PKCS5Padding"),DES("DES", "DES/ECB/PKCS5Padding");SYMMETRY_ENCRYPT(String type, String encrypt) {this.type = type;this.encrypt = encrypt;}private String type;private String encrypt;public String getType() {return type;}public String getEncrypt() {return encrypt;}}
}

注意事项

之前尝试过用拦截器去进行接口传入参数解密的操作但是发现由于拦截器是在dispatcherServlet之后,调用controller之前进行操作,与@RequestBody注解一起使用会报steam closed 异常,因此最好使用过滤器进行处理。

过滤器代码:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;/*** 过滤器* @author 章瑜亮*/
@Component
@Slf4j
@WebFilter(filterName = "requestBodyFilter", urlPatterns = "/*", asyncSupported = true)
public class RequestBodyFilter implements Filter {@Overridepublic void destroy() {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response,FilterChain chain)  throws ServletException, IOException{//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。// 在chain.doFiler方法中传递新的request对象ServletRequest requestWrapper = null;if(request instanceof HttpServletRequest) {requestWrapper = new RequestWrapper((HttpServletRequest) request);}if(requestWrapper == null) {chain.doFilter(request, response);} else {chain.doFilter(requestWrapper, response);}}@Overridepublic void init(FilterConfig arg0) throws ServletException {}
}

此外,为了解决请求只能拿取一次的情况,实现了HttpServletRequestWrapper这个类
代码如下:

import com.alibaba.fastjson.JSONObject;
import com.zygswo.test1.common.exception.MyException;
import com.zygswo.test1.common.util.RsaEncryptUtil;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;/*** 确保request可以多次读取* @author 章瑜亮*/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {/*** 私钥*/private String privateKey = "";/*** 请求体*/private byte[] body;public RequestWrapper(HttpServletRequest request) {super(request);// 获取requestbody中的数据body = getBodyString(request);}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {// 定义内存中的输入流final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() throws IOException {// 使用内存输入流读取数据return bais.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}/*** 永久获取request请求头并进行请求体参数处理* @param request* @return*/private byte[] getBodyString(HttpServletRequest request)  {try (BufferedInputStream bis = new BufferedInputStream(request.getInputStream());ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int len;while ((len = bis.read(buffer)) > 0) {baos.write(buffer, 0, len);}byte[] bytes= baos.toByteArray();String body = new String(bytes);log.info("------------------- body = " + body + "------------------------");if (isNotBlank(body)) {JSONObject jsonBody = JSONObject.parseObject(body);if (null != jsonBody) {String aesKey = request.getHeader("token");String dataEncrypt = jsonBody.getString("data");String data;JSONObject json = null;try {log.info("------------------- dataEncrypt = " + dataEncrypt + "------------------------");log.info("------------------- aesKey = " + aesKey + "------------------------");data = RsaEncryptUtil.decryptionRsaAndAes(privateKey, dataEncrypt, aesKey);// 如果数据不为空就编译if (isNotBlank(data)) {//如果参数为空前端传回undefinedif ("undefined".equalsIgnoreCase(data)) {json = new JSONObject();} else {json = JSONObject.parseObject(data);}}} catch (Exception e) {log.error(e.getMessage());e.printStackTrace();throw new MyException("解密失败,请检查加解密密钥");}if (json != null) {body = json.toJSONString();}log.info("------------------- body = " + body + "------------------------");return body.getBytes();}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());throw new MyException("解密失败,请检查加解密密钥");}return null;}/*** 判断字符串是否为空* @param string 字符串* @return 是否为空*/private boolean isNotBlank(String string) {return string != null && !"".equals(string);}
}

最后只要将过滤器进行注册就可以使用啦

前端代码实现

准备工作:

  1. npm 下载 Crypto.js npm install crypto-js
  2. npm 下载 jsencrypt.min.js
import CryptoJS, { enc } from "crypto-js";
import JSEncrypt from './jsencrypt.min.js'export default {// AES密钥(加密前)aesKey : '',// AES密钥(加密后)aesKeyEncrypt: '',/*** rsa + aes加密*@param word:需要加密的内容*@returns {*} :返回加密的内容*/encryptFun:function(word) {//初始化AES密钥this.aesKey = this.get32RandomNum();//加密AES密钥var encrypt = new JSEncrypt();console.log(publicKey);encrypt.setPublicKey(publicKey);this.aesKeyEncrypt = encrypt.encrypt(this.aesKey);//AES加密密钥var key = CryptoJS.enc.Utf8.parse(this.aesKey);//AES加密内容var srcs = CryptoJS.enc.Utf8.parse(word);//AES加密var encrypted =CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding:CryptoJS.pad.Pkcs7 });//返回return encrypted.toString();},/*** rsa + aes解密*@param encodedText:需要解密的内容*@returns {*} :返回解密的内容*/decryptFun:function(encodedText) {//获取参数var resultObj = JSON.parse(encodedText);//获取加密后的aeskey 和内容var aesKeyEncrypt = resultObj['aesKeyEncrypt'];var encodedText = resultObj['encodedText'];console.log("解密前密文 ===> " + encodedText);//解密AES密钥var decrypt = new JSEncrypt();console.log(privateKey);decrypt.setPrivateKey(privateKey);var aesKey = decrypt.decrypt(aesKeyEncrypt);console.log("解密后AES密钥 ===> " + aesKey);//AES解密密钥var key = CryptoJS.enc.Utf8.parse(aesKey);//AES解密var decrypted = CryptoJS.AES.decrypt(encodedText, key, {mode: CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});//返回console.log("解密后密文 ===> " + CryptoJS.enc.Utf8.stringify(decrypted).toString());var result=CryptoJS.enc.Utf8.stringify(decrypted).toString();return JSON.parse(result);},/*** 加密参数* @param data* @returns {string}*/getEncryptData: function(data) {return JSON.stringify({"data":this.encryptFun(JSON.stringify(data))});},/*** 获取32位随机码* @returns {string}*/get32RandomNum: function(){var chars = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];var nums="";for(var i=0;i<32;i++){var id = parseInt(Math.random()*61);nums+=chars[id];}return nums;},/*** 判断url是否在验证白名单中* @param url*/verifyWhiteList: function(url) {var whiteList = ["/login"];for (var i = 0;i < whiteList.length;i++){if(url.indexOf(whiteList[i]) != -1){return true;}}return false;},/*** rsa加密*/rasEncrypt: function() {var encrypt = new JSEncrypt();encrypt.setPublicKey(this.publicKey);// 这里输出加密后的字符串console.log(encrypt.encrypt("你好asd1"));},/*** aes加密*@param word:需要加密的内容*@returns {*} :返回加密的内容*/encrypt: function(word) {varkey = CryptoJS.enc.Utf8.parse("abcdefgabcdefg12");var srcs =CryptoJS.enc.Utf8.parse(word);var encrypted =CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding:CryptoJS.pad.Pkcs7 });return encrypted.toString();},/*** aes解密* @param word* @returns {*}*/decrypt: function(word) {var key =CryptoJS.enc.Utf8.parse("abcdefgabcdefg12");var decrypt =CryptoJS.AES.decrypt(word, key, { mode: CryptoJS.mode.ECB, padding:CryptoJS.pad.Pkcs7 });returnCryptoJS.enc.Utf8.stringify(decrypt).toString();}
}

写在最后

还有个问题需要注意,那就是jre环境的security包需要修改两个jar包 local_policy.jar 和 US_export_policy.jar 以允许进行256位的密钥加解密。

踩了不少坑,终于搞定了,最后放张实现后的截图

程序猿成长之路番外篇之前后端加解密(rsa+aes混合加解密算法)相关推荐

  1. 程序猿成长之路之密码学篇-DES介绍

    上一篇讲到了密码学的定义和发展历史,讲到了一些基础理论如扩散(明文的一位可以影响密文的多位)和混淆(密钥和密文的对应关系复杂化),如信息安全系统三要素CIA(机密性.完整性.可用性)等,并简要的介绍了 ...

  2. 程序猿致富之路——(投资理财篇)

    最近笔者在闲暇之余,泡了好几周基吧(基金吧)和在股票网站模拟一番,越来越起劲,对于我们程序猿来说,写代码固然重要,学会投资理财无疑让自己多了一条路,所也重要! 基金界"一姐"王茹远 ...

  3. java20K之路(番外篇):程序员的技术KPI指标是什么呢

    由于程序员在工作时候,往往是很难衡量程序员的优秀程度的,因此借着java20K之路的专栏,写一下如何通过技术KPI来提升自己,也是在未来能够给企业带来价值的地方. 技术KPI与常规的企业管理的KPI是 ...

  4. java基础巩固-宇宙第一AiYWM:为了维持生计,编程语言番外篇之机器学习(项目预测模块总结:线性回归算法、逻辑回归分类算法)~整起

    机器学习 一.机器学习常见算法(未完待续...) 1.算法一:线性回归算法:找一条完美的直线,完美拟合所有的点,使得直线与点的误差最小 2.算法二:逻辑回归分类算法 3.算法三:贝叶斯分类算法 4.算 ...

  5. 番外篇(1)模块次序表、代数环及其检测算法

    文章目录 模块次序表 直通模块与端点模块 代数环简介 模块次序表需要注意的其它细节 缺少一次更新的加法器 直通模块交叉1:DFS的问题 直通模块交叉2:BFS的问题 重排算法与代数环检测 其它容易出错 ...

  6. 程序员面试金典——番外篇之下一个较大元素II

    程序员面试金典--番外篇之下一个较大元素II Solution1:我的答案,时间复杂度为O(n2)O(n2)O(n^2) 垃圾算法 class NextElement { public:vector& ...

  7. 程序员面试金典——番外篇之下一个较大元素I

    程序员面试金典--番外篇之下一个较大元素I Solution1:我的答案,时间复杂度为O(n2)O(n2)O(n^2) 垃圾算法 class NextElement { public:vector&l ...

  8. 【重点】程序员面试金典——番外篇之数组中的逆序对

    程序员面试金典--番外篇之数组中的逆序对 此题曾多次遇到,然鹅还是本能的想起来复杂度为O(n2)O(n2)O(n^2)的笨蛋方法... Solution1:笨蛋方法 class AntiOrder { ...

  9. 程序员面试金典——番外篇之约瑟夫问题2

    程序员面试金典--番外篇之约瑟夫问题2 参考网址:https://www.nowcoder.com/profile/9270572/codeBookDetail?submissionId=157791 ...

最新文章

  1. 成本管理系统开源_开源教科书如何降低大学成本
  2. nil slice 和 空 slice
  3. [译] 帮助你成为一名成功的 Web 开发工程师的 21 步
  4. 从零开始学视觉Transformer(3):视觉问题中的注意力机制
  5. linux 缩小链接库体积,两个奇技淫巧,将 Docker 镜像体积减小 99%
  6. 【poj3358】消因子+BSGS 或 消因子+欧拉定理 两种方法
  7. 线上故障快速定位及恢复
  8. Stateflow_after计时计数
  9. 中国的城市看多了,贴贴美国的城市,肯定对你的视觉产生冲击
  10. linux入门_Linux从入门到入土(抽奖送10本新书)
  11. 这个严重 0day 可导致79款 Netgear 路由器遭远程接管,无补丁
  12. 【POJ】1276 Cash Machine 【背包问题】
  13. gdal-3.1.2-release下载页面出错
  14. pandas获取dataframe的行数,列数,元素个数
  15. 紫光华宇拼音输入法不为人知的秘密(一)
  16. 为什么要做数据分析?数据分析给企业带来了什么?
  17. js得到今天日期、本周、本月、本季度、本年起始和结束日期
  18. linux openerp,openerp
  19. BLE射频杂散源的由来,以及常见处理办法
  20. 节俭生活-如何让机票打两折

热门文章

  1. 软件测试学习笔记(hgy)
  2. ABAQUS基本知识
  3. 04UEc++【打飞艇:无人机发射胡萝卜】
  4. 【论文笔记】Adaptive Reinforcement Learning Neural Network Control for Uncertain Nonlinear System
  5. 给玩家看贷款、黄色小说广告——不只《羊了个羊》,游戏行业的ESG踩雷有多频繁?
  6. 基于腾讯云的艺术展小程序
  7. 上硅谷mysql笔记基础视频03笔记
  8. SAP 物料移动类型
  9. 你看到的是PPT报告,我看到的是思维!
  10. Scrum Meeting [7]