SpringBoot前后台交互 -- 登录功能实现(拦截器+异常捕获器)
1. 登录
1.1 前端
实现登录功能,需要前端向后端发送请求,前端请求如下:
methods: {submitForm() {console.log(this.ruleForm)axios({method:"post",url:"/auth/login",data:this.ruleForm}).then(res =>{alert(res.data)if (res.data.code=="200"){// 登录成功提示this.$message.success(res.data.msg)// 存储用户名到CookieCookies.set("empname",this.ruleForm.username)// 两秒后跳转到员工页面setTimeout(function (){location.href = "emp.html"},2000)}else{// 登录失败提示this.$message.error(res.data.msg)}}).catch(res =>{// 网络请求异常处理})},resetForm(formName) {this.$refs[formName].resetFields();}}
- emp.html页面
/*在页面被加载的时候就去请求后台*/mounted() {console.log("init....")/*发起请求 获取数据 把数据交给渲染层展示*/this.init()/*获取用户的登录信息*/this.username = Cookies.get("empname")/*定时器获取在线人数*/var _this = this// setInterval(function (){// axios.get("emp/getCount").then(resp=>{// console.log("在线人数:"+resp.data)// _this.personcount = resp.data// })// },5000)},
前端发送请求之后需要后端接受请求,进行响应
1.2 后端
1.2.1 Emps实体类接收
package com.gaohe.ssm1.pojo;import lombok.Data;
import org.springframework.context.annotation.PropertySource;@Data
public class Emps {private int id;private String username;private String password;private String addr;private int age;private String phone;}
但是我们会发现前端页面的参数是这样的:
单靠Emps是无法接收number字段的,所以在这里我们可以创建一个vo类,继承Emps类,使之可以同时接收username、password和number字段
package com.gaohe.ssm1.vo;import com.gaohe.ssm1.pojo.Emps;
import lombok.Data;public class LoginVo extends Emps {private boolean number;public boolean getNumber() {return number;}public void setNumber(boolean number) {this.number = number;}
}
1.2.2 Controller层
主要实现以下几个功能:
- 根据用户名查询数据库用户信息
- 根据查到的用户信息进行登陆判断
- 若number为true,需要将用户名和密码存入session,方便前端保存cookies
package com.gaohe.ssm1.controller;import com.gaohe.ssm1.common.R;
import com.gaohe.ssm1.pojo.Emps;
import com.gaohe.ssm1.service.EmpsService;
import com.gaohe.ssm1.vo.LoginVo;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/auth")
public class AuthController {@Autowiredprivate EmpsService empsService;@PostMapping("/login")public R login(@RequestBody LoginVo loginVo, HttpSession session, HttpServletResponse response){
// 接收参数
// System.out.println(loginVo.getNumber());String username = loginVo.getUsername();String password = loginVo.getPassword();Emps emp1 = empsService.findByUserName(username);if (emp1 == null){return R.fail("用户名不存在");}if (!emp1.getPassword().equals(password)){
// System.out.println(password);
// System.out.println(emp1.getPassword());return R.fail("密码输入错误");}boolean number = loginVo.getNumber();if (number){// 创建两个 Cookie,用于保存用户名和密码Cookie username1 = new Cookie("username1", emp1.getUsername());Cookie pass1 = new Cookie("pass1", emp1.getPassword());// 设置 Cookie 的最大生存时间为 7 天(单位:秒)username1.setMaxAge(60 * 60 * 24 * 7);pass1.setMaxAge(60 * 60 * 24 * 7);// 设置 Cookie 的路径为根路径,确保在整个应用中可用username1.setPath("/");pass1.setPath("/");// 将 Cookie 添加到响应对象中response.addCookie(username1);response.addCookie(pass1);}session.setAttribute("username",emp1.getUsername());return R.success("登录成功");}
}
1.2.3 mapper层
package com.gaohe.ssm1.mapper;import com.gaohe.ssm1.pojo.Emps;
import org.apache.ibatis.annotations.*;import java.util.List;@Mapper
public interface EmpsMapper {// 查询所有@Select("select * from emps")public List<Emps> list();// 通过id查询@Select("select * from emps where id = #{id}")public Emps findById(int id);// 通过name查询@Select("select * from emps where username = #{username}")public Emps findByUserName(String username);// 新增@Insert("insert into emps(id,username,password,addr,age,phone) " +"values (null ,#{username},#{password},#{addr},#{age},#{phone})")public int save(Emps emps);// 修改@Update("update emps set username=#{username},password = #{password}," +"addr=#{addr},age=#{age},phone=#{phone} where id = #{id}")public int update(Emps emps);// 删除@Delete("delete from emps where id = #{id}")public int delete(int id);
}
1.2.4 service层
package com.gaohe.ssm1.service;import com.gaohe.ssm1.pojo.Emps;import java.util.List;public interface EmpsService {// 查询所有public List<Emps> list();// 通过id查询public Emps findById(int id);public Emps findByUserName(String username);// 新增public int save(Emps emps);// 修改public int update(Emps emps);// 删除public int delete(int id);
}
1.2.5 实现类
@Overridepublic Emps findByUserName(String username) {return empsMapper.findByUserName(username);}
2. SpringMvc拦截器
2.1 概念
我们登录网站时经常会看到登陆成功后可以查询到页面信息,进行一系列操作,如果我们跳过登录直接访问网站是不可以的,这可以极大的保护信息安全,实现拦截可以用到springMvc拦截
Spring MVC拦截器(Interceptor)是基于Java反射机制和动态代理实现的,用于在请求处理的不同阶段进行拦截和处理的一种机制。它类似于Servlet中的Filter,但提供了更精细的控制能力。
拦截器的主要特点:
- 基于AOP思想实现
- 与Spring框架深度集成
- 可以获取Spring容器中的Bean
- 针对Handler(Controller方法)进行拦截
作用
- 预处理(PreHandle)
- 在Controller方法执行前拦截
- 常用于:权限验证、参数校验、日志记录
- 后处理(PostHandle)
- 在Controller方法执行后,视图渲染前拦截
- 常用于:修改ModelAndView、记录响应数据
- 完成后处理(AfterCompletion)
- 在整个请求完成后拦截(视图渲染完成)
- 常用于:资源清理、性能监控
与Filter的区别
1. 拦截器是Spring MVC框架层面的,Filter是Servlet规范层面的
2. 拦截器可以获取Spring容器中的Bean,Filter不能
3. 拦截器可以针对具体Controller方法,Filter只能针对URL
4. 拦截器有更精细的生命周期控制(pre/post/complete)
拦截器通过实现HandlerInterceptor接口或继承HandlerInterceptorAdapter类来创建,然后在Spring MVC配置中注册
2.2 实战
- LoginInterceptor 声明拦截器,扫描加载bean
package com.gaohe.ssm1.interceptor;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.security.auth.login.LoginException;/*** 登录拦截器,用于处理用户登录状态的验证* 该拦截器会在请求到达Controller之前进行预处理*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 前置拦截@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle login interceptor");
// 1.获取路径 2.静态资源放行
// 3.登录就放行Object username = request.getSession().getAttribute("username");if (username != null){return true;}
// 4.没有登录 拦截response.getWriter().write("notlogin");true 拦截放行 false 拦截return false;// throw new LoginException("notlogin");}// 后置拦截@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle login interceptor");}// 最终拦截@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion login interceptor");}
}
- 添加拦截器设定拦截访问路径
package com.gaohe.ssm1.config;import com.gaohe.ssm1.interceptor.LoginInterceptor;
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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
@EnableWebMvc
@Slf4j
public class SpringMvc implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {
// 拦截下路径/pages/** 放行到 /pages/registry.addResourceHandler("/pages/**").addResourceLocations("classpath:/pages/");
// 打印日志log.info("静态资源已经放行");}// 拦截器配置@Autowiredprivate LoginInterceptor loginInterceptor;// 拦截路径配置@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/emps");}
}
- 前端请求拦截
如果响应notlogin,前端则进行页面跳转,跳转到登录页面进行登录
axios.interceptors.request.use(req => {return req;
})
axios.interceptors.response.use(resp => {if(resp.data == "notlogin") {alert("用户未登录")setTimeout(function () {window.location.href = "/pages/login.html"}, 2000)}return resp;
})
3. 异常捕获器
3.1 概念
Spring MVC异常捕获器是Spring框架提供的一套统一异常处理机制,主要用于集中处理Controller层抛出的各种异常。核心注解是@ExceptionHandler
,它允许开发者在Controller内部或通过@ControllerAdvice
全局定义异常处理方法。
3.2 主要组件
-
@ExceptionHandler - 标注在方法上,定义处理特定异常的逻辑
-
@ControllerAdvice - 配合
@ExceptionHandler
实现全局异常处理 -
ResponseEntityExceptionHandler - Spring提供的默认异常处理基类
3.3 实战
那我们项目中比较常见的sql异常为例
-
定义全局异常处理器
@RestControllerAdvice 是 Spring 框架中的一个组合注解,结合了 @ControllerAdvice 和 @ResponseBody 的功能,用于全局处理控制器(Controller)中抛出的异常,并统一返回结构化的响应数据。
@ExceptionHandler 是 Spring 框架中的一个注解,用于处理控制器(Controller)中抛出的异常。它通常用在方法上,表示该方法用于捕获和处理当前 Controller 中发生的特定异常。但是只能处理当前 Controller 类中抛出的异常。
package com.gaohe.ssm1.handler;import com.gaohe.ssm1.common.R;
import com.mysql.cj.jdbc.exceptions.MysqlDataTruncation;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.sql.SQLIntegrityConstraintViolationException;/*** 全局异常处理器,用于捕获和处理数据库相关异常。* 结合Spring的@RestControllerAdvice注解,实现全局异常统一响应格式。*/
@RestControllerAdvice
public class SQLHandler {
// 同名异常@ExceptionHandler(SQLIntegrityConstraintViolationException.class)public R ex2(SQLIntegrityConstraintViolationException e){String msg = e.getMessage();if (msg.contains("Duplicate entry")){String[] split = msg.split(" ");msg= split[2]+"用户名已存在";}return R.fail(msg);}// 字段过长 异常@ExceptionHandler(MysqlDataTruncation.class)public R ex1(MysqlDataTruncation e){String msg = e.getMessage();if (msg.contains("Data too long")) {String[] split = msg.split(" ");if (split.length > 8) { // 确保数组长度足够避免越界msg = split[8] + "输入过长";}}return R.fail(e.getMessage());}}
-
前端接收到后端异常信息,渲染到页面
-
执行优先级
-
优先匹配当前Controller中的
@ExceptionHandler
-
其次匹配
@ControllerAdvice
中定义的处理器 -
最后是Spring默认的异常处理机制
4.优化
学习异常捕获器之后,我们可以将SpringMvc拦截器的代码进行优化,创建一个登录的异常捕获类和异常捕获器用来捕获未登录的异常,从而给前端以响应,优化代码如下:
- LoginException登录异常类
package com.gaohe.ssm1.common;public class LoginException extends RuntimeException{public LoginException(String message) {super(message);}
}
- LoginHandler登陆异常捕获器
package com.gaohe.ssm1.handler;import com.gaohe.ssm1.common.LoginException;
import com.gaohe.ssm1.common.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
public class LoginHandler {@ExceptionHandler(LoginException.class)public R ex(LoginException e){System.out.println(e.getMessage());return R.fail(e.getMessage());}
}
- LoginInterceptor登录拦截器,如果验证不通过则抛出异常
package com.gaohe.ssm1.interceptor;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.security.auth.login.LoginException;/*** 登录拦截器,用于处理用户登录状态的验证* 该拦截器会在请求到达Controller之前进行预处理*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 前置拦截@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle login interceptor");
// 1.获取路径 2.静态资源放行
// 3.登录就放行Object username = request.getSession().getAttribute("username");if (username != null){return true;}
// 4.没有登录 拦截
// response.getWriter().write("notlogin");true 拦截放行 false 拦截
// return false;throw new LoginException("notlogin");}// 后置拦截@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle login interceptor");}// 最终拦截@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion login interceptor");}
}
- 前端拦截器给用户做出回应
axios.interceptors.request.use(req => {return req;
})
axios.interceptors.response.use(resp => {if(resp.data == "notlogin") {alert("用户未登录")setTimeout(function () {window.location.href = "/pages/login.html"}, 2000)}return resp;
})
大致流程给大家画了个图,方便大家理解