WEB 资源或 API 方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性是系统的接口对外一种承诺(而不是实现), 承诺只要调用接口成功, 外部多次调用对系统的影响是一致的。幂等性是分布式系统设计中的一个重要概念,对超时处理、系统恢复等具有重要意义。声明为幂等的接口会认为外部调用失败是常态, 并且失败之后必然会有重试。例如,在因网络中断等原因导致请求方未能收到请求返回值的情况下,如果该资源具备幂等性,请求方只需要重新请求即可,而无需担心重复调用会产生错误。实际上,我们常用的 HTTP 协议的方法是具有幂等性语义要求的,比如:get 方法用于获取资源,不应有副作用,因此是幂等的;post 方法用于创建资源,每次请求都会产生新的资源,因此不具备幂等性;put 方法用于更新资源,是幂等的;delete 方法用于删除资源,也是幂等的。
MVCC方案
多版本并发控制,该策略主要使用 update with condition(更新带条件来防止)来保证多次外部请求调用对系统的影响是一致的。在系统设计的过程中,合理的使用乐观锁,通过 version 或者 updateTime(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。例如
select * from tablename where condition=#condition# // 取出要跟新的对象,带有版本 versoin
update tableName set name=#name#,version=version+1 where version=#version#
在更新的过程中利用 version 来防止,其他操作对对象的并发更新,导致更新丢失。为了避免失败,通常需要一定的重试机制。
去重表
在插入数据的时候,插入去重表,利用数据库的唯一索引特性,保证唯一的逻辑。
悲观锁
select for update,整个执行过程中锁定该订单对应的记录。注意:这种在 DB 读大于写的情况下尽量少用。
Token机制,防止页面重复提交
业务要求:页面的数据只能被点击提交一次
发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交
解决办法:
集群环境:采用 token 加 redis(redis 单线程的,处理需要排队)
单 JVM 环境:采用 token 加 redis 或 token 加 jvm 内存
处理流程:
数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间
提交后后台校验 token,同时删除 token,生成新的 token 返回
token 特点:要申请,一次有效性,可以限流
基于Token方式防止API接口幂等
客户端每次在调用接口的时候,需要在请求头中,传递令牌参数,每次令牌只能用一次。
一旦使用之后,就会被删除,这样可以有效防止重复提交。
步骤:
1.生成令牌接口
2. 接口中获取令牌验证
生成令牌接口
public class TokenUtils {
private static Map
// 获取token
public static synchronized String getToken() {
// 1.生成令牌
String token = "token-" + System.currentTimeMillis();
// 2.存入tokenMap
tokenMap.put(token, token);
return token;
}
// 验证token,并且删除对应的token
public static Boolean exisToken(String token) {
// 1.从集合中获取token
Object result = tokenMap.get(token);
if (result == null) {
return false;
}
// 2.删除对应的token
tokenMap.remove(token);
return true;
}
}
接口中获取令牌验证
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
// 获取Token
@RequestMapping("/getToken")
public String getToken() {
return TokenUtils.getToken();
}
// 验证Token
@RequestMapping(value = "/addOrder", produces = "application/json; charset=utf-8")
public String addOrder(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
return "参数错误!";
}
if (!TokenUtils.exisToken(token)) {
return "请勿重复提交!";
}
int result = orderMapper.addOrder(orderEntity);
return result > 0 ? "添加成功" : "添加失败" + "";
}
}
@Component public class BaseRedisService {
@Autowired private StringRedisTemplate stringRedisTemplate;
public void setString(String key, Object data, Long timeout) { if (data instanceof String) { String value = (String) data; stringRedisTemplate.opsForValue().set(key, value); } if (timeout != null) { stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } }
public Object getString(String key) { return stringRedisTemplate.opsForValue().get(key); }
public void delKey(String key) { stringRedisTemplate.delete(key); }
} |
@Component public class RedisTokenUtils { private long timeout = 60 * 60; @Autowired private BaseRedisService baseRedisService;
// 将token存入在redis public String getToken() { String token = "token" + System.currentTimeMillis(); baseRedisService.setString(token, token, timeout); return token; }
public boolean findToken(String tokenKey) { String token = (String) baseRedisService.getString(tokenKey); if (StringUtils.isEmpty(token)) { return false; } // token 获取成功后 删除对应tokenMapstoken baseRedisService.delKey(token); return true; }
} |
@Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ExtApiIdempotent { String value(); } |
@Aspect @Component public class ExtApiAopIdempotent { @Autowired private RedisTokenUtils redisTokenUtils;
@Pointcut("execution(public * com.itmayiedu.controller.*.*(..))") public void rlAop() { }
@Around("rlAop()") public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent == null) { // 直接执行程序 Object proceed = proceedingJoinPoint.proceed(); return proceed; } // 代码步骤: // 1.获取令牌 存放在请求头中 HttpServletRequest request = getRequest(); String token = request.getHeader("token"); if (StringUtils.isEmpty(token)) { response("参数错误!"); return null; } // 2.判断令牌是否在缓存中有对应的令牌 // 3.如何缓存没有该令牌的话,直接报错(请勿重复提交) // 4.如何缓存有该令牌的话,直接执行该业务逻辑 // 5.执行完业务逻辑之后,直接删除该令牌。 if (!redisTokenUtils.findToken(token)) { response("请勿重复提交!"); return null; } Object proceed = proceedingJoinPoint.proceed(); return proceed; }
public HttpServletRequest getRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; }
public void response(String msg) throws IOException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletResponse response = attributes.getResponse(); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); try { writer.println(msg); } catch (Exception e) {
} finally { writer.close(); }
}
} |
// 从redis中获取Token @RequestMapping("/redisToken") public String RedisToken() { return redisTokenUtils.getToken(); }
// 验证Token @RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8") @ExtApiIdempotent public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) { int result = orderMapper.addOrder(orderEntity); return result > 0 ? "添加成功" : "添加失败" + ""; } |
@Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ExtApiToken {
}
|
@Aspect @Component public class ExtApiAopIdempotent { @Autowired private RedisTokenUtils redisTokenUtils;
@Pointcut("execution(public * com.itmayiedu.controller.*.*(..))") public void rlAop() { }
// 前置通知转发Token参数 @Before("rlAop()") public void before(JoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class); if (extApiToken != null) { extApiToken(); } }
// 环绕通知验证参数 @Around("rlAop()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent != null) { return extApiIdempotent(proceedingJoinPoint, signature); } // 放行 Object proceed = proceedingJoinPoint.proceed(); return proceed; }
// 验证Token public Object extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature) throws Throwable { ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent == null) { // 直接执行程序 Object proceed = proceedingJoinPoint.proceed(); return proceed; } // 代码步骤: // 1.获取令牌 存放在请求头中 HttpServletRequest request = getRequest(); String valueType = extApiIdempotent.value(); if (StringUtils.isEmpty(valueType)) { response("参数错误!"); return null; } String token = null; if (valueType.equals(ConstantUtils.EXTAPIHEAD)) { token = request.getHeader("token"); } else { token = request.getParameter("token"); } if (StringUtils.isEmpty(token)) { response("参数错误!"); return null; } if (!redisTokenUtils.findToken(token)) { response("请勿重复提交!"); return null; } Object proceed = proceedingJoinPoint.proceed(); return proceed; }
public void extApiToken() { String token = redisTokenUtils.getToken(); getRequest().setAttribute("token", token);
}
public HttpServletRequest getRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; }
public void response(String msg) throws IOException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletResponse response = attributes.getResponse(); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); try { writer.println(msg); } catch (Exception e) {
} finally { writer.close(); }
}
} |
@RestController public class OrderController {
@Autowired private OrderMapper orderMapper; @Autowired private RedisTokenUtils redisTokenUtils;
// 从redis中获取Token @RequestMapping("/redisToken") public String RedisToken() { return redisTokenUtils.getToken(); }
// 验证Token @RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8") @ExtApiIdempotent(value = ConstantUtils.EXTAPIHEAD) public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) { int result = orderMapper.addOrder(orderEntity); return result > 0 ? "添加成功" : "添加失败" + ""; } } |
@Controller public class OrderPageController { @Autowired private OrderMapper orderMapper;
@RequestMapping("/indexPage") @ExtApiToken public String indexPage(HttpServletRequest req) { return "indexPage"; }
@RequestMapping("/addOrderPage") @ExtApiIdempotent(value = ConstantUtils.EXTAPIFROM) public String addOrder(OrderEntity orderEntity) { int addOrder = orderMapper.addOrder(orderEntity); return addOrder > 0 ? "success" : "fail"; }
} |
不管是以何种方式传递url时,如果要传递的url中包含特殊字符,如想要传递一个+,但是这个+会被url会被编码成空格,想要传递&,被url处理成分隔符。
尤其是当传递的url是经过Base64加密或者RSA加密后的,存在特殊字符时,这里的特殊字符一旦被url处理,就不是原先你加密的结果了。
url特殊符号及对应的编码:
符号 |
url中的含义 |
编码 |
+ |
URL 中+号表示空格 |
%2B |
空格 |
URL中的空格可以用+号或者编码 |
%20 |
/ |
分隔目录和子目录 |
%2F |
? |
分隔实际的URL和参数 |
%3F |
% |
指定特殊字符 |
%25 |
# |
表示书签 |
%23 |
& |
URL中指定的参数间的分隔符 |
%26 |
= |
URL中指定参数的值 |
%3D |
接受参数案例tranIndex
@RestController public class TranController {
// 接受客户端参数 @RequestMapping("/tranIndex") public String tranIndex(String name) { System.out.println("name:" + name); return name; } } |
客户端访问结果
传入+参数变为了空格。
解决办法:将+变为%2B
URLEncoder.encode和decode
String encode = URLEncoder.encode("1+1", "UTF-8"); String decode = URLDecoder.decode(encode, "UTF-8"); System.out.println("encode:" + encode + ",decode:" + decode); |
Http接口参数编码处理
String url = "http://127.0.0.1:8080/tranIndex?"; // 参数转码 String strParam = "name=" + URLEncoder.encode("1+1", "utf-8"); String newUrl = url + strParam; String result = HttpClientUtils.httpGet(newUrl); System.out.println("result:" + result); |
散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。
单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:
1、MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。
2、SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;
SHA-1与MD5的比较
因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:
1、对强行供给的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2128数量级的操作,而对SHA-1则是2160数量级的操作。这样,SHA-1对强行攻击有更大的强度。
2、对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。
3、速度:在相同的硬件上,SHA-1的运行速度比MD5慢。
1、特征:雪崩效应、定长输出和不可逆。
2、作用是:确保数据的完整性。
3、加密算法:md5(标准密钥长度128位)、sha1(标准密钥长度160位)、md4、CRC-32
4、加密工具:md5sum、sha1sum、openssl dgst。
5、计算某个文件的hash值,例如:md5sum/shalsum FileName,openssl dgst –md5/-sha
MD5加密
在线MD5解密与加密
http://www.cmd5.com/
Java操作MD5加密
MD5加盐实现方式
一般使用的加盐:
md5(Password+UserName),即将用户名和密码字符串相加再MD5,这样的MD5摘要基本上不可反查。
但有时候用户名可能会发生变化,发生变化后密码即不可用了(验证密码实际上就是再次计算摘要的过程)。
----------
因此我们做了一个非常简单的加盐算法,每次保存密码到数据库时,都生成一个随机16位数字,将这16位数字和密码相加再求MD5摘要,然后在摘要中再将这16位数字按规则掺入形成一个48位的字符串。
在验证密码时再从48位字符串中按规则提取16位数字,和用户输入的密码相加再MD5。按照这种方法形成的结果肯定是不可直接反查的,且同一个密码每次保存时形成的摘要也都是不同的。
代码如下:
/** * MD5加盐加密 */ public class PasswordUtil { /** * 生成含有随机盐的密码 */ public static String generate(String password) { Random r = new Random(); StringBuilder sb = new StringBuilder(16); sb.append(r.nextInt(99999999)).append(r.nextInt(99999999)); int len = sb.length(); if (len < 16) { for (int i = 0; i < 16 - len; i++) { sb.append("0"); } } String salt = sb.toString(); password = md5Hex(password + salt); char[] cs = new char[48]; for (int i = 0; i < 48; i += 3) { cs[i] = password.charAt(i / 3 * 2); char c = salt.charAt(i / 3); cs[i + 1] = c; cs[i + 2] = password.charAt(i / 3 * 2 + 1); } return new String(cs); }
/** * 校验密码是否正确 */ public static boolean verify(String password, String md5) { char[] cs1 = new char[32]; char[] cs2 = new char[16]; for (int i = 0; i < 48; i += 3) { cs1[i / 3 * 2] = md5.charAt(i); cs1[i / 3 * 2 + 1] = md5.charAt(i + 2); cs2[i / 3] = md5.charAt(i + 1); } String salt = new String(cs2); return md5Hex(password + salt).equals(new String(cs1)); }
/** * 获取十六进制字符串形式的MD5摘要 */ public static String md5Hex(String src) { try { MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] bs = md5.digest(src.getBytes()); return new String(new Hex().encode(bs)); } catch (Exception e) { return null; } }
public static void main(String[] args) { // 加密+加盐 String password1 = generate("admin"); System.out.println("结果:" + password1 + " 长度:" + password1.length()); // 解码 System.out.println(verify("admin", password1)); // 加密+加盐 String password2 = generate("admin"); System.out.println("结果:" + password2 + " 长度:" + password2.length()); // 解码 System.out.println(verify("admin", password2)); } } |
单向散列加密
对称加密
非对称加密
安全密钥管理
如何保证HTTP接口请求的安全呢?