1. 程式人生 > 實用技巧 >許可權控制之驗證碼

許可權控制之驗證碼

SpringBoot整合Captcha驗證碼

目錄

1. 基本結構

使用Captcha生成驗證碼, 利用Redis儲存驗證碼

Redis中的結構為, Key是32位的UUID, Value為Captcha的4位隨機字母以及數字的集合

設定Redis過期時間為1min, 即可實現過期驗證碼的自動失效

2. 額外的依賴

基本的依賴這裡不再敘述, 主要說一下要匯入Captcha的依賴

<!--Kaptcha-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

所有的依賴如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wang</groupId>
    <artifactId>spring_security_framework</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring_security_framework</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--Redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--JDBC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--SpringSecurity-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--Thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--Validation-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!--SpringBoot Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <!--SpringSecurity with thymeleaf-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

        <!--MySQL connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--Test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--Druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.2</version>
        </dependency>

        <!--FastJSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.74</version>
        </dependency>

        <!--log4j-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!--Swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>

        <!--HuTool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.7</version>
        </dependency>

        <!--Kaptcha-->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3. 配置SpringBoot

配置SpringBoot的配置檔案, 這裡主要關注一個session的過期時間

#Port
server:
  port: 80
  servlet:
    session:
      timeout: 1

spring:
  application:
    name: SpringSecurityFramework

  #dataBase Setting
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Druid Setting
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 30000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      #Setting For Druid StatView and Filter
      filters: stat,wall,log4j
      max-pool-prepared-statement-per-connection-size: 20
      use-global-data-source-stat: true
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSql

  #Redis Setting
  redis:
    host: 127.0.0.1
    port: 6379

  #Thymeleaf
  thymeleaf:
    cache: false

#Mybatis
mybatis:
  type-aliases-package: com.wang.entity
  mapper-locations: classpath:Mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

其餘的配置, 如log4j, druid, SpringSecurity, RedisTemplate,這裡就不再贅述

4. 配置Captcha

我們可以通過JAVA的配置類來配置Captcha生成驗證碼的一些規則

package com.wang.spring_security_framework.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

//Kaptcha配置
@Configuration
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha producer() {
        //Properties類
        Properties properties = new Properties();
        // 圖片邊框
        properties.setProperty("kaptcha.border", "yes");
        // 邊框顏色
        properties.setProperty("kaptcha.border.color", "105,179,90");
        // 字型顏色
        properties.setProperty("kaptcha.textproducer.font.color", "blue");
        // 圖片寬
        properties.setProperty("kaptcha.image.width", "110");
        // 圖片高
        properties.setProperty("kaptcha.image.height", "40");
        // 字型大小
        properties.setProperty("kaptcha.textproducer.font.size", "30");
        // session key
        properties.setProperty("kaptcha.session.key", "code");
        // 驗證碼長度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        // 字型
        properties.setProperty("kaptcha.textproducer.font.names", "宋體,楷體,微軟雅黑");
        //圖片干擾
        properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.DefaultNoise");

        //Kaptcha 使用上述配置
        Config config = new Config(properties);

        //DefaultKaptcha物件使用上述配置, 並返回這個Bean
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

5. 工具類

使用UUID作為key, 同時考慮到對驗證碼的輸出結果可能有不同的要求, 這裡寫兩個工具類來處理它們

  • UUIDUtil
package com.wang.spring_security_framework.util;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class UUIDUtil {

    /**
     * 生成32位的隨機UUID
     * @return 字元形式的小寫UUID
     */
    @Bean
    public String getUUID32() {
        return UUID.randomUUID().toString()
                .replace("-", "").toLowerCase();
    }
}
  • CaptchaUtil
package com.wang.spring_security_framework.util;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.wang.spring_security_framework.service.CaptchaService;
import io.netty.handler.codec.base64.Base64Encoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

@Component
//Captcha 生成工具
public class CaptchaUtil {
    @Autowired
    private DefaultKaptcha producer;
    @Autowired
    private CaptchaService captchaService;

    //生成catchCreator的map
    public Map<String, Object> catchaImgCreator() throws IOException {
        //生成文字驗證碼
        String text = producer.createText();
        //生成文字對應的圖片驗證碼
        BufferedImage image = producer.createImage(text);

        //將圖片寫出
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image, "jpg", outputStream);

        //對寫出的位元組陣列進行Base64編碼 ==> 用於傳遞8位元位元組碼
        BASE64Encoder encoder = new BASE64Encoder();

        //生成token
        Map<String, Object> token = captchaService.createToken(text);
        token.put("img", encoder.encode(outputStream.toByteArray()));
        return token;
    }

}

6. 介面以及實現類

1. 介面

package com.wang.spring_security_framework.service;

import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Map;

public interface CaptchaService {
    //生成token
    Map<String, Object> createToken(String captcha);
    //生成captcha驗證碼
    Map<String, Object> captchaCreator() throws IOException;
    //驗證輸入的驗證碼是否正確
    String versifyCaptcha (String token, String inputCode);
}

2. 實現類

