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. 创建操作

  2. 修改pom.xml文件



点击查看代码
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>SpringCloud202208</artifactId>
  7. <groupId>com.xiaostudy</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>common</artifactId>
  12. <dependencies>
  13. <!-- SpringBoot Boot Redis -->
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-data-redis</artifactId>
  17. </dependency>
  18. <!-- JSON 解析器和生成器 -->
  19. <dependency>
  20. <groupId>com.alibaba.fastjson2</groupId>
  21. <artifactId>fastjson2</artifactId>
  22. <version>2.0.12</version>
  23. </dependency>
  24. </dependencies>
  25. </project>
  1. redis序列化、配置类、工具类

序列化
  1. package com.xiaostudy.common.redis;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.alibaba.fastjson2.JSONReader;
  4. import com.alibaba.fastjson2.JSONWriter;
  5. import org.springframework.data.redis.serializer.RedisSerializer;
  6. import org.springframework.data.redis.serializer.SerializationException;
  7. import java.nio.charset.Charset;
  8. import java.nio.charset.StandardCharsets;
  9. /**
  10. * Redis使用FastJson序列化
  11. */
  12. public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
  13. private Class<T> clazz;
  14. public FastJson2JsonRedisSerializer(Class<T> clazz) {
  15. super();
  16. this.clazz = clazz;
  17. }
  18. @Override
  19. public byte[] serialize(T t) throws SerializationException {
  20. if (t == null) {
  21. return new byte[0];
  22. }
  23. return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
  24. }
  25. @Override
  26. public T deserialize(byte[] bytes) throws SerializationException {
  27. if (bytes == null || bytes.length <= 0) {
  28. return null;
  29. }
  30. String str = new String(bytes, StandardCharsets.UTF_8);
  31. return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
  32. }
  33. }
配置类
  1. package com.xiaostudy.common.redis;
  2. import org.springframework.boot.autoconfigure.AutoConfigureBefore;
  3. import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
  4. import org.springframework.cache.annotation.CachingConfigurerSupport;
  5. import org.springframework.cache.annotation.EnableCaching;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.data.redis.connection.RedisConnectionFactory;
  9. import org.springframework.data.redis.core.RedisTemplate;
  10. import org.springframework.data.redis.serializer.StringRedisSerializer;
  11. /**
  12. * redis配置
  13. */
  14. @Configuration
  15. // 启动redis
  16. @EnableCaching
  17. // RedisConfig在RedisAutoConfiguration之前加载
  18. @AutoConfigureBefore(RedisAutoConfiguration.class)
  19. public class RedisConfig extends CachingConfigurerSupport {
  20. @Bean
  21. @SuppressWarnings(value = {"unchecked" , "rawtypes"})
  22. public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
  23. RedisTemplate<Object, Object> template = new RedisTemplate<>();
  24. template.setConnectionFactory(connectionFactory);
  25. FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
  26. // 使用StringRedisSerializer来序列化和反序列化redis的key值
  27. template.setKeySerializer(new StringRedisSerializer());
  28. template.setValueSerializer(serializer);
  29. // Hash的key也采用StringRedisSerializer的序列化方式
  30. template.setHashKeySerializer(new StringRedisSerializer());
  31. template.setHashValueSerializer(serializer);
  32. template.afterPropertiesSet();
  33. return template;
  34. }
  35. }
工具类
  1. package com.xiaostudy.common.redis;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.BoundSetOperations;
  4. import org.springframework.data.redis.core.HashOperations;
  5. import org.springframework.data.redis.core.RedisTemplate;
  6. import org.springframework.data.redis.core.ValueOperations;
  7. import org.springframework.stereotype.Component;
  8. import java.util.*;
  9. import java.util.concurrent.TimeUnit;
  10. /**
  11. * spring redis 工具类
  12. **/
  13. @SuppressWarnings(value = {"unchecked" , "rawtypes"})
  14. @Component
  15. public class RedisService {
  16. @Autowired
  17. public RedisTemplate redisTemplate;
  18. /**
  19. * 缓存基本的对象,Integer、String、实体类等
  20. *
  21. * @param key 缓存的键值
  22. * @param value 缓存的值
  23. */
  24. public <T> void setCacheObject(final String key, final T value) {
  25. redisTemplate.opsForValue().set(key, value);
  26. }
  27. /**
  28. * 缓存基本的对象,Integer、String、实体类等
  29. *
  30. * @param key 缓存的键值
  31. * @param value 缓存的值
  32. * @param timeout 时间
  33. * @param timeUnit 时间颗粒度
  34. */
  35. public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {
  36. redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
  37. }
  38. /**
  39. * 设置有效时间
  40. *
  41. * @param key Redis键
  42. * @param timeout 超时时间
  43. * @return true=设置成功;false=设置失败
  44. */
  45. public boolean expire(final String key, final long timeout) {
  46. return expire(key, timeout, TimeUnit.SECONDS);
  47. }
  48. /**
  49. * 设置有效时间
  50. *
  51. * @param key Redis键
  52. * @param timeout 超时时间
  53. * @param unit 时间单位
  54. * @return true=设置成功;false=设置失败
  55. */
  56. public boolean expire(final String key, final long timeout, final TimeUnit unit) {
  57. return redisTemplate.expire(key, timeout, unit);
  58. }
  59. /**
  60. * 获取有效时间
  61. *
  62. * @param key Redis键
  63. * @return 有效时间
  64. */
  65. public long getExpire(final String key) {
  66. return redisTemplate.getExpire(key);
  67. }
  68. /**
  69. * 判断 key是否存在
  70. *
  71. * @param key 键
  72. * @return true 存在 false不存在
  73. */
  74. public Boolean hasKey(String key) {
  75. return redisTemplate.hasKey(key);
  76. }
  77. /**
  78. * 获得缓存的基本对象。
  79. *
  80. * @param key 缓存键值
  81. * @return 缓存键值对应的数据
  82. */
  83. public <T> T getCacheObject(final String key) {
  84. ValueOperations<String, T> operation = redisTemplate.opsForValue();
  85. return operation.get(key);
  86. }
  87. /**
  88. * 删除单个对象
  89. *
  90. * @param key
  91. */
  92. public boolean deleteObject(final String key) {
  93. return redisTemplate.delete(key);
  94. }
  95. /**
  96. * 删除集合对象
  97. *
  98. * @param collection 多个对象
  99. * @return
  100. */
  101. public boolean deleteObject(final Collection collection) {
  102. return redisTemplate.delete(collection) > 0;
  103. }
  104. /**
  105. * 缓存List数据
  106. *
  107. * @param key 缓存的键值
  108. * @param dataList 待缓存的List数据
  109. * @return 缓存的对象
  110. */
  111. public <T> long setCacheList(final String key, final List<T> dataList) {
  112. Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
  113. return count == null ? 0 : count;
  114. }
  115. /**
  116. * 获得缓存的list对象
  117. *
  118. * @param key 缓存的键值
  119. * @return 缓存键值对应的数据
  120. */
  121. public <T> List<T> getCacheList(final String key) {
  122. return redisTemplate.opsForList().range(key, 0, -1);
  123. }
  124. /**
  125. * 缓存Set
  126. *
  127. * @param key 缓存键值
  128. * @param dataSet 缓存的数据
  129. * @return 缓存数据的对象
  130. */
  131. public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
  132. BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
  133. Iterator<T> it = dataSet.iterator();
  134. while (it.hasNext()) {
  135. setOperation.add(it.next());
  136. }
  137. return setOperation;
  138. }
  139. /**
  140. * 获得缓存的set
  141. *
  142. * @param key
  143. * @return
  144. */
  145. public <T> Set<T> getCacheSet(final String key) {
  146. return redisTemplate.opsForSet().members(key);
  147. }
  148. /**
  149. * 缓存Map
  150. *
  151. * @param key
  152. * @param dataMap
  153. */
  154. public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
  155. if (dataMap != null) {
  156. redisTemplate.opsForHash().putAll(key, dataMap);
  157. }
  158. }
  159. /**
  160. * 获得缓存的Map
  161. *
  162. * @param key
  163. * @return
  164. */
  165. public <T> Map<String, T> getCacheMap(final String key) {
  166. return redisTemplate.opsForHash().entries(key);
  167. }
  168. /**
  169. * 往Hash中存入数据
  170. *
  171. * @param key Redis键
  172. * @param hKey Hash键
  173. * @param value 值
  174. */
  175. public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
  176. redisTemplate.opsForHash().put(key, hKey, value);
  177. }
  178. /**
  179. * 获取Hash中的数据
  180. *
  181. * @param key Redis键
  182. * @param hKey Hash键
  183. * @return Hash中的对象
  184. */
  185. public <T> T getCacheMapValue(final String key, final String hKey) {
  186. HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
  187. return opsForHash.get(key, hKey);
  188. }
  189. /**
  190. * 获取多个Hash中的数据
  191. *
  192. * @param key Redis键
  193. * @param hKeys Hash键集合
  194. * @return Hash对象集合
  195. */
  196. public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
  197. return redisTemplate.opsForHash().multiGet(key, hKeys);
  198. }
  199. /**
  200. * 删除Hash中的某条数据
  201. *
  202. * @param key Redis键
  203. * @param hKey Hash键
  204. * @return 是否成功
  205. */
  206. public boolean deleteCacheMapValue(final String key, final String hKey) {
  207. return redisTemplate.opsForHash().delete(key, hKey) > 0;
  208. }
  209. /**
  210. * 获得缓存的基本对象列表
  211. *
  212. * @param pattern 字符串前缀
  213. * @return 对象列表
  214. */
  215. public Collection<String> keys(final String pattern) {
  216. return redisTemplate.keys(pattern);
  217. }
  218. }
  1. 字符串工具类

点击查看代码
  1. package com.xiaostudy.common.utils;
  2. import java.util.Arrays;
  3. import java.util.List;
  4. public class StringUtils {
  5. public static final String BLANK = " ";
  6. public static final String EMPTY = "";
  7. public static final String DEFAULT_LOGOUT_SUCCESS_URL = "/web/webLogin/isLogout";
  8. public static final String DEFAULT_LOGIN_URL_1 = "/web/webLogin/form";
  9. public static final String DEFAULT_LOGIN_MAIL_URL_1 = "/web/webLogin/emailLogin";
  10. public static final String DEFAULT_LOGOUT_URL_1 = "/web/webLogin/logout";
  11. public static final String DEFAULT_REGISTER_URL_1 = "/web/webLogin/register";
  12. public static final String DEFAULT_REGISTER_HTML_1 = "/web/register.html";
  13. public static final String DEFAULT_LOGOUT_HTML_1 = "/web/login.html";
  14. public static final String DEFAULT_LOGIN_MAIL_HTML = "/web/loginMail.html";
  15. public static final String DEFAULT_INDEX_HTML_1 = "/web/index.html";
  16. public static final String COMMA = ",";
  17. public static final String EMAIL = "email";
  18. public static final String WILDCARD = "**";
  19. public static final String[] REQUEST_RUL_WHITE_S = {
  20. DEFAULT_LOGOUT_HTML_1
  21. , "/web/webLogin/login"
  22. , DEFAULT_LOGOUT_SUCCESS_URL
  23. , DEFAULT_LOGIN_MAIL_HTML
  24. , DEFAULT_REGISTER_URL_1
  25. , DEFAULT_REGISTER_HTML_1
  26. , DEFAULT_LOGIN_URL_1
  27. , "/security/verifyCode"
  28. , "/security/sendMailVerifyCode"
  29. , "/security/sendPhoneVerifyCode"
  30. , "/web/webLogin/test"
  31. , "/web/webLogin/test2"
  32. , "/web/img/**"
  33. };
  34. public static final List<String> REQUEST_RUL_WHITE_LIST = Arrays.asList(REQUEST_RUL_WHITE_S);
  35. public static final List<String> REQUEST_IP_WHITE_LIST = Arrays.asList(
  36. "192.168.1.6"
  37. , "192.168.1.2"
  38. , "127.0.0.1"
  39. );
  40. }
  1. 验证码工具类
