1. 项目说明

当前这篇教程是:
1. 抽取公共模块common,集成redis,虽然只有几个工具类和redis
2. 新建Gateway网关,集成Security,做登陆和资源权限控制
3. 前端登陆,做了2种方式。用户、密码、验证码;邮箱、验证码、图片滑块;并且前端加密传给后端解密;登陆异常次数限制
4. 在分布式系统中基于Token的身份验证
5. 每次请求刷新用户会话有效时间
6. 通过AOP方式动态切换数据源

简单创建一个SpringCloud2021.0.3项目(一)
简单创建一个SpringCloud2021.0.3项目(二)
简单创建一个SpringCloud2021.0.3项目(三)
简单创建一个SpringCloud2021.0.3项目(四)

1. 版本

  1. SpringCloud版本为2021.0.3
  2. SpringBoot版本为2.7.2

2. 用到组件

  1. 注册中心:暂时用Eureka,后面再改成Nacos
  2. 网关:Gateway
  3. 权限:Security,Gateway集成
  4. 负载均衡:LoadBalancer,SpringCloud2020版之后就集成LoadBalancer
  5. 限流、熔断降级:Sentinel
  6. 配置中心:暂时用Config,后面改成Nacos
  7. 服务间访问:Feign

3. 功能

  1. 项目最基本功能,权限控制,在分布式系统中基于Token的身份验证。
  2. 前端登陆,做了2种方式。用户、密码、验证码;邮箱、验证码、图片滑块;并且前端加密传给后端解密;登陆异常次数限制;
  3. 限流、负载均衡,应对高并发情况,降低系统负载;
  4. 服务熔断降级:避免系统雪崩,提高系统可用性;
  5. 两种方式的多数据源,一种是通过AOP方式动态切换数据源,另一种是不同数据源管理的数据各不相同;
  6. 日志系统Logback,是SpringBoot默认集成

2. 上一篇教程

简单创建一个SpringCloud2021.0.3项目(一)

  1. 新建Eureka注册中心
  2. 新建Config配置中心,producerService服务读取参数
  3. 2个业务服务(producerService和webService),webService通过Feign调用producerService的服务
  4. webService用到多数据源,不同的数据源管理不同的数据

3. 创建公共模块Common

  1. 创建操作
    image

  2. 修改pom.xml文件
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    image

点击查看代码
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>SpringCloud202208</artifactId>
        <groupId>com.xiaostudy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>common</artifactId>

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

        <!-- JSON 解析器和生成器 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.12</version>
        </dependency>
    </dependencies>

</project>
  1. redis序列化、配置类、工具类
    image
序列化
package com.xiaostudy.common.redis;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Redis使用FastJson序列化
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {

    private Class<T> clazz;


    public FastJson2JsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, StandardCharsets.UTF_8);

        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
    }
}
配置类
package com.xiaostudy.common.redis;

import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 */
@Configuration
// 启动redis
@EnableCaching
// RedisConfig在RedisAutoConfiguration之前加载
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    @SuppressWarnings(value = {"unchecked" , "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}
工具类
package com.xiaostudy.common.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * spring redis 工具类
 **/
@SuppressWarnings(value = {"unchecked" , "rawtypes"})
@Component
public class RedisService {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection) {
        return redisTemplate.delete(collection) > 0;
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 删除Hash中的某条数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey) {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}
  1. 字符串工具类
    image
点击查看代码
package com.xiaostudy.common.utils;


import java.util.Arrays;
import java.util.List;

public class StringUtils {

    public static final String BLANK = " ";
    public static final String EMPTY = "";
    public static final String DEFAULT_LOGOUT_SUCCESS_URL = "/web/webLogin/isLogout";
    public static final String DEFAULT_LOGIN_URL_1 = "/web/webLogin/form";
    public static final String DEFAULT_LOGIN_MAIL_URL_1 = "/web/webLogin/emailLogin";
    public static final String DEFAULT_LOGOUT_URL_1 = "/web/webLogin/logout";
    public static final String DEFAULT_REGISTER_URL_1 = "/web/webLogin/register";
    public static final String DEFAULT_REGISTER_HTML_1 = "/web/register.html";
    public static final String DEFAULT_LOGOUT_HTML_1 = "/web/login.html";
    public static final String DEFAULT_LOGIN_MAIL_HTML = "/web/loginMail.html";
    public static final String DEFAULT_INDEX_HTML_1 = "/web/index.html";
    public static final String COMMA = ",";
    public static final String EMAIL = "email";
    public static final String WILDCARD = "**";
    public static final String[] REQUEST_RUL_WHITE_S = {
            DEFAULT_LOGOUT_HTML_1
            , "/web/webLogin/login"
            , DEFAULT_LOGOUT_SUCCESS_URL
            , DEFAULT_LOGIN_MAIL_HTML
            , DEFAULT_REGISTER_URL_1
            , DEFAULT_REGISTER_HTML_1
            , DEFAULT_LOGIN_URL_1
            , "/security/verifyCode"
            , "/security/sendMailVerifyCode"
            , "/security/sendPhoneVerifyCode"
            , "/web/webLogin/test"
            , "/web/webLogin/test2"
            , "/web/img/**"
    };
    public static final List<String> REQUEST_RUL_WHITE_LIST = Arrays.asList(REQUEST_RUL_WHITE_S);
    public static final List<String> REQUEST_IP_WHITE_LIST = Arrays.asList(
            "192.168.1.6"
            , "192.168.1.2"
            , "127.0.0.1"
    );
}
  1. 验证码工具类
点击查看代码
package com.xiaostudy.common.utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class VerifyCodeUtils {

    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "1234567890ABCDEFGHIJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();

    private VerifyCodeUtils() {
    }

    public static final String EMAIL_REGEX = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";

    public static boolean isEMail(String email) {
        Pattern regex = Pattern.compile(EMAIL_REGEX);
        Matcher matcher = regex.matcher(email);
        return matcher.matches();
    }


    /**
     * 使用系统默认字符源生成验证码
     */
    public static String generateVerifyCode(int verifySize) {
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }

    /**
     * 使用指定源生成验证码
     */
    public static String generateVerifyCode(int verifySize, String sources) {
        if (sources == null || sources.length() == 0) {
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for (int i = 0; i < verifySize; i++) {
            verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
        }
        return verifyCode.toString();
    }

    /**
     * 生成随机验证码文件,并返回验证码值
     */
    public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, outputFile, verifyCode);
        return verifyCode;
    }

    /**
     * 输出随机验证码图片流,并返回验证码值
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }

    /**
     * 生成指定验证码图像文件
     */
    public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
        if (outputFile == null) {
            return;
        }
        File dir = outputFile.getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        outputFile.createNewFile();
        FileOutputStream fos = new FileOutputStream(outputFile);
        outputImage(w, h, fos, code);
        fos.close();
    }

    /**
     * 输出指定验证码图片流
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW};
        float[] fractions = new float[colors.length];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h - 4);

        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使图片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h - 4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for (int i = 0; i < verifySize; i++) {
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(40) + 10;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }

        }

    }

}

image

  1. 解密工具类
点击查看代码
package com.xiaostudy.common.utils;

import javax.crypto.*;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

public class DESUtils {
    /**
     * 用户名称密码加密,密钥
     */
    private static final String SECRET_KEY = "mwPZ7ISbC!ox6@7cP*^…5@%$)2*V";
    // 向量
    private static final String IV = "mwPZ7C!n";
    // 加解密统一使用的编码方式
    private static final Charset encoding = StandardCharsets.UTF_8;

    /**
     * 3DES解密
     *
     * @param encryptText 加密文本
     * @return
     * @throws Exception
     */
    public static String decode(String encryptText) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
        DESedeKeySpec spec = new DESedeKeySpec(SECRET_KEY.getBytes());
        SecretKeyFactory keyfactory = SecretKeyFactory.getInstance("desede");
        Key deskey = keyfactory.generateSecret(spec);
        Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
        IvParameterSpec ips = new IvParameterSpec(IV.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, deskey, ips);
        byte[] decryptData = cipher.doFinal(hexToBytes(encryptText));
        return new String(decryptData, encoding);
    }

    public static byte[] hexToBytes(String hex) {
        hex = hex.length() % 2 != 0 ? "0" + hex : hex;

        byte[] b = new byte[hex.length() / 2];
        for (int i = 0; i < b.length; i++) {
            int index = i * 2;
            int v = Integer.parseInt(hex.substring(index, index + 2), 16);
            b[i] = (byte) v;
        }
        return b;
    }
}

4. 网关Gateway

1. 创建Security

  1. 创建操作
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    image

  2. 父模块添加子模块
    image

<module>security</module>
  1. 修改pom.xml
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    image
点击查看代码
<?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>com.xiaostudy</groupId>
        <artifactId>SpringCloud202208</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <groupId>com.xiaostudy</groupId>
    <artifactId>security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security</name>
    <description>security</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--集成响应式web框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <!-- druid数据源驱动 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.1</version>
        </dependency>

        <!--AOP-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <!--动态切换数据源用到-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <!-- JWT Token验证机制 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.13.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!--邮箱依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <!--SpringBoot中集成了jasypt在一定程度上保证密码的安全-->
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>com.xiaostudy</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <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>
    </dependencies>

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

</project>
  1. 修改配置文件application.properties
    image
点击查看代码
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driverClassName: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8
      username: root
      password: 密码
    druid2:
      driverClassName: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=utf8
      username: root
      password: 密码
  mail:
    default-encoding: UTF-8
    # 阿里云发送服务器地址
    host: smtp.mxhichina.com
    #    port: 25                      #端口号
    # 发送人地址
    username: liwei@xiaostudy.com
    # 密码
    password: ENC(密码加密后的字符串)
    properties:
      mail:
        smtp:
          starttls:
            enable: true
            required: true
          auth: true
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory
            port: 465
jasypt:
  encryptor:
    password: 密钥
mybatis:
  configuration:
    # 下划线转驼峰
    map-underscore-to-camel-case: true
  # 注册映射文件
  mapper-locations: mapper/*Mapper.xml
  # 注册实体类别名
  type-aliases-package: com.xiaostudy.security.entity

session:
  # session过期时间,单位秒
  timeout: 1800
#  timeout: 30
  1. 查询用户的实体类、service、mapper
用户实体类
package com.xiaostudy.security.entity;

public class UserEentity {

    private String username;
    private String password;
    private String role;
    private Integer errorCount;
    private String url;
    private String email;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public Integer getErrorCount() {
        return errorCount;
    }

    public void setErrorCount(Integer errorCount) {
        this.errorCount = errorCount;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
Mapper接口
package com.xiaostudy.security.mapper;

import com.xiaostudy.security.entity.UserEentity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface UserMapper {

    public List<UserEentity> selectUserAll();
    public UserEentity selectUserByName(@Param("name") String username);
    public UserEentity selectUserByEmail(@Param("email") String email);
    public UserEentity selectUserByPhone(@Param("phone") String phone);

    public int loginPasswordErrorAdd(@Param("name")String username);
    public int loginPasswordErrorClean(@Param("name")String username);
}
Mapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaostudy.security.mapper.UserMapper">

    <select id="selectUserAll" resultType="com.xiaostudy.security.entity.UserEentity">
        SELECT username, password, role, error_count FROM `user`
    </select>

    <select id="selectUserByName" resultType="com.xiaostudy.security.entity.UserEentity">
        SELECT username, password, role, error_count, url, email FROM `user` where username = #{name}
    </select>

    <select id="selectUserByEmail" resultType="com.xiaostudy.security.entity.UserEentity">
        SELECT username, password, role, error_count, url, email FROM `user` where email = #{email}
    </select>

    <select id="selectUserByPhone" resultType="com.xiaostudy.security.entity.UserEentity">
        SELECT username, password, role, error_count, url, email, phone FROM `user` where phone = #{phone}
    </select>

    <update id="loginPasswordErrorAdd" parameterType="java.lang.String">
        update `user` set error_count = error_count + 1 where username = #{name}
    </update>

    <update id="loginPasswordErrorClean" parameterType="java.lang.String">
        update `user` set error_count = 0 where username = #{name}
    </update>

</mapper>
service
package com.xiaostudy.security.service;


import com.xiaostudy.security.entity.UserEentity;

import java.util.List;

public interface UserService {

    public List<UserEentity> selectUserAll();
    public UserEentity selectUserByNameDb1(String username);
    public UserEentity selectUserByEmailDb1(String email);
    public UserEentity selectUserByPhoneDb1(String phone);
    public UserEentity selectUserByNameDb2(String username);

    public boolean loginPasswordErrorAdd(String username);
    public boolean loginPasswordErrorClean(String username);
}
service实现类
package com.xiaostudy.security.service.impl;

import com.xiaostudy.security.datasources.annotation.DataSource;
import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
import com.xiaostudy.security.entity.UserEentity;
import com.xiaostudy.security.mapper.UserMapper;
import com.xiaostudy.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public List<UserEentity> selectUserAll() {
        return userMapper.selectUserAll();
    }

    @DataSource(name = DataSourceNameEnum.FIRST)
    @Override
    public UserEentity selectUserByNameDb1(String username) {
        return userMapper.selectUserByName(username);
    }

    @DataSource(name = DataSourceNameEnum.FIRST)
    @Override
    public UserEentity selectUserByEmailDb1(String email) {
        return userMapper.selectUserByEmail(email);
    }
    @DataSource(name = DataSourceNameEnum.FIRST)
    @Override
    public UserEentity selectUserByPhoneDb1(String phone) {
        return userMapper.selectUserByPhone(phone);
    }

    @DataSource(name = DataSourceNameEnum.SECOND)
    @Override
    public UserEentity selectUserByNameDb2(String username) {
        return userMapper.selectUserByName(username);
    }

    @DataSource(name = DataSourceNameEnum.FIRST)
    @Override
    public boolean loginPasswordErrorAdd(String username) {
        int i = userMapper.loginPasswordErrorAdd(username);
        return 0 != i;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean loginPasswordErrorClean(String username) {
        int i = userMapper.loginPasswordErrorClean(username);
        return 0 != i;
    }
}
多数据源枚举
package com.xiaostudy.security.datasources.enums;

/**
 * 多数据源配置数据源枚举
 */
