在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


强制登录


虽然我们做了用户登录,但是我们发现,用户不登录,依然可以操作图书

这是有极大风险的。所以我们需要进行强制登录。

如果用户未登录就访问图书列表或者添加图书等页面,强制跳转到登录页面


实现思路分析


用户登录时,我们已经把登录用户的信息存储在了Session中。那就可以通过Session中的信息来判断用户是否登录。

  1. 如果Session中可以取到登录用户的信息,说明用户已经登录了,可以进行后续操作
  2. 如果Session中取不到登录用户的信息,说明用户未登录,则跳转到登录页面。
Client Server Database 未登录状态 GET /book_list.html (无Cookie) HTTP 302 重定向到/login (Location: /login.html) 登录请求 POST /login (用户名密码) 创建Session (sessionId, userId, 过期时间) HTTP 200 (Set-Cookie: JSESSIONID=abc123) 存储Cookie到浏览器 HTTP 401 错误 alt [验证成功] [验证失败] 已登录状态 GET /book_list.html (Cookie: JSESSIONID=abc123) 查询Session(sessionId=abc123) HTTP 200 (返回页面) HTTP 302 重定向到/login alt [Session有效] [Session无效/过期] 登出请求 POST /logout (Cookie: JSESSIONID=abc123) 删除Session(abc123) HTTP 200 (清除Cookie) Client Server Database

在控制器中对 session 和用户信息进行显式检查,防止未授权访问,也称为防御式编程 (Defensive Programming)


实现服务器代码


会话管理


修改图书列表接口,进行登录校验;

因为用户登录逻辑,是靠 session 进行验证的,所以我们可以根据 session 中获取信息,来判断用户的登录状态,接下来,我们先获取 session;


我们可以设置参数 HttpServletRequest request,通过 request 获取 session

image-20250411173134496


参数为 true,表示如果没有拿到 session ,会创建一个空的 session 对象

image-20250411173620016

如果参数为 false,就需要对 request 进行判空,一些用户没有登录直接访问, request 为空,再调用 getSession ,可能会出现空指针异常;


也可以直接在参数中创建一个空 HttpSession session,通过 HttpSession 实现基于会话的用户认证

image-20250411173508188


如果用户登录,参数 session 就是的session.getAttribute("session_user_info")会拿到 session 相应的信息,我们实用 UserInfo 来接收 session 信息

image-20250411174150255

// 存储数据到Session (通常用在登录成功后)
HttpSession session = request.getSession(); // 获取当前Session
session.setAttribute("userId", 123);       // 存入用户ID
session.setAttribute("role", "admin");    // 存入用户角色

// 在其他请求中读取数据
Integer userId = (Integer) session.getAttribute("userId"); // 获取用户ID
String role = (String) session.getAttribute("role");      // 获取用户角色

image-20250411175459870


设置常量类管理会话键


问题:如果修改常量 session 的 key,就需要修改所有使用到这个 key 的地方,出于高内聚低耦合的思想,我们把常量集中在一个类里

image-20250411174639680


创建类:Constants

image-20250411175111542


常量名命名规则:

  • 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚不要在意名字长度
  • 正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
  • 反例:COUNT / TIME

image-20250411175444805


使用常量替代直接写死的字符串(如"session_user_info"),可以减少拼写错误,方便统一修改:

image-20250411180144106

修改后,后续如果 session_user_key 要改成 session_user_key1,我们只需要修改常量类对应的 SESSION_USER_KEY 即可,不需要回到代码中进行多处修改;

  • SESSION_USER_KEY 用于标识存储在HttpSession中的用户信息,避免硬编码字符串散落在代码各处。
  • 这种做法也就做会话键管理,可以提高代码的可维护性和可读性。

处理用户未登录逻辑:

image-20250411180256500


如果用户未登录,返回一个空响应给前端?

image-20250411180524870


统一响应封装


对于空响应,前端是不太好处理的,以图书列表为例,如果我们登录成功,会进入图书列表,这时候会带着 cookie 信息