点击查看代码
  1. package com.xiaostudy.common.utils;
  2. import javax.imageio.ImageIO;
  3. import java.awt.*;
  4. import java.awt.geom.AffineTransform;
  5. import java.awt.image.BufferedImage;
  6. import java.io.File;
  7. import java.io.FileOutputStream;
  8. import java.io.IOException;
  9. import java.io.OutputStream;
  10. import java.util.Arrays;
  11. import java.util.Map;
  12. import java.util.Random;
  13. import java.util.concurrent.ConcurrentHashMap;
  14. import java.util.regex.Matcher;
  15. import java.util.regex.Pattern;
  16. public final class VerifyCodeUtils {
  17. //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
  18. public static final String VERIFY_CODES = "1234567890ABCDEFGHIJKLMNPQRSTUVWXYZ";
  19. private static Random random = new Random();
  20. private VerifyCodeUtils() {
  21. }
  22. 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,}$";
  23. public static boolean isEMail(String email) {
  24. Pattern regex = Pattern.compile(EMAIL_REGEX);
  25. Matcher matcher = regex.matcher(email);
  26. return matcher.matches();
  27. }
  28. /**
  29. * 使用系统默认字符源生成验证码
  30. */
  31. public static String generateVerifyCode(int verifySize) {
  32. return generateVerifyCode(verifySize, VERIFY_CODES);
  33. }
  34. /**
  35. * 使用指定源生成验证码
  36. */
  37. public static String generateVerifyCode(int verifySize, String sources) {
  38. if (sources == null || sources.length() == 0) {
  39. sources = VERIFY_CODES;
  40. }
  41. int codesLen = sources.length();
  42. Random rand = new Random(System.currentTimeMillis());
  43. StringBuilder verifyCode = new StringBuilder(verifySize);
  44. for (int i = 0; i < verifySize; i++) {
  45. verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
  46. }
  47. return verifyCode.toString();
  48. }
  49. /**
  50. * 生成随机验证码文件,并返回验证码值
  51. */
  52. public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
  53. String verifyCode = generateVerifyCode(verifySize);
  54. outputImage(w, h, outputFile, verifyCode);
  55. return verifyCode;
  56. }
  57. /**
  58. * 输出随机验证码图片流,并返回验证码值
  59. */
  60. public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
  61. String verifyCode = generateVerifyCode(verifySize);
  62. outputImage(w, h, os, verifyCode);
  63. return verifyCode;
  64. }
  65. /**
  66. * 生成指定验证码图像文件
  67. */
  68. public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
  69. if (outputFile == null) {
  70. return;
  71. }
  72. File dir = outputFile.getParentFile();
  73. if (!dir.exists()) {
  74. dir.mkdirs();
  75. }
  76. outputFile.createNewFile();
  77. FileOutputStream fos = new FileOutputStream(outputFile);
  78. outputImage(w, h, fos, code);
  79. fos.close();
  80. }
  81. /**
  82. * 输出指定验证码图片流
  83. */
  84. public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
  85. int verifySize = code.length();
  86. BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
  87. Random rand = new Random();
  88. Graphics2D g2 = image.createGraphics();
  89. g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  90. Color[] colors = new Color[5];
  91. Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
  92. Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
  93. Color.PINK, Color.YELLOW};
  94. float[] fractions = new float[colors.length];
  95. for (int i = 0; i < colors.length; i++) {
  96. colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
  97. fractions[i] = rand.nextFloat();
  98. }
  99. Arrays.sort(fractions);
  100. g2.setColor(Color.GRAY);// 设置边框色
  101. g2.fillRect(0, 0, w, h);
  102. Color c = getRandColor(200, 250);
  103. g2.setColor(c);// 设置背景色
  104. g2.fillRect(0, 2, w, h - 4);
  105. //绘制干扰线
  106. Random random = new Random();
  107. g2.setColor(getRandColor(160, 200));// 设置线条的颜色
  108. for (int i = 0; i < 20; i++) {
  109. int x = random.nextInt(w - 1);
  110. int y = random.nextInt(h - 1);
  111. int xl = random.nextInt(6) + 1;
  112. int yl = random.nextInt(12) + 1;
  113. g2.drawLine(x, y, x + xl + 40, y + yl + 20);
  114. }
  115. // 添加噪点
  116. float yawpRate = 0.05f;// 噪声率
  117. int area = (int) (yawpRate * w * h);
  118. for (int i = 0; i < area; i++) {
  119. int x = random.nextInt(w);
  120. int y = random.nextInt(h);
  121. int rgb = getRandomIntColor();
  122. image.setRGB(x, y, rgb);
  123. }
  124. shear(g2, w, h, c);// 使图片扭曲
  125. g2.setColor(getRandColor(100, 160));
  126. int fontSize = h - 4;
  127. Font font = new Font("Algerian", Font.ITALIC, fontSize);
  128. g2.setFont(font);
  129. char[] chars = code.toCharArray();
  130. for (int i = 0; i < verifySize; i++) {
  131. AffineTransform affine = new AffineTransform();
  132. affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
  133. g2.setTransform(affine);
  134. g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
  135. }
  136. g2.dispose();
  137. ImageIO.write(image, "jpg", os);
  138. }
  139. private static Color getRandColor(int fc, int bc) {
  140. if (fc > 255)
  141. fc = 255;
  142. if (bc > 255)
  143. bc = 255;
  144. int r = fc + random.nextInt(bc - fc);
  145. int g = fc + random.nextInt(bc - fc);
  146. int b = fc + random.nextInt(bc - fc);
  147. return new Color(r, g, b);
  148. }
  149. private static int getRandomIntColor() {
  150. int[] rgb = getRandomRgb();
  151. int color = 0;
  152. for (int c : rgb) {
  153. color = color << 8;
  154. color = color | c;
  155. }
  156. return color;
  157. }
  158. private static int[] getRandomRgb() {
  159. int[] rgb = new int[3];
  160. for (int i = 0; i < 3; i++) {
  161. rgb[i] = random.nextInt(255);
  162. }
  163. return rgb;
  164. }
  165. private static void shear(Graphics g, int w1, int h1, Color color) {
  166. shearX(g, w1, h1, color);
  167. shearY(g, w1, h1, color);
  168. }
  169. private static void shearX(Graphics g, int w1, int h1, Color color) {
  170. int period = random.nextInt(2);
  171. boolean borderGap = true;
  172. int frames = 1;
  173. int phase = random.nextInt(2);
  174. for (int i = 0; i < h1; i++) {
  175. double d = (double) (period >> 1)
  176. * Math.sin((double) i / (double) period
  177. + (6.2831853071795862D * (double) phase)
  178. / (double) frames);
  179. g.copyArea(0, i, w1, 1, (int) d, 0);
  180. if (borderGap) {
  181. g.setColor(color);
  182. g.drawLine((int) d, i, 0, i);
  183. g.drawLine((int) d + w1, i, w1, i);
  184. }
  185. }
  186. }
  187. private static void shearY(Graphics g, int w1, int h1, Color color) {
  188. int period = random.nextInt(40) + 10;
  189. boolean borderGap = true;
  190. int frames = 20;
  191. int phase = 7;
  192. for (int i = 0; i < w1; i++) {
  193. double d = (double) (period >> 1)
  194. * Math.sin((double) i / (double) period
  195. + (6.2831853071795862D * (double) phase)
  196. / (double) frames);
  197. g.copyArea(i, 0, 1, h1, 0, (int) d);
  198. if (borderGap) {
  199. g.setColor(color);
  200. g.drawLine(i, (int) d, i, 0);
  201. g.drawLine(i, (int) d + h1, i, h1);
  202. }
  203. }
  204. }
  205. }

  1. 解密工具类

点击查看代码
  1. package com.xiaostudy.common.utils;
  2. import javax.crypto.*;
  3. import javax.crypto.spec.DESedeKeySpec;
  4. import javax.crypto.spec.IvParameterSpec;
  5. import java.nio.charset.Charset;
  6. import java.nio.charset.StandardCharsets;
  7. import java.security.InvalidAlgorithmParameterException;
  8. import java.security.InvalidKeyException;
  9. import java.security.Key;
  10. import java.security.NoSuchAlgorithmException;
  11. import java.security.spec.InvalidKeySpecException;
  12. public class DESUtils {
  13. /**
  14. * 用户名称密码加密,密钥
  15. */
  16. private static final String SECRET_KEY = "mwPZ7ISbC!ox6@7cP*^…5@%$)2*V";
  17. // 向量
  18. private static final String IV = "mwPZ7C!n";
  19. // 加解密统一使用的编码方式
  20. private static final Charset encoding = StandardCharsets.UTF_8;
  21. /**
  22. * 3DES解密
  23. *
  24. * @param encryptText 加密文本
  25. * @return
  26. * @throws Exception
  27. */
  28. public static String decode(String encryptText) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
  29. DESedeKeySpec spec = new DESedeKeySpec(SECRET_KEY.getBytes());
  30. SecretKeyFactory keyfactory = SecretKeyFactory.getInstance("desede");
  31. Key deskey = keyfactory.generateSecret(spec);
  32. Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
  33. IvParameterSpec ips = new IvParameterSpec(IV.getBytes());
  34. cipher.init(Cipher.DECRYPT_MODE, deskey, ips);
  35. byte[] decryptData = cipher.doFinal(hexToBytes(encryptText));
  36. return new String(decryptData, encoding);
  37. }
  38. public static byte[] hexToBytes(String hex) {
  39. hex = hex.length() % 2 != 0 ? "0" + hex : hex;
  40. byte[] b = new byte[hex.length() / 2];
  41. for (int i = 0; i < b.length; i++) {
  42. int index = i * 2;
  43. int v = Integer.parseInt(hex.substring(index, index + 2), 16);
  44. b[i] = (byte) v;
  45. }
  46. return b;
  47. }
  48. }

4. 网关Gateway

1. 创建Security

  1. 创建操作



  2. 父模块添加子模块

  1. <module>security</module>
  1. 修改pom.xml







点击查看代码
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>com.xiaostudy</groupId>
  7. <artifactId>SpringCloud202208</artifactId>
  8. <version>1.0-SNAPSHOT</version>
  9. <relativePath>../pom.xml</relativePath>
  10. </parent>
  11. <groupId>com.xiaostudy</groupId>
  12. <artifactId>security</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <name>security</name>
  15. <description>security</description>
  16. <dependencies>
  17. <dependency>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-starter-data-jpa</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-security</artifactId>
  24. </dependency>
  25. <!--集成响应式web框架-->
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-webflux</artifactId>
  29. </dependency>
  30. <!-- druid数据源驱动 -->
  31. <dependency>
  32. <groupId>com.alibaba</groupId>
  33. <artifactId>druid-spring-boot-starter</artifactId>
  34. <version>1.2.1</version>
  35. </dependency>
  36. <!--AOP-->
  37. <dependency>
  38. <groupId>org.aspectj</groupId>
  39. <artifactId>aspectjweaver</artifactId>
  40. </dependency>
  41. <!--动态切换数据源用到-->
  42. <dependency>
  43. <groupId>org.springframework.boot</groupId>
  44. <artifactId>spring-boot-starter-data-jpa</artifactId>
  45. </dependency>
  46. <dependency>
  47. <groupId>mysql</groupId>
  48. <artifactId>mysql-connector-java</artifactId>
  49. </dependency>
  50. <dependency>
  51. <groupId>org.mybatis.spring.boot</groupId>
  52. <artifactId>mybatis-spring-boot-starter</artifactId>
  53. <version>2.1.2</version>
  54. </dependency>
  55. <!-- JWT Token验证机制 -->
  56. <dependency>
  57. <groupId>com.auth0</groupId>
  58. <artifactId>java-jwt</artifactId>
  59. <version>3.13.0</version>
  60. </dependency>
  61. <dependency>
  62. <groupId>io.jsonwebtoken</groupId>
  63. <artifactId>jjwt</artifactId>
  64. <version>0.9.0</version>
  65. </dependency>
  66. <!--邮箱依赖-->
  67. <dependency>
  68. <groupId>org.springframework.boot</groupId>
  69. <artifactId>spring-boot-starter-mail</artifactId>
  70. </dependency>
  71. <!--SpringBoot中集成了jasypt在一定程度上保证密码的安全-->
  72. <dependency>
  73. <groupId>com.github.ulisesbocchio</groupId>
  74. <artifactId>jasypt-spring-boot-starter</artifactId>
  75. <version>2.1.1</version>
  76. </dependency>
  77. <dependency>
  78. <groupId>com.xiaostudy</groupId>
  79. <artifactId>common</artifactId>
  80. <version>1.0-SNAPSHOT</version>
  81. </dependency>
  82. <dependency>
  83. <groupId>org.springframework.boot</groupId>
  84. <artifactId>spring-boot-starter-test</artifactId>
  85. <scope>test</scope>
  86. </dependency>
  87. <dependency>
  88. <groupId>org.springframework.security</groupId>
  89. <artifactId>spring-security-test</artifactId>
  90. <scope>test</scope>
  91. </dependency>
  92. </dependencies>
  93. <build>
  94. <plugins>
  95. <plugin>
  96. <groupId>org.springframework.boot</groupId>
  97. <artifactId>spring-boot-maven-plugin</artifactId>
  98. </plugin>
  99. </plugins>
  100. </build>
  101. </project>
  1. 修改配置文件application.properties