public enum DataSourceNameEnum {
    FIRST("first")
    ,SECOND("second");

    private String name;

    DataSourceNameEnum(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
动态数据源路由
package com.xiaostudy.security.datasources;

import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

/**
 * 动态数据源路由
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<DataSourceNameEnum> contextHolder = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return this.getDataSource();
    }

    public static void setDataSource(DataSourceNameEnum dataSource) {
        contextHolder.set(dataSource);
    }

    public static DataSourceNameEnum getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }

}
多数据源注解
package com.xiaostudy.security.datasources.annotation;

import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;

import java.lang.annotation.*;

/**
 * 多数据源注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    DataSourceNameEnum name();
}
多数据源AOP类
package com.xiaostudy.security.datasources.aop;

import com.xiaostudy.security.datasources.DynamicDataSource;
import com.xiaostudy.security.datasources.annotation.DataSource;
import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 多数据源,切面处理类
 */
@Aspect
@Component
public class DataSourceAspect implements Ordered {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 针对上面注解做切面拦截
     */
    @Pointcut("@annotation(com.xiaostudy.security.datasources.annotation.DataSource)")
//    @Pointcut("execution(* com.xiaostudy.security.datasources..*.*(..))")
    public void dataSourcePointCut() {}

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if(dataSource == null){
            //如果没有注解,使用默认数据源
            DynamicDataSource.setDataSource(DataSourceNameEnum.FIRST);
        }else {
            //根据注解中设置的数据源名称,选择对应的数据源
            DynamicDataSource.setDataSource(dataSource.name());
            LOGGER.info("set datasource is " + dataSource.name().getName());
        }

        try {
            return point.proceed();
        } finally {
            //清除数据源配置
            DynamicDataSource.clearDataSource();
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }
}
多数据配置类
package com.xiaostudy.security.datasources.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.xiaostudy.security.datasources.DynamicDataSource;
import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 多数据源配置类
 */
@Configuration
public class DynamicDataSourceConfig {

    //如果ioc容器中,同一个类型有多个bean,则bean的名称为方法的名称
    @Bean("firstDataSource")
    @ConfigurationProperties("spring.datasource.druid")
    public DataSource firstDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean("secondDataSource")
    @ConfigurationProperties("spring.datasource.druid2")
    public DataSource secondDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceNameEnum.FIRST, firstDataSource);
        targetDataSources.put(DataSourceNameEnum.SECOND, secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }
}

image

2. Security登陆配置

  1. 配置密码加密、解析器
点击查看代码
package com.xiaostudy.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class BeanConfig {

    //配置密码加密、解析器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  1. IP工具类
点击查看代码
package com.xiaostudy.security.utils;

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

public final class IpUtils {

    private IpUtils() {
    }

    public static final String UNKNOWN = "unknown";
    public static final String LOCAL_IPV6 = "0:0:0:0:0:0:0:1";
    public static final String LOCAL_IPV4 = "127.0.0.1";

    public static String getIpAddr(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip) && ip.indexOf(",") != -1) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            ip = ip.split(",")[0];
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return LOCAL_IPV6.equals(ip) ? LOCAL_IPV4 : ip;
    }
}
  1. Token工具类
点击查看代码
package com.xiaostudy.security.utils;

import com.xiaostudy.common.utils.StringUtils;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.util.ObjectUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;

public class JwtTokenUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtils.class);
    // 有效时间,单位毫秒
