第五章 JWT原理和应用以及实现功能菜单
提示:本博客个为人独立博客,不是权威,仅供参考!所有思路只做交流之用!如有不足之处,望各位在评论区友善指正。
文章目录
前言
这一章将介绍JWT原理和应用以及实现功能菜单
一、Json Web Token
1.JWT原理
转载自:https://www.cnblogs.com/Jason-Xiang/p/9808596.html
传统的身份验证的方法
HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth。
OAuth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
下面是OAuth2.0的流程:
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。
Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效。非前后端分离项目采用这种方式的较多。
基于 Token 的身份验证方法
使用基于 Token 的身份验证方法,大概的流程是这样的:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token然后保存(缓存或者数据库),再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
Token机制相对于Cookie机制的好处:
- 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
- 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token
自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息. - 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows
8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。 - CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理. - 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby,
Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
基于JWT的Token认证机制实现
实施 Token 验证的方法挺多的,还有一些标准规范,其中JSON Web Token(JWT)是一个非常轻巧的规范 。JWT 标准的 Token 有三个部分:
- header(头部)
- payload(数据)
- signature(签名)
中间用点分隔开,并且都会使用 Base64 编码,所以真正的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
每个 JWT token 里面都有一个 header,也就是头部数据。里面包含了使用的算法,这个 JWT 是不是带签名的或者加密的。主要就是说明一下怎么处理这个 JWT token 。
头部里包含的东西可能会根据 JWT 的类型有所变化,比如一个加密的 JWT 里面要包含使用的加密的算法。唯一在头部里面要包含的是 alg 这个属性,如果是加密的 JWT,这个属性的值就是使用的签名或者解密用的算法。如果是未加密的 JWT,这个属性的值要设置成 none。
示例:
{
"alg": "HS256"
}
意思是这个 JWT 用的算法是 HS256。上面的内容得用 base64url 的形式编码一下,所以就变成这样:
eyJhbGciOiJIUzI1NiJ9
Payload
Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:
- iss:Issuer,发行者
- sub:Subject,主题
- aud:Audience,观众
- exp:Expiration time,过期时间
- nbf:Not before
- iat:Issued at,发行时间
- jti:JWT ID
比如下面这个 Payload ,用到了 iss 发行人,还有 exp 过期时间这两个标准字段。另外还有两个自定义的字段,一个是 name ,还有一个是 admin。
{
"iss": "ninghao.net",
"exp": "1438955445",
"name": "wanghao",
"admin": true
}
使用 base64url 编码以后就变成了这个样子:
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
Signature
JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64 编码的 header.payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端。
- header
- payload
- secret
const encodedString = base64UrlEncode(header) + “.” +
base64UrlEncode(payload); HMACSHA256(encodedString, ‘secret’);
处理完成以后看起来像这样:
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。
2.实现JWT
引入jjwt
创建JWTUtil通用类
package com.sisyphus.framework.jwt;
import com.sisyphus.entity.Student;
import com.sisyphus.entity.User;
import com.sisyphus.framework.exception.MyException;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* jwt的工具类
*/
public class JWTUtil {
public static String token = "token";
//秘钥
public static String jwt_secret="yanzhen@cms@cc596183363.";
//过期时长
public static long jwt_expr = 3600*24*1000;
//1、生成token
//用户token
public static String sign(User user){
//1、指定签名的时候使用的签名算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//2、生成签发时间
long nowMillis = System.currentTimeMillis();
Date date = new Date(nowMillis);
//3、创建playLoad的私有声明
Map<String,Object> claims = new HashMap<>();
claims.put("id",user.getId());
claims.put("userName",user.getUserName());
claims.put("type","USER");
//4、生成签发人
String subject = user.getUserName();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())
.setIssuedAt(date)
.setSubject(subject)
.signWith(signatureAlgorithm,jwt_secret);
//设置过期时间
Date exprDate = new Date(nowMillis + jwt_expr);
builder.setExpiration(exprDate);
return builder.compact();
}
//学生token
public static String signForStudent(Student student){
//1、指定签名的时候使用的签名算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//2、生成签发时间
long nowMillis = System.currentTimeMillis();
Date date = new Date(nowMillis);
//3、创建playLoad的私有声明
Map<String,Object> claims = new HashMap<>();
claims.put("id",student.getId());
claims.put("stuNo",student.getStuNo());
claims.put("type","STUDENT");
//4、生成签发人
String subject = student.getStuNo();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())
.setIssuedAt(date)
.setSubject(subject)
.signWith(signatureAlgorithm,jwt_secret);
//设置过期时间
Date exprDate = new Date(nowMillis + jwt_expr);
builder.setExpiration(exprDate);
return builder.compact();
}
//2、验证token
public static boolean verify(String token){
try {
if(StringUtils.isEmpty(token)){
return false;
}
Jwts.parser().setSigningKey(jwt_secret).parseClaimsJws(token).getBody();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static String getType(String token){
try {
if(StringUtils.isEmpty(token)){
throw new MyException("token不能为空");
}
if(verify(token)){
Claims claims = Jwts.parser().setSigningKey(jwt_secret).parseClaimsJws(token).getBody();
return claims.get("type")+"";
}else{
throw new MyException("超时或不合法token");
}
} catch (Exception e) {
throw new MyException("超时或不合法token");
}
}
//3、获取用户信息
public static User getUser(String token){
try {
if(StringUtils.isEmpty(token)){
throw new MyException("token不能为空");
}
if(verify(token)){
Claims claims = Jwts.parser().setSigningKey(jwt_secret).parseClaimsJws(token).getBody();
User user = new User();
user.setId(Integer.parseInt(claims.get("id")+""));
user.setUserName(claims.get("userName")+"");
return user;
}else{
throw new MyException("超时或不合法token");
}
} catch (Exception e) {
throw new MyException("超时或不合法token");
}
}
public static Student getStudent(String token){
try {
if(StringUtils.isEmpty(token)){
throw new MyException("token不能为空");
}
if(verify(token)){
Claims claims = Jwts.parser().setSigningKey(jwt_secret).parseClaimsJws(token).getBody();
Student student = new Student();
student.setId(Integer.parseInt(claims.get("id")+""));
student.setStuNo(claims.get("stuNo")+"");
return student;
}else{
throw new MyException("超时或不合法token");
}
} catch (Exception e) {
throw new MyException("超时或不合法token");
}
}
public static void main(String[] args) {
User user = new User();
user.setId(1);
user.setUserName("admin");
System.out.println(sign(user));
}
}
创建exception文件夹和MyException异常类
二、实现登录Token
1.新建TokenInterceptor
package com.sisyphus.framework.mvc;
import com.sisyphus.entity.User;
import com.sisyphus.framework.exception.MyException;
import com.sisyphus.framework.jwt.JWTUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(JWTUtil.token);
//根据token获取user对象
User user = JWTUtil.getUser(token);
if(user == null){
throw new MyException("超时或不合法的token");
}
String newToken = JWTUtil.sign(user);
response.setHeader(JWTUtil.token,newToken);
request.setAttribute("user",user);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
2.注册拦截器
3.把token插入实体entity中
4.把token存入本地存储中
封装localStorage
在lay-config.js中注册
在axios.js中引入store.js
把token存入本地存储中
三、实现功能菜单
1.在数据库中写入数据
2.创建MenuController
为Menu设置一个child属性,方便分级
重写MenuMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sisyphus.mapper.MenuMapper">
<resultMap id="Menu" type="com.sisyphus.entity.Menu">
<id column="id" property="id"/>
<result column="title" property="title"/>
<result column="icon" property="icon"/>
<result column="href" property="href"/>
<result column="target" property="target"/>
<result column="parent_id" property="parentId"/>
<result column="type" property="type"/>
</resultMap>
<insert id="deleteUserMenu">
delete from tb_user_menu where user_id = #{userId}
</insert>
<insert id="createUserMenu">
insert into tb_user_menu(user_id,menu_id) values(#{userId},#{menuId})
</insert>
<select id="query" resultMap="Menu">
select tb_menu.* from tb_menu,tb_user_menu where tb_menu.id=tb_user_menu.menu_id
and user_id = #{userId} and tb_menu.type = 0
</select>
<select id="queryByType" resultMap="Menu">
select tb_menu.* from tb_menu where type = 1
</select>
<select id="queryCheckMenuId" resultType="int">
select menu_id from tb_user_menu where user_id = #{userId}
</select>
<select id="list" resultMap="Menu">
select tb_menu.* from tb_menu where type = 0
</select>
</mapper>
重写MenuMapper
重写Menu.Service
新建MenuController
package com.sisyphus.controller;
import com.sisyphus.entity.Menu;
import com.sisyphus.entity.User;
import com.sisyphus.service.MenuService;
import com.sisyphus.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private MenuService menuService;
@GetMapping("/query")
public Result query(HttpServletRequest request){
User user = (User)request.getAttribute("user");
List<Menu> menus = menuService.query(user.getId());
List<Menu> menuList1 = new ArrayList<>();
//找出一级菜单
for (Menu menu : menus) {
if(menu.getParentId() == 0){
menuList1.add(menu);
}
}
//嵌套循环找出关联设置child属性
for (Menu parent : menuList1) {
List<Menu> child = new ArrayList<>();
for (Menu entity : menus) {
if(parent.getId() == entity.getParentId()){
child.add(entity);
}
}
parent.setChild(child);
}
return Result.ok(menuList1);
}
}
3.在index页面显示功能菜单
引入store
判断是否携带token,若没有则不允许访问,并跳转到登录页面
修改iniUrl
在miniAdmin.js中用axios请求token
退出登录清除token
4.创建GlobalControllerAdvice全局异常处理类
四、Token响应处理
1.在axios.js中的响应拦截器中添加更新token的代码
1.在token拦截器中添加暴露token的代码
这样处理之后,第一次响应的token会成为下次请求的token,下次响应的token会成为下下次请求的token
# 总结 难点:理解JWT原理
至此本项目已经完成了三分之一,代码技术的难点基本都解决了,剩下的大部分工作都是在写业务逻辑
第六章预计三天内更新,涉及用户管理和年级管理功能