JavaWeb学习笔记(Day12)

一、登录功能

1. 思路分析

登录服务端的核心逻辑:接收前端请求传递的用户名和密码 ,然后再根据用户名和密码查询用户信息,如果用户信息存在,则说明用户输入的用户名和密码正确。如果查询到的用户不存在,则说明用户输入的用户名和密码错误。

2. 代码实现

(1) LoginController

java 复制代码
@RestController
public class LoginController {

    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        Emp e = empService.login(emp);
	    return  e != null ? Result.success():Result.error("用户名或密码错误");
    }
}

**Q:**为什么形参用 Emp emp,登录功能只需要从前端里传输username和password,但是emp不是还有很多属性吗?只用这两个,其他怎么办?

A:SpringMVC 接收 JSON 参数时,会按「属性名匹配」规则仅绑定前端传递的属性,未传递的属性会保持默认值(null / 基本类型默认值),且这些未赋值的属性不会影响登录功能的执行

(2) EmpService

java 复制代码
public interface EmpService {

    /**
     * 用户登录
     * @param emp
     * @return
     */
    public Emp login(Emp emp);

    //省略其他代码...
}

(3) EmpServiceImpl

java 复制代码
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public Emp login(Emp emp) {
        //调用dao层功能:登录
        Emp loginEmp = empMapper.getByUsernameAndPassword(emp);

        //返回查询结果给Controller
        return loginEmp;
    }   
    
    //省略其他代码...
}

(4) EmpMapper

java 复制代码
@Mapper
public interface EmpMapper {

    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time " +
            "from emp " +
            "where username=#{username} and password =#{password}")
    public Emp getByUsernameAndPassword(Emp emp);
    
    //省略其他代码...
}

二、登录校验

1. 概述

所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。

2. 会话技术
(1) 会话技术介绍

① 在web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。

② 会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。

(2) 会话跟踪方案
方案一:Cookie
① 原理

比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。

服务器端在给客户端在响应数据的时候,会自动 的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动 的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

② 代码实现
java 复制代码
@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
        return Result.success();
    }
	
    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        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();
    }
}    
③ 优缺点

1). 优点:

HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)

2). 缺点:

  • 移动端APP(Android、IOS)中无法使用Cookie

  • 不安全,用户可以自己禁用Cookie

  • Cookie不能跨域

方案二:Session
① 原理
  • **获取Session:**如果我们现在要基于 Session 来进行会话跟踪,浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。
  • **响应Cookie (JSESSIONID):**接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。在响应头当中增加了一个 Set-Cookie 响应头。cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。
  • **查找Session:**在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
② 优缺点

1). 优点:

Session是存储在服务端的,安全

2). 缺点:

  • 服务器集群环境下无法直接使用Session

  • 移动端APP(Android、IOS)中无法使用Cookie

  • 用户可以自己禁用Cookie

  • Cookie不能跨域

PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。

方案三 令牌技术(主流)
① 优点:
  • 支持PC端、移动端

  • 解决集群环境下的认证问题

  • 减轻服务器的存储压力(无需在服务器端存储)

② 缺点:

需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)

3. JWT令牌
(1) JWT的组成
  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

(2) 生成和校验
① 生成JWT令牌

第一步:需要先引入JWT的依赖

XML 复制代码
<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

第二步:生成JWT代码实现

java 复制代码
@Test
public void genJwt(){
    Map<String,Object> claims = new HashMap<>();
    claims.put("id",1);
    claims.put("username","Tom");
    
    String jwt = Jwts.builder()
        .setClaims(claims) //自定义内容(载荷)          
        .signWith(SignatureAlgorithm.HS256, "itheima") //签名算法        
        .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期   
        .compact();
    
    System.out.println(jwt);
}
② 校验JWT令牌
java 复制代码
@Test
public void parseJwt(){
    Claims claims = Jwts.parser()
        .setSigningKey("itheima")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)  
	    .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
        .getBody();

    System.out.println(claims);
}
③ 生成与解析的闭环对应