//    public static final long EXPIRATION = 30 * 1000L;
    public static final long EXPIRATION = 40 * 60 * 1000L;
    public static final long TOKEN_REFRESH_DATE = 15 * 1000L;
    //    public static final long TOKEN_REFRESH_DATE = 20 * 60 * 1000L;
    public static final String TOKEN_REFRESH_DATE_STR = "TOKEN_REFRESH_DATE";
    //JWT密钥
    public static final String SECRET = "123654";

    public static final String BASIC_EMPTY = "Basic ";
    public static final String BASIC_EMPTY_ = "Basic%20";
    public static final String AUTHENTICATION = "Authorization";
    public static final String COOKIE_AUTHENTICATION_BASIC_EMPTY_ = "Authorization=Basic%20";
    public static final String COOKIE_SPLIT = ";";
    public static final String COOKIE = "Cookie";
    public static final String TOKEN_CREATED = "created";
    public static final String TOKEN_REFRESH_FLAG = "RefreshTokenFlag";
    public static final String TOKEN_REFRESH_YES = "1";
    public static final String TOKEN_REFRESH_NO = "0";

    public static final String VERIFY_CODE = "verifyCode";
    public static final String COOKIE_VERIFY_CODE = "verifyCode=";
    public static final String USER_NAME = "userName";
    public static final String PASS_WORD = "passWord";

    public static final int LOGIN_ERROR_COUNT = 5;

    /**
     * 生成token令牌
     *
     * @param username 用户
     * @param payloads 令牌中携带的附加信息
     * @return 令token牌
     */
    public static String generateToken(String username, Map<String, Object> payloads) {
        int payloadSizes = payloads == null ? 0 : payloads.size();

        Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
        claims.put(Claims.SUBJECT, username);
        claims.put(TOKEN_CREATED, new Date());

        if (payloadSizes > 0) {
            claims.putAll(payloads);
        }

        return generateToken(claims);
    }

    /**
     * 从claims生成令牌,如果看不懂就看谁调用它
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private static String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION);
        // 刷新token时间
        claims.put(JwtTokenUtils.TOKEN_REFRESH_DATE_STR, new Date(System.currentTimeMillis() + TOKEN_REFRESH_DATE));
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public static boolean isTokenExpired(String token) {
        if (ObjectUtils.isEmpty(token)) {
            return false;
        }
        try {
            Claims claims = getClaimsFromToken(token);
            if (ObjectUtils.isEmpty(claims)) {
                return false;
            }
            Date expiration = claims.getExpiration();
            return new Date().before(expiration);
        } catch (Exception e) {
            LOGGER.error("判断令牌是否过期异常", e);
            return false;
        }
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public static String getUsernameFromToken(String token) {
        if (ObjectUtils.isEmpty(token)) {
            return null;
        }
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            LOGGER.error("从令牌中获取用户名异常1", e);
            username = null;
        }
        return username;
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public static String refreshToken(String token) {
        if (ObjectUtils.isEmpty(token)) {
            return null;
        }
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(TOKEN_CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            LOGGER.error("刷新令牌异常", e);
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 从令牌中获取数据声明,如果看不懂就看谁调用它
     *
     * @param token 令牌
     * @return 数据声明
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            JwtParser jwtParser = Jwts.parser().setSigningKey(SECRET);
            Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
            claims = claimsJws.getBody();
        } catch (Exception e) {
            LOGGER.error("从令牌中获取数据声明异常");
//            LOGGER.error("从令牌中获取数据声明异常", e);
            claims = null;
        }
        return claims;
    }

    public static String getCookieUsername(HttpHeaders headers) {
        String authentication = getCookieAuthentication(headers);
        if (ObjectUtils.isEmpty(authentication)) {
            return null;
        }
        return getUsernameFromToken(authentication);
    }

    public static String getCookieAuthentication(HttpHeaders headers) {
        String authentication = headers.getFirst(JwtTokenUtils.AUTHENTICATION);
        if (ObjectUtils.isEmpty(authentication)) {
            String cookieStr = headers.getFirst(JwtTokenUtils.COOKIE);
            if (!ObjectUtils.isEmpty(cookieStr)) {
                cookieStr = cookieStr.replaceAll(StringUtils.BLANK, StringUtils.EMPTY);
                String[] cookies = cookieStr.split(JwtTokenUtils.COOKIE_SPLIT);
                for (String c : cookies) {
                    if (!ObjectUtils.isEmpty(c) && c.startsWith(JwtTokenUtils.COOKIE_AUTHENTICATION_BASIC_EMPTY_)) {
                        authentication = c.replaceFirst(JwtTokenUtils.COOKIE_AUTHENTICATION_BASIC_EMPTY_, StringUtils.EMPTY);
                        break;
                    }
                }
            }
        }
        if (!ObjectUtils.isEmpty(authentication)) {
            if (authentication.startsWith(JwtTokenUtils.BASIC_EMPTY_)) {
                authentication = authentication.replaceFirst(JwtTokenUtils.BASIC_EMPTY_, StringUtils.EMPTY);
            } else if (authentication.startsWith(JwtTokenUtils.BASIC_EMPTY)) {
                authentication = authentication.replaceFirst(JwtTokenUtils.BASIC_EMPTY, StringUtils.EMPTY);
            }
        }
        return authentication;
    }

    public static String getCookieVerifyCode(HttpHeaders headers) {
        String cookieStr = headers.getFirst(JwtTokenUtils.COOKIE);
        if (ObjectUtils.isEmpty(cookieStr)) {
            return null;
        }
        cookieStr = cookieStr.replaceAll(StringUtils.BLANK, StringUtils.EMPTY);
        String[] cookies = cookieStr.split(JwtTokenUtils.COOKIE_SPLIT);
        for (String c : cookies) {
            if (!ObjectUtils.isEmpty(c) && c.startsWith(JwtTokenUtils.COOKIE_VERIFY_CODE)) {
                return c.replaceFirst(JwtTokenUtils.COOKIE_VERIFY_CODE, StringUtils.EMPTY);
            }
        }
        return null;
    }

    private static final Map<String, Date> LOG_TOKEN_DATE_MAP = new ConcurrentHashMap<>();
    private static final ExecutorService POOL = java.util.concurrent.Executors.newFixedThreadPool(2);

    public static boolean checkTokenAndRefreshToken(HttpHeaders headers, String authentication) {
        boolean tokenExpired = false;
        Date now = new Date();
        if (ObjectUtils.isEmpty(authentication)) {
            return tokenExpired;
        }
        try {
            Claims claims = getClaimsFromToken(authentication);
            if (ObjectUtils.isEmpty(claims)) {
                return tokenExpired;
            }
            Date expiration = claims.getExpiration();
            if (now.after(expiration)) {
                return tokenExpired;
            }
            Date expirationTokenRefresh = claims.get(TOKEN_REFRESH_DATE_STR, Date.class);
            tokenExpired = now.before(expirationTokenRefresh);
        } catch (Exception e) {
            LOGGER.error("判断令牌是否过期异常", e);
            return false;
        }

        headers.set(TOKEN_REFRESH_FLAG, TOKEN_REFRESH_NO);
        if (tokenExpired) {
            // token有效
            POOL.execute(new LogTokenRunnable(LOG_TOKEN_DATE_MAP, authentication, now));
            return tokenExpired;
        } else {
            Date date = LOG_TOKEN_DATE_MAP.get(authentication);
            if (ObjectUtils.isEmpty(date)) {
                return tokenExpired;
            }
            Date expirationDate = new Date(date.getTime() + EXPIRATION);
            if (expirationDate.before(now)) {
                return tokenExpired;
            }
            String refreshToken = refreshToken(authentication);
            if (ObjectUtils.isEmpty(refreshToken)) {
                return tokenExpired;
            }
            headers.set(TOKEN_REFRESH_FLAG, TOKEN_REFRESH_YES);
            headers.set(AUTHENTICATION, BASIC_EMPTY + refreshToken);
            return true;
        }
    }

    // 记录token最后请求时间
    private static class LogTokenRunnable implements Runnable {
        private Map<String, Date> map;
        private String token;
        private Date now;

        LogTokenRunnable(Map<String, Date> map, String token, Date now) {
            this.map = map;
            this.token = token;
            this.now = now;
        }

        @Override
        public void run() {
            if (null == map || ObjectUtils.isEmpty(token) || ObjectUtils.isEmpty(now)) {
                return;
            }
            Date date = map.get(token);
            if (ObjectUtils.isEmpty(date) || now.after(date)) {
                map.put(token, now);
            }
        }
    }
}
  1. 自定义UsernamePasswordAuthenticationToken
点击查看代码
package com.xiaostudy.security.entity;

import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.utils.JwtTokenUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.util.MultiValueMap;

public class MyUserDetails extends UsernamePasswordAuthenticationToken {
    private static final long serialVersionUID = 1L;
    private String username;
    private String password;
    private String verifyCode;
    private String ipAddr;
    private String email;

    public MyUserDetails(String username, String password, String verifyCode, String ipAddr, String email) {
        super(username, password);
        this.username = username;
        this.password = password;
        this.verifyCode = verifyCode;
        this.ipAddr = ipAddr;
        this.email = email;
    }

    public static MyUserDetails unauthenticated(String username, String password, String verifyCode, String ipAddr, String email) {
        return new MyUserDetails(username, password, verifyCode, ipAddr, email);
    }

    public static MyUserDetails unauthenticated(String username, String password) {
        return new MyUserDetails(username, password, null, null, null);
    }

    public static MyUserDetails createAuthentication(MultiValueMap<String, String> data, String ipAddr) {
        String username = data.getFirst(JwtTokenUtils.USER_NAME);
        String password = data.getFirst(JwtTokenUtils.PASS_WORD);
        String verifyCode = data.getFirst(JwtTokenUtils.VERIFY_CODE);
        String email = data.getFirst(StringUtils.EMAIL);
        return MyUserDetails.unauthenticated(username, password, verifyCode, ipAddr, email);
    }


    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getVerifyCode() {
        return verifyCode;
    }

    public void setVerifyCode(String verifyCode) {
        this.verifyCode = verifyCode;
    }

    public String getIpAddr() {
        return ipAddr;
    }

    public void setIpAddr(String ipAddr) {
        this.ipAddr = ipAddr;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

}
  1. 自定义ServerFormLoginAuthenticationConverter,从表单获取参数转成自定义UsernamePasswordAuthenticationToken类
点击查看代码
package com.xiaostudy.security.config;

import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.entity.MyUserDetails;
import com.xiaostudy.security.utils.IpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

// 请求认证过滤器,从表单获取参数,不用security的默认参数名username、password
@Configuration
public class MyServerFormLoginAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyServerFormLoginAuthenticationConverter.class);

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        LOGGER.info("请求认证过滤器----MyServerFormLoginAuthenticationConverter.........");
        String uri = exchange.getRequest().getURI().getPath();
        if (StringUtils.DEFAULT_LOGIN_URL_1.equals(uri)) {  //登录操作才对body做特殊操作,其他请求直接调用原有请求
            return this.apply(exchange);
        } else { //非登录操作,基本不用在网关里读取body,默认方法就行
            return super.convert(exchange);
        }
    }

    @Override
    public Mono<Authentication> apply(ServerWebExchange exchange) {
        final String ipAddr = IpUtils.getIpAddr(exchange.getRequest());
        return exchange.getFormData().map((data) -> MyUserDetails.createAuthentication(data, ipAddr));
    }

}
  1. 自定义登陆处理
点击查看代码
package com.xiaostudy.security.config;

import com.xiaostudy.common.redis.RedisService;
import com.xiaostudy.common.utils.DESUtils;
import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.entity.MyUserDetails;
import com.xiaostudy.security.entity.UserEentity;
import com.xiaostudy.security.service.UserService;
import com.xiaostudy.security.utils.JwtTokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Mono;

// 自定义处理登陆
@Configuration
public class MyReactiveAuthenticationManager implements ReactiveAuthenticationManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyReactiveAuthenticationManager.class);

    /**
     * @see BeanConfig#passwordEncoder()
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserService userService;

    @Autowired
    private RedisService redisService;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        LOGGER.info("自定义处理登陆----MyReactiveAuthenticationManager.........");
        //获取输入的用户名
        String username = authentication.getName();
        //获取输入的明文
        String rawPassword = (String) authentication.getCredentials();
        MyUserDetails myUserDetails = null;
        String verifyCode = null;
        String ipAddr = null;
        if (authentication instanceof MyUserDetails) {
            myUserDetails = (MyUserDetails) authentication;
            username = myUserDetails.getUsername();
            rawPassword = myUserDetails.getPassword();
            verifyCode = myUserDetails.getVerifyCode();
            ipAddr = myUserDetails.getIpAddr();
            String email = myUserDetails.getEmail();
            if (!ObjectUtils.isEmpty(email)) {
                return this.authenticateEmail(email, verifyCode, ipAddr);
            }
        } else if (authentication instanceof UsernamePasswordAuthenticationToken) {
            UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;
            // TODO 不是的话要处理
            username = (String) authenticationToken.getPrincipal();
            rawPassword = (String) authenticationToken.getCredentials();
            myUserDetails = MyUserDetails.unauthenticated(username, rawPassword);
        }
        if (null != ipAddr) {
            if (ObjectUtils.isEmpty(verifyCode)) {
                return Mono.error(new DisabledException("请填写验证码!"));
            }
            String s = redisService.getCacheObject(ipAddr);
            if (ObjectUtils.isEmpty(s)) {
                return Mono.error(new DisabledException("验证码过期,请重新获取验证码!"));
            }
            if (!s.equals(verifyCode) && !s.equals(verifyCode.toLowerCase())) {
                return Mono.error(new DisabledException("验证码有误,请重新输入!"));
            }
        }

        try {
            if (!ObjectUtils.isEmpty(username)) {
                username = DESUtils.decode(username);
            }
            if (!ObjectUtils.isEmpty(rawPassword)) {
                rawPassword = DESUtils.decode(rawPassword);
            }
        } catch (Exception e) {
            LOGGER.error("解密用户密码出错!");
            return Mono.error(new DisabledException("解密用户密码出错!"));
        }

        if ((ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(rawPassword))) {
            return Mono.error(new DisabledException("请填写用户名或密码"));
        }

        UserDetails user = null;
        UserEentity userEentity = null;
        try {
            userEentity = userService.selectUserByNameDb1(username);
            if (ObjectUtils.isEmpty(userEentity)) {
                return Mono.error(new UsernameNotFoundException("系统无此用户,请先注册!"));
            }
            Integer errorCount = userEentity.getErrorCount();
            if (!ObjectUtils.isEmpty(errorCount) && JwtTokenUtils.LOGIN_ERROR_COUNT == errorCount) {
                return Mono.error(new DisabledException("登陆异常次数大于" + JwtTokenUtils.LOGIN_ERROR_COUNT));
            }
            User.UserBuilder userBuilder = User.builder().passwordEncoder(passwordEncoder::encode)
                    .username(userEentity.getUsername())
                    .password(userEentity.getPassword());
            String role = userEentity.getRole();
            if (!ObjectUtils.isEmpty(role)) {
                userBuilder.roles(role);
            }
            String url = userEentity.getUrl();
            if (!ObjectUtils.isEmpty(url)) {
                userBuilder.authorities(url);
            }
            if (ObjectUtils.isEmpty(role) && ObjectUtils.isEmpty(url)) {
                userBuilder.authorities(StringUtils.DEFAULT_INDEX_HTML_1);
            }
            user = userBuilder.build();
        } catch (UsernameNotFoundException ufe) {
            return Mono.error(ufe);
        }

        if (!user.isEnabled()) {
            return Mono.error(new DisabledException("该账户已被禁用,请联系管理员"));
        } else if (!user.isAccountNonLocked()) {
            return Mono.error(new LockedException("该账号已被锁定"));
        } else if (!user.isAccountNonExpired()) {
            return Mono.error(new AccountExpiredException("该账号已过期,请联系管理员"));
        } else if (!user.isCredentialsNonExpired()) {
            return Mono.error(new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录"));
        }

        //验证密码
        if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
            userService.loginPasswordErrorAdd(username);
            return Mono.error(new BadCredentialsException("密码错误:" + username));
        }

        final Authentication authentication1 = new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());

        // TODO WebFlux方式默认没有放到context中,需要手动放入
        SecurityContextHolder.getContext().setAuthentication(authentication1);

        return Mono.just(authentication1);
    }

    /**
     * 自定义处理登陆----邮箱登陆
     *
     * @param email
     * @param verifyCode
     * @param ipAddr
     * @return reactor.core.publisher.Mono<org.springframework.security.core.Authentication>
     * @author liwei
     */
    public Mono<Authentication> authenticateEmail(String email, String verifyCode, String ipAddr) {
        LOGGER.info("自定义处理登陆----邮箱登陆.........");
        if (ObjectUtils.isEmpty(ipAddr)) {
            return Mono.error(new DisabledException("系统处理邮箱出错!"));
        }
        if (ObjectUtils.isEmpty(verifyCode)) {
            return Mono.error(new DisabledException("请填写验证码!"));
        }
        String s = redisService.getCacheObject(ipAddr);
        if (ObjectUtils.isEmpty(s)) {
            return Mono.error(new DisabledException("验证码过期,请重新获取验证码!"));
        }
        if (!s.equals(verifyCode) && !s.equals(verifyCode.toLowerCase())) {
            return Mono.error(new DisabledException("验证码有误,请重新输入!"));
        }
        redisService.deleteObject(ipAddr);

        try {
            if (!ObjectUtils.isEmpty(email)) {
                email = DESUtils.decode(email);
            }
        } catch (Exception e) {
            LOGGER.error("解密邮箱出错!");
            return Mono.error(new DisabledException("解密邮箱出错!"));
        }

        if (ObjectUtils.isEmpty(email)) {
            return Mono.error(new DisabledException("请填写邮箱"));
        }

        UserDetails user;
        UserEentity userEentity;
        try {
            userEentity = userService.selectUserByEmailDb1(email);
            if (ObjectUtils.isEmpty(userEentity)) {
                return Mono.error(new DisabledException("系统无此邮箱,不支持邮箱注册"));
            }
            User.UserBuilder userBuilder = User.builder().passwordEncoder(passwordEncoder::encode)
                    .username(userEentity.getUsername())
                    .password(userEentity.getPassword());
            String role = userEentity.getRole();
            if (!ObjectUtils.isEmpty(role)) {
                userBuilder.roles(role);
            }
            String url = userEentity.getUrl();
            if (!ObjectUtils.isEmpty(url)) {
                userBuilder.authorities(url);
            }
            if (ObjectUtils.isEmpty(role) && ObjectUtils.isEmpty(url)) {
                userBuilder.authorities(StringUtils.DEFAULT_INDEX_HTML_1);
            }
            user = userBuilder.build();
        } catch (UsernameNotFoundException ufe) {
            return Mono.error(ufe);
        }
        if (!user.isEnabled()) {
            return Mono.error(new DisabledException("该账户已被禁用,请联系管理员"));
        } else if (!user.isAccountNonLocked()) {
            return Mono.error(new LockedException("该账号已被锁定"));
        } else if (!user.isAccountNonExpired()) {
            return Mono.error(new AccountExpiredException("该账号已过期,请联系管理员"));
        } else if (!user.isCredentialsNonExpired()) {
            return Mono.error(new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录"));
        }

        userService.loginPasswordErrorAdd(userEentity.getUsername());

        final Authentication authentication1 = new UsernamePasswordAuthenticationToken(user, userEentity.getPassword(), user.getAuthorities());

        // TODO WebFlux方式默认没有放到context中,需要手动放入
        SecurityContextHolder.getContext().setAuthentication(authentication1);

        return Mono.just(authentication1);
    }

}
  1. 自定义鉴权处理
点击查看代码
package com.xiaostudy.security.config;

import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.entity.UserEentity;
import com.xiaostudy.security.service.UserService;
import com.xiaostudy.security.utils.IpUtils;
import com.xiaostudy.security.utils.JwtTokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