点击查看代码
  1. spring:
  2. datasource:
  3. type: com.alibaba.druid.pool.DruidDataSource
  4. druid:
  5. driverClassName: com.mysql.cj.jdbc.Driver
  6. url: jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8
  7. username: root
  8. password: 密码
  9. druid2:
  10. driverClassName: com.mysql.cj.jdbc.Driver
  11. url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=utf8
  12. username: root
  13. password: 密码
  14. mail:
  15. default-encoding: UTF-8
  16. # 阿里云发送服务器地址
  17. host: smtp.mxhichina.com
  18. # port: 25 #端口号
  19. # 发送人地址
  20. username: liwei@xiaostudy.com
  21. # 密码
  22. password: ENC(密码加密后的字符串)
  23. properties:
  24. mail:
  25. smtp:
  26. starttls:
  27. enable: true
  28. required: true
  29. auth: true
  30. socketFactory:
  31. class: javax.net.ssl.SSLSocketFactory
  32. port: 465
  33. jasypt:
  34. encryptor:
  35. password: 密钥
  36. mybatis:
  37. configuration:
  38. # 下划线转驼峰
  39. map-underscore-to-camel-case: true
  40. # 注册映射文件
  41. mapper-locations: mapper/*Mapper.xml
  42. # 注册实体类别名
  43. type-aliases-package: com.xiaostudy.security.entity
  44. session:
  45. # session过期时间,单位秒
  46. timeout: 1800
  47. # timeout: 30
  1. 查询用户的实体类、service、mapper
用户实体类
  1. package com.xiaostudy.security.entity;
  2. public class UserEentity {
  3. private String username;
  4. private String password;
  5. private String role;
  6. private Integer errorCount;
  7. private String url;
  8. private String email;
  9. public String getUsername() {
  10. return username;
  11. }
  12. public void setUsername(String username) {
  13. this.username = username;
  14. }
  15. public String getPassword() {
  16. return password;
  17. }
  18. public void setPassword(String password) {
  19. this.password = password;
  20. }
  21. public String getRole() {
  22. return role;
  23. }
  24. public void setRole(String role) {
  25. this.role = role;
  26. }
  27. public Integer getErrorCount() {
  28. return errorCount;
  29. }
  30. public void setErrorCount(Integer errorCount) {
  31. this.errorCount = errorCount;
  32. }
  33. public String getUrl() {
  34. return url;
  35. }
  36. public void setUrl(String url) {
  37. this.url = url;
  38. }
  39. public String getEmail() {
  40. return email;
  41. }
  42. public void setEmail(String email) {
  43. this.email = email;
  44. }
  45. }
Mapper接口
  1. package com.xiaostudy.security.mapper;
  2. import com.xiaostudy.security.entity.UserEentity;
  3. import org.apache.ibatis.annotations.Mapper;
  4. import org.apache.ibatis.annotations.Param;
  5. import java.util.List;
  6. @Mapper
  7. public interface UserMapper {
  8. public List<UserEentity> selectUserAll();
  9. public UserEentity selectUserByName(@Param("name") String username);
  10. public UserEentity selectUserByEmail(@Param("email") String email);
  11. public UserEentity selectUserByPhone(@Param("phone") String phone);
  12. public int loginPasswordErrorAdd(@Param("name")String username);
  13. public int loginPasswordErrorClean(@Param("name")String username);
  14. }
Mapper.xml文件
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.xiaostudy.security.mapper.UserMapper">
  4. <select id="selectUserAll" resultType="com.xiaostudy.security.entity.UserEentity">
  5. SELECT username, password, role, error_count FROM `user`
  6. </select>
  7. <select id="selectUserByName" resultType="com.xiaostudy.security.entity.UserEentity">
  8. SELECT username, password, role, error_count, url, email FROM `user` where username = #{name}
  9. </select>
  10. <select id="selectUserByEmail" resultType="com.xiaostudy.security.entity.UserEentity">
  11. SELECT username, password, role, error_count, url, email FROM `user` where email = #{email}
  12. </select>
  13. <select id="selectUserByPhone" resultType="com.xiaostudy.security.entity.UserEentity">
  14. SELECT username, password, role, error_count, url, email, phone FROM `user` where phone = #{phone}
  15. </select>
  16. <update id="loginPasswordErrorAdd" parameterType="java.lang.String">
  17. update `user` set error_count = error_count + 1 where username = #{name}
  18. </update>
  19. <update id="loginPasswordErrorClean" parameterType="java.lang.String">
  20. update `user` set error_count = 0 where username = #{name}
  21. </update>
  22. </mapper>
service
  1. package com.xiaostudy.security.service;
  2. import com.xiaostudy.security.entity.UserEentity;
  3. import java.util.List;
  4. public interface UserService {
  5. public List<UserEentity> selectUserAll();
  6. public UserEentity selectUserByNameDb1(String username);
  7. public UserEentity selectUserByEmailDb1(String email);
  8. public UserEentity selectUserByPhoneDb1(String phone);
  9. public UserEentity selectUserByNameDb2(String username);
  10. public boolean loginPasswordErrorAdd(String username);
  11. public boolean loginPasswordErrorClean(String username);
  12. }
service实现类
  1. package com.xiaostudy.security.service.impl;
  2. import com.xiaostudy.security.datasources.annotation.DataSource;
  3. import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
  4. import com.xiaostudy.security.entity.UserEentity;
  5. import com.xiaostudy.security.mapper.UserMapper;
  6. import com.xiaostudy.security.service.UserService;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Service;
  9. import org.springframework.transaction.annotation.Transactional;
  10. import java.util.List;
  11. @Service
  12. public class UserServiceImpl implements UserService {
  13. @Autowired
  14. private UserMapper userMapper;
  15. @Override
  16. public List<UserEentity> selectUserAll() {
  17. return userMapper.selectUserAll();
  18. }
  19. @DataSource(name = DataSourceNameEnum.FIRST)
  20. @Override
  21. public UserEentity selectUserByNameDb1(String username) {
  22. return userMapper.selectUserByName(username);
  23. }
  24. @DataSource(name = DataSourceNameEnum.FIRST)
  25. @Override
  26. public UserEentity selectUserByEmailDb1(String email) {
  27. return userMapper.selectUserByEmail(email);
  28. }
  29. @DataSource(name = DataSourceNameEnum.FIRST)
  30. @Override
  31. public UserEentity selectUserByPhoneDb1(String phone) {
  32. return userMapper.selectUserByPhone(phone);
  33. }
  34. @DataSource(name = DataSourceNameEnum.SECOND)
  35. @Override
  36. public UserEentity selectUserByNameDb2(String username) {
  37. return userMapper.selectUserByName(username);
  38. }
  39. @DataSource(name = DataSourceNameEnum.FIRST)
  40. @Override
  41. public boolean loginPasswordErrorAdd(String username) {
  42. int i = userMapper.loginPasswordErrorAdd(username);
  43. return 0 != i;
  44. }
  45. @Transactional(rollbackFor = Exception.class)
  46. @Override
  47. public boolean loginPasswordErrorClean(String username) {
  48. int i = userMapper.loginPasswordErrorClean(username);
  49. return 0 != i;
  50. }
  51. }
多数据源枚举
  1. package com.xiaostudy.security.datasources.enums;
  2. /**
  3. * 多数据源配置数据源枚举
  4. */
  5. public enum DataSourceNameEnum {
  6. FIRST("first")
  7. ,SECOND("second");
  8. private String name;
  9. DataSourceNameEnum(String name) {
  10. this.name = name;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. }
动态数据源路由
  1. package com.xiaostudy.security.datasources;
  2. import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
  3. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  4. import javax.sql.DataSource;
  5. import java.util.Map;
  6. /**
  7. * 动态数据源路由
  8. */
  9. public class DynamicDataSource extends AbstractRoutingDataSource {
  10. private static final ThreadLocal<DataSourceNameEnum> contextHolder = new ThreadLocal<>();
  11. public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
  12. super.setDefaultTargetDataSource(defaultTargetDataSource);
  13. super.setTargetDataSources(targetDataSources);
  14. super.afterPropertiesSet();
  15. }
  16. @Override
  17. protected Object determineCurrentLookupKey() {
  18. return this.getDataSource();
  19. }
  20. public static void setDataSource(DataSourceNameEnum dataSource) {
  21. contextHolder.set(dataSource);
  22. }
  23. public static DataSourceNameEnum getDataSource() {
  24. return contextHolder.get();
  25. }
  26. public static void clearDataSource() {
  27. contextHolder.remove();
  28. }
  29. }
多数据源注解
  1. package com.xiaostudy.security.datasources.annotation;
  2. import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
  3. import java.lang.annotation.*;
  4. /**
  5. * 多数据源注解
  6. */
  7. @Target(ElementType.METHOD)
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @Documented
  10. public @interface DataSource {
  11. DataSourceNameEnum name();
  12. }
多数据源AOP类
  1. package com.xiaostudy.security.datasources.aop;
  2. import com.xiaostudy.security.datasources.DynamicDataSource;
  3. import com.xiaostudy.security.datasources.annotation.DataSource;
  4. import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
  5. import org.aspectj.lang.ProceedingJoinPoint;
  6. import org.aspectj.lang.annotation.Around;
  7. import org.aspectj.lang.annotation.Aspect;
  8. import org.aspectj.lang.annotation.Pointcut;
  9. import org.aspectj.lang.reflect.MethodSignature;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.core.Ordered;
  13. import org.springframework.stereotype.Component;
  14. import java.lang.reflect.Method;
  15. /**
  16. * 多数据源,切面处理类
  17. */
  18. @Aspect
  19. @Component
  20. public class DataSourceAspect implements Ordered {
  21. private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);
  22. /**
  23. * 针对上面注解做切面拦截
  24. */
  25. @Pointcut("@annotation(com.xiaostudy.security.datasources.annotation.DataSource)")
  26. // @Pointcut("execution(* com.xiaostudy.security.datasources..*.*(..))")
  27. public void dataSourcePointCut() {}
  28. @Around("dataSourcePointCut()")
  29. public Object around(ProceedingJoinPoint point) throws Throwable {
  30. MethodSignature signature = (MethodSignature) point.getSignature();
  31. Method method = signature.getMethod();
  32. DataSource dataSource = method.getAnnotation(DataSource.class);
  33. if(dataSource == null){
  34. //如果没有注解,使用默认数据源
  35. DynamicDataSource.setDataSource(DataSourceNameEnum.FIRST);
  36. }else {
  37. //根据注解中设置的数据源名称,选择对应的数据源
  38. DynamicDataSource.setDataSource(dataSource.name());
  39. LOGGER.info("set datasource is " + dataSource.name().getName());
  40. }
  41. try {
  42. return point.proceed();
  43. } finally {
  44. //清除数据源配置
  45. DynamicDataSource.clearDataSource();
  46. }
  47. }
  48. @Override
  49. public int getOrder() {
  50. return 1;
  51. }
  52. }
多数据配置类
  1. package com.xiaostudy.security.datasources.config;
  2. import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
  3. import com.xiaostudy.security.datasources.DynamicDataSource;
  4. import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
  5. import org.springframework.boot.context.properties.ConfigurationProperties;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.context.annotation.Primary;
  9. import javax.sql.DataSource;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. /**
  13. * 多数据源配置类
  14. */
  15. @Configuration
  16. public class DynamicDataSourceConfig {
  17. //如果ioc容器中,同一个类型有多个bean,则bean的名称为方法的名称
  18. @Bean("firstDataSource")
  19. @ConfigurationProperties("spring.datasource.druid")
  20. public DataSource firstDataSource() {
  21. return DruidDataSourceBuilder.create().build();
  22. }
  23. @Bean("secondDataSource")
  24. @ConfigurationProperties("spring.datasource.druid2")
  25. public DataSource secondDataSource() {
  26. return DruidDataSourceBuilder.create().build();
  27. }
  28. @Bean
  29. @Primary
  30. public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
  31. Map<Object, Object> targetDataSources = new HashMap<>();
  32. targetDataSources.put(DataSourceNameEnum.FIRST, firstDataSource);
  33. targetDataSources.put(DataSourceNameEnum.SECOND, secondDataSource);
  34. return new DynamicDataSource(firstDataSource, targetDataSources);
  35. }
  36. }

2. Security登陆配置

  1. 配置密码加密、解析器

点击查看代码
  1. package com.xiaostudy.security.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  5. import org.springframework.security.crypto.password.PasswordEncoder;
  6. @Configuration
  7. public class BeanConfig {
  8. //配置密码加密、解析器
  9. @Bean
  10. public PasswordEncoder passwordEncoder() {
  11. return new BCryptPasswordEncoder();
  12. }
  13. }
  1. IP工具类
点击查看代码
  1. package com.xiaostudy.security.utils;
  2. import org.springframework.http.HttpHeaders;
  3. import org.springframework.http.server.reactive.ServerHttpRequest;
  4. public final class IpUtils {
  5. private IpUtils() {
  6. }
  7. public static final String UNKNOWN = "unknown";
  8. public static final String LOCAL_IPV6 = "0:0:0:0:0:0:0:1";
  9. public static final String LOCAL_IPV4 = "127.0.0.1";
  10. public static String getIpAddr(ServerHttpRequest request) {
  11. HttpHeaders headers = request.getHeaders();
  12. String ip = headers.getFirst("x-forwarded-for");
  13. if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip) && ip.indexOf(",") != -1) {
  14. // 多次反向代理后会有多个ip值,第一个ip才是真实ip
  15. ip = ip.split(",")[0];
  16. }
  17. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  18. ip = headers.getFirst("Proxy-Client-IP");
  19. }
  20. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  21. ip = headers.getFirst("WL-Proxy-Client-IP");
  22. }
  23. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  24. ip = headers.getFirst("HTTP_CLIENT_IP");
  25. }
  26. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  27. ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
  28. }
  29. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  30. ip = headers.getFirst("X-Real-IP");
  31. }
  32. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  33. ip = request.getRemoteAddress().getAddress().getHostAddress();
  34. }
  35. return LOCAL_IPV6.equals(ip) ? LOCAL_IPV4 : ip;
  36. }
  37. }
  1. Token工具类