package com.wang.spring_security_framework.service.serviceImpl;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.wang.spring_security_framework.service.CaptchaService;
import com.wang.spring_security_framework.util.CaptchaUtil;
import com.wang.spring_security_framework.util.UUIDUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class CaptchaServiceImpl implements CaptchaService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private UUIDUtil uuidUtil;
    @Autowired
    private CaptchaUtil captchaUtil;

    //從SpringBoot的配置檔案中取出過期時間
    @Value("${server.servlet.session.timeout}")
    private Integer timeout;

    //UUID為key, 驗證碼為Value放在Redis中
    @Override
    public Map<String, Object> createToken(String captcha) {
        //生成一個token
        String key = uuidUtil.getUUID32();

        //生成驗證碼對應的token  以token為key  驗證碼為value存在redis中
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, captcha);
        //設定驗證碼過期時間
        redisTemplate.expire(key, timeout, TimeUnit.MINUTES);

        Map<String, Object> map = new HashMap<>();
        map.put("token", key);
        map.put("expire", timeout);
        return map;
    }

    //生成captcha驗證碼
    @Override
    public Map<String, Object> captchaCreator() throws IOException {
        return captchaUtil.catchaImgCreator();
    }

    //驗證輸入的驗證碼是否正確
    @Override
    public String versifyCaptcha(String token, String inputCode) {
        //根據前端傳回的token在redis中找對應的value
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        if (redisTemplate.hasKey(token)) {
            //驗證通過, 刪除對應的key
            if (valueOperations.get(token).equals(inputCode)) {
                redisTemplate.delete(token);
                return "true";
            } else {
                return "false";
            }
        } else {
            return "false";
        }
    }
}
  • 這裡的驗證, 只是簡單的驗證了輸入是否能從Redis中匹配, 返回了字串
  • 真實的驗證中, 我們還要再邏輯中新增使用者名稱和密碼的考慮

7. Controller

package com.wang.spring_security_framework.controller;

import com.wang.spring_security_framework.service.CaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.Map;

@RestController
public class LoginController {
    @Autowired
    CaptchaService captchaService;

    @GetMapping("/captcha")
    public Map<String, Object> captcha() throws IOException {
        return captchaService.captchaCreator();
    }

    @GetMapping("/login1")
    public String login(@RequestParam("token") String token,
                              @RequestParam("inputCode") String inputCode) {
        return captchaService.versifyCaptcha(token, inputCode);
    }
}
  • captcha 用於獲取一個驗證碼
  • login1 用於接收到前端的請求後驗證並返回結果
  • login1 這裡為了測試簡便實用了GET方法, 而實際中最好使用POST方法, 這樣安全性更高

8. 前端頁面的實現

前端結構如圖, 實現了一個簡單的驗證碼

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登入</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
</head>
<body>

<div>
    <div>
        <form th:action="@{/login1}" method="get">
            <input type="text" id="userName" placeholder="請輸入使用者名稱" name="userName">
            <br>
            <input type="password" id="password" placeholder="請輸入密碼" name="password">
            <br>
            <input type="text" id="inputCode" placeholder="請輸入驗證碼" maxlength="4" name="inputCode">
            <!--通過隱藏域傳遞值, 在下面的驗證碼點選事件中, 將值繫結過來, 這樣就可以獲得最新的驗證碼對應的值了!-->
            <input id="token" value="" type="hidden" name="token">
            <input type="submit" value="登入">
        </form>

    </div>
    <div>
        <!-- 當用戶連結時,void(0)計算為0,使用者點選不會發生任何效果 -->
        <a href="javascript:void(0);" title="點選更換驗證碼">
            <!--this引數, 返回當前的DOM元素-->
            <img src="" alt="更換驗證碼" id="imgVerify" onclick="getVerify(this)">
        </a>
    </div>
</div>

<script>
    //獲得img物件
    let imgVerify = $("#imgVerify").get(0);
    //$(function())等同於$(document).ready(function()) ==> 頁面載入完畢之後, 才執行函式
    $(function () {
        getVerify(imgVerify);
    });
    //onclick時間繫結的getVerify函式
    function getVerify(obj) {
        $.ajax({
            type: "GET",
            url: "/captcha",
            success: function (result) {
                obj.src = "data:image/jpeg;base64," + result.img;
                $("#token").val(result.token);
            }
        });
    }
</script>

</body>
</html>
  • 用一個 a 標籤包圍 img 標籤, 這樣如果圖片沒有加載出來也有一個超連結, 不過點了以後沒有效果
  • \((function())等同於\)(document).ready(function()) ==> 頁面載入完畢之後, 才執行函式, 這裡必須要寫這個函式, 否則第一次載入不會呼叫 onclick 方法, 也就不會生成驗證碼!
  • 我們利用隱藏域將驗證碼的key傳遞到表單中, 我們在 img 的點選事件對應的函式的ajax回撥函式中可以利用jQuery操作DOM, 順帶取出key值放到我們的隱藏域中, 這樣提交的時候就會提交 key 和使用者輸入的 value 了