// 自定义的鉴权服务,通过鉴权的才能继续访问某个请求。反应式授权管理器接口
@Configuration
public class MyReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyReactiveAuthorizationManager.class);
    @Autowired
    private UserService userService;

    /**
     * 实现权限验证判断
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
        LOGGER.info("---自定义的鉴权服务---MyReactiveAuthorizationManager---");
        ServerWebExchange exchange = authorizationContext.getExchange();
        ServerHttpRequest request = exchange.getRequest();
        String ipAddr = IpUtils.getIpAddr(request);
        if (!StringUtils.REQUEST_IP_WHITE_LIST.contains(ipAddr)) {
            LOGGER.debug("---自定义的鉴权服务---MyReactiveAuthorizationManager---非白名单IP不可访问");
            return Mono.error(new DisabledException(String.format("IP:%s,非白名单,不可访问" , ipAddr)));
        }
        // option请求默认放行,解决跨域问题
        if (request.getMethod().equals(HttpMethod.OPTIONS)) {
            LOGGER.debug("---自定义的鉴权服务---MyReactiveAuthorizationManager---跨域放行");
            return Mono.just(new AuthorizationDecision(true));
        }
        //请求资源
        final String url = request.getURI().getPath();
        // 白名单放行,不用登陆就可以访问
        for (String requestRulWhite : StringUtils.REQUEST_RUL_WHITE_S) {
            if ((requestRulWhite.endsWith(StringUtils.WILDCARD) && url.startsWith(requestRulWhite.substring(0, requestRulWhite.length() - StringUtils.WILDCARD.length())))
                    || requestRulWhite.equals(url)) {
                LOGGER.debug("---自定义的鉴权服务---MyReactiveAuthorizationManager---白名单url放行");
                return Mono.just(new AuthorizationDecision(true));
            }
        }
        final HttpHeaders requestHeaders = request.getHeaders();
        final HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
        String authentication = JwtTokenUtils.getCookieAuthentication(requestHeaders);
        boolean tokenExpired = JwtTokenUtils.checkTokenAndRefreshToken(responseHeaders, authentication);
        if (!tokenExpired) {
            LOGGER.warn("token过期");
            return Mono.error(new CredentialsExpiredException("token过期,请重新登陆"));
        } else {
            LOGGER.debug("token有效");

        }
        return authenticationMono.map(auth ->
                new AuthorizationDecision(this.checkAuthorities(auth, url))
        ).defaultIfEmpty(
                new AuthorizationDecision(defaultIsToken(authentication, url))
//                new AuthorizationDecision(false)
        );
    }

    // 只有token情况下处理
    private boolean defaultIsToken(String token, String url) {
        if (ObjectUtils.isEmpty(token)) {
            return false;
        }
        String username = JwtTokenUtils.getUsernameFromToken(token);
        return this.checkAuthorities(username, url);
    }

    //权限校验,指定的url需要对应的角色,不指定的登陆成功就可以访问
    private boolean checkAuthorities(Authentication auth, String url) {
        if (ObjectUtils.isEmpty(auth)) {
            return false;
        }
        UserDetails principal = (UserDetails) auth.getPrincipal();
        if (ObjectUtils.isEmpty(principal)) {
            return false;
        }
        return this.checkAuthorities(principal.getUsername(), url);
    }

    //权限校验,指定的url需要对应的角色,不指定的登陆成功就可以访问
    private boolean checkAuthorities(String username, String url) {
        LOGGER.info("---自定义的鉴权服务---url:{}---" , url);
        if (ObjectUtils.isEmpty(username)) {
            return false;
        }
        UserEentity userEentity = userService.selectUserByNameDb1(username);
        if (ObjectUtils.isEmpty(userEentity)) {
            return false;
        }
        LOGGER.info("访问的URI是:{},用户信息:{}" , url, username);
        String role = userEentity.getRole();
        if ("/web/webLogin/user1".equals(url)) {
            return "3".equals(role);
        }
        if ("/web/webLogin/useri".equals(url)) {
            return "k".equals(role);
        }
        if ("/web/webLogin/usera".equals(url)) {
            return "c".equals(role) || "k".equals(role);
        }

        // 非指定接口,只要登陆都有权限
        return true;
    }
}
  1. 自定义处理未登陆无访问权限的返回结果
点击查看代码
package com.xiaostudy.security.handler;

import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

// 未登陆无访问权限的返回结果
@Component
public class AuthEntryPointExceptionHandler extends HttpBasicServerAuthenticationEntryPoint {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthEntryPointExceptionHandler.class);

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        LOGGER.info("未登陆无访问权限---{}--AuthEntryPointExceptionHandler.........", exchange.getRequest().getURI().getPath());
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        String jsonString = "{\"code\":200,\"status\":4,\"msg\":\"您未登陆或登陆已过期,请先登陆!\"}";
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        DataBuffer wrap = exchange.getResponse().bufferFactory().wrap(jsonString.getBytes(CharsetUtil.UTF_8));
        return exchange.getResponse().writeWith(Flux.just(wrap));
    }
}
  1. 自定义登出成功后操作
点击查看代码
package com.xiaostudy.security.handler;

import com.xiaostudy.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import reactor.core.publisher.Mono;

import java.net.URI;

// 成功登出实现类
@Configuration
public class MyRedirectServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyRedirectServerLogoutSuccessHandler.class);
    private URI logoutSuccessUrl = URI.create(StringUtils.DEFAULT_LOGOUT_SUCCESS_URL);
    private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();

    public MyRedirectServerLogoutSuccessHandler() {
    }

    @Override
    public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
        LOGGER.info("成功登出实现类----MyRedirectServerLogoutSuccessHandler.........");
        return this.redirectStrategy.sendRedirect(exchange.getExchange(), this.logoutSuccessUrl);
    }

}
  1. 自定义处理登录失败或其他异常访问调用
点击查看代码
package com.xiaostudy.security.handler;

import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

// 登录失败或其他异常访问调用的自定义处理类
@Component
public class MyServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyServerAuthenticationFailureHandler.class);

    private static final String USER_NOT_EXISTS = "用户不存在,请先注册!";

    private static final String USERNAME_PASSWORD_ERROR = "用户或密码错误!";

    private static final String USER_LOCKED = "用户锁定!";
    private static final String USER_ACCOUNT_EXPIRED = "账号已过期!";
    private static final String USER_CREDENTIALS_EXPIRE = "票据已过期!";

    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        LOGGER.info("登录失败时调用的自定义处理类----MyServerAuthenticationFailureHandler.........");
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        if (exception instanceof UsernameNotFoundException) {
            return writeErrorMessage(response, USER_NOT_EXISTS);
        } else if (exception instanceof BadCredentialsException) {
            return writeErrorMessage(response, USERNAME_PASSWORD_ERROR);
        } else if (exception instanceof LockedException) {
            return writeErrorMessage(response, USER_LOCKED);
        } else if (exception instanceof AccountExpiredException) {
            return writeErrorMessage(response, USER_ACCOUNT_EXPIRED);
        } else if (exception instanceof CredentialsExpiredException) {
            return writeErrorMessage(response, USER_CREDENTIALS_EXPIRE);
        } else if (exception instanceof DisabledException) {
            return writeErrorMessage(response, "不可访问," + exception.getMessage());
        }
        return writeErrorMessage(response, exception.getMessage());
    }

    private Mono<Void> writeErrorMessage(ServerHttpResponse response, String message) {
        String jsonString = String.format("{\"code\":200,\"status\":1,\"msg\":\"%s\"}", message);
        DataBuffer buffer = response.bufferFactory().wrap(jsonString.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
  1. 自定义处理登陆成功后返回结果
点击查看代码
package com.xiaostudy.security.handler;

import com.xiaostudy.security.utils.JwtTokenUtils;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

// 登录成功时调用的自定义处理类
@Component
public class MyServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyServerAuthenticationSuccessHandler.class);

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        LOGGER.info("登录成功时调用的自定义处理类----MyServerAuthenticationSuccessHandler.........");
        // 登录成功后可以放入一些参数到session中
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        HttpHeaders headers = response.getHeaders();

        UserDetails principal = (UserDetails) authentication.getPrincipal();
        String username = principal.getUsername();

        String token = JwtTokenUtils.generateToken(username, null);
        headers.set(JwtTokenUtils.AUTHENTICATION, String.format("%s%s", JwtTokenUtils.BASIC_EMPTY, token));

        String jsonString = String.format("{\"code\":200,\"status\":0,\"msg\":\"%s您登陆成功!\"}", username);
        headers.setContentType(MediaType.APPLICATION_JSON);
        DataBuffer buffer = response.bufferFactory().wrap(jsonString.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
  1. 自定义处理登陆后无权限访问返回结果
点击查看代码
package com.xiaostudy.security.handler;

import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.utils.JwtTokenUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

// 无权限访问被拒绝时的自定义处理器。如不自己处理,默认返回403错误<br>
@Component
public class MyWebFluxServerAccessDeniedHandler implements ServerAccessDeniedHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyWebFluxServerAccessDeniedHandler.class);

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
        LOGGER.info("无权限访问被拒绝时的自定义处理器----MyAccessDeniedHandlerWebFlux.........");
        String username = JwtTokenUtils.getCookieUsername(exchange.getRequest().getHeaders());
        if (ObjectUtils.isEmpty(username)) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (!ObjectUtils.isEmpty(authentication)) {
                UserDetails userDetails = (UserDetails) authentication.getPrincipal();
                if (!ObjectUtils.isEmpty(userDetails)) {
                    username = userDetails.getUsername();
                }
            }
        }
        if (null == username) {
            username = StringUtils.EMPTY;
        }

        String jsonString = String.format("{\"code\":200,\"status\":3,\"msg\":\"%s您无此资源的访问权限!\"}" , username, exchange.getRequest().getURI().getPath());
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return response.writeAndFlushWith(Flux.just(Flux.just(response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8)))));
    }
}
  1. 重写存储认证信息,实时修改用户session的过期时间
点击查看代码
package com.xiaostudy.security.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.time.Duration;

// 重写存储认证信息,修改session默认时效和更新会话时间
@Configuration
public class MyWebSessionServerSecurityContextRepository extends WebSessionServerSecurityContextRepository {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyWebSessionServerSecurityContextRepository.class);
    @Value("${session.timeout}")
    private Long timeout;

    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        // 只有登陆时执行,并且在load()执行之后
        LOGGER.info("存储认证信息---save---url:{}", exchange.getRequest().getURI().getPath());
        return exchange.getSession()
                .doOnNext(session -> {
                    if (context == null) {
                        session.getAttributes().remove(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
                    } else {
                        session.getAttributes().put(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
                        // 在这里设置过期时间 单位使用Duration类中的定义  有秒、分、天等
                        session.setMaxIdleTime(Duration.ofSeconds(timeout));
                    }
                })
                .flatMap(session -> session.changeSessionId());
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        String url = request.getURI().getPath();
        LOGGER.info("存储认证信息---load---url:{}", url);
        return exchange.getSession().flatMap((session) -> {
            SecurityContext context = session.getAttribute(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
            if (context == null) {
                session.getAttributes().remove(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
            } else {
                session.getAttributes().put(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
                // 在这里设置过期时间 单位使用Duration类中的定义  有秒、分、天等
                session.setMaxIdleTime(Duration.ofSeconds(timeout));
            }
            return Mono.justOrEmpty(context);
        });
    }

}
  1. 主要过滤配置类
点击查看代码
package com.xiaostudy.security.config;

import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.filter.MyWebSessionServerSecurityContextRepository;
import com.xiaostudy.security.handler.AuthEntryPointExceptionHandler;
import com.xiaostudy.security.handler.MyServerAuthenticationFailureHandler;
import com.xiaostudy.security.handler.MyServerAuthenticationSuccessHandler;
import com.xiaostudy.security.handler.MyWebFluxServerAccessDeniedHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.server.WebFilter;

import java.util.Iterator;

@Configuration
@EnableWebFluxSecurity
public class SecurityWebFluxConfig {
    private static final Logger LOG = LoggerFactory.getLogger(SecurityWebFluxConfig.class);

    @Autowired
    private MyReactiveAuthorizationManager reactiveAuthorizationManager;

    @Autowired
    private AuthEntryPointExceptionHandler serverAuthenticationEntryPoint;

    @Autowired
    private MyServerAuthenticationSuccessHandler myServerAuthenticationSuccessHandler;

    @Autowired
    private MyServerAuthenticationFailureHandler myServerAuthenticationFailureHandler;

    @Autowired
    private MyWebFluxServerAccessDeniedHandler myWebFluxServerAccessDeniedHandler;

    @Autowired
    private ServerLogoutSuccessHandler logoutSuccessHandler;
    @Autowired
    private MyWebSessionServerSecurityContextRepository myWebSessionServerSecurityContextRepository;
    @Autowired
    private MyServerFormLoginAuthenticationConverter myServerFormLoginAuthenticationConverter;
    @Autowired
    private MyReactiveAuthenticationManager myReactiveAuthenticationManager;

    // 主要过滤配置类
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        LOG.info("加载security 权限配置....");
        http
//                .headers()
//                .cors()
                // 关闭csrf
                .csrf().disable()
                // 存储认证信息,这里修改session时效
                .securityContextRepository(myWebSessionServerSecurityContextRepository)
                // 设置登陆地址,如果是前后端分离,就不用设置,前端处理。
                .formLogin().loginPage(StringUtils.DEFAULT_LOGOUT_HTML_1)
                // 登陆请求方式和接口
                .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, StringUtils.DEFAULT_LOGIN_URL_1, StringUtils.DEFAULT_LOGIN_MAIL_URL_1))
                // 处理登陆
                .authenticationManager(myReactiveAuthenticationManager)
                // 登录成功handler
                .authenticationSuccessHandler(myServerAuthenticationSuccessHandler)
                // 登陆失败handler
                .authenticationFailureHandler(myServerAuthenticationFailureHandler)

                // 关闭默认登录验证
                .and().httpBasic().disable()

//                .requestCache()

                // 登出,设置登出请求类型和URL
                .logout().requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, StringUtils.DEFAULT_LOGOUT_URL_1))
                // 登出成功后自定义处理
                .logoutSuccessHandler(logoutSuccessHandler)

                // 未登陆无访问权限handler
                .and().exceptionHandling().authenticationEntryPoint(serverAuthenticationEntryPoint)
                // 登陆无访问权限
                .and().exceptionHandling().accessDeniedHandler(myWebFluxServerAccessDeniedHandler)

                // 自定义鉴权
//                .and().authorizeExchange().pathMatchers(StringUtils.REQUEST_RUL_WHITE_S).permitAll()
                .and().authorizeExchange().anyExchange().access(reactiveAuthorizationManager)
//                .anyExchange().authenticated()
        ;
        SecurityWebFilterChain chain = http.build();
        Iterator<WebFilter> weIterable = chain.getWebFilters().toIterable().iterator();
        while (weIterable.hasNext()) {
            WebFilter f = weIterable.next();
            if (f instanceof AuthenticationWebFilter) {
                AuthenticationWebFilter webFilter = (AuthenticationWebFilter) f;
                //将自定义的AuthenticationConverter添加到过滤器中
                webFilter.setServerAuthenticationConverter(myServerFormLoginAuthenticationConverter);
            }
        }
        return chain;
    }
}

上面的图,验证码和解密工具类已经抽取到公共模块

  1. 邮箱实体类
点击查看代码
package com.xiaostudy.security.email;

import java.io.File;

public class MailEntity {
    /**
     * 主题
     */
    private String subject;
    /**
     * 内容
     */
    private String content;
    /**
     * 邮箱
     */
    private String toAccount;
    /**
     * 附件
     */
    private File attachmentFile;
    /**
     * 附件文件名
     */
    private String attachmentFileName;

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getToAccount() {
        return toAccount;
    }

    public void setToAccount(String toAccount) {
        this.toAccount = toAccount;
    }

    public File getAttachmentFile() {
        return attachmentFile;
    }

    public void setAttachmentFile(File attachmentFile) {
        this.attachmentFile = attachmentFile;
    }

    public String getAttachmentFileName() {
        return attachmentFileName;
    }

    public void setAttachmentFileName(String attachmentFileName) {
        this.attachmentFileName = attachmentFileName;
    }
}
  1. 邮箱工具类