点击查看代码
  1. package com.xiaostudy.security.utils;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import io.jsonwebtoken.*;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.http.HttpHeaders;
  7. import org.springframework.util.ObjectUtils;
  8. import java.util.Date;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. import java.util.concurrent.ConcurrentHashMap;
  12. import java.util.concurrent.ExecutorService;
  13. public class JwtTokenUtils {
  14. private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtils.class);
  15. // 有效时间,单位毫秒
  16. // public static final long EXPIRATION = 30 * 1000L;
  17. public static final long EXPIRATION = 40 * 60 * 1000L;
  18. public static final long TOKEN_REFRESH_DATE = 15 * 1000L;
  19. // public static final long TOKEN_REFRESH_DATE = 20 * 60 * 1000L;
  20. public static final String TOKEN_REFRESH_DATE_STR = "TOKEN_REFRESH_DATE";
  21. //JWT密钥
  22. public static final String SECRET = "123654";
  23. public static final String BASIC_EMPTY = "Basic ";
  24. public static final String BASIC_EMPTY_ = "Basic%20";
  25. public static final String AUTHENTICATION = "Authorization";
  26. public static final String COOKIE_AUTHENTICATION_BASIC_EMPTY_ = "Authorization=Basic%20";
  27. public static final String COOKIE_SPLIT = ";";
  28. public static final String COOKIE = "Cookie";
  29. public static final String TOKEN_CREATED = "created";
  30. public static final String TOKEN_REFRESH_FLAG = "RefreshTokenFlag";
  31. public static final String TOKEN_REFRESH_YES = "1";
  32. public static final String TOKEN_REFRESH_NO = "0";
  33. public static final String VERIFY_CODE = "verifyCode";
  34. public static final String COOKIE_VERIFY_CODE = "verifyCode=";
  35. public static final String USER_NAME = "userName";
  36. public static final String PASS_WORD = "passWord";
  37. public static final int LOGIN_ERROR_COUNT = 5;
  38. /**
  39. * 生成token令牌
  40. *
  41. * @param username 用户
  42. * @param payloads 令牌中携带的附加信息
  43. * @return 令token牌
  44. */
  45. public static String generateToken(String username, Map<String, Object> payloads) {
  46. int payloadSizes = payloads == null ? 0 : payloads.size();
  47. Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
  48. claims.put(Claims.SUBJECT, username);
  49. claims.put(TOKEN_CREATED, new Date());
  50. if (payloadSizes > 0) {
  51. claims.putAll(payloads);
  52. }
  53. return generateToken(claims);
  54. }
  55. /**
  56. * 从claims生成令牌,如果看不懂就看谁调用它
  57. *
  58. * @param claims 数据声明
  59. * @return 令牌
  60. */
  61. private static String generateToken(Map<String, Object> claims) {
  62. Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION);
  63. // 刷新token时间
  64. claims.put(JwtTokenUtils.TOKEN_REFRESH_DATE_STR, new Date(System.currentTimeMillis() + TOKEN_REFRESH_DATE));
  65. return Jwts.builder().setClaims(claims)
  66. .setExpiration(expirationDate)
  67. .signWith(SignatureAlgorithm.HS512, SECRET)
  68. .compact();
  69. }
  70. /**
  71. * 判断令牌是否过期
  72. *
  73. * @param token 令牌
  74. * @return 是否过期
  75. */
  76. public static boolean isTokenExpired(String token) {
  77. if (ObjectUtils.isEmpty(token)) {
  78. return false;
  79. }
  80. try {
  81. Claims claims = getClaimsFromToken(token);
  82. if (ObjectUtils.isEmpty(claims)) {
  83. return false;
  84. }
  85. Date expiration = claims.getExpiration();
  86. return new Date().before(expiration);
  87. } catch (Exception e) {
  88. LOGGER.error("判断令牌是否过期异常", e);
  89. return false;
  90. }
  91. }
  92. /**
  93. * 从令牌中获取用户名
  94. *
  95. * @param token 令牌
  96. * @return 用户名
  97. */
  98. public static String getUsernameFromToken(String token) {
  99. if (ObjectUtils.isEmpty(token)) {
  100. return null;
  101. }
  102. String username;
  103. try {
  104. Claims claims = getClaimsFromToken(token);
  105. username = claims.getSubject();
  106. } catch (Exception e) {
  107. LOGGER.error("从令牌中获取用户名异常1", e);
  108. username = null;
  109. }
  110. return username;
  111. }
  112. /**
  113. * 刷新令牌
  114. *
  115. * @param token 原令牌
  116. * @return 新令牌
  117. */
  118. public static String refreshToken(String token) {
  119. if (ObjectUtils.isEmpty(token)) {
  120. return null;
  121. }
  122. String refreshedToken;
  123. try {
  124. Claims claims = getClaimsFromToken(token);
  125. claims.put(TOKEN_CREATED, new Date());
  126. refreshedToken = generateToken(claims);
  127. } catch (Exception e) {
  128. LOGGER.error("刷新令牌异常", e);
  129. refreshedToken = null;
  130. }
  131. return refreshedToken;
  132. }
  133. /**
  134. * 从令牌中获取数据声明,如果看不懂就看谁调用它
  135. *
  136. * @param token 令牌
  137. * @return 数据声明
  138. */
  139. private static Claims getClaimsFromToken(String token) {
  140. Claims claims;
  141. try {
  142. JwtParser jwtParser = Jwts.parser().setSigningKey(SECRET);
  143. Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
  144. claims = claimsJws.getBody();
  145. } catch (Exception e) {
  146. LOGGER.error("从令牌中获取数据声明异常");
  147. // LOGGER.error("从令牌中获取数据声明异常", e);
  148. claims = null;
  149. }
  150. return claims;
  151. }
  152. public static String getCookieUsername(HttpHeaders headers) {
  153. String authentication = getCookieAuthentication(headers);
  154. if (ObjectUtils.isEmpty(authentication)) {
  155. return null;
  156. }
  157. return getUsernameFromToken(authentication);
  158. }
  159. public static String getCookieAuthentication(HttpHeaders headers) {
  160. String authentication = headers.getFirst(JwtTokenUtils.AUTHENTICATION);
  161. if (ObjectUtils.isEmpty(authentication)) {
  162. String cookieStr = headers.getFirst(JwtTokenUtils.COOKIE);
  163. if (!ObjectUtils.isEmpty(cookieStr)) {
  164. cookieStr = cookieStr.replaceAll(StringUtils.BLANK, StringUtils.EMPTY);
  165. String[] cookies = cookieStr.split(JwtTokenUtils.COOKIE_SPLIT);
  166. for (String c : cookies) {
  167. if (!ObjectUtils.isEmpty(c) && c.startsWith(JwtTokenUtils.COOKIE_AUTHENTICATION_BASIC_EMPTY_)) {
  168. authentication = c.replaceFirst(JwtTokenUtils.COOKIE_AUTHENTICATION_BASIC_EMPTY_, StringUtils.EMPTY);
  169. break;
  170. }
  171. }
  172. }
  173. }
  174. if (!ObjectUtils.isEmpty(authentication)) {
  175. if (authentication.startsWith(JwtTokenUtils.BASIC_EMPTY_)) {
  176. authentication = authentication.replaceFirst(JwtTokenUtils.BASIC_EMPTY_, StringUtils.EMPTY);
  177. } else if (authentication.startsWith(JwtTokenUtils.BASIC_EMPTY)) {
  178. authentication = authentication.replaceFirst(JwtTokenUtils.BASIC_EMPTY, StringUtils.EMPTY);
  179. }
  180. }
  181. return authentication;
  182. }
  183. public static String getCookieVerifyCode(HttpHeaders headers) {
  184. String cookieStr = headers.getFirst(JwtTokenUtils.COOKIE);
  185. if (ObjectUtils.isEmpty(cookieStr)) {
  186. return null;
  187. }
  188. cookieStr = cookieStr.replaceAll(StringUtils.BLANK, StringUtils.EMPTY);
  189. String[] cookies = cookieStr.split(JwtTokenUtils.COOKIE_SPLIT);
  190. for (String c : cookies) {
  191. if (!ObjectUtils.isEmpty(c) && c.startsWith(JwtTokenUtils.COOKIE_VERIFY_CODE)) {
  192. return c.replaceFirst(JwtTokenUtils.COOKIE_VERIFY_CODE, StringUtils.EMPTY);
  193. }
  194. }
  195. return null;
  196. }
  197. private static final Map<String, Date> LOG_TOKEN_DATE_MAP = new ConcurrentHashMap<>();
  198. private static final ExecutorService POOL = java.util.concurrent.Executors.newFixedThreadPool(2);
  199. public static boolean checkTokenAndRefreshToken(HttpHeaders headers, String authentication) {
  200. boolean tokenExpired = false;
  201. Date now = new Date();
  202. if (ObjectUtils.isEmpty(authentication)) {
  203. return tokenExpired;
  204. }
  205. try {
  206. Claims claims = getClaimsFromToken(authentication);
  207. if (ObjectUtils.isEmpty(claims)) {
  208. return tokenExpired;
  209. }
  210. Date expiration = claims.getExpiration();
  211. if (now.after(expiration)) {
  212. return tokenExpired;
  213. }
  214. Date expirationTokenRefresh = claims.get(TOKEN_REFRESH_DATE_STR, Date.class);
  215. tokenExpired = now.before(expirationTokenRefresh);
  216. } catch (Exception e) {
  217. LOGGER.error("判断令牌是否过期异常", e);
  218. return false;
  219. }
  220. headers.set(TOKEN_REFRESH_FLAG, TOKEN_REFRESH_NO);
  221. if (tokenExpired) {
  222. // token有效
  223. POOL.execute(new LogTokenRunnable(LOG_TOKEN_DATE_MAP, authentication, now));
  224. return tokenExpired;
  225. } else {
  226. Date date = LOG_TOKEN_DATE_MAP.get(authentication);
  227. if (ObjectUtils.isEmpty(date)) {
  228. return tokenExpired;
  229. }
  230. Date expirationDate = new Date(date.getTime() + EXPIRATION);
  231. if (expirationDate.before(now)) {
  232. return tokenExpired;
  233. }
  234. String refreshToken = refreshToken(authentication);
  235. if (ObjectUtils.isEmpty(refreshToken)) {
  236. return tokenExpired;
  237. }
  238. headers.set(TOKEN_REFRESH_FLAG, TOKEN_REFRESH_YES);
  239. headers.set(AUTHENTICATION, BASIC_EMPTY + refreshToken);
  240. return true;
  241. }
  242. }
  243. // 记录token最后请求时间
  244. private static class LogTokenRunnable implements Runnable {
  245. private Map<String, Date> map;
  246. private String token;
  247. private Date now;
  248. LogTokenRunnable(Map<String, Date> map, String token, Date now) {
  249. this.map = map;
  250. this.token = token;
  251. this.now = now;
  252. }
  253. @Override
  254. public void run() {
  255. if (null == map || ObjectUtils.isEmpty(token) || ObjectUtils.isEmpty(now)) {
  256. return;
  257. }
  258. Date date = map.get(token);
  259. if (ObjectUtils.isEmpty(date) || now.after(date)) {
  260. map.put(token, now);
  261. }
  262. }
  263. }
  264. }
  1. 自定义UsernamePasswordAuthenticationToken
点击查看代码
  1. package com.xiaostudy.security.entity;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import com.xiaostudy.security.utils.JwtTokenUtils;
  4. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  5. import org.springframework.util.MultiValueMap;
  6. public class MyUserDetails extends UsernamePasswordAuthenticationToken {
  7. private static final long serialVersionUID = 1L;
  8. private String username;
  9. private String password;
  10. private String verifyCode;
  11. private String ipAddr;
  12. private String email;
  13. public MyUserDetails(String username, String password, String verifyCode, String ipAddr, String email) {
  14. super(username, password);
  15. this.username = username;
  16. this.password = password;
  17. this.verifyCode = verifyCode;
  18. this.ipAddr = ipAddr;
  19. this.email = email;
  20. }
  21. public static MyUserDetails unauthenticated(String username, String password, String verifyCode, String ipAddr, String email) {
  22. return new MyUserDetails(username, password, verifyCode, ipAddr, email);
  23. }
  24. public static MyUserDetails unauthenticated(String username, String password) {
  25. return new MyUserDetails(username, password, null, null, null);
  26. }
  27. public static MyUserDetails createAuthentication(MultiValueMap<String, String> data, String ipAddr) {
  28. String username = data.getFirst(JwtTokenUtils.USER_NAME);
  29. String password = data.getFirst(JwtTokenUtils.PASS_WORD);
  30. String verifyCode = data.getFirst(JwtTokenUtils.VERIFY_CODE);
  31. String email = data.getFirst(StringUtils.EMAIL);
  32. return MyUserDetails.unauthenticated(username, password, verifyCode, ipAddr, email);
  33. }
  34. public String getUsername() {
  35. return username;
  36. }
  37. public void setUsername(String username) {
  38. this.username = username;
  39. }
  40. public String getPassword() {
  41. return password;
  42. }
  43. public void setPassword(String password) {
  44. this.password = password;
  45. }
  46. public String getVerifyCode() {
  47. return verifyCode;
  48. }
  49. public void setVerifyCode(String verifyCode) {
  50. this.verifyCode = verifyCode;
  51. }
  52. public String getIpAddr() {
  53. return ipAddr;
  54. }
  55. public void setIpAddr(String ipAddr) {
  56. this.ipAddr = ipAddr;
  57. }
  58. public String getEmail() {
  59. return email;
  60. }
  61. public void setEmail(String email) {
  62. this.email = email;
  63. }
  64. }
  1. 自定义ServerFormLoginAuthenticationConverter,从表单获取参数转成自定义UsernamePasswordAuthenticationToken类
点击查看代码
  1. package com.xiaostudy.security.config;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import com.xiaostudy.security.entity.MyUserDetails;
  4. import com.xiaostudy.security.utils.IpUtils;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.security.core.Authentication;
  9. import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
  10. import org.springframework.web.server.ServerWebExchange;
  11. import reactor.core.publisher.Mono;
  12. // 请求认证过滤器,从表单获取参数,不用security的默认参数名username、password
  13. @Configuration
  14. public class MyServerFormLoginAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
  15. private static final Logger LOGGER = LoggerFactory.getLogger(MyServerFormLoginAuthenticationConverter.class);
  16. @Override
  17. public Mono<Authentication> convert(ServerWebExchange exchange) {
  18. LOGGER.info("请求认证过滤器----MyServerFormLoginAuthenticationConverter.........");
  19. String uri = exchange.getRequest().getURI().getPath();
  20. if (StringUtils.DEFAULT_LOGIN_URL_1.equals(uri)) { //登录操作才对body做特殊操作,其他请求直接调用原有请求
  21. return this.apply(exchange);
  22. } else { //非登录操作,基本不用在网关里读取body,默认方法就行
  23. return super.convert(exchange);
  24. }
  25. }
  26. @Override
  27. public Mono<Authentication> apply(ServerWebExchange exchange) {
  28. final String ipAddr = IpUtils.getIpAddr(exchange.getRequest());
  29. return exchange.getFormData().map((data) -> MyUserDetails.createAuthentication(data, ipAddr));
  30. }
  31. }
  1. 自定义登陆处理