Jwts.builder()(生成) ↔ Jwts.parser()(解析)

.signWith(密钥)(生成签名) ↔ .setSigningKey(密钥)(验证签名)

.claim/setExpiration(放入数据) ↔ .getBody()(提取数据)

.compact()(生成令牌串) ↔ .parseClaimsJws(令牌串)(解析令牌串)

4. 登录下发令牌
(1) 代码实现

① JWT工具类

java 复制代码
public class JwtUtils {

    private static String signKey = "itheima";//签名密钥
    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)//指定令牌Token
                .getBody();
        return claims;
    }
}

② LoginController.java

java 复制代码
@RestController
@Slf4j
public class LoginController {
    //依赖业务层对象
    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        //调用业务层:登录功能
        Emp loginEmp = empService.login(emp);

        //判断:登录用户是否存在
        if(loginEmp !=null ){
            //自定义信息
            Map<String , Object> claims = new HashMap<>();
            claims.put("id", loginEmp.getId());
            claims.put("username",loginEmp.getUsername());
            claims.put("name",loginEmp.getName());

            //使用JWT工具类,生成身份令牌
            String token = JwtUtils.generateJwt(claims);
            return Result.success(token);
        }
        return Result.error("用户名或密码错误");
    }
}
5. 过滤器Filter
(1) 快速入门

第1步:定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。

第2步:配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。

(2) 代码实现

① 定义过滤器

java 复制代码
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
    @Override //初始化方法, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法执行了");
    }

    @Override //拦截到请求之后调用, 调用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Demo 拦截到了请求...放行前逻辑");
        //放行
        chain.doFilter(request,response);
    }

    @Override //销毁方法, 只调用一次
    public void destroy() {
        System.out.println("destroy 销毁方法执行了");
    }
}
  • init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。

  • doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。

  • destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。

6. Filter详解
(1) 拦截路径
拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截
(2) 过滤器链

① 定义:所谓过滤器链指的是在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。

(3) 登录校验-Filter
① 具体流程

基于上面的业务流程,我们分析出具体的操作步骤:

  • 获取请求url

  • 判断请求url中是否包含login,如果包含,说明是登录操作,放行

  • 获取请求头中的令牌(token)

  • 判断令牌是否存在,如果不存在,返回错误结果(未登录)

  • 解析token,如果解析失败,返回错误结果(未登录)

  • 放行

② 代码实现

1). 登录校验过滤器:LoginCheckFilter

java 复制代码
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        //前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1.获取请求url
        String url = request.getRequestURL().toString();
        log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login


        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if(url.contains("/login")){
            chain.doFilter(request, response);//放行请求
            return;//结束当前方法的执行
        }


        //3.获取请求头中的令牌(token)
        String token = request.getHeader("token");
        log.info("从请求头中获取的令牌:{}",token);


        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
        if(!StringUtils.hasLength(token)){
            log.info("Token不存在");

            Result responseResult = Result.error("NOT_LOGIN");
            //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
            String json = JSONObject.toJSONString(responseResult);
            response.setContentType("application/json;charset=utf-8");
            //响应
            response.getWriter().write(json);

            return;
        }

        //5.解析token,如果解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJWT(token);
        }catch (Exception e){
            log.info("令牌解析失败!");

            Result responseResult = Result.error("NOT_LOGIN");
            //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
            String json = JSONObject.toJSONString(responseResult);
            response.setContentType("application/json;charset=utf-8");
            //响应
            response.getWriter().write(json);

            return;
        }


        //6.放行
        chain.doFilter(request, response);

    }
}

核心逻辑:

hasLength()的底层判断逻辑:

它的校验规则是:字符串不是 null,并且实际字符长度 > 0 ,只有同时满足这两个条件,才会返回true;否则返回false

Q: 为什么要把Result对象转换成 JSON 字符串返回?

