总括
用户输入用户名和密码和验证码即可进行登录
验证码
VerifyCode:生成验证码的工具类
/*** 生成验证码的工具类*/
public class VerifyCode {private int w = 70;//设置缓冲区的宽private int h = 35;//设置缓冲区的宽private Random r = new Random();//从字体中随机选一个 {"宋体", "华文楷体", "黑体", "华文新魏", "华文隶书", "微软雅黑", "楷体_GB2312"}private String[] fontNames = {"宋体", "华文楷体", "黑体", "华文新魏", "华文隶书", "微软雅黑", "楷体_GB2312"};//从下述字符中随机选 源private String codes = "123456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";// 从0~255背景颜色中随机选private Color bgColor = new Color(255, 255, 255);// 保存随机生成的图片当中的内容。private String text ;// 随机生成颜色private Color randomColor () {int red = r.nextInt(150);int green = r.nextInt(150);int blue = r.nextInt(150);return new Color(red, green, blue);}// 随机生成字体private Font randomFont () {int index = r.nextInt(fontNames.length);String fontName = fontNames[index];//根据随机的索引,获取随机字体int style = r.nextInt(4);//0,1,2,3, 0:没有任何样式,1,加粗,2,斜体,3,加粗和斜体 PLAIN(0)、BOLD(1)、ITALIC(2) 或 BOLD+ITALIC(3)。int size = r.nextInt(5) + 24; //随机生成字号return new Font(fontName, style, size);}// 画干扰线private void drawLine (BufferedImage image) {int num = 3;//画三条干扰线Graphics2D g2 = (Graphics2D)image.getGraphics();for(int i = 0; i < num; i++) {int x1 = r.nextInt(w);int y1 = r.nextInt(h);int x2 = r.nextInt(w);int y2 = r.nextInt(h);g2.setStroke(new BasicStroke(1.5F));g2.setColor(Color.BLUE); //给干扰线设置了颜色g2.drawLine(x1, y1, x2, y2);//划线}}//随机生成字符private char randomChar () {int index = r.nextInt(codes.length());return codes.charAt(index);}// 得到一个缓冲区private BufferedImage createImage () {// 获取一个缓冲区BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);// 得到一个画笔Graphics2D g2 = (Graphics2D)image.getGraphics();// 设置画笔的颜色 白颜色g2.setColor(this.bgColor);// 填充图片的缓冲区。g2.fillRect(0, 0, w, h);// 将缓冲区返回。return image;}// 调用该方法,可以得到验证码public BufferedImage getImage () {BufferedImage image = createImage();//创建图片的缓冲区Graphics2D g2 = (Graphics2D)image.getGraphics();//得到绘制环境(画笔)StringBuilder sb = new StringBuilder();//定义一个容器,用来装在生成的验证码//向图片当中画四个字符for(int i = 0; i < 4; i++) {//循环四次,每次生成一个字符String s = randomChar() + "";//随机成成一个字符sb.append(s); //将生成的字符放在缓冲区float x = i * 1.0F * w / 4; //设置当前字符的x轴坐标g2.setFont(randomFont()); //设置随机生成的字体g2.setColor(randomColor()); //设置字符的随机颜色g2.drawString(s, x, h-5); //画图}this.text = sb.toString(); //随机生成的图片的内容复制给this.textdrawLine(image); //画干扰线return image;}// 获取图片当中的内容public String getText() {return text;}// 保存图片到指定的输出流public static void output (BufferedImage image, OutputStream out)throws IOException {ImageIO.write(image, "JPEG", out);}
}
再在LoginController中写一个方法来获取验证码
// 获取验证码@RequestMapping("/login/VerifyCode")public void VerifyCode(HttpServletRequest request, HttpServletResponse response) throws Exception{request.setCharacterEncoding("utf-8");response.setContentType("text/html;charset=utf-8");VerifyCode code = new VerifyCode();BufferedImage image = code.getImage(); //得到验证码的图片String text = code.getText(); //得到验证码的文本HttpSession session = request.getSession(); //将验证码的值存放到session中session.setAttribute("verify",text);VerifyCode.output(image,response.getOutputStream()); //将验证码图片输出到页面}
相应的接收生成的验证码内容的前端页面
验证码部分html
<div class="item" id="code-item"><span>验证码</span><input class="input_code" type="text" name="Imgcode" id="Imgcode" /><img alt="验证码" src="/login/VerifyCode" id="imgCode" onclick="reloadCode()"></div>
验证码部分JavaScript
//更换验证码/** 由于用户可能每次切换的速度是非常快的,服务器默认的选项是短时间内多次点击只响应一下* 我们既不能让用户连续点我们就连续响应,也不能使用服务器默认的点很多很多下只响应一次,如何达到一个平衡?* 较为经典的做法是连续点很多次的话,显示“更新太频繁”* */function reloadCode(){var time=new Date().getTime(); //时间戳,每点一次就是一个时间,用于区分用户是否点击切换了验证码//下述做法是:用户点击几次就更新几次验证码//鼠标每单击一次验证码图片,设置img标签的src属性,然后图片标签就会调用src指向的资源document.getElementById("imgCode").src="/login/VerifyCode?id="+time;}
向后台发送用户输入的数据(发送了用户名、密码、验证码
// 登录按钮点击事件处理
$(".login").on("click", function () {//获取用户输入的值var username = $(".username").val();var password = $(".password").val();var Imgcode = $("#Imgcode").val(); //验证码值$.ajax({url: "/login/managerLogin", // 修改为与后端LoginController中checkLogin方法对应的路径type: "get",data: {username: username,password: password,Imgcode:Imgcode,},success: function (value){console.log(value.msg)if(value.code === 200){//进入到页面alert(value.msg)window.location.href = "http://localhost:8080/backend";}else if(value.code === 444){alert(value.msg);}else if(value.code === 999){alert(value.msg);}},error: function () {alert("登录失败,请检查账号、密码、邮箱、用户类型或验证码是否正确");}});
});
用户点击登录后LoginController部分
注意Login实体类中多添加了一个验证码字段
public class Login {private Integer id;private String username;private String password;private Integer admin; //0:不是管理员 1:是管理员private String email;private String Imgcode; //非数据库中字段,只是用于接收前端发过来的验证码信息
}
// 登录验证及相关操作@RequestMapping(value = "/login/managerLogin", method = RequestMethod.GET)@ResponseBodypublic ResponseResult managerLogin(Login login, HttpServletRequest request, HttpServletResponse response) throws IOException {//获取上边VerifyCode()方法中的session信息HttpSession session = request.getSession();// 使用合并后的managerLogin方法查询用户信息(写sql语句,用用户名和密码在数据库中查询是否存在这个用户)List<Login> logins = loginDao.managerLogin(login);//如果能在数据库中查询到数据且数据有且仅有一个if (logins.size() == 1) {// 获取邮箱信息(Login对象有email属性),后续用于发送邮件的时候获取邮箱信息String email = logins.get(0).getEmail();// 进行验证码验证if (session.getAttribute("verify").equals(login.getImgcode())) {// 验证码正确,生成token等操作// null是因为在JwtUtil中已经设置了过期时间,所以这里不用设置了String token = JwtUtil.createJWT(UUID.randomUUID().toString(), String.valueOf(logins.get(0).getId()), email, null);// 生成cookie对象,将token信息设置在cookie当中Cookie cookie = new Cookie("token", token);cookie.setPath("/");cookie.setMaxAge(36000);response.addCookie(cookie);// 登录成功return new ResponseResult(200, "登录成功", logins.get(0).getAdmin());} else {return new ResponseResult(999, "验证码错误");}} else {// 登录失败return new ResponseResult(444, "登录失败");}}
拦截器
LoginConfig:登录拦截配置类
一般不对登录、注册、静态资源信息等进行拦截
/*** 登录拦截配置类*/
@Configuration //表明为配置类
public class LoginConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;/*** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor) //添加拦截器.addPathPatterns("/**") //配置拦截路径 **表示拦截所有路径.excludePathPatterns("/login/**","/login_not_admin","/login_admin","/","/static/**","/templates/**,/register");//配置排除路径,排除了就不会拦截了}
}
LoginInterceptor:拦截器
继承自HandlerInterceptor,它里边有三个方法,我们这里重写了第一个
public interface HandlerInterceptor {default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return true;}default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}
拦截其实就是拦截的从处理器适配器到相应的Mapping注解下的方法(去与回都可做拦截)
我的LoginInterceptor是在相应方法执行前进行拦截
/*** 拦截器*/
@Component //将类交给spring管理
public class LoginInterceptor implements HandlerInterceptor {/*** 除了登录请求以外的其他请求都要进行过滤* @param request* @param response* @param handler* @return* @throws Exception*/// 除了前面登录拦截配置类LoginConfig中配置过排除拦截的路径,前端的其余所有请求都要打入preHandle中,判断有无正确的登陆信息@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Cookie[] cookies = request.getCookies(); //获取前端的cookie信息if(cookies != null){for (Cookie cookie:cookies){if("token".equals(cookie.getName())){String userToken = cookie.getValue();//如果token信息为空,发送错误提示if(!StringUtils.hasText(userToken)){response.sendError(HttpServletResponse.SC_UNAUTHORIZED);}//解析token看看是否成功try {Claims claims = JwtUtil.parseJWT(userToken);claims.getSubject();}catch (Exception e){//e.printStackTrace();System.out.println("token信息出错");return false;}return true; //有登录信息且登录信息正确→放行}}}return false; //没有登录信息或登录信息错误→不放行,访问不到相应路径下的方法}
}
JWT
一、什么是JWT
JWT(JSON WEB TOKEN),通过数字签名的方式,以json对象为载体,在不同的服务终端之间安全的传输信息,用来解决传统session的弊端。
JWT在前后端分离系统,或跨平台系统中,通过JSON形式作为WEB应用中的令牌(token),用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。
token一般都是用来认证的,比如我们系统中常用的用户登录token可以用来认证该用户是否登录。jwt也是经常作为一种安全的token使用。
二、JWT能做什么
1.授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
2.信息交换:jwt是在各方之间安全地传输信息的好方法,因为可以对JWT进行签名(例如,使用公钥/私钥对),所以可以确保发件人是他们所说的人,此外,由于签名嘶使用标头和有效负债计算的,因此可以验证内容是否遭到篡改。
这次讲课我们主要针对的是JWT实现的授权登录
三、为什么有JWT
传统的session认证有如下的问题
1.每个用户经过我们的应用认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证用户的增多,服务器内存开销会明显增大;
2.用户认证之后,服务端使用session保存认证信息,那么要取到认证信息,只能访问同一台服务器,才能拿到授权的资源。这样在分布式应用上,就需要实现session共享机制,不方便集群应用;
3.因为session是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于JWT的认证流程
- 前端通过web表单将自己的用户和密码发送到后端接口(一般是http-post请求,建议使用SSL加密传输(https协议),以免敏感信息被嗅探)目前只是发起普通的http请求,未进行加密
- 后端核对用户名和密码成功后,将用户的Id等其他信息作为JWT Payload(负载),将其与头部分别进行BASE64编码拼接后签名,形成一个JWT(Token),形成的JWT就是一个字符(head.payload.singueater)
- 后端将JWT字符串作为登录成功的结果返回给前端。前端结果保存在localStorage(本地缓存)或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入http header中的Authorization位(解决XSS和XSRF问题)
- 后端检查是否存在,如存在验证JWT的有效性,例如:检查签名是否正确,检查token是否过期,检查token的接收方是否是自己(可选)。
JWT的优势 - 简洁,可以通过URL、POST参数或Http header发送,因为数据量小,传输速度快;
- 自包含,负载(属于JWT的一部分)中包含了用户所需要的信息,不需要在服务器端保存会话信息,不占服务器内存,也避免了多次查询数据库,特别适用于分布式微服务;
- 因为token是以json加密的形式保存在客户端的,所以JWT可以跨语言使用,原则上任何WEB形式都支持。
- 不需要再服务端保存会话信息,特别适用于分布式微服务。
四、JWT的结构
使用JWTUtil工具类
JWT其实就是一段字符串,由标头(Header)、有效载荷(Payload)和签名(Signature)这三部分组成,用 . 拼接。在传输的时候,会将JWT的三部分分别进行Base64编码后用 . 进行连接形成最终传输的字符串。
1.头部(Header): JWT的头部是一个JSON对象,用于描述JWT的元数据(默认会生成,可以不设置),例如令牌的类型(typ)和签名算法(alg)。通常情况下,头部会包含以下信息:
{"alg": "HS256","typ": "JWT"
}
- alg:指定签名算法,常见的有HMAC SHA256(HS256)和RSA SHA256(RS256)等。 默认HS256
- typ:指定令牌的类型,一般为JWT。
头部需要经过Base64编码后作为JWT的第一部分。
- typ:指定令牌的类型,一般为JWT。
2.载荷(Payload):JWT的载荷是存储实际数据的部分(即存储用户信息的部分),也是一个JSON对象。它包含了一些声明(claims),用于描述令牌的信息。常见的声明有
{"sub": "1234567890","name": "John Doe","admin": true
}
前面两部分都使用Base64进行编码,前端可以解开知道里面的信息, Signature需要使用编码后的header和payload以及我们提供的一密钥,然后使用header中指定的签名算法进行签名,以保证JWT没有被篡改过。
3. 签名(Signature):使用Signature签名可以防止内容被篡改(签名即类似于现实生活中你的签名,比如一个合同,得有你的亲笔签名才能生效)。如果有人对头部及负载内容解码后进行修改,再进行编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。签名一般不允许对外泄露
需要注意的是,JWT中的信息是经过Base64编码的,虽然可以通过Base64解码获取原始内容,但是不能修改JWT中的内容,因为签名会验证JWT的完整性和真实性。
五、JWT的第一个程序
在测试类当中获取token
/*** 获取token*/
@Test
void setToken(){HashMap<String,Object> map = new HashMap<>();//Calendar java提供的日历类Calendar instance = Calendar.getInstance();instance.add(Calendar.SECOND,20);String token = JWT.create().withHeader(map) //header---------------->这个可以去掉,不影响.withClaim("userId",21) //payload.withClaim("username","xiaoming").withExpiresAt(instance.getTime()) //指定令牌的过期时间.sign(Algorithm.HMAC256("!@Q#WW22")); //签名:一般不对外泄露System.out.println(token);
}
在测试类当中验证token
@Test
public void getToken(){//创建验证对象JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!@Q#WW22")).build(); //签名就相当于是密钥,没有签名就打不开字符串DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3Mjc4NjE3ODYsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoieGlhb21pbmcifQ.GODRy8B3ZC1SlV49w4sI0QBDlwvx_ayzezjE0OdJS6k");System.out.println(verify.getClaim("userId").asInt());System.out.println(verify.getClaim("username").asString());System.out.println("过期时间:"+verify.getExpiresAt());
}
六、JWT的工具类封装
/*** JWT工具类* @author 王*/
public class JWTUtils {//秘钥---》这个不能被外界获取private static final String SING = "@#S2$$*^^";/*** 生成 token* @param map* @return*/public static String getToken(Map<String,String> map){Calendar instance = Calendar.getInstance();instance.add(Calendar.DATE,7 ); //默认7天过期//创建jwt builderJWTCreator.Builder builder = JWT.create();//payloadmap.forEach((k,v)->{builder.withClaim(k,v);});String token = builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));return token;}/*** 验证token的合法性* @param token*/public static void verify(String token){JWT.require(Algorithm.HMAC256(SING)).build().verify(token);}/*** 获取token信息方式* @param token* @return*/public static DecodedJWT getTokenInfo(String token){DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);return verify;}}
七、进行使用
同样是验证字符串,session与token有什么区别
1.session生成的字符串JSESSIONID存储在服务器上,用户登录后访问相应路径时会携带着JSESSIONID,这时会将服务器中存储的JSESSIONID与用户携带的进行比对,一致即放行
2.token生成的字符串是一个加密后的信息,我们只需要在该字符串发送过来后,使用密钥进行解密即可,如果能解密成功,那就是我们发送过去的token信息;如果不能机密成功或解密成功后发现登录信息已经过期了,那就认为当前字符串不正确。故无需存储在服务器上,减小了服务器的压力
在LoginController中,一旦用户登陆成功,就马上生成相应的token
// 登录验证及相关操作@RequestMapping(value = "/login/managerLogin", method = RequestMethod.GET)@ResponseBodypublic ResponseResult managerLogin(Login login, HttpServletRequest request, HttpServletResponse response) throws IOException {//获取上边VerifyCode()方法中的session信息HttpSession session = request.getSession();// 使用合并后的managerLogin方法查询用户信息(写sql语句,用用户名和密码在数据库中查询是否存在这个用户)List<Login> logins = loginDao.managerLogin(login);//如果能在数据库中查询到数据且数据有且仅有一个if (logins.size() == 1) {// 获取邮箱信息(Login对象有email属性),后续用于发送邮件的时候获取邮箱信息String email = logins.get(0).getEmail();// 进行验证码验证if (session.getAttribute("verify").equals(login.getImgcode())) {// 验证码正确,生成token等操作/** UUID.randomUUID():随机生成一个数* logins.get(0).getId():也可以不获取id,获取其他信息如username也行* email:后续发送邮件功能会用到,故也设置上了* null是因为在JwtUtil中已经设置了过期时间,所以这里不用设置了会使用默认的过期时间;如果设置了就用你设置的过期时间* */String token = JwtUtil.createJWT(UUID.randomUUID().toString(), String.valueOf(logins.get(0).getId()), email, null);// 生成cookie对象,将token信息设置在cookie当中//注意:cookie是保存在浏览器上的Cookie cookie = new Cookie("token", token);cookie.setPath("/"); //设置浏览器访问路径cookie.setMaxAge(36000); //设置cookie的过期时间response.addCookie(cookie); //将cookie返回给浏览器// 登录成功return new ResponseResult(200, "登录成功", logins.get(0).getAdmin());} else {return new ResponseResult(999, "验证码错误");}} else {// 登录失败return new ResponseResult(444, "登录失败");}}
JwtUtil:JWT工具类
package com.qcby.springbootdemo.util;/*** JWT工具类*/import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;public class JwtUtil {//设置时间:有效期为public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 * 1000//设置秘钥明文public static final String JWT_KEY = "qcby";/*** 创建token* @param id* @param subject* @param ttlMillis* @return*/public static String a_email; //设置全局变量存储用户输入的email信息public static String createJWT(String id, String subject, String email, Long ttlMillis) {a_email = email;SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);SecretKey secretKey = generalKey();//生成token信息:一般想设置啥就设置啥,但用户id一般是要设置上的JwtBuilder builder = Jwts.builder().setId(id) //唯一的ID(默认下是必须有用户id的,因为用户id一般是不重复的,后续添加删除之类的一般会用到用户id,所以这里必须要保存着).setSubject(subject) // 主题 可以是JSON数据.setIssuer("wd") // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate)// 设置过期时间.claim("email", email); // 将用户邮箱信息添加到Claims中,这里键名为"email",可根据需求自定义return builder.compact();}/*** 生成加密后的秘钥 secretKey* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析* @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}public static void main(String[] args) {String token = JwtUtil.createJWT(UUID.randomUUID().toString(),"qd", a_email,null );System.out.println(token);}}
当用户登陆后就会在浏览器中生成包含token的cookie信息
这样用户在访问其他页面的时候就会在之前提到的拦截器类LoginInterceptor中被拦截,核对信息后放行
/*** 拦截器*/
@Component //将类交给spring管理
public class LoginInterceptor implements HandlerInterceptor {/*** 除了登录请求以外的其他请求都要进行过滤* @param request* @param response* @param handler* @return* @throws Exception*/// 除了前面登录拦截配置类LoginConfig中配置过排除拦截的路径,前端的其余所有请求都要打入preHandle中,判断有无正确的登陆信息@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Cookie[] cookies = request.getCookies(); //获取前端的cookie信息//先看看是否有cookie信息if(cookies != null){for (Cookie cookie:cookies){//看看cookie中是否有token信息if("token".equals(cookie.getName())){String userToken = cookie.getValue();//如果token信息为空,发送错误提示if(!StringUtils.hasText(userToken)){response.sendError(HttpServletResponse.SC_UNAUTHORIZED);}//使用JwtUtil中的parseJWT解析token,看看是否成功try {Claims claims = JwtUtil.parseJWT(userToken);claims.getSubject();}catch (Exception e){//e.printStackTrace();System.out.println("token信息出错");return false;}return true; //有登录信息且登录信息正确→放行}}}return false; //没有登录信息或登录信息错误→不放行,访问不到相应路径下的方法}
}
这样便实现了只有登录后才能访问到相应页面信息的功能