点击查看代码
  1. package com.xiaostudy.security.config;
  2. import com.xiaostudy.common.redis.RedisService;
  3. import com.xiaostudy.common.utils.DESUtils;
  4. import com.xiaostudy.common.utils.StringUtils;
  5. import com.xiaostudy.security.entity.MyUserDetails;
  6. import com.xiaostudy.security.entity.UserEentity;
  7. import com.xiaostudy.security.service.UserService;
  8. import com.xiaostudy.security.utils.JwtTokenUtils;
  9. import org.slf4j.Logger;
  10. import org.slf4j.LoggerFactory;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.context.annotation.Configuration;
  13. import org.springframework.security.authentication.*;
  14. import org.springframework.security.core.Authentication;
  15. import org.springframework.security.core.context.SecurityContextHolder;
  16. import org.springframework.security.core.userdetails.User;
  17. import org.springframework.security.core.userdetails.UserDetails;
  18. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  19. import org.springframework.security.crypto.password.PasswordEncoder;
  20. import org.springframework.util.ObjectUtils;
  21. import reactor.core.publisher.Mono;
  22. // 自定义处理登陆
  23. @Configuration
  24. public class MyReactiveAuthenticationManager implements ReactiveAuthenticationManager {
  25. private static final Logger LOGGER = LoggerFactory.getLogger(MyReactiveAuthenticationManager.class);
  26. /**
  27. * @see BeanConfig#passwordEncoder()
  28. */
  29. @Autowired
  30. private PasswordEncoder passwordEncoder;
  31. @Autowired
  32. private UserService userService;
  33. @Autowired
  34. private RedisService redisService;
  35. @Override
  36. public Mono<Authentication> authenticate(Authentication authentication) {
  37. LOGGER.info("自定义处理登陆----MyReactiveAuthenticationManager.........");
  38. //获取输入的用户名
  39. String username = authentication.getName();
  40. //获取输入的明文
  41. String rawPassword = (String) authentication.getCredentials();
  42. MyUserDetails myUserDetails = null;
  43. String verifyCode = null;
  44. String ipAddr = null;
  45. if (authentication instanceof MyUserDetails) {
  46. myUserDetails = (MyUserDetails) authentication;
  47. username = myUserDetails.getUsername();
  48. rawPassword = myUserDetails.getPassword();
  49. verifyCode = myUserDetails.getVerifyCode();
  50. ipAddr = myUserDetails.getIpAddr();
  51. String email = myUserDetails.getEmail();
  52. if (!ObjectUtils.isEmpty(email)) {
  53. return this.authenticateEmail(email, verifyCode, ipAddr);
  54. }
  55. } else if (authentication instanceof UsernamePasswordAuthenticationToken) {
  56. UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;
  57. // TODO 不是的话要处理
  58. username = (String) authenticationToken.getPrincipal();
  59. rawPassword = (String) authenticationToken.getCredentials();
  60. myUserDetails = MyUserDetails.unauthenticated(username, rawPassword);
  61. }
  62. if (null != ipAddr) {
  63. if (ObjectUtils.isEmpty(verifyCode)) {
  64. return Mono.error(new DisabledException("请填写验证码!"));
  65. }
  66. String s = redisService.getCacheObject(ipAddr);
  67. if (ObjectUtils.isEmpty(s)) {
  68. return Mono.error(new DisabledException("验证码过期,请重新获取验证码!"));
  69. }
  70. if (!s.equals(verifyCode) && !s.equals(verifyCode.toLowerCase())) {
  71. return Mono.error(new DisabledException("验证码有误,请重新输入!"));
  72. }
  73. }
  74. try {
  75. if (!ObjectUtils.isEmpty(username)) {
  76. username = DESUtils.decode(username);
  77. }
  78. if (!ObjectUtils.isEmpty(rawPassword)) {
  79. rawPassword = DESUtils.decode(rawPassword);
  80. }
  81. } catch (Exception e) {
  82. LOGGER.error("解密用户密码出错!");
  83. return Mono.error(new DisabledException("解密用户密码出错!"));
  84. }
  85. if ((ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(rawPassword))) {
  86. return Mono.error(new DisabledException("请填写用户名或密码"));
  87. }
  88. UserDetails user = null;
  89. UserEentity userEentity = null;
  90. try {
  91. userEentity = userService.selectUserByNameDb1(username);
  92. if (ObjectUtils.isEmpty(userEentity)) {
  93. return Mono.error(new UsernameNotFoundException("系统无此用户,请先注册!"));
  94. }
  95. Integer errorCount = userEentity.getErrorCount();
  96. if (!ObjectUtils.isEmpty(errorCount) && JwtTokenUtils.LOGIN_ERROR_COUNT == errorCount) {
  97. return Mono.error(new DisabledException("登陆异常次数大于" + JwtTokenUtils.LOGIN_ERROR_COUNT));
  98. }
  99. User.UserBuilder userBuilder = User.builder().passwordEncoder(passwordEncoder::encode)
  100. .username(userEentity.getUsername())
  101. .password(userEentity.getPassword());
  102. String role = userEentity.getRole();
  103. if (!ObjectUtils.isEmpty(role)) {
  104. userBuilder.roles(role);
  105. }
  106. String url = userEentity.getUrl();
  107. if (!ObjectUtils.isEmpty(url)) {
  108. userBuilder.authorities(url);
  109. }
  110. if (ObjectUtils.isEmpty(role) && ObjectUtils.isEmpty(url)) {
  111. userBuilder.authorities(StringUtils.DEFAULT_INDEX_HTML_1);
  112. }
  113. user = userBuilder.build();
  114. } catch (UsernameNotFoundException ufe) {
  115. return Mono.error(ufe);
  116. }
  117. if (!user.isEnabled()) {
  118. return Mono.error(new DisabledException("该账户已被禁用,请联系管理员"));
  119. } else if (!user.isAccountNonLocked()) {
  120. return Mono.error(new LockedException("该账号已被锁定"));
  121. } else if (!user.isAccountNonExpired()) {
  122. return Mono.error(new AccountExpiredException("该账号已过期,请联系管理员"));
  123. } else if (!user.isCredentialsNonExpired()) {
  124. return Mono.error(new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录"));
  125. }
  126. //验证密码
  127. if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
  128. userService.loginPasswordErrorAdd(username);
  129. return Mono.error(new BadCredentialsException("密码错误:" + username));
  130. }
  131. final Authentication authentication1 = new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
  132. // TODO WebFlux方式默认没有放到context中,需要手动放入
  133. SecurityContextHolder.getContext().setAuthentication(authentication1);
  134. return Mono.just(authentication1);
  135. }
  136. /**
  137. * 自定义处理登陆----邮箱登陆
  138. *
  139. * @param email
  140. * @param verifyCode
  141. * @param ipAddr
  142. * @return reactor.core.publisher.Mono<org.springframework.security.core.Authentication>
  143. * @author liwei
  144. */
  145. public Mono<Authentication> authenticateEmail(String email, String verifyCode, String ipAddr) {
  146. LOGGER.info("自定义处理登陆----邮箱登陆.........");
  147. if (ObjectUtils.isEmpty(ipAddr)) {
  148. return Mono.error(new DisabledException("系统处理邮箱出错!"));
  149. }
  150. if (ObjectUtils.isEmpty(verifyCode)) {
  151. return Mono.error(new DisabledException("请填写验证码!"));
  152. }
  153. String s = redisService.getCacheObject(ipAddr);
  154. if (ObjectUtils.isEmpty(s)) {
  155. return Mono.error(new DisabledException("验证码过期,请重新获取验证码!"));
  156. }
  157. if (!s.equals(verifyCode) && !s.equals(verifyCode.toLowerCase())) {
  158. return Mono.error(new DisabledException("验证码有误,请重新输入!"));
  159. }
  160. redisService.deleteObject(ipAddr);
  161. try {
  162. if (!ObjectUtils.isEmpty(email)) {
  163. email = DESUtils.decode(email);
  164. }
  165. } catch (Exception e) {
  166. LOGGER.error("解密邮箱出错!");
  167. return Mono.error(new DisabledException("解密邮箱出错!"));
  168. }
  169. if (ObjectUtils.isEmpty(email)) {
  170. return Mono.error(new DisabledException("请填写邮箱"));
  171. }
  172. UserDetails user;
  173. UserEentity userEentity;
  174. try {
  175. userEentity = userService.selectUserByEmailDb1(email);
  176. if (ObjectUtils.isEmpty(userEentity)) {
  177. return Mono.error(new DisabledException("系统无此邮箱,不支持邮箱注册"));
  178. }
  179. User.UserBuilder userBuilder = User.builder().passwordEncoder(passwordEncoder::encode)
  180. .username(userEentity.getUsername())
  181. .password(userEentity.getPassword());
  182. String role = userEentity.getRole();
  183. if (!ObjectUtils.isEmpty(role)) {
  184. userBuilder.roles(role);
  185. }
  186. String url = userEentity.getUrl();
  187. if (!ObjectUtils.isEmpty(url)) {
  188. userBuilder.authorities(url);
  189. }
  190. if (ObjectUtils.isEmpty(role) && ObjectUtils.isEmpty(url)) {
  191. userBuilder.authorities(StringUtils.DEFAULT_INDEX_HTML_1);
  192. }
  193. user = userBuilder.build();
  194. } catch (UsernameNotFoundException ufe) {
  195. return Mono.error(ufe);
  196. }
  197. if (!user.isEnabled()) {
  198. return Mono.error(new DisabledException("该账户已被禁用,请联系管理员"));
  199. } else if (!user.isAccountNonLocked()) {
  200. return Mono.error(new LockedException("该账号已被锁定"));
  201. } else if (!user.isAccountNonExpired()) {
  202. return Mono.error(new AccountExpiredException("该账号已过期,请联系管理员"));
  203. } else if (!user.isCredentialsNonExpired()) {
  204. return Mono.error(new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录"));
  205. }
  206. userService.loginPasswordErrorAdd(userEentity.getUsername());
  207. final Authentication authentication1 = new UsernamePasswordAuthenticationToken(user, userEentity.getPassword(), user.getAuthorities());
  208. // TODO WebFlux方式默认没有放到context中,需要手动放入
  209. SecurityContextHolder.getContext().setAuthentication(authentication1);
  210. return Mono.just(authentication1);
  211. }
  212. }
  1. 自定义鉴权处理
点击查看代码
  1. package com.xiaostudy.security.config;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import com.xiaostudy.security.entity.UserEentity;
  4. import com.xiaostudy.security.service.UserService;
  5. import com.xiaostudy.security.utils.IpUtils;
  6. import com.xiaostudy.security.utils.JwtTokenUtils;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.http.HttpHeaders;
  12. import org.springframework.http.HttpMethod;
  13. import org.springframework.http.server.reactive.ServerHttpRequest;
  14. import org.springframework.security.authentication.CredentialsExpiredException;
  15. import org.springframework.security.authentication.DisabledException;
  16. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  17. import org.springframework.security.authorization.AuthorizationDecision;
  18. import org.springframework.security.authorization.ReactiveAuthorizationManager;
  19. import org.springframework.security.core.Authentication;
  20. import org.springframework.security.core.userdetails.User;
  21. import org.springframework.security.core.userdetails.UserDetails;
  22. import org.springframework.security.web.server.authorization.AuthorizationContext;
  23. import org.springframework.util.ObjectUtils;
  24. import org.springframework.web.server.ServerWebExchange;
  25. import reactor.core.publisher.Mono;
  26. // 自定义的鉴权服务,通过鉴权的才能继续访问某个请求。反应式授权管理器接口
  27. @Configuration
  28. public class MyReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
  29. private static final Logger LOGGER = LoggerFactory.getLogger(MyReactiveAuthorizationManager.class);
  30. @Autowired
  31. private UserService userService;
  32. /**
  33. * 实现权限验证判断
  34. */
  35. @Override
  36. public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
  37. LOGGER.info("---自定义的鉴权服务---MyReactiveAuthorizationManager---");
  38. ServerWebExchange exchange = authorizationContext.getExchange();
  39. ServerHttpRequest request = exchange.getRequest();
  40. String ipAddr = IpUtils.getIpAddr(request);
  41. if (!StringUtils.REQUEST_IP_WHITE_LIST.contains(ipAddr)) {
  42. LOGGER.debug("---自定义的鉴权服务---MyReactiveAuthorizationManager---非白名单IP不可访问");
  43. return Mono.error(new DisabledException(String.format("IP:%s,非白名单,不可访问" , ipAddr)));
  44. }
  45. // option请求默认放行,解决跨域问题
  46. if (request.getMethod().equals(HttpMethod.OPTIONS)) {
  47. LOGGER.debug("---自定义的鉴权服务---MyReactiveAuthorizationManager---跨域放行");
  48. return Mono.just(new AuthorizationDecision(true));
  49. }
  50. //请求资源
  51. final String url = request.getURI().getPath();
  52. // 白名单放行,不用登陆就可以访问
  53. for (String requestRulWhite : StringUtils.REQUEST_RUL_WHITE_S) {
  54. if ((requestRulWhite.endsWith(StringUtils.WILDCARD) && url.startsWith(requestRulWhite.substring(0, requestRulWhite.length() - StringUtils.WILDCARD.length())))
  55. || requestRulWhite.equals(url)) {
  56. LOGGER.debug("---自定义的鉴权服务---MyReactiveAuthorizationManager---白名单url放行");
  57. return Mono.just(new AuthorizationDecision(true));
  58. }
  59. }
  60. final HttpHeaders requestHeaders = request.getHeaders();
  61. final HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
  62. String authentication = JwtTokenUtils.getCookieAuthentication(requestHeaders);
  63. boolean tokenExpired = JwtTokenUtils.checkTokenAndRefreshToken(responseHeaders, authentication);
  64. if (!tokenExpired) {
  65. LOGGER.warn("token过期");
  66. return Mono.error(new CredentialsExpiredException("token过期,请重新登陆"));
  67. } else {
  68. LOGGER.debug("token有效");
  69. }
  70. return authenticationMono.map(auth ->
  71. new AuthorizationDecision(this.checkAuthorities(auth, url))
  72. ).defaultIfEmpty(
  73. new AuthorizationDecision(defaultIsToken(authentication, url))
  74. // new AuthorizationDecision(false)
  75. );
  76. }
  77. // 只有token情况下处理
  78. private boolean defaultIsToken(String token, String url) {
  79. if (ObjectUtils.isEmpty(token)) {
  80. return false;
  81. }
  82. String username = JwtTokenUtils.getUsernameFromToken(token);
  83. return this.checkAuthorities(username, url);
  84. }
  85. //权限校验,指定的url需要对应的角色,不指定的登陆成功就可以访问
  86. private boolean checkAuthorities(Authentication auth, String url) {
  87. if (ObjectUtils.isEmpty(auth)) {
  88. return false;
  89. }
  90. UserDetails principal = (UserDetails) auth.getPrincipal();
  91. if (ObjectUtils.isEmpty(principal)) {
  92. return false;
  93. }
  94. return this.checkAuthorities(principal.getUsername(), url);
  95. }
  96. //权限校验,指定的url需要对应的角色,不指定的登陆成功就可以访问
  97. private boolean checkAuthorities(String username, String url) {
  98. LOGGER.info("---自定义的鉴权服务---url:{}---" , url);
  99. if (ObjectUtils.isEmpty(username)) {
  100. return false;
  101. }
  102. UserEentity userEentity = userService.selectUserByNameDb1(username);
  103. if (ObjectUtils.isEmpty(userEentity)) {
  104. return false;
  105. }
  106. LOGGER.info("访问的URI是:{},用户信息:{}" , url, username);
  107. String role = userEentity.getRole();
  108. if ("/web/webLogin/user1".equals(url)) {
  109. return "3".equals(role);
  110. }
  111. if ("/web/webLogin/useri".equals(url)) {
  112. return "k".equals(role);
  113. }
  114. if ("/web/webLogin/usera".equals(url)) {
  115. return "c".equals(role) || "k".equals(role);
  116. }
  117. // 非指定接口,只要登陆都有权限
  118. return true;
  119. }
  120. }
  1. 自定义处理未登陆无访问权限的返回结果
