登录认证
需求:输入登录请求服务器判断用户的用户名和密码
//控制层
@PostMapping("/login")
public Result login(@RequestBody Emp emp);
@Override
public Result login(Emp emp) {
Emp emp1 = empService.selectLogin(emp);
if(emp1 == null){
System.out.println("用户名或密码错误");
return Result.error("登录失败");
}else{
System.out.println("登录成功");
Map<String,Object> map = new HashMap<>();
map.put(emp1.getUsername(),emp1.getPassword());
String secret = JWTUtils.generateJwt(map);
return Result.success(secret);
}
}
//业务层
Emp selectLogin(Emp emp);
@Override
public Emp selectLogin(Emp emp) {
return empMapper.selectLogin(emp);
}
//持久层
@Select("select * from emp where username = #{username} and password = #{password}")
Emp selectLogin(Emp emp);
过滤:
如果没有过滤,用户可以直接通过链接直接访问功能,绕过登录.
所以我们需要
将登录成功的信息进行保存和封装记录成为登录成功的标记.
进行判断和拦截
使用
Filter过滤器和Interceptor拦截器
但是因为HTTP是无状态的,不能在多次请求之间共享数据,所以我们需要使用会话跟踪技术解决
会话跟踪技术解决
会话:
用户打开浏览器,访问web服务器的资源,会话建立,直到一方断开连接结束会话.
在一次会话中可以包含多次请求和响应
从浏览器发出请求到服务端,服务端再响应数据给前端,就完成了一次会话的建立
如果建立会话后,浏览器和服务端都没有被关闭,就会持续会话直到一方结束
中途可以一直使用该会话进行请求的发送和响应
会话跟踪:
一种维护浏览器的方法,服务器需要识别多次请求是否来自于同一浏览器,以便于在同一次会话的多次请求间共享数据
服务器会收到多个请求,多个请求可能来自于多个浏览器
所以浏览器需要进行以下操作
使用会话跟踪来进行识别
识别请求是否来自于同一个浏览器
识别浏览器后在同一个会话中多次请求间共享数据
实现会话跟踪
客户端会话跟踪技术:Cookie
服务端会话跟踪技术:Session
都可以实现会话跟踪,但Cookie存储在浏览器端,而Session是存储在服务端
请求时创建唯一id的session保存在服务端,响应时封装成cookie在响应头中返回给客户端
传统会话跟踪的问题(服务器集群,客户端多样化)
服务器集群:
服务器的并发访问量有限,需要通过代理服务器来分配访问给多个服务器(集群中数据无法共享)
问题主要体现在两个方面
服务器集群环境下Session的共享问题
移动端APP端无法使用Cookie
所以我们使用
令牌技术
登录请求时,如果登录成功可以给前端响应一个令牌(一个特殊的字符串,代表每个用户合法的身份凭证)
前端将登录返回的令牌记录下来保存在自己的客户端
在后续的请求中每次请求都会携带该令牌,在之后服务端使用Filter或Interceptor对所有请求进行拦截并校验,获取请求中携带的令牌进行判断,如果合法就放行,如果不合法就返回错误信息并跳转到登录页面.
解决了集群环境下的认证问题,减轻服务器的存储压力
支持PC端,移动端
JWT令牌
JSON Web Token
一个开发的行业标准,定义了一种简介的,自包含的协议格式,用于在通信双方传递JSON对象,传递的信息经过数字签名可以被验证和信任.
分为三个部分
Header(头)
记录令牌类型,签名算法等,使用Base64编码
Payload(有效载荷)
携带用户信息和过期信息,使用Base64编码
Signature(签名)
防止Token被篡改,确保安全性,是一个字符串
使用秘钥加密
校验时的签名秘钥必须与生成令牌时的秘钥一致.
如果JWT令牌解析校验时报错,说明JWT令牌被篡改或失效了,令牌非法.
使用json进行数据传输,通用型广泛,体积小,便于传输
无需在服务器端保存相关信息
jwt载荷部分可以存储业务相关信息(不能是敏感信息)
生成令牌
public class JWTUtils {
private static String signKey = "cfjg";
// private static Long expire = 43200000L;
public static String generateJwt(Map<String,Object> claims){//生成令牌
String jwt = Jwts.builder().addClaims(claims)
//添加数据
.signWith(SignatureAlgorithm.HS256,signKey)
//设置算法
.setExpiration(new Date(System.currentTimeMillis() + 1000L*3*60))
//设置过期时间
.compact();
//生成
return jwt;
}
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
//设置秘钥
.parseClaimsJws(jwt)
//解析令牌
.getBody();
//获取数据
return claims;
}
}
过滤器Filter
JavaWeb三大组件之一(Servlet,Filter,Listener)
底层使用动态代理,对请求进行拦截和处理
对token进行判断和解析,如果失败就跳转回login,如果成功就正常访问
public class Filter implements javax.servlet.Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//请求对象,响应对象,拦截对象
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
String url = httpServletRequest.getRequestURI();
System.out.println(url);
if(url.matches(".?login.?")){
System.out.println("login:登录页面");
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}else{
String token = httpServletRequest.getHeader("token");
if(token == null || token.equals("")){
httpServletResponse.getWriter().write(JSONObject.toJSONString(Result.error("NOT_LOGIN")));
System.out.println("令牌错误");
return;
}
try{
JWTUtils.parseJWT(token);
System.out.println("令牌正确");
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}catch (Exception e){
// filterChain.doFilter(httpServletRequest,httpServletResponse);
httpServletResponse.getWriter().write(JSONObject.toJSONString(Result.error("NOT_LOGIN")));
System.out.println("令牌错误");
return;
}
}
}
}
Interceptor拦截器
注册拦截器
@Configuration
public class InterceptorRegist implements WebMvcConfigurer {//注册拦截器
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**");
}
}
使用拦截器
package com.example.tlias.Interceptor;
import com.alibaba.fastjson.JSONObject;
import com.example.tlias.pojo.Result;
import com.example.tlias.util.JWTUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {//过滤前
String url = httpServletRequest.getRequestURI();
System.out.println(url);
if(url.matches(".?login.?")){
System.out.println("interceptor running");
return true;//放行
}else{
String token = httpServletRequest.getHeader("token");
if(token == null || token.equals("")){
httpServletResponse.getWriter().write(JSONObject.toJSONString(Result.error("NOT_LOGIN")));
System.out.println("interceptor running");
return false;//拦截
}
try{
JWTUtils.parseJWT(token);
System.out.println("interceptor running");
return true;
}catch (Exception e){
// filterChain.doFilter(httpServletRequest,httpServletResponse);
httpServletResponse.getWriter().write(JSONObject.toJSONString(Result.error("NOT_LOGIN")));
System.out.println("interceptor running");
return false;
}
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {//过滤后
System.out.println("方法执行结束");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//请求结束
System.out.println("请求结束");
}
}
Filter和Interceptor的区别
接口规范的不同:过滤器要实现Filter接口,拦截器需要实现HandlerInterceptor接口
拦截范围不同:过滤器Filter会拦截所有资源,但Interceptor只会拦截Spring环境中的资源
异常处理
在SpringBoot项目中的异常如果从控制层向上抛出会暴露给用户,这是不被允许的.
处理异常的方案
1,在Controller中使用trycatch进行处理(过于冗长)
2,全局异常处理器(推荐)
由SpringMVC提供,接收所有Controller中产生的异常,一般在exception包下定义
@RestControllerAdvice//相当于@ResponseBody+@ControllerAdvice
public class GlobalExceptionHandler {//全局异常处理器
@ExceptionHandler(Exception.class)//接收需要处理的异常的字节码文件
public Result exceptionHandler(Exception exception){
return Result.error(exception.getMessage());
}
}