介绍
- 通过这个例子,分别介绍
ThreadLocal
和MD5加密
的使用。
- 使用
ThreadLocal
:线程局部变量:作用实现把数据绑定到线程中,从而实现线程安全。- 使用
MessageDigest
:类为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。
问题和解决
- 当转账时遇到的问题(其他小问题这里不展示):
- (1)转账时,一个账户减钱成功,但是这是出现异常,导致另一账户没有加上钱。
- (2)表单重复提交
- 解决方法:
- (1)开启事务,但是事务也有一个问题。
- 问题:就是开启的
事务连接
和数据库连接
不是同一个连接。数据库连接是从连接池中拿的,事务连接不是从连接池中拿的,这样就造成开启的事务无效。事务也可以从连接池中拿,但是这两个永远也拿不到同一个连接- 解决:使用
ThreadLocal
线程局部变量- (2)重复提交表单两种解决方式:
- 利用
Session
防止表单重复提交(推荐),保存一个标识(令牌)
,令牌一样时可提交,不一样时不可提交。使用输入框(hidden)
把令牌隐藏掉,使客户看不到- 通过
JavaScript
屏蔽提交按钮(不推荐),但是js容易被破坏,不安全。
粘贴部分代码(全部的代码太多了)
threadLocal线程绑定
- 把对象绑定线程上,线程不变对象不变
- 这种方式调用事务就是使用的一个连接
- 这里使用Druid数据连接池
- DataSourceUtils工具类代码示例:(省略部分代码)
//创建ThreadLocal线程 private static ThreadLocal<Connection> threadLocal; static{ threadLocal = new ThreadLocal<>(); } //创建连接,绑定对象 public static Connection getConnection(){ Connection connection = threadLocal.get(); if(connection==null) { try { connection = dataSource.getConnection(); } catch (SQLException e) { e.printStackTrace(); } threadLocal.set(connection); } return connection; } //启动事务 public static void beginTranscation(){ Connection connection = getConnection(); try { connection.setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } //提交 public static void commit(){ Connection connection = getConnection(); try { connection.commit(); } catch (SQLException e) { e.printStackTrace(); } } //回滚 public static void rollBack(){ Connection connection = getConnection(); try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); } } //关闭 public static void close(){ Connection connection = getConnection(); try { connection.close(); //关闭后,移除对象 threadLocal.remove(); } catch (SQLException e) { e.printStackTrace(); } }
- 代码示例:
//存钱 @Override public void save(Integer id, BigDecimal money) { QueryRunner qr = new QueryRunner(); try { //获取连接(必须在该方法中获取连接),执行SQL语句 qr.update(DataSourceUtil.getConnection(),"update account set money=money+? where id=?",money,id); } catch (SQLException e) { e.printStackTrace(); } } //取钱 @Override public void take(Integer id, BigDecimal money) { QueryRunner qr = new QueryRunner(); try { //获取连接(必须在该方法中获取连接),执行SQL语句 qr.update(DataSourceUtil.getConnection(),"update account set money=money-? where id=?",money,id); } catch (SQLException e) { e.printStackTrace(); } }
- Servlet中代码:
try { //开始事务 DataSourceUtil.beginTranscation(); as.transfer(Integer.parseInt(id1),Integer.parseInt(id2),new BigDecimal(account1)); //提交事务 DataSourceUtil.commit(); response.getWriter().write("转账成功"); } catch (Exception e) { e.printStackTrace(); //事务回滚 DataSourceUtil.rollBack(); response.getWriter().write("转账失败,错误信息:"+e.getMessage()); }finally { //关闭事务 DataSourceUtil.close(); }
使用MD5加密生成令牌
- 简要说明:
- 当可客户端请求服务器时,创建一个指令,发送给客户端并保存。
- 在发送给客户端后,服务器立即移除Session中的指令。
- 客户端每次请求都会比较指令是否一致,一致可以提交,不一致不可提价
- 这样就可以防止表单重复提交
- 代码示例:
public class TokenProccessor { //创建单例,饿汉式 public TokenProccessor() { } private static final TokenProccessor instance = new TokenProccessor(); public static TokenProccessor getInstance(){ return instance; } //加密,生成令牌 public String makeToken(){ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); String str = sdf.format(new Date()) + new Random().nextInt(999999999); //md5加密 try { //使用md5消息摘要 MessageDigest md = MessageDigest.getInstance("MD5"); //更新数据 md.update(str.getBytes()); //加密,返回加密之后的结果 byte[] digest = md.digest(); //编码,转换成string类型 return Base64.getEncoder().encodeToString(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } }
- 创建一个生成指令的工具类:
- 代码示例:
public class TokenTools { //生成指令,并保存到Session中 public static void createToken(HttpServletRequest request,String tokenkey){ String token = TokenProccessor.getInstance().makeToken(); request.getSession().setAttribute(tokenkey,token); } //移除Session中的指令 public static void removeToken(HttpServletRequest request,String tokenkey){ request.getSession().removeAttribute(tokenkey); } }
- Servlet中代码:
//获取客户端的令牌 String client_token = request.getParameter("tokenkey"); //获取服务端的令牌 String server_token = (String) request.getSession().getAttribute("tokenkey"); if(client_token==null||client_token.trim().length()==0){ response.getWriter().write("不可重复提交"); return; } if(!client_token.equals(server_token)){ response.getWriter().write("不可重复提交"); return; } //调用移除session中的指令 TokenTools.removeToken(request,"tokenkey");
- HTML页面中的代码:
<!--客户端保存指令--> <input type="hidden" value="${tokenkey}" name="tokenkey"/>