点击查看代码
  1. package com.xiaostudy.security.handler;
  2. import io.netty.util.CharsetUtil;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.core.io.buffer.DataBuffer;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.MediaType;
  8. import org.springframework.http.server.reactive.ServerHttpResponse;
  9. import org.springframework.security.core.AuthenticationException;
  10. import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
  11. import org.springframework.stereotype.Component;
  12. import org.springframework.web.server.ServerWebExchange;
  13. import reactor.core.publisher.Flux;
  14. import reactor.core.publisher.Mono;
  15. // 未登陆无访问权限的返回结果
  16. @Component
  17. public class AuthEntryPointExceptionHandler extends HttpBasicServerAuthenticationEntryPoint {
  18. private static final Logger LOGGER = LoggerFactory.getLogger(AuthEntryPointExceptionHandler.class);
  19. @Override
  20. public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
  21. LOGGER.info("未登陆无访问权限---{}--AuthEntryPointExceptionHandler.........", exchange.getRequest().getURI().getPath());
  22. ServerHttpResponse response = exchange.getResponse();
  23. response.setStatusCode(HttpStatus.OK);
  24. String jsonString = "{\"code\":200,\"status\":4,\"msg\":\"您未登陆或登陆已过期,请先登陆!\"}";
  25. response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
  26. DataBuffer wrap = exchange.getResponse().bufferFactory().wrap(jsonString.getBytes(CharsetUtil.UTF_8));
  27. return exchange.getResponse().writeWith(Flux.just(wrap));
  28. }
  29. }
  1. 自定义登出成功后操作
点击查看代码
  1. package com.xiaostudy.security.handler;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.security.core.Authentication;
  7. import org.springframework.security.web.server.DefaultServerRedirectStrategy;
  8. import org.springframework.security.web.server.ServerRedirectStrategy;
  9. import org.springframework.security.web.server.WebFilterExchange;
  10. import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
  11. import reactor.core.publisher.Mono;
  12. import java.net.URI;
  13. // 成功登出实现类
  14. @Configuration
  15. public class MyRedirectServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {
  16. private static final Logger LOGGER = LoggerFactory.getLogger(MyRedirectServerLogoutSuccessHandler.class);
  17. private URI logoutSuccessUrl = URI.create(StringUtils.DEFAULT_LOGOUT_SUCCESS_URL);
  18. private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
  19. public MyRedirectServerLogoutSuccessHandler() {
  20. }
  21. @Override
  22. public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
  23. LOGGER.info("成功登出实现类----MyRedirectServerLogoutSuccessHandler.........");
  24. return this.redirectStrategy.sendRedirect(exchange.getExchange(), this.logoutSuccessUrl);
  25. }
  26. }
  1. 自定义处理登录失败或其他异常访问调用
点击查看代码
  1. package com.xiaostudy.security.handler;
  2. import io.netty.util.CharsetUtil;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.core.io.buffer.DataBuffer;
  6. import org.springframework.http.MediaType;
  7. import org.springframework.http.server.reactive.ServerHttpResponse;
  8. import org.springframework.security.authentication.*;
  9. import org.springframework.security.core.AuthenticationException;
  10. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  11. import org.springframework.security.web.server.WebFilterExchange;
  12. import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
  13. import org.springframework.stereotype.Component;
  14. import reactor.core.publisher.Mono;
  15. // 登录失败或其他异常访问调用的自定义处理类
  16. @Component
  17. public class MyServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
  18. private static final Logger LOGGER = LoggerFactory.getLogger(MyServerAuthenticationFailureHandler.class);
  19. private static final String USER_NOT_EXISTS = "用户不存在,请先注册!";
  20. private static final String USERNAME_PASSWORD_ERROR = "用户或密码错误!";
  21. private static final String USER_LOCKED = "用户锁定!";
  22. private static final String USER_ACCOUNT_EXPIRED = "账号已过期!";
  23. private static final String USER_CREDENTIALS_EXPIRE = "票据已过期!";
  24. @Override
  25. public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
  26. LOGGER.info("登录失败时调用的自定义处理类----MyServerAuthenticationFailureHandler.........");
  27. ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
  28. response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
  29. if (exception instanceof UsernameNotFoundException) {
  30. return writeErrorMessage(response, USER_NOT_EXISTS);
  31. } else if (exception instanceof BadCredentialsException) {
  32. return writeErrorMessage(response, USERNAME_PASSWORD_ERROR);
  33. } else if (exception instanceof LockedException) {
  34. return writeErrorMessage(response, USER_LOCKED);
  35. } else if (exception instanceof AccountExpiredException) {
  36. return writeErrorMessage(response, USER_ACCOUNT_EXPIRED);
  37. } else if (exception instanceof CredentialsExpiredException) {
  38. return writeErrorMessage(response, USER_CREDENTIALS_EXPIRE);
  39. } else if (exception instanceof DisabledException) {
  40. return writeErrorMessage(response, "不可访问," + exception.getMessage());
  41. }
  42. return writeErrorMessage(response, exception.getMessage());
  43. }
  44. private Mono<Void> writeErrorMessage(ServerHttpResponse response, String message) {
  45. String jsonString = String.format("{\"code\":200,\"status\":1,\"msg\":\"%s\"}", message);
  46. DataBuffer buffer = response.bufferFactory().wrap(jsonString.getBytes(CharsetUtil.UTF_8));
  47. return response.writeWith(Mono.just(buffer));
  48. }
  49. }
  1. 自定义处理登陆成功后返回结果
点击查看代码
  1. package com.xiaostudy.security.handler;
  2. import com.xiaostudy.security.utils.JwtTokenUtils;
  3. import io.netty.util.CharsetUtil;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.core.io.buffer.DataBuffer;
  7. import org.springframework.http.HttpHeaders;
  8. import org.springframework.http.HttpStatus;
  9. import org.springframework.http.MediaType;
  10. import org.springframework.http.server.reactive.ServerHttpResponse;
  11. import org.springframework.security.core.Authentication;
  12. import org.springframework.security.core.userdetails.UserDetails;
  13. import org.springframework.security.web.server.WebFilterExchange;
  14. import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
  15. import org.springframework.stereotype.Component;
  16. import reactor.core.publisher.Mono;
  17. // 登录成功时调用的自定义处理类
  18. @Component
  19. public class MyServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
  20. private static final Logger LOGGER = LoggerFactory.getLogger(MyServerAuthenticationSuccessHandler.class);
  21. @Override
  22. public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
  23. LOGGER.info("登录成功时调用的自定义处理类----MyServerAuthenticationSuccessHandler.........");
  24. // 登录成功后可以放入一些参数到session中
  25. ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
  26. response.setStatusCode(HttpStatus.OK);
  27. HttpHeaders headers = response.getHeaders();
  28. UserDetails principal = (UserDetails) authentication.getPrincipal();
  29. String username = principal.getUsername();
  30. String token = JwtTokenUtils.generateToken(username, null);
  31. headers.set(JwtTokenUtils.AUTHENTICATION, String.format("%s%s", JwtTokenUtils.BASIC_EMPTY, token));
  32. String jsonString = String.format("{\"code\":200,\"status\":0,\"msg\":\"%s您登陆成功!\"}", username);
  33. headers.setContentType(MediaType.APPLICATION_JSON);
  34. DataBuffer buffer = response.bufferFactory().wrap(jsonString.getBytes(CharsetUtil.UTF_8));
  35. return response.writeWith(Mono.just(buffer));
  36. }
  37. }
  1. 自定义处理登陆后无权限访问返回结果
点击查看代码
  1. package com.xiaostudy.security.handler;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import com.xiaostudy.security.utils.JwtTokenUtils;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.MediaType;
  8. import org.springframework.http.server.reactive.ServerHttpResponse;
  9. import org.springframework.security.access.AccessDeniedException;
  10. import org.springframework.security.core.Authentication;
  11. import org.springframework.security.core.context.SecurityContextHolder;
  12. import org.springframework.security.core.userdetails.UserDetails;
  13. import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
  14. import org.springframework.stereotype.Component;
  15. import org.springframework.util.ObjectUtils;
  16. import org.springframework.web.server.ServerWebExchange;
  17. import reactor.core.publisher.Flux;
  18. import reactor.core.publisher.Mono;
  19. import java.nio.charset.StandardCharsets;
  20. // 无权限访问被拒绝时的自定义处理器。如不自己处理,默认返回403错误<br>
  21. @Component
  22. public class MyWebFluxServerAccessDeniedHandler implements ServerAccessDeniedHandler {
  23. private static final Logger LOGGER = LoggerFactory.getLogger(MyWebFluxServerAccessDeniedHandler.class);
  24. @Override
  25. public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
  26. LOGGER.info("无权限访问被拒绝时的自定义处理器----MyAccessDeniedHandlerWebFlux.........");
  27. String username = JwtTokenUtils.getCookieUsername(exchange.getRequest().getHeaders());
  28. if (ObjectUtils.isEmpty(username)) {
  29. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  30. if (!ObjectUtils.isEmpty(authentication)) {
  31. UserDetails userDetails = (UserDetails) authentication.getPrincipal();
  32. if (!ObjectUtils.isEmpty(userDetails)) {
  33. username = userDetails.getUsername();
  34. }
  35. }
  36. }
  37. if (null == username) {
  38. username = StringUtils.EMPTY;
  39. }
  40. String jsonString = String.format("{\"code\":200,\"status\":3,\"msg\":\"%s您无此资源的访问权限!\"}" , username, exchange.getRequest().getURI().getPath());
  41. ServerHttpResponse response = exchange.getResponse();
  42. response.setStatusCode(HttpStatus.OK);
  43. response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
  44. return response.writeAndFlushWith(Flux.just(Flux.just(response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8)))));
  45. }
  46. }
  1. 重写存储认证信息,实时修改用户session的过期时间