现在图书列表接口返回的内容如下:

image-20250411181100559

这个结果上,前端没办法确认用户是否登录了

  • 用户未登录 返回空对象

  • 没有记录 返回 tatal = 0

  • 有记录 返回 tatal > 0

  • 后端出错 返回 null,那后端为什么出错呢,是因为前端参数不对,还是后端内部处理错误?

而且后端返回数据为空时,前端也无法确认是后端无数据,还是后端出错了


当前后端接口数据返回类:

image-20250411160645465


我们需要再增加两个属性告知后端的状态以及后端出错的原因。修改如下:

image-20250411181630036

  • code = -1 表示用户未登录
  • code = -2 表示后端出错
  • code = 200 后端正常响应…

前端只需要判断 code 的值,进而展示不同的页面即可;errMessage 可能用不到,也可能用得到,主要用于写一些错误的原因;


但是当前只是图书列表而已,图书的增加、修改、删除接口都需要强制登录,跟着修改,添加两个字段,这对我们的代码修改是巨大的。

image-20250411182411103


封装接口返回数据


我们不妨对所有后端返回的数据进行一个封装 :

image-20250411184951430


在 Result 类中还要有之前 BookController 中所有增删改查接口返回的数据,所以我们进行如下修改:

image-20250411185257384

Result<T> 使用泛型保持返回数据的类型安全,data 为之前接口返回的数据


封装好 Result 后,我们修改 getListByPage 接口:

image-20250411185654707

使用 Result<T> 类统一封装所有API响应,包含状态码、错误信息和数据。这是一种常见的API设计模式。


枚举状态码


status 为后端业务处理的状态码,也可以使用枚举来表示

image-20250411185951790


通过 ResultCodeEnum 定义标准化的返回状态码,提高代码可读性和可维护性。

image-20250411190156919


图书列表接口


设置好业务状态码后,我们就需要封装接口的返回结果,进而在接口返回数据的基础上,再多返回一个业务状态码;

前端就可以根据返回结果中的业务状态码,来决定跳转页面,进而实现强制登录功能;

image-20250411190611995


image-20250411190729954


使用工厂方法模式实现状态码对应方法


但是上面的代码是重复的,写的比较啰嗦,我们可以到 Result 中:

image-20250411191503028


除了登录成功 success() 和 登录失败 fail() ,还需要用户未登录接口,来对应枚举的三种状态码:

image-20250411191807173


image-20250411192353622


在静态方法中,参数未泛型,就需要重新声明一下泛型:

image-20250411192540842

Result 类中的 success() / fail() / unlogin()静态方法类似于简化版的工厂方法,用于创建不同类型的响应对象。


简化图书列表接口代码


设置好 Result 对应的状态方法后,我们简化一下 getListByPage 接口:

image-20250411192815351


在打开登录页面,调用 login 接口时,服务器会创建 session,这个 session 不会是 null,但是如果登录失败,session.getAttribute() 就未 null,所以这种情况,也是未登录的情况之一:

image-20250412150343896


接口测试


重新运行程序,使用 Postman 构造请求:

image-20250412152904881


此时用户未登录,说明 session.getAttrbute(“Constants.SESSION_USER_KEY”) 没有拿到值;

这时候,我们可以先构造一个用户登录请求发送给后端;

image-20250412153209198


后端会调用登录接口,执行 session.setAttribute(“Constants.SESSION_USER_KEY”, userInfo) 方法

image-20250412153240554


此时,Postman 成功接收到后端响应,会接收到 set-cookie,并且存储好 cookie

image-20250412153525133


此时重新构造获取图书列表请求:

image-20250412154016190


此时获取图书列表接口还是强制我们登录,这种情况说明 Session 未被正确传递或识别,煮啵进一步排查,发现这两个请求的端口号一个是 8080,一个是 9090,我们重新统 一 端口号 9090:

image-20250412154238468


image-20250412154626757


思维导图


