**说在前面:该项目是跟着B站一位大佬写的,不分享源码,支持项目付费 **
获取图形验证码
可以看到这里有2两种图形验证码,分为:
type=0:如上图下面那个,是完成操作后要进行注册的验证码
type=1: 如上图上面那个,是要发送邮箱之前的图形验证码。
这里在交互时,设置了session值,存放了两种图形验证码,便于后面进行用户输入情况和session值比对。
那什么是session呢?
(1)Session用于记录用户的状态。Session指的是一段时间内,单个客户端与Web服务器的一连串相关的交互过程。
(2)在一个Session中,客户可能会多次请求访问同一个资源,也有可能请求访问各种不同的服务器资源。
(3)Session是由服务器端创建的一个对象
可能涉及到的session操作
-
session.setAttribute(key,value)
类似hashmap,存放键值。
-
session.getAttribute(key)
根据key获取值,获得值的类型为object,要根据情况进行强转
-
session.removeAttribute(key)
删除该key和对应的值,但session这个对象还在
-
session.invalidate()
销毁了session对象
那如果是多服务器上部署这个项目,涉及多个session:
当系统部署到多台服务器时,由于每个服务器都有自己独立的内存空间,默认情况下 HttpSession 是无法在不同服务器之间共享的。这会导致用户在不同服务器上的请求无法正确识别其会话状态,例如用户在一台服务器上登录成功,但下一次请求被分配到另一台服务器时,该服务器无法获取到用户的登录状态。为了解决这个问题:
使用分布式缓存(如 Redis)是一种比较常用的解决方案。它将 Session 数据存储在 Redis 中,而不是存储在服务器的内存中。这样,所有服务器都可以通过 Redis 来获取和更新 Session 数据,从而实现了 Session 的共享。这种方案具有较好的扩展性和性能,而且可以避免会话复制带来的网络开销。
这里给出图形验证码是如何生成的:
public class CreateImageCode {// 图片的宽度。private int width = 160;// 图片的高度。private int height = 40;// 验证码字符个数private int codeCount = 4;// 验证码干扰线数private int lineCount = 20;// 验证码private String code = null;// 验证码图片Bufferprivate BufferedImage buffImg = null;Random random = new Random();public CreateImageCode() {creatImage();}public CreateImageCode(int width, int height) {this.width = width;this.height = height;creatImage();}public CreateImageCode(int width, int height, int codeCount) {this.width = width;this.height = height;this.codeCount = codeCount;creatImage();}public CreateImageCode(int width, int height, int codeCount, int lineCount) {this.width = width;this.height = height;this.codeCount = codeCount;this.lineCount = lineCount;creatImage();}// 生成图片private void creatImage() {int fontWidth = width / codeCount;// 字体的宽度int fontHeight = height - 5;// 字体的高度int codeY = height - 8;// 图像bufferbuffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics g = buffImg.getGraphics();//Graphics2D g = buffImg.createGraphics();// 设置背景色g.setColor(getRandColor(200, 250));g.fillRect(0, 0, width, height);// 设置字体//Font font1 = getFont(fontHeight);Font font = new Font("Fixedsys", Font.BOLD, fontHeight);g.setFont(font);// 设置干扰线for (int i = 0; i < lineCount; i++) {int xs = random.nextInt(width);int ys = random.nextInt(height);int xe = xs + random.nextInt(width);int ye = ys + random.nextInt(height);g.setColor(getRandColor(1, 255));g.drawLine(xs, ys, xe, ye);}// 添加噪点float yawpRate = 0.01f;// 噪声率int area = (int) (yawpRate * width * height);for (int i = 0; i < area; i++) {int x = random.nextInt(width);int y = random.nextInt(height);buffImg.setRGB(x, y, random.nextInt(255));}String str1 = randomStr(codeCount);// 得到随机字符this.code = str1;for (int i = 0; i < codeCount; i++) {String strRand = str1.substring(i, i + 1);g.setColor(getRandColor(1, 255));// g.drawString(a,x,y);// a为要画出来的东西,x和y表示要画的东西最左侧字符的基线位于此图形上下文坐标系的 (x, y) 位置处g.drawString(strRand, i * fontWidth + 3, codeY);}}// 得到随机字符private String randomStr(int n) {String str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";String str2 = "";int len = str1.length() - 1;double r;for (int i = 0; i < n; i++) {r = (Math.random()) * len;str2 = str2 + str1.charAt((int) r);}return str2;}// 得到随机颜色private Color getRandColor(int fc, int bc) {// 给定范围获得随机颜色if (fc > 255) fc = 255;if (bc > 255) bc = 255;int r = fc + random.nextInt(bc - fc);int g = fc + random.nextInt(bc - fc);int b = fc + random.nextInt(bc - fc);return new Color(r, g, b);}/*** 产生随机字体*/private Font getFont(int size) {Random random = new Random();Font font[] = new Font[5];font[0] = new Font("Ravie", Font.PLAIN, size);font[1] = new Font("Antique Olive Compact", Font.PLAIN, size);font[2] = new Font("Fixedsys", Font.PLAIN, size);font[3] = new Font("Wide Latin", Font.PLAIN, size);font[4] = new Font("Gill Sans Ultra Bold", Font.PLAIN, size);return font[random.nextInt(5)];}// 扭曲方法private void shear(Graphics g, int w1, int h1, Color color) {shearX(g, w1, h1, color);shearY(g, w1, h1, color);}private void shearX(Graphics g, int w1, int h1, Color color) {int period = random.nextInt(2);boolean borderGap = true;int frames = 1;int phase = random.nextInt(2);for (int i = 0; i < h1; i++) {double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);g.copyArea(0, i, w1, 1, (int) d, 0);if (borderGap) {g.setColor(color);g.drawLine((int) d, i, 0, i);g.drawLine((int) d + w1, i, w1, i);}}}private void shearY(Graphics g, int w1, int h1, Color color) {int period = random.nextInt(40) + 10; // 50;boolean borderGap = true;int frames = 20;int phase = 7;for (int i = 0; i < w1; i++) {double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);g.copyArea(i, 0, 1, h1, 0, (int) d);if (borderGap) {g.setColor(color);g.drawLine(i, (int) d, i, 0);g.drawLine(i, (int) d + h1, i, h1);}}}public void write(OutputStream sos) throws IOException {ImageIO.write(buffImg, "png", sos);sos.close();}public BufferedImage getBuffImg() {return buffImg;}public String getCode() {return code.toLowerCase();}
}
发邮箱前验证码
这段的实现逻辑:
根据传过来的参数,其中type 0:注册邮箱时 1:重置密码时(后续)
先进行发邮箱前的图形验证码校对,对应后,执行发送邮箱操作。
最后,清空一下session存的当前验证码。如果验证码在使用后不被清除,那么恶意用户可能会获取到该验证码,并在后续的请求中重复使用,从而绕过验证码的验证机制,对系统的安全性造成威胁。
这段逻辑就是发送验证码前检验该账号是否已经注册
发送邮箱后,如果重复发送发送邮箱的话,需要将之前的邮箱验证码设置为1:用过了
然后是发送邮箱的实现:
AOP切面实现参数校验
在前端传过来的各种参数,我们需要进行非空校验,最笨的方法就是if else判断,这样耗时耗力,很不适用,为了减少冗杂性判断为空代码,通过注解作用在参数上,实现切面类判断。
首先,创建拦截器:
第一个是对参数进行校验,@GlobalInterceptor 注解的主要目的是标记需要进行特定拦截处理的方法,其中 checkParams = true 表示需要对方法的参数进行校验。
而这个,是对某参数进行具体的校验:
然后就是切面设计:AOP 可以在方法执行前拦截方法调用,并对方法的参数进行校验。
注册
这段逻辑
首先进行注册前的图形验证码校对
然后执行注册操作
最后再清空session存的当前图形验证码。
service层实现:
再次检查是否邮箱已经注册,比对昵称唯一(项目要求唯一)
然后根据表中之前存的邮箱和邮箱验证码,跟现在填的进行比对 ,校验通过后,插入用户信息,其中用户网盘空间分配:已用空间0,总空间初始分配5MB.
其中校验对应:
分两种情况
1:邮箱和邮箱验证码不能对应起来,视为错误
2:邮箱验证码之前设置过15分钟内有效。一旦超时,视为无效,再次更改表,将这个邮箱验证码设置为1:已用过
System.currentTimeMillis():用来获取当前的总毫秒数
getTime():返回毫秒数
登录
这段的逻辑
同样检查图形验证码
然后设置了一个dto(含id昵称头像)来接收登录信息,设置session当前登录信息, 并将这个dto返回给VO
serviceImpl:
这段逻辑
通过邮箱比对是否该账号注册了或者密码对不上。不能分情况if,不让用户知道具体是哪个错了。
然后就算是它登录了,就要更新其登录时间,给dto的属性赋值
另外还查看是否是超级管理员,如果是,则dto的isAdmin设为true。
成功登陆后,给用户分配网盘空间。注册时是给user标注空间分配信息,现在设置的是方便在redis中存储
忘记密码
和注册逻辑相似,重置密码,主要就是更新一下password。而发送邮箱等业务是已经完成了的,才把emailCode传过来了。
修改密码
这个和忘记密码重置有区别,忘记密码是没登录进去时重置,这个是登进去后在个人设置里修改。
controller
这段逻辑
通过继承AbaseController类里的方法getUserInfoFromSession,通过session找到其SESSION_KEY对应的dto返回,再通过dto获取当前用户id,进行密码修改。
获取默认头像
mkdir()和mkdirs()
创建文件夹。
mkdir方法是用于创建最后一个/后面的文件夹,最后一个/前面的文件夹必须都存在。
mkdirs方法是无论父文件夹是否存在都会创建。
这段逻辑
先在配置文件中定义了项目根目录文件夹,随后定义文件file/和用于存放头像的avatar/文件夹,如果没有这个文件夹,就创建。总的合起来组成一个完整的头像路径字符串
如项目根目录/file/avatar/user123.jpg
如果还是没有这个文件,再看看有没有设置默认头像路径 项目根目录/file/avatar/default.jpg
然后读取文件。
上传头像
这段逻辑
先获取dto用户信息,然后就是找到或创建存放头像的文件夹,将用户上传的文件保存到这个文件夹里
因为自定义头像需要覆盖原来的qq头像,所以qq头像设置为空
然后webUserDto的avatar也被设为null,并更新到session中,这样下次请求时用户的最新头像信息会被重新加载现在自定义好的或者使用默认头像。
关于MultipartFile
MultipartFile:
public interface MultipartFile extends InputStreamSource { //getName() 返回参数的名称 String getName(); //获取源文件的昵称 @Nullable String getOriginalFilename(); //getContentType() 返回文件的内容类型 @Nullable String getContentType(); //isEmpty() 判断是否为空,或者上传的文件是否有内容 boolean isEmpty(); //getSize() 返回文件大小 以字节为单位 long getSize(); //getBytes() 将文件内容转化成一个byte[] 返回 byte[] getBytes() throws IOException; //getInputStream() 返回InputStream读取文件的内容 InputStream getInputStream() throws IOException;default Resource getResource() {return new MultipartFileResource(this); } //transferTo是复制file文件到指定位置(比如D盘下的某个位置),不然程序执行完,文件就会消失,程序运行时,临时存储在temp这个文件夹中 void transferTo(File var1) throws IOException, IllegalStateException;default void transferTo(Path dest) throws IOException, IllegalStateException {FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest)); } }
这里前端传过来文件,我们用MultipartFile avatar 接收
关于getPath()
将抽象路径名转换为一个路径名字符串。所得到的字符串使用默认名称分隔符来分隔名称序列中的名称。
public static void test1() {File file1 = new File(".\\test1.txt");File file2 = new File("D:\\workspace\\test\\test1.txt");System.out.println("-----默认相对路径:取得路径不同------");System.out.println(file1.getPath());System.out.println(file1.getAbsolutePath());System.out.println("-----默认绝对路径:取得路径相同------");System.out.println(file2.getPath());System.out.println(file2.getAbsolutePath()); }----- 默认相对路径:取得路径不同 ------ .\test1.txt D:\workspace\test\.\test1.txt ----- 默认绝对路径:取得路径相同 ------ D:\workspace\test\test1.txt D:\workspace\test\test1.txt