点击查看代码
  1. package com.xiaostudy.security.filter;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.beans.factory.annotation.Value;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.http.server.reactive.ServerHttpRequest;
  7. import org.springframework.security.core.context.SecurityContext;
  8. import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
  9. import org.springframework.web.server.ServerWebExchange;
  10. import reactor.core.publisher.Mono;
  11. import java.time.Duration;
  12. // 重写存储认证信息,修改session默认时效和更新会话时间
  13. @Configuration
  14. public class MyWebSessionServerSecurityContextRepository extends WebSessionServerSecurityContextRepository {
  15. private static final Logger LOGGER = LoggerFactory.getLogger(MyWebSessionServerSecurityContextRepository.class);
  16. @Value("${session.timeout}")
  17. private Long timeout;
  18. @Override
  19. public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
  20. // 只有登陆时执行,并且在load()执行之后
  21. LOGGER.info("存储认证信息---save---url:{}", exchange.getRequest().getURI().getPath());
  22. return exchange.getSession()
  23. .doOnNext(session -> {
  24. if (context == null) {
  25. session.getAttributes().remove(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
  26. } else {
  27. session.getAttributes().put(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
  28. // 在这里设置过期时间 单位使用Duration类中的定义 有秒、分、天等
  29. session.setMaxIdleTime(Duration.ofSeconds(timeout));
  30. }
  31. })
  32. .flatMap(session -> session.changeSessionId());
  33. }
  34. @Override
  35. public Mono<SecurityContext> load(ServerWebExchange exchange) {
  36. ServerHttpRequest request = exchange.getRequest();
  37. String url = request.getURI().getPath();
  38. LOGGER.info("存储认证信息---load---url:{}", url);
  39. return exchange.getSession().flatMap((session) -> {
  40. SecurityContext context = session.getAttribute(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
  41. if (context == null) {
  42. session.getAttributes().remove(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
  43. } else {
  44. session.getAttributes().put(super.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
  45. // 在这里设置过期时间 单位使用Duration类中的定义 有秒、分、天等
  46. session.setMaxIdleTime(Duration.ofSeconds(timeout));
  47. }
  48. return Mono.justOrEmpty(context);
  49. });
  50. }
  51. }
  1. 主要过滤配置类
点击查看代码
  1. package com.xiaostudy.security.config;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import com.xiaostudy.security.filter.MyWebSessionServerSecurityContextRepository;
  4. import com.xiaostudy.security.handler.AuthEntryPointExceptionHandler;
  5. import com.xiaostudy.security.handler.MyServerAuthenticationFailureHandler;
  6. import com.xiaostudy.security.handler.MyServerAuthenticationSuccessHandler;
  7. import com.xiaostudy.security.handler.MyWebFluxServerAccessDeniedHandler;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.context.annotation.Bean;
  12. import org.springframework.context.annotation.Configuration;
  13. import org.springframework.http.HttpMethod;
  14. import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
  15. import org.springframework.security.config.web.server.ServerHttpSecurity;
  16. import org.springframework.security.web.server.SecurityWebFilterChain;
  17. import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
  18. import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
  19. import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
  20. import org.springframework.web.server.WebFilter;
  21. import java.util.Iterator;
  22. @Configuration
  23. @EnableWebFluxSecurity
  24. public class SecurityWebFluxConfig {
  25. private static final Logger LOG = LoggerFactory.getLogger(SecurityWebFluxConfig.class);
  26. @Autowired
  27. private MyReactiveAuthorizationManager reactiveAuthorizationManager;
  28. @Autowired
  29. private AuthEntryPointExceptionHandler serverAuthenticationEntryPoint;
  30. @Autowired
  31. private MyServerAuthenticationSuccessHandler myServerAuthenticationSuccessHandler;
  32. @Autowired
  33. private MyServerAuthenticationFailureHandler myServerAuthenticationFailureHandler;
  34. @Autowired
  35. private MyWebFluxServerAccessDeniedHandler myWebFluxServerAccessDeniedHandler;
  36. @Autowired
  37. private ServerLogoutSuccessHandler logoutSuccessHandler;
  38. @Autowired
  39. private MyWebSessionServerSecurityContextRepository myWebSessionServerSecurityContextRepository;
  40. @Autowired
  41. private MyServerFormLoginAuthenticationConverter myServerFormLoginAuthenticationConverter;
  42. @Autowired
  43. private MyReactiveAuthenticationManager myReactiveAuthenticationManager;
  44. // 主要过滤配置类
  45. @Bean
  46. public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
  47. LOG.info("加载security 权限配置....");
  48. http
  49. // .headers()
  50. // .cors()
  51. // 关闭csrf
  52. .csrf().disable()
  53. // 存储认证信息,这里修改session时效
  54. .securityContextRepository(myWebSessionServerSecurityContextRepository)
  55. // 设置登陆地址,如果是前后端分离,就不用设置,前端处理。
  56. .formLogin().loginPage(StringUtils.DEFAULT_LOGOUT_HTML_1)
  57. // 登陆请求方式和接口
  58. .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, StringUtils.DEFAULT_LOGIN_URL_1, StringUtils.DEFAULT_LOGIN_MAIL_URL_1))
  59. // 处理登陆
  60. .authenticationManager(myReactiveAuthenticationManager)
  61. // 登录成功handler
  62. .authenticationSuccessHandler(myServerAuthenticationSuccessHandler)
  63. // 登陆失败handler
  64. .authenticationFailureHandler(myServerAuthenticationFailureHandler)
  65. // 关闭默认登录验证
  66. .and().httpBasic().disable()
  67. // .requestCache()
  68. // 登出,设置登出请求类型和URL
  69. .logout().requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, StringUtils.DEFAULT_LOGOUT_URL_1))
  70. // 登出成功后自定义处理
  71. .logoutSuccessHandler(logoutSuccessHandler)
  72. // 未登陆无访问权限handler
  73. .and().exceptionHandling().authenticationEntryPoint(serverAuthenticationEntryPoint)
  74. // 登陆无访问权限
  75. .and().exceptionHandling().accessDeniedHandler(myWebFluxServerAccessDeniedHandler)
  76. // 自定义鉴权
  77. // .and().authorizeExchange().pathMatchers(StringUtils.REQUEST_RUL_WHITE_S).permitAll()
  78. .and().authorizeExchange().anyExchange().access(reactiveAuthorizationManager)
  79. // .anyExchange().authenticated()
  80. ;
  81. SecurityWebFilterChain chain = http.build();
  82. Iterator<WebFilter> weIterable = chain.getWebFilters().toIterable().iterator();
  83. while (weIterable.hasNext()) {
  84. WebFilter f = weIterable.next();
  85. if (f instanceof AuthenticationWebFilter) {
  86. AuthenticationWebFilter webFilter = (AuthenticationWebFilter) f;
  87. //将自定义的AuthenticationConverter添加到过滤器中
  88. webFilter.setServerAuthenticationConverter(myServerFormLoginAuthenticationConverter);
  89. }
  90. }
  91. return chain;
  92. }
  93. }

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

  1. 邮箱实体类
点击查看代码
  1. package com.xiaostudy.security.email;
  2. import java.io.File;
  3. public class MailEntity {
  4. /**
  5. * 主题
  6. */
  7. private String subject;
  8. /**
  9. * 内容
  10. */
  11. private String content;
  12. /**
  13. * 邮箱
  14. */
  15. private String toAccount;
  16. /**
  17. * 附件
  18. */
  19. private File attachmentFile;
  20. /**
  21. * 附件文件名
  22. */
  23. private String attachmentFileName;
  24. public String getSubject() {
  25. return subject;
  26. }
  27. public void setSubject(String subject) {
  28. this.subject = subject;
  29. }
  30. public String getContent() {
  31. return content;
  32. }
  33. public void setContent(String content) {
  34. this.content = content;
  35. }
  36. public String getToAccount() {
  37. return toAccount;
  38. }
  39. public void setToAccount(String toAccount) {
  40. this.toAccount = toAccount;
  41. }
  42. public File getAttachmentFile() {
  43. return attachmentFile;
  44. }
  45. public void setAttachmentFile(File attachmentFile) {
  46. this.attachmentFile = attachmentFile;
  47. }
  48. public String getAttachmentFileName() {
  49. return attachmentFileName;
  50. }
  51. public void setAttachmentFileName(String attachmentFileName) {
  52. this.attachmentFileName = attachmentFileName;
  53. }
  54. }
  1. 邮箱工具类
点击查看代码
  1. package com.xiaostudy.security.email;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.boot.autoconfigure.mail.MailProperties;
  4. import org.springframework.mail.SimpleMailMessage;
  5. import org.springframework.mail.javamail.JavaMailSender;
  6. import org.springframework.mail.javamail.MimeMessageHelper;
  7. import org.springframework.stereotype.Component;
  8. import javax.mail.MessagingException;
  9. import javax.mail.internet.MimeMessage;
  10. @Component
  11. public class MailUtils {
  12. @Autowired
  13. private MailProperties mailProperties;
  14. @Autowired
  15. private JavaMailSender javaMailSender;
  16. /**
  17. * 发送邮件,里面有判断是否发文件
  18. */
  19. public void sendMail(MailEntity mailEntity) {
  20. if (null != mailEntity) {
  21. if (null != mailEntity.getAttachmentFile() && mailEntity.getAttachmentFile().exists()) {
  22. if (null == mailEntity.getAttachmentFileName()) {
  23. mailEntity.setAttachmentFileName(mailEntity.getAttachmentFile().getName());
  24. }
  25. sendMailAttachment(mailEntity);
  26. } else {
  27. sendSimpleMail(mailEntity);
  28. }
  29. }
  30. }
  31. /**
  32. * 发送邮件,这里只发内容,不发文件
  33. */
  34. public void sendSimpleMail(MailEntity mailEntity) {
  35. SimpleMailMessage mimeMessage = new SimpleMailMessage();
  36. mimeMessage.setFrom(mailProperties.getUsername());
  37. mimeMessage.setTo(mailEntity.getToAccount());
  38. mimeMessage.setSubject(mailEntity.getSubject());
  39. mimeMessage.setText(mailEntity.getContent());
  40. javaMailSender.send(mimeMessage);
  41. }
  42. /**
  43. * 发送邮件-附件邮件
  44. *
  45. * @param mailEntity
  46. */
  47. public boolean sendMailAttachment(MailEntity mailEntity) {
  48. try {
  49. MimeMessage mimeMessage = javaMailSender.createMimeMessage();
  50. MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
  51. helper.setFrom(mailProperties.getUsername());
  52. helper.setTo(mailEntity.getToAccount());
  53. helper.setSubject(mailEntity.getSubject());
  54. helper.setText(mailEntity.getContent(), true);
  55. // 增加附件名称和附件
  56. helper.addAttachment(mailEntity.getAttachmentFileName(), mailEntity.getAttachmentFile());
  57. javaMailSender.send(mimeMessage);
  58. return true;
  59. } catch (MessagingException e) {
  60. e.printStackTrace();
  61. return false;
  62. }
  63. }
  64. }
  1. 验证码接口
点击查看代码
  1. package com.xiaostudy.security.controller;
  2. import com.xiaostudy.common.redis.RedisService;
  3. import com.xiaostudy.common.utils.DESUtils;
  4. import com.xiaostudy.common.utils.StringUtils;
  5. import com.xiaostudy.common.utils.VerifyCodeUtils;
  6. import com.xiaostudy.security.email.MailEntity;
  7. import com.xiaostudy.security.email.MailUtils;
  8. import com.xiaostudy.security.utils.IpUtils;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.core.io.buffer.DataBuffer;
  11. import org.springframework.http.MediaType;
  12. import org.springframework.http.server.reactive.ServerHttpResponse;
  13. import org.springframework.util.ObjectUtils;
  14. import org.springframework.web.bind.annotation.RequestMapping;
  15. import org.springframework.web.bind.annotation.RestController;
  16. import org.springframework.web.server.ServerWebExchange;
  17. import reactor.core.publisher.Flux;
  18. import reactor.core.publisher.Mono;
  19. import java.io.ByteArrayOutputStream;
  20. import java.io.IOException;
  21. import java.util.concurrent.TimeUnit;
  22. @RestController
  23. @RequestMapping("security")
  24. public class VerifyCodeController {
  25. @Autowired
  26. private MailUtils mailUtils;
  27. @Autowired
  28. private RedisService redisService;
  29. @RequestMapping("/verifyCode")
  30. public Mono<Void> verifyCode(ServerWebExchange exchange) throws IOException {
  31. String code = VerifyCodeUtils.generateVerifyCode(4).toLowerCase();
  32. redisService.setCacheObject(IpUtils.getIpAddr(exchange.getRequest()), code, 60L, TimeUnit.SECONDS);
  33. ByteArrayOutputStream data = new ByteArrayOutputStream();
  34. VerifyCodeUtils.outputImage(100, 40, data, code);
  35. ServerHttpResponse response = exchange.getResponse();
  36. response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
  37. DataBuffer buffer = response.bufferFactory().wrap(data.toByteArray());
  38. return response.writeWith(Flux.just(buffer));
  39. }
  40. @RequestMapping("/sendMailVerifyCode")
  41. public Mono<String> sendMailVerifyCode(ServerWebExchange exchange) {
  42. exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
  43. return exchange.getFormData().map(data -> {
  44. String code = VerifyCodeUtils.generateVerifyCode(4).toLowerCase();
  45. redisService.setCacheObject(IpUtils.getIpAddr(exchange.getRequest()), code, 5L, TimeUnit.MINUTES);
  46. String email = data.getFirst(StringUtils.EMAIL);
  47. try {
  48. if (!ObjectUtils.isEmpty(email)) {
  49. email = DESUtils.decode(email);
  50. }
  51. } catch (Exception e) {
  52. return "{\"code\":200,\"status\":1,\"msg\":\"解密邮箱出错!\"}";
  53. }
  54. if (ObjectUtils.isEmpty(email)) {
  55. return "{\"code\":200,\"status\":1,\"msg\":\"请输入邮箱!\"}";
  56. }
  57. if (!VerifyCodeUtils.isEMail(email)) {
  58. return "{\"code\":200,\"status\":1,\"msg\":\"邮箱格式不对!\"}";
  59. }
  60. if ("xxxxx@163.com".equals(email)) {
  61. MailEntity mailEntity = new MailEntity();
  62. mailEntity.setToAccount(email);
  63. mailEntity.setSubject("登陆系统验证码");
  64. mailEntity.setContent(String.format("5分钟有效,您登陆的验证码是:%s" , code));
  65. mailUtils.sendMail(mailEntity);
  66. return "{\"code\":200,\"status\":0,\"msg\":\"验证码已发送至邮箱!\"}";
  67. }
  68. // TODO
  69. return "{\"code\":200,\"status\":1,\"msg\":\"测试,非自己邮箱不发!\"}";
  70. });
  71. }
  72. }
  1. 获取当前用户名、测试动态切换数据源接口
