HM新出springboot入门项目《苍穹外卖》,笔者打算写一个系列学习笔记,“苍穹外卖项目解读”,内容主要从HM课程,自己实践,以及踩坑填坑出发,以技术,经验为主,记录学习,也希望能给在学想学的小伙伴一个参考。
注:本文章是直接拿到项目的最终代码,然后从代码出发,快速逆向学习技术经验! 可能需要一些前置知识
觉得文章有用可以关注点赞收藏期待更新^^,期待您的评论留言
苍穹外卖项目解读(一) 完整代码本地部署运行
苍穹外卖项目解读(二) 管理端JWT令牌、AOP注解开发、分页
苍穹外卖项目解读(三) redis、cache缓存解读
本系列文章很少(基本没有)对业务进行解读,HM课程的老师已经讲解的很清晰了,所以有关业务的CRUD也就不再展开解读。今天的主要解读是JWT
苍穹外卖项目使用的springboot + mybatis开发,没有用到MP,所以项目的分页功能就在文章中分析一下。课程中已经讲得很详细了,这里主要记录一下。
使用分页插件PageHelper.startPage
,调用dao层填充查询条件%#{name}%
,按照创建时间降序排列。
PageHelper.startPage()
可以根据页码和条数拼合在select语句中,其Page对象是一个ArrayList,所以我们要声明PageResult,包括total全部条数,record 一页的记录信息
AOP切面开发,一个切面开发要包括三个部分,切面,切点,通知,我总结了3W记忆法(when what where)。
简单理解切面aspect:是一个类,表明了在何时(@before、@around…),做何事(通知方法)。
切点:何处(某个包下的所有方法,加注解的方法…)
注解AOP开发的话,就是我们声明出来一个注解,用注解来标定我们的切点
代码注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value(); //变量value 类型OperationType 枚举,在使用注解时可以填充value=xxx
}
------------------------------------------------
/**
* 切入点
*/
//真正切入点pointcut
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){} //切点名称,方便通知使用
------------------------------------------------
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()") //指明切点
public void autoFill(JoinPoint joinPoint){} //具体通知,内容省略
------------------------------------------------
/**
* 插入员工数据
* @param employee
*/
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
@AutoFill(value = OperationType.INSERT)
//注解调用(流程:因为AutoFill被指明为切点,AOP会介入通知,进入insert前,执行@before通知,进入insert方法)
void insert(Employee employee);
笔者结合前后端代码,以及JWT源码,梳理本项目的token访问认证。
首先介绍本项目token的作用:根据拦截器可知,SpringMVC在HandlerMapping之前,有一个preHandle,也就是去访问控制器时,预先处理一些符合路径的请求之后再打到控制器。
以下从拦截器介绍、JWT介绍、项目代码分析三个部分,逐步揭开项目JWT访问认证功能
preHandler(HttpServletRequest request, HttpServletResponse response, Object handler)
方法将在请求处理之前进行调用。SpringMVC中的Interceptor同Filter一样都是链式调用。每个Interceptor的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor中的preHandle方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是布尔值Boolean 类型的,当它返回为false时,表示请求结束,后续的Interceptor和Controller都不会再执行;当返回值为true时就会继续调用下一个Interceptor 的preHandle 方法,如果已经是最后一个Interceptor 的时候就会是调用当前请求的Controller 方法。
postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。
afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。总结一点就是:preHandle是请求执行前执行postHandle是请求结束执行afterCompletion是视图渲染完成后执行
原文链接:https://blog.csdn.net/yxg520s/article/details/122348512
**
后端实现自定义拦截器:
1、实现HandlerInterceptor接口,注册为bean @Component,按照需求实现上述三种方法
2、在webmvc配置类中 (继承WebMvcConfigurationSupport),自定义一个方法使用InterceptorRegistry 参数添加自定义拦截器并设置拦截路径和不拦截路径
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
//public class JwtTokenAdminInterceptor implements HandlerInterceptor{}
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")//对admin/的任何请求都拦截
.excludePathPatterns("/admin/employee/login");//排除login拦截
1、用户使用账号、密码登录应用,登录的请求发送到 Authentication Server。
2、Authentication Server 进行用户验证,然后创建 JWT 字符串返回给客户端。
3、客户端请求接口时,在请求头带上 JWT。
4、Application Server 验证 JWT 合法性,如果合法则继续调用应用接口返回结果。
关键在于生成 JWT 和解析 JWT
JWT 一般是这样xxxxx.yyyyy.zzzzz
一个字符串,分为三个部分,以 “.” 隔开:
1、JWT 第一部分是头部分,它是一个描述 JWT 元数据的 Json 对象
2、JWT 第二部分是 Payload,也是一个 Json 对象,可传入自定义对象。需要注意的是,默认情况下JWT 是未加密的,任何人都可以解读其内容,因此一些敏感信息不要存放于此,以防信息泄露。JSON 对象也使用 Base64 URL 算法转换为字符串后保存,是可以反向反编码回原样的,这也是为什么不要在 JWT 中放敏感数据的原因。
3、JWT 第三部分是签名。是这样生成的,首先需要指定一个 secret,该 secret 仅仅保存在服务器中,保证不能让其他用户知道。这个部分需要 base64URL 加密后的 header 和 base64URL 加密后的 payload 使用 . 连接组成的字符串,然后通过header 中声明的加密算法 进行加盐secret组合加密,然后就得出一个签名哈希,也就是Signature,且无法反向解密。
4、服务端验证利用 JWT 前两段,用同一套哈希算法和同一个 secret 计算一个签名值,然后把计算出来的签名值和收到的 JWT 第三段比较,如果相同则认证通过。
项目代码样例
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
====================================================
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,
// 一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();//得到token string字符串
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token)
//得到claims (包含了加密时的用户信息和ttl)
.getBody();
return claims;
}
}
====================================================
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
====================================================
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:{}", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
====================================================
前端相关代码
=======================api/employee.ts=============================
// 登录、前端请求路径,请求controller
export const login = (data: any) =>
request({
'url': '/employee/login',
'method': 'post',
data
})
========================store/modules/user.ts============================
const { data } = await login({ username, password }) //等待响应返回token
if (String(data.code) === '1') {//返回成功
this.SET_TOKEN(data.data.token)
setToken(data.data.token)//设置cookies
this.SET_USERINFO(data.data)
Cookies.set('user_info', data.data)
return data
} else {
return Message.error(data.msg)
}
======================utils/cookies.ts==============================
// User 设置cookie key为“token”
const tokenKey = 'token';
export const getToken = () => Cookies.get(tokenKey);
export const setToken = (token: string) => Cookies.set(tokenKey, token);
export const removeToken = () => Cookies.remove(tokenKey);
==========================utils/request.ts==========================
service.interceptors.request.use(
(config: any) => {
if (UserModule.token) {
config.headers['token'] = UserModule.token //存在token设置请求头中token
} else if (UserModule.token && config.url != '/login') {
//不存在token,访问路径不是login,转到login(这里我觉得这个if elseif写错了,请前端大佬指点,但应该是这个意思)
window.location.href = '/login'
return false
}
==========================utils/request.ts==========================
service.interceptors.response.use(
(response: any) => {
// console.log(response, 'response')
if (response.data.status === 401) { //后端响应401 直接回login
router.push('/login')
}
首先JWT在后端是设置了过期时间的,所以当我们token过期的时候,会抛出超时异常,被程序的try catch捕获处理了,response了401并拦截,前端的cookie没有设置有效时间(可通过过期后跳回登录界面时,在header里还能看到上次的token),前端的response拦截器收到401跳回login。
解析JWT过期
//通过Claims claims = Jwts.parser()一步步跟进
public static JwtParser parser() {
return new DefaultJwtParser();
}
====================================================
public DefaultJwtParser() {
this.clock = DefaultClock.INSTANCE;
//获取解析token时当前时间public Date now() {return new Date();}
this.allowedClockSkewMillis = 0L;
}
====================================================
Jws<Claims> parseClaimsJws(String var1) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
//获取claim对象时比对过期时间 ,过期抛ExpiredJwtException