前后端分离 , 如何保证接口安全性 ?

您所在的位置:网站首页 前后端交互加密方法有哪些 前后端分离 , 如何保证接口安全性 ?

前后端分离 , 如何保证接口安全性 ?

2023-09-06 06:18| 来源: 网络整理| 查看: 265

1. 完整的代码

前端vue代码 后端java代码

2. Api有哪些安全问题?http接口—前后端分离mvvm

在这里插入图片描述

3. Token授权机制

用户使用用户名密码登录后服务器给客户端返回一个Token(通常是UUID),并将Token-UserId以键值对的形式存放在缓存服务器中。服务端接收到请求后进行Token验证,如果Token不存在,说明请求无效。

在这里插入图片描述

4. 时间戳超时机制

时间戳,是客户端调用接口时对应的当前时间戳,时间戳用于防止DoS攻击。当黑客劫持了请求的url去DoS攻击,每次调用接口时接口都会判断服务器当前系统时间和接口中传的的timestamp的差值,如果这个差值超过某个设置的时间(假如5分钟),那么这个请求将被拦截掉,如果在设置的超时时间范围内,是不能阻止DoS攻击的。 timestamp机制只能减轻DoS攻击的时间,缩短攻击时间。如果黑客修改了时间戳的值可通过sign签名机制来处理。

DoS 重放攻击

DoS是Denial of Service的简称,即拒绝服务,造成DoS的攻击行为被称为DoS攻击,其目的是使计算机或网络无法提供正常的服务。最常见的DoS攻击有计算机网络带宽攻击和连通性攻击。

DoS攻击是指故意的攻击网络协议实现的缺陷或直接通过野蛮手段残忍地耗尽被攻击对象的资源,目的是让目标计算机或网络无法提供正常的服务或资源访问,使目标系统服务系统停止响应甚至崩溃,而在此攻击中并不包括侵入目标服务器或目标网络设备。这些服务资源包括网络带宽,文件系统空间容量,开放的进程或者允许的连接。这种攻击会导致资源的匮乏,无论计算机的处理速度多快、内存容量多大、网络带宽的速度多快都无法避免这种攻击带来的后果。

一句话,重复使用请求参数伪造二次请求的隐患

5. sign机制

nonce:随机值,是客户端随机生成的值,作为参数传递过来,随机值的目的是增加sign签名的多变性。随机值一般是数字和字母的组合,6位长度,随机值的组成和长度没有固定规则。

sign: 一般用于参数签名,防止参数被非法篡改,最常见的是修改金额等重要敏感参数, sign的值一般是将所有非空参数按照升续排序然后+token+key+timestamp+nonce(随机数)拼接在一起,然后使用某种加密算法进行加密,作为接口中的一个参数sign来传递,也可以将sign放到请求头中 。

并将该签名存放到缓存服务器中,超时时间设定为跟时间戳的超时时间一致(这就是为什么要尽量短,二者时间一致可以保证无论在timestamp规定时间内还是外本URL都只能访问一次)。 同一个签名只能使用一次,如果发现缓存服务器中已经存在了本次签名,则拒绝服务。(可以有效防止重放攻击 — DoS攻击)

接口在网络传输过程中如果被黑客挟持,并修改其中的参数值,然后再继续调用接口,虽然参数的值被修改了,但是因为黑客不知道sign是如何计算出来的,不知道sign都有哪些值构成,不知道以怎样的顺序拼接在一起的,最重要的是不知道签名字符串中的key是什么,所以黑客可以篡改参数的值,但没法修改sign的值,当服务器调用接口前会按照sign的规则重新计算出sign的值然后和接口传递的sign参数的值做比较,如果相等表示参数值没有被篡改,如果不等,表示参数被非法篡改了,就不执行接口了。

6. 防止重复提交(重放攻击)

对于一些重要的操作需要防止客户端重复提交的(如非幂等性重要操作),具体办法是当请求第一次提交时将sign作为key保存到redis,并设置超时时间,超时时间和Timestamp中设置的差值相同。当同一个请求第二次访问时会先检测redis是否存在该sign,如果存在则证明重复提交了,接口就不再继续调用了。如果sign在缓存服务器中因过期时间到了,而被删除了,此时当这个url再次请求服务器时,因token的过期时间和sign的过期时间一直,sign过期也意味着token过期,那样同样的url再访问服务器会因token错误会被拦截掉,这就是为什么sign和token的过期时间要保持一致的原因。拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据)。

对于哪些接口需要防止重复提交可以自定义个注解来标记。

注意:

所有的安全措施都用上的话有时候难免太过复杂,在实际项目中需要根据自身情况作出裁剪,比如可以只使用签名机制就可以保证信息不会被篡改,或者定向提供服务的时候只用Token机制就可以了。如何裁剪,全看项目实际情况和对接口安全性的要求。

7. 数据加密

在这里插入图片描述

8. 加密方法 1,对称加密

AES,3DES,DES等,适合做大量数据或数据文件的加解密。

2,非对称加密

如RSA,Rabin。公钥加密,私钥解密。对大数据量进行加解密时性能较低。

9. 前端 h5 页面如何保存签名key

思考一个问题:

如果是app可以通过加密固化处理,但是前端h5该如何处理呢,如果将参与生成签名的key,放入到页面中,会导致任意一个人访问网站后都可以按F12查看源代码,就知道key了,同时也知道生成签名的流程,因此中间者可以修改参数后自己再重新生成签名。

解决办法:

前端在调用接口前随机生成一个字符串,然后通过rsa公钥进行加密处理,将加密结果放入到请求头中key , 然后将随机生成的字符串 — key , 参与sign的生成,最后进行接口的调用。

10. 使用流程

前端随机生成一个字符串,然后通过rsa公钥进行加密,将加密结果放入到请求头中 key = 加密结果(key)

客户端携带参数 nonce(随机数)、ts、sign去调用服务器端的API token , sign = md5(所有非空参数按照升续排序然后 + token + key + ts(当前时间戳) + nonce)

第一步,

设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),比如现在有url = http://ip:port?a=1&b=3&c=4

stringA = a1b3c4

特别注意以下重要规则:

参数名ASCII码从小到大排序(字典序);如果参数的值为空不参与签名;参数名区分大小写;

如果存在请求体,则将请求体中的json属性按照上面的规律进行排序 如果您的请求url包含路径参数以及请求体,那所有的参数都将参与运算

第二步

在stringA + token + key + ts(当前时间戳) + nonce 得到stringSignTemp字符并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。

将 token , sign , ts , nonce , 加密后的key —> 放入到请求头中访问后端

sign的作用是防止参数被篡改,客户端调用服务端时需要传递sign参数,服务器响应客户端时也可以返回一个sign用于客户度校验返回的值是否被非法篡改了。客户端传的sign和服务器端响应的sign算法可能会不同。

11. 后端使用

签名格式 data(字典升序) + token + key(随机生成的秘钥) + ts(当前时间戳timestamp) + nonce(随机数)

具体代码请看文章开头部分

11.1. 目录结构

在这里插入图片描述

11.2. 使用方法

配置文件

sign: # 用于加密参与签名加密的key,同时也可以用于数据加密 # 公钥放到前端页面上 rsa: privateKey: MIICdgIBADANBgkqhkiG9w

控制层

