SpringBoot - 登录

会话技术

登录流程:

会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一起会话中包含多次请求和响应

会话跟踪: 一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

会话跟踪方案:

  • 客户端会话技术:Cookie
  • 服务端会话跟踪技术: Session
  • 令牌技术

传统方案:

为什么session 不适应于集群:

令牌技术:

Session 案例演示

服务端代码:

package com.chuangzhou.controller;

import com.chuangzhou.pojo.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.http.HttpRequest;

@Slf4j
@RestController
public class TestController {


    //往session 中存储值
    @GetMapping("/s1")
    public Result session1(HttpSession session) {
        log.info("HttpSession - s1:{}", session.hashCode());

        session.setAttribute("loginUser", "tome");
        return Result.success();
    }

    //从session 中获取值
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request) {
        HttpSession session = request.getSession();
        log.info("HttpSession - s2:{}", session.hashCode());

        Object loginUser = session.getAttribute("loginUser");
        log.info("loginUser:{}",loginUser);
        return Result.success();
    }
}

第一次访问服务端:

服务端Tomcat容器会判断浏览器是否第一次访问如果是第一次访问就会创建一个Session对象,并通过响应头的 Set-Cookie 将session 的id 返回给浏览器

第二次访问:

服务端输出:

2023-08-22T22:19:15.928+08:00  INFO 13696 --- [nio-8080-exec-1] c.chuangzhou.controller.TestController   : HttpSession - s1:1010831818
2023-08-22T22:19:22.155+08:00  INFO 13696 --- [nio-8080-exec-2] c.chuangzhou.controller.TestController   : HttpSession - s2:1010831818
2023-08-22T22:19:22.155+08:00  INFO 13696 --- [nio-8080-exec-2] c.chuangzhou.controller.TestController   : loginUser:tome
2023-08-22T22:20:38.941+08:00  INFO 13696 --- [nio-8080-exec-5] c.chuangzhou.controller.TestController   : HttpSession - s1:1010831818

JWT 令牌

JWT: JSON WEB Tokenks。定义了一种简洁的、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠

官网:https://jwt.io/

组成:

  • Header: 记录令牌类型、签名算法等。e.g:
  • Payload:携带一些自定义信息、默认信息等。e.g:
  • Signature: 防止Token被篡改、确保安全性。将header、payload合并然后加入指定密钥,通过指定签名算法计算而来

令牌生成代码:

    @Test
    public void testGenJWT(){
        Map<String, Object> map = new HashMap<>();
        map.put("id",1);
        map.put("username","Tome");
        String jwts = Jwts.builder()    //SignatureAlgorithm.HS256:签名算法。chuangzhou: 密钥
                .signWith(SignatureAlgorithm.HS256, "chuangzhou")
                .setClaims(map)  //载荷
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置密钥的有效期为一个小时
                .compact();

        System.out.println(jwts);
    }

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjkyODA1MTM0LCJ1c2VybmFtZSI6IlRvbWUifQ.Uc2KYmPSo8jpK_0aRx2yeatLri0BZ-txl9HnVpox4Oc

可以在官网进行解码:

其中前两个部分也就是header 和 payload 部分是 BASE64 编码后,解码后就可以看到内容:

解析令牌:

    @Test
    public void testparseJwt(){
        Claims chuangzhou = Jwts.parser()
                .setSigningKey("chuangzhou")
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjkyODA1MTM0LCJ1c2VybmFtZSI6IlRvbWUifQ.Uc2KYmPSo8jpK_0aRx2yeatLri0BZ-txl9HnVpox4Oc")
                .getBody();
        System.out.println(chuangzhou);
    }

解析出来的内容就为载荷:

{id=1, exp=1692805134, username=Tome}

exp 为令牌的有效期

注意事项:

  • JWT校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥是配套的
  • 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法

登录案例

引入JWT 工具类:

package com.chuangzhou.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;

public class JwtUtils {

    private static String signKey = "chuangzhou";  //密钥
    private static Long expire = 43200000L; //过期时间

    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @return
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

改造controller:

package com.chuangzhou.controller;


import com.chuangzhou.pojo.Emp;
import com.chuangzhou.pojo.Result;
import com.chuangzhou.serivce.EmpService;
import com.chuangzhou.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
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.RestController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class LoginController {


    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        Emp e  = empService.getEepByUsernamePassword(emp);

        if (e != null){
            //登录成功
            //1.设置载荷
            Map<String,Object> map = new HashMap<>();
            map.put("id",e.getId());
            map.put("username",e.getUsername());
            map.put("name",e.getName());

            //2.生成令牌
            String jwt = JwtUtils.generateJwt(map);
            //3.返回jwt令牌
            return Result.success(jwt);
        }

        return Result.error("账号或密码错误");
    }
}

