上篇中主要负责后端部分完成了一个简单的学习辅助系统部分界面,主要针对增删改查进行了练习,过程中遇到了一些细节上的问题以及当时做的时候去查阅的一些之前没有太注意到的额外知识,所以还需要进行进一步梳理,像登录校验的方法以及cookie、session、jwt的区别和联系以及Filter和Interceptor区别和使用方式等。(学习辅助系统)
目录
1.环境搭建
2.登陆校验(重要)
2.1 会话技术
②session
③令牌技术
3.过滤器Filter
4.拦截器 Interceptor
1.环境搭建
一个简单的单体架构项目,对于后端部分来说,首先需要进行环境搭建,准备好数据库表,然后创建springboot工程,引入相应的起步依赖,然后在配置文件中引入相应的配置信息(mybatis mysql等)。前端、后端、数据库整体关系如下图所示:
在yml配置文件中有一些需要注意的地方,很容易出现错误,如下图所示:
这里简单介绍一下@Value和@ConfigurationProperties注解的区别:
@ConfigurationProperties注解可以批量注入配置文件中的属性,支持松散绑定(松散语法),不支持SpEL,支持JSR303数据校验和复杂类型封装。
@Value注解需要一个个指定,支持三种取值方式,分别是 字面量、${key}从环境变量、配置文件中获取值以及 #{SpEL},不支持松散绑定(松散语法)、JSR303数据校验和复杂类型封装,支持SpEL。
松散语法的意思就是一个属性在配置文件中可以有多个属性名。
SpEL 使用 #{…} 作为定界符 , 所有在大括号中的字符都将被认为是 SpEL , SpEL 为 bean 的属性进行动态赋值提供了便利。
复杂类型封装指的是,在对象以及 map (如学生类中的老师类以及 scores map)等属性中,用 @Value 取是取不到值
2.登陆校验(重要)
首先用户登录之后会存一个登陆标记,然后在每一次发送请求之后会进行统一拦截,取出登陆标记,如果可以取出的话说明用户已经登陆了,所以可以直接放行,对于没有标记的需要进行拦截,无法跳转到对应界面。
登录校验的四种方法:
下面两种是用于跟踪:1、会话技术2、JWT令牌
下面两个是用于拦截校验的,可以进行Jwt令牌校验:3、过滤器Filter 4、拦截器Interceptor
2.1 会话技术
下面的请求1,2,3是同一个浏览器发出的,所以是同一个会话,可以进行共享数据;但是采用http协议时,该协议每次请求都是独立上一个请求和下一个请求没有联系,所以两次请求不知道是否是一个浏览器发出的,该协议效率比较高,但是不好确定是否是同一会话从而进行共享数据,所以要用到会话跟踪方案(会话跟踪是一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一个浏览器,确保同一次会话之间的请求可以共享数据。)
①cookie
Cookie是一段文本数据,在用户完成”身份认证或会话状态更新“后,它会被服务端放在HTTP响应包的
Set-Cookie
中,并发送给客户端进行保存会话跟踪:首先客户端发送请求后例如login请求,服务端会设置一个cookie,然后响应数据时会自动连带cookie也发送到客户端,然后客户端会自动保存,然后下次请求的时候会连带这个cookie自动发送给服务端,然后服务端进行检查是否可以检查到cookie相应的值,进而会话跟踪;
cookie的优缺点:
优点:是HTTP协议中支持的技术
缺点:1.移动端APP无法使用cookie 2.cookie存储在客户端,相对来说不安全,并且可以将cookie进行拦截,并且用户可以自己去禁用cookie 3.cookie不能跨域使用
举个例子:浏览器先去访问前端ip为192.168.150.200,端口号为80,此时发送登录请求到服务端ip地址为192.168.150.100,端口号为8080,ip和端口号都不同,说明已经跨域了,此时就不能用cookie了;如下图:
②session
session是基于cookie的,他存储在服务端,相对来说安全一些。
原理:
浏览器第一次请求的时候,服务器里面会进行存储,生成一个sessionID(唯一的),然后通过cookie发送给浏览器。每次访问的浏览器不同的时候,服务器就会进行上面的操作,生成sessionID并且发送给浏览器。当同一个浏览器去访问服务器的时候,由于之前存储过sessionID,因此在登录的时候会把sessionID一起发送给服务器,登录成功之后,就会把身份信息存储到对应的sessionID下面。下次再登录,服务器发现这个sessionID有对应的身份信息,即登录过,就不需要再次登录了。
但是在服务器集群环境下不能直接使用session,后续篇章会介绍点评项目中使用redis代替session解决集群环境下的问题。本身session是基于cookie的,所以一些cookie的缺点也会带过来。
小结:cookie和session都是传统的方案,现在主流的是令牌技术。
③令牌技术
原理:首先浏览器向服务端发送请求,然后此时服务端就会生成一个令牌,然后响应的时候将令牌响应到前端,然后前端会将此令牌存储起来,然后每次发送请求都会将此令牌发送给服务端,然后服务端进行校验,如果令牌是有效的那么就相当于是同一个会话,可以进行共享数据,例如,login请求,然后接着查询请求,先校验令牌成功后说明已经登陆过,无需再登录。
需要自己生成,如何存储,如何携带到服务端需要自己实现。令牌是存储到客户端的,是比较安全的;因为就算篡改了令牌,后面发送请求校验的时候会被检测出来。
JWT令牌:
JWT(Json Web Token)由三个部分组成,第一个部分就是头部,alg是签名算法,type是令牌类型;第二个部分是有效载荷,可以携带自定义的信息;这两个部分都是Json类型的,并且这两部分内容通过Base64编码之后就会用这64个字符来表示内容;第三部分是数字签名,是计算出来的,并不是通过Base64编码得到的,正是有了这个部分保证了安全性。
3.过滤器Filter
过滤器是javaweb三大组件(Servlet、Filter、Listener)之一,可以把对资源的请求拦截下来,然后进行一些特殊的处理(校验、敏感字符处理等)。
Filter的练习例子:
1.定义Filter:定义一个类,实现Filter接口,然后重写方法
2.配置Filter:Filter类上加@WebFilter注解,配置拦截请求的路径,引导类上加@servletComScan注解开启Servlet组件支持。
整体的逻辑如下图:
Filter路径拦截:在@WebFilter注解中配置
过滤器链:
一个web应用中,可以配置多个过滤器,多个过滤器就构成了过滤器链。
注意,注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。
举个实践的例子:
下面实现了一个简单的登录校验Filter流程:
- 首先获取拦截url
- 判断url中是否包含login,如果包含,说明是登录请求,此时需要放行。然后服务端生成jwt令牌,然后保存到请求头中。
- 获取请求中的令牌(token)
- 判断令牌是否存在,如果不存在,返回错误结果 未登录
- 如果令牌存在,解析令牌,解析失败返回错误信息。
- 放行
具体代码如下:
//过滤器的校验:这个是最先执行的,拦截后的校验,这部分校验后才会执行controller层中的代码;例如,如果是登录请求,此处放行之后才会执行controller层的方法LoginController里面的访问web资源以及生成jwt令牌操作。 @Slf4j //@WebFilter("/*") public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //在Http协议中使用的是HttpServletRequest,所以要强转一下; HttpServletRequest req=(HttpServletRequest) servletRequest; HttpServletResponse resp=(HttpServletResponse) servletResponse; //1.获取请求url String url=req.getRequestURL().toString(); log.info("请求的url为:"+url); //2.判断请求中是否包含login请求,如果包含说明是登录请求,放行; if(url.contains("login")){ log.info("登录请求,放行!"); filterChain.doFilter(servletRequest,servletResponse); return;//就不需要执行后面的部分了; } //3.获取请求头token中的令牌 String jwt=req.getHeader("token"); //4.判断令牌是否存在,如果不存在,返回错误结果(未登录) if(!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录信息!"); //注意,在controller层中有@RestController注解可以将返回的类型转化为json格式,此处并不是controller层并没有这个注解,所以要自己转换,在pom文件中引入一个fastJSON的依赖 Result error=Result.error("NOT_LOGIN"); String notLogin= JSONObject.toJSONString(error);//这样返回一个json格式的字符串 resp.getWriter().write(notLogin);//然后通过resp响应给浏览器 return; } //5.解析token,如果解析失败,返回错误结果(未登录) try { JwtUtils.parseJWT(jwt); } catch (Exception e) {//解析失败 e.printStackTrace(); log.info("解析失败,返回未登陆的错误信息!"); Result error=Result.error("NOT_LOGIN"); String notLogin=JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return; } //6.放行 log.info("令牌合法,放行!"); filterChain.doFilter(servletRequest,servletResponse); } }
4.拦截器 Interceptor
拦截器的实现:
首先定义拦截器,实现HandlerInterceptor接口,并重写所有的方法。
然后在配置类中,添加定义的拦截器,并配置拦截路径:
拦截器路径:
实现案例:
//定义一个拦截器, @Component @Slf4j public class LoginCheckInteceptor implements HandlerInterceptor { @Override//目标资源方法执行前执行,返回true放行,返回false不放行 public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { //1.获取请求url String url=req.getRequestURL().toString(); log.info("请求的url为:"+url); //2.判断请求中是否包含login请求,如果包含说明是登录请求,放行; if(url.contains("login")){ log.info("登录请求,放行!"); return true; } //3.获取请求头token中的令牌 String jwt=req.getHeader("token"); //4.判断令牌是否存在,如果不存在,返回错误结果(未登录) if(!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录信息!"); //注意,在controller层中有@RestController注解可以将返回的类型转化为json格式,此处并不是controller层并没有这个注解,所以要自己转换,在pom文件中引入一个fastJSON的依赖 Result error=Result.error("NOT_LOGIN");//TODO 后面不能加!,要严格按照接口文档里写的来做,如果notlogin后面加了!就会导致前端呈现出不一样的反应。 String notLogin= JSONObject.toJSONString(error);//这样返回一个json格式的字符串 resp.getWriter().write(notLogin);//然后通过resp响应给浏览器 return false; } //5.解析token,如果解析失败,返回错误结果(未登录) try { JwtUtils.parseJWT(jwt); } catch (Exception e) {//解析失败 e.printStackTrace(); log.info("解析失败,返回未登陆的错误信息!"); Result error=Result.error("NOT_LOGIN"); String notLogin=JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return false; } //6.放行 log.info("令牌合法,放行!"); return true; } @Override//目标资源方法执行后执行 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); } @Override//视图渲染完毕后运行,最后运行 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterHandle..."); } }
//TODO 注册拦截器,将定义好的拦截器添加进去。 @Configuration//表明是一个配置类 public class WebConfig implements WebMvcConfigurer { @Autowired LoginCheckInteceptor loginCheckInteceptor; public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(loginCheckInteceptor).addPathPatterns("/**").excludePathPatterns("/login");//此处要注册一个拦截器并且表明拦截的是什么请求,注册的就是刚刚在Logincheckinteceptor中定义好的拦截器,所以前面要进行依赖注入 //注意此处想要拦截所有请求是/**,过滤器中的是/*,用excludePathPatterns取消拦截指定请求,也就是说直接放行。 } }
注意:如果过滤器和拦截器都存在的话,是这样的执行流程:
Filter过滤器是servlet里面的,拦截范围比较大,interceptor是spring里面的,只有进入spring环境才会进行拦截;在拦截器中,限制性preHandle,如果校验正确,会执行controller层中的方法,访问web资源,然后再执行postHandle和afterCompletion中的方法。执行流程如下图所示:
Filter与Interceptor的区别:
接口规范不同,过滤器实现Filter接口,Interceptor实现的是HandlerInterceptor接口
拦截范围不同:过滤器会拦截所有的资源,拦截器只会拦截spring环境中的资源。