登录认证——登录功能、登录校验(会话技术、JWT令牌、过滤器Filter和拦截器Interceptor
登录功能1.Controller层对于这种响应数据格式我们如果给Emp实体类加一个pojo其实也可以实现业务逻辑但语义并不太好所以我们直接定义一个用于登录验证的类LoginInfo来返回给前端/** * 登录成功结果封装类 */ Data NoArgsConstructor AllArgsConstructor public class LoginInfo { private Integer id; //员工ID private String username; //用户名 private String name; //姓名 private String token; //令牌 }由于请求路径前缀是/login我们再创建一个LoginController类RestController Slf4j RequestMapping(/login) public class LoginController { Autowired private EmpService empService; /** * 登录验证 * * param emp * return */ PostMapping() public Result login(RequestBodyEmp emp) { log.info(登录验证:{}, emp); LoginInfo loginInfo empService.login(emp); if (loginInfo ! null) { return Result.success(loginInfo); } return Result.error(用户名或密码错误); } }这里我们响应给前端的数据需要动态处理也就是如果Service层给我们返回null也就是未查询到匹配的账号那么返回Result.error,如果不为空将我们在Service层封装好的返回类loginInfo返回给前端2.Service层Override public LoginInfo login(Emp emp) { //根据用户名和密码查询是否有员工数据返回 Emp empDB empMapper.selectByUsernameAndPassword(emp.getUsername(), emp.getPassword()); if (empDB ! null) { // 登录成功 log.info(员工【{}】登录成功, empDB.getName()); LoginInfo loginInfo new LoginInfo(empDB.getId(), empDB.getUsername(), empDB.getName(),); return loginInfo; } return null; }在这一层根据用户名和密码查询员工数据并且因为用户唯一字段约束(UNIQUE CONSTRAINT)所以最多返回一个数据拿一个对象接就行。如果没有查询到数据则返回给这个对象的是null所以我们这里再次进行一个判断如果为null我们就返回给Controller层空值反之我们就可以进行LoginInfo类的封装了3.Mapper层!--根据用户名和密码查询员工-- select idselectByUsernameAndPassword resultTypecom.itheima.pojo.Emp select id,username,name from emp where username #{username} and password #{password} /select其实这里返回全部字段我们Emp也能接收但意义不大登录校验为什么要进行登录校验呢因为登录校验思路当我们的用户登录成功之后会返回一个登录标记用此标记来标注用户登陆成功并且设置统一的拦截层来进行校验如果发现请求有登录标记并且合法且时效性没过那么就可以允许访问否则拦截会话技术会话跟踪服务器是怎么“记住”你的(DeepSeek提供)一、先理解问题HTTP 是无状态的你每发一次请求服务器都当你是第一次来的陌生人。比如你登录了 → 服务器验证通过你刷新页面 → 服务器问你是谁你再登录一次这显然不行。所以需要会话跟踪。二、一个巧妙的例子游乐园的腕带你带孩子去游乐园买票入园工作人员给小孩戴上一个防水腕带上面写着一个编号9527。玩项目时每次去玩项目工作人员看一眼腕带“哦你是9527刚买过票进去吧。”腕带的秘密游乐园里有一本大册子记录着9527→ “已购票家庭套餐下午4点离园”腕带Cookie戴在小孩手上浏览器保存大册子Session游乐园自己保管服务器存储三、映射到 Web 开发游乐园Web 开发腕带编号9527JSESSIONID存在 Cookie 里大册子记录HttpSession对象存在服务器内存工作人员看腕带查册子服务器根据JSESSIONID找到对应用户数据四、一句话总结会话跟踪 第一次见面时发个“身份证号”之后每次来都带上它服务器就知道还是你。验证码、登录状态、购物车都靠这个机制实现。客户端跟踪方案—Cookiecookie 是客户端会话跟踪技术它是存储在客户端浏览器的我们使用 cookie 来跟踪会话我们就可以在浏览器第一次发起请求来请求服务器的时候我们在服务器端来设置一个cookie。比如第一次请求了登录接口登录接口执行完成之后我们就可以设置一个cookie在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名用户的ID。服务器端在给客户端在响应数据的时候会自动的将 cookie 响应给浏览器浏览器接收到响应回来的 cookie 之后会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中都会将浏览器本地所存储的 cookie自动地携带到服务端。接下来在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在如果不存在这个cookie就说明客户端之前是没有访问登录接口的如果存在 cookie 的值就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。我刚才在介绍流程的时候用了 3 个自动- 服务器会自动的将 cookie 响应给浏览器。- 浏览器接收到响应回来的数据之后会自动的将 cookie 存储在浏览器本地。- 在后续的请求当中浏览器会自动的将 cookie 携带到服务器端。为什么这一切都是自动化进行的是因为 cookie 它是 HTP 协议当中所支持的技术而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头- 响应头Set-Cookie设置Cookie数据的- 请求头Cookie携带Cookie数据的类方向能操控什么HttpServletRequest请求浏览器 → 服务器获取请求头、请求参数、请求体HttpServletResponse响应服务器 → 浏览器设置响应头、响应体、状态码Slf4j RestController public class CookieController {//在响应头中设置Cookie(返回给前端保存)GetMapping(/c1) public Result cookie1(HttpServletResponseresponse) {//这里我们利用HttpServletResponse这个类来设置响应头在这里我们可以设置返回给前端的Cookie值response.addCookie(new Cookie(login_username, itheima)); //设置Cookie/响应Cookie(Key:Value)return Result.success(); }// 获取Cookie从请求头中读取前端带回来的Cookie GetMapping(/c2) public Result cookie2(HttpServletRequest request) {//获取请求头中的所有Cookie对象Cookie[] cookies request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals(login_username)) { System.out.println(login_username: cookie.getValue()); //输出name为login_username的cookie} } return Result.success(); } }总结精炼版第一步后端通过response.addCookie(key, value)在响应头中设置 Cookie返回给前端。第二步前端自动保存 Cookie并在后续请求中通过请求头带回来。第三步后端通过request.getCookies()获取请求头中的 Cookie取出对应的值。Cookie技术的优缺点缺点:1.不能跨域(我们现在项目都是前后端分离独立部署一定会涉及到跨域) 2.不安全用户可以自己禁用Cookie(设置安全/隐私) 3移动端APP无法使用Cookie服务器端跟踪方案—Session(基于Cookie)底层基于Cookie(Set-Cookie,Cookie)实现的Slf4j RestController public class SessionController { GetMapping(/s1) public Result session1(HttpSession session){ log.info(HttpSession-s1: {}, session); session.setAttribute(loginUser, tom); //往session中存储数据return Result.success(); } GetMapping(/s2) public Result session2(HttpServletRequest request){ HttpSession session request.getSession();//请求头中获取Sessionlog.info(HttpSession-s2: {}, session); Object loginUser session.getAttribute(loginUser); // 从session中获取数据 log.info(loginUser: {}, loginUser); return Result.success(loginUser); } }如图我们在服务器端并不能看到响应头携带的Cookie的Key和Value底层流程Tomcat 生成一个唯一且随机的 ID比如ABC123Tomcat 自动下发 CookieJSESSIONIDABC123你看不到也改不了服务器内部建立一个 Map{ ABC123 → { loginUser: user对象(这里我们定义为tom) } }你作为开发者操作的是session对象浏览器看到的只有一个乱码JSESSIONID其实相比于传统CookieSession技术就相当于把Cookie定义在了服务端你前端什么都看不到访问Session技术优缺点由于其还是基于Cookie技术实现的所以自然Cookie有的缺点他也有并且服务器集群环境下无法直接使用可以通过技术解决但我们都有更好的方法Session因为我们的集群环境一般会有一台负载均衡服务器每次转发请求时向哪个服务器发送请求都是基于策略的所以有可接收到请求的这台服务器他并没有存储我们的Session对象那么就会判定该用户没有登陆重新去登录令牌方案官网https://jwt.io/ 提供了解密算法等名称如HS256第二个部分Payload是自定义信息自定义过期时间统一存放为Claims对象最后一个部分并不是基本Base64编码的而是通过指定算法签名计算而来并且将前两个字段融入以及融入我们自己的指定密钥如果前两个部分发生改变校验的时候便会识别出来从而识别出Token被篡改JWT生成/解析生成(1)引入jjwt依赖(2)调用官方提供工具类Jwts来生成令牌,具体操作下方给出Test public void testGenerateJwt() { MapString, Object dataMap new HashMap(); dataMap.put(id, 1); dataMap.put(username, admin); dataMap.put(password, admin); String jwt Jwts.builder().signWith(SignatureAlgorithm.HS256, aXRoZWltYQ).//指定签名算法并且添加密钥(密钥支持base64编码) addClaims(dataMap).//添加自定义信息 setExpiration(new Date(System.currentTimeMillis() 3600 * 1000)).compact();//最终组装为String类型的jwt令牌System.out.println(jwt); } }JWT 生成核心方法方法作用示例Jwts.builder()创建一个 JWT 构建器Jwts.builder().signWith(算法, 密钥)指定签名算法 密钥生成 Signature这里密钥是给第三部分使用的并且支持Base64编码.signWith(HS256, aXRoZWltYQ).addClaims(Map)添加自定义数据(Map集合格式)放入 Payload.addClaims(dataMap).setExpiration(Date)设置过期时间放入 Payload.setExpiration(new Date(...)).compact()拼接三部分返回最终 JWT 字符串.compact()解析解析令牌即提供令牌的密钥(我们设置的)并且获取令牌中我们自定义的信息Claims(Map集合存活时间等等信息)并收集因为这个jwt令牌中第一部分指定了我们使用的签名算法所以根据一二部分密钥进行运算看看结果是否和第三部分一致并且如果过期就会抛异常而不会进行对比了解析 JWT 的核心方法方法作用说明Jwts.parser()创建一个解析器和builder()对称.setSigningKey(密钥)设置验证签名用的密钥必须和生成时的密钥一致.parseClaimsJws(jwt字符串)解析并验证 JWT会校验签名和过期时间.getBody()获取 Payload 中的数据(自定义的数据exp自定义存活时间)返回Claims对象登陆成功后下发令牌至此我们讲述了三种会话技术我们基于JWT令牌实现会话跟踪当用户成功登录时我们便下发给用户下发token也就是LoginInfo的token值工具类如下(提供两个生成方式和一种解码方式) public class JwtUtils { private static final String SECRET aXRoZWltYQ;//设置密钥 private static final long EXPIRE 3600 * 1000 * 12;//有效时间12小时 // 方式一传 Map最灵活 public static StringgenerateJwt(MapString, Object claims){ return Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, SECRET) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .compact(); } // 方式二传简单参数方便常用场景 public static String generateJwt(Integer id, String username) { MapString, Object claims Map.of(id, id, username, username); returngenerateJwt(claims); } // 解析 JWT public static Claims parseJwt(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }服务层改进Override public LoginInfo login(Emp emp) { //根据用户名和密码查询是否有员工数据返回 Emp empDB empMapper.selectByUsernameAndPassword(emp.getUsername(), emp.getPassword()); if (empDB ! null) { // 登录成功 log.info(员工【{}】登录成功, empDB.getName()); MapString, Object claims Map.of(id, empDB.getId(), username, empDB.getUsername());String token JwtUtils.generateJwt(claims);LoginInfo loginInfo new LoginInfo(empDB.getId(), empDB.getUsername(), empDB.getName(),token); return loginInfo; } return null; }这里有个小提醒当你自定义信息的时候不要自定义隐私信息或者私密信息base64的解码编码是很简单的我们的jwt令牌只有最后一个部分进行了加密过滤器Filter快速入门注意事项(1)实现Filter这个接口的类记得加上WebFilter配置拦截路径并且实现的Filter这个接口是servlet组件提供的(2)启动类配置ServletComponentScan这个注解开启Servlet组件支持(3)拦截到了如果满足请求一定要chain.doFilter()来放行不然这个请求就不执行了令牌校验Filter思考什么请求需要校验令牌 拦截请求后什么情况能放行流程Slf4j WebFilter(urlPatterns /*) public class TokenFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request (HttpServletRequest) servletRequest; HttpServletResponse response (HttpServletResponse) servletResponse;//TODO 1.获取请求路径String path request.getRequestURI();//TODO 2.判断是否是登陆请求也就是是否包含/login if (path.contains(/login)) { log.info(登录请求放行); filterChain.doFilter(servletRequest,servletResponse);//如果是登录则放行否则继续执行 return; } //TODO 3.获取请求头中的token String token request.getHeader(token); //TODO 4.判断token是否存在如果不存在就不放行返回错误信息(响应401) if (token null || token.isEmpty()) {//因为这里是字符串要判断是否为null和是否为空字符串log.info(token为空响应401); response.setStatus(401); return; } //TODO 5.如果存在就进行解析判断是否过期如果过期就不放行返回错误信息(响应401) try { JwtUtils.parseJwt(token); } catch (Exception e) { log.info(token非法响应401); response.setStatus(401); return; } //TODO 6.如果没过期就进行放行 log.info(token合法放行); filterChain.doFilter(servletRequest,servletResponse); } }Slf4j WebFilter(urlPatterns /*) public class TokenFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request (HttpServletRequest) servletRequest; HttpServletResponse response (HttpServletResponse) servletResponse;//TODO 1.获取请求路径String path request.getRequestURI();//TODO 2.判断是否是登陆请求也就是是否包含/login if (path.contains(/login)) { log.info(登录请求放行); filterChain.doFilter(servletRequest,servletResponse);//如果是登录则放行否则继续执行 return; } //TODO 3.获取请求头中的token String token request.getHeader(token); //TODO 4.判断token是否存在如果不存在就不放行返回错误信息(响应401) if (token null || token.isEmpty()) {//因为这里是字符串要判断是否为null和是否为空字符串log.info(token为空响应401); response.setStatus(401);return; } //TODO 5.如果存在就进行解析判断是否过期如果过期就不放行返回错误信息(响应401) try { JwtUtils.parseJwt(token); } catch (Exception e) { log.info(token非法响应401); response.setStatus(401); return; } //TODO 6.如果没过期就进行放行 log.info(token合法放行); filterChain.doFilter(servletRequest,servletResponse); } }概念包含内容举例URI请求路径不含协议、域名、端口/depts/1URL协议 域名 端口 路径完整地址http://localhost:8080/depts/1TokenFilter 强转原理小结TokenFilter中的doFilter方法接收的参数类型是ServletRequest和ServletResponse但在方法内部直接强转成了HttpServletRequest和HttpServletResponse。为什么可以这样强转而不报错Filter组件是由Tomcat负责调用的Tomcat 在调用doFilter方法时实际传入的对象是HttpServletRequest和HttpServletResponse的实现类如RequestFacade、ResponseFacade这两个实现类同时继承了ServletRequest/ServletResponse和HttpServletRequest/HttpServletResponseTomcat 以父接口类型ServletRequest传入体现多态我们在 Filter 内部向下转型为子接口HttpServletRequest是为了调用 HTTP 特有的方法如getHeader多态让 Tomcat 用父接口传参强转让我们用子接口的能力。一些注意事项过滤器执行流程也就是放行之后我才会让这个请求去访问对应的三层架构从而去访问资源访问完之后还要再次回到Filter过滤器中执行放行后的代码最后才响应WebFilter拦截路径的设置过滤器链—doFilter方法的第三个参数FilterChain filterChain什么是过滤器链呢所谓过滤器链指的是在一个web应用程序当中可以配置多个过滤器多个过滤器就形成了一个过滤器链。也就是从第一个过滤器开始访问放行后并不是直接去访问资源而是执行第二个过滤器这里过滤器链的执行流程是按照类名的字符串比较决定的大的先执行以此类推拦截器Interceptor快速入门这里我们需要在Interceptor这个包下自定义一个我们的拦截器当然类名最好也是以Interceptor结尾记得加入Component注解将这个类交给IOC容器管理因为后面我们注册的时候需要使用它如下是我们定义的一个拦截器其实现了HandlerInterceptor接口这里其实我们一般常用的也就是重写preHandler这个方法Slf4j Component public class DemoInterceptor implementsHandlerInterceptor{ //目标方法资源访问之前运行该方法返回的是一个boolean类型值决定了是否拦截Override publicboolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info(拦截方法...); return true; } //目标方法资源访问之后运行 Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info(拦截方法之后...); } //视图渲染完毕之后运行 Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info(视图渲染完毕之后...); } }注册(配置)拦截器新建一个包包名为config在此包下面进行我们的拦截器配置定义一个配置类实现WebMvcConfigurer接口Configuration public class WebConfig implementsWebMvcConfigurer{ // 拦截器对象 Autowired private DemoInterceptor demoInterceptor; Override public void addInterceptors(InterceptorRegistryregistry) { // 注册自定义拦截器对象 registry.addInterceptor(demoInterceptor) .addPathPatterns(/**)// 设置拦截器拦截的请求路径 /** 表示拦截所有请求 .excludePathPatterns(/login);// 设置不拦截的请求路径 } }Configuration这个注解包含了Component所以自然是IOC容器的管理对象重写addInterceptors这个方法在里面注册并且配置拦截器指明并且设置拦截器拦截的请求路径当然也可以设置不拦截哪些请求路径令牌校验原理其实和Filter非常相像只不过我们单独的将拦截的情况配置在了配置类里我们的拦截器则只需关注如果拦截了满足什么条件放行即可Slf4j Component public class TokenInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info(拦截方法...); //1.获取 token String token request.getHeader(token); //2.如果token为null或空字符串返回false并相应401 if (token null || token.isEmpty()) { log.info(token为空响应401); response.setStatus(401); return false; } try { JwtUtils.parseJwt(token); } catch (Exception e) { log.info(token非法响应401); response.setStatus(401); return false; } return true; } }注意事项拦截路径—在配置类中可以自行配置因此我们设置拦截所有路径时不同于Filter过滤器这里要设置为/**执行流程—如果过滤器和拦截器同时存在即先执行过滤器Filter执行到doFilter()如果放行则会进一步去执行拦截器之后拦截器如果放行则会去访问资源之后原路返回无论多少过滤器拦截器有多少都一定是过滤器先执行完再执行拦截器Filter和Interceptor的区别