点击查看代码
package com.xiaostudy.security.email;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

@Component
public class MailUtils {

    @Autowired
    private MailProperties mailProperties;
    @Autowired
    private JavaMailSender javaMailSender;

    /**
     * 发送邮件,里面有判断是否发文件
     */
    public void sendMail(MailEntity mailEntity) {
        if (null != mailEntity) {
            if (null != mailEntity.getAttachmentFile() && mailEntity.getAttachmentFile().exists()) {
                if (null == mailEntity.getAttachmentFileName()) {
                    mailEntity.setAttachmentFileName(mailEntity.getAttachmentFile().getName());
                }
                sendMailAttachment(mailEntity);
            } else {
                sendSimpleMail(mailEntity);
            }
        }
    }

    /**
     * 发送邮件,这里只发内容,不发文件
     */
    public void sendSimpleMail(MailEntity mailEntity) {
        SimpleMailMessage mimeMessage = new SimpleMailMessage();
        mimeMessage.setFrom(mailProperties.getUsername());
        mimeMessage.setTo(mailEntity.getToAccount());
        mimeMessage.setSubject(mailEntity.getSubject());
        mimeMessage.setText(mailEntity.getContent());
        javaMailSender.send(mimeMessage);
    }

    /**
     * 发送邮件-附件邮件
     *
     * @param mailEntity
     */
    public boolean sendMailAttachment(MailEntity mailEntity) {
        try {
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
            helper.setFrom(mailProperties.getUsername());
            helper.setTo(mailEntity.getToAccount());
            helper.setSubject(mailEntity.getSubject());
            helper.setText(mailEntity.getContent(), true);
            // 增加附件名称和附件
            helper.addAttachment(mailEntity.getAttachmentFileName(), mailEntity.getAttachmentFile());
            javaMailSender.send(mimeMessage);
            return true;
        } catch (MessagingException e) {
            e.printStackTrace();
            return false;
        }
    }
}
  1. 验证码接口
点击查看代码
package com.xiaostudy.security.controller;

import com.xiaostudy.common.redis.RedisService;
import com.xiaostudy.common.utils.DESUtils;
import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.common.utils.VerifyCodeUtils;
import com.xiaostudy.security.email.MailEntity;
import com.xiaostudy.security.email.MailUtils;
import com.xiaostudy.security.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("security")
public class VerifyCodeController {

    @Autowired
    private MailUtils mailUtils;
    @Autowired
    private RedisService redisService;

    @RequestMapping("/verifyCode")
    public Mono<Void> verifyCode(ServerWebExchange exchange) throws IOException {
        String code = VerifyCodeUtils.generateVerifyCode(4).toLowerCase();
        redisService.setCacheObject(IpUtils.getIpAddr(exchange.getRequest()), code, 60L, TimeUnit.SECONDS);
        ByteArrayOutputStream data = new ByteArrayOutputStream();
        VerifyCodeUtils.outputImage(100, 40, data, code);
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        DataBuffer buffer = response.bufferFactory().wrap(data.toByteArray());

        return response.writeWith(Flux.just(buffer));
    }

    @RequestMapping("/sendMailVerifyCode")
    public Mono<String> sendMailVerifyCode(ServerWebExchange exchange) {
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return exchange.getFormData().map(data -> {
            String code = VerifyCodeUtils.generateVerifyCode(4).toLowerCase();
            redisService.setCacheObject(IpUtils.getIpAddr(exchange.getRequest()), code, 5L, TimeUnit.MINUTES);
            String email = data.getFirst(StringUtils.EMAIL);
            try {
                if (!ObjectUtils.isEmpty(email)) {
                    email = DESUtils.decode(email);
                }
            } catch (Exception e) {
                return "{\"code\":200,\"status\":1,\"msg\":\"解密邮箱出错!\"}";
            }
            if (ObjectUtils.isEmpty(email)) {
                return "{\"code\":200,\"status\":1,\"msg\":\"请输入邮箱!\"}";
            }
            if (!VerifyCodeUtils.isEMail(email)) {
                return "{\"code\":200,\"status\":1,\"msg\":\"邮箱格式不对!\"}";
            }
            if ("xxxxx@163.com".equals(email)) {
                MailEntity mailEntity = new MailEntity();
                mailEntity.setToAccount(email);
                mailEntity.setSubject("登陆系统验证码");
                mailEntity.setContent(String.format("5分钟有效,您登陆的验证码是:%s" , code));
                mailUtils.sendMail(mailEntity);

                return "{\"code\":200,\"status\":0,\"msg\":\"验证码已发送至邮箱!\"}";
            }
            // TODO
            return "{\"code\":200,\"status\":1,\"msg\":\"测试,非自己邮箱不发!\"}";
        });
    }

}
  1. 获取当前用户名、测试动态切换数据源接口
点击查看代码
package com.xiaostudy.security.controller;

import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.security.datasources.annotation.DataSource;
import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
import com.xiaostudy.security.entity.UserEentity;
import com.xiaostudy.security.service.UserService;
import com.xiaostudy.security.utils.JwtTokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("getCurrentUserName")
    public String getCurrentUserName(Authentication authentication, ServerHttpRequest request) {
        String username = JwtTokenUtils.getCookieUsername(request.getHeaders());
        if (!ObjectUtils.isEmpty(username)) {
            return username;
        }
        if (ObjectUtils.isEmpty(authentication)) {
            authentication = SecurityContextHolder.getContext().getAuthentication();
        }
        if (ObjectUtils.isEmpty(authentication)) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        if (ObjectUtils.isEmpty(principal)) {
            return null;
        }
        if (principal instanceof UserDetails) {
            return ((UserDetails) principal).getUsername();
        } else if (principal instanceof String) {
            return (String) principal;
        }
        return null;
    }

    @DataSource(name = DataSourceNameEnum.FIRST)
    @GetMapping("testDataSource1")
    public String testDataSource1() {
        List<UserEentity> userEentities = userService.selectUserAll();
        if (ObjectUtils.isEmpty(userEentities)) {
            return null;
        }
        return userEentities.stream().map(UserEentity::getUsername).collect(Collectors.joining(StringUtils.COMMA));
    }

    @DataSource(name = DataSourceNameEnum.SECOND)
    @GetMapping("testDataSource2")
    public String testDataSource2() {
        List<UserEentity> userEentities = userService.selectUserAll();
        if (ObjectUtils.isEmpty(userEentities)) {
            return null;
        }
        return userEentities.stream().map(UserEentity::getUsername).collect(Collectors.joining(StringUtils.COMMA));
    }
}

  1. 删除启动类

3. 创建Gateway服务

  1. 创建操作
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客

  2. 父模块添加子模块

<module>gateway</module>
  1. 修改pom.xml文件
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
点击查看代码
<?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>com.xiaostudy</groupId>
        <artifactId>SpringCloud202208</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <groupId>com.xiaostudy</groupId>
    <artifactId>gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway</name>
    <description>gateway</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>com.xiaostudy</groupId>
            <artifactId>security</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>
  1. 配置文件application.properties修改为application.yml,然后配置
点击查看代码
server:
  port: '@gateway.port@'

eureka:
  port: '@eureka.port@'
  ip: '@eureka.ip@'
  url-name: '@eureka.url.name@'
  instance:
    # 把本机IP注册到eureka而不是本机机器名
    preferIpAddress: true
    # 把本机IP注册到eureka,由下面参数组成
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    serviceUrl:
      defaultZone: http://@eureka.user.name@:@eureka.user.password@@${eureka.ip}:${eureka.port}/${eureka.url-name}/

