目录
在登录页面加入图形验证码
一、验证码的生成与校验
1. 创建生成验证码的工具类
package com.Util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class ImageCodeUtils {
/**
* 图片的宽度
*/
private int width = 160;
/**
* 图片的高度
*/
private int height = 40;
/**
* 验证码字符个数
*/
private int codeCount = 4;
/**
* 验证码干扰线数
*/
private int lineCount = 20;
/**
* 验证码
*/
private String code = null;
private BufferedImage buffImg = null;
Random random = new Random();
public ImageCodeUtils() {
createImage();
}
public ImageCodeUtils(int width, int height) {
this.width = width;
this.height = height;
createImage();
}
public ImageCodeUtils(int width, int height, int codeCount) {
this.width = width;
this.height = height;
this.codeCount = codeCount;
createImage();
}
public ImageCodeUtils(int width, int height, int codeCount, int lineCount) {
this.width = width;
this.height = height;
this.codeCount = codeCount;
this.lineCount = lineCount;
createImage();
}
/**
* 生成图片
*/
private void createImage() {
// 字体的宽度
int fontWidth = width / codeCount;
// 字体的高度
int fontHeight = height - 5;
int codeY = height - 8;
// 图像buffer
buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = buffImg.getGraphics();
// 设置背景色
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));
// a为要画出来的东西,x和y表示要画的东西最左侧字符的基线位于此图形上下文坐标系的 (x, y) 位置处
g.drawString(strRand, i*fontWidth+3, codeY);
}
}
/**
* 得到随机字符串
* @param n
* @return
*/
private String randomStr(int n) {
String str1 = "ABCDEFGHJKMNOPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz1234567890";
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;
}
/**
* 得到随机颜色
* @param fc
* @param bc
* @return
*/
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)];
}
/**
* 扭曲方法
* @param g
* @param w1
* @param h1
* @param color
*/
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;
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();
}
}
2. 写一个 Controller
方法:前端访问,调用一下工具类生成验证码图片并返回
引入一下依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
3. 实现验证码验证
1. 获取验证码
已经获取到验证码了,那么下一步怎么验证验证码呢?
2. 验证码请求过程
游览器那么多,服务器怎么知道是哪个游览器请求的?
这就可以使用到 Cookie 和 Session
3. 验证码的校验
把字符串形式的验证码存到 Session
从Session 中获取验证码
4. 原理说明
5. 验证
提示验证码错误
先查看验证验证码的SessionID是否和获取验证码的SessionID 一样
可以看到,两个是不同的SessionID ,服务器根据ID去获取的值是不同的,所以验证码错误
那么为什么是不同的呢?
这是因为 Vue 开启了代理转发,所以每次请求的 Cookie中JSESSIONID 值会发生变化
如何解决呢?
官方的解决方案是代理转发的 请求 URL 前缀和服务器请求路径的项目名相同就没问题了
原来的请求路径
修改后的请求路径
注意:vue 的配置文件修改后需要重启
修改服务器的 URL ,修改后重启服务器
可以看到SessionID值是一样的了
验证成功
6. 总结
- 会话开始:当一个用户首次访问一个网站,服务器会创建一个新的HttpSession对象,并生成一个唯一的会话ID(通常称为JSESSIONID)。
- 会话ID存储在Cookie中:服务器会在响应中包含一个Set-Cookie头,将这个会话ID作为cookie的一部分发送给浏览器。浏览器会存储这个cookie。
- 后续请求:当用户继续与网站交互时,浏览器会自动在每个后续请求中包含这个会话ID的cookie。这使得服务器能够识别出请求来自于哪个具体的会话。
- 服务器识别会话:服务器接收到请求后,会读取请求头中的cookie,提取出会话ID,然后使用这个ID在服务器端的会话存储中查找相应的HttpSession对象。
- 会话数据访问:一旦找到了HttpSession对象,服务器就可以从这个对象中读取或写入数据,比如用户信息、登录状态等。
- 会话结束:当用户关闭浏览器或会话超时,HttpSession对象会被销毁,除非服务器端有特别的配置来延长会话的存活时间。
二、JWT登录鉴权
JWT 全程 JSON Web Token
1. 为什么要做登录鉴权?
安全考虑,需要登录之后有操作权限了之后才能访问API接口
2. 什么是 JWT
JWT(JSON Web Token)是一种在网络应用中用于身份验证和授权的令牌。你可以把它想象成一张电子版的“身份证”或“通行证”。
当你登录一个网站后,服务器会生成一个JWT,JWT 本质就是一条字符串,它把你的身份信息(比如用户名)保存到一个JSON字符串中,然后进行编码得到一个token 令牌,然后把这个令牌发回给你的浏览器。之后,每当你的浏览器想要访问受保护的资源时,它都会带上这个JWT。
3. JWT相比传统的鉴权方式的优点
1. Session 认证
我们知道 HTTP 是一种无状态的协议,HTTP协议在设计上不保留任何两次请求之间的信息。换句话说,当你向一个网站发送请求时,比如浏览一个网页,这个请求是独立的,它并不依赖于你之前对该网站做的任何事情。一旦服务器处理完你的请求并返回了响应,它就会忘记这次交互,就像从来没有发生过一样。
所以为了让服务器知道是谁在访问,我们会在游览器第一次登陆成功的时候,创建一个SessionID,然后把用户信息保存在 Session 对象中,最后把SessionID放到 Cookie 返回给游览器,这样下次游览器再访问的时候就知道是谁了,这就是基于Session 认证的过程
2. Session 认证的缺点
由于基于Cookie,而cookie无法跨域,所以session的认证也无法跨域
Session 是保存在服务器的,会使服务器的开销增大
3. JWT 的优点
简洁,
无状态存储,以加密的形式保存在客户端,
时效性,可以设置多少时间失效
4. 创建 JWT
1. 引入依赖
<dependency>
<groupId>io.github.qyg2297248353.components</groupId>
<artifactId>jsonwebtoken-jjwt</artifactId>
<version>2.0.0</version>
</dependency>
2. 创建JWT 工具类
package com.Util;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Map;
public class JwtUtil {
// 私钥
private static String privateKey = "12345678901234567890123456789012abcdefghijklmn";
/*
* 生成token
* @param claims 要加密的数据
* @return
* */
public static String generateToken(Map<String, Object> claims) {
// 使用JWT构建器构建令牌
// 添加负载(claims),存储用户信息
// 使用HS256算法和私钥进行签名,确保令牌的完整性和安全性
// 最后将令牌以JSON格式编码,并压缩为紧凑字符串格式
String token = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, privateKey) // HS256加密,密钥长度必须大于等于256 bit
.compact();
return token;
}
}
3. 生成一个token
在 Service 层调JWT工具类生成 token
登录成功后返回的 token
那么如何在每次请求都带上这个 token 呢?
可以把token 放到 Cookie 里
5. 使用 JWT
1. Vue 导入依赖
npm i js-cookie@2.2.0 -S
2. 前端创建 Cookie 工具类
3. 在登录组件设置 token
引入token 工具类的方法
4. 判断是否有 token
在路由器文件的路由守卫中判断,如果有 token 则放行
验证
5. 发送 token
在自定义 axios 文件的请求拦截器中添加 token 自定义 axios
可以看到在登录成功后,请求头里有 token
6. 服务器验证token
服务器在哪个 Controller 里判断?在所有 Controller 都判断一下比较麻烦
这就可以使用拦截器,在拦截器中统一判断
1. 创建登录拦截器
2. 配置拦截器
在spring 配置文件配置拦截器
<!--配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/> <!--拦截所有请求-->
<mvc:exclude-mapping path="/Admin/Login"/> <!--放行登录操作-->
<mvc:exclude-mapping path="/ImageCode/Captcha"/> <!--放行请求验证码-->
<bean class="com.Interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
这样就可以在每次请求资源时都带有token,服务器根据 token 判断是否有权限访问数据
目前,在 Vue 的路由守卫有判断是否登录(token)以及在服务器的拦截器也有判断