A:前后端交互的通用数据格式是 JSON,前端只能识别并解析 JSON 格式的字符串,无法直接识别 Java 中的Result对象 ,转换的本质是将后端的 Java 对象「序列化」为前后端都能识别的 JSON 字符串,实现跨语言 / 跨端的数据传输

7. 拦截器Interceptor
(1) 快速入门

① 定义拦截器

java 复制代码
v//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行。 返回true:放行    返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        
        return true; //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 .... ");
    }
}

② 注册配置拦截器

java 复制代码
@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
    }
}
8. Interceptor详解
(1) 拦截路径
java 复制代码
@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //拦截器对象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器对象
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
                .excludePathPatterns("/login");//设置不拦截的请求路径
    }
}

在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:

拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1
/** 任意级路径 能匹配/depts,/depts/1,/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1
(2) 执行流程
  • 当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。

  • Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。

  • 当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。

  • 在controller当中的方法执行完毕之后,再回过来执行postHandle()这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。

(3) 过滤器和拦截器的区别
  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

9. 登录校验- Interceptor
(1) 登录校验拦截器
java 复制代码
//自定义拦截器
@Component //当前拦截器对象由Spring创建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    //前置方式
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        //1.获取请求url
        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行

        //3.获取请求头中的令牌(token)
        String token = request.getHeader("token");
        log.info("从请求头中获取的令牌:{}",token);

        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
        if(!StringUtils.hasLength(token)){
            log.info("Token不存在");

            //创建响应结果对象
            Result responseResult = Result.error("NOT_LOGIN");
            //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
            String json = JSONObject.toJSONString(responseResult);
            //设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
            response.setContentType("application/json;charset=utf-8");
            //响应
            response.getWriter().write(json);

            return false;//不放行
        }

        //5.解析token,如果解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJWT(token);
        }catch (Exception e){
            log.info("令牌解析失败!");

            //创建响应结果对象
            Result responseResult = Result.error("NOT_LOGIN");
            //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
            String json = JSONObject.toJSONString(responseResult);
            //设置响应头
            response.setContentType("application/json;charset=utf-8");
            //响应
            response.getWriter().write(json);

            return false;
        }

        //6.放行
        return true;
    }
(2) 注册配置拦截器
java 复制代码
@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //拦截器对象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

三、异常处理

1. 全局异常处理器

(1) 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。

(2) 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    //处理异常
    @ExceptionHandler(Exception.class) //指定能够处理的异常类型
    public Result ex(Exception e){
        e.printStackTrace();//打印堆栈中的异常信息

        //捕获到异常之后,响应一个标准的Result
        return Result.error("对不起,操作失败,请联系管理员");
    }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

处理异常的方法返回值会转换为json后再响应给前端

相关推荐
阿蒙Amon2 小时前
TypeScript学习-第11章:配置文件(tsconfig.json)
学习·typescript·json
babe小鑫2 小时前
中专学历转内容运营分析的学习路径
学习·产品运营·内容运营
●VON2 小时前
React Native for OpenHarmony:贪吃蛇游戏的开发与跨平台适配实践
学习·react native·react.js·游戏·openharmony
妄汐霜3 小时前
小白学习笔记(javaweb前端三大件)
笔记·学习·web
tb_first4 小时前
万字超详细苍穹外卖学习笔记5
java·数据库·spring boot·笔记·学习·spring
Horizon_Ruan4 小时前
从零开始掌握AI:LLM、RAG到Agent的完整学习路线图
人工智能·学习·ai编程
im_AMBER4 小时前
Leetcode 113 合并 K 个升序链表
数据结构·学习·算法·leetcode·链表
rainbow7242444 小时前
系统学习AI的标准化路径,分阶段学习更高效
大数据·人工智能·学习
山岚的运维笔记4 小时前
SQL Server笔记 -- 第16章:MERGE
java·笔记·sql·microsoft·sqlserver