spring:
  application:
    name: '@gateway.application.name@'
  cloud:
    loadbalancer:
      retry:
        # 关闭重试
        enabled: false
    gateway:
      routes:
        # 路由的id,没有规定规则但要求唯一,建议配合服务名
        - id: '@producer.application.name@'
          # 匹配后提供服务的路由地址
          uri: lb://@producer.application.name@
          predicates:
            - Path=/producer/** # 断言,路径相匹配的进行路由
          filters:
            # 去掉url一级前缀,例如http://localhost:9904/producer/test/getByName,等同于http://localhost:9904/test/getByName
            - StripPrefix=1
        - id: '@web.application.name@'
          # lb:协议表示开启负载均衡
          uri: lb://@web.application.name@
          predicates:
            - Path=/web/** #断言,路径相匹配的进行路由
          filters:
            - StripPrefix=1
  redis:
    # 默认值:localhost
    host: localhost
    # 默认值:6379
    port: 6379
    # 默认值:0
    database: 1
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制),默认值:8
        max-active: 20
        # 连接池中的最大空闲连接,默认值:8
        max-idle: 10
        #连接池中的最小空闲连接,默认值:0
        min-idle: 1
        # 连接池最大阻塞等待时间(使用负值表示没有限制),默认值:-1,单位:毫秒
        max-wait: 2000

  profiles:
    # 使用的配置文件后缀application-security.yml。一个或多个,中间英文逗号分开
    active: security

  1. 启动类添加注解
点击查看代码
@ComponentScan(
        basePackages = {
                // 把security服务下的包交给spring管理
                "com.xiaostudy.security"
                , "com.xiaostudy.gateway"
                , "com.xiaostudy.common"
        }
)
@MapperScan("com.xiaostudy.security.mapper")
  1. 启动
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客

  2. 注册中心看服务

4. feign模块添加gateway接口

  1. application-feign.yml添加配置
此时application-feign.yml
producer:
  application:
    name: @producer.application.name@
gateway:
  application:
    name: @gateway.application.name@
feign:
  client:
    config:
      default:
        # 默认是1000
        connect-timeout: 5000
        read-timeout: 5000
  1. 添加gateway接口
点击查看代码
package com.xiaostudy.feign.apis;

import com.xiaostudy.feign.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "${gateway.application.name}" , contextId = "GatewayServiceApis" , configuration = FeignConfig.class)
public interface GatewayServiceApis {
    @GetMapping(value = "/user/getCurrentUserName")
    public String getCurrentUserName();

    @GetMapping(value = "/user/testDataSource1")
    public String testDataSource1();

    @GetMapping(value = "/user/testDataSource2")
    public String testDataSource2();
}

5. webService简单登陆

  1. 注册请求类
点击查看代码
package com.xiaostudy.webservice.entity;

import java.io.Serializable;

public class RegisterRequest implements Serializable {
    private static final Long serialVersionUID = 1L;

    private String userName;
    private String passWord;
    private String verifyCode;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public String getVerifyCode() {
        return verifyCode;
    }

    public void setVerifyCode(String verifyCode) {
        this.verifyCode = verifyCode;
    }
}
  1. IP工具类
点击查看代码
package com.xiaostudy.webservice.utils;

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

public final class IpUtils {

    private IpUtils() {
    }

    public static final String UNKNOWN = "unknown";
    public static final String LOCAL_IPV6 = "0:0:0:0:0:0:0:1";
    public static final String LOCAL_IPV4 = "127.0.0.1";

    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (null == ipAddress || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (null == ipAddress || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (null == ipAddress || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("HTTP_CLIENT_IP");
            }
            if (null == ipAddress || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (null == ipAddress || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("X-Real-IP");
            }
            if (null == ipAddress || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (LOCAL_IPV4.equals(ipAddress) || LOCAL_IPV6.equals(ipAddress)) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                if (ipAddress.indexOf(',') > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(','));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }

        return LOCAL_IPV6.equals(ipAddress) ? LOCAL_IPV4 : ipAddress;
    }

    public static String getIpAddr(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip) && ip.indexOf(",") != -1) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            ip = ip.split(",")[0];
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return LOCAL_IPV6.equals(ip) ? LOCAL_IPV4 : ip;
    }
}
  1. 添加公共模块
<dependency>
    <groupId>com.xiaostudy</groupId>
    <artifactId>common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  1. 登陆跳转和一些测试
点击查看代码
package com.xiaostudy.webservice.controller;

import com.xiaostudy.common.redis.RedisService;
import com.xiaostudy.common.utils.DESUtils;
import com.xiaostudy.common.utils.StringUtils;
import com.xiaostudy.feign.apis.GatewayServiceApis;
import com.xiaostudy.webservice.entity.RegisterRequest;
import com.xiaostudy.webservice.entity.db1.UserEentity;
import com.xiaostudy.webservice.utils.IpUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/webLogin")
public class LoginController {

    @Autowired
    private GatewayServiceApis gatewayServiceApis;

    @Value("${my.gateway.ip}")
    private String ip;

    @Value("${my.gateway.port}")
    private String port;
    @Value("${server.port}")
    private String applicationPort;

    @Autowired
    private com.xiaostudy.webservice.service.db1.UserService userService1;

    @Autowired
    private RedisService redisService;

    @RequestMapping("/login")
    public String login() {
        return String.format("redirect:http://%s:%s/web/login.html" , ip, port);
    }

    @RequestMapping("/isLogout")
    @ResponseBody
    public String isLogout() {
        return "{\"code\":200,\"status\":0,\"msg\":\"登出成功!\"}";
    }

    @RequestMapping("/register")
    @ResponseBody
    public String register(HttpServletRequest request, @RequestBody RegisterRequest registerRequest) {
        String verifyCode = registerRequest.getVerifyCode();
        String userName = registerRequest.getUserName();
        String passWord = registerRequest.getPassWord();
        if (ObjectUtils.isEmpty(verifyCode)) {
            return "{\"code\":200,\"status\":1,\"msg\":\"请输入验证码!\"}";
        }
        String ipAddr = IpUtils.getIpAddr(request);
        if (ObjectUtils.isEmpty(ipAddr)) {
            return "{\"code\":200,\"status\":1,\"msg\":\"系统出错,请稍后再试!\"}";
        }
        String s = redisService.getCacheObject(ipAddr);
        if (ObjectUtils.isEmpty(s)) {
            return "{\"code\":200,\"status\":1,\"msg\":\"验证码过期,请重新获取!\"}";
        }
        if (!s.equals(verifyCode) && !s.equals(verifyCode.toLowerCase())) {
            return "{\"code\":200,\"status\":1,\"msg\":\"验证码错误,请重新输入!\"}";
        }
        try {
            if (!ObjectUtils.isEmpty(userName)) {
                userName = DESUtils.decode(userName);
            }
            if (!ObjectUtils.isEmpty(passWord)) {
                passWord = DESUtils.decode(passWord);
            }
        } catch (Exception e) {
            return "{\"code\":200,\"status\":1,\"msg\":\"解密用户名密码出错!\"}";
        }
        if (ObjectUtils.isEmpty(userName)) {
            return "{\"code\":200,\"status\":1,\"msg\":\"请输入用户名!\"}";
        }
        if (ObjectUtils.isEmpty(passWord)) {
            return "{\"code\":200,\"status\":1,\"msg\":\"请输入密码!\"}";
        }
        UserEentity userEentity = userService1.selectUserByUsername(userName);
        if (!ObjectUtils.isEmpty(userEentity)) {
            return String.format("{\"code\":200,\"status\":1,\"msg\":\"%s用户名已存在!\"}" , userName);
        }
        userEentity = new UserEentity();
        userEentity.setUsername(userName);
        userEentity.setPassword(passWord);
        userEentity.setErrorCount(0);
        userEentity.setUrl(StringUtils.DEFAULT_INDEX_HTML_1);
        boolean insertUser = userService1.insertUser(userEentity);
        if (!insertUser) {
            return "{\"code\":200,\"status\":1,\"msg\":\"创建用户失败!\"}";
        }
        redisService.deleteObject(ipAddr);
        return "{\"code\":200,\"status\":0,\"msg\":\"创建用户成功!\"}";
    }

    @RequestMapping("/getCurrentUserName")
    @ResponseBody
    public String getCurrentUserName() {
        return gatewayServiceApis.getCurrentUserName();
    }

    @RequestMapping("/test")
    @ResponseBody
    public String test() {
        return "不用登陆";
    }

    @RequestMapping("/yes")
    @ResponseBody
    public String yes() {
        return gatewayServiceApis.getCurrentUserName() + "登陆成功就可以查看,应用端口:" + applicationPort;
    }

    @RequestMapping("/test2")
    @ResponseBody
    public String test2() {
        return "不用登陆2";
    }

    @RequestMapping("/useri")
    @ResponseBody
    public String useri() {
        String currentUserName = gatewayServiceApis.getCurrentUserName();
        return String.format("你好用户%s或用户x,有角色k权限" , currentUserName);
    }

    @RequestMapping("/usera")
    @ResponseBody
    public String usera() {
        String currentUserName = gatewayServiceApis.getCurrentUserName();
        return String.format("你好用户%s或用户x,有角色c权限" , currentUserName);
    }

    @RequestMapping("/user1")
    @ResponseBody
    public String user1() {
        String currentUserName = gatewayServiceApis.getCurrentUserName();
        return String.format("你好用户%s,有角色3权限" , currentUserName);
    }
}
  1. 前端-首页html
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页</title>
    <style>
        * {
            padding: 0;
            margin: 0;
            font-family: "楷体";
        }

        header {
            background-color: #9b9c98;
            height: 100vh;
            background-size: cover;
            background-position: center;
        }

        ul {
            float: right;
            list-style-type: none;
            margin: 15px;
        }

        ul li {
            display: inline-block;
        }

        ul li a {
            text-decoration: none;
            color: #fff;
            padding: 5px 20px;
            border: 1px solid transparent;
            transition: .6s ease;
            border-radius: 20px;
        }

        ul li a:hover {
            background-color: #fff;
            color: #000;
        }

        ul li.active a {
            background-color: #fff;
            color: #000;
        }

        .title {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        .title h1 {
            color: #fff;
            font-size: 70px;
            font-family: Century Gothic;
        }
    </style>
</head>
<body>
<header>
    <div class="main">
        <ul>
            <li id="user">你好</li>
            <li class="active"><a href="javascript:void(0);" onclick="logout();">退出</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webUser/multiDataSource')">多数据源</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webLogin/useri')">i有权</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webLogin/usera')">a有权</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webLogin/user1')">1有权</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webLogin/yes')">登陆看yes</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webLogin/test')">不用登看test</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webLogin/test2')">不用登陆看test2</a></li>
            <li><a href="javascript:void(0);" onclick="index('/producer/producerTest/getByName')">直接访问producer</a></li>
            <li><a href="javascript:void(0);" onclick="index('/web/webUser/getProducerTest')">登陆看getTest</a></li>
        </ul>
    </div>
    <div class="title">
        <h1><span style="color: crimson;">My</span> Homepage</h1>
    </div>
</header>
</body>

<script type="application/javascript">
    window.onload = getCurrentUserName;
    var userName = "";
    var Authorization = "";
    var authorizationName = "Authorization";
    var refreshTokenFlag = "RefreshTokenFlag";
    var TOKEN_REFRESH_YES = "1";

    function getHeader() {
        var req = new XMLHttpRequest();
        req.open('GET', document.location.href, false);
        req.send(null);
        var refreshTokenFlagValue = req.getResponseHeader(refreshTokenFlag);
        if (TOKEN_REFRESH_YES === refreshTokenFlagValue) {
            Authorization = req.getResponseHeader(authorizationName);
            setCookie(authorizationName, Authorization, document.location.href);
        }
    }

    function getCurrentUserName() {
        userName = getCookie("username");
        Authorization = getCookie(authorizationName);
        getHeader();
        // if (undefined === userName || "" == userName || null == userName) {
            //步骤一:创建异步对象
            var xhr = new XMLHttpRequest();
            //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
            xhr.open('get', '/web/webLogin/getCurrentUserName');
            //步骤三:发送请求
            xhr.send();
            //步骤四:注册事件 onreadystatechange 状态改变就会调用
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                    userName = xhr.responseText;
                    document.getElementById("user").innerText = "你好:" + userName + "!";

                    var refreshTokenFlagValue = xhr.getResponseHeader(refreshTokenFlag);
                    if (TOKEN_REFRESH_YES === refreshTokenFlagValue) {
                        Authorization = xhr.getResponseHeader(authorizationName);
                        setCookie(authorizationName, Authorization, "/web/index.html");
                    }
                }
            }
        // } else {
        //     document.getElementById("user").innerText = "你好:" + userName + "!";
        // }
    }

    function logout() {
        //步骤一:创建异步对象
        var xhr = new XMLHttpRequest();
        //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
        xhr.open('get', '/web/webLogin/logout');
        //步骤三:发送请求
        xhr.send();
        //步骤四:注册事件 onreadystatechange 状态改变就会调用
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                var jsonStr = xhr.responseText;
                jsonStr = eval("(" + jsonStr + ")");
                var code = jsonStr.code;
                if (code != undefined && 200 === code) {
                    var status = jsonStr.status;
                    if (status != undefined && 0 === status) {
                        location.replace("/web/login.html");
                    } else {
                        alert(jsonStr.msg);
                    }
                } else {
                    // alert("登陆异常");
                }
            } else {
                console.log("登出异常");
            }
        }
    }

    function index(url) {
        //步骤一:创建异步对象
        var xhr = new XMLHttpRequest();
        //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
        xhr.open('get', url);
        xhr.setRequestHeader(authorizationName, Authorization);
        xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
        //步骤三:发送请求
        xhr.send();
        //步骤四:注册事件 onreadystatechange 状态改变就会调用
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                var jsonStr = xhr.responseText;
                console.log(jsonStr);
            } else {
                // console.log("异常,状态非200");
            }
        }
    }

    function setCookie(name, value, url) {
        /*
        *--------------- setCookie(name,value) -----------------
        * setCookie(name,value)
        * 功能:设置得变量name的值
        * 参数:name,字符串;value,字符串.
        * 实例:setCookie('username','baobao')
        *--------------- setCookie(name,value) -----------------
        */
        var Days = 30; //此 cookie 将被保存 30 天
        var exp = new Date();
        exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
        document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
        location.href = url; //接收页面.
    }

    function getCookie(name) {
        /*
        * getCookie(name)
        * 功能:取得变量name的值
        * 参数:name,字符串.
        * 实例:alert(getCookie("username"));
        */
        var arr = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)"));
        if (arr != null) {
            console.log(arr);
            return unescape(arr[2]);
        }
        return null;
    }
</script>
</html>
  1. 前端-普通账号密码登陆
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登陆</title>
    <style>
        body {
            background: #353f42;
        }

        * {
            padding: 0;
            margin: 0;
        }

        .main {
            margin: 0 auto;
            padding-left: 25px;
            padding-right: 25px;
            padding-top: 15px;
            width: 350px;
            /*height: 350px;*/
            height: 430px;
            background: #FFFFFF;
            /*以下css用于让登录表单垂直居中在界面,可删除*/
            position: absolute;
            top: 50%;
            left: 50%;
            margin-top: -175px;
            margin-left: -175px;
        }

        .title {
            width: 100%;
            height: 40px;
            line-height: 40px;
        }

        .title span {
            font-size: 18px;
            color: #353f42;
        }

        .title-msg {
            width: 100%;
            height: 64px;
            line-height: 64px;
        }

        .title:hover {
            cursor: default;
        }

        .title-msg:hover {
            cursor: default;
        }

        .title-msg span {
            font-size: 12px;
            color: #707472;
        }

        .input-content {
            width: 100%;
            /*height: 120px;*/
            height: 200px;
        }

        .input-content input {
            width: 330px;
            height: 40px;
            border: 1px solid #dad9d6;
            background: #ffffff;
            padding-left: 10px;
            padding-right: 10px;
        }

        .enter-btn {
            width: 350px;
            height: 40px;
            color: #fff;
            background: #0bc5de;
            line-height: 40px;
            text-align: center;
            border: 0px;
        }

        .foor {
            width: 100%;
            height: auto;
            color: #9b9c98;
            font-size: 12px;
            margin-top: 20px;
        }

        .enter-btn:hover {
            cursor: pointer;
            background: #1db5c9;
        }

        .foor div:hover {
            cursor: pointer;
            color: #484847;
            font-weight: 600;
        }

        .left {
            float: left;
        }

        .right {
            float: right;
        }

    </style>
</head>

<body>
<div class="main">
    <div class="title">
        <span>密码登录</span>
    </div>

    <div class="title-msg">
        <span>请输入登录账户和密码</span>
    </div>

    <!--输入框-->
    <div class="input-content">
        <!--autoFocus-->
        <div>
            <input type="text" autocomplete="off"
                   placeholder="用户名" name="username" id="username" required/>
        </div>

        <div style="margin-top: 16px">
            <input type="password"
                   autocomplete="off" placeholder="登录密码" name="password" id="password" required maxlength="32"/>
        </div>
        <div style="margin-top: 16px">
            <img src="/security/verifyCode" title="看不清,请点我" onclick="refresh(this)" onmouseover="mouseover(this)"/>
            <input type="text" class="form-control" name="verifyCode" id="verifyCode" required="required" placeholder="验证码" autocomplete="off">
        </div>
    </div>

    <!--登入按钮-->
    <div style="text-align: center;margin-top: 30px;">
        <button type="submit" class="enter-btn" onclick="login()">登录</button>
    </div>

    <div class="foor">
        <div class="left" onclick="loginMail()"><span>邮箱登陆</span></div>
        <div class="right" onclick="register()"><span>注册账户</span></div>
    </div>
</div>

<!-- 引入 CDN Crypto.js 开始 AES加密 注意引入顺序 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-base64.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/md5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/evpkdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/cipher-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/aes.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/pad-pkcs7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/mode-ecb.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-utf8.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-hex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/tripledes.js"></script>
<!-- 引入 CDN Crypto.js 结束 -->