接口成功返回jwt:

前端会将token 存储在浏览器的本地存储空间:

之后的请求前端需要将token 放入到 请求头 以便后端 解析校验

Filter

概念:Filter 过滤器,是JavaWeb 三大组件(Servlet、Filter、Litener) 之一

  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
  • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等

Springboot 中的使用步骤:

  1. Filter 实现类: 标注 @WebFilter
package com.chuangzhou.filter;


import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter(urlPatterns = "/*") // 拦截所有请求
public class DemoFilter  implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {  //项目启动时执行
        System.out.println("init 方法执行了");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("放行前 doFilter 执行逻辑");

        filterChain.doFilter(servletRequest,servletResponse); // 放行请求

        System.out.println("放行后 doFilter 执行逻辑");
    }

    @Override
    public void destroy() {  //项目结束时执行
        System.out.println("destroy 方法执行了");
    }
}


  1. 启动类上使用:@ServletComponentScan
@SpringBootApplication
@ServletComponentScan
public class BliasWebManagerApplication {

    public static void main(String[] args) {
        SpringApplication.run(BliasWebManagerApplication.class, args);
    }

}

执行流程:

总结:放行后的资源会重新进入Fileter,执行 放行后的逻辑

拦截路径常见用法:

过滤器链

介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链

顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。

利用Filter 实现登录校验案例

package com.chuangzhou.filter;

import com.alibaba.fastjson.JSONObject;
import com.chuangzhou.pojo.Result;
import com.chuangzhou.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.IOException;

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        //1. 判断是否是登录请求
        StringBuffer requestURL = req.getRequestURL();
        log.info("请求路径为:{}",requestURL);
        //2.如果是登录请求放行
        if (requestURL.toString().contains("login")){
            chain.doFilter(request,response);
            return;
        }
        //3.如果不是登录请求判断请求头是否有token
        String token = req.getHeader("token");
        if (!StringUtils.hasLength(token)){
            log.info("请求未携带token:{}",token);
            //4.如果没有返回错误
            Result error = Result.error("NOT_LOGIN");
            String respStr = JSONObject.toJSONString(error);
            resp.getWriter().write(respStr);
            return;
        }

        //5.如果有解析令牌
        try{
            JwtUtils.parseJWT(token);
        }catch (Exception e){  //解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result not_log = Result.error("NOT_LOG");
            String s = JSONObject.toJSONString(not_log);
            resp.getWriter().write(s);
            return;
        }

        //6.解析令牌成功放行
        log.info("令牌合法,放行");
        chain.doFilter(request,response);
    }
}

拦截器

入门案例

介绍: 是一种动态拦截方法调用的机制,类似于过滤器。Spring 框架中提供的,用来动态拦截控制器方法的执行。
作用: 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码

  1. 编写拦截器:
package com.chuangzhou.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;


@Component
public class CheckLoginInterceptor  implements HandlerInterceptor {
    @Override   // controller 执行之前执行,return true:放行。 返回false 不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle ...");
        return true;
    }

    @Override  // controller 执行之后执行
    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 ...");
    }
}
  1. 编写配置类注册拦截器
package com.chuangzhou.config;

import com.chuangzhou.interceptor.CheckLoginInterceptor;
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
public class WebConfig  implements WebMvcConfigurer {

    @Autowired
    private CheckLoginInterceptor checkLoginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(checkLoginInterceptor).addPathPatterns("/**");
    }


}

拦截路径配置

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // excludePathPatterns:指定了不拦截的请求
        // /*: 可以拦截/depts、/emps, 但是无法拦截 /depts/1 
        registry.addInterceptor(checkLoginInterceptor).addPathPatterns("/*").excludePathPatterns("/login");
    }

具体规则:

拦截器的执行流程 :

当filter 和 inteper 同时存在时:

Filter 和 Intercetor 的不同点:

  • 接口规范不同:过滤器需要实现Filter 接口,而拦截器需要实现HandlerIntercetor 接口
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor 只会拦截Spring 环境中的资源
posted @ 2023-08-20 17:00  chuangzhou  阅读(222)  评论(0编辑  收藏  举报