/** * Description: 令牌签名控制层 * @author shenguangyang * @date 2021/01/20 */ @RestController @ResultBody @RequestMapping("token") public class TokenSignController { @Resource TokenSignService tokenSignService; @GetMapping("find") @ApiSafe(SafeType.SIGN) public User findUser(@RequestParam("uid") String uid) { User user = tokenSignService.findUser(uid); return user; } @PostMapping("save") @ApiSafe(SafeType.CRYPTO) public void save(@RequestBody User user) { System.out.println("保存成功 ---> " + user); } /** * 用户登录 * @return token */ @PostMapping("login") public String login(@RequestBody User user) { return tokenSignService.login(user); } }

在需要只有登录才可以访问的方法上标注@ApiSafe注解 如果需要登录并且加签名,防止参数篡改以及重放攻击,在方法上标注 @ApiSafe(SafeType.SIGN)注解 如果需要登录并且对参数进行加密,在方法上标注 @ApiSafe(SafeType.CRYPTO)注解

11.3. 原理讲解

项目是使用springboot进行构建,使用到了拦截器和过滤器

当前端发出一个请求,分两种情况,这两种情况都经过过滤器 RequestApiSafeFilter

加签名,请求头中必须携带 token , nonce , ts , sign , key(加密后的值)加密 , 请求头中必须携带token

请求流程

在这里插入图片描述

加签名

后端在验证签名的时候最重要的一步就是获取请求中的所有参数,获取url中的参数比较简单,但是获取请求体的参数遇到一个坑,请求体就是前端通过post请求提交且数据为json格式,后端需要通过数据输入流读取数据,但是读取完后就不可以在读取了,如果这时候不经过任何处理,当springboot在解析参数的时候读取输入流就是报错,因为数据已经被读走了

BodyReaderHttpServletRequestWrapper类 主要作用是复制 HttpServletRequest 的输入流,不然你拿出 body 参数后验签后,到 Controller 时,接收参数会为 null

加密

DecryptRequest类最后也复制了HttpServletRequest 的输入流 , 不然到控制层拿到参数也会变成null

加密算法采用的是RSA非对称加密,根据您的项目需求可以进行替换,比如采用AES加密

关于过滤器返回给前端非法请求

当在过滤器中遇到非法请求时候,需要将错误的结果响应给页面 ,但是我自己通过如下代码写入页面失败

response.setContentType("application/json; charset=UTF-8"); Result result = Result.failure(code, msg); PrintWriter out = response.getWriter(); out.write(JSON.toJSONString(result));

因此我的解决方案是将错误的结果写入到request中,通过拦截器进行拦截,然后将错误结果返回给前端页面

过滤器

request.setAttribute("code",1002); request.setAttribute("msg","连接服务器超时!!!");

拦截器

