登录校验
JWT令牌的相关操作需要添加相关依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
一、摘要
场景:当我们想要访问一个网站时,我们一般都需要进行登录验证,这时候就会涉及到一系列的问题
问题:
- 什么时候进行登录校验?
- 登录校验你该如何实现?如何设计高效,高健壮性的校验方式?
- 进行登录校验时,你是以什么方式进行数据传递的?如何实现数据的传递
二、为什么要进行身份校验
试想以下场景,我们在如果我们对一个网站不进行有效的登录校验,而是可以直接通过网址:
http://localhost:90/#/system/emp
直接进入到网站内部,是不是极其不安全,我们应该在响应每一次请求前都进行一次登录校验,如果用户还没有登录。那么就必须先进行登录校验才能进行后续操作
三、拦截器
我们已经知道使用登录校验的使用场景,那么我们该如何设计一个高效的登录校验功能?是在每一个请求处理函数里面都添加一次if判断吗?这显然是不合理的,如果我们一个网站功能很复杂,那么面对如此多的请求并在每一个请求中都添加一个if判断就显得十分low,而且健壮性不高,不利于维护。因此,我们就引出了
拦截器
的概念,我们创建一个工具类,用于处理每一次请求,我们每次向服务器发送请求时,都需要先经过拦截器,经过身份校验后再能传递到对应的处理函数,然后,服务器响应回来的数据同样需要先经过拦截器才能返回给浏览器
四、传递数据三种方式
我们已经知道拦截器的概念了,接下来就很有必要了解
数据传递
这样玩意,由于HTTP 协议是无状态的
,这意味着每个请求都是独立
的,服务器不会保存关于之前请求的任何信息。每次客户端发送请求时,服务器都会独立处理该请求,并在发送响应后忘记关于该请求的任何信息。这就意味着,即使我们已经通过登录页面登录进去了,但是浏览器还是不知道我们已经登录了,因为每一次请求都是独立的,所以后续我们进行的每一次请求,都至少要携带用户登录的信息,如用户名,密码等,这样才能正常通过拦截器,将数据发送给后端继续后续操作
,如果不能实现数据传递,这样就很麻烦了,所以这里就引出了以下几种数据传递的方式,会话跟踪技术
- 客户端会话跟踪技术:Cookie
- 服务器会话跟踪技术:Session
- 令牌技术
这三种方式各有优缺点,以下是三种技术的优缺点对比
简单解释为什么 session会话无法在集群中使用:
在我们进行登录验证时,如果用户没有进行登录过,就会在服务器那边创建一个session记录,在用户下次登录时会自动检测判断用户有没有登录过,但是集群技术呢,会将我们的数据存储在不同的服务器中,而不是只保存在一个服务器,这就导致了如果我们下次访问资源时访问了另一个没有进行登录校验过的服务器,就得重新进行验证,所以无法在集群中有效使用
由于session的底层还是由cookie,所以无法在移动端中使用,而且用户甚至可以禁用掉cookie,慢慢的,cookie和session技术已不再是主流,目前的主流技术就是令牌技术
,这里我们以JWT令牌技术
为例
为什么令牌技术能够解决集群认证,移动端认证等问题?
在我们们的登录请求发送到后端时,后端服务器会检测有没有创建令牌,如果没有,那么就创建令牌并传递回前端,然后前端将令牌保存起来,注意这里的保存可以保存在任意存储空间中,可以保存到cookie,也可以保存在其他地方,在后续的请求中,前端浏览器会
自动
将令牌发送给后端,这样就解决了cookie技术不能在安卓中使用的问题,同时,因为令牌经Base64编码和数字签名加密,安全性也不需要考虑,解决了cookie安全性不高的问题,由于令牌也不在后端服务器中保存,因此也不会存在集群的问题。在令牌发送给后端后会解析令牌,获取其中的数据,这样就能通过登录校验了。
五、相关会话技术的实现
1、cookie技术
@Slf4j
@RestController
public class SessionController {/*** 设置Cookie* @param response 响应* @return 设置Cookie*/@GetMapping("/setCookie")public Result setCookie(HttpServletResponse response){response.addCookie(new Cookie("name", "zhangsan"));log.info("设置Cookie成功, name=zhangsan");return Result.success();}/*** 获取Cookie* @param request 请求* @return 获取Cookie*/@GetMapping("/getCookie")public Result getCookie(HttpServletRequest request){Cookie[] cookies = request.getCookies();for(Cookie cookit : cookies){if(cookit.getName().equals("name")){log.info("获取Cookie成功, name={}", cookit.getValue());}}return Result.success();}}
2、session技术
如果是第一次请求,那么session对象是不存在的,后端服务器会自动创建一个session对象,每个对象都有一个对应的ID,然后服务器响应数据时,会以cookie的方式将sessionID返回给浏览器,即在响应头中添加了一个
Set-Cookie:JSESSIONID=session_ID
,然后浏览器会自动将这个cookie存储在浏览器,然后在后续的每一次请求当中都会将cookie中的数据获取出来并携带到服务端,然后服务器通过请求中的session_ID查看本地有没有这个会话对象,这样就能在同一个会话的多次请求中共享数据了
/*** 设置Session* @param session 会话* @return 设置Session*/@GetMapping("/session1")// 如果session对象不存在,会自动创建一个,如果存在,会直接返回session对象public Result session1(HttpSession session){log.info("HttpSession-s1: {}", session.hashCode());session.setAttribute("loginUser", "tom"); //往session中存储数据log.info("session.getAttribute: {}", session.getAttribute("loginUser"));return Result.success();}/*** 设置Session* @param request 请求* @return 设置Session*/@GetMapping("/session2")public Result session2(HttpServletRequest request){// 如果session对象不存在,会自动创建一个,如果存在,会直接返回session对象HttpSession session = request.getSession();log.info("HttpSession-s2: {}", session.hashCode());Object loginUser = session.getAttribute("loginUser"); //从session中获取数据log.info("loginUser: {}", loginUser);return Result.success(loginUser);}
3、JWT令牌
同样的,第一次请求时没有JWT令牌会创建令牌并返回给前端,前端会将这个令牌保存起来,在以后的每一次请求中,都会在请求头中携带这个令牌(token),然后通过后端拦截器或过滤器的解析验证,进行后续操作
/*** 生成JWT令牌*/@Testpublic void generateJwt() {String signKey = "terminal";Long expire = 43200000L;Map<String, Object> claims = new HashMap<>();claims.put("username", "admin");String jwt = Jwts.builder().addClaims(claims).signWith(SignatureAlgorithm.HS256, signKey).setExpiration(new Date(System.currentTimeMillis() + expire)).compact();System.out.println("jwt = " + jwt);}/*** 解析JWT令牌*/@Testpublic void parseJWT() {String signKey = "terminal";String token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzE3MDQxMjk2fQ.p-UYxhJQY30cYELv8hETYHWMs74D-lo1a0tdi3xXXBU";String jwt = Jwts.parser().setSigningKey(signKey).parseClaimsJws(token).getBody().toString();System.out.println("jwt = " + jwt);}
六、使用过滤器和拦截器进行身份验证
1、过滤器(Filter)
package com.example.filter;import com.alibaba.fastjson.JSONObject;
import com.example.pojo.Result;
import com.example.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;// @WebFilter(urlPatterns = "/*") 表示拦截所有请求
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("demoFilter init");Filter.super.init(filterConfig);}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;// 获取URLString url = request.getRequestURI().toString();if (url.contains("login")) {filterChain.doFilter(servletRequest, servletResponse);return;}// 获取tokenString jwt = request.getHeader("token");log.info("token = {}", jwt);// 如果token为空,但会未登录信号if (!StringUtils.hasLength(jwt)) {Result result = Result.error("NOT_LOGIN");String notLogin = JSONObject.toJSONString(result);response.getWriter().write(notLogin);return;}try {JwtUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("jwt令牌无效");Result result = Result.error("NOT_LOGIN");String notLogin = JSONObject.toJSONString(result);response.getWriter().write(notLogin);return;}//放行filterChain.doFilter(servletRequest, servletResponse);}@Overridepublic void destroy() {log.info("demoFilter destroy");Filter.super.destroy();}
}
2、拦截器(Interceptor)
- 拦截器配置
package com.example.config;import com.example.interceptor.LoginCheckInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 拦截器配置类*/
// Configuration 设置该类为一个配置类
@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginCheckInterceptor loginCheckInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册一个拦截器,拦截除了/login之外的所有请求registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");}
}
- 拦截器处理逻辑
package com.example.interceptor;import com.alibaba.fastjson.JSONObject;
import com.example.pojo.Result;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 拦截器处理逻辑*/
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {@Override// 目标资源方法前执行,true为方行,false为拦截// 与doFilter的fileChain前执行的方法类似public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取tokenString token = request.getHeader("token");// 如果token为空,返回未登录信号if(!StringUtils.hasLength(token)){Result error = Result.error("NOT_LOGIN");String notLogin = JSONObject.toJSONString(error);response.getWriter().write(notLogin);return false;}// 放行return true;}@Override// 目标资源方法后执行后执行// 与doFilter的fileChain后执行的方法类似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");}
}