赞
踩
参考 API安全接口安全设计
参考 系列学习互联网安全架构第 3 篇 —— 自定义注解,防止表单重复提交
参考 API接口安全设计
参考 数据加密之RSA
参考 这个轮子让SpringBoot实现api加密So Easy
运行在外网服务器的接口暴露在整个互联网中,可能会受到各种攻击,例如恶意爬取服务器数据、恶意篡改请求数据等,因此需要一个机制去保证api接口是相对安全的。
本项目api接口的安全性主要是为了请求参数不会被篡改和防止接口被多次调用而产生脏数据,实现方案主要围绕令牌(token)、时间戳(timestamp)、签名(signature)三个机制展开设计。
自定义Generator密钥生成器:
- package com.admin.utils;
-
- import java.security.KeyPair;
- import java.security.KeyPairGenerator;
- import java.security.interfaces.RSAPrivateKey;
- import java.security.interfaces.RSAPublicKey;
-
- import org.apache.tomcat.util.codec.binary.Base64;
-
- /**
- * 描述:密钥生成器
- */
-
- public class Generator {
-
- public static final String ALGORITHM_RSA = "RSA";
-
- private static final String RSA_CHARSET = "UTF-8";
-
- public static void main(String[] args) throws Exception {
- KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM_RSA);
- keyPairGen.initialize(1024);
- // 生成密钥对
- KeyPair keyPair = keyPairGen.generateKeyPair();
- // 获取公钥
- RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
- byte[] keyBs = rsaPublicKey.getEncoded();
- String publicKey = encodeBase64(keyBs);
- System.out.println("生成的公钥:\r\n" + publicKey);
- // 获取私钥
- RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
- keyBs = rsaPrivateKey.getEncoded();
- String privateKey = encodeBase64(keyBs);
- System.out.println("生成的私钥:\r\n" + privateKey);
-
- }
-
- /**
- * 描述:byte数组转String
- *
- * @param source
- * @return
- * @throws Exception
- */
- public static String encodeBase64(byte[] source) throws Exception {
- return new String(Base64.encodeBase64(source), RSA_CHARSET);
- }
-
- /**
- * 描述:String转byte数组
- *
- * @param target
- * @return
- * @throws Exception
- */
-
- public static byte[] decodeBase64(String target) throws Exception {
- return Base64.decodeBase64(target.getBytes(RSA_CHARSET));
- }
-
- }
测试效果:
- 生成的公钥:
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoewDlVLsRQoYulINY2Szh89jWA0J7KB2RPdcMKWpX1WCPcw3wB8q/7bBrJFYr2899V59QcJpd7LRf6hEjLmjaz+7QzDhWFz2x11kqROmy0+PWt+SiSlRkGGWmfQGwUF8waAB/fzaQ5V0naDDF48XbAcvAR7DKOnYBcwpRhEHd3wIDAQAB
- 生成的私钥:
- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKh7AOVUuxFChi6Ug1jZLOHz2NYDQnsoHZE91wwpalfVYI9zDfAHyr/tsGskVivbz31Xn1Bwml3stF/qESMuaNrP7tDMOFYXPbHXWSpE6bLT49a35KJKVGQYZaZ9AbBQXzBoAH9/NpDlXSdoMMXjxdsBy8BHsMo6dgFzClGEQd3fAgMBAAECgYAXH8LQtx9x0AKgtAuPD0fEv3Y8cXgXdTsRqz4v0iNhaMz3A2CfWEJws0vqeLNHE8VXu8YHAV1+lLVxEKxHeuAzJjJlekGSTZAkfgF4Xm5mdfGuhVRF4ldHHbguQ5oT+OhdsxC3mMAUIwSjAkhuqEU/tK3quZeKt4Z/V3s2qa4fiQJBAPjtQSjHmnhEHayIaIiyWm/Nu+d3plq3tZsu1Mz8yj4oTPQ/rt4MvcMbI2G/tSXh1fzWmleQDlc1sATP27lzdn0CQQCtRJDX+gJejoJ9PMf/0Nzrqr1LH64UaBL0MrFc58vGN0UHtMkwKC1g58R5x006b3m8pp1mUNKutR9tMB/80SiLAkEArqECzTD6VNS0XI11iDBW8YhLAh8WPR4T8UHxV70fxGtRUSg77NrTZURsle5/jovYKwACVttgtB2d1kJbysYNoQJAZ/coYi+llE82hScfaqRMqyv8AUO1FJGOLfDs864yW3F2fjVAMyEoeWkYP2oTMOkKxuPCtk3w3NvZS48A4pYuGQJALU19SKEWk+JGC8tIOdO/tE2rSp/Uc+S/trbOkmSD0vA8sCetJ4jth68eNf60csGXDF2s4fZnc/8vyhkyX51gtg==
keytool口令:生成一个名称为jwt.jks、别名为jwt的RSA密钥证书
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
测试类:
- package com.jwt.test;
-
- import com.alibaba.nacos.common.codec.Base64;
- import lombok.extern.slf4j.Slf4j;
- import org.junit.Test;
- import org.junit.runner.RunWith;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
- import org.springframework.test.context.junit4.SpringRunner;
-
- import java.security.KeyPair;
- import java.security.interfaces.RSAPrivateKey;
- import java.security.interfaces.RSAPublicKey;
-
- @Slf4j
- @SpringBootTest
- @RunWith(SpringRunner.class)
- public class SecurityUtilTest {
-
- @Configuration
- public static class KeyPairConfig {
- @Bean
- public KeyPair keyPair() {
- //从classpath下的证书中获取秘钥对
- KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "yushanma".toCharArray());
- return keyStoreKeyFactory.getKeyPair("jwt", "yushanma".toCharArray());
- }
- }
-
- @Autowired
- private KeyPair keyPair;
-
- @Test
- public void test() throws Exception {
- RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
- log.info("公钥信息 => \n {} \n 公钥:{}", publicKey.toString(), encodeBase64(publicKey.getEncoded()));
-
- RSAPrivateKey priKey = (RSAPrivateKey) keyPair.getPrivate();
- log.info("私钥信息 => \n {} \n 私钥:{}", priKey.toString(), encodeBase64(priKey.getEncoded()));
- }
-
- /**
- * byte数组转String
- *
- * @param source
- * @return
- * @throws Exception
- */
- public static String encodeBase64(byte[] source) throws Exception {
- return new String(Base64.encodeBase64(source), "UTF-8");
- }
-
- }
测试效果:
- 2021-06-27 19:16:56.763 INFO [service-oauth2-auth,,,] 9660 --- [ main] com.jwt.test.SecurityUtilTest : 公钥信息 =>
- Sun RSA public key, 2048 bits
- params: null
- modulus: 20567319899170327840617709116508613419444179747880097355983274966140263504127642335368881422806411882720105980898002775462182893840998598143067464437617108429341174218868746433850709448012734699906126672691529515616716370127079548559157542234879717092569729835437000192116554597443494711257376818024341910971296796534185532013474935580582324311859637511618273147841849708608228263695599394403252262972361043417011840182980810496043340558930295790493636277300954108517771868053504903170991016889800274282430555222393346553171856856567070173388221305727470309637173891604587252568686869543168050575459512799421310277329
- public exponent: 65537
- 公钥:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAouyvwqbhaqIdIU4kRYTv43j04XtmGZywCcdV3I+Zer6hNHRad5hhN69WSEzFlTiXx2/6ktBwtNSM8wRCNjC3RQFK8BNSkaapHN5X7uQeP4TVrsl+/ow4D0Hgc6+GBcAYjKdXdxv4rkuy7s+mQVgCCA2zra4bopAZCKS6uM1f/C1Ki7UTCmTiM7LQdKSQsAio7MjZSKX6T1FAmr7AhjXxxUO80h8YInyboIhc7OyGevhicw7ebciVcz0ATRBrC/9Y4sf4OQV5t+g92vqcVGt2Rhczh8gQnEETVipAlH0W/syF0Xu2zr6HBtOxGP30Szg9jAytX4cL3G3WCwv47+WW0QIDAQAB
- 2021-06-27 19:16:56.766 INFO [service-oauth2-auth,,,] 9660 --- [ main] com.jwt.test.SecurityUtilTest : 私钥信息 =>
- SunRsaSign RSA private CRT key, 2048 bits
- params: null
- modulus: 20567319899170327840617709116508613419444179747880097355983274966140263504127642335368881422806411882720105980898002775462182893840998598143067464437617108429341174218868746433850709448012734699906126672691529515616716370127079548559157542234879717092569729835437000192116554597443494711257376818024341910971296796534185532013474935580582324311859637511618273147841849708608228263695599394403252262972361043417011840182980810496043340558930295790493636277300954108517771868053504903170991016889800274282430555222393346553171856856567070173388221305727470309637173891604587252568686869543168050575459512799421310277329
- private exponent: 19303536124600864633358183739817886254024619623908704828822363598689100937468777637800630122172549779607148921754675232596531269361731903684637376406576870157144447048272221693793691169068820840002225485409096853770911453476916292046840417212680644496451837621156444173781059146569866788065386698760658421102988458584474980507935585707721550065745072389564711878488207910700168001180802928703831440170322346912658552971247917296991274641100029517759307731419438082221755651445909904889771910842153508992367099498241878354429306538249813955752652096678699781581144570632109930711682397263860234110176066187681387955073
- 私钥:MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCi7K/CpuFqoh0hTiRFhO/jePThe2YZnLAJx1Xcj5l6vqE0dFp3mGE3r1ZITMWVOJfHb/qS0HC01IzzBEI2MLdFAUrwE1KRpqkc3lfu5B4/hNWuyX7+jDgPQeBzr4YFwBiMp1d3G/iuS7Luz6ZBWAIIDbOtrhuikBkIpLq4zV/8LUqLtRMKZOIzstB0pJCwCKjsyNlIpfpPUUCavsCGNfHFQ7zSHxgifJugiFzs7IZ6+GJzDt5tyJVzPQBNEGsL/1jix/g5BXm36D3a+pxUa3ZGFzOHyBCcQRNWKkCUfRb+zIXRe7bOvocG07EY/fRLOD2MDK1fhwvcbdYLC/jv5ZbRAgMBAAECggEBAJjp2JSzGEKC4SBPPQ/ak2RGNGAk91D2lOq4okeep4hivt6Cjh5NcIFZGXxGQfOp6BqRaPa+l+nAzIGR76r40in76p+lIwv9BiBINvPKOvGW9Q9VotG6PStkwwsRJJLlFqV8skTihebguZIWZo5R0aZJZeiOzvUmlbhdE7s7VulQ+8E24YcBUuwXGwOGFtmwymnCLZDPcjWZMWsujxEjPOoEqkUGCFDSKi7t9Ydi/D6yBhkAv7jnZggY1CnMfCt2M9nkpW6AUde+YPlKAKFltSNvEJnuYir9LI645wYUz5E8up13p4vvrDL+2X2CEwbVwEISw4aN36xqY7+J+30iw4ECgYEA8x0PcoNYv2lZlo5Tnu94aIWTEog6DhA6qykX4GUxd4P6vHsKyfIqGl1uDizhZNAOMu89wrENRWaJaSU0ZF5C37aCihizeNHbyR9gk/CkUeBaphCckTUvQcu9O784L39/OXKsGYcSmJRyVzknLVo+R+ZNrfvVjr2aLxvaQEyfb3kCgYEAq49/okjKL7QB2JufJ1eRZDoK2mROPntSc/bAFD7wwhWpX6hpD8XiGpMWBvnxKaKIE9EzPDXDnbJTbqhV3MbQfrudOYGk0OlPiO+pzoqft9uknPGXzqPT/8nY9q/QAW8nBp++hXNTO19/VwJtJ0+hye5q2FyamOM4K5NABpHAVBkCgYEAjx5beqlyNHTbhbNR7O3C7507AJzruF27fAmcAcDwxxAOKqkwp8QFHzJDWNr48XU99qQ6soOycVm0qQ568l8/dR2naY6zEPxSK+tp2o2+3mh6VOrQkPdDU7OSOjsO439mMTadtAV9YA975HdD5gILSh59OmBXz0k1HGiEKngxH9kCgYAXXRJ3qkwGlRAPTJovBGjjalgiB7j0H11KN5dO6odlFwga49dy83LoRZGhX5ZtIhpAAKRmlbfPGQLttfUDfPvV1n0B4NruLGfNcT4Bx7Ual8niKbCPzpXHZtiqN6UvHNEGwOh0ShFSq52u3sC4ssqIsnRQhMP1ADSdEo+MlXrIuQKBgFIZD+xT/fEw2O6UQ/4FJy3cIqV/6gAxwtFI4VlztEIG44TSlm9IA9WopMazuuwOZKSbb61/EVnYDQIovEE7V0DNu830cejxmREcL8xa6+rR2R6TsjEYCxxX3Gt6gP4RqrI/pZpmknqEjs+FbXRx+ThPWxWhKbAb2qiKVZNWt/Cx
-
- <dependencies>
- <!-- springboot-web-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!--nacos客户端-->
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
- </dependency>
- <!--fegin组件-->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-openfeign</artifactId>
- </dependency>
- <!-- Feign Client for loadBalancing -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-loadbalancer</artifactId>
- </dependency>
- <!-- 注解-->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>1.18.20</version>
- <scope>provided</scope>
- </dependency>
- <!-- 容错组件sentinel-->
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
- </dependency>
- </dependencies>
- package com.sign;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.cloud.client.SpringCloudApplication;
-
- @SpringCloudApplication
- public class SignApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(SignApplication.class);
- }
- }
- package com.sign.util;
-
-
- import org.apache.tomcat.util.codec.binary.Base64;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
-
- import javax.crypto.Cipher;
- import java.security.KeyFactory;
- import java.security.KeyPair;
- import java.security.PrivateKey;
- import java.security.PublicKey;
- import java.security.spec.PKCS8EncodedKeySpec;
- import java.security.spec.X509EncodedKeySpec;
-
- /**
- * 描述:安全工具类
- */
-
- public final class SecurityUtil {
-
- private static final String ALGORITHM_RSA = "RSA";
-
- private static final String RSA_CHARSET = "UTF-8";
-
- /**
- * 描述:将字符串通过RSA算法公钥加密
- *
- * @param content 需要加密的内容
- * @param pubKey 公钥
- * @return 加密后字符串
- * @throws Exception
- */
- private static String EncryptByRSAPubKey(String content, String pubKey) throws Exception {
- try {
- PublicKey publicKey = SecurityUtil.getRSAPubKey(pubKey);
- Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, publicKey);
- cipher.update(content.getBytes(RSA_CHARSET));
- return SecurityUtil.encodeBase64(cipher.doFinal());
-
- } catch (Exception e) {
- e.printStackTrace();
- throw new Exception();
-
- }
-
- }
-
- /**
- * 描述:将字符串通过RSA算法公钥解密
- *
- * @param content 需要解密的内容
- * @param pubKey 公钥
- * @return 解密后字符串
- * @throws Exception
- */
- public static String DecryptByRSAPubKey(String content, String pubKey) throws Exception {
- try {
- PublicKey publicKey = SecurityUtil.getRSAPubKey(pubKey);
- Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
- cipher.init(Cipher.DECRYPT_MODE, publicKey);
- cipher.update(SecurityUtil.decodeBase64(content));
- return new String(cipher.doFinal(), RSA_CHARSET);
- } catch (Exception e) {
- e.printStackTrace();
- throw new Exception();
- }
- }
-
- /**
- * 描述:将字符串通过RSA算法私钥加密
- *
- * @param content 需要加密的内容
- * @param priKey 私钥
- * @return 加密后字符串
- * @throws Exception
- */
- public static String EncryptByRSAPriKey(String content, String priKey) throws Exception {
- try {
- PrivateKey privateKey = SecurityUtil.getRSAPriKey(priKey);
- Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, privateKey);
- cipher.update(content.getBytes(RSA_CHARSET));
- return SecurityUtil.encodeBase64(cipher.doFinal());
-
- } catch (Exception e) {
- e.printStackTrace();
- throw new Exception();
-
- }
-
- }
-
- /**
- * 描述:将字符串通过RSA算法私钥解密
- *
- * @param content 需要解密的内容
- * @param priKey 私钥
- * @return 解密后字符串
- * @throws Exception
- */
- public static String DecryptByRSAPriKey(String content, String priKey) throws Exception {
- try {
- PrivateKey privateKey = SecurityUtil.getRSAPriKey(priKey);
- Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
- cipher.init(Cipher.DECRYPT_MODE, privateKey);
- cipher.update(SecurityUtil.decodeBase64(content));
- return new String(cipher.doFinal(), RSA_CHARSET);
-
- } catch (Exception e) {
- e.printStackTrace();
- throw new Exception();
-
- }
-
- }
-
- /**
- * 获取密钥对
- *
- * @return
- */
- private static KeyPair getKeyPair() {
- //从classpath下的证书中获取秘钥对
- KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "yushanma".toCharArray());
- return keyStoreKeyFactory.getKeyPair("jwt", "yushanma".toCharArray());
- }
-
- /**
- * 获取公钥字符串
- *
- * @return
- * @throws Exception
- */
- public static String getPublicKey() throws Exception {
- // 获取密钥对
- KeyPair keyPair = getKeyPair();
- // 获取私钥信息
- PublicKey publicKey = keyPair.getPublic();
- // byte 转 String
- return encodeBase64(publicKey.getEncoded());
- }
-
- /**
- * 获取私钥字符串
- *
- * @return
- * @throws Exception
- */
- public static String getPrivateKey() throws Exception {
- // 获取密钥对
- KeyPair keyPair = SecurityUtil.getKeyPair();
- // 获取私钥信息
- PrivateKey privateKey = keyPair.getPrivate();
- // byte 转 String
- return SecurityUtil.encodeBase64(privateKey.getEncoded());
- }
-
- /**
- * 描述:获取RSA公钥
- *
- * @return PublicKey
- * @throws Exception
- */
- private static PublicKey getRSAPubKey(String pubKey) throws Exception {
- try {
- X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(SecurityUtil.decodeBase64(pubKey));
- KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
- return keyFactory.generatePublic(publicKeySpec);
-
- } catch (Exception e) {
- e.printStackTrace();
- throw new Exception();
-
- }
-
- }
-
- /**
- * 描述:获取RSA私钥
- *
- * @param priKey 私钥
- * @return PrivateKey
- * @throws Exception
- */
- private static PrivateKey getRSAPriKey(String priKey) throws Exception {
- try {
- PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(SecurityUtil.decodeBase64(priKey));
- KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA);
- return keyFactory.generatePrivate(privateKeySpec);
-
- } catch (Exception e) {
- e.printStackTrace();
- throw new Exception();
-
- }
-
- }
-
- /**
- * base64编码
- *
- * @param source
- * @return
- * @throws Exception
- */
- public static String encodeBase64(byte[] source) throws Exception {
- return new String(Base64.encodeBase64(source), RSA_CHARSET);
-
- }
-
- /**
- * Base64解码
- *
- * @param target
- * @return
- * @throws Exception
- */
- public static byte[] decodeBase64(String target) throws Exception {
- return Base64.decodeBase64(target.getBytes(RSA_CHARSET));
-
- }
-
- public static void main(String[] args) throws Exception {
-
- String pubKey = getPublicKey();
-
- String priKey = getPrivateKey();
-
- String content = "age=18&name=yushanma";
- String s = EncryptByRSAPubKey(content, pubKey);
- System.out.println("公钥加密后:" + s);
- System.out.println("私钥解密后:" + DecryptByRSAPriKey(s, priKey));
-
- content = "age=18&name=yushanma";
- s = EncryptByRSAPriKey(content, priKey);
- System.out.println("私钥加密后:" + s);
- System.out.println("公钥解密后:" + DecryptByRSAPubKey(s, pubKey));
-
- }
- }
测试:
- 公钥加密后:Gx9epQIqlKTHaV7a57eUkGQ02egvT1FhvD0vblqau1ncmB8ZgyNTu29gM6N+UdgoNkQZyPYx490tekmttk6B6q307rY2P+7ADtJ0L4ZUflCTCrihYdFROtMI0ZdHd/zCOw47FE7n9IsChjpHdIvngJ7cvVCtzejC5E0w1lpH/5/Nb0JT3cEqdi6sI7ybePyq+jg5FQwmOloxKHJ8X1GxqxqVX7LgKBvpZsMrTnyZ9gJeWSbRhZXDe5de0TvOabdMvEPHxFaq3nqOM+seFSk1TLG/LRvAwJizetVV/RWCfz9hAFMZ+f2ThCS547zghuXGRqCNsARa/YumRexehpkNZQ==
- 私钥解密后:age=18&name=yushanma
- 私钥加密后:ci6hNfue3LWLhn3LLmEnKchtWOznTPi7bnhOta6JyxoFx8aMnSWKgsbc4+eW9KtTH9NC05Ol1z8ksur5PpyAy16en7P4fGubq3m8fRW44gxU3Lbwz/rSMJNu3YK/P/E2rXdg8i0MtGynhfQj6ox48PXCGjjaKW0U0YUWndArmd/aebJD3nERVuYfS/2l+o9slWWqKVDzjqSSoqLG31gfYtykDEjscSG5zEUGBO/vDdETcdmIHpzJOAEe6oMvpDogniFDl9br/9W8nFc/8C082yexYggIqlJoWpjF66ywfhefQMj5olT3M9sAJCrhzdlnLi8kvtTd/83c9HUk3Ui+fw==
- 公钥解密后:age=18&name=yushanma
- package com.sign.util;
-
-
- import com.alibaba.csp.sentinel.util.StringUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.codec.digest.DigestUtils;
-
- import java.io.UnsupportedEncodingException;
- import java.net.URLDecoder;
- import java.net.URLEncoder;
- import java.util.*;
-
- @Slf4j
- public class ApiUtil {
-
- /**
- * 参数排序
- *
- * @param data
- * @return
- */
- private static String getSortedContent(Map<String, String> data) {
- StringBuffer content = new StringBuffer();
- List<String> keys = new ArrayList<>(data.keySet());
- Collections.sort(keys);
- int index = 0;
- for (String key : keys) {
- String value = data.get(key);
- content.append((index == 0 ? "" : "&")).append(key).append("=").append(value);
- index++;
- }
- return content.toString();
- }
-
- /**
- * 参数加密
- *
- * @param data
- * @return
- * @throws Exception
- */
- public static String getSignature(Map<String, String> data) throws Exception {
- // 对排序好的字符串进行加密,然后转大写
- String summary = DigestUtils.md5Hex(getSortedContent(data)).toLowerCase();
- log.info("md5 summary:" + summary);
- // 对summary进行私钥加密
- return URLEncoder.encode(SecurityUtil.EncryptByRSAPriKey(summary, SecurityUtil.getPrivateKey()), "utf-8");
- }
-
- /**
- * 验证签名
- *
- * @param params
- * @return
- */
- public static boolean verifySign(Map<String, String> params, String sign, String signType) throws UnsupportedEncodingException {
- if (StringUtil.isEmpty(URLDecoder.decode(sign, "utf-8"))) {
- return false;
- }
- //暂不支持非RSA的签名
- if (StringUtil.isEmpty(signType) || !"RSA".equals(signType)) {
- return false;
- }
- //参与签名的数据
- String data = getSortedContent(params);
- log.info("sign data:" + data);
- String summary = DigestUtils.md5Hex(data).toLowerCase();
- log.info("sign summary:" + summary);
-
- String summaryDecode = null;
- try {
- summaryDecode = SecurityUtil.DecryptByRSAPubKey(URLDecoder.decode(sign, "utf-8"), SecurityUtil.getPublicKey());
- } catch (Exception e) {
- throw new RuntimeException("do_digest_error", e);
- }
- return summary.equals(summaryDecode);
- }
-
- public static void main(String[] args) throws Exception {
- Map<String,String> data = new HashMap<>();
- data.put("name","yushanma");
- data.put("age","20");
- String sign = getSignature(data);
- log.info("参数签名:{}",sign);
- String signType = "RSA";
- log.info("验证结果:{}",verifySign(data,sign,signType));
- }
- }
测试:
- 21:07:33.131 [main] INFO com.commons.util.ApiUtil - md5 summary:da034d300b12c71b6480bf232d09fe34
- 21:07:34.964 [main] INFO com.commons.util.ApiUtil - 参数签名:E8tHAwV736SJi9OU8U86%2FBJHGbJxMBgUgOkR5%2BdARtW1MO4WtcosLv97vRWKuslpfrV1AD9%2FxOD4Jm65zczUXZIjzWFcGGI8S9NODHteAC7D7EUj9Szb0%2FA%2Bn0OZac1ev0dtXCY27FGr5MpFSziLJGkAxyhG56MxmwBpsimm9o3BuQrfaaT6CYj2JNqeDZj4Aqk9HkqtCxaS5GIAx9vud6wVxzIDBBUEWzjBbh%2FxAuUIQl8AjcNC6W8yfh1%2FKli98Ghc4hesrrRXT3t6OlXkJSfb56i0n9I19ulbld0SC96RL9CI0B%2Bvc8FIkq9s272v07M9Q6u%2F1LTgSFqj01819Q%3D%3D
- 21:07:34.972 [main] INFO com.commons.util.ApiUtil - sign data:age=20&name=yushanma
- 21:07:34.973 [main] INFO com.commons.util.ApiUtil - sign summary:da034d300b12c71b6480bf232d09fe34
- 21:07:35.119 [main] INFO com.commons.util.ApiUtil - 验证结果:true
- package com.sign.controller;
-
- import com.sign.util.ApiUtil;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
-
- import javax.servlet.http.HttpServletRequest;
- import java.io.UnsupportedEncodingException;
- import java.util.HashMap;
- import java.util.Map;
-
- @RestController
- public class SignController {
-
- /**
- * 模拟前端参数签名
- * @param data
- * @return
- * @throws Exception
- */
- @GetMapping("/sign/getSign")
- public Map<String, Object> getSign(@RequestParam Map<String, String> data) throws Exception {
- // 返回信息
- Map<String, Object> result = new HashMap<>();
- result.put("code", 0);// 状态码
- result.put("msg", "success");// 信息
- String sign = ApiUtil.getSignature(data);
- result.put("data", sign);// 数据
- result.put("verify",ApiUtil.verifySign(data,sign,"RSA"));
- return result;
- }
-
- /**
- * 模拟后端校验签名
- * @param request
- * @param data
- * @return
- * @throws UnsupportedEncodingException
- */
- @GetMapping("/sign/verifySign")
- public boolean verifySign(HttpServletRequest request, @RequestParam Map<String, String> data) throws UnsupportedEncodingException {
- String sign = request.getHeader("sign");
- String signType = request.getHeader("sign_type");
- return ApiUtil.verifySign(data, sign, signType);
- }
-
- }
- server:
- port: 8011
- spring:
- application:
- name: service-sign-center
- cloud:
- nacos:
- discovery:
- server-addr: localhost:8848
- sentinel:
- transport:
- port: 9999
- dashboard: localhost:8080
这里我将sign放到header中,是因为签名signature中有些字符是特殊字符,放到parameter中可能会导致有些字符被过滤,最终在校验签名时抛出签名长度不够的错误,这个问题可以通过url编码解决:
- java中URL 的编码和解码函数 :
- java.net.URLEncoder.encode(String s)和java.net.URLDecoder.decode(String s);
-
- 在javascript 中URL 的编码和解码函数 :
- escape(String s)和unescape(String s);
-
- 编码的格式为:%加字符的ASCII码,即一个百分号%,后面跟对应字符的ASCII(16进制)码值。例如 空格的编码值是"%20"。
当参数被修改时,签名验证就会返回false:
至此,通过signature签名机制,我们可以保证请求参数不会被修改,但这不能保证接口不被重复调用,因此还需要token、timestamp来辅助。
模拟高并发测试接口:
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
- import java.util.HashMap;
- import java.util.Map;
-
- @Slf4j
- @RestController
- public class TestController {
-
- /**
- * 模拟高并发重复提交
- * @param data
- * @return
- */
- @PostMapping("/form/repeatSubmitTest")
- public Map<String, Object> repeatSubmitTest(@RequestParam Map<String, String> data) {
- // 模拟提交表单信息
- Map<String, Object> result = new HashMap<>();
- result.put("code", 0);// 状态码
- result.put("msg", "success");// 信息
- log.info("提交表单[]");
- return result;
- }
- }
开200个线程请求测试接口:
表单被重复提交:
前端在提交表单之前,先调用后端接口获取临时全局唯一的token,将token存入header,最后才提交表单。
后端生成token时,将token暂时缓存在redis中,设置一个有效期。当后端收到表单提交的请求时,先判断header的 token 是否在缓存中:如果在,则继续处理业务逻辑,并且处理完业务后,删除缓存中的 token;如果不在,说明无效或者重复提交。
redis依赖:
- <!-- redis 依赖-->
- <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- <exclusions>
- <exclusion>
- <groupId>io.lettuce</groupId>
- <artifactId>lettuce-core</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>redis.clients</groupId>
- <artifactId>jedis</artifactId>
- </dependency>
- package com.sign.util;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Component;
-
- import java.util.concurrent.TimeUnit;
-
- @Component
- public class RedisTemplateUtil {
-
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
-
- /**
- * 设置 String 对象
- * @param key
- * @param data
- * @param timeout
- * @return
- */
- public Boolean setString(String key, Object data, Long timeout) {
- if (data instanceof String) {
- if (null != timeout) {
- stringRedisTemplate.opsForValue().set(key, (String) data, timeout, TimeUnit.SECONDS);
- } else {
- stringRedisTemplate.opsForValue().set(key, (String) data);
- }
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * 获取 String 对象
- * @param key
- * @return
- */
- public Object getString(String key) {
- return stringRedisTemplate.opsForValue().get(key);
- }
-
- /**
- * 删除某个 key
- * @param key
- */
- public void delKey(String key) {
- stringRedisTemplate.delete(key);
- }
-
- }
- package com.sign.util;
-
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- import org.springframework.util.StringUtils;
-
- import java.util.Objects;
- import java.util.UUID;
-
-
- @Component
- public class TokenUtil {
-
- @Autowired
- private RedisTemplateUtil redisTemplateUtil;
-
- // 时间为 秒L ,如 30分钟 应为 60*30L ,这里设置 1分钟
- private static final Long TIMEOUT = 60L;
-
- /**
- * 生成 token
- * @return
- */
- public String getToken() {
- StringBuilder token = new StringBuilder("token_");
- token.append(UUID.randomUUID().toString().replaceAll("-", ""));
- redisTemplateUtil.setString(token.toString(), token.toString(), TIMEOUT);
- return token.toString();
- }
-
- /**
- * 判断是否有 token
- * @param tokenKey
- * @return
- */
- public Boolean findToken(String tokenKey) {
- if (Objects.nonNull(redisTemplateUtil.getString(tokenKey))) {
- String token = redisTemplateUtil.getString(tokenKey).toString();
- return !StringUtils.isEmpty(token);
- }
- return false;
- }
-
- /**
- * 删除某个 key
- * @param key
- */
- public void deleteKey(String key) {
- redisTemplateUtil.delKey(key);
- }
-
- }
获取token接口、模拟业务接口:
- package com.sign.controller;
-
- import com.sign.util.TokenUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.util.StringUtils;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.HashMap;
- import java.util.Map;
-
- @Slf4j
- @RestController
- public class tokenController {
-
- @Autowired
- private TokenUtil tokenUtil;
-
- /**
- * 获取 token
- * @return
- */
- @GetMapping("/getToken")
- public String getToken(){
- return tokenUtil.getToken();
- }
-
- /**
- * 模拟高并发重复提交,根据 token 缓存防止重复提交
- * @param data
- * @return
- */
- @PostMapping("/form/repeatSubmitTest")
- public Map<String, Object> repeatSubmitTest(HttpServletRequest request, @RequestParam Map<String, String> data) {
- // 返回信息
- Map<String, Object> result = new HashMap<>();
- // 检查 header 中的 token
- String token = request.getHeader("token");
- if(!StringUtils.isEmpty(token)) {
- if (tokenUtil.findToken(token)) {
- // 模拟提交表单信息
- // TODO Something
- result.put("code", 0);// 状态码
- result.put("msg", "success");// 信息
- log.info("提交表单[]");
- // 删除缓存中的token
- tokenUtil.deleteKey(token);
- } else {
- log.info("请勿重复提交[]");
- result.put("code", -1);// 状态码
- result.put("msg", "fail");// 信息
- }
- }else{
- log.info("表单无token[]");
- result.put("code", -1);// 状态码
- result.put("msg", "fail");// 信息
- }
- return result;
- }
- }
header无token时:业务逻辑不会执行
header带token请求:阻断重复请求
正常一次请求:正常完成业务逻辑
无论是参数签名校验还是表单token校验,以上的代码都显得太臃肿,因此采用注解与aop拦截的方式来实现参数校验和token校验,复用代码的同时,让程序变得简洁与高效。
- package com.sign.constant;
-
- /**
- * 请求参数常量
- */
- public class ReqParameterConstant {
- // 头部
- public static final String HEAD = "head";
-
- // 表单
- public static final String FORM = "form";
- }
- package com.sign.annotation;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- /**
- * 验证参数签名
- */
- @Target(value = ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface VerifySign {
- String type();
- }
- package com.sign.component;
-
- import com.sign.annotation.VerifySign;
- import com.sign.constant.ReqParameterConstant;
- import com.sign.util.ApiUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang.StringUtils;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.PrintWriter;
- import java.util.Enumeration;
- import java.util.HashMap;
- import java.util.Map;
-
- /**
- * 先定义一个切面,myPointCut,这个切面主要针对 controller 层的方法。
- * 环绕通知:aroundAOP,主要用于判断签名是否有效。
- */
- @Slf4j
- @Aspect
- @Component
- public class VerifySignAop {
-
- @Autowired
- private ApiUtil apiUtil;
-
- /**
- * 定义切入面:使用 AOP 环绕通知拦截所有访问,拦截的是 controller 层
- */
- @Pointcut("execution(public * com.sign.controller.*.*(..))")
- public void myPointCut() {
-
- }
-
- /**
- * 环绕通知,主要用于验证签名的方法上
- *
- * @param joinPoint
- * @return
- * @throws Throwable
- */
- @Around("myPointCut()")
- public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable {
-
- // 判断方法上是否有注解 @VerifySign
- MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
- VerifySign verifySign = methodSignature.getMethod().getDeclaredAnnotation(VerifySign.class);
- String sign = null;
- String signType = null;
- // 如果方法上有此注解,则拦截
- if (null != verifySign) {
-
- // 获取上下文的请求
- HttpServletRequest request = getRequest();
- String type = verifySign.type();
- log.info("type[] " + type);
- if (type.equals(ReqParameterConstant.HEAD)) {
- sign = request.getHeader("sign");
- signType = request.getHeader("sign_type");
- } else if (type.equals(ReqParameterConstant.FORM)) {
- sign = request.getParameter("sign");
- signType = request.getParameter("sign_type");
- }
- if (StringUtils.isBlank(sign)) {
- response("该请求无签名!");
- return null;
- }
- if (StringUtils.isBlank(signType) || !"RSA".equals(signType)) {
- response("无签名加密方式或签名加密方式不支持!");
- return null;
- }
- log.info("sign[] " + sign);
- // 获取请求的所有参数
- Map<String, String> data = new HashMap();
- Enumeration<String> parameterNames = request.getParameterNames();
- while (parameterNames.hasMoreElements()) {
- String key = parameterNames.nextElement();
- data.put(key, request.getParameter(key));
- }
- boolean verifyFlag = apiUtil.verifySign(data, sign, signType);
- if (!verifyFlag) {
- response("无效的签名!");
- return null;
- } else {
- log.info("有效的签名。");
- }
- }
-
- // 继续往后执行
- Object proceed = joinPoint.proceed();
- return proceed;
- }
-
- /**
- * 获取容器上下文请求
- *
- * @return
- */
- public HttpServletRequest getRequest() {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- return request;
- }
-
- /**
- * 相应错误响应信息
- *
- * @param msg
- */
- private void response(String msg) {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletResponse response = attributes.getResponse();
- response.setHeader("Content-type", "text/html;charset=UTF-8");
- try (PrintWriter writer = response.getWriter()) {
- writer.println(msg);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- }
- @VerifySign(type = ReqParameterConstant.HEAD)
- @GetMapping("sign/test")
- public String test(@RequestParam Map<String,String> data){
- // 模拟业务逻辑
- // TODO Something
- return "test";
- }
参数没有被修改时:签名有效
参数被修改时:签名无效
- package com.sign.annotation;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- /**
- * 检查表单 token 是否在缓存中
- */
- @Target(value = ElementType.METHOD)// 表示此注解用在方法上
- @Retention(RetentionPolicy.RUNTIME)// 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
- public @interface ExistsApiToken {
- String type();
- }
- package com.sign.component;
-
- import com.sign.annotation.ExistsApiToken;
- import com.sign.constant.ReqParameterConstant;
- import com.sign.util.TokenUtil;
- import lombok.Synchronized;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang.StringUtils;
- import org.aspectj.lang.JoinPoint;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Before;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.PrintWriter;
-
-
- /**
- * 先定义一个切面,myPointCut,这个切面主要针对 controller 层的方法。
- * <p>
- * 前置通知:before,主要用于获取 token。
- * <p>
- * 环绕通知:aroundAOP,主要用于判断 token 是否重复。
- */
- @Aspect
- @Component
- @Slf4j
- public class ExtApiAop {
-
- @Autowired
- private TokenUtil tokenUtil;
-
- /**
- * 定义切入面:使用 AOP 环绕通知拦截所有访问,拦截的是 controller 层
- */
- @Pointcut("execution(public * com.sign.controller.*.*(..))")
- public void myPointCut() {
-
- }
-
- /**
- * 环绕通知,主要用于业务逻辑的方法上
- *
- * @param joinPoint
- * @return
- * @throws Throwable
- */
- @Around("myPointCut()")
- @Synchronized
- public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable {
-
- // 判断方法上是否有注解 @ExistsApiToken
- MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
- ExistsApiToken extApiToken = methodSignature.getMethod().getDeclaredAnnotation(ExistsApiToken.class);
- String token = null;
-
- // 如果方法上有此注解,则拦截
- if (null != extApiToken) {
-
- // 获取上下文的请求
- HttpServletRequest request = getRequest();
- String type = extApiToken.type();
- log.info("type[] " + type);
- if (type.equals(ReqParameterConstant.HEAD)) {
- token = request.getHeader("token");
- } else if (type.equals(ReqParameterConstant.FORM)) {
- token = request.getParameter("token");
- }
- if (StringUtils.isBlank(token)) {
- response("请求失效,请勿重复提交!");
- return null;
- }
- log.info("token[] " + token);
- // 判断缓存里是否有该 token
- Boolean existsFlag = tokenUtil.findToken(token);
- log.info("existsFlag[] " + existsFlag);
- if (!existsFlag) {
- response("请求失效,请勿重复提交!");
- log.info("请求失效,请勿重复提交!");
- return null;
- } else {
- // 删除 token
- tokenUtil.deleteKey(token);
- log.info("请求有效,删除token[]");
- }
- }
-
- // 继续往后执行
- Object proceed = joinPoint.proceed();
- return proceed;
- }
-
- /**
- * 获取容器上下文请求
- *
- * @return
- */
- public HttpServletRequest getRequest() {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- return request;
- }
-
- /**
- * 相应错误响应信息
- *
- * @param msg
- */
- private void response(String msg) {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletResponse response = attributes.getResponse();
- response.setHeader("Content-type", "text/html;charset=UTF-8");
- try (PrintWriter writer = response.getWriter()) {
- writer.println(msg);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- }
- /**
- * 模拟高并发重复提交,根据 token 缓存防止重复提交
- * 添加自定义 @ExistsApiToken 注解通过 aop 切面进行表单 token 认证
- * type 类型为 head ,token 存储在 header 中
- * @param data
- * @return
- */
- @ExistsApiToken(type = ReqParameterConstant.HEAD)
- @PostMapping("/form/repeatSubmitTest")
- public Map<String, Object> repeatSubmitTest(@RequestParam Map<String, String> data) {
- Map<String, Object> result = new HashMap<>();
-
- // 模拟提交表单信息
- // TODO Something
-
- result.put("code", 0);
- result.put("msg", "success");
-
- return result;
- }
模拟前端获取token:
header加入表单token:
高并发测试:
可以看到,有效提交次数为1,重复提交的表单被拦截。
aop切面的环绕通知方法中使用了@Synchronized注解:同步锁(将action封装原子操作),解决线程安全问题。因为在环绕通知中,有校验token是否在缓存和删除token缓存的操作,如果线程不安全,则会导致token被删除之前,接口被多次调用,业务逻辑被多次执行,不能防止表单重复提交。线程不安全的情况如下图所示:
可以看到,有效提交次数大于1。
- @ExistsApiToken(type = ReqParameterConstant.HEAD)
- @VerifySign(type = ReqParameterConstant.HEAD)
- @GetMapping("/test")
- public String test(@RequestParam Map<String,String> data){
- // 模拟业务逻辑
- // TODO Something
- return "test";
- }
运行正常:
重复的请求失效:
签名失效:
至此,我们可以保证请求的参数不会被修改,而且该请求只能有效提交一次。
防止表单重复提交还可以采用时间戳超时机制:每次请求加上客户端当前的时间戳,后端接收请求时,与服务端当前时间戳做对比,若不超过3s则认为该请求有效,否则返回服务超时。这种机制还可以有效防御爬取数据,如果有人劫持URL进行DOS攻击和爬取数据,那么他最多只能使用3s。
考虑到客户端与服务端的时钟有微小的差异导致时间戳校验出错,我们采取的对齐方式是客户端第一次连接服务端时请求一个接口获取服务端的当前时间A1,再和客户端的当前时间B1做一个差异化计算(A1-B1=AB),得出差异值AB,客户端再后面的请求中都是传B1+AB给到服务端。
- package com.sign.controller;
-
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.time.LocalDateTime;
- import java.time.ZoneOffset;
- import java.util.HashMap;
- import java.util.Map;
-
- /**
- * @Author: 马建生
- * @Date: 2021/07/02/10:20
- * @Description: 模拟时间戳超时机制,防御表单重复提交和数据爬取。
- * 使用的时区都是东8区(北京时间),这是为了防止服务器设置时区错误时导致时间不对。
- */
- @RestController
- @Slf4j
- public class TimestampController {
-
- /**
- * 模拟前端获取客户端当前时间,与服务端获取的时间做差异对齐,最后返回对齐后的值
- * 为了模拟真实情况中时钟可能不一致,这里做一个微小的时钟差异
- * 时间戳采用毫秒级别,客户端时间为A,服务端时间为B,差异为 A - B = AB
- * 每次请求附带的时间戳为 A - AB
- *
- * @return
- */
- @GetMapping("/getClientTimestamp")
- public Long getClientTimestamp() {
- // 获取客户端的时间戳
- // 获取秒数,比当前时间快一分钟
- // Long second = LocalDateTime.now().plusMinutes(1).toEpochSecond(ZoneOffset.of("+8"));
- // log.info("timestamp_second[] " + second);
- // 获取毫秒数,慢一分钟
- Long milliSecond = LocalDateTime.now().minusMinutes(1).toInstant(ZoneOffset.of("+8")).toEpochMilli();
- log.info("timestamp_milliSecond[] " + milliSecond);
-
- // 模拟差异比较,客户端时间为A,服务端时间为B,差异为 A - B = AB
- Long difference = milliSecond - getServerTimestamp();
-
- // 模拟时钟对齐,每次请求附带的应为该时间戳
- return milliSecond - difference;
- }
-
- /**
- * 模拟前端获取服务器当前时间
- *
- * @return
- */
- @GetMapping("/getServerTimestamp")
- public Long getServerTimestamp() {
- // 获取秒数
- Long second = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
- log.info("timestamp_second[] " + second);
- // 获取毫秒数
- Long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
- log.info("timestamp_milliSecond[] " + milliSecond);
- return milliSecond;
- }
-
- /**
- * 模拟业务逻辑,先检查时间戳差异是否在可控范围,若通过则执行业务逻辑,否则返回超时信息
- * 为了方便,这里模拟超时 10s
- * @param data
- * @return
- */
- @GetMapping("/test")
- public Map<String, Object> test(@RequestParam Map<String, String> data) {
-
- // 获取参数中的时间戳
- Long timestamp = Long.parseLong(data.get("timestamp"));
-
- // 返回信息
- Map<String,Object> result = new HashMap<>();
- if (getServerTimestamp() - timestamp <= 10000 && getServerTimestamp() - timestamp >= 0) {
- // 模拟业务逻辑
- // TODO Something
- result.put("code",0);
- result.put("msg","success");
- }else{
- result.put("code",-1);
- result.put("msg","请求超时,请重新提交!");
- }
-
- return result;
- }
- }
-
自定义注解:
- package com.sign.annotation;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- /**
- * @Author: 马建生
- * @Date: 2021/07/02/11:19
- * @Description: 检查请求的时间戳是否超时
- */
-
- @Target(value = ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface CheckTimestamp {
- String type();
- }
aop拦截器:
- package com.sign.component;
-
- import com.sign.annotation.CheckTimestamp;
- import com.sign.constant.ReqParameterConstant;
- import lombok.Synchronized;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.PrintWriter;
- import java.time.LocalDateTime;
- import java.time.ZoneOffset;
-
- /**
- * @Author: 马建生
- * @Date: 2021/07/02/11:21
- * @Description:
- * 先定义一个切面,myPointCut,这个切面主要针对 controller 层的方法。
- * 环绕通知:aroundAOP,主要用于判断 token 是否重复。
- *
- */
- @Aspect
- @Component
- @Slf4j
- public class CheckTimestampAop {
-
- /**
- * 定义切入面:使用 AOP 环绕通知拦截所有访问,拦截的是 controller 层
- */
- @Pointcut("execution(public * com.sign.controller.*.*(..))")
- public void myPointCut() {
-
- }
-
-
- /**
- * 环绕通知,主要用于业务逻辑的方法上
- *
- * @param joinPoint
- * @return
- * @throws Throwable
- */
- @Around("myPointCut()")
- @Synchronized
- public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable {
-
- // 判断方法上是否有注解 @CheckTimestamp
- MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
- CheckTimestamp extApiToken = methodSignature.getMethod().getDeclaredAnnotation(CheckTimestamp.class);
- long timestamp = 0;
-
- // 如果方法上有此注解,则拦截
- if (null != extApiToken) {
-
- // 获取上下文的请求
- HttpServletRequest request = getRequest();
- String type = extApiToken.type();
- log.info("type[] " + type);
- if (type.equals(ReqParameterConstant.HEAD)) {
- timestamp = Long.parseLong(request.getHeader("timestamp"));
- } else if (type.equals(ReqParameterConstant.FORM)) {
- timestamp = Long.parseLong(request.getParameter("timestamp"));
- }
- if (timestamp == 0) {
- response("请求无效,请重新提交!");
- return null;
- }
- long serverTimestamp = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
- // 判断超时
- if (serverTimestamp - timestamp <= 10000 && serverTimestamp - timestamp >= 0) {
- log.info("请求有效。");
- } else {
- response("请求超时,请重新提交!");
- return null;
- }
- }
-
- // 继续往后执行
- Object proceed = joinPoint.proceed();
- return proceed;
- }
-
- /**
- * 获取容器上下文请求
- *
- * @return
- */
- public HttpServletRequest getRequest() {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- return request;
- }
-
- /**
- * 相应错误响应信息
- *
- * @param msg
- */
- private void response(String msg) {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletResponse response = attributes.getResponse();
- response.setHeader("Content-type", "text/html;charset=UTF-8");
- try (PrintWriter writer = response.getWriter()) {
- writer.println(msg);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
控制器的业务方法上添加注解:
- /**
- * 模拟业务逻辑,先检查时间戳差异是否在可控范围,若通过则执行业务逻辑,否则返回超时信息
- * 为了方便,这里模拟超时 10s
- *
- * @param data
- * @return
- */
- @CheckTimestamp(type = ReqParameterConstant.FORM)
- @GetMapping("/timestamp/test")
- public Map<String, Object> test(@RequestParam Map<String, String> data) {
-
- Map<String, Object> result = new HashMap<>();
-
- // 模拟业务逻辑
- // TODO Something
- result.put("code", 0);
- result.put("msg", "success");
-
- return result;
- }
测试:
带时间戳请求:
超时请求:
因为用户可以在多端登录(手机app,小程序,网页),所以一个用户可能有多个有效token。当用户修改登录密码时需要把全部的token删除,我们将username与list<token>作为键值对缓存在redis中,修改密码时会把username对应的token列表中的全部token删除。
将 token与username以键值对缓存在redis中,并设置失效时间。服务端接收到请求后进行token验证,如果token不存在,说明请求无效。
在oauth模块完成认证后,将token缓存到redis;在gateway网关模块拦截所有请求检查token是否在redis中;也可以在oauth模块checktoken过程中进行检查(可参考Spring Cloud OAuth2 资源服务器CheckToken 源码解析)。
生成token之后将token与username缓存到redis:应将redis操作封装为util,这里为方便未作封装
- package com.oauth2.controller;
-
-
- import cn.hutool.core.collection.CollUtil;
- import cn.hutool.core.convert.Convert;
- import com.oauth2.api.CommonResult;
- import com.oauth2.constant.RedisConstant;
- import com.oauth2.domain.dto.Oauth2TokenDto;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.security.oauth2.common.OAuth2AccessToken;
- import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
- import org.springframework.web.HttpRequestMethodNotSupportedException;
- import org.springframework.web.bind.annotation.*;
-
- import javax.annotation.Resource;
- import java.security.Principal;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
-
- @Slf4j
- @RestController
- public class AuthController {
-
- @Autowired
- private TokenEndpoint tokenEndpoint;
-
- @Resource
- private RedisTemplate<String, Object> redisTemplate;
-
- /**
- * Oauth2登录认证
- */
- @PostMapping("/oauth/token")
- public CommonResult<Oauth2TokenDto> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
- OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
- Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
- .token(oAuth2AccessToken.getValue())
- .refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
- .expiresIn(oAuth2AccessToken.getExpiresIn())
- .tokenHead("Bearer ").build();
- String username = parameters.get("username");
- String token = oauth2TokenDto.getToken();
- // 将 token => username 作为键值对存入redis缓存,并设定有效时间为24小时
- // 生产环境时可以将 token 有效期设置长一点,以免 redis 数据频繁变动
- redisTemplate.opsForValue().set(token, username, 3600 * 24, TimeUnit.SECONDS);
- // 判断用户是否首次登录
- if (!redisTemplate.opsForHash().hasKey(RedisConstant.USERID_TOKEN_MAP, username)) {
- log.info("user log[] 首次登录");
- // 将 username 与 list<token> 作为键值对缓存在 redis 中
- redisTemplate.opsForHash().put(RedisConstant.USERID_TOKEN_MAP, username, CollUtil.toList(token));
- }else{
- // 先将之前的 token 拿出来
- Object object = redisTemplate.opsForHash().get(RedisConstant.USERID_TOKEN_MAP, username);
- List<String> userToken = Convert.toList(String.class, object);
- log.info("user log[] 非首次登录 tokenList => {}",userToken.toString());
- // 删除已有的 token
- redisTemplate.opsForHash().delete(RedisConstant.USERID_TOKEN_MAP,username);
- // 添加新增的 token
- userToken.add(token);
- // 将 username 与 list<token> 作为键值对缓存在 redis 中
- redisTemplate.opsForHash().put(RedisConstant.USERID_TOKEN_MAP, username, userToken);
- }
- return CommonResult.success(oauth2TokenDto);
- }
- }
在网关拦截请求对token进行检查,如果不在redis缓存中,则返回401:
- package com.gateway.filter;
-
- import cn.hutool.core.util.StrUtil;
- import com.alibaba.fastjson.JSONObject;
- import com.nimbusds.jose.JWSObject;
- import lombok.extern.slf4j.Slf4j;
- import org.slf4j.LoggerFactory;
- import org.springframework.cloud.gateway.filter.GatewayFilterChain;
- import org.springframework.cloud.gateway.filter.GlobalFilter;
- import org.springframework.core.Ordered;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpRequest;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import reactor.core.publisher.Flux;
- import reactor.core.publisher.Mono;
-
- import javax.annotation.Resource;
- import java.nio.charset.StandardCharsets;
- import java.text.ParseException;
- //import java.util.logging.Logger;
-
- @Slf4j
- @Component
- public class AuthGlobalFilter implements GlobalFilter, Ordered {
-
- // private static Logger LOGGER = (Logger) LoggerFactory.getLogger(AuthGlobalFilter.class);
-
- @Resource
- private RedisTemplate<String, Object> redisTemplate;
-
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- String token = exchange.getRequest().getHeaders().getFirst("Authorization");
- if (StrUtil.isEmpty(token)) {
- return chain.filter(exchange);
- }
- try {
- // 从token中解析用户信息并设置到Header中去
- String realToken = token.replace("Bearer ", "");
- // 检查token是否在redis缓存中
- Object object = redisTemplate.opsForValue().get(realToken);
- if (object != null) {
- JWSObject jwsObject = JWSObject.parse(realToken);
- String userStr = jwsObject.getPayload().toString();
- log.info("AuthGlobalFilter.filter() user:{}", userStr);
- // LOGGER.info(String.format("AuthGlobalFilter.filter() user:{0}",userStr));
- // 将json格式的用户信息转成json对象
- JSONObject user = JSONObject.parseObject(userStr);
- // 将用户名加到header中,控制器通过HttpServletRequest的getHeader方法获取username
- ServerHttpRequest request = exchange.getRequest().mutate().header("username", user.getString("user_name")).build();
- exchange = exchange.mutate().request(request).build();
- } else {
- // 设置status和body
- ServerWebExchange finalExchange = exchange;
- return Mono.defer(() -> {
- finalExchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//设置status
- final ServerHttpResponse response = finalExchange.getResponse();
- // token无效,返回401,这里可以用json对象实现
- byte[] bytes = "{\"code\":\"401\",\"data\":\"Access Denied\",\"message\":\"暂未登录或token已经过期\"}".getBytes(StandardCharsets.UTF_8);
- DataBuffer buffer = finalExchange.getResponse().bufferFactory().wrap(bytes);
- log.info("token不在redis缓存中,请求无效");
- return response.writeWith(Flux.just(buffer));//设置body
- });
- }
- } catch (ParseException e) {
- e.printStackTrace();
- }
- return chain.filter(exchange);
- }
-
- @Override
- public int getOrder() {
- return 0;
- }
- }
- /**
- * 修改密码接口
- * 需要旧密码 oldPwd 验证,旧密码通过则更新新密码 newPwd 到数据库
- * 撤销 redis 缓存该用户的所有有效 token
- *
- * @param request
- * @param oldPwd
- * @param newPwd
- * @return
- */
- @ApiOperation(
- value = "更新学生账号密码",
- notes = "需要在旧密码的校验,新密码二次校验需在前端完成"
- )
- @GlobalTransactional
- @GetMapping("/changePassword")
- public Map<String, Object> changePassword(HttpServletRequest request, @RequestParam("oldPwd") String oldPwd, @RequestParam("newPwd") String newPwd) throws Exception {
-
- // 从 header 获取 username
- String username = request.getHeader("username");
- Student student = new Student();
-
- student.setUsername(username);
- // 先查询该学生信息
- QueryWrapper<Student> studentQueryWrapper = new QueryWrapper<>(student);
- Student studentResult = studentMapper.selectOne(studentQueryWrapper);
-
- log.info("student => {}", studentResult.toString());
- // 返回信息
- Map<String, Object> result = new HashMap<>();
-
- // 如果旧密码匹配,则更新密码,销毁 redis 中该 user 所有有效的 token
- // 第一个参数为明文,第二个参数为密文
- if (passwordEncoder.matches(oldPwd, studentResult.getPassword())) {
- student.setId(studentResult.getId());
- // 密码需要加密
- student.setPassword(passwordEncoder.encode(newPwd));
-
- // 通过 id 更新
- if (studentMapper.updateById(student) == 0) {
- throw new Exception(MySQLMessageConstant.UPDATE_FAIL);
- }
-
- // 销毁所有有效 token
- Object object = redisTemplate.opsForHash().get(RedisConstant.USERID_TOKEN_MAP, username);
- List<String> userToken = Convert.toList(String.class, object);
- redisTemplate.opsForHash().delete(RedisConstant.USERID_TOKEN_MAP, username);
- userToken.forEach(p -> redisTemplate.delete(p));
- result.put("code", 0);// 状态码
- result.put("msg", "success");// 信息
- } else {
- result.put("code", -1);// 状态码
- result.put("msg", "旧密码错误!");// 信息
- }
- return result;
- }
-
获取token:
- {
- "code": 200,
- "message": "操作成功",
- "data": {
- "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiIxODAxNTYxNTI5QHFxLmNvbSIsInNjb3BlIjpbImFsbCJdLCJpZCI6MTQwNzYxNzg3ODI3MDM4MjA4MSwiZXhwIjoxNjI1NDY3Mjc0LCJhdXRob3JpdGllcyI6WyJTVFVERU5UIl0sImp0aSI6ImE1N2M4MDM4LTI1OTgtNGE4Yi04M2ViLTc3YWRhMDUyYzYwNSIsImNsaWVudF9pZCI6ImNsaWVudC1hcHAifQ.MKN-BgekPMu-DhmGE6F9sUxCtQWxMY7hDHBNUeHkAGfU7NO3xzWZKoyC5pLbxnO8njhSJ57UTNTPuM-kxx-lrjmu542wAqgBQTTEQHTruxR_sGbVLrqRqV0JkJf24JV2NhWgVoIvpNNgB9cOPaUdBxJ52lictfKRFjxei9bcLDH22GwkHBqzNJHnoJIUgms0vS59WwZl5eamkHWwik7RjhySje7dg4PpzaQfSO0n-Q1rfJShioEQdECsELHSKujK6mzZpPvKUpEhYXdlZjwt9jUcS5pHNeCe9CVc2RpKmDGdl6YnX-oh-jvjF2KfVqnRIqUo_fYeDxAPEGWc5YaD5A",
- "refreshToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiIxODAxNTYxNTI5QHFxLmNvbSIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJhNTdjODAzOC0yNTk4LTRhOGItODNlYi03N2FkYTA1MmM2MDUiLCJpZCI6MTQwNzYxNzg3ODI3MDM4MjA4MSwiZXhwIjoxNjI1NTUwMDc0LCJhdXRob3JpdGllcyI6WyJTVFVERU5UIl0sImp0aSI6IjYwNDFhYzgwLTE5YzAtNDA2MC05YWM2LTRmMTg5ZWE4N2MwZiIsImNsaWVudF9pZCI6ImNsaWVudC1hcHAifQ.E69t1dajlW3DQ0JcEbL1-n--AshAOJ0NTLe5ZZaT1LXEPgG2ENm0gztlHVAwHcK5fvl7Fx5OlaxbkKXoO4oeT0ppokXr8r6mFeNzrmBgvy6Is2PElNekkSkMG0c4pb7Syidg6vmP2MTMcwQ2hudUz0HJ6Bm0nTCkCtpxEMN9aVDVp2QOZsNQdIbqivysF3ZArEC0JghQy2Wk_e8NbB70jQaBbgXDzB2c2VqMQDLYNT8BTXTa4LEb6j8KMtGO_eWtsPxAPBPwtPrzbVvEED1Wa2ZnR9w4kOs5OCMtR_2LemtJuHBRgVxsbHeChmw55E7AOpStDTWhoVtfAx3W0J4-8A",
- "tokenHead": "Bearer ",
- "expiresIn": 3599
- }
- }
-
访问信息接口:
- {
- "msg": "success",
- "code": 0,
- "data": {
- "openid": null,
- "roles": null,
- "updateTime": null,
- "sid": 11819,
- "password": null,
- "regTime": null,
- "loginTime": "2021-07-05T05:41:14",
- "major": null,
- "sno": "pG1kj6V9zeu6GFcosJ2sEg==",
- "nickname": "sam",
- "id": 1407617878270382081,
- "status": true,
- "username": "1801561529@qq.com"
- }
- }
-
修改token缓存的ttl,使其快速过期:
token过期时重新请求:
模拟多端登录:多次请求登录接口
可以看到redis缓存中,用户有多个有效token。
修改密码测试:
输入错误的旧密码则无法修改。
旧密码正确则修改成功。
redis 清空了该用户所有的token,用户需重新登录。
正如该包所描述:Spring Boot 接口请求参数自动加解密。
- <!-- https://mvnrepository.com/artifact/cn.shuibo/rsa-encrypt-body-spring-boot -->
- <dependency>
- <groupId>cn.shuibo</groupId>
- <artifactId>rsa-encrypt-body-spring-boot</artifactId>
- <version>1.0.1.RELEASE</version>
- </dependency>
- package com.sign;
-
- import cn.shuibo.annotation.EnableSecurity;
- import org.springframework.boot.SpringApplication;
- import org.springframework.cloud.client.SpringCloudApplication;
-
- @EnableSecurity
- @SpringCloudApplication
- public class SignApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(SignApplication.class);
- }
- }
- 生成的公钥:
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoewDlVLsRQoYulINY2Szh89jWA0J7KB2RPdcMKWpX1WCPcw3wB8q/7bBrJFYr2899V59QcJpd7LRf6hEjLmjaz+7QzDhWFz2x11kqROmy0+PWt+SiSlRkGGWmfQGwUF8waAB/fzaQ5V0naDDF48XbAcvAR7DKOnYBcwpRhEHd3wIDAQAB
- 生成的私钥:
- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKh7AOVUuxFChi6Ug1jZLOHz2NYDQnsoHZE91wwpalfVYI9zDfAHyr/tsGskVivbz31Xn1Bwml3stF/qESMuaNrP7tDMOFYXPbHXWSpE6bLT49a35KJKVGQYZaZ9AbBQXzBoAH9/NpDlXSdoMMXjxdsBy8BHsMo6dgFzClGEQd3fAgMBAAECgYAXH8LQtx9x0AKgtAuPD0fEv3Y8cXgXdTsRqz4v0iNhaMz3A2CfWEJws0vqeLNHE8VXu8YHAV1+lLVxEKxHeuAzJjJlekGSTZAkfgF4Xm5mdfGuhVRF4ldHHbguQ5oT+OhdsxC3mMAUIwSjAkhuqEU/tK3quZeKt4Z/V3s2qa4fiQJBAPjtQSjHmnhEHayIaIiyWm/Nu+d3plq3tZsu1Mz8yj4oTPQ/rt4MvcMbI2G/tSXh1fzWmleQDlc1sATP27lzdn0CQQCtRJDX+gJejoJ9PMf/0Nzrqr1LH64UaBL0MrFc58vGN0UHtMkwKC1g58R5x006b3m8pp1mUNKutR9tMB/80SiLAkEArqECzTD6VNS0XI11iDBW8YhLAh8WPR4T8UHxV70fxGtRUSg77NrTZURsle5/jovYKwACVttgtB2d1kJbysYNoQJAZ/coYi+llE82hScfaqRMqyv8AUO1FJGOLfDs864yW3F2fjVAMyEoeWkYP2oTMOkKxuPCtk3w3NvZS48A4pYuGQJALU19SKEWk+JGC8tIOdO/tE2rSp/Uc+S/trbOkmSD0vA8sCetJ4jth68eNf60csGXDF2s4fZnc/8vyhkyX51gtg==
- server:
- port: 8011
- spring:
- redis:
- host: localhost
- port: 6379
- database: 0
- application:
- name: service-sign-center
- cloud:
- nacos:
- discovery:
- server-addr: localhost:8848
- sentinel:
- transport:
- port: 9999
- dashboard: localhost:8080
-
- # api 加密
- rsa:
- encrypt:
- open: true # 是否开启加密 true or false
- showLog: true # 是否打印加解密log true or false
- publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoewDlVLsRQoYulINY2Szh89jWA0J7KB2RPdcMKWpX1WCPcw3wB8q/7bBrJFYr2899V59QcJpd7LRf6hEjLmjaz+7QzDhWFz2x11kqROmy0+PWt+SiSlRkGGWmfQGwUF8waAB/fzaQ5V0naDDF48XbAcvAR7DKOnYBcwpRhEHd3wIDAQAB
- privateKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKh7AOVUuxFChi6Ug1jZLOHz2NYDQnsoHZE91wwpalfVYI9zDfAHyr/tsGskVivbz31Xn1Bwml3stF/qESMuaNrP7tDMOFYXPbHXWSpE6bLT49a35KJKVGQYZaZ9AbBQXzBoAH9/NpDlXSdoMMXjxdsBy8BHsMo6dgFzClGEQd3fAgMBAAECgYAXH8LQtx9x0AKgtAuPD0fEv3Y8cXgXdTsRqz4v0iNhaMz3A2CfWEJws0vqeLNHE8VXu8YHAV1+lLVxEKxHeuAzJjJlekGSTZAkfgF4Xm5mdfGuhVRF4ldHHbguQ5oT+OhdsxC3mMAUIwSjAkhuqEU/tK3quZeKt4Z/V3s2qa4fiQJBAPjtQSjHmnhEHayIaIiyWm/Nu+d3plq3tZsu1Mz8yj4oTPQ/rt4MvcMbI2G/tSXh1fzWmleQDlc1sATP27lzdn0CQQCtRJDX+gJejoJ9PMf/0Nzrqr1LH64UaBL0MrFc58vGN0UHtMkwKC1g58R5x006b3m8pp1mUNKutR9tMB/80SiLAkEArqECzTD6VNS0XI11iDBW8YhLAh8WPR4T8UHxV70fxGtRUSg77NrTZURsle5/jovYKwACVttgtB2d1kJbysYNoQJAZ/coYi+llE82hScfaqRMqyv8AUO1FJGOLfDs864yW3F2fjVAMyEoeWkYP2oTMOkKxuPCtk3w3NvZS48A4pYuGQJALU19SKEWk+JGC8tIOdO/tE2rSp/Uc+S/trbOkmSD0vA8sCetJ4jth68eNf60csGXDF2s4fZnc/8vyhkyX51gtg==
参数说明:open 为是否开启加密,showLog 为是否打印加解密日志,publicKey 为 RSA 公钥,privateKey 为 RSA 私钥,公私钥均为程序生成。
- // 使用 @Encrypt 注解进行加密
-
- @Encrypt
- @GetMapping("/testEncrypt")
- public Map<String, Object> testEncrypt() {
- Map<String, Object> result = new HashMap<>();
- result.put("code", 1);
- result.put("msg", "success");
- return result;
- }
当 open 为 false 时,不开启加密:
当 open 为 true 时,开启加密:
"EwLkCMZPR+DEenZ4RLipEjEzvk9c4LdA1dAqzFH0TPuwmxvoyyI6+XY+W/V8WVHT349QyAbYpCTgjDZ49ZUCUFxDdSYG0CQWPpL+5QryVzd6G2E8bwiwGJmFq3DRl64BSjGxlKZ+bOEF4m1b3yEGyTNWiUy7VXg1AgEp03O+Yv4="
控制台信息:
2021-07-10 10:22:37.345 INFO [service-sign-center,e02b9067dec97e30,e02b9067dec97e30,true] 11872 --- [nio-8011-exec-3] c.s.advice.EncryptResponseBodyAdvice : Pre-encrypted data:{"msg":"success","code":1},After encryption:EwLkCMZPR+DEenZ4RLipEjEzvk9c4LdA1dAqzFH0TPuwmxvoyyI6+XY+W/V8WVHT349QyAbYpCTgjDZ49ZUCUFxDdSYG0CQWPpL+5QryVzd6G2E8bwiwGJmFq3DRl64BSjGxlKZ+bOEF4m1b3yEGyTNWiUy7VXg1AgEp03O+Yv4=
- // 使用 @Decrypt 注解对已加密的参数解密
-
- // 其他java端程序可以用注解,如果是vue,请用RSA密钥解密
-
- @Decrypt
- @GetMapping("/testDecrypt")
- public Map<String, Object> testDecrypt(@RequestBody Map<String,Object> data) {
- return data;
- }
解密成功:
控制台信息:
2021-07-10 10:36:23.851 INFO [service-sign-center,0763359838ba829a,0763359838ba829a,true] 6516 --- [nio-8011-exec-5] c.shuibo.advice.DecryptHttpInputMessage : Encrypted data received:"EwLkCMZPR+DEenZ4RLipEjEzvk9c4LdA1dAqzFH0TPuwmxvoyyI6+XY+W/V8WVHT349QyAbYpCTgjDZ49ZUCUFxDdSYG0CQWPpL+5QryVzd6G2E8bwiwGJmFq3DRl64BSjGxlKZ+bOEF4m1b3yEGyTNWiUy7VXg1AgEp03O+Yv4=",After decryption:{"msg":"success","code":1}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。