精华内容
下载资源
问答
  • 常见大Web安全问题

    千次阅读 2021-11-26 17:36:41
    一、 XSS ...XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私

    一、 XSS

    Cross-Site Scripting(跨站脚本攻击)简称 XSS(因为缩写和 CSS重叠,所以只能叫 XSS),是一种代码注入攻击。

    攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

    1.1 原理

    XSS 的原理是恶意攻击者往 Web 页面里插入恶意可执行网页脚本代码,当用户浏览该页之时,嵌入其中 Web 里面的脚本代码会被执行,从而可以达到攻击者盗取用户信息或其他侵犯用户安全隐私的目的。

    1.2分类

    类型存储区插入点
    存储型 XSS后端数据库HTML
    反射型 XSSURLHTML
    DOM 型 XSS后端数据库/前端存储/URL前端 JavaScript

    1.2.1反射型 XSS

    一般是通过给别人发送带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。

    <select>
        <script>
            document.write(''
                + '<option value=1>'
                +     location.href.substring(location.href.indexOf('default=') + 8)
                + '</option>'
            );
            document.write('<option value=2>English</option>');
        </script>
    </select>
    

    攻击者可以直接通过 URL (类似:https://xxx.com/xxx?default=<script>alert(document.cookie)</script>) 注入可执行的脚本代码。不过一些浏览器如Chrome其内置了一些XSS过滤器,可以防止大部分反射型XSS攻击。

    反射型 XSS攻击有下面几个特征:

    • 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据。
    • 攻击者需要诱骗点击,必须要通过用户点击链接才能发起
    • 反馈率低,所以较难发现和响应修复
    • 盗取用户敏感保密信息

    防范措施:

    • Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。
    • 尽量不要从 URL,document.referrer,document.forms 等这种 DOM API 中获取数据直接渲染。
    • 尽量不要使用 eval, new Function(),document.write(),document.writeln(),window.setInterval(),window.setTimeout(),innerHTML,document.createElement() 等可执行字符串的方法。
    • 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。
      前端渲染的时候对任何的字段都需要做 escape 转义编码。

    1.2.2 存储型 XSS

    一般存在于 Form 表单提交等交互功能,如文章留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。

    存储型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可。

    攻击成功需要同时满足以下几个条件:

    • POST 请求提交表单后端没做转义直接入库。
    • 后端从数据库中取出数据没做转义直接输出给前端。
    • 前端拿到后端数据没做转义直接渲染成 DOM。

    存储型 XSS 有以下几个特点:

    • 持久性,植入在数据库中
    • 盗取用户敏感私密信息
    • 危害面广

    1.2.3 DOM 型 XSS

    DOM 型 XSS 攻击,实际上就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML、.outerHTML、.appendChild、document.write()等API时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText、.textContent、.setAttribute() 等。

    1.3 防御措施

    1.3.1 csp(Content-Security-Policy)

    CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击

    1.3.1.1 开启csp方式

    通常可以通过两种方式来开启 CSP:

    • 设置 HTTP Header 中的 Content-Security-Policy
    // 限制所有的外部资源,都只能从当前域名加载
    Content-Security-Policy: default-src 'self'
    
    // default-src 是 CSP 指令,多个指令之间用英文分号分割;多个指令值用英文空格分割
    Content-Security-Policy: default-src https://host1.com https://host2.com; frame-src 'none'; object-src 'none'  
    
    // 错误写法,第二个指令将会被忽略
    Content-Security-Policy: script-src https://host1.com; script-src https://host2.com
    
    // 正确写法如下
    Content-Security-Policy: script-src https://host1.com https://host2.com
    
    // 通过report-uri指令指示浏览器发送JSON格式的拦截报告到某个地址
    Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser; 
    
    • 设置 meta 标签的方式
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
    

    meta标签方式配置的内容安全策略还是比较好理解的,但是http head方式配置的,难道对任意一条api配置一下就可以吗?这里一般是配置在nginx上的,就像这样:

    		add_header Content-Security-Policy "default-src 'self' static4.segway.com(该地址按需修改) 'unsafe-inline' 'unsafe-eval' blob: data: ;";
            add_header X-Xss-Protection "1;mode=block";
            add_header X-Content-Type-Options nosniff;
    
    

    1.3.1.2 常用的常用的CSP指令

    指令指令和指令值示例指令说明
    default-src‘self’ cdn.guangzhul.com默认加载策略
    script-src‘self’ js.guangzhul.com对 JavaScript 的加载策略。
    style-src‘self’ css.guangzhul.com对样式的加载策略。
    img-src‘self’ img.guangzhul.com对图片的加载策略。
    connect-src‘self’对 Ajax、WebSocket 等请求的加载策略。不允许的情况下,浏览器会模拟一个状态为 400 的响应。
    font-srcfont.cdn.guangzhul.com针对 WebFont 的加载策略。
    object-src‘self’针对 、 或 等标签引入的 flash 等插件的加载策略。
    media-srcmedia.cdn.guangzhul.com针对媒体引入的 HTML 多媒体的加载策略。
    frame-src‘self’针对 frame 的加载策略。
    report-urireport-uri告诉浏览器如果请求的资源不被策略允许时,往哪个地址提交日志信息。 特别的:如果想让浏览器只汇报日志,不阻止任何内容,可以改用 Content-Security-Policy-Report-Only 头。

    1.3.2 转义字符

    用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

    function escape(str) {
      str = str.replace(/&/g, '&amp;')
      str = str.replace(/</g, '&lt;')
      str = str.replace(/>/g, '&gt;')
      str = str.replace(/"/g, '&quto;')
      str = str.replace(/'/g, '&#39;')
      str = str.replace(/`/g, '&#96;')
      str = str.replace(/\//g, '&#x2F;')
      return str
    }
    

    但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。

    const xss = require('xss')
    let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
    // -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;
    console.log(html)
    

    以上示例使用了 js-xss 来实现,可以看到在输出中保留了 h1 标签且过滤了 script 标签。

    1.3.3 HttpOnly Cookie

    HttpOnly是加在cookies上的一个标识,用于告诉浏览器不要向客户端脚本(document.cookie或其他)暴露cookie。

    当你在cookie上设置HttpOnly标识后,浏览器就会知会到这是特殊的cookie,只能由服务器检索到,所有来自客户端脚本的访问都会被禁止。当然也有前提:使用新版的浏览器。

    Set-Cookie: Name=Value; expires=Wednesday, 01-May-2014 12:45:10 GMT; HttpOnly
    

    禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie

    二、CSRF

    CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

    2.1 攻击的原理

    完成 CSRF 攻击必须要有三个条件:

    1. 用户已经登录了站点 A,并在本地记录了 cookie
    2. 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。
    3. 站点 A 没有做任何 CSRF 防御

    下面是个典型场景

    • 受害者登录a.com,并保留了登录凭证(Cookie)。
    • 攻击者引诱受害者访问了b.com。
    • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会在请求头部携带a.com的cookie信息
    • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
    • a.com以受害者的名义执行了act=xx。
    • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。

    2.2 特点

    1. 攻击通常在第三方网站发起,如图上的站点B,站点A无法防止攻击发生。
    2. 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;并不会去获取cookie信息(cookie有同源策略)
    3. 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等(来源不明的链接,不要点击)

    ps: 注意是盗用了cookie,是冒充,不是获取,cookie是发请求的时候浏览器自动塞到请求头部的

    浏览器会依据加载的域名附带上对应域名cookie。

    就是如果用户在a网站登录且生成了授权的cookies,然后访问b网站,b站故意构造请求a站的请求,如删除操作之类的,用script,img或者iframe之类的加载a站着个地址,浏览器会附带上a站此登录用户的授权cookie信息,这样就构成crsf,会删除掉当前用户的数据。

    2.3 类别

    2.3.1 GET类型的CSRF

    GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:

     <img src="http://bank.example/withdraw?amount=10000&for=hacker" > 
    

    在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。

    2.3.2 POST类型的CSRF

    这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:

     <form action="http://bank.example/withdraw" method=POST>
        <input type="hidden" name="account" value="xiaoming" />
        <input type="hidden" name="amount" value="10000" />
        <input type="hidden" name="for" value="hacker" />
    </form>
    <script> document.forms[0].submit(); </script> 
    

    访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。

    POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。

    2.3.3 链接类型的CSRF

    链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

      <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
      重磅消息!!
      <a/>
    

    由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。

    2.4 防御

    2.4.1 SameSite

    为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax。部署简单,并能有效防御CSRF攻击,但是存在兼容性问题。

    Set-Cookie: CookieName=CookieValue; SameSite=Strict;
    // or
    Set-Cookie: CookieName=CookieValue; SameSite=Lax;
    
    • Samesite=Strict: 被称为是严格模式,表明这个 Cookie 在任何情况都不可能作为第三方的 Cookie,有能力阻止所有CSRF攻击。此时,我们在B站点下发起对A站点的任何请求,A站点的 Cookie 都不会包含在cookie请求头中。
    • Samesite=Lax: 被称为是宽松模式,与 Strict 相比,放宽了限制,允许发送安全 HTTP 方法带上 Cookie,如 Get / OPTIONS 、 HEAD 请求。但是不安全 HTTP 方法,如: POST , PUT , DELETE 请求时,不能作为第三方链接的 Cookie

    2.4.2 Referer Check

    HTTP Referer是header的一部分,当浏览器向web服务器发送请求时,一般会带上Referer信息告诉服务器是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。可以通过检查请求的来源来防御CSRF攻击。正常请求的referer具有一定规律,如在提交表单的referer必定是在该页面发起的请求。所以通过检查http包头referer的值是不是这个页面,来判断是不是CSRF攻击。

    但在某些情况下如从https跳转到http,浏览器处于安全考虑,不会发送referer,服务器就无法进行check了。若与该网站同域的其他网站有XSS漏洞,那么攻击者可以在其他网站注入恶意脚本,受害者进入了此类同域的网址,也会遭受攻击。出于以上原因,无法完全依赖Referer Check作为防御CSRF的主要手段。但是可以通过Referer Check来监控CSRF攻击的发生。

    2.4.3 使用Token(主流)

    CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开。跟验证码类似,只是用户无感知。

    1. 服务端给用户生成一个token,加密后传递给用户
    2. 用户在提交请求时,需要携带这个token
    3. 服务端验证token是否正确

    这种方法相比Referer检查要安全很多,token可以在用户登陆后产生并放于session或cookie中,然后在每次请求时服务器把token从session或cookie中拿出,与本次请求中的token 进行比对。由于token的存在,攻击者无法再构造出一个完整的URL实施CSRF攻击。但在处理多个页面共存问题时,当某个页面消耗掉token后,其他页面的表单保存的还是被消耗掉的那个token,其他页面的表单提交时会出现token错误。

    这个方法个人觉得已经可以杜绝99%的CSRF攻击了,那还有1%呢…由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,这就另外的1%。

    2.4.4 验证码

    用程序和用户进行交互过程中,特别是账户交易这种核心步骤,强制用户输入验证码,才能完成最终请求。在通常情况下,验证码够很好地遏制CSRF攻击。但增加验证码降低了用户的体验,网站不能给所有的操作都加上验证码。所以只能将验证码作为一种辅助手段,在关键业务点设置验证码。

    三、点击劫持

    点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

    3.1 特点

    • 隐蔽性较高,骗取用户操作
    • “UI-覆盖攻击”
    • 利用iframe或者其它标签的属性

    3.2 点击劫持的原理

    用户在登陆 A 网站的系统后,被攻击者诱惑打开第三方网站,而第三方网站通过 iframe 引入了 A 网站的页面内容,用户在第三方网站中点击某个按钮(被装饰的按钮),实际上是点击了 A 网站的按钮。
    接下来我们举个例子:我在优酷发布了很多视频,想让更多的人关注它,就可以通过点击劫持来实现

    iframe {
    width: 1440px;
    height: 900px;
    position: absolute;
    top: -0px;
    left: -0px;
    z-index: 2;
    -moz-opacity: 0;
    opacity: 0;
    filter: alpha(opacity=0);
    }
    button {
    position: absolute;
    top: 270px;
    left: 1150px;
    z-index: 1;
    width: 90px;
    height:40px;
    }
    </style>
    ......
    <button>点击脱衣</button>
    <img src="http://pic1.win4000.com/wallpaper/2018-03-19/5aaf2bf0122d2.jpg">
    <iframe src="http://i.youku.com/u/UMjA0NTg4Njcy" scrolling="no"></iframe>
    

    从上图可知,攻击者通过图片作为页面背景,隐藏了用户操作的真实界面,当你按耐不住好奇点击按钮以后,真正的点击的其实是隐藏的那个页面的订阅按钮,然后就会在你不知情的情况下订阅了。

    3.3 如何防御

    3.3.1 X-FRAME-OPTIONS

    X-FRAME-OPTIONS是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

    该响应头有三个值可选,分别是

    • DENY,表示页面不允许通过 iframe 的方式展示
    • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
    • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

    四、URL跳转漏洞

    定义:借助未验证的URL跳转,将应用程序引导到不安全的第三方区域,从而导致的安全问题。

    4.1 URL跳转漏洞原理

    黑客利用URL跳转漏洞来诱导安全意识低的用户点击,导致用户信息泄露或者资金的流失。其原理是黑客构建恶意链接(链接需要进行伪装,尽可能迷惑),发在QQ群或者是浏览量多的贴吧/论坛中。

    安全意识低的用户点击后,经过服务器或者浏览器解析后,跳到恶意的网站中。

    恶意链接需要进行伪装,经常的做法是熟悉的链接后面加上一个恶意的网址,这样才迷惑用户。

    诸如伪装成像如下的网址,你是否能够识别出来是恶意网址呢?

    http://gate.baidu.com/index?act=go&url=http://t.cn/RVTatrd
    http://qt.qq.com/safecheck.html?flag=1&url=http://t.cn/RVTatrd
    http://tieba.baidu.com/f/user/passport?jumpUrl=http://t.cn/RVTatrd
    
    

    4.2 实现方式

    • Header头跳转
    • Javascript跳转
    • META标签跳转

    这里我们举个Header头跳转实现方式:

    <?php
    $url=$_GET['jumpto'];
    header("Location: $url");
    ?>
    
    http://www.wooyun.org/login.php?jumpto=http://www.evil.com
    

    这里用户会认为www.wooyun.org都是可信的,但是点击上述链接将导致用户最终访问www.evil.com这个恶意网址。

    4.3 如何防御

    4.3.1 referer的限制

    如果确定传递URL参数进入的来源,我们可以通过该方式实现安全限制,保证该URL的有效性,避免恶意用户自己生成跳转链接

    4.3.2 加入有效性验证Token

    我们保证所有生成的链接都是来自于我们可信域的,通过在生成的链接里加入用户不可控的Token对生成的链接进行校验,可以避免用户生成自己的恶意链接从而被利用,但是如果功能本身要求比较开放,可能导致有一定的限制。

    五、SQL注入

    SQL注入是一种常见的Web安全漏洞,攻击者利用这个漏洞,可以访问或修改数据,或者利用潜在的数据库漏洞进行攻击。

    5.1 SQL注入的原理

    我们先举一个万能钥匙的例子来说明其原理:

    <form action="/login" method="POST">
        <p>Username: <input type="text" name="username" /></p>
        <p>Password: <input type="password" name="password" /></p>
        <p><input type="submit" value="登陆" /></p>
    </form>
    

    后端的 SQL 语句可能是如下这样的:

    let querySQL = `
        SELECT *
        FROM user
        WHERE username='${username}'
        AND psw='${password}'
    `;
    // 接下来就是执行 sql 语句...
    

    这是我们经常见到的登录页面,但如果有一个恶意攻击者输入的用户名是 admin’ --,密码随意输入,就可以直接登入系统了。why! ----这就是SQL注入

    我们之前预想的SQL 语句是:

    SELECT * FROM user WHERE username='admin' AND psw='password'
    

    但是恶意攻击者用奇怪用户名将你的 SQL 语句变成了如下形式:

    SELECT * FROM user WHERE username='admin' --' AND psw='xxxx'
    

    在 SQL 中,’ --是闭合和注释的意思,-- 是注释后面的内容的意思,所以查询语句就变成了:

    SELECT * FROM user WHERE username='admin'
    

    5.2 SQL注入的必备条件

    1. 可以控制输入的数据
    2. 服务器要执行的代码拼接了控制的数据。

    我们会发现SQL注入流程中与正常请求服务器类似,只是黑客控制了数据,构造了SQL查询,而正常的请求不会SQL查询这一步,SQL注入的本质:数据和代码未分离,即数据当做了代码来执行。

    5.3 防御

    • 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
    • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。
    • 对进入数据库的特殊字符(’,",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。
    • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

    六、OS命令注入攻击

    OS命令注入和SQL注入差不多,只不过SQL注入是针对数据库的,而OS命令注入是针对操作系统的。OS命令注入攻击指通过Web应用,执行非法的操作系统命令达到攻击的目的。只要在能调用Shell函数的地方就有存在被攻击的风险。倘若调用Shell时存在疏漏,就可以执行插入的非法命令。

    命令注入攻击可以向Shell发送命令,让Windows或Linux操作系统的命令行启动程序。也就是说,通过命令注入攻击可执行操作系统上安装着的各种程序。

    6.1 原理

    黑客构造命令提交给web应用程序,web应用程序提取黑客构造的命令,拼接到被执行的命令中,因黑客注入的命令打破了原有命令结构,导致web应用执行了额外的命令,最后web应用程序将执行的结果输出到响应页面中。

    我们通过一个例子来说明其原理,假如需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去返回一个结果给用户

    // 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
    const exec = require('mz/child_process').exec;
    let params = {/* 用户输入的参数 */};
    exec(`git clone ${params.repo} /some/path`);
    

    如果 params.repo 传入的是 https://github.com/admin/admin.github.io.git 确实能从指定的 git repo 上下载到想要的代码。
    但是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf /* && 恰好你的服务是用 root 权限起的就糟糕了。

    6.2 如何防御

    • 后端对前端提交内容进行规则限制(比如正则表达式)。
    • 在调用系统命令前对所有传入参数进行命令行参数转义过滤。
    • 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm包

    参考

    展开全文
  • SimpleDateFormat类不是线程安全的根本原因和解决方案,冰河吐血整理,建议收藏!!

    大家好,我是冰河~~

    首先问下大家:你使用的SimpleDateFormat类还安全吗?为什么说SimpleDateFormat类不是线程安全的?带着问题从本文中寻求答案。

    提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到SimpleDateFormat类出现问题的并发量,也就是说你们的系统没啥负载!

    接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。

    重现SimpleDateFormat类的线程安全问题

    为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。

    有关CountDownLatch类和Semaphore类的具体用法和底层原理与源码解析在【高并发专题】后文会深度分析。这里,大家只需要知道CountDownLatch类可以使一个线程等待其他线程各自执行完毕后再执行。而Semaphore类可以理解为一个计数信号量,必须由获取它的线程释放,经常用来限制访问某些资源的线程数量,例如限流等。

    好了,先来看下重现SimpleDateFormat类的线程安全问题的代码,如下所示。

    package io.binghe.concurrent.lab06;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 测试SimpleDateFormat的线程不安全问题
     */
    public class SimpleDateFormatTest01 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
        //SimpleDateFormat对象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            simpleDateFormat.parse("2020-01-01");
                        } catch (ParseException e) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }catch (NumberFormatException e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    可以看到,在SimpleDateFormatTest01类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和CountDownLatch类与Semaphore类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。

    simpleDateFormat.parse("2020-01-01");
    

    当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。

    运行程序输出的结果信息如下所示。

    Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败
    线程:pool-1-thread-9 格式化日期失败
    线程:pool-1-thread-10 格式化日期失败
    Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败
    线程:pool-1-thread-21 格式化日期失败
    Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败
    线程:pool-1-thread-11 格式化日期失败
    java.lang.ArrayIndexOutOfBoundsException
    线程:pool-1-thread-27 格式化日期失败
    	at java.lang.System.arraycopy(Native Method)
    	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
    	at java.lang.StringBuffer.append(StringBuffer.java:367)
    	at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败
    
    	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
    	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    线程:pool-1-thread-14 格式化日期失败
    	at java.text.DateFormat.parse(DateFormat.java:364)
    	at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
    线程:pool-1-thread-13 格式化日期失败	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    
    	at java.lang.Thread.run(Thread.java:748)
    java.lang.NumberFormatException: For input string: ""
    	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    线程:pool-1-thread-20 格式化日期失败	at java.lang.Long.parseLong(Long.java:601)
    	at java.lang.Long.parseLong(Long.java:631)
    
    	at java.text.DigitList.getLong(DigitList.java:195)
    	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
    	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
    	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    	at java.text.DateFormat.parse(DateFormat.java:364)
    	at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    	at java.lang.Thread.run(Thread.java:748)
    java.lang.NumberFormatException: For input string: ""
    	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    	at java.lang.Long.parseLong(Long.java:601)
    	at java.lang.Long.parseLong(Long.java:631)
    	at java.text.DigitList.getLong(DigitList.java:195)
    	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
    	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    	at java.text.DateFormat.parse(DateFormat.java:364)
    
    Process finished with exit code 1
    

    说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!

    接下来,我们就看下,SimpleDateFormat类为何不是线程安全的。

    SimpleDateFormat类为何不是线程安全的?

    那么,接下来,我们就一起来看看真正引起SimpleDateFormat类线程不安全的根本原因。

    通过查看SimpleDateFormat类的源码,我们得知:SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量,如下所示。

    /**
      * The {@link Calendar} instance used for calculating the date-time fields
      * and the instant of time. This field is used for both formatting and
      * parsing.
      *
      * <p>Subclasses should initialize this field to a {@link Calendar}
      * appropriate for the {@link Locale} associated with this
      * <code>DateFormat</code>.
      * @serial
      */
    protected Calendar calendar;
    

    从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。接下来,我们再查看parse()方法接近最后的部分。

    @Override
    public Date parse(String text, ParsePosition pos){
        ################此处省略N行代码##################
        Date parsedDate;
        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }
        return parsedDate;
    }
    

    可见,最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。

    接下来,我们再来看看CalendarBuilder.establish()方法,如下所示。

    Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
            && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }
    
        cal.clear();
        // Set the fields from the min stamp to the max stamp so that
        // the field resolution works in the Calendar.
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }
    
        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }
    

    在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。

    因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。

    那么,得知了SimpleDateFormat类不是线程安全的,以及造成SimpleDateFormat类不是线程安全的原因,那么如何解决这个问题呢?接下来,我们就一起探讨下如何解决SimpleDateFormat类在高并发场景下的线程安全问题。

    解决SimpleDateFormat类的线程安全问题

    解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。

    1.局部变量法

    最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,如下所示的代码,将SimpleDateFormat类对象定义在parse(String)方法的上面,即可解决问题。

    package io.binghe.concurrent.lab06;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 局部变量法解决SimpleDateFormat类的线程安全问题
     */
    public class SimpleDateFormatTest02 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                            simpleDateFormat.parse("2020-01-01");
                        } catch (ParseException e) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }catch (NumberFormatException e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    此时运行修改后的程序,输出结果如下所示。

    所有线程格式化日期成功
    

    至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM专题】的JVM内存模式相关内容中深入剖析,这里不做过多的介绍了。

    当然,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。

    2.synchronized锁方式

    将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,此时在调用格式化时间的方法时,对SimpleDateFormat对象进行同步即可,代码如下所示。

    package io.binghe.concurrent.lab06;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题
     */
    public class SimpleDateFormatTest03 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
        //SimpleDateFormat对象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            synchronized (simpleDateFormat){
                                simpleDateFormat.parse("2020-01-01");
                            }
                        } catch (ParseException e) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }catch (NumberFormatException e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    此时,解决问题的关键代码如下所示。

    synchronized (simpleDateFormat){
    	simpleDateFormat.parse("2020-01-01");
    }
    

    运行程序,输出结果如下所示。

    所有线程格式化日期成功
    

    需要注意的是,虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。

    3.Lock锁方式

    Lock锁方式与synchronized锁方式实现原理相同,都是在高并发下通过JVM的锁机制来保证程序的线程安全。通过Lock锁方式解决问题的代码如下所示。

    package io.binghe.concurrent.lab06;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题
     */
    public class SimpleDateFormatTest04 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
        //SimpleDateFormat对象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        //Lock对象
        private static Lock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            lock.lock();
                            simpleDateFormat.parse("2020-01-01");
                        } catch (ParseException e) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }catch (NumberFormatException e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }finally {
                            lock.unlock();
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    通过代码可以得知,首先,定义了一个Lock类型的全局静态变量作为加锁和释放锁的句柄。然后在simpleDateFormat.parse(String)代码之前通过lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中,如下所示。

    finally {
    	lock.unlock();
    }
    

    运行程序,输出结果如下所示。

    所有线程格式化日期成功
    

    此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。

    4.ThreadLocal方式

    使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题,使用ThreadLocal解决线程安全问题的代码如下所示。

    package io.binghe.concurrent.lab06;
    
    import java.text.DateFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
     */
    public class SimpleDateFormatTest05 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
    
        private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
            @Override
            protected DateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd");
            }
        };
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            threadLocal.get().parse("2020-01-01");
                        } catch (ParseException e) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }catch (NumberFormatException e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    通过代码可以得知,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。

    运行程序,输出结果如下所示。

    所有线程格式化日期成功
    

    此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。

    另外,使用ThreadLocal也可以写成如下形式的代码,效果是一样的。

    package io.binghe.concurrent.lab06;
    
    import java.text.DateFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
     */
    public class SimpleDateFormatTest06 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
    
        private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
    
        private static DateFormat getDateFormat(){
            DateFormat dateFormat = threadLocal.get();
            if(dateFormat == null){
                dateFormat = new SimpleDateFormat("yyyy-MM-dd");
                threadLocal.set(dateFormat);
            }
            return dateFormat;
        }
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            getDateFormat().parse("2020-01-01");
                        } catch (ParseException e) {
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }catch (NumberFormatException e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    5.DateTimeFormatter方式

    DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。代码如下所示。

    package io.binghe.concurrent.lab06;
    
    import java.time.LocalDate;
    import java.time.format.DateTimeFormatter;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 通过DateTimeFormatter类解决线程安全问题
     */
    public class SimpleDateFormatTest07 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
    
       private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            LocalDate.parse("2020-01-01", formatter);
                        }catch (Exception e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    可以看到,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。

    运行程序,输出结果如下所示。

    所有线程格式化日期成功
    

    使用DateTimeFormatter类来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用

    6.joda-time方式

    joda-time是第三方处理日期时间格式化的类库,是线程安全的。如果使用joda-time来处理日期和时间的格式化,则需要引入第三方类库。这里,以Maven为例,如下所示引入joda-time库。

    <dependency>
    	<groupId>joda-time</groupId>
    	<artifactId>joda-time</artifactId>
    	<version>2.9.9</version>
    </dependency>
    

    引入joda-time库后,实现的程序代码如下所示。

    package io.binghe.concurrent.lab06;
    
    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    import org.joda.time.format.DateTimeFormatter;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 通过DateTimeFormatter类解决线程安全问题
     */
    public class SimpleDateFormatTest08 {
        //执行总次数
        private static final int EXECUTE_COUNT = 1000;
        //同时运行的线程数量
        private static final int THREAD_COUNT = 20;
    
        private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
    
        public static void main(String[] args) throws InterruptedException {
            final Semaphore semaphore = new Semaphore(THREAD_COUNT);
            final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < EXECUTE_COUNT; i++){
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        try {
                            DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
                        }catch (Exception e){
                            System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                            e.printStackTrace();
                            System.exit(1);
                        }
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.out.println("信号量发生错误");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            System.out.println("所有线程格式化日期成功");
        }
    }
    

    这里,需要注意的是:DateTime类是org.joda.time包下的类,DateTimeFormat类和DateTimeFormatter类都是org.joda.time.format包下的类,如下所示。

    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    import org.joda.time.format.DateTimeFormatter;
    

    运行程序,输出结果如下所示。

    所有线程格式化日期成功
    

    使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。

    解决SimpleDateFormat类的线程安全问题的方案总结

    综上所示:在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用

    synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。

    ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。

    DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的,经压测,DateTimeFormatter类处理日期和时间的性能效果还不错(后文单独写一篇关于高并发下性能压测的文章)。所以,推荐在高并发场景下的生产环境使用。

    joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用

    写在最后

    如果你想进大厂,想升职加薪,或者对自己现有的工作比较迷茫,都可以私信我交流,希望我的一些经历能够帮助到大家~~

    推荐阅读:

    好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~~

    展开全文
  • Java安全() 动态代理

    千次阅读 2021-12-12 13:22:34
    java.lang.reflect.Proxy类是通过创建一个的Java类(类名为com.sun.proxy.$ProxyXXX)的方式来实现无侵入的类方法代理功能的。 需要注意以下技术细节和特点: 动态代理的必须是接口类,通过动态生成一个接口实现类...

    概念

    代理模式Java当中最常用的设计模式之一 , 提供了对目标对象额外的访问方式 , 即通过代理对象访问目标对象.

    举个例子 , 存在一个对象A , 但是开发人员不希望程序直接访问对象A , 而是通过访问一个中介对象B来间接访问对象A , 以达成访问对象A的目的。此时 , 对象A被称为 “委托类” , 对象B被称为 “代理类”

    代理模式特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。

    Java的代理机制分为静态代理和动态代理。

    静态代理示例讲解

    JDK动态代理

    JDK原生动态代理利用拦截器和反射来实现,JDK原生动态代理的核心API为如下两个类 。

    java.lang.reflect.Proxy

    java.lang.reflect.InvocationHandler

    • java.lang.reflect.Proxy:负责动态构建代理类

      该类的功能为 : 提供四个静态方法来为一组接口动态地生成的代理类并返回代理类的实例对象

      在这里插入图片描述

      getProxyClass(ClassLoader,Class<?>[] interfaces):获取指定类加载器和一组接口的动态代理类的类对象。

      newProxyInstance(ClassLoader,Class<?>[],InvocationHandler):指定类加载器,一组接口,调用处理器;

      isProxyClass(Class<?>):判断获取的类是否为一个动态代理类;

      getInvocationHandler(Object):获取指定代理类实例查找与它相关联的调用处理器实例;

    • java.lang.reflect.InvocationHandler:负责提供调用代理操作

      在这里插入图片描述

      该接口定义了一个 invoke() 方法 , 用于集中处理在动态代理类对象上的方法调用 . 当程序通过代理对象调用某一个方法时 , 该方法调用会被自动转发到 InvocationHandler.invoke() 方法来进行调用。

    动态代理例子(看看怎么用)

    接口类

    package com.study.dy_proxy;
    
    public interface test_inter {
        public void say();
    }
    

    委托类

    package com.study.dy_proxy;
    
    public class test_entrust implements test_inter {
        @Override
        public void say() {
            System.out.println("hello!");
        }
    }
    

    实现调用处理器的也称中介类

    package com.study.dy_proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class test_handler implements InvocationHandler {
        private Object target;//target为委托类对象
    
        public test_handler(Object target){
            this.target = target;
        }
    
        //实现invoke方法
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("test proxy");
            Object result = method.invoke(target,args);
            return result;
        }
    }
    

    测试类

    package com.study.dy_proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Proxy;
    
    public class test {
        public static void main(String[] args) {
            test_entrust entrust = new test_entrust();
    
            //classloader
            ClassLoader classLoader = entrust.getClass().getClassLoader();
    
            //interfaces
            Class[] interfaces = entrust.getClass().getInterfaces();
    
            //调用处理器
            InvocationHandler invocationHandler = new test_handler(entrust);
    
            //代理对象
            test_inter proxy = (test_inter) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
            proxy.say();
        }
    }
    

    从而成功实现

    在这里插入图片描述

    YSoSerial

    ysoserial是集合了各种java反序列化payload的工具,关于该工具的分析

    解密ysoserial

    java.lang.reflect.Proxy

    java.lang.reflect.Proxy类有个nativedefineClass0方法可实现动态创建类对象。

    java.lang.reflect.Proxy类是通过创建一个新的Java类(类名为com.sun.proxy.$ProxyXXX)的方式来实现无侵入的类方法代理功能的。

    需要注意以下技术细节和特点:

    • 动态代理的必须是接口类,通过动态生成一个接口实现类来代理接口的方法调用(反射机制)。
    • 该类继承于java.lang.reflect.Proxy并实现了需要被代理的接口类,因为java.lang.reflect.Proxy类实现了java.io.Serializable接口,所以被代理的类支持序列化/反序列化
    • 如果动过动态代理生成了多个动态代理类,新生成的类名中的0会自增,如com.sun.proxy.$Proxy0/$Proxy1/$Proxy2
    • 该类方法中包含了被代理的接口类的所有方法,通过调用动态代理处理类(InvocationHandler)的invoke方法获取方法执行结果。

    动态代理类实例的序列化

    动态代理生成的类在反序列化/反序列化时不会序列化该类的成员变量,并且serialVersionUID0L 也将是说将该类的Class对象传递给java.io.ObjectStreamClass的静态lookup方法时,返回的ObjectStreamClass实例将具有以下特性:

    1. 调用其getSerialVersionUID方法将返回0L
    2. 调用其getFields方法将返回长度为零的数组。
    3. 调用其getField方法将返回null

    但其父类(java.lang.reflect.Proxy)在序列化时不受影响,父类中的h变量(InvocationHandler)将会被序列化,这个h存储了动态代理类的处理类实例以及动态代理的接口类的实现类的实例。

    (学习java序列化和反序列化的时候再补补)

    展开全文
  • 深圳零时科技有限公司(简称:零时科技),公司成立于2018年11月,是一家专注于区块链生态安全的实战创新型网络安全企业,团队扎根区块链安全与应用技术研究,以丰富的安全攻防实战经验结合人工智能数据分析处理,为...

    ​零时科技——专注于区块链安全领域

    深圳零时科技有限公司(简称:零时科技),公司成立于2018年11月,是一家专注于区块链生态安全的实战创新型网络安全企业,团队扎根区块链安全与应用技术研究,以丰富的安全攻防实战经验结合人工智能数据分析处理,为用户提供区块链安全漏洞风险检测、安全审计、安全防御、资产追溯,以及企业级区块链应用创新解决方案。

    零时科技区块链安全100问正式上线,以通俗易懂的语言形式为大家讲解区块链行业知识,以及区块链生态应用存在的安全问题,让更多人了解区块链及区块链安全。

    前言

    当前区块链技术和应用尚处于快速发展的初级阶段,面临的安全风险种类繁多,从区块链生态应用的安全,到智能合约安全,共识机制安全和底层基础组件安全,安全问题分布广泛且危险性高,对生态体系,安全审计,技术架构,隐私数据保护和基础设施的全局发展提出了全新的考验。

    PART01-什么是智能合约?

    智能合约(英语:Smartcontract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。一个智能合约是一套以数字形式定义的承诺(promises),包括合约参与方可以在上面执行这些承诺的协议。

    随着区块链技术的发展,智能合约将会与未来生活密切相关,如今,大多数区块链都具有智能合约功能,用户可以将智能合约应用于各种情况,包括金融衍生品、保险费、合同违约、财产法、金融服务、法律程序和众筹协议、数字身份、供应链管理、数据存储等。

    PART02-智能合约审计是什么?

    智能合约审计其实就是仔细研究代码的过程,即把合约部署到以太坊(假设此项目是运行在以太坊上的)主网络中,并对其进行错误、漏洞和风险等方面的审查,然后讨论如何改进。因为合约一旦部署不可修改、合约执行后不可逆、所有执行事务可追踪;所以智能合约审计就成为重中之重的工作。

    PART03-智能合约审计的必要性

    1)“智能合约”是区块链项目的重要部分,合约中如果有漏洞被作恶者利用,将导致资产被盗且往往无法收回,给用户造成巨大损失。

    2)由于智能合约部署后不能修改的特性,如果智能合约存在安全风险问题,将无法安全运行,需要重新修改部署,导致浪费资源,项目延期等一些列问题;所以尽量在部署上线前通过专业安全审计机构进行全面安全审计尽可能先于黑客发现安全漏洞。

    3)因为智能合约的创新性和区块链项目的去中心化特性,导致很多应用开发者初心不正,在合约中加入隐藏的漏洞后门,未经安全审计很难发现安全漏洞,导致项目上线后用户投资资产被跑路。

    4)智能合约之间的高耦合性导致的非预期安全问题;目前基于智能合约的去中心化应用多种多样;比如借贷、去中心化交易平台、抵押、聚合器、预言机等;很多项目直接相互调用,导致高耦合性,从而出现一些未知的安全问题,这些问题往往无法测试到,所以通过专业的安全审计团队丰富的审计经验,找出其中存在的安全风险。

    • 区块链安全100问正在持续更新,欢迎大家后台评论自己的观点,如有对区块链行业及应用有独到见解或者疑问的朋友留言哦!

    展开全文
  • 、数据库安全性与恢复技术

    千次阅读 2021-03-11 12:23:43
    安全性控制 授权与收回 GRANT和REVOKE 用户的权限 创建的用户有三种权限:DBA, RESOURCE,CONNECT 恢复技术 事务 所谓事务是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作...
  • 接下来我将开启安全系列,叫“系统安全”,也是免费的100篇文章,作者将更加深入的去研究恶意样本分析、逆向分析、内网渗透、网络攻防实战等,也将通过在线笔记和实践操作的形式分享与博友们学习,希望能与您...
  • 《信息安全数学基础》学习笔记 摘要 这里是关于《信息安全数学基础》书的个人笔记和总结,包括将一些概念(整除、同余、群、环、域、多项式等代数学基础)重点记录下来,这样容易快速记忆一些重点,建立信息安全需要...
  • 我们这里讨论的线程安全,限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的
  • 网络数据安全管理将迎来规 人脸作为敏感个人信息,一旦泄露容易对个人的人身和财产安全造成极大危害,甚至还可能威胁公共安全。但此前,一些小区物业、经营场所将人脸识别作为出入的唯一验证方式;一些手机APP...
  • 过去,互联网企业在安全方面的预算主要投入到了防火墙、堡垒机等传统安全产品,但未来互联网企业势必会不断提升在数据安全方面的投入。
  • 接下来我将开启安全系列,叫“系统安全”,也是免费的100篇文章,作者将更加深入的去研究恶意样本分析、逆向分析、内网渗透、网络攻防实战等,也将通过在线笔记和实践操作的形式分享与博友们学习,希望能与您...
  • 非常感谢举办方让我们学到了知识,DataCon也是我比较喜欢和推荐的大数据安全比赛,这篇文章2020年10月就进了我的草稿箱,但由于小珞珞刚出生,所以今天才发表,希望对您有所帮助!感恩同行,不负青春。
  • 为了保障数据安全,促进数据开发利用,保护公民、组织的合法权益,维护国家主权、安全和发展利益,制定本法。 【解读】 本条规定了《数据安全法(草案)》的立法目的。 第二条 在中华人民共和国境内开展数据活动...
  • 2021年安全类公众号合集

    千次阅读 2021-09-09 20:34:47
    零CERT 奇安信CERT 绿盟科技CERT 快识 3072 Xsafe n1nty lin先森 杂术馆 安全鸭 黑战士 T9Sec Bypass huasec XG小刚 SecWiki 404安全 安译Sec(已注销) 国产008 Kali笔记 边界安全 阿乐你好 乌雲安全 Hack之道 ...
  • 安全体系建设-基础安全

    千次阅读 2021-11-30 21:55:08
    安全体系建设安全层级建设步骤组建安全团队工作内容**生产:** 安全层级 安全体系的建设需要进行分级,粗略的分为以下三个层级。 基础安全 第一阶段大部分公司关注的层级,充当应急的角色,在不断的应急中了解公司...
  • 一文读懂云安全

    2021-05-21 00:27:45
    钛云服已为您服务1063天本文讨论如下问题:1.云安全和传统安全有什么区别2.云安全的挑战及应对3.如何衡量云安全水平4.如何构建有效的云安全团队5.公有云与私有云安全的区别6...
  • 信息安全发展的三个阶段:通信保密,信息安全,信息保障 Wind River的安全专家则针对IoT设备安全提出了如下建议: 安全启动 设备首次开机时,理应采用数字证书对运行的系统和软件作认证; 访问控制 采用不同...
  • 信息安全法律法规

    千次阅读 2021-09-04 10:07:16
    信息安全法律法规一、信息保护相关法律法规1、保护国家秘密相关法律法规2、保护商业秘密相关法律法规3、保护个人信息相关法律法规二、打击网络违法犯罪相关法律法规三、信息安全管理相关法律法规 一、信息保护相关...
  • 《数据安全法》全文见此处,自2021年9月1日起施行。 此处仅对涉及企业部分的条款做记录。 第四章 数据安全保护义务 第二十七条 开展数据处理活动应当依照法律、法规的规定,建立健全全流程数据安全管理制度,组织...
  • 这一年来,网络安全行业兴奋异常。各种会议、攻防大赛、黑客秀,马不停蹄。随着物联网大潮的到来,在这个到处都是安全漏洞的世界,似乎黑客才是安全行业的主宰。然而,我们看到的永远都是自己的世界,正如医生看到的...
  • 数据安全分类分级剖析

    千次阅读 2021-09-15 00:04:46
    数据分类分级对于数据的安全管理至关重要,安全分类分级是一个“硬核课题”,从数据治理开始,除了标准化和价值应用,重要的课题就是质量+安全安全是底线,是价值应用的前提和基础。数据分类可以为数据资产结构化...
  • 为了确保关键信息基础设施供应链安全,维护国家安全,依据《中华人民共和国国家安全法》《中华人民共和国网络安全法》,制定本办法。 【解读】 该条明确了《网络安全审查办法》的立法目的和制定依据。 从立法目的...
  • 《当人工智能遇上安全》系列博客将详细介绍人工智能与安全相关的论文、实践,并分享各种案例,涉及恶意代码检测、恶意请求识别、入侵检测、对抗样本等等。前一篇文章普及了基于机器学习的恶意代码检测技术,主要参考...
  • 接下来我将开启安全系列,叫“系统安全”,也是免费的100篇文章,作者将更加深入的去研究恶意样本分析、逆向分析、内网渗透、网络攻防实战等,也将通过在线笔记和实践操作的形式分享与博友们学习,希望能与您...
  • 本文从网络空间的概念,网络空间安全学科,密码学,网络安全,信息系统安全和信息内容安全个方面进行介绍。 目录 第一章:概念 1.1 网络空间的概念 1.2 网络空间安全的概念 第二章:网络空间安全学科 ...
  • 计算机信息安全认识实习报告

    千次阅读 2020-12-20 20:42:19
    认知实习,通过了解公司的相关信息和技术发展以及招聘情况,让我们了解我们信息安全专业在未来的发展趋势,使我们了解本专业相关领域的发展现状,了解到计算机相关领域的发展现状和最新科研成果,以及在生产科研中的...
  • 看来看去觉得未来安全上能有比较大作为的还是Azure,根本原因有很多,包括Azure Active Directory的身份联动、安全产品的丰富度、Office远程办公安全、ToB的安全基因、安全研发SDL体系等。回归本着能改变云安全,为...
  • title: NCRE三级信息安全笔记 categories: CS Professional tags: Informatica security description: 考NCRE三级信息安全时的一些笔记 | 不完整 abbrlink: 758c date: 2021-03-19 00:00:00 第一章 信息安全保障...
  • 网络空间安全复习归纳

    千次阅读 2021-01-03 13:55:38
    网络空间安全复习归纳 第一章:网络空间安全概述 1.生活中的网络空间安全问题: 1)QQ账号被盗 2)支付宝账号被盗 3)银行卡被盗刷 (钓鱼网站 ,个人隐私泄露,网络诈骗,摄像头等遭到入侵) 2.工作中的网络空间...
  • 网络安全期末总结

    千次阅读 多人点赞 2021-01-07 16:40:19
    写在前面 作者:夏日 ...题型 选择题:20*1 判断题:5*2 简答题:包括问答题和分析题 8*5 应用题:20*2 选择题 选择题不必每个都记忆...也可能让选实体安全(物理安全)、系统安全、运行安全、管理安全、应用安全。 3.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 305,298
精华内容 122,119
关键字:

安全六新