只需要接受请求参数username和password,然后调用接口在数据库表中查询键值匹配的数据项即可
登录校验通常分为两步,一是登录标记,二是统一拦截
会话:用户打开浏览器,访问web服务器的资源,会话建立,知道有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应
会话跟踪:一种维护浏览器的方法,服务器要识别多次请求是否来自于同一浏览器,以便于在同义词会话的多次请求间共享数据。主要有Cookie、Session、JWT令牌技术。
Cookie是存储在客户端服务器的,客户端(浏览器)第一次访问服务器,会在服务器设置一个cookie,服务器响应时会将响应头set-cookie(设置有cookie数据)返回给浏览器,浏览器会将cookie的内容(name=value)存储在浏览器本地。之后浏览器每次想服务器发出请求,都会自动携带cookie,服务器判断出cookie存在,则可以基于cookie在同义词会话的不同请求之间共享数据。
优点:HTTP协议支持的技术,无需手动操作数据的解析和携带等
缺点:移动端APP不适用;不安全,用户可在浏览器禁用cookie;cookie不能跨域(协议、ip/域名、端口)
session是存储在服务端的,底层依然是基于cookie来实现。浏览器第一次请求服务器,那么会话对象不存在,此时服务器会自动创建会话对象session,并设置一个ID,然后响应头set-cookie对应的的内容为(JSSSIONID = ID),浏览器会自动识别响应头,将cookie存到本地。后续的每次浏览器请求,都会携带cookie到服务器,服务器就可以找到对应lD的会话对象session
优点:浏览器获取不到session对象,较安全
缺点:服务器集群的环境不能直接使用;实质上还是cookie,因此不适合移动端、用户可以禁用、不能跨域
由程序员实现令牌的生成、传递与校验
根据自定义令牌获取规则,客户端会生成的JWT令牌返回给浏览器,之后浏览器会在请求头中携带一个token,token的值就是令牌的BASE64编码(是编码,不是加密),令牌原本为JSON格式,包括头部(算法、类型),负载(name、ID等属性),数字签名三个部分。当请求头传递的token为空或无效,则会被拦截。注:令牌是要设置有效期限的。
以登录校验为例:
/** * 生成JWT令牌 * @param claims JWT第二部分负载 payload 中存储的内容 * @return */ public static String generateJwt(Mapclaims){ String jwt = Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, signKey) .setExpiration(new Date(System.currentTimeMillis() + expire)) .compact(); return jwt; } // 使用 @PostMapping("/login") public Result Login(@RequestBody Emp emp){ log.info("员工登录:{}",emp); Emp e = empService.login(emp); //登录成功,生成令牌并下发 if(e!=null){ Map claims = new HashMap<>(); claims.put("id", e.getId()); claims.put("name", e.getName()); claims.put("username", e.getUsername()); String jwt = JwtUtils.generateJwt(claims);//jwt包含了当前登录的员工信息 return Result.success(jwt); } //登录失败,返回错误信息 return Result.error("用户名或密码错误"); }
/** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分负载 payload 中存储的内容 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); return claims; } // 获取并解析(片段) String jwt = request.getHeader("token"); if (!StringUtils.hasLength(jwt)){ log.info("请求头token为空,返回未登录信息"); Result error = Result.error("NOT_LOGIN"); String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false; } try { JwtUtils.parseJWT(jwt); }catch (Exception e){ e.printStackTrace(); log.info("令牌解析失败,返回未登录信息"); Result error = Result.error("NOT_LOGIN"); String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false; }
核心是在Filter接口的doFilter()函数,doFilter()函数中的chain.doFilter(request,response)实现放行,在其前后实现放行前逻辑和放行后逻辑
通过@WebFilter(urlPatterns = "/*")注解设置过滤范围,"/*"表示全过滤,也可以是"/emp",这样只拦截到emp这一级
通常可以用过滤器类名来确定优先级(A>B),也可以设置@Order()注解确定顺序。过滤时,先执行AFilter过滤前逻辑,再执行BFilter过滤前逻辑,然后放行完成Controller中的任务,之后执行BFilter过滤后逻辑,而后是A……
我们可以在过滤器执行前逻辑编写,查看并校验token的代码,来进行登录校验
与过滤器功能相似,由于是springboot框架提供的方法,因此拦截时是先完成filter才进行interceptor的拦截
其核心是HandlerInterceptor接口的preHandler方法,定义放行逻辑,通过则返回true,不通过则返回false
@Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override //目标方法运行前执行,返回为true则放行,false拦截 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 实现放行前逻辑 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("afterCompletion……"); } }
需要定义一个configuration的类,实现WebMvcConfigurer接口的addInterceptor方法来控制拦截范围
//与filter不同,Interceptor的"/*"只拦截本级目录,而"/**"才能拦截全部目录 //配置类如下 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); } }