一、会话技术和令牌技术
1、概念:在java中会话技术就是浏览器和服务器之间的连接,一次连接代表一次会话
2、会话跟踪技术:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
为什么要引入会话跟踪技术:在应用系统服务端,每时每刻都会接收请求或者会话,那么服务器是如何不会混淆这些请求呢,如何判断这些来自哪个客户端,这时候就需要用到会话跟踪技术
3、会话跟踪技术的几种方式
- Cookie(客户端会话跟踪技术)
-
- 数据存储在客户端浏览器当中
- Session(服务端会话跟踪技术)
-
- 数据存储在储在服务端
- 令牌技术
Cookie:(客户端会话跟踪技术)
Cookie是客户端会话跟踪技术,存储在客户端浏览器,在浏览器与服务端第一次建立会话(登录)的时候,客户端(浏览器)会建立Cookie,Cookie中一般会存储一定的身份识别信息,例如ID或username,这样在客户端后面的请求中都会自动携带Cookie,交由服务端识别判断。
优缺点
- 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
- 缺点:
- 移动端APP(Android、IOS)中无法使用Cookie
- 不安全,用户可以自己禁用Cookie
- Cookie不能跨域(例子:服务端集群状态)
Session:(服务端会话技术)
Session是基于Cookie技术来实现的,它与Cookie的区别在于,Cookie是将用户的身份信息之间存储在客户端浏览器中,而Session是将用户身份信息存储在服务器端,并生成一串ID反馈给浏览器,浏览器每次请求即可携带这个ID进行身份的辨别。
优缺点
- 优点:Session是存储在服务端的,安全
- 缺点:
- 服务器集群环境下无法直接使用Session
- 移动端APP(Android、IOS)中无法使用Cookie
- 用户可以自己禁用Cookie
- Cookie不能跨域
上述两种技术(Cookie和Session)都有一定的局限性,比如集群环境无法实现,而往往现实的服务器环境都要求采用集群提高运行效率,那么此时上述两种方法就不能满足要求,这时候就引入了一种新的技术令牌技术,这是完全基于前后端代码实现的,不受客户端和服务端环境的影响,可满足集群或移动端的使用,弥补了之前两种技术的缺点,也是目前应用最广泛的会话跟踪技术。
令牌的原理:
令牌的原理与Session几乎一致,也是在客户端第一次与服务端建立连接(登录)的时候,由服务端生成的一串用于身份识别的字符串(token)并反馈给前端,由前端代码控制存储在浏览器的存储空间中,后续前端的请求由前端代码控制将这个token携带在请求头或者请求体中,交由服务端解析,与Session不同的时候令牌的所有实现都是由代码控制的。
优缺点
- 优点:
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器的存储压力(无需在服务器端存储)
- 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
常用令牌技术:JWT令牌
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来
JWT令牌最典型的应用场景就是登录认证:
- 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。
- 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
- 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。
JWt令牌的生成和解析:
1、导入依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、创建工具类或者直接导入
public class JwtUtils {
private static String signKey = "itheima";//签名密钥
private static Long expire = 43200000L; //有效时间
/** * 生成JWT令牌 * @param claims JWT第二部分负载 payload 中存储的内容 * @return */
public static String generateJwt(Map<String, Object> claims){//claims可以添加用户的相关信息ID,username等
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间
.compact();
return jwt;
}
/** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分负载 payload 中存储的内容 */
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}
二、过滤器和拦截器
上述会话跟踪技术中可以让系统可以识别会话请求的所属客户端,那么在应用前端发送请求的时候每次都需要进行一次判断该请求是否携带了令牌,这样写起来代码重复率高且效率低,于是引入了拦截器和过滤器,这两种技术功能类似,都可以拦截客户端发送到后端的请求,那么就可以将令牌的识别放在过滤器或者拦截器中进行统一判断,判断通过则放行请求,否则拒绝请求。
1、过滤器(Filter)的使用
(1)、定义一个类,实现 Filter 接口,并重写其所有方法。(重点在doFilter()上)
(2)、配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
定义过滤器
//定义一个类,实现一个标准的Filter过滤器的接口
public class DemoFilter implements Filter {
@Override //初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
@Override //拦截到请求之后调用, 调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
}
@Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
添加启动类@ServletComponentScan注解
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
这样就完成了一个过滤器的定义,后续的请求就可以在过滤器的doFilter方法中,去实现自定义的过滤
应用示例:登录验证,解析JWT令牌
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//1.获取请求路径
//强转成http子类方便获取请求路径
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURL = httpRequest.getRequestURL().toString();
log.info("请求已拦截:"+requestURL);
//2.判断请求路径是否为login
if (requestURL.contains("login")){
log.info("放行登录方法");
//判断为登录请求后直接放行
chain.doFilter(request, response);
//放行结束后直接结束方法
return;
}
//3.获取token
String token = httpRequest.getHeader("token");
log.info("获取token值为:{}",token);
//4.判断是否携带token
if(token==null){
//写入返回的json并结束方法
httpResponse.getWriter().write("{\"code\": 0,\"msg\": \"NOT_LOGIN\"}");
log.info("token为null,拒绝访问");
return;
}
//5解析token
try {
JwtUtils.parseJWT(token);
} catch (Exception e) {
log.info("token解析失败,拒绝访问");
httpResponse.getWriter().write("{\"code\": 0,\"msg\": \"NOT_LOGIN\"}");
return;
}
chain.doFilter(request, response);
}
2、拦截器Interceptor
定义:
- 是一种动态拦截方法调用的机制,类似于过滤器。
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
拦截器的作用:
- 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
拦截器的使用:
-
-
- 定义拦截器
- 注册配置拦截器
-
1、定义拦截器,核心方法(preHandle)
//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //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("afterCompletion .... ");
}
}
2、注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}
3、实际代码应用:登录校验
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginCheckInterceptor preHandle");
//获取token String token = request.getHeader("token");
if (token==null){
//token不存在则不放行 String notLogin = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.getWriter().write(notLogin);
return false;
}
try {
JwtUtils.parseJWT(token);
} catch (Exception e) {
String notLogin = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.getWriter().write(notLogin);
return false;
}
return true;
}
3、拦截器和过滤器的区别:
1、拦截器是基于java的反射机制的,而过滤器是基于函数回调(职责链)
2、过滤器依赖与servlet容器,而拦截器不依赖与servlet容器
3、拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用
4、拦截器可以访问action上下文、值栈里的对象,而过滤器不能
5、在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次
三、全局异常处理
在代码运行中,会存在一定的异常现象像编译时异常和运行时异常,正常的处理方式一般时抛出或者try。。catch,处理少量的代码可以,但是在项目代码较多往往存在许多的异常,如果每个可能发生的异常都去抛出最后会提交到JVM中,这样不进行处理,系统就很不友好,如果每个异常全都try...catch那么代码就会很臃肿,可读性就很差,那么如何解决这个问题呢?Spring框架提供了一个全局异常处理的思想,就是专门定义一个类,用来捕获整个项目中所有代码可能会产生的异常,进行统一处理。
如何使用:
-
-
- 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
- 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
-
示例代码:
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息
//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}