<script type="application/javascript">
    function loginMail() {
        location.replace("/web/loginMail.html");
    }
    function register() {
        location.replace("/web/register.html");
    }
    function refresh(obj) {
        obj.src = "/security/verifyCode?" + Math.random();
    }

    function mouseover(obj) {
        obj.style.cursor = "pointer";
    }

    var authorizationName = "Authorization";
    function login() {
        var username1 = document.getElementById("username").value;
        var password = document.getElementById("password").value;
        var verifyCode = document.getElementById("verifyCode").value;
        var username = encryptByDES(username1);
        password = encryptByDES(password);
        var indexUrl = "/web/index.html";
        //步骤一:创建异步对象
        var xhr = new XMLHttpRequest();
        //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端

        xhr.open('post', '/web/webLogin/form');
        xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
        xhr.send("userName=" + username + "&passWord=" + password + "&verifyCode=" + verifyCode + "&_t=" + new Date().getTime());


        //步骤四:注册事件 onreadystatechange 状态改变就会调用
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                var jsonStr = xhr.responseText;
                jsonStr = eval("(" + jsonStr + ")");
                var code = jsonStr.code;
                if (code != undefined && 200 === code) {
                    var status = jsonStr.status;
                    if (status != undefined && 0 === status) {
                        location.replace(indexUrl);
                        setCookie("username", username1, indexUrl);
                        setCookie(authorizationName, xhr.getResponseHeader(authorizationName), indexUrl);
                    } else {
                        alert(jsonStr.msg);
                    }
                } else {
                    // alert("登陆异常");
                }
            } else {
                // alert("登陆异常");
            }
        }
    }

    function setCookie(name, value, url) {
        /*
        *--------------- setCookie(name,value) -----------------
        * setCookie(name,value)
        * 功能:设置得变量name的值
        * 参数:name,字符串;value,字符串.
        * 实例:setCookie('username','baobao')
        *--------------- setCookie(name,value) -----------------
        */
        var Days = 30; //此 cookie 将被保存 30 天
        var exp = new Date();
        exp.setTime(exp.getTime() + Days*24*60*60*1000);
        document.cookie = name + "="+ escape(value) + ";expires=" + exp.toGMTString();
        location.href = url; //接收页面.
    }

    var cryptoJSKey = CryptoJS.enc.Utf8.parse('mwPZ7ISbC!ox6@7cP*^…5@%$)2*V');
    var cryptoJSIv = CryptoJS.enc.Utf8.parse('mwPZ7C!n');

    function encryptBy(username, password) {
        let message = username + ':' + password;

        return encryptByDES(message);
    }

    //base64 账号加密码
    function encryptByDES(message) {
        let option = {
            iv: cryptoJSIv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }
        let encrypted = CryptoJS.TripleDES.encrypt(message, cryptoJSKey, option);
        return encrypted.ciphertext.toString().toUpperCase();
    }
</script>
</body>
  1. 前端-邮箱登陆
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登陆</title>
    <style>
        body {
            background: #353f42;
        }

        * {
            padding: 0;
            margin: 0;
        }

        .main {
            margin: 0 auto;
            padding-left: 25px;
            padding-right: 25px;
            padding-top: 15px;
            width: 350px;
            height: 580px;
            background: #FFFFFF;
            /*以下css用于让登录表单垂直居中在界面,可删除*/
            position: absolute;
            top: 40%;
            left: 50%;
            margin-top: -175px;
            margin-left: -175px;
        }

        .title {
            width: 100%;
            height: 40px;
            line-height: 40px;
        }

        .title span {
            font-size: 18px;
            color: #353f42;
        }

        .title-msg {
            width: 100%;
            height: 64px;
            line-height: 64px;
        }

        .title:hover {
            cursor: default;
        }

        .title-msg:hover {
            cursor: default;
        }

        .title-msg span {
            font-size: 12px;
            color: #707472;
        }

        .input-content {
            width: 100%;
            height: 360px;
        }

        .input-content input {
            width: 330px;
            height: 40px;
            border: 1px solid #dad9d6;
            background: #ffffff;
            padding-left: 10px;
            padding-right: 10px;
        }

        .enter-btn {
            margin-top: 30px;
            width: 350px;
            height: 40px;
            color: #fff;
            background: #CCCCCC;
            line-height: 40px;
            text-align: center;
            border: 0px;
            cursor: not-allowed;
        }

        .foor {
            width: 100%;
            height: auto;
            color: #9b9c98;
            font-size: 12px;
            margin-top: 20px;
        }

        .foor div:hover {
            cursor: pointer;
            color: #484847;
            font-weight: 600;
        }

        .left {
            float: left;
        }

        .right {
            float: right;
        }

        /*滑块开始*/
        .container {
            width: 350px;
            margin: 16px auto;
        }

        #msg {
            width: 100%;
            line-height: 40px;
            font-size: 14px;
            text-align: center;
        }

        a:link,
        a:visited,
        a:hover,
        a:active {
            margin-left: 100px;
            color: #0366D6;
        }

        .block {
            position: absolute;
            left: 0;
            top: 0;
        }

        .sliderContainer {
            position: relative;
            text-align: center;
            width: 350px;
            height: 40px;
            line-height: 40px;
            margin-top: 15px;
            background: #f7f9fa;
            color: #45494c;
            border: 1px solid #e4e7eb;
        }

        .sliderContainer_active .slider {
            height: 38px;
            top: -1px;
            border: 1px solid #1991FA;
        }

        .sliderContainer_active .sliderMask {
            height: 38px;
            border-width: 1px;
        }

        .sliderContainer_success .slider {
            height: 38px;
            top: -1px;
            border: 1px solid #52CCBA;
            background-color: #52CCBA !important;
        }

        .sliderContainer_success .sliderMask {
            height: 38px;
            border: 1px solid #52CCBA;
            background-color: #D2F4EF;
        }

        .sliderContainer_success .sliderIcon {
            background-position: 0 0 !important;
        }

        .sliderContainer_fail .slider {
            height: 38px;
            top: -1px;
            border: 1px solid #f57a7a;
            background-color: #f57a7a !important;
        }

        .sliderContainer_fail .sliderMask {
            height: 38px;
            border: 1px solid #f57a7a;
            background-color: #fce1e1;
        }

        .sliderContainer_fail .sliderIcon {
            background-position: 0 -83px !important;
        }

        .sliderContainer_active .sliderText,
        .sliderContainer_success .sliderText,
        .sliderContainer_fail .sliderText {
            display: none;
        }

        .sliderMask {
            position: absolute;
            left: 0;
            top: 0;
            height: 40px;
            border: 0 solid #1991FA;
            background: #D1E9FE;
        }

        .slider {
            position: absolute;
            top: 0;
            left: 0;
            width: 40px;
            height: 40px;
            background: #fff;
            box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            transition: background .2s linear;
        }

        .slider:hover {
            background: #1991FA;
        }

        .slider:hover .sliderIcon {
            background-position: 0 -13px;
        }

        .sliderIcon {
            position: absolute;
            top: 15px;
            left: 13px;
            width: 14px;
            height: 11px;
            background: #f57a7a;
            background-size: 20px 14px;
        }

        .refreshIcon {
            position: absolute;
            right: 0;
            top: 0;
            width: 34px;
            height: 34px;
            cursor: pointer;
            background: url(img/refresh.png) 50% 50%;
            background-size: 30px 30px;
        }

        /*滑块结束*/

    </style>
</head>

<body>
<div class="main">
    <div class="title">
        <span>邮箱登录</span>
    </div>

    <div class="title-msg">
        <span>请输入邮箱获取验证码</span>
    </div>

    <!--输入框-->
    <div class="input-content">
        <div>
            <input type="text" autocomplete="off"
                   placeholder="邮箱" name="username" id="email" required/>
        </div>
        <div style="margin-top: 16px">
            <div class="clear"></div>
            <input name="code" type="text" class="form-control" id="code" placeholder="请输入验证码" autocomplete="off">
            <input style="width: 200px;" type="button" value="发送验证码" id="send" onclick="onclickSend()">
            <span id="smscode_info" class="res-error"></span>
        </div>

        <div class="container" style="margin-top: 16px">
            <div id="captcha" style="position: relative"></div>
        </div>
    </div>

    <!--登入按钮-->
    <div>
        <button type="submit" class="enter-btn" onclick="login()" id="submit" disabled>登录</button>
    </div>

    <div class="foor">
        <div class="left" onclick="loginHtml()"><span>账号登陆</span></div>

        <div class="right" onclick="register()"><span>注册账户</span></div>
    </div>
</div>

<!-- 引入 CDN Crypto.js 开始 AES加密 注意引入顺序 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-base64.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/md5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/evpkdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/cipher-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/aes.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/pad-pkcs7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/mode-ecb.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-utf8.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-hex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/tripledes.js"></script>
<!-- 引入 CDN Crypto.js 结束 -->