/** * 登录拦截器 * @author shenguangyang */ @Component public class Interceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(Interceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.debug("request url = " + request.getRequestURL()); // code和msg是从过滤器传过来的 Object code = request.getAttribute("code"); if ((code != null ) && ((int)code != ResultEnum.SUCCESS.getCode())) { String msg = (String) request.getAttribute("msg"); ApiUtils.result(response, (int) code, msg); return false; } return true; } }

springboot中拦截器与跨域配置冲突解决

解决原理:一个http请求,先走filter,到达servlet后才进行拦截器的处理,所以我们可以把cors放在filter里,就可以优先于权限拦截器执行。

把 WebMvcConfigurer 接口中的 addCorsMappings 配置跨域去掉,这个跨域配置针对拦截器的配置,如果你项目中使用了过滤器会发生问题

@Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { //1. 添加 CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //放行哪些原始域 config.addAllowedOrigin("*"); //是否发送 Cookie config.setAllowCredentials(true); //放行哪些请求方式 config.addAllowedMethod("*"); //放行哪些原始请求头部信息 config.addAllowedHeader("*"); //2. 添加映射路径 UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); corsConfigurationSource.registerCorsConfiguration("/**",config); //3. 返回新的CorsFilter return new CorsFilter(corsConfigurationSource); } }

针对拦截器的配置跨域,如果项目中使用到了过滤器,就需要将该配置去掉,并配置跨域配置类中将CorsFilter类注入到容器中

// @Override // public void addCorsMappings(CorsRegistry registry) { // registry.addMapping("/**") // .allowCredentials(true) // .allowedHeaders("*") // .allowedOrigins("*") // .allowedMethods("*"); // }

由于异步请求会先发出OPTIONS请求,因此过滤器需要对该请求进行过滤

String m1 = request.getMethod(); if (HttpMethod.OPTIONS.toString().equals(m1)){ response.setStatus(HttpStatus.NO_CONTENT.value()); filterChain.doFilter(request, response); return; } 11.4. 代码 11.4.1. yaml server: port: 9090 spring: #reids配置 # Redis数据库索引(默认为0) redis: database: 0 # Redis服务器地址 host: 192.168.1.110 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: flzx3000C!@)( lettuce: pool: #连接池最大连接数(使用负值表示没有限制) max-active: 8 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 8 # 连接池中的最小空闲连接 min-idle: 0 sign: # 用于加密参与签名加密的key,同时也可以用于数据加密 rsa: privateKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJIMoK69fZKh3j4WAMIMdIVmGYA4ODlyexDoFmYKX5mzd1c01x logging: level: # 注意注意注意 一定要修改成自己的包名 com.itvip666: debug file: path: logs name: logs/assist-apisafe.log clean-history-on-start: true pattern: console: "%d{yyyy-MM-dd} [%thread] %-5level %logger{50} ===> %msg%n" 11.4.2. pom springboot-assist com.itvip666 1.0 4.0.0 assist-apisafe com.itvip666 assist-common 1.0 cn.hutool hutool-all 5.3.10 org.projectlombok lombok provided com.alibaba fastjson 1.2.51 org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 2.4.2 org.apache.commons commons-lang3 3.9 11.4.3. ApiSafe /** * Description: api接口安全注解,标注这个注解就代表必须登录才可以访问所标注的方法接口 * 本接口需要配合redis缓存使用 * @author shenguangyang * @date 2021/01/22 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ApiSafe { SafeType value() default SafeType.NONE; }

SafeType

public enum SafeType { /** * 无 */ NONE, /** * 签名,通常用于参数签名,防止参数被非法篡改,最常见的是修改金额等重要敏感参数, * sign的值一般是将所有非空参数按照升续排序然后+token+key+ts+nonce(随机数) * 拼接在一起,然后使用某种加密算法进行加密,这种方式的好处就是,当被劫持后,修改 * 其中的参数值,然后再继续调用接口,虽然参数的值被修改了,但是因为攻击者并不清楚 * sign是如何计算出来的,所以即可是篡改参数的值,但没法修改sign的值,当服务器调用 * 接口前会按照sign的规则重新计算出sign的值然后和接口传递的sign参数的值做比较, * 如果相等表示参数值没有被篡改,如果不等,表示参数被非法篡改了,则不会返回真实的响应信息。 * * 如果是h5页面,不法分子很容易知道是如何生成签名的,因此需要动态获取秘钥key , 然后进行加密 * 传输 * * 默认判断接收到的时间戳 - 当前时间戳 > 120 , 也就是说2分钟后,链接失效 * * 要求请求头中带有一下参数: * token: 令牌 * ts: 10位时间戳 (必填) * nonce: 8位随机数,数字+字母(必填) * sign: 签名 (必填,如果不使用签名,可以随便填写,只是不能为空) * * 签名格式 data(字典升序) + token + key(随机生成的秘钥) + ts(当前时间戳timestamp) + nonce(随机数) * 思考一个问题: * 如果是app可以通过加密固化处理,但是前端h5该如何处理呢,如果将参与生成签名的key, * 放入到页面中,会导致任意一个人访问网站后都可以按F12查看源代码,就知道key了,同时也知道生成 * 签名的流程,因此中间者可以修改参数后自己再重新生成签名。 * 解决办法: * 前端在调用接口前随机生成一个字符串,然后通过rsa公钥进行加密处理,将加密结果传输到 * 后端,后端存入到redis缓存中,失效时间设置为30s * 具体工作流程: * 1. 前端随机生成一个字符串,然后通过rsa公钥进行加密,将加密结果传输到后端,后端使用rsa私钥进行 * 解密,将结果存入到缓存中,失效时间为30s * 2. 前端将 将所有非空参数按照升续排序然后 / body(请求体) + token + key + ts(当前时间戳) + nonce 进行拼接 * md5加密 * 第一步, * 设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序), * 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA 或者 body(请求体)为字符串stringA。 * 特别注意以下重要规则: * 参数名ASCII码从小到大排序(字典序); * 如果参数的值为空不参与签名; * 参数名区分大小写; * 第二步, * 在stringA最后拼接上 + token + key + ts(当前时间戳) + nonce 得到stringSignTemp字符串, * 并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。 * 第三步 * 将 token , sign , ts , nonce 放入到请求头中访问后端 */ SIGN, /** * 加密 */ CRYPTO } 11.4.4. GlobalCorsConfig /** * Description: springboot中拦截器与跨域配置冲突解决 * 解决原理:一个http请求,先走filter,到达servlet后才进行拦截器的处理,所以我们可以 * 把cors放在filter里,就可以优先于权限拦截器执行。 * * 把 WebMvcConfigurer 接口中的 addCorsMappings 配置跨域去掉,这个跨域配置针对拦截器 * 的配置,如果你项目中使用了过滤器会发生问题 * @author shenguangyang * @date 2021/01/22 */ @Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { //1. 添加 CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //放行哪些原始域 config.addAllowedOrigin("*"); //是否发送 Cookie config.setAllowCredentials(true); //放行哪些请求方式 config.addAllowedMethod("*"); //放行哪些原始请求头部信息 config.addAllowedHeader("*"); //2. 添加映射路径 UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); corsConfigurationSource.registerCorsConfiguration("/**",config); //3. 返回新的CorsFilter return new CorsFilter(corsConfigurationSource); } } 11.4.5. Interceptor /** * 登录拦截器 * @author shenguangyang */ @Component public class Interceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(Interceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.debug("request url = " + request.getRequestURL()); // code和msg是从过滤器传过来的 Object code = request.getAttribute("code"); if ((code != null ) && ((int)code != ResultEnum.SUCCESS.getCode())) { String msg = (String) request.getAttribute("msg"); ApiUtils.result(response, (int) code, msg); return false; } return true; } } 11.4.6. RedisConfig /** * Description:redis配置,EnableCaching开启缓存 * * @author shenguangyang * @date 2020/12/31 */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean @Override public KeyGenerator keyGenerator() { return (o, method, objects) -> { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(o.getClass().getName()); stringBuilder.append(method.getName()); for (Object obj : objects) { stringBuilder.append(obj.toString()); } return stringBuilder.toString(); }; } //注意:方法名必须是redisTemplate /** * 重写Redis序列化方式,使用Json方式: * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。 * RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。 * Spring Data JPA为我们提供了下面的Serializer: * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、 * JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。 * 在此我们将自己配置RedisTemplate并定义Serializer。 * * @param redisConnectionFactory * @return */ @Bean public RedisTemplate myRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置默认的序列化json序列化 // 设置值(value)的序列化采用FastJsonRedisSerializer。 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); // redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); // 设置键(key)的序列化采用StringRedisSerializer。 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } private Jackson2JsonRedisSerializer jackson2JsonRedisSerializer() { Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); return jackson2JsonRedisSerializer; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { /*设置缓存过期时间 120s*/ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(120)) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); } } 11.4.7. WebFilterConfig /** * Description: 拦截器 * @author shenguangyang * @date 2021/01/20 */ @Configuration public class WebFilterConfig implements WebMvcConfigurer { /** * 防止redis报空,在context前初始化 * * @return */ @Bean public Interceptor getLoginHandlerInterceptor() { return new Interceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { InterceptorRegistration registration = registry.addInterceptor(getLoginHandlerInterceptor()); registration.addPathPatterns("/**"); registration.excludePathPatterns("/**/login"); } // /** // * 针对拦截器的配置跨域,如果项目中使用到了过滤器,就需要将该配置去掉,并配置跨域配置类 // * 中将CorsFilter类注入到容器中 // * @param registry // */ // @Override // public void addCorsMappings(CorsRegistry registry) { // registry.addMapping("/**") // .allowCredentials(true) // .allowedHeaders("*") // .allowedOrigins("*") // .allowedMethods("*"); // } } 11.4.8. TokenSignController /** * Description: 令牌签名控制层 * @author shenguangyang * @date 2021/01/20 */ @RestController @ResultBody @RequestMapping("token") public class TokenSignController { @Resource TokenSignService tokenSignService; @GetMapping("find") @ApiSafe(SafeType.SIGN) public User findUser(@RequestParam("uid") String uid) { User user = tokenSignService.findUser(uid); return user; } @PostMapping("save") @ApiSafe(SafeType.CRYPTO) public void save(@RequestBody User user) { System.out.println("保存成功 ---> " + user); } /** * 用户登录 * @return token */ @PostMapping("login") public String login(@RequestBody User user) { return tokenSignService.login(user); } } 11.4.9. RequestApiSafeFilter package com.itvip666.apisafe.filter; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.itvip666.apisafe.annotations.ApiSafe; import com.itvip666.apisafe.annotations.SafeType; import com.itvip666.apisafe.service.CacheService; import com.itvip666.apisafe.util.ApiUtils; import com.itvip666.apisafe.util.RSAUtils; import com.itvip666.apisafe.util.SignUtils; import com.itvip666.result.ResultEnum; import lombok.SneakyThrows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; import javax.servlet.FilterChain; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.util.*; /** * Description: 请求参数解密过滤器 / 签名 * 需要解密的方法,在对应方法或者类上添加注解@CryptoType(CryptoType.CRYPTO) * 前端加密会把参数通过AES方式加密成base64,“encrypt”: 加密后内容 * FIXME 当前只做请求参数加密,返回值未做加密 * * @author shenguangyang * @date 2021/01/22 */ @WebFilter(value = "/*",filterName ="RequestApiSafeFilter" ) public class RequestApiSafeFilter extends OncePerRequestFilter implements ApplicationContextAware { private static final Logger log = LoggerFactory.getLogger(RequestApiSafeFilter.class); @Autowired CacheService cacheService; /** * 用于加密参与签名加密的key */ @Value("${sign.rsa.privateKey}") private String signKeyRsaPrivateKey; /** 方法映射集 */ private List handlerMappings; private static final String APPLICATION_JSON = "application/json"; /** * 时间戳超时时间以及签名存在缓存中的存活时间 */ private static final long EXPIRE_TIME = 60; /** RSA加解密 */ @Value("${sign.rsa.privateKey}") private String rsaPrivateKey; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList(matchingBeans.values()); // We keep HandlerMappings in sorted order. AnnotationAwareOrderComparator.sort(this.handlerMappings); } } @SneakyThrows @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { String m1 = request.getMethod(); if (HttpMethod.OPTIONS.toString().equals(m1)){ response.setStatus(HttpStatus.NO_CONTENT.value()); filterChain.doFilter(request, response); return; } Object handler = getHandler(request).getHandler(); if (handler instanceof HandlerMethod) { // 方法上是否有注解 HandlerMethod method = (HandlerMethod) handler; ApiSafe type = method.getMethodAnnotation(ApiSafe.class); if (null == type) { // 类上是否有注解 type = method.getBeanType().getAnnotation(ApiSafe.class); if (null == type) { filterChain.doFilter(request, response); return; } } // 获取令牌,签名,以及时间戳 String token = request.getHeader("token"); String ts = request.getHeader("ts"); // 随机数,前端生成随机数主要保证签名的多变性 String nonce = request.getHeader("nonce"); String sign = request.getHeader("sign"); // 校验token是否合法 String key = "user:token:" + token; if (StringUtils.isEmpty(token) || !cacheService.hasKey(key)) { request.setAttribute("code",1005); request.setAttribute("msg","令牌失效,请重新登录!!!"); filterChain.doFilter(request, response); return; } // 判断是否是解密 // 如果是加密,则请求头中不需要携带nonce,ts , sign等字段 if (type.value() == SafeType.CRYPTO) { // 不是解密跳过 filterChain.doFilter(new DecryptRequest(request), response); return; } // 判断需要的参数是否为空,不为空进行后续操作 if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts) || StringUtils.isEmpty(nonce)){ request.setAttribute("code",1001); request.setAttribute("msg","非法参数!!!"); filterChain.doFilter(request, response); return; } // 进行续命操作判断ticket是否即将过期 Object vaule = cacheService.find(key); cacheService.delay(key , vaule , 10 * 60); System.out.println(SignUtils.getTimestamp()); // 判断是否需要验证签名 if (type.value() == SafeType.SIGN) { // 防止流读取一次后就没有了, 所以需要将流继续写出去 HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request); // 是签名 // 获取全部参数(包括URL和body上的) Map allParams = ApiUtils.getAllParams(requestWrapper); SignRequest signRequest = new SignRequest(); // 签名验证失败会往请求中写入数据 signRequest.verifySign(requestWrapper,allParams); filterChain.doFilter(requestWrapper, response); return; } } filterChain.doFilter(request, response); } /** * 获取访问目标方法 * * @param request * @return HandlerExecutionChain * @throws Exception */ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping hm : this.handlerMappings) { if (log.isTraceEnabled()) { log.trace("Testing handler map [" + hm + "] in DispatcherServlet with name ''"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } } return null; } /** * 签名请求 */ private class SignRequest { /** * 校验签名是否一致 */ public void verifySign(HttpServletRequest request , Map params) { String ts = request.getHeader("ts"); String sign = request.getHeader("sign"); // key由前端传过来的加密字符串,通过rsa进行加密 String key = request.getHeader("key"); try { // 判断是否重复访问 (重放攻击),存在重放攻击的时间窗口期 ,只能减轻重放攻击 if (SignUtils.getTimestamp() - Long.valueOf(ts) > EXPIRE_TIME){ request.setAttribute("code",1002); request.setAttribute("msg","连接服务器超时!!!"); return; } // [采用 sign存入缓存 + 重放攻击的时间窗口期]防止重放攻击 if (isRepeatedSubmit(EXPIRE_TIME,sign)) { request.setAttribute("code",1004); request.setAttribute("msg","请不要重复提交!!!"); return; } try { key = RSAUtils.decrypt(key, signKeyRsaPrivateKey); } catch (Exception e) { request.setAttribute("code", ResultEnum.ILLEGAL_PARAM.getCode()); request.setAttribute("msg",ResultEnum.ILLEGAL_PARAM.getMsg()); return; } // 验证签名 if (!SignUtils.checkSign(params , request , sign , key)){ request.setAttribute("code",1003); request.setAttribute("msg","无效的签名"); } } catch (Exception e) { request.setAttribute("code",1006); request.setAttribute("msg",e.getMessage()); } } /** * 判断是否是重复提交 * 每次请求进来,根据key查询redis,如果存在则说明是重复提交,给予结果,如果不存在,则是正常提交,将key存入redis。 * @param timeout 缓存超时时间 单位为s * @param sign 签名 */ protected boolean isRepeatedSubmit(long timeout , String sign) throws Exception { String key = "sign:" + sign; Object obj = cacheService.find(key); if (obj == null) { cacheService.save(key , "" , timeout); return false; } return true; } } /** * 解密request封装 * * @author ldy * */ private class DecryptRequest extends HttpServletRequestWrapper { private static final String APPLICATION_JSON = "application/json"; /** 所有参数的Map集合 */ private Map parameterMap; /** 输入流 */ private InputStream inputStream; public DecryptRequest(HttpServletRequest request) throws IOException { super(request); String contentType = request.getHeader("Content-Type"); log.debug("DecryptRequest -> contentType:{}", contentType); String encrypt = null; if (null != contentType && contentType.contains(APPLICATION_JSON)) { // json ServletInputStream io = request.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = io.read(buffer)) != -1) { os.write(buffer, 0, length); } byte[] bytes = os.toByteArray(); encrypt = (String) JSON.parseObject(new String(bytes)).get("encrypt"); } else { // url encrypt = request.getParameter("encrypt"); } log.debug("DecryptRequest -> encrypt:{}", encrypt); // 解密 String params = decrypt(encrypt); // 解密失败 if (StringUtils.isEmpty(params)) { request.setAttribute("code",1009); request.setAttribute("msg","服务器拒绝请求"); return; } if (null != contentType && contentType.contains(APPLICATION_JSON)) { if (this.inputStream == null) { this.inputStream = new DecryptInputStream(new ByteArrayInputStream(params.getBytes())); } } parameterMap = buildParams(params); } private String decrypt(String encrypt) throws IOException { try { // 解密 return RSAUtils.decrypt(encrypt,rsaPrivateKey); } catch (Exception e) { return null; } } private Map buildParams(String src) throws UnsupportedEncodingException { Map map = new HashMap(); Map params = JSONObject.parseObject(src, new TypeReference() { }); for (String key : params.keySet()) { map.put(key, new String[] { params.get(key) }); } return map; } @Override public String getParameter(String name) { String[] values = getParameterMap().get(name); if (values != null) { return (values.length > 0 ? values[0] : null); } return super.getParameter(name); } @Override public String[] getParameterValues(String name) { String[] values = getParameterMap().get(name); if (values != null) { return values; } return super.getParameterValues(name); } @Override public Enumeration getParameterNames() { Map multipartParameters = getParameterMap(); if (multipartParameters.isEmpty()) { return super.getParameterNames(); } Set paramNames = new LinkedHashSet(); Enumeration paramEnum = super.getParameterNames(); while (paramEnum.hasMoreElements()) { paramNames.add(paramEnum.nextElement()); } paramNames.addAll(multipartParameters.keySet()); return Collections.enumeration(paramNames); } @Override public Map getParameterMap() { return null == parameterMap ? super.getParameterMap() : parameterMap; } @Override public ServletInputStream getInputStream() throws IOException { return this.inputStream == null ? super.getInputStream() : (ServletInputStream) this.inputStream; } } /** * 自定义ServletInputStream * * @author ldy * */ private class DecryptInputStream extends ServletInputStream { private final InputStream sourceStream; /** * Create a DelegatingServletInputStream for the given source stream. * * @param sourceStream * the source stream (never {@code null}) */ public DecryptInputStream(InputStream sourceStream) { Assert.notNull(sourceStream, "Source InputStream must not be null"); this.sourceStream = sourceStream; } @Override public int read() throws IOException { return this.sourceStream.read(); } @Override public void close() throws IOException { super.close(); this.sourceStream.close(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } } } 11.4.10. BodyReaderHttpServletRequestWrapper package com.itvip666.apisafe.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.Charset; /** * Description: 保存过滤器里面的流 * BodyReaderHttpServletRequestWrapper 类 主要作用是复制 HttpServletRequest 的输入流, * 不然你拿出 body 参数后验签后,到 Controller 时,接收参数会为 null * @author shenguangyang * @date 2021/01/22 */ public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { private static final Logger log = LoggerFactory.getLogger(BodyReaderHttpServletRequestWrapper.class); private final byte[] body; public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) { super(request); String sessionStream = getBodyString(request); body = sessionStream.getBytes(Charset.forName("UTF-8")); } /** * 获取请求Body * * @param request * @return */ public String getBodyString(final ServletRequest request) { StringBuilder sb = new StringBuilder(); try ( InputStream inputStream = cloneInputStream(request.getInputStream()); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))) ) { String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } /** * Description: 复制输入流 * * @param inputStream * @return */ public InputStream cloneInputStream(ServletInputStream inputStream) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; try { while ((len = inputStream.read(buffer)) > -1) { byteArrayOutputStream.write(buffer, 0, len); } byteArrayOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } return new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() { return bais.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } } 11.4.11. pojo

CacheKeyPrefix

/** * Description: 存放缓存key的前缀 * @author shenguangyang * @date 2021/01/22 */ public interface CacheKeyPrefix { String SIGN_KEY_PRE_KEY = "sign:key:"; }

User

/** * Description: 用户表 * @author shenguangyang * @date 2021/01/20 */ @Data public class User { private String id; private String username; private String password; private String token; } 11.4.12. service /** * Description: 缓存服务 * @author shenguangyang * @date 2021/01/20 */ public interface CacheService { /** * 保存 * @param key 键 * @param value 值 * @param timeout 超时时间,单位为s */ void save(Object key , Object value , long timeout); /** * 对key进行延时,如果key即将过期,将重新设置过期时间为 timeout s * @param key 键 * @param value 值 * @param timeout 过期时间 单位为s */ void delay(Object key , Object value , long timeout); /** * 查找 * @param key * @return */ Object find(Object key); /*** * 判断是否有某个key */ Boolean hasKey(Object key); } /** * Description: 缓存服务 * @author shenguangyang * @date 2021/01/20 */ @Service public class CacheServiceImpl implements CacheService { @Resource RedisTemplate myRedisTemplate; @Override public void save(Object key, Object value, long timeout) { myRedisTemplate.opsForValue().set(key,value,timeout, TimeUnit.SECONDS); } @Override public void delay(Object key, Object value, long timeout) { if (myRedisTemplate.opsForValue().getOperations().getExpire(key) != -2 && myRedisTemplate.opsForValue().getOperations().getExpire(key) Object o = myRedisTemplate.opsForValue().get(key); return o; } @Override public Boolean hasKey(Object key) { Boolean aBoolean = myRedisTemplate.hasKey(key); return aBoolean; } } /** * Description: RSA服务 * @author shenguangyang * @date 2021/01/22 */ public interface RSAService { /** * 保存Api签名key到缓存中,默认过期时间为30s * @return 返回缓存中的key */ String saveApiSignKeyToCache(String key); } /** * Description: RSA非对称加密服务 * @author shenguangyang * @date 2021/01/22 */ @Service public class RSAServiceImpl implements RSAService { @Resource CacheService cacheService; @Override public String saveApiSignKeyToCache(String key) { String cache = UUID.randomUUID().toString().replace("-",""); cacheService.save(CacheKeyPrefix.SIGN_KEY_PRE_KEY + cache,key,30); return cache; } } /** * Description: 令牌签名服务层 * @author shenguangyang * @date 2021/01/20 */ public interface TokenSignService { /** * 用户登录 * @param user 用户对象 * @return token */ String login(User user); /** * 通过用户id查找用户 * @param uid * @return */ User findUser(String uid); } /** * Description: * @author shenguangyang * @date 2021/01/20 */ @Service public class TokenSignServiceImpl implements TokenSignService { @Resource CacheService cacheService; @Override public String login(User user) { String username = user.getUsername(); String password = user.getPassword(); if (username.equals("test") && password.equals("test")) { String token = UUID.randomUUID().toString().replace("-",""); cacheService.save("user:token:" + token , user , 10 * 60); return token; } else { throw new BusinessException(ResultEnum.USERNAME_PASSOWRD_ERROR); } } @Override public User findUser(String uid) { return null; } } 11.4.13. util

ApiUtils

package com.itvip666.apisafe.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.itvip666.result.Result; import org.springframework.http.HttpMethod; import org.springframework.web.method.HandlerMethod; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.lang.reflect.Method; import java.util.*; /** * Description: Api工具类 * @author shenguangyang * @date 2021/01/21 */ public class ApiUtils { /** * 响应结果 * @param response * @param code 错误代码 * @param msg 错误消息 * @throws Exception */ public static void result(HttpServletResponse response , int code , String msg) throws Exception { response.setContentType("application/json; charset=UTF-8"); Result result = Result.failure(code, msg); PrintWriter out = response.getWriter(); out.write(JSON.toJSONString(result)); } /** * 将URL的参数和body参数合并,如果是get请求是不需要获取body参数,只需要获取url参数 * @author show * @date 14:24 2019/5/29 * @param request */ public static Map getAllParams(HttpServletRequest request) throws IOException { Map result = new HashMap(); //获取URL上的参数 Map urlParams = getUrlParams(request); for (Map.Entry entry : urlParams.entrySet()) { result.put((String) entry.getKey(), (String) entry.getValue()); } Map allRequestParam = new HashMap(16); // get请求不需要拿body参数 if (!HttpMethod.GET.name().equals(request.getMethod())) { allRequestParam = getAllRequestParam(request); } //将URL的参数和body参数进行合并 if (allRequestParam != null) { for (Map.Entry entry : allRequestParam.entrySet()) { result.put((String) entry.getKey(), (String) entry.getValue()); } } return result; } /** * 获取 Body 参数 * @author show * @date 15:04 2019/5/30 * @param request */ public static Map getAllRequestParam(final HttpServletRequest request) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream())); String str = ""; StringBuilder wholeStr = new StringBuilder(); //一行一行的读取body体里面的内容; while ((str = reader.readLine()) != null) { wholeStr.append(str); } //转化成json对象 return !(wholeStr.length() == 0) ? JSONObject.parseObject(wholeStr.toString(), Map.class) : new HashMap(); } /** * 将URL请求参数转换成Map * @author show * @param request */ public static Map getUrlParams(HttpServletRequest request) { Map params = new HashMap(); Enumeration pNames = request.getParameterNames(); while (pNames.hasMoreElements()) { String pName = (String) pNames.nextElement(); String pValue = (String)request.getParameter(pName); params.put(pName, pValue); } return params; } }

MD5Utils

package com.itvip666.apisafe.util; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Description: MD5加密/验证工具类 * @author shenguangyang * @date 2021/01/20 */ public class MD5Utils { static final char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; static final char hexDigitsLower[] = { '0', '1', '2', '3', '4', '5', '6', '7','8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * 对字符串 MD5 无盐值加密 * * @param plainText * 传入要加密的字符串 * @return * MD5加密后生成32位(小写字母+数字)字符串 */ public static String MD5Lower(String plainText) { try { // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest md = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 md.update(plainText.getBytes()); // digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符 // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值。1 固定值 return new BigInteger(1, md.digest()).toString(16); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } /** * 对字符串 MD5 加密 * * @param plainText * 传入要加密的字符串 * @return * MD5加密后生成32位(大写字母+数字)字符串 */ public static String MD5Upper(String plainText) { try { // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest md = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 md.update(plainText.getBytes()); // 获得密文 byte[] mdResult = md.digest(); // 把密文转换成十六进制的字符串形式 int j = mdResult.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i e.printStackTrace(); return null; } } /** * 对字符串 MD5 加盐值加密 * * @param plainText * 传入要加密的字符串 * @param saltValue * 传入要加的盐值 * @return * MD5加密后生成32位(小写字母+数字)字符串 */ public static String MD5Lower(String plainText, String saltValue) { try { // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest md = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 md.update(plainText.getBytes()); md.update(saltValue.getBytes()); // digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符 // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值。1 固定值 return new BigInteger(1, md.digest()).toString(16); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } /** * 对字符串 MD5 加盐值加密 * * @param plainText * 传入要加密的字符串 * @param saltValue * 传入要加的盐值 * @return * MD5加密后生成32位(大写字母+数字)字符串 */ public static String MD5Upper(String plainText, String saltValue) { try { // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest md = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 md.update(plainText.getBytes()); md.update(saltValue.getBytes()); // 获得密文 byte[] mdResult = md.digest(); // 把密文转换成十六进制的字符串形式 int j = mdResult.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i e.printStackTrace(); return null; } } /** * MD5加密后生成32位(小写字母+数字)字符串 * 同 MD5Lower() 一样 */ public final static String MD5(String plainText) { try { MessageDigest mdTemp = MessageDigest.getInstance("MD5"); mdTemp.update(plainText.getBytes("UTF-8")); byte[] md = mdTemp.digest(); int j = md.length; char str[] = new char[j * 2]; int k = 0; for (int i = 0; i return null; } } /** * 校验MD5码 * * @param text * 要校验的字符串 * @param md5 * md5值 * @return 校验结果 */ public static boolean valid(String text, String md5) { return md5.equals(MD5(text)) || md5.equals(MD5(text).toUpperCase()); } /** * 测试 * @param args */ public static void main(String[] args) { String plainText = "admin"; String saltValue = "admin123"; System.out.println(MD5Lower(plainText)); System.out.println(MD5Upper(plainText)); System.out.println(MD5Lower(plainText, saltValue)); System.out.println(MD5Upper(plainText, saltValue)); System.out.println(MD5(plainText)); System.out.println("=====校验结果======"); System.out.println(valid(plainText,MD5(plainText))); } }

RSAUtils

package com.itvip666.apisafe.util; import javax.crypto.Cipher; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.HashMap; import java.util.Map; /** * Description: RSA加密工具类 * @author shenguangyang * @date 2021/01/20 */ public class RSAUtils { /** * 密钥长度 于原文长度对应 以及越长速度越慢 */ private final static int KEY_SIZE = 1024; /** * 用于封装随机产生的公钥与私钥 */ private static Map keyMap = new HashMap(); /** * 随机生成密钥对 */ public static void genKeyPair() throws NoSuchAlgorithmException { // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); // 初始化密钥对生成器 keyPairGen.initialize(KEY_SIZE, new SecureRandom()); // 生成一个密钥对,保存在keyPair中 KeyPair keyPair = keyPairGen.generateKeyPair(); // 得到私钥 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到公钥 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded()); // 得到私钥字符串 String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded()); // 将公钥和私钥保存到Map //0表示公钥 keyMap.put(0, publicKeyString); //1表示私钥 keyMap.put(1, privateKeyString); } /** * RSA公钥加密 * * @param str 加密字符串 * @param publicKey 公钥 * @return 密文 * @throws Exception 加密过程中的异常信息 */ public static String encrypt(String str, String publicKey) throws Exception { //base64编码的公钥 byte[] decoded = Base64.getDecoder().decode(publicKey); RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); //RSA加密 Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); String outStr = Base64.getEncoder().encodeToString(cipher.doFinal(str.getBytes("UTF-8"))); return outStr; } /** * RSA私钥解密 * * @param str 加密字符串 * @param privateKey 私钥 * @return 明文 * @throws Exception 解密过程中的异常信息 */ public static String decrypt(String str, String privateKey) throws Exception { //64位解码加密后的字符串 byte[] inputByte = Base64.getDecoder().decode(str); //base64编码的私钥 byte[] decoded = Base64.getDecoder().decode(privateKey); RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); //RSA解密 Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, priKey); String outStr = new String(cipher.doFinal(inputByte)); return outStr; } public static void main(String[] args) throws Exception { long temp = System.currentTimeMillis(); //生成公钥和私钥 genKeyPair(); //加密字符串 System.out.println("公钥:" + keyMap.get(0)); System.out.println("私钥:" + keyMap.get(1)); System.out.println("生成密钥消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); //客户id + 授权时间 + 所用模块 String message = "4028138151b3cf300151b419df0904028138151b3cf300151b419df0900074028138151b3cf300151b419df0900074028138151b3cf300151b419df090007007" + "2015-12-17 11:30:22" + "A01,A02"; System.out.println("原文:" + message); System.out.println("原文MD5加密后 : " + (message = MD5Utils.MD5(message))); temp = System.currentTimeMillis(); //通过原文,和公钥加密。 String messageEn = encrypt(message, keyMap.get(0)); System.out.println("密文:" + messageEn); System.out.println("加密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); temp = System.currentTimeMillis(); //通过密文,和私钥解密。 String messageDe = decrypt(messageEn, keyMap.get(1) + "21"); System.out.println("解密:" + messageDe); System.out.println("解密消耗时间:" + (System.currentTimeMillis() - temp) / 1000.0 + "秒"); String message1 = "402813818151b3cf300151b419df090007007" + "2015-12-17 11:30:22" + "A01,A02"; System.out.println("message1: " + message1); System.out.println("原文MD5加密后 : " + (message1 = MD5Utils.MD5(message1))); String messageEn1 = encrypt(message1, keyMap.get(0)); System.out.println("密文:" + messageEn1); String messageDe1 = decrypt(messageEn1, keyMap.get(1)); System.out.println("解密:" + messageDe1); } }

SignUtils

package com.itvip666.apisafe.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.util.*; import java.util.concurrent.TimeUnit; public class SignUtils { private static final Logger log = LoggerFactory.getLogger(SignUtils.class); private static final String secretKeyOfWxh = "e10adc3949ba59abbe56e057f20f883f"; private static final String APPLICATION_JSON = "application/json"; public static void main(String[] args) throws Exception { // 参数签名算法测试 HashMap signMap = new HashMap(); String ts = String.valueOf(getTimestamp()); String token = ""; String nonce = ""; signMap.put("token","daca676bb80e46a5a36e6abe85b1a86d"); signMap.put("nonce","123456"); signMap.put("uid","2"); signMap.put("ts",ts); String verifySign = getSign(signMap, ts , token , nonce , secretKeyOfWxh); System.out.println("得到签名sign1:"+verifySign); System.out.println(ts); } /** * 校验签名是否合法 * @param params 访问的全部参数 * @param request 请求 * @param sign 客户端传过来的签名 * @param secretKeyOfWxh 加密使用的秘钥 * @return */ public static Boolean checkSign(Map params , HttpServletRequest request , String sign , String secretKeyOfWxh) throws Exception { Boolean flag= false; String token = request.getHeader("token"); String ts = request.getHeader("ts"); // 随机数,前端生成随机数主要保证签名的多变性 String nonce = request.getHeader("nonce"); String verifySign = getSign(params, ts , token , nonce , secretKeyOfWxh); log.debug("现在的sign-->>" + sign); log.debug("验证的sign-->>" + verifySign); if(sign.equals(verifySign)){ flag = true; } return flag; } /** * 得到签名 * 签名格式 data(字典升序) + token + key(随机生成的秘钥) + ts(当前时间戳timestamp) + nonce(随机数) * @param params 参数集合不含secretkey * @param secret 验证接口的secretkey * @param token 令牌 * @param nonce 随机字符串 * @param ts 时间戳 * @return */ public static String getSign(Map params, String ts , String token , String nonce , String secret) throws IOException { String sign=""; StringBuilder sb = new StringBuilder(); //step1:先对请求参数排序 Set keyset = params.keySet(); TreeSet sortSet = new TreeSet(); sortSet.addAll(keyset); Iterator it = sortSet.iterator(); //step2:把参数的key value链接起来 secretkey放在最后面,得到要加密的字符串 while(it.hasNext()) { String key=it.next(); String value=params.get(key); sb.append(key).append(value); } sb.append(token).append(secret).append(ts).append(nonce); byte[] md5Digest; // 得到Md5加密得到sign md5Digest = getMD5Digest(sb.toString()); sign = byte2hex(md5Digest); return sign; } public static long getTimestamp(){ long timestampLong = System.currentTimeMillis(); long timestampsStr = timestampLong / 1000; return timestampsStr; } public static String utf8Encoding(String value, String sourceCharsetName) { try { return new String(value.getBytes(sourceCharsetName), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalArgumentException(e); } } private static byte[] getMD5Digest(String data) throws IOException { byte[] bytes = null; try { MessageDigest md = MessageDigest.getInstance("MD5"); bytes = md.digest(data.getBytes("UTF-8")); } catch (GeneralSecurityException gse) { throw new IOException(gse); } return bytes; } private static String byte2hex(byte[] bytes) { StringBuilder sign = new StringBuilder(); for (int i = 0; i sign.append("0"); } sign.append(hex.toUpperCase()); } return sign.toString(); } } 11.4.14. Main /** * api接口安全 * 所有的请求必须携带以下参数 * 1. ts: 时间戳 * 2. sign: 签名 (必填,如果不使用签名,可以随便填写,只是不能为空) * 3. token: 令牌 * 4. nonce: 随机字符串 * @author shenguangyang */ @SpringBootApplication @ServletComponentScan("com.itvip666.apisafe.filter") public class ApiSafeMain { public static void main(String[] args) { SpringApplication.run(ApiSafeMain.class,args); } } 12. 前端使用 12.1. 目录结果

在这里插入图片描述

12.2. 代码 12.2.1. axios.js /** * axios.js提供request请求封装 * 包括 get、post、delete、put等方式 * @author: sgy */ import axios from 'axios'; import store from '@/store'; import router from '@/router' import {aes, sign} from '@/common/js/crypto'; import {RSAencrypt} from "@/common/js/encrypt" import { Message } from 'element-ui'; const ajax = axios.create({ // baseURL: store.getters.serviceHost, // url前缀 baseURL: 'http://127.0.0.1:9090', timeout: 10000, // 超时毫秒数 withCredentials: true // 携带认证信息cookie }); /** * get方式请求,url传参 * @param url 请求url * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 */ const get = (url, params, level) => ajax(getConfig(url, 'get', true, params, level)).then(res => successback(res)).catch(error => errback(error)); /** * post方式请求 json方式传参 * @param url 请求url * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 */ const postJson = (url, params, level) => ajax(getConfig(url, 'post', true, params, level)).then(res => successback(res)).catch(error => errback(error)); /** * post方式请求 表单传参 * @param url 请求url * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 */ const post = (url, params, level) => ajax(getConfig(url, 'post', false, params, level)).then(res => successback(res)).catch(error => errback(error)); /** * delete方式请求 url传参 * @param url 请求url * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 */ const del = (url, params, level) => ajax(getConfig(url, 'delete', true, params, level)).then(res => successback(res)).catch(error => errback(error)); /** * put方式请求 json传参 * @param url 请求url * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 */ const putJson = (url, params, level) => ajax(getConfig(url, 'put', true, params, level)).then(res => successback(res)).catch(error => errback(error)); /** * put方式请求 表单传参 * @param url 请求url * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 */ const put = (url, params, level) => ajax(getConfig(url, 'put', false, params, level)).then(res => successback(res)).catch(error => errback(error)); // 参数转换 const param2String = data => { console.log('data', data); if (typeof data === 'string') { return data; } let ret = ''; for (let it in data) { let val = data[it]; if (typeof val === 'object' && // (!(val instanceof Array) || (val.length > 0 && (typeof val[0] === 'object')))) { val = JSON.stringify(val); } ret += it + '=' + encodeURIComponent(val) + '&'; } if (ret.length > 0) { ret = ret.substring(0, ret.length - 1); } return ret; }; /** * 生成随机字符串 * @param len 指定长度 */ const randomString = len => { len = len || 32; let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/ let maxPos = $chars.length; let pwd = '';  for (let i = 0; i if ('code' in error) { // 未登录 if (error.code === 1005) { sessionStorage.clear(); router.push({path: '/login',query: {redirect: router.currentRoute.fullPath}}) return {}; } return Promise.reject(error); } // 网络异常,或链接超时 Message({ message: error.message, type: 'error' }); return Promise.reject({data: error.message}); }; // 成功回调函数 const successback = res => { if (res.status === 200 && res.data.code !== 200) { Message({ message: res.data.msg, type: 'error' }); return Promise.reject(res.data); } return res.data; }; /** * 从1970年开始的毫秒数然后截取10位变成 从1970年开始的秒数 * @return 返回10位时间戳 */ const timestamp = () => { // new Date().getTime(); let tmp = Date.parse( new Date() ).toString(); tmp = tmp.substr(0,10); return tmp; } /** * @param url 请求url * @param method get,post,put,delete * @param isjson 是否json提交参数 * @param params 参数 * @param level 0:无加密,1:参数加密,2: 签名+时间戳; 默认0 * 签名格式 data(字典升序) + token + key(随机生成的秘钥) + ts(当前时间戳timestamp) + nonce(随机数) */ const getConfig = (url, method, isjson, params, level = 0) => { let config_ = { url, method, // params, data, headers: { level } }; // 获取token let token = store.state.token; if (!token) { let user = sessionStorage.getItem('user') token = user == null ? '' : JSON.parse(user).token; store.state.token = token; } // 时间戳 if (level === 1) { // 加密 params = {encrypt: RSAencrypt(JSON.stringify(params))}; config_.headers = { level, token, }; } else if (level === 2) { // 签名 let ts = timestamp() // 签名key let key = randomString(16); // 加密后的签名key let keyEncrypt = RSAencrypt(key) // 随机字符串 let nonce = randomString(10) // 签名串 let signstr = sign(token, ts , params , key , nonce); console.log("params",params); console.log('token', token); console.log('sign', signstr); console.log('nonce',nonce); console.log('ts',ts); config_.headers = { level, ts, sign: signstr, token, nonce, key: keyEncrypt }; } // 表单提交参数 if (!isjson) { config_.headers['Content-Type'] = 'application/x-www-form-urlencoded'; config_.responseType = 'text'; config_.transformRequest = [function (data) { return param2String(data); }]; } // 设置参数 if (method in {'get': true, 'delete': true}) { config_.params = params; } else if (method in {'post': true, 'put': true}) { config_.data = params; } return config_; }; // 统一方法输出口 export { ajax, get, postJson, post, del, putJson, put }; 12.2.2. cypto /** * 通过crypto-js实现 加解密工具 * AES、HASH(MD5、SHA256)、base64 * @author: sgy */ import CryptoJS from 'crypto-js'; const KP = { key: '1234567812345678', // 秘钥 16*n: iv: '1234567812345678' // 偏移量 }; function getAesString(data, key, iv) { // 加密 key = CryptoJS.enc.Utf8.parse(key); // alert(key); iv = CryptoJS.enc.Utf8.parse(iv); let encrypted = CryptoJS.AES.encrypt(data, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 返回的是base64格式的密文 } function getDAesString(encrypted, key, iv) { // 解密 key = CryptoJS.enc.Utf8.parse(key); iv = CryptoJS.enc.Utf8.parse(iv); let decrypted = CryptoJS.AES.decrypt(encrypted, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); // } // AES 对称秘钥加密 const aes = { en: (data) => getAesString(data, KP.key, KP.iv), de: (data) => getDAesString(data, KP.key, KP.iv) }; // BASE64 const base64 = { en: (data) => CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)), de: (data) => CryptoJS.enc.Base64.parse(data).toString(CryptoJS.enc.Utf8) }; // SHA256 const sha256 = (data) => { return CryptoJS.SHA256(data).toString(); }; // MD5 const md5 = (data) => { return CryptoJS.MD5(data).toString(); }; /** * 签名 * @param token 身份令牌 * @param timestamp 签名时间戳 * @param data 签名数据 * @param key 签名key * @param nonce 随机字符串 */ const sign = (token, timestamp, data , key , nonce) => { // 签名格式 data(字典升序) + token + key(随机生成的秘钥) + ts(当前时间戳timestamp) + nonce(随机数) let ret = []; for (let it in data) { let val = data[it]; if (typeof val === 'object' && // (!(val instanceof Array) || (val.length > 0 && (typeof val[0] === 'object')))) { val = JSON.stringify(val); } ret.push(it + val); } // 字典升序 ret.sort(); let signsrc = ret.join('') + token + key + timestamp + nonce; return md5(signsrc).toUpperCase(); }; export { aes, md5, sha256, base64, sign }; 12.2.3. encrypt import Vue from 'vue' import JsEncrypt from 'jsencrypt' Vue.prototype.$jsEncrypt = JsEncrypt /** * 接口安全 * * 1. 安装 * jsencrypt是一个ras加密解密库 * npm install jsencrypt --save * * 2. main.js中引入 * import JsEncrypt from 'jsencrypt' * Vue.prototype.$jsEncrypt = JsEncrypt */ let publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCSDKCuvX2Sod4+FgDCDHSFZhmAODg5cnsQ6BZmCl+Zs3dXNNcU4DOTbCCvmT1dXDQyM9DKh6jog6q1hWAB1dUDa1VNiKZNZbZMZKReO1At4y0xh1EVRNFFnLSzzr1+JY1k6McuoiVfpfqRSn7Vw4IcpzJVc3T8jpRcqnstgMLKMQIDAQAB' let privateKey = '这里是封装的私钥' //加密方法 export function RSAencrypt(pas) { //实例化jsEncrypt对象 let jse = new JSEncrypt(); //设置公钥 jse.setPublicKey(publicKey); // console.log('加密:'+jse.encrypt(pas)) return jse.encrypt(pas); } //解密方法 export function RSAdecrypt(pas) { let jse = new JSEncrypt(); // 私钥 jse.setPrivateKey(privateKey) // console.log('解密:'+jse.decrypt(pas)) return jse.decrypt(pas); } 12.2.4. router/index.js import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/', name: 'home', component: () => import("@/views/Home.vue") }, { path: '/login', name: 'login', component: () => import("@/views/Login.vue") } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router 12.2.5. views

Home.vue

默认按钮 查找用户 保存用户 信息按钮 警告按钮 import {get,postJson} from "@/common/js/axios" export default { methods: { open1() { get("/token/find",{uid:2},2).then(res => { if ('code' in res) { console.log(res); this.$message({ showClose: true, message: '查找用户' }); } }); }, open2() { postJson("/token/save",{id: "",username:"test",password:"test"},1).then(res => { if ('code' in res) { console.log(res); this.$message({ showClose: true, message: '保存用户', type: 'success' }); } }); }, open3() { this.$message({ showClose: true, message: '警告哦,这是一条警告消息', type: 'warning' }); }, open4() { this.$message({ showClose: true, message: '错了哦,这是一条错误消息', type: 'error' }); } } }

Login.vue

登录 取消 import {postJson} from "@/common/js/axios" export default { data() { return { form: { username: '', password: '', token: '' } } }, methods: { onSubmit() { postJson("/token/login",this.form,0).then(res => { console.log(res); if ( 'code' in res ) { // 将对象转为json this.form.token = res.data sessionStorage.setItem('user',JSON.stringify(this.form)) // 从哪来回哪去 if (this.$route.query.redirect) { // 跳转到原页面 this.$router.push({path: decodeURIComponent(this.$route.query.redirect)}) } else { this.$router.push("/") //正常登录流程进入的页面 } } }) } } } .login { width: 400px; } 12.2.6. main.js import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); import axios from "axios" axios.defaults.baseURL = "http://127.0.0.1:8080" axios.defaults.timeout = 5000 // 将axios挂载在Vue原型链上 Vue.prototype.$axios=axios Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3