图书管理系统/强制登录/思维导图/强制登录功能实现.pdf · 9ef0356 · XiaoLei/思维导图 - Gitee.com


修改客户端代码


由于后端接口发生变化,所以前端接口也需要进行调整

image-20250412154722937

  • 这也就是为什么前后端交互接口一旦定义好,尽量不要发生变化。
  • 所以后端接口返回的数据类型一般不定义为基本类型,包装类型或者集合类等,而是定义为自定义对象。方便后续做扩展

下面的逻辑表示的是,如果后端返回结果为空,或者返回结果的列表为空,就不进行下面展示列表框及对列表框赋值的逻辑,直接返回;

image-20250412154900543

现在,我们已经在获取图书列表接口中,加上强制登录逻辑,所以我们要修改一下前端处理后端返回响应的逻辑;


后端返回的结果,在原有数据的基础上,还增加了状态码、错误信息

这时候前端的逻辑应该改为:

  • 如果后端返回的响应(状态码、错误信息、接口数据)为空,说明列表页面不支持访问,跳转登录页面,强制退出
  • 如果响应中获取到的状态码为“UNLOGIN”,就直接跳转登录页面,强制用户登录

image-20250412172532598


我们把 list 页面中,原来的 result 部分,全部修改为 result.data

image-20250412173008872


接口测试


ctrl+s 保存代码,重新启动程序;

用户未登录情况,访问图书列表:http://127.0.0.1:8080/book_list.html

image-20250412173416339


点击确定,发现跳转到了登录页面 :

image-20250412173436618


登录用户,图书列表正常返回

image-20250412173510559


思考

强制登录的模块,我们只实现了一个图书列表,上述还有图书修改、图书删除等接口,也需要实现。

如果应用程序功能更多的话,这样写下来会非常浪费时间,并且容易出错。

有没有更简单的处理办法呢?

接下来我们学习SpringBoot对于这种“统一问题”的处理办法。


完整代码


BookController

@RestController
@RequestMapping("/book")
@Slf4j
public class BookController {
    @Autowired
    private BookService bookService;

    // 返回页数接口
    @RequestMapping("/getListByPage")
    public Result<ResponseResult<BookInfo>> getListByPage(PageRequest pageRequest, HttpSession session){
        if(session.getAttribute("Constants.SESSION_USER_KEY")==null){
            return Result.unlogin();
        }

        UserInfo userInfo = (UserInfo)session.getAttribute("Constants.SESSION_USER_KEY");
        if(userInfo == null || userInfo.getId() <= 0){
            // 用户未登录
            return Result.unlogin();
        }

        // 用户登录成功
        ResponseResult<BookInfo> listByPage = bookService.getListByPage(pageRequest);
        return Result.success(listByPage);
    }

}


Result

@Data
public class Result<T> {
    private ResultCodeEnum code;
    private String errorMessage;
    private T data;

    public static <T> Result fail(String errorMessage){
        Result result = new Result();
        result.setCode(ResultCodeEnum.UNLOGIN);
        result.setErrorMessage(errorMessage);
        result.setData(null);
        return result;
    }

    public static <T> Result success(T data){
        Result result = new Result();
        result.setCode(ResultCodeEnum.SUCCESS);
        result.setErrorMessage("");
        result.setData(data);
        return result;
    }

    public static <T> Result unlogin(){
        Result result = new Result();
        result.setCode(ResultCodeEnum.UNLOGIN);
        result.setErrorMessage("用户未登录");
        result.setData(null);
        return result;
    }
}

ResultCodeEnum

package com.bit.book.enums;

public enum ResultCodeEnum {
    UNLOGIN(-1),
    SUCCESS(200),
    FATL(-2);

    private int code;

    ResultCodeEnum(int code) {
        this.code = code;
    }
}


ResponseResult

@AllArgsConstructor
@NoArgsConstructor
@Data
public class ResponseResult<T> {
    private Integer total;
    private List<T> records;
    private PageRequest pageRequest;

}


在这里插入图片描述

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