<script type="application/javascript">
    function loginHtml() {
        location.replace("/web/login.html");
    }

    function register() {
        location.replace("/web/register.html");
    }

    function onclickSend() {
        var email = document.getElementById("email").value;
        if ('' == email) {
            alert("请填写邮箱");
            return;
        }
        email = encryptByDES(email);
        var info = "秒后重新发送";
        var num = 6;
        var send = document.getElementById("send");
        send.setAttribute('value', num + info);
        send.setAttribute('disabled', 'true');
        send.removeAttribute('onclick');
        var t = setInterval(() => {
            num -= 1;
            var send = document.getElementById("send");
            send.setAttribute('value', num + info);
            if (num == 0) {
                clearInterval(t);
                send.setAttribute('value', '发送验证码');
                send.setAttribute('onclick', 'onclickSend()');
                send.removeAttribute('disabled');
            }
        }, 1000);

        //步骤一:创建异步对象
        var xhr = new XMLHttpRequest();
        //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端

        xhr.open('post', '/security/sendMailVerifyCode');
        xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
        xhr.send("email=" + email + "&_t=" + new Date().getTime());


        //步骤四:注册事件 onreadystatechange 状态改变就会调用
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                var jsonStr = xhr.responseText;
                jsonStr = eval("(" + jsonStr + ")");
                var code = jsonStr.code;
                if (code != undefined && 200 === code) {
                    var status = jsonStr.status;
                    if (status != undefined && 0 === status) {
                        info = "秒后重新发送,已发送至邮箱";
                    } else {
                        alert(jsonStr.msg);
                    }
                } else {
                    // alert("登陆异常");
                }
            } else {
                // alert("登陆异常");
            }
        }
    }

    var authorizationName = "Authorization";

    function login() {
        var email = document.getElementById("email").value;
        if ('' == email) {
            alert("请填写邮箱");
            return;
        }
        var verifyCode = document.getElementById("code").value;
        if ('' == verifyCode) {
            alert("请填写验证码");
            return;
        }
        email = encryptByDES(email);
        var indexUrl = "/web/index.html";
        //步骤一:创建异步对象
        var xhr = new XMLHttpRequest();
        //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端

        xhr.open('post', '/web/webLogin/emailLogin');
        xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
        xhr.send("email=" + email + "&verifyCode=" + verifyCode + "&_t=" + new Date().getTime());


        //步骤四:注册事件 onreadystatechange 状态改变就会调用
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                var jsonStr = xhr.responseText;
                jsonStr = eval("(" + jsonStr + ")");
                var code = jsonStr.code;
                if (code != undefined && 200 === code) {
                    var status = jsonStr.status;
                    if (status != undefined && 0 === status) {
                        location.replace(indexUrl);
                        setCookie(authorizationName, xhr.getResponseHeader(authorizationName), indexUrl);
                    } else {
                        alert(jsonStr.msg);
                    }
                } else {
                    // alert("登陆异常");
                }
            } else {
                // alert("登陆异常");
            }
        }
    }

    function setCookie(name, value, url) {
        /*
        *--------------- setCookie(name,value) -----------------
        * setCookie(name,value)
        * 功能:设置得变量name的值
        * 参数:name,字符串;value,字符串.
        * 实例:setCookie('username','baobao')
        *--------------- setCookie(name,value) -----------------
        */
        var Days = 30; //此 cookie 将被保存 30 天
        var exp = new Date();
        exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
        document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
        location.href = url; //接收页面.
    }

    var cryptoJSKey = CryptoJS.enc.Utf8.parse('mwPZ7ISbC!ox6@7cP*^…5@%$)2*V');
    var cryptoJSIv = CryptoJS.enc.Utf8.parse('mwPZ7C!n');

    function encryptBy(username, password) {
        let message = username + ':' + password;

        return encryptByDES(message);
    }

    //base64 账号加密码
    function encryptByDES(message) {
        let option = {
            iv: cryptoJSIv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }
        let encrypted = CryptoJS.TripleDES.encrypt(message, cryptoJSKey, option);
        return encrypted.ciphertext.toString().toUpperCase();
    }

    // ==============================================================滑动开始===========================
    (function(window) {
        const l = 42, // 滑块边长
            r = 10, // 滑块半径
            w = 350, // canvas宽度
            h = 155, // canvas高度
            PI = Math.PI
        const L = l + r * 2 // 滑块实际边长

        function getRandomNumberByRange(start, end) {
            return Math.round(Math.random() * (end - start) + start)
        }

        function createCanvas(width, height) {
            const canvas = createElement('canvas')
            canvas.width = width
            canvas.height = height
            return canvas
        }

        function createImg(onload) {
            const img = createElement('img')
            img.crossOrigin = "Anonymous"
            img.onload = onload
            img.onerror = () => {
                img.src = getRandomImg()
            }
            img.src = getRandomImg()
            return img
        }

        function createElement(tagName) {
            return document.createElement(tagName)
        }

        function addClass(tag, className) {
            tag.classList.add(className)
        }

        function removeClass(tag, className) {
            tag.classList.remove(className)
        }

        function getRandomImg() {
            return 'https://picsum.photos/300/150/?image=' + getRandomNumberByRange(0, 100)
        }

        function draw(ctx, operation, x, y) {
            ctx.beginPath()
            ctx.moveTo(x, y)
            ctx.lineTo(x + l / 2, y)
            ctx.arc(x + l / 2, y - r + 2, r, 0, 2 * PI)
            ctx.lineTo(x + l / 2, y)
            ctx.lineTo(x + l, y)
            ctx.lineTo(x + l, y + l / 2)
            ctx.arc(x + l + r - 2, y + l / 2, r, 0, 2 * PI)
            ctx.lineTo(x + l, y + l / 2)
            ctx.lineTo(x + l, y + l)
            ctx.lineTo(x, y + l)
            ctx.lineTo(x, y)
            ctx.fillStyle = '#fff'
            ctx[operation]()
            ctx.beginPath()
            ctx.arc(x, y + l / 2, r, 1.5 * PI, 0.5 * PI)
            ctx.globalCompositeOperation = "xor"
            ctx.fill()
        }

        function sum(x, y) {
            return x + y
        }

        function square(x) {
            return x * x
        }

        class jigsaw {
            constructor(el, success, fail) {
                this.el = el
                this.success = success
                this.fail = fail
            }

            init() {
                this.initDOM()
                this.initImg()
                this.draw()
                this.bindEvents()
            }

            initDOM() {
                const canvas = createCanvas(w, h) // 画布
                const block = canvas.cloneNode(true) // 滑块
                const sliderContainer = createElement('div')
                const refreshIcon = createElement('div')
                const sliderMask = createElement('div')
                const slider = createElement('div')
                const sliderIcon = createElement('span')
                const text = createElement('span')

                block.className = 'block'
                sliderContainer.className = 'sliderContainer'
                refreshIcon.className = 'refreshIcon'
                sliderMask.className = 'sliderMask'
                slider.className = 'slider'
                sliderIcon.className = 'sliderIcon'
                text.innerHTML = '向右滑动滑块填充拼图'
                text.className = 'sliderText'

                const el = this.el
                el.appendChild(canvas)
                el.appendChild(refreshIcon)
                el.appendChild(block)
                slider.appendChild(sliderIcon)
                sliderMask.appendChild(slider)
                sliderContainer.appendChild(sliderMask)
                sliderContainer.appendChild(text)
                el.appendChild(sliderContainer)

                Object.assign(this, {
                    canvas,
                    block,
                    sliderContainer,
                    refreshIcon,
                    slider,
                    sliderMask,
                    sliderIcon,
                    text,
                    canvasCtx: canvas.getContext('2d'),
                    blockCtx: block.getContext('2d')
                })
            }

            initImg() {
                const img = createImg(() => {
                    this.canvasCtx.drawImage(img, 0, 0, w, h)
                    this.blockCtx.drawImage(img, 0, 0, w, h)
                    const y = this.y - r * 2 + 2
                    const ImageData = this.blockCtx.getImageData(this.x, y, L, L)
                    this.block.width = L
                    this.blockCtx.putImageData(ImageData, 0, y)
                })
                this.img = img
            }

            draw() {
                // 随机创建滑块的位置
                this.x = getRandomNumberByRange(L + 10, w - (L + 10))
                this.y = getRandomNumberByRange(10 + r * 2, h - (L + 10))
                draw(this.canvasCtx, 'fill', this.x, this.y)
                draw(this.blockCtx, 'clip', this.x, this.y)
            }

            clean() {
                this.canvasCtx.clearRect(0, 0, w, h)
                this.blockCtx.clearRect(0, 0, w, h)
                this.block.width = w
            }

            bindEvents() {
                this.el.onselectstart = () => false
                this.refreshIcon.onclick = () => {
                    this.reset()
                }

                let originX, originY, trail = [],
                    isMouseDown = false
                this.slider.addEventListener('mousedown', function(e) {
                    originX = e.x, originY = e.y
                    isMouseDown = true
                })
                document.addEventListener('mousemove', (e) => {
                    if(!isMouseDown) return false
                    const moveX = e.x - originX
                    const moveY = e.y - originY
                    if(moveX < 0 || moveX + 38 >= w) return false
                    this.slider.style.left = moveX + 'px'
                    var blockLeft = (w - 40 - 20) / (w - 40) * moveX
                    this.block.style.left = blockLeft + 'px'

                    addClass(this.sliderContainer, 'sliderContainer_active')
                    this.sliderMask.style.width = moveX + 'px'
                    trail.push(moveY)
                })
                document.addEventListener('mouseup', (e) => {
                    if(!isMouseDown) return false
                    isMouseDown = false
                    if(e.x == originX) return false
                    removeClass(this.sliderContainer, 'sliderContainer_active')
                    this.trail = trail
                    const {
                        spliced,
                        TuringTest
                    } = this.verify()
                    if(spliced) {
                        if(TuringTest) {
                            addClass(this.sliderContainer, 'sliderContainer_success')
                            this.success && this.success()
                        } else {
                            addClass(this.sliderContainer, 'sliderContainer_fail')
                            this.text.innerHTML = '再试一次'
                            this.reset()
                        }
                    } else {
                        // alert("验证失败");
                        addClass(this.sliderContainer, 'sliderContainer_fail')
                        this.fail && this.fail();
                        //验证失败后,1秒后重新加载图片
                        setTimeout(() => {
                            this.reset()
                        }, 1000)
                    }
                })
            }

            verify() {
                const arr = this.trail // 拖动时y轴的移动距离
                const average = arr.reduce(sum) / arr.length // 平均值
                const deviations = arr.map(x => x - average) // 偏差数组
                const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // 标准差
                const left = parseInt(this.block.style.left)
                return {
                    spliced: Math.abs(left - this.x) < 10,
                    TuringTest: average !== stddev, // 只是简单的验证拖动轨迹,相等时一般为0,表示可能非人为操作
                }
            }

            reset() {
                this.sliderContainer.className = 'sliderContainer'
                this.slider.style.left = 0
                this.block.style.left = 0
                this.sliderMask.style.width = 0
                this.clean()
                this.img.src = getRandomImg()
                this.draw()
            }

        }

        window.jigsaw = {
            init: function(element, success, fail) {
                new jigsaw(element, success, fail).init()
            }
        }
    }(window))

    jigsaw.init(document.getElementById('captcha'), function() {
        var slider = document.querySelector('.slider');
        slider.setAttribute('disabled', 'true');
        var submit = document.querySelector('.enter-btn');
        submit.style.background = '#0bc5de';
        submit.style.setProperty('cursor', 'pointer');
        submit.removeAttribute('disabled');
    })
    // ==============================================================滑动结束===========================
</script>
</body>

图片

  1. 前端-用户注册
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册</title>
    <style>
        body {
            background: #353f42;
        }

        * {
            padding: 0;
            margin: 0;
        }

        .main {
            margin: 0 auto;
            padding-left: 25px;
            padding-right: 25px;
            padding-top: 15px;
            width: 350px;
            /*height: 350px;*/
            height: 430px;
            background: #FFFFFF;
            /*以下css用于让登录表单垂直居中在界面,可删除*/
            position: absolute;
            top: 50%;
            left: 50%;
            margin-top: -175px;
            margin-left: -175px;
        }

        .title {
            width: 100%;
            height: 40px;
            line-height: 40px;
        }

        .title span {
            font-size: 18px;
            color: #353f42;
        }

        .title-msg {
            width: 100%;
            height: 64px;
            line-height: 64px;
        }

        .title:hover {
            cursor: default;
        }

        .title-msg:hover {
            cursor: default;
        }

        .title-msg span {
            font-size: 12px;
            color: #707472;
        }

        .input-content {
            width: 100%;
            /*height: 120px;*/
            height: 200px;
        }

        .input-content input {
            width: 330px;
            height: 40px;
            border: 1px solid #dad9d6;
            background: #ffffff;
            padding-left: 10px;
            padding-right: 10px;
        }

        .enter-btn {
            width: 350px;
            height: 40px;
            color: #fff;
            background: #0bc5de;
            line-height: 40px;
            text-align: center;
            border: 0px;
        }

        .foor {
            width: 100%;
            height: auto;
            color: #9b9c98;
            font-size: 12px;
            margin-top: 20px;
        }

        .enter-btn:hover {
            cursor: pointer;
            background: #1db5c9;
        }

        .foor div:hover {
            cursor: pointer;
            color: #484847;
            font-weight: 600;
        }

        .left {
            float: left;
        }

        .right {
            float: right;
        }

    </style>
</head>

<body>
<div class="main">
    <div class="title">
        <span>用户注册</span>
    </div>

    <div class="title-msg">
        <span>请输入账户和密码</span>
    </div>

    <!--输入框-->
    <div class="input-content">
        <!--autoFocus-->
        <div>
            <input type="text" autocomplete="off"
                   placeholder="用户名" name="username" id="username" required/>
        </div>

        <div style="margin-top: 16px">
            <input type="password"
                   autocomplete="off" placeholder="登录密码" name="password" id="password" required maxlength="32"/>
        </div>
        <div style="margin-top: 16px">
            <img src="/security/verifyCode" title="看不清,请点我" onclick="refresh(this)" onmouseover="mouseover(this)"/>
            <input type="text" class="form-control" name="verifyCode" id="verifyCode" required="required" placeholder="验证码">
        </div>
    </div>

    <!--登入按钮-->
    <div style="text-align: center;margin-top: 30px;">
        <button type="submit" class="enter-btn" onclick="register()">注册</button>
    </div>

    <div class="foor">
        <div class="right" onclick="login()"><span>返回登陆</span></div>
    </div>
</div>

<!-- 引入 CDN Crypto.js 开始 AES加密 注意引入顺序 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-base64.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/md5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/evpkdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/cipher-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/aes.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/pad-pkcs7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/mode-ecb.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-utf8.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/enc-hex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.3.0/tripledes.js"></script>
<!-- 引入 CDN Crypto.js 结束 -->

<script type="application/javascript">
    function login() {
        location.replace("/web/login.html");
    }
    function refresh(obj) {
        obj.src = "/security/verifyCode?" + Math.random();
    }

    function mouseover(obj) {
        obj.style.cursor = "pointer";
    }

    function register() {
        var username1 = document.getElementById("username").value;
        var password = document.getElementById("password").value;
        var verifyCode = document.getElementById("verifyCode").value;
        var username = encryptByDES(username1);
        password = encryptByDES(password);
        var indexUrl = "/web/login.html";
        //步骤一:创建异步对象
        var xhr = new XMLHttpRequest();
        //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端

        xhr.open('post', '/web/webLogin/register');
        xhr.setRequestHeader('Content-Type', 'application/json;charset=utf8');
        var para=JSON.stringify({"userName":username,"passWord":password,"verifyCode":verifyCode});
        xhr.send(para);

        //步骤四:注册事件 onreadystatechange 状态改变就会调用
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的    console.log(xhr.responseText);//输入相应的内容    }
                var jsonStr = xhr.responseText;
                jsonStr = eval("(" + jsonStr + ")");
                var code = jsonStr.code;
                if (code != undefined && 200 === code) {
                    var status = jsonStr.status;
                    if (status != undefined && 0 === status) {
                        alert("注册成功,请前往登陆!");
                        // location.replace(indexUrl);
                    } else {
                        alert(jsonStr.msg);
                    }
                } else {
                    // alert("登陆异常");
                }
            } else {
                // alert("登陆异常");
            }
        }
    }

    var cryptoJSKey = CryptoJS.enc.Utf8.parse('mwPZ7ISbC!ox6@7cP*^…5@%$)2*V');
    var cryptoJSIv = CryptoJS.enc.Utf8.parse('mwPZ7C!n');

    function encryptBy(username, password) {
        let message = username + ':' + password;

        return encryptByDES(message);
    }

    //base64 账号加密码
    function encryptByDES(message) {
        let option = {
            iv: cryptoJSIv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }
        let encrypted = CryptoJS.TripleDES.encrypt(message, cryptoJSKey, option);
        return encrypted.ciphertext.toString().toUpperCase();
    }
</script>
</body>

6. 演示

  1. 未登陆-无需登陆就可以看到,访问url白名单
    http://localhost:9904/web/webLogin/test

  2. 未登录
    http://localhost:9904/web/webLogin/getTest

  3. 登陆-用户不存在
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客

  4. 验证码错误
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客

  5. 账号密码正确

  6. url权限控制
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客

  7. 邮箱登陆
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    简单创建一个SpringCloud2021.0.3项目(二)-小白菜博客
    image