点击查看代码
  1. package com.xiaostudy.security.controller;
  2. import com.xiaostudy.common.utils.StringUtils;
  3. import com.xiaostudy.security.datasources.annotation.DataSource;
  4. import com.xiaostudy.security.datasources.enums.DataSourceNameEnum;
  5. import com.xiaostudy.security.entity.UserEentity;
  6. import com.xiaostudy.security.service.UserService;
  7. import com.xiaostudy.security.utils.JwtTokenUtils;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.http.server.reactive.ServerHttpRequest;
  10. import org.springframework.security.core.Authentication;
  11. import org.springframework.security.core.context.SecurityContextHolder;
  12. import org.springframework.security.core.userdetails.UserDetails;
  13. import org.springframework.util.ObjectUtils;
  14. import org.springframework.web.bind.annotation.GetMapping;
  15. import org.springframework.web.bind.annotation.RequestMapping;
  16. import org.springframework.web.bind.annotation.RestController;
  17. import java.util.List;
  18. import java.util.stream.Collectors;
  19. @RestController
  20. @RequestMapping("user")
  21. public class UserController {
  22. @Autowired
  23. private UserService userService;
  24. @GetMapping("getCurrentUserName")
  25. public String getCurrentUserName(Authentication authentication, ServerHttpRequest request) {
  26. String username = JwtTokenUtils.getCookieUsername(request.getHeaders());
  27. if (!ObjectUtils.isEmpty(username)) {
  28. return username;
  29. }
  30. if (ObjectUtils.isEmpty(authentication)) {
  31. authentication = SecurityContextHolder.getContext().getAuthentication();
  32. }
  33. if (ObjectUtils.isEmpty(authentication)) {
  34. return null;
  35. }
  36. Object principal = authentication.getPrincipal();
  37. if (ObjectUtils.isEmpty(principal)) {
  38. return null;
  39. }
  40. if (principal instanceof UserDetails) {
  41. return ((UserDetails) principal).getUsername();
  42. } else if (principal instanceof String) {
  43. return (String) principal;
  44. }
  45. return null;
  46. }
  47. @DataSource(name = DataSourceNameEnum.FIRST)
  48. @GetMapping("testDataSource1")
  49. public String testDataSource1() {
  50. List<UserEentity> userEentities = userService.selectUserAll();
  51. if (ObjectUtils.isEmpty(userEentities)) {
  52. return null;
  53. }
  54. return userEentities.stream().map(UserEentity::getUsername).collect(Collectors.joining(StringUtils.COMMA));
  55. }
  56. @DataSource(name = DataSourceNameEnum.SECOND)
  57. @GetMapping("testDataSource2")
  58. public String testDataSource2() {
  59. List<UserEentity> userEentities = userService.selectUserAll();
  60. if (ObjectUtils.isEmpty(userEentities)) {
  61. return null;
  62. }
  63. return userEentities.stream().map(UserEentity::getUsername).collect(Collectors.joining(StringUtils.COMMA));
  64. }
  65. }

  1. 删除启动类

3. 创建Gateway服务

  1. 创建操作



  2. 父模块添加子模块

  1. <module>gateway</module>
  1. 修改pom.xml文件



点击查看代码
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>com.xiaostudy</groupId>
  7. <artifactId>SpringCloud202208</artifactId>
  8. <version>1.0-SNAPSHOT</version>
  9. <relativePath>../pom.xml</relativePath>
  10. </parent>
  11. <groupId>com.xiaostudy</groupId>
  12. <artifactId>gateway</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <name>gateway</name>
  15. <description>gateway</description>
  16. <dependencies>
  17. <dependency>
  18. <groupId>org.springframework.cloud</groupId>
  19. <artifactId>spring-cloud-starter-gateway</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.cloud</groupId>
  23. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  24. </dependency>
  25. <dependency>
  26. <groupId>com.xiaostudy</groupId>
  27. <artifactId>security</artifactId>
  28. <version>0.0.1-SNAPSHOT</version>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-test</artifactId>
  33. <scope>test</scope>
  34. </dependency>
  35. </dependencies>
  36. <build>
  37. <plugins>
  38. <plugin>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-maven-plugin</artifactId>
  41. </plugin>
  42. </plugins>
  43. </build>
  44. </project>
  1. 配置文件application.properties修改为application.yml,然后配置
点击查看代码
  1. server:
  2. port: '@gateway.port@'
  3. eureka:
  4. port: '@eureka.port@'
  5. ip: '@eureka.ip@'
  6. url-name: '@eureka.url.name@'
  7. instance:
  8. # 把本机IP注册到eureka而不是本机机器名
  9. preferIpAddress: true
  10. # 把本机IP注册到eureka,由下面参数组成
  11. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  12. client:
  13. serviceUrl:
  14. defaultZone: http://@eureka.user.name@:@eureka.user.password@@${eureka.ip}:${eureka.port}/${eureka.url-name}/
  15. spring:
  16. application:
  17. name: '@gateway.application.name@'
  18. cloud:
  19. loadbalancer:
  20. retry:
  21. # 关闭重试
  22. enabled: false
  23. gateway:
  24. routes:
  25. # 路由的id,没有规定规则但要求唯一,建议配合服务名
  26. - id: '@producer.application.name@'
  27. # 匹配后提供服务的路由地址
  28. uri: lb://@producer.application.name@
  29. predicates:
  30. - Path=/producer/** # 断言,路径相匹配的进行路由
  31. filters:
  32. # 去掉url一级前缀,例如http://localhost:9904/producer/test/getByName,等同于http://localhost:9904/test/getByName
  33. - StripPrefix=1
  34. - id: '@web.application.name@'
  35. # lb:协议表示开启负载均衡
  36. uri: lb://@web.application.name@
  37. predicates:
  38. - Path=/web/** #断言,路径相匹配的进行路由
  39. filters:
  40. - StripPrefix=1
  41. redis:
  42. # 默认值:localhost
  43. host: localhost
  44. # 默认值:6379
  45. port: 6379
  46. # 默认值:0
  47. database: 1
  48. lettuce:
  49. pool:
  50. # 连接池最大连接数(使用负值表示没有限制),默认值:8
  51. max-active: 20
  52. # 连接池中的最大空闲连接,默认值:8
  53. max-idle: 10
  54. #连接池中的最小空闲连接,默认值:0
  55. min-idle: 1
  56. # 连接池最大阻塞等待时间(使用负值表示没有限制),默认值:-1,单位:毫秒
  57. max-wait: 2000
  58. profiles:
  59. # 使用的配置文件后缀application-security.yml。一个或多个,中间英文逗号分开
  60. active: security
  1. 启动类添加注解

点击查看代码
  1. @ComponentScan(
  2. basePackages = {
  3. // 把security服务下的包交给spring管理
  4. "com.xiaostudy.security"
  5. , "com.xiaostudy.gateway"
  6. , "com.xiaostudy.common"
  7. }
  8. )
  9. @MapperScan("com.xiaostudy.security.mapper")
  1. 启动



  2. 注册中心看服务

4. feign模块添加gateway接口

  1. application-feign.yml添加配置

此时application-feign.yml
  1. producer:
  2. application:
  3. name: @producer.application.name@
  4. gateway:
  5. application:
  6. name: @gateway.application.name@
  7. feign:
  8. client:
  9. config:
  10. default:
  11. # 默认是1000
  12. connect-timeout: 5000
  13. read-timeout: 5000
  1. 添加gateway接口

点击查看代码
  1. package com.xiaostudy.feign.apis;
  2. import com.xiaostudy.feign.config.FeignConfig;
  3. import org.springframework.cloud.openfeign.FeignClient;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. @FeignClient(name = "${gateway.application.name}" , contextId = "GatewayServiceApis" , configuration = FeignConfig.class)
  6. public interface GatewayServiceApis {
  7. @GetMapping(value = "/user/getCurrentUserName")
  8. public String getCurrentUserName();
  9. @GetMapping(value = "/user/testDataSource1")
  10. public String testDataSource1();
  11. @GetMapping(value = "/user/testDataSource2")
  12. public String testDataSource2();
  13. }

5. webService简单登陆

  1. 注册请求类
点击查看代码
  1. package com.xiaostudy.webservice.entity;
  2. import java.io.Serializable;
  3. public class RegisterRequest implements Serializable {
  4. private static final Long serialVersionUID = 1L;
  5. private String userName;
  6. private String passWord;
  7. private String verifyCode;
  8. public String getUserName() {
  9. return userName;
  10. }
  11. public void setUserName(String userName) {
  12. this.userName = userName;
  13. }
  14. public String getPassWord() {
  15. return passWord;
  16. }
  17. public void setPassWord(String passWord) {
  18. this.passWord = passWord;
  19. }
  20. public String getVerifyCode() {
  21. return verifyCode;
  22. }
  23. public void setVerifyCode(String verifyCode) {
  24. this.verifyCode = verifyCode;
  25. }
  26. }
  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. 登陆-用户不存在



  4. 验证码错误



  5. 账号密码正确

  6. url权限控制



  7. 邮箱登陆





简单创建一个SpringCloud2021.0.3项目(二)的更多相关文章

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

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上三篇教程 3. 日志处理 1. 创建日志公共模块 2. Eureka引入日志模块 4. 到此的功能代码 5. 注册中心换成naco ...

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

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上俩篇教程 3. Gateway集成sentinel,网关层做熔断降级 1. 超时熔断降级 2. 异常熔断 3. 集成sentine ...

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

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 新建父模块和注册中心 1. 新建父模块 2. 新建注册中心Eureka 3. 新建配置中心Config 4. 新建两个业务服务 1. ...

  4. 【Vue-Cli3.0】【1】创建一个Vue-Cli3.0的项目

    最近在做爬虫,然后要爬好多数据,代码写完了,就让它在爬了.不想闲着就复习一下Vue吧! 开始开始! ***正式讲解之前 先下载一个node.js吧! 一.首先检查一下 版本 PS D:\徐孟林\D D ...

  5. 通过beego快速创建一个Restful风格API项目及API文档自动化

    通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...

  6. 通过beego快速创建一个Restful风格API项目及API文档自动化(转)

    通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...

  7. 如何使用maven建一个web3.0的项目

    使用eclipse手动建一个maven的web project可能会有版本不合适的情况,例如使用spring的websocket需要web3.0什么的,不全面的修改可能会出现各种红叉,甚是苦恼.我从我 ...

  8. 以sb7code为基础创建一个基本的OpenGL项目

      以sb7code为基础创建一个基本的OpenGL项目   从github上面下载sb7code代码: https://github.com/openglsuperbible/sb7code 打开H ...

  9. 简单创建一个完整的struts2框架小程序

    要完成一个struts2框架的搭建, 1.首先应该从官网上下载最新的jar包,网络连接:http://struts.apache.org/download.cgi#struts2514.1,选择下载F ...

随机推荐

  1. 技术分享 | App常见bug解析

    原文链接 功能Bug 内容显示错误 前端页面展示的内容有误. 这种错误的产生有两种可能 1.前端代码写的文案错误 2.接口返回值错误 功能错误 功能错误是在测试过程中最常见的类型之一,也就是产品的功能 ...

  2. ExtJS 布局-Card 布局(Card layout)

    更新记录: 2022年6月1日 开始. 2022年6月6日 发布. 1.说明 卡片布局类似牌堆,每次只有一个子组件可见,子组件几乎填满了整个容器.卡片布局常用于向导(Wizard)和选项卡(Tabs) ...

  3. UiPath Level3讲解

    匠厂出品,必属精品   Uipath中文社区qq交流群:465630324 uipath中文交流社区:https://uipathbbs.com RPA之家qq群:465620839 第一课--UiP ...

  4. Java开发学习(六)----DI依赖注入之setter及构造器注入解析

    一.DI依赖注入 首先来介绍下Spring中有哪些注入方式? 我们先来思考 向一个类中传递数据的方式有几种? 普通方法(set方法) 构造方法 依赖注入描述了在容器中建立bean与bean之间的依赖关 ...

  5. java: 程序包org.springframework.boot不存在

    如果你的settings中的maven配置没问题的话,尝试下面这个 在控制台输入  mvn idea:idea  重构一下

  6. 一文搞懂 Netty 发送数据全流程 | 你想知道的细节全在这里

    欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 在<Netty如何高效接收网络数据 ...

  7. Learning Latent Graph Representations for Relational VQA

    The key mechanism of transformer-based models is cross-attentions, which implicitly form graphs over ...

  8. c# SerialPort HEX there is no data received

    C#窗口程序进行串口通信,按照串口通信协议,设置com口,波特率,停止位,校验位,数据位,本地虚拟串口调试ok,但是和外设调试时,发送HEX模式数据命令,没有数据返回, 所以关键问题在于HEX模式,发 ...

  9. AI2(App Inventor 2) 离线版

    介绍 我们的目标:搭建一个本地多用户的App Inventor 2 服务器目的:课堂教学,社团活动,兴趣学习优势:管理权限(用户管理,账号切换,资源打包),网络链接速度快,拥有配套服务.注意:每次退出 ...

  10. DHCP原理及配置

    DHCP工作原理 集中的管理.分配IP地址,使client动态的获得IP地址.Gateway地址.DNS服务器地址等信息,并能够提升地址的使用率. 简单来说,DHCP就是一个不需要账号密码登录的.自动 ...