{"id":1610,"date":"2012-08-26T16:37:29","date_gmt":"2012-08-26T14:37:29","guid":{"rendered":"http:\/\/doanduyhai.wordpress.com\/?p=1610"},"modified":"2015-01-30T20:47:33","modified_gmt":"2015-01-30T20:47:33","slug":"recaptcha-login-form-with-spring-security","status":"publish","type":"post","link":"https:\/\/www.doanduyhai.com\/blog\/?p=1610","title":{"rendered":"ReCaptcha login form with Spring Security"},"content":{"rendered":"<p>Today I&#8217;ll show you how to customize Spring Security to create a login form with ReCaptcha verification<\/p>\n<p> The captcha is based on <strong>Google ReCaptcha<\/strong> plugin. More info on ReCaptcha <a href=\"http:\/\/www.google.com\/recaptcha\" title=\"http:\/\/www.google.com\/recaptcha\" target=\"_blank\">here<\/a> <\/p>\n<p><!--more--><\/p>\n<blockquote><p>The following implementation is based on <strong>Spring MVC<\/strong> &amp; <strong>ThymeLeaf<\/strong>. If you are using another web framework, you&#8217;ll need to adapt a little bit the code<\/p><\/blockquote>\n<h1>I Features description<\/h1>\n<p> The final login form looks like this:<\/p>\n<p><a href=\"http:\/\/doanduyhai.files.wordpress.com\/2012\/08\/recaptcha_login.png\"><img loading=\"lazy\" src=\"http:\/\/doanduyhai.files.wordpress.com\/2012\/08\/recaptcha_login.png\" alt=\"ReCaptcha_Login\" title=\"ReCaptcha_Login\" width=\"385\" height=\"429\" class=\"aligncenter size-full wp-image-1612\" srcset=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/08\/recaptcha_login.png 385w, https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/08\/recaptcha_login-269x300.png 269w\" sizes=\"(max-width: 385px) 100vw, 385px\" \/><\/a><\/p>\n<p>  Below is the functional requirements for such a login form:<\/p>\n<ul>\n<li>Do not attempt to verify ReCaptcha response if the user input is blank<\/li>\n<li>Check ReCaptcha first before any attempt to verify login\/password<\/li>\n<li>Return a default error message if the Recaptcha verification fails. Pre-fill the login field with previous value but reset the password field<\/li>\n<li>Return a default error message if login\/password verification fails<\/li>\n<\/ul>\n<p> The requirements are quite simple. I will not explain in details the ReCaptcha verification process because it is out of the scope of this post. You can have more details on their <a href=\"https:\/\/developers.google.com\/recaptcha\/?hl=fr\" title=\"https:\/\/developers.google.com\/recaptcha\/?hl=fr\" target=\"_blank\">documentation<\/a> page<\/p>\n<p>&nbsp;<\/p>\n<h1>II Code Implementation<\/h1>\n<h3>A Custom Authentication filter<\/h3>\n<p> Let&#8217;s see how we can implements the requirement with a custom Authentication filter, extending the existing <strong>UsernamePasswordAuthenticationFilter<\/strong><\/p>\n<pre class=\"brush: java; highlight: [10,85]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\npublic class ReCaptchaAuthenticationFilter extends UsernamePasswordAuthenticationFilter implements InitializingBean\r\n{\r\n\tprivate final String CAPTCHA_CHALLENGE_FIELD = &quot;recaptcha_challenge_field&quot;;\r\n\tprivate final String CAPTCHA_RESPONSE_FIELD = &quot;recaptcha_response_field&quot;;\r\n\tprivate final ReCaptchaImpl reCaptcha;\r\n\tprivate String privateKey;\r\n\tprivate final Logger log = LoggerFactory.getLogger(ReCaptchaAuthenticationFilter.class);\r\n\r\n\tpublic ReCaptchaAuthenticationFilter() {\r\n\t\tthis.reCaptcha = new ReCaptchaImpl();\r\n\t}\r\n\r\n\tpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException\r\n\t{\r\n\r\n\t\tString reCaptchaChallenge = request.getParameter(CAPTCHA_CHALLENGE_FIELD);\r\n\t\tString reCaptchaResponse = request.getParameter(CAPTCHA_RESPONSE_FIELD);\r\n\t\tString remoteAddress = request.getRemoteAddr();\r\n\r\n\t\tif (!StringUtils.isEmpty(reCaptchaChallenge))\r\n\t\t{\r\n\t\t\tlog.debug(&quot;ReCaptcha Challenge not null&quot;);\r\n\t\t\tif (!StringUtils.isEmpty(reCaptchaResponse))\r\n\t\t\t{\r\n\t\t\t\tlog.debug(&quot;ReCaptcha Answser not null, call ReCaptcha to verify it&quot;);\r\n\t\t\t\tReCaptchaResponse reCaptchaCheck = reCaptcha.checkAnswer(remoteAddress, reCaptchaChallenge, reCaptchaResponse);\r\n\r\n\t\t\t\tif (reCaptchaCheck.isValid())\r\n\t\t\t\t{\r\n\t\t\t\t\tlog.debug(&quot;ReCaptcha answer is valid, attempt authentication&quot;);\r\n\t\t\t\t\treturn super.attemptAuthentication(request, response);\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tthis.reCaptchaError(request, response, &quot;ReCaptcha failed : &quot; + reCaptchaCheck.getErrorMessage());\r\n\t\t\t\t\treturn null;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tthis.reCaptchaError(request, response, &quot;ReCaptcha failed : empty answer&quot;);\r\n\t\t\t\treturn null;\r\n\t\t\t}\r\n\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\treturn super.attemptAuthentication(request, response);\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void reCaptchaError(HttpServletRequest request, HttpServletResponse response, String errorMsg)\r\n\t{\r\n\t\tlog.error(&quot;ReCaptcha failed : &quot; + errorMsg);\r\n\t\ttry\r\n\t\t{\r\n\r\n\t\t\tRequestDispatcher dispatcher = request.getRequestDispatcher(&quot;\/login?error=2&quot;);\r\n\r\n\t\t\tdispatcher.forward(request, response);\r\n\t\t}\r\n\t\tcatch (ServletException e)\r\n\t\t{\r\n\t\t\tthrow new AuthenticationServiceException(&quot;ReCaptcha failed : &quot; + errorMsg);\r\n\t\t}\r\n\t\tcatch (IOException e)\r\n\t\t{\r\n\t\t\tthrow new AuthenticationServiceException(&quot;Recaptcha failed : &quot; + errorMsg);\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void setPrivateKey(String privateKey)\r\n\t{\r\n\t\tthis.privateKey = privateKey;\r\n\t}\r\n\r\n\tpublic void afterPropertiesSet()\r\n\t{\r\n\t\tif (StringUtils.isEmpty(this.privateKey))\r\n\t\t{\r\n\t\t\tthrow new IllegalArgumentException(&quot;The 'privateKey' should be set for the bean type 'ReCaptchaAuthenticationFilter'&quot;);\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\treCaptcha.setPrivateKey(this.privateKey);\r\n\t\t}\r\n\t}\r\n}\r\n<\/pre>\n<p> The implementation is quite simple.<\/p>\n<ul>\n<li>If <strong>CAPTCHA_CHALLENGE_FIELD<\/strong> is blank (the requested page is not the login page), call the <strong>super<\/strong>.<em>attemptAuthentication()<\/em> method<\/li>\n<li>\n\tElse<\/p>\n<ul>\n<li>\n\tIf the <strong>CAPTCHA_RESPONSE_FIELD<\/strong> is empty (the user does not answer the <strong>ReCaptcha<\/strong>), forward the user to the login page with the <strong>error code 2<\/strong>\n\t<\/li>\n<li>\n\tElse<\/p>\n<ul>\n<li>If the <strong>ReCaptcha<\/strong> verification fails, forward the user to the login page with the <strong>error code 2<\/strong><\/li>\n<li>Else call the <strong>super<\/strong>.<em>attemptAuthentication()<\/em> method<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Please note the injection of the <strong>private key<\/strong> for <strong>ReCaptcha<\/strong> plugin and the initialization of the <strong>ReCaptchaImpl<\/strong> instance in the constructor.<\/p>\n<p>&nbsp;<\/p>\n<h3>B Spring MVC Controller for login page<\/h3>\n<pre class=\"brush: java; highlight: [7,9,11,14]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@RequestMapping(&quot;\/login&quot;)\r\npublic String loginPage(@RequestParam(value = &quot;error&quot;, required = false) Integer errorCode, Model model, HttpServletRequest servletRequest)\r\n{\r\n\tlog.info(&quot;Is current device mobile : &quot; + currentDevice.isMobile());\r\n\tif (errorCode != null)\r\n\t{\r\n\t\tif (errorCode.equals(1))\r\n\t\t{\r\n\t\t\tmodel.addAttribute(&quot;authenticationError&quot;, true);\r\n\t\t}\r\n\t\telse if (errorCode.equals(2))\r\n\t\t{\r\n\t\t\tString login = servletRequest.getParameter(&quot;j_username&quot;);\r\n\t\t\tmodel.addAttribute(&quot;reCaptchaError&quot;, true);\r\n\t\t\tmodel.addAttribute(&quot;login&quot;, login);\r\n\t\t}\r\n\t}\r\n\r\n\treturn &quot;pages\/login.html&quot;;\r\n}\r\n<\/pre>\n<p> In the controller class, we extract the request parameter <strong>error<\/strong> (if any) and, depending on its value, set the <em>authenticationError<\/em> or <em>reCaptchaError<\/em> value to the model to be passed to the view handler.<\/p>\n<p> Please notice the <strong>required<\/strong> = <em>false<\/em> setting on the <em>@RequestParam(<strong>value<\/strong> = &#8220;error&#8221;, <strong>required<\/strong> = false)<\/em>. If not set (default value is <strong>true<\/strong>) <strong>Spring MVC<\/strong> will raise an error if the parameter is not found, which we don&#8217;t want.<\/p>\n<p>&nbsp;<\/p>\n<h3>C Login form design<\/h3>\n<p> Below is the design of the login form. Please notice that I use <a href=\"http:\/\/www.thymeleaf.org\/\" title=\"http:\/\/www.thymeleaf.org\/\" target=\"_blank\">ThymeLeaf<\/a> and <a href=\"http:\/\/twitter.github.com\/bootstrap\/\" title=\"http:\/\/twitter.github.com\/bootstrap\/\" target=\"_blank\">Twitter Bootstrap<\/a>. <\/p>\n<pre class=\"brush: xml; highlight: [5,11]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n            &lt;h1&gt;Authentication&lt;\/h1&gt;\r\n            &lt;form action=&quot;authentication&quot; th:action=&quot;@{\/authentication}&quot; method=&quot;post&quot; class=&quot;well center&quot;&gt;\r\n            \t\r\n            \t&lt;br th:if=&quot;${authenticationError}&quot;\/&gt;\r\n            \t&lt;div th:if=&quot;${authenticationError}&quot; class=&quot;alert alert-error&quot;&gt;\r\n            \t\tIncorrect Login\/Password\r\n            \t&lt;\/div&gt;\r\n            \t&lt;br th:if=&quot;${authenticationError}&quot;\/&gt;\r\n            \t\r\n            \t&lt;br th:if=&quot;${reCaptchaError}&quot;\/&gt;\r\n            \t&lt;div th:if=&quot;${reCaptchaError}&quot; class=&quot;alert alert-error&quot;&gt;\r\n            \t\tIncorrect reCaptcha\r\n            \t&lt;\/div&gt;\r\n            \t&lt;br th:if=&quot;${reCaptchaError}&quot;\/&gt;\r\n            \t\r\n                &lt;fieldset&gt;\r\n                    &lt;label&gt;Login :&lt;\/label&gt; \r\n                    \t&lt;input id=&quot;j_username&quot; name=&quot;j_username&quot;\r\n                        \ttype=&quot;text&quot; required=&quot;required&quot; autofocus=&quot;autofocus&quot; class=&quot;input span3&quot;\r\n                        \tth:value=&quot;${login}&quot;\r\n                            placeholder=&quot;Your login...&quot;\/&gt;\r\n                    &lt;label&gt;Password :&lt;\/label&gt; \r\n                    \t&lt;input id=&quot;j_password&quot; name=&quot;j_password&quot;\r\n                        \ttype=&quot;password&quot; required=&quot;required&quot; class=&quot;input span3&quot;\r\n                            placeholder=&quot;Your password...&quot;\/&gt;\r\n\t\t\t\t&lt;label class=&quot;checkbox left&quot;&gt;\r\n\t                &lt;input type=&quot;checkbox&quot;\r\n\t                       name=&quot;_spring_security_remember_me&quot; \r\n\t                       id=&quot;_spring_security_remember_me&quot;\r\n\t                       value=&quot;true&quot; \/&gt;&amp;nbsp;Remember me\r\n            \t&lt;\/label&gt;\r\n                                                     \r\n                &lt;\/fieldset&gt;\r\n                &lt;br\/&gt;\r\n                Additional anti-bot authentication\r\n                &lt;br\/&gt;\r\n                &lt;script type=&quot;text\/javascript&quot;\r\n     \t\t\t\tsrc=&quot;http:\/\/www.google.com\/recaptcha\/api\/challenge?k=your_public_recaptcha_key&quot;&gt;\r\n                &lt;\/script&gt;\r\n                &lt;noscript&gt;\r\n\t\t\t\t\t&lt; iframe src=&quot;http:\/\/www.google.com\/recaptcha\/api\/noscript?k=your_public_recaptcha_key&quot; height=&quot;300&quot; width=&quot;500&quot; &gt; &lt; \/iframe &gt;\r\n\t\t\t\t\t&lt;br\/&gt;\r\n\t\t\t\t\t&lt;textarea name=&quot;recaptcha_challenge_field&quot; rows=&quot;3&quot; cols=&quot;40&quot;&gt;&lt;\/textarea&gt;\r\n\t\t\t\t\t&lt;input type=&quot;hidden&quot; name=&quot;recaptcha_response_field&quot; value=&quot;manual_challenge&quot;&gt;&lt;\/input&gt;\r\n\t\t\t\t&lt;\/noscript&gt;\r\n\t\t\t\t&lt;br\/&gt;\r\n\t\t\t\t&lt;button type=&quot;submit&quot; class=&quot;btn btn-success&quot;&gt;Authenticate&lt;\/button&gt;\r\n            &lt;\/form&gt;\r\n<\/pre>\n<p> Notice the check for <em>authenticationError<\/em> and <em>reCaptchaError<\/em> attribute for error message display (<strong>lines 5 &amp; 11<\/strong>)<br \/>\n&nbsp;<\/p>\n<h1>III Spring Security configuration<\/h1>\n<h3>A Full XML config<\/h3>\n<p> Below is the full XML config to use the above custom <strong>ReCaptchaAuthenticationFilter<\/strong><\/p>\n<pre class=\"brush: xml; highlight: [12,15]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n\t&lt;bean id=&quot;authenticationProcessingFilter&quot; class=&quot;fr.doan.security.ReCaptchaAuthenticationFilter&quot;&gt;\r\n  \t\t&lt;property name=&quot;authenticationManager&quot; ref=&quot;authenticationManager&quot;\/&gt;\r\n  \t\t&lt;property name=&quot;filterProcessesUrl&quot; value=&quot;\/authentication&quot;\/&gt;\r\n  \t\t&lt;property name=&quot;authenticationSuccessHandler&quot;&gt;\r\n\t        &lt;bean class=&quot;org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler&quot;&gt;\r\n\t        \t&lt;property name=&quot;alwaysUseDefaultTargetUrl&quot; value=&quot;true&quot;\/&gt;\r\n\t            &lt;property name=&quot;defaultTargetUrl&quot; value=&quot;\/home&quot; \/&gt;\r\n\t        &lt;\/bean&gt;\r\n\t    &lt;\/property&gt;\r\n\t    &lt;property name=&quot;authenticationFailureHandler&quot;&gt;\r\n\t    \t&lt;bean class=&quot;org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler&quot;&gt;\r\n\t            &lt;property name=&quot;defaultFailureUrl&quot; value=&quot;\/login?error=1&quot; \/&gt;\r\n\t        &lt;\/bean&gt;\r\n\t    &lt;\/property&gt;\r\n\t    &lt;property name=&quot;privateKey&quot; value=&quot;your_private_recaptcha_key&quot;\/&gt;\r\n\t&lt;\/bean&gt; \r\n<\/pre>\n<p> The configuration is quite straightforward. The only major difference with a &#8220;classic&#8221; <strong>UsernamePasswordAuthenticationFilter<\/strong> is the injection of the privateKey (<strong>line 15<\/strong>).<\/p>\n<p>&nbsp;<\/p>\n<h3>B  Spring Security namespace config<\/h3>\n<p>If you&#8217;re using the Spring Security namespace, the configuration is a little bit trickier<\/p>\n<pre class=\"brush: xml; highlight: [3,4,7,8,9,10,11,12,13,20,25,26,42]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n   &lt;http auto-config=&quot;false&quot; \r\n    \tuse-expressions=&quot;true&quot;\r\n    \tentry-point-ref=&quot;loginUrlAuthenticationEntryPoint&quot;&gt;\r\n    \t&lt;custom-filter ref=&quot;authenticationProcessingFilter&quot; position=&quot;FORM_LOGIN_FILTER&quot;\/&gt;\r\n        &lt;intercept-url pattern=&quot;\/**&quot; access=&quot;isAuthenticated()&quot; \/&gt;\r\n        &lt;!-- \r\n        &lt;form-login\r\n                login-processing-url=&quot;\/authentication&quot;\r\n                login-page=&quot;\/login&quot;\r\n                authentication-failure-url=&quot;\/login?error=1&quot;\r\n                default-target-url=&quot;\/home&quot;\r\n                always-use-default-target=&quot;true&quot;\r\n\t\t\/&gt;\r\n\t\t --&gt;\r\n        &lt;remember-me user-service-ref=&quot;userDetailsService&quot;\/&gt;\r\n        &lt;logout logout-url=&quot;\/logout&quot;\r\n                logout-success-url=&quot;\/login&quot;\/&gt;\r\n    &lt;\/http&gt;\r\n\t\r\n\t&lt;beans:bean id=&quot;loginUrlAuthenticationEntryPoint&quot;\r\n  \t\tclass=&quot;org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint&quot;&gt;\r\n  \t\t&lt;beans:property name=&quot;loginFormUrl&quot; value=&quot;\/login&quot; \/&gt;\r\n\t&lt;\/beans:bean&gt;\r\n\t\r\n\t&lt;beans:bean id=&quot;authenticationProcessingFilter&quot; class=&quot;fr.doan.security.ReCaptchaAuthenticationFilter&quot;&gt;\r\n  \t\t&lt;beans:property name=&quot;authenticationManager&quot; ref=&quot;authenticationManager&quot;\/&gt;\r\n  \t\t&lt;beans:property name=&quot;filterProcessesUrl&quot; value=&quot;\/authentication&quot;\/&gt;\r\n  \t\t&lt;beans:property name=&quot;authenticationSuccessHandler&quot;&gt;\r\n\t        &lt;beans:bean class=&quot;org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler&quot;&gt;\r\n\t        \t&lt;beans:property name=&quot;alwaysUseDefaultTargetUrl&quot; value=&quot;true&quot;\/&gt;\r\n\t            &lt;beans:property name=&quot;defaultTargetUrl&quot; value=&quot;\/home&quot; \/&gt;\r\n\t        &lt;\/beans:bean&gt;\r\n\t    &lt;\/beans:property&gt;\r\n\t    &lt;beans:property name=&quot;authenticationFailureHandler&quot;&gt;\r\n\t    \t&lt;beans:bean class=&quot;org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler&quot;&gt;\r\n\t            &lt;beans:property name=&quot;defaultFailureUrl&quot; value=&quot;\/login?error=1&quot; \/&gt;\r\n\t        &lt;\/beans:bean&gt;\r\n\t    &lt;\/beans:property&gt;\r\n\t    &lt;beans:property name=&quot;privateKey&quot; value=&quot;your_private_recaptcha_keyword&quot;\/&gt;\r\n\t&lt;\/beans:bean&gt; \r\n\t\t\r\n    &lt;authentication-manager alias=&quot;authenticationManager&quot;&gt;\r\n        &lt;authentication-provider user-service-ref=&quot;userDetailsService&quot;\/&gt;\r\n    &lt;\/authentication-manager&gt;\r\n<\/pre>\n<p>First, we need to declare the same <strong>ReCaptchaAuthenticationFilter<\/strong> as previously (<strong>line 25<\/strong>). No big change here.<\/p>\n<p> To inject this filter into the security chain, we need to<\/p>\n<ul>\n<li>Add a custom filter at the same position as the <strong>FORM_LOGIN_FILTER<\/strong><\/li>\n<li>Remove the <strong>&lt;form-login&gt;<\/strong> tag (<strong>lines 7 to 13<\/strong>) otherwise you&#8217;ll end up having the <strong>ReCaptchaAuthenticationFilter<\/strong> and the <strong>UsernamePasswordAuthenticationFilter<\/strong> at the same time ( the <strong>&lt;form-login&gt;<\/strong> tag is translated by <strong>Spring Security<\/strong> into a <strong>UsernamePasswordAuthenticationFilter<\/strong>)<\/li>\n<\/ul>\n<p> Since the <strong>&lt;form-login&gt;<\/strong> tag is removed, we need to define an entry point for the login process (<strong>line 3<\/strong>) by instanciating the <strong>LoginUrlAuthenticationEntryPoint<\/strong> separately (<strong>line 20<\/strong>)<\/p>\n<p> Last but not least, we need to give an alias to the AuthenticationManager defined with the <strong>&lt;authentication-manager&gt;<\/strong> tag. By default this tag is read and converted into a bean declaration with the default name <em>&#8216;org.springframework.security.authenticationManager&#8217;<\/em>.<\/p>\n<p> Either we use this default name and inject it into our custom <strong>RecaptchaAuthenticationFilter<\/strong>, or we define an alias and use this alias for injection (<strong>lines 26 &amp; 42<\/strong>).<\/p>\n<p>&nbsp;<br \/>\n&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today I&#8217;ll show you how to customize Spring Security to create a login form with ReCaptcha verification The captcha is based on Google ReCaptcha plugin. More info on ReCaptcha here<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[30,14],"tags":[],"_links":{"self":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1610"}],"collection":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1610"}],"version-history":[{"count":2,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1610\/revisions"}],"predecessor-version":[{"id":1702,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/1610\/revisions\/1702"}],"wp:attachment":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1610"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1610"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1610"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}