{"id":1505,"date":"2023-03-25T13:57:00","date_gmt":"2023-03-25T05:57:00","guid":{"rendered":"https:\/\/www.appblog.cn\/?p=1505"},"modified":"2023-04-28T20:22:37","modified_gmt":"2023-04-28T12:22:37","slug":"springboot-google-two-step-verification","status":"publish","type":"post","link":"https:\/\/www.appblog.cn\/index.php\/2023\/03\/25\/springboot-google-two-step-verification\/","title":{"rendered":"SpringBoot-Google\u4e8c\u6b65\u9a8c\u8bc1"},"content":{"rendered":"<p>\u6982\u5ff5\uff1aGoogle\u8eab\u4efd\u9a8c\u8bc1\u5668Google Authenticator\u662f\u8c37\u6b4c\u63a8\u51fa\u7684\u57fa\u4e8e\u65f6\u95f4\u7684\u4e00\u6b21\u6027\u5bc6\u7801(Time-based One-time Password\uff0c\u7b80\u79f0TOTP)\uff0c\u53ea\u9700\u8981\u5728\u624b\u673a\u4e0a\u5b89\u88c5\u8be5APP\uff0c\u5c31\u53ef\u4ee5\u751f\u6210\u4e00\u4e2a\u968f\u7740\u65f6\u95f4\u53d8\u5316\u7684\u4e00\u6b21\u6027\u5bc6\u7801\uff0c\u7528\u4e8e\u5e10\u6237\u9a8c\u8bc1\u3002<\/p>\n<p>Google\u8eab\u4efd\u9a8c\u8bc1\u5668\u662f\u4e00\u6b3e\u57fa\u4e8e\u65f6\u95f4\u4e0e\u54c8\u5e0c\u7684\u4e00\u6b21\u6027\u5bc6\u7801\u7b97\u6cd5\u7684\u4e24\u6b65\u9a8c\u8bc1\u8f6f\u4ef6\u4ee4\u724c\uff0c\u6b64\u8f6f\u4ef6\u7528\u4e8eGoogle\u7684\u8ba4\u8bc1\u670d\u52a1\u3002\u6b64\u9879\u670d\u52a1\u6240\u4f7f\u7528\u7684\u7b97\u6cd5\u5df2\u5217\u4e8eRFC 6238\u548cRFC 4226\u4e2d\u3002<\/p>\n<p><!-- more --><\/p>\n<h2>\u6d41\u7a0b<\/h2>\n<ul>\n<li>\u7528\u6237\u8bf7\u6c42\u670d\u52a1\u5668\u751f\u6210\u5bc6\u94a5<\/li>\n<li>\u670d\u52a1\u5668\u751f\u6210\u4e00\u4e2a\u5bc6\u94a5\u5e76\u4e0e\u7528\u6237\u4fe1\u606f\u8fdb\u884c\u5173\u8054\uff0c\u5e76\u8fd4\u56de\u5bc6\u94a5(\u7c7b\u4f3c\uff1aXX57HWC7D2FA4X4GLOHOASTGPMVI5EFA)\u548c\u4e00\u4e2a\u4e8c\u7ef4\u7801\u4fe1\u606f\uff08\u6b64\u6b65\u9aa4\u8fd8\u6ca1\u6709\u7ed1\u5b9a\uff09<\/li>\n<li>\u7528\u6237\u628a\u8fd4\u56de\u7684\u4e8c\u7ef4\u7801\u4fe1\u606f\u4f20\u7ed9\u670d\u52a1\u5668\uff0c\u751f\u6210\u4e00\u4e2a\u4e8c\u7ef4\u7801\u3002\u4fe1\u606f\u5927\u6982\u957f\u8fd9\u6837\u7684\uff1a<code>otpauth:\/\/totp\/https%3A%2F%2Fwww.appblog.cn%3Arstyro?secret=XX57HWC7D2FA4X4GLOHOASTGPMVI5EFA&amp;issuer=https%3A%2F%2Fwww.appblog.cn<\/code><\/li>\n<li>\u7528\u6237\u901a\u8fc7\u8eab\u4efd\u9a8c\u8bc1\u5668\u626b\u63cf\u4e8c\u7ef4\u7801\u5373\u53ef\u751f\u6210\u4e00\u4e2a\u52a8\u6001\u7684\u9a8c\u8bc1\u7801<\/li>\n<li>\u7528\u6237\u4f20\u5f53\u524d\u52a8\u6001\u7684\u9a8c\u8bc1\u7801\u548c\u5bc6\u94a5\u7ed9\u670d\u52a1\u5668\uff0c\u6821\u9a8c\u5bc6\u94a5\u7684\u6b63\u786e\u6027\uff08\u6b64\u5bc6\u94a5\u4e0e\u7528\u6237\u771f\u6b63\u7684\u7ed1\u5b9a\uff09<\/li>\n<\/ul>\n<p>\u4e0a\u9762\u6709\u4e9b\u6b65\u9aa4\u4e0d\u5fc5\u987b\u7684\uff0c\u770b\u9700\u6c42\uff0c\u53ef\u4ee5\u7b80\u5316\u4e3a\u4e24\u6b65\u3002<br \/>\n1\u3001\u751f\u6210\u5bc6\u94a5<br \/>\n2\u3001\u626b\u7801<\/p>\n<h2>\u5b89\u88c5\u8eab\u4efd\u9a8c\u8bc1\u5668<\/h2>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener\" href=\"https:\/\/itunes.apple.com\/cn\/app\/google-authenticator\/id388497605\" title=\"IOS \u7248\u672c\uff1aGoogle Authenticator\uff08\u53ef\u4ee5\u5728App Store\u641c\u7d22google authenticator\uff09\">IOS \u7248\u672c\uff1aGoogle Authenticator\uff08\u53ef\u4ee5\u5728App Store\u641c\u7d22google authenticator\uff09<\/a><\/li>\n<li><a target=\"_blank\" rel=\"noopener\" href=\"https:\/\/play.google.com\/store\/apps\/details?id=com.google.android.apps.authenticator2\" title=\"\u5b89\u5353\uff1aGoogle Authenticator\">\u5b89\u5353\uff1aGoogle Authenticator<\/a><\/li>\n<\/ul>\n<p>\u5ba2\u6237\u7aef\u6bcf30\u79d2\u5c31\u4f1a\u751f\u6210\u65b0\u7684\u9a8c\u8bc1\u7801<\/p>\n<h2>\u4ee3\u7801\u5b9e\u73b0<\/h2>\n<h3>\u524d\u8a00<\/h3>\n<ul>\n<li>\u4e3a\u4e86\u6bd4\u8f83\u771f\u5b9e\u6240\u4ee5\u6dfb\u52a0\u4e86\u6ce8\u518c\u548c\u767b\u5f55\u63a5\u53e3<\/li>\n<li>\u4e3a\u4e86\u65b9\u4fbf\u96c6\u6210\u4e86Swagger-ui \u548c\u5168\u90e8\u662fGET\u8bf7\u6c42\uff0c\u4e0d\u4f1a\u7528Swagger-ui\uff0c\u5c31\u76f4\u63a5\u5730\u5740\u680f\u8bf7\u6c42\u6216\u8005Postman\u90fd\u53ef<\/li>\n<li>\u6ce8\u518c\u7528\u6237\u5168\u90e8\u653eRedis<\/li>\n<li>\u767b\u5f55\u4e0eGoogle\u6821\u9a8c\uff0c\u4e5f\u4ee5\u6ce8\u89e3\u65b9\u5f0f\u5b9e\u73b0<\/li>\n<li>\u767b\u5f55\u4ee5token\u65b9\u5f0f\uff0c\u6240\u4ee5\u8bf7\u6c42\u5176\u4ed6\u63a5\u53e3\u7684\u65f6\u5019\u90fd\u8981\u5e26\u4e0atoken\uff0c\u53ef\u4ee5\u628atoken\u653e\u5728header\u91cc\u9762<\/li>\n<\/ul>\n<h3>\u4ee3\u7801<\/h3>\n<h4>UserController<\/h4>\n<ul>\n<li>\u63a7\u5236\u5c42\u63a5\u53e3<\/li>\n<li>\u6d41\u7a0b\u4ece\u4e0a\u5f80\u4e0b\u6267\u884c\u5373\u53ef<\/li>\n<\/ul>\n<pre><code class=\"language-java\">import io.swagger.annotations.Api;\nimport io.swagger.annotations.ApiOperation;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.*;\nimport cn.appnlog.googlecheck.annotation.NeedLogin;\nimport cn.appnlog.googlecheck.base.BaseController;\nimport cn.appnlog.googlecheck.common.Result;\nimport cn.appnlog.googlecheck.dto.GoogleDTO;\nimport cn.appnlog.googlecheck.dto.LoginDTO;\nimport cn.appnlog.googlecheck.service.UserService;\nimport cn.appnlog.googlecheck.utils.QRCodeUtil;\n\nimport javax.imageio.ImageIO;\nimport javax.servlet.http.HttpServletResponse;\nimport java.awt.image.BufferedImage;\nimport java.io.OutputStream;\n\n@Controller\n@RequestMapping(&quot;\/user&quot;)\n@Api(tags = &quot;\u7528\u6237\u6a21\u5757&quot;)\npublic class UserController extends BaseController {\n\n    @Autowired\n    private UserService userService;\n\n    @GetMapping(&quot;\/register&quot;)\n    @ApiOperation(&quot;\u6ce8\u518c&quot;)\n    @ResponseBody\n    public Result register(LoginDTO dto) throws Exception {\n        return userService.register(dto);\n    }\n\n    @GetMapping(&quot;\/login&quot;)\n    @ApiOperation(&quot;\u767b\u5f55&quot;)\n    @ResponseBody\n    public Result login(LoginDTO dto)throws Exception {\n        return userService.login(dto);\n    }\n\n    @GetMapping(&quot;\/generateGoogleSecret&quot;)\n    @ResponseBody\n    @NeedLogin\n    @ApiOperation(&quot;\u751f\u6210google\u5bc6\u94a5&quot;)\n    public Result generateGoogleSecret()throws Exception {\n        return userService.generateGoogleSecret(this.getUser());\n    }\n\n    \/**\n     * \u663e\u793a\u4e00\u4e2a\u4e8c\u7ef4\u7801\u56fe\u7247\n     * @param secretQrCode   generateGoogleSecret\u63a5\u53e3\u8fd4\u56de\u7684\uff1asecretQrCode\n     * @param response\n     * @throws Exception\n     *\/\n    @GetMapping(&quot;\/genQrCode&quot;)\n    @ApiOperation(&quot;\u751f\u6210\u4e8c\u7ef4\u7801&quot;)\n    public void genQrCode(String secretQrCode, HttpServletResponse response) throws Exception {\n        response.setContentType(&quot;image\/png&quot;);\n        OutputStream stream = response.getOutputStream();\n        QRCodeUtil.encode(secretQrCode,stream);\n    }\n\n    @GetMapping(&quot;\/bindGoogle&quot;)\n    @ResponseBody\n    @NeedLogin\n    @ApiOperation(&quot;\u7ed1\u5b9agoogle\u9a8c\u8bc1&quot;)\n    public Result bindGoogle(GoogleDTO dto)throws Exception {\n        return userService.bindGoogle(dto,this.getUser(),this.getRequest());\n    }\n\n    @GetMapping(&quot;\/googleLogin&quot;)\n    @ResponseBody\n    @NeedLogin\n    @ApiOperation(&quot;google\u767b\u5f55&quot;)\n    public Result googleLogin(Long code) throws Exception {\n        return userService.googleLogin(code,this.getUser(),this.getRequest());\n    }\n\n    @GetMapping(&quot;\/getData&quot;)\n    @NeedLogin(google = true)\n    @ApiOperation(&quot;\u83b7\u53d6\u7528\u6237\u6570\u636e\u4e0etoken\u6570\u636e\uff0c\u524d\u63d0\u9700\u8981google\u8ba4\u8bc1\u624d\u80fd\u8bbf\u95ee&quot;)\n    @ResponseBody\n    public Result getData()throws Exception {\n        return userService.getData();\n    }\n}<\/code><\/pre>\n<h4>UserService<\/h4>\n<ul>\n<li>\u670d\u52a1\u5c42\uff0c\u4e1a\u52a1\u4ee3\u7801<\/li>\n<\/ul>\n<pre><code class=\"language-java\">import org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\nimport cn.appnlog.encryption.MDUtil;\nimport cn.appnlog.googlecheck.common.*;\nimport cn.appnlog.googlecheck.dto.GoogleDTO;\nimport cn.appnlog.googlecheck.dto.LoginDTO;\nimport cn.appnlog.googlecheck.entity.User;\nimport cn.appnlog.googlecheck.utils.GoogleAuthenticator;\nimport cn.appnlog.googlecheck.utils.Tools;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\n@Service\npublic class UserService {\n\n    @Autowired\n    private RedisTemplate&lt;String, Object&gt; redisTemplate;\n\n    \/**\n     * \u83b7\u53d6\u7f13\u5b58\u4e2d\u7684\u6570\u636e\n     * @return\n     *\/\n    public Result getData() {\n        Map&lt;String, Object&gt; data = new HashMap&lt;&gt;();\n        setData(CacheKey.REGISTER_USER_KEY, data);\n        setData(CacheKey.TOKEN_KEY_LOGIN_KEY, data);\n        return Result.ok(data);\n    }\n\n    public void setData(String keyword, Map&lt;String, Object&gt; data) {\n        Set&lt;String&gt; keys = redisTemplate.keys(keyword);\n        Iterator&lt;String&gt; iterator = keys.iterator();\n        while (iterator.hasNext()) {\n            String key = iterator.next();\n            data.put(key,redisTemplate.opsForValue().get(key));\n        }\n    }\n\n    \/**\n     * \u6ce8\u518c\n     * @param dto\n     * @return\n     * @throws Exception\n     *\/\n    public Result register(LoginDTO dto) throws Exception {\n        User user = new User();\n        user.setUserId(Tools.getUUID());\n        user.setUsername(dto.getUsername());\n        user.setPassword(MDUtil.bcMD5(dto.getPassword()));\n        addUser(user);\n        return Result.ok();\n    }\n\n    \/\/\u83b7\u53d6\u7528\u6237\n    public User getUser(String username) {\n        User cacheUser = (User) redisTemplate.opsForValue().get(String.format(CacheKey.REGISTER_USER, username));\n        return cacheUser;\n    }\n\n    \/\/\u6dfb\u52a0\u6ce8\u518c\u7528\u6237\n    public void addUser(User user) {\n        if (user == null) throw new ApiException(ApiResultEnum.ERROR_NULL);\n        User isRepeat = getUser(user.getUsername());\n        if (isRepeat != null) {\n            throw new ApiException(ApiResultEnum.USER_IS_EXIST);\n        }\n        redisTemplate.opsForValue().set(String.format(CacheKey.REGISTER_USER, user.getUsername()), user, 1, TimeUnit.DAYS);\n    }\n\n    \/\/\u66f4\u65b0token\u7528\u6237\n    public void updateUser(User user, HttpServletRequest request) {\n        if (user == null) throw new ApiException(ApiResultEnum.ERROR_NULL);\n        redisTemplate.opsForValue().set(Tools.getTokenKey(request, CacheEnum.LOGIN), user, 1, TimeUnit.DAYS);\n    }\n\n    \/**\n     * \u767b\u5f55\n     * @param dto\n     * @return\n     * @throws Exception\n     *\/\n    public Result login(LoginDTO dto) throws Exception {\n        User user = getUser(dto.getUsername());\n        if (user == null){\n            throw new ApiException(ApiResultEnum.USER_NOT_EXIST);\n        }\n        if (!user.getPassword().equals(MDUtil.bcMD5(dto.getPassword()))) {\n            throw new ApiException(ApiResultEnum.USERNAME_OR_PASSWORD_IS_WRONG);\n        }\n        \/\/\u968f\u673a\u751f\u6210token\n        String token = Tools.getUUID();\n        redisTemplate.opsForValue().set(String.format(CacheKey.TOKEN_KEY_LOGIN,token), user, 1, TimeUnit.DAYS);\n        Map&lt;String,Object&gt; data = new HashMap&lt;&gt;();\n        data.put(Consts.TOKEN, token);\n        return Result.ok(data);\n    }\n\n    \/**\n     * \u751f\u6210Google \u5bc6\u94a5\n     * secret\uff1a\u5bc6\u94a5\n     * secretQrCode\uff1aGoogle Authenticator \u626b\u63cf\u6761\u5f62\u7801\u7684\u5185\u5bb9\n     * @param user\n     * @return\n     *\/\n    public Result generateGoogleSecret(User user) {\n        \/\/Google\u5bc6\u94a5\n        String randomSecretKey = GoogleAuthenticator.getRandomSecretKey();\n        String googleAuthenticatorBarCode = GoogleAuthenticator.getGoogleAuthenticatorBarCode(randomSecretKey, user.getUsername(), &quot;https:\/\/www.appblog.cn&quot;);\n        Map&lt;String,Object&gt; data = new HashMap&lt;&gt;();\n        \/\/Google\u5bc6\u94a5\n        data.put(&quot;secret&quot;, randomSecretKey);\n        \/\/\u7528\u6237\u4e8c\u7ef4\u7801\u5185\u5bb9\n        data.put(&quot;secretQrCode&quot;, googleAuthenticatorBarCode);\n        return Result.ok(data);\n    }\n\n    \/**\n     * \u7ed1\u5b9aGoogle\n     * @param dto\n     * @param user\n     * @return\n     *\/\n    public Result bindGoogle(GoogleDTO dto, User user, HttpServletRequest request) {\n        if(!StringUtils.isEmpty(user.getGoogleSecret())) {\n            throw new ApiException(ApiResultEnum.GOOGLE_IS_BIND);\n        }\n        boolean isTrue = GoogleAuthenticator.check_code(dto.getSecret(), dto.getCode(), System.currentTimeMillis());\n        if (!isTrue) {\n            throw new ApiException(ApiResultEnum.GOOGLE_CODE_NOT_MATCH);\n        }\n        User cacheUser = getUser(user.getUsername());\n        cacheUser.setGoogleSecret(dto.getSecret());\n        updateUser(cacheUser, request);\n        return Result.ok();\n    }\n\n    \/**\n     * Google\u767b\u5f55\n     * @param code\n     * @param user\n     * @return\n     *\/\n    public Result googleLogin(Long code, User user, HttpServletRequest request) {\n        if (StringUtils.isEmpty(user.getGoogleSecret())) {\n            throw new ApiException(ApiResultEnum.GOOGLE_NOT_BIND);\n        }\n        boolean isTrue = GoogleAuthenticator.check_code(user.getGoogleSecret(), code, System.currentTimeMillis());\n        if (!isTrue) {\n            throw new ApiException(ApiResultEnum.GOOGLE_CODE_NOT_MATCH);\n        }\n        redisTemplate.opsForValue().set(Tools.getTokenKey(request,CacheEnum.GOOGLE), Consts.SUCCESS, 1, TimeUnit.DAYS);\n        return Result.ok();\n    }\n}<\/code><\/pre>\n<h4>GoogleAuthenticator<\/h4>\n<ul>\n<li>Google\u8eab\u4efd\u9a8c\u8bc1\u5668\u5de5\u5177\u7c7b<\/li>\n<\/ul>\n<pre><code class=\"language-java\">import com.google.zxing.BarcodeFormat;\nimport com.google.zxing.MultiFormatWriter;\nimport com.google.zxing.WriterException;\nimport com.google.zxing.client.j2se.MatrixToImageWriter;\nimport com.google.zxing.common.BitMatrix;\nimport org.apache.commons.codec.binary.Base32;\nimport org.apache.commons.codec.binary.Hex;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\n\npublic class GoogleAuthenticator {\n    public static String getRandomSecretKey() {\n        SecureRandom random = new SecureRandom();\n        byte[] bytes = new byte[20];\n        random.nextBytes(bytes);\n        Base32 base32 = new Base32();\n        String secretKey = base32.encodeToString(bytes);\n        \/\/ make the secret key more human-readable by lower-casing and\n        \/\/ inserting spaces between each group of 4 characters\n        return secretKey.toUpperCase(); \/\/ .replaceAll(&quot;(.{4})(?=.{4})&quot;, &quot;$1 &quot;);\n    }\n\n    public static String getTOTPCode(String secretKey) {\n        String normalizedBase32Key = secretKey.replace(&quot; &quot;, &quot;&quot;).toUpperCase();\n        Base32 base32 = new Base32();\n        byte[] bytes = base32.decode(normalizedBase32Key);\n        String hexKey = Hex.encodeHexString(bytes);\n        long time = (System.currentTimeMillis() \/ 1000) \/ 30;\n        String hexTime = Long.toHexString(time);\n        return TOTP.generateTOTP(hexKey, hexTime, &quot;6&quot;);\n    }\n\n    public static String getGoogleAuthenticatorBarCode(String secretKey,\n            String account, String issuer) {\n        String normalizedBase32Key = secretKey.replace(&quot; &quot;, &quot;&quot;).toUpperCase();\n        try {\n            return &quot;otpauth:\/\/totp\/&quot;\n                    + URLEncoder.encode(issuer + &quot;:&quot; + account, &quot;UTF-8&quot;)\n                            .replace(&quot;+&quot;, &quot;%20&quot;)\n                    + &quot;?secret=&quot;\n                    + URLEncoder.encode(normalizedBase32Key, &quot;UTF-8&quot;).replace(\n                            &quot;+&quot;, &quot;%20&quot;) + &quot;&amp;issuer=&quot;\n                    + URLEncoder.encode(issuer, &quot;UTF-8&quot;).replace(&quot;+&quot;, &quot;%20&quot;);\n        } catch (UnsupportedEncodingException e) {\n            throw new IllegalStateException(e);\n        }\n    }\n\n    public static void createQRCode(String barCodeData, String filePath,\n            int height, int width) throws WriterException, IOException {\n        BitMatrix matrix = new MultiFormatWriter().encode(barCodeData,\n                BarcodeFormat.QR_CODE, width, height);\n        try (FileOutputStream out = new FileOutputStream(filePath)) {\n            MatrixToImageWriter.writeToStream(matrix, &quot;png&quot;, out);\n        }\n    }\n\n    static int window_size = 3; \/\/ default 3 - max 17 (from google docs)\u6700\u591a\u53ef\u504f\u79fb\u7684\u65f6\u95f4\n\n    \/**\n     * set the windows size. This is an integer value representing the number of\n     * 30 second windows we allow The bigger the window, the more tolerant of\n     * clock skew we are.\n     * \n     * @param s\n     *            window size - must be &gt;=1 and &lt;=17. Other values are ignored\n     *\/\n    public static void setWindowSize(int s) {\n        if (s &gt;= 1 &amp;&amp; s &lt;= 17)\n            window_size = s;\n    }\n\n    \/**\n     * Check the code entered by the user to see if it is valid\n     * \n     * @param secret\n     *            The users secret.\n     * @param code\n     *            The code displayed on the users device\n     * @param timeMsec\n     *            The time in msec (System.currentTimeMillis() for example)\n     * @return\n     *\/\n    public static boolean check_code(String secret, long code, long timeMsec) {\n        Base32 codec = new Base32();\n        byte[] decodedKey = codec.decode(secret);\n        \/\/ convert unix msec time into a 30 second &quot;window&quot;\n        \/\/ this is per the TOTP spec (see the RFC for details)\n        long t = (timeMsec \/ 1000L) \/ 30L;\n        \/\/ Window is used to check codes generated in the near past.\n        \/\/ You can use this value to tune how far you&#039;re willing to go.\n        for (int i = -window_size; i &lt;= window_size; ++i) {\n            long hash;\n            try {\n                hash = verify_code(decodedKey, t + i);\n            } catch (Exception e) {\n                \/\/ Yes, this is bad form - but\n                \/\/ the exceptions thrown would be rare and a static\n                \/\/ configuration problem\n                \/\/ e.printStackTrace();\n                throw new RuntimeException(e.getMessage());\n                \/\/ return false;\n            }\n            if (hash == code) {\n                return true;\n            }\n        }\n        \/\/ The validation code is invalid.\n        return false;\n    }\n\n    private static int verify_code(byte[] key, long t)\n            throws NoSuchAlgorithmException, InvalidKeyException {\n        byte[] data = new byte[8];\n        long value = t;\n        for (int i = 8; i-- &gt; 0; value &gt;&gt;&gt;= 8) {\n            data[i] = (byte) value;\n        }\n        SecretKeySpec signKey = new SecretKeySpec(key, &quot;HmacSHA1&quot;);\n        Mac mac = Mac.getInstance(&quot;HmacSHA1&quot;);\n        mac.init(signKey);\n        byte[] hash = mac.doFinal(data);\n        int offset = hash[20 - 1] &amp; 0xF;\n        \/\/ We&#039;re using a long because Java hasn&#039;t got unsigned int.\n        long truncatedHash = 0;\n        for (int i = 0; i &lt; 4; ++i) {\n            truncatedHash &lt;&lt;= 8;\n            \/\/ We are dealing with signed bytes:\n            \/\/ we just keep the first byte.\n            truncatedHash |= (hash[offset + i] &amp; 0xFF);\n        }\n        truncatedHash &amp;= 0x7FFFFFFF;\n        truncatedHash %= 1000000;\n        return (int) truncatedHash;\n    }\n}<\/code><\/pre>\n<p>\u4ee3\u7801\u5730\u5740<\/p>\n<ul>\n<li>Github: <a target=\"_blank\" rel=\"noopener\" href=\"https:\/\/github.com\/rstyro\/Springboot\/tree\/master\/SpringBoot-Google-Check\">https:\/\/github.com\/rstyro\/Springboot\/tree\/master\/SpringBoot-Google-Check<\/a><\/li>\n<li>Gitee: <a target=\"_blank\" rel=\"noopener\" href=\"https:\/\/gitee.com\/rstyro\/spring-boot\/tree\/master\/SpringBoot-Google-Check\">https:\/\/gitee.com\/rstyro\/spring-boot\/tree\/master\/SpringBoot-Google-Check<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>\u6982\u5ff5\uff1aGoogle\u8eab\u4efd\u9a8c\u8bc1\u5668Google Authenticator\u662f\u8c37\u6b4c\u63a8\u51fa\u7684\u57fa\u4e8e\u65f6\u95f4\u7684\u4e00\u6b21\u6027\u5bc6\u7801(Time [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[41],"tags":[239,368],"class_list":["post-1505","post","type-post","status-publish","format-standard","hentry","category-spring-boot","tag-google","tag-totp"],"_links":{"self":[{"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/posts\/1505","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/comments?post=1505"}],"version-history":[{"count":0,"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/posts\/1505\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/media?parent=1505"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/categories?post=1505"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.appblog.cn\/index.php\/wp-json\/wp\/v2\/tags?post=1505"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}