【图书管理系统】深入解析基于会话管理 Session 、统一响应封装、枚举状态码、工厂方法模式实现状态码方法:全栈开发获取图书列表接口的强制登录功能(含会话管理、统一响应封装思维导图、核心部分代码)
图书列表接口的强制登录功能
强制登录
虽然我们做了用户登录,但是我们发现,用户不登录,依然可以操作图书
。
这是有极大风险的。所以我们需要进行强制登录。
如果用户未登录就访问图书列表或者添加图书等页面,强制跳转到登录页面
。
实现思路分析
用户登录时,我们已经把登录用户的信息存储在了Session中。那就可以通过Session中的信息来判断用户是否登录。
- 如果Session中可以取到登录用户的信息,说明用户已经登录了,可以进行后续操作
- 如果Session中取不到登录用户的信息,说明用户未登录,则跳转到登录页面。
在控制器中对 session 和用户信息进行显式检查,防止未授权访问,也称为防御式编程 (Defensive Programming)
实现服务器代码
会话管理
修改图书列表接口,进行登录校验;
因为用户登录逻辑,是靠 session 进行验证的,所以我们可以根据 session 中获取信息,来判断用户的登录状态,接下来,我们先获取 session;
我们可以设置参数 HttpServletRequest request,通过 request 获取 session
参数为 true,表示如果没有拿到 session ,会创建一个空的 session 对象
:
如果参数为 false,就需要对 request 进行判空,一些用户没有登录直接访问, request 为空,再调用 getSession ,可能会出现空指针异常;
也可以直接在参数中创建一个空 HttpSession session,通过 HttpSession 实现基于会话的用户认证
。
如果用户登录,参数 session 就是的session.getAttribute("session_user_info")
会拿到 session 相应的信息,我们实用 UserInfo 来接收 session 信息
// 存储数据到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"); // 获取用户角色
设置常量类管理会话键
问题:如果修改常量 session 的 key,就需要修改所有使用到这个 key 的地方,出于高内聚低耦合的思想,我们把常量集中在一个类里
创建类:Constants
常量名命名规则:
- 常量命名
全部大写
,单词间用下划线
隔开,力求语义表达完整清楚
,不要在意名字长度
。- 正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
- 反例:COUNT / TIME
使用常量替代直接写死的字符串(如"session_user_info"
),可以减少拼写错误,方便统一修改:
修改后,后续如果 session_user_key 要改成 session_user_key1,我们只需要修改常量类对应的 SESSION_USER_KEY 即可,不需要回到代码中进行多处修改;
SESSION_USER_KEY
用于标识存储在HttpSession
中的用户信息,避免硬编码字符串散落在代码各处。- 这种做法也就做
会话键管理
,可以提高代码的可维护性和可读性。
处理用户未登录逻辑:
如果用户未登录,返回一个空响应给前端?
统一响应封装
对于空响应,前端是不太好处理的,以图书列表为例,如果我们登录成功,会进入图书列表,这时候会带着 cookie 信息
;
现在图书列表接口返回的内容如下:
这个结果上,前端没办法确认用户是否登录了
。
用户未登录 返回空对象
没有记录 返回 tatal = 0
有记录 返回 tatal > 0
后端出错 返回 null,那后端为什么出错呢,是因为前端参数不对,还是后端内部处理错误?
而且后端返回数据为空时,前端也无法确认是后端无数据,还是后端出错了
。
当前后端接口数据返回类:
我们需要再增加两个属性告知后端的状态以及后端出错的原因。修改如下:
- code = -1 表示用户未登录
- code = -2 表示后端出错
- code = 200 后端正常响应…
前端只需要判断 code 的值,进而展示不同的页面即可;errMessage 可能用不到,也可能用得到,主要用于写一些错误的原因;
但是当前只是图书列表而已,图书的增加、修改、删除接口都需要强制登录,跟着修改,添加两个字段
,这对我们的代码修改是巨大的。
封装接口返回数据
我们不妨对所有后端返回的数据
进行一个封装 :
在 Result 类中还要有之前 BookController 中所有增删改查接口返回的数据,所以我们进行如下修改:
Result<T> 使用泛型保持返回数据的类型安全,data 为之前接口返回的数据
封装好 Result 后,我们修改 getListByPage 接口:
使用 Result<T> 类统一封装所有API响应,包含状态码、错误信息和数据。这是一种常见的API设计模式。
枚举状态码
status 为后端业务处理的状态码,也可以使用枚举来表示
通过 ResultCodeEnum 定义标准化的返回状态码,提高代码可读性和可维护性。
图书列表接口
设置好业务状态码后,我们就需要封装接口的返回结果,进而在接口返回数据的基础上,再多返回一个业务状态码;
前端就可以根据返回结果中的业务状态码,来决定跳转页面,进而实现强制登录功能;
使用工厂方法模式实现状态码对应方法
但是上面的代码是重复的,写的比较啰嗦,我们可以到 Result 中:
除了登录成功 success() 和 登录失败 fail() ,还需要用户未登录接口,来对应枚举的三种状态码:
在静态方法中,参数未泛型,就需要重新声明一下泛型:
Result 类中的 success() / fail() / unlogin()静态方法类似于简化版的工厂方法,用于创建不同类型的响应对象。
简化图书列表接口代码
设置好 Result 对应的状态方法后,我们简化一下 getListByPage 接口:
在打开登录页面,调用 login 接口时,服务器会创建 session,这个 session 不会是 null,但是如果登录失败,session.getAttribute() 就未 null,所以这种情况,也是未登录的情况之一:
接口测试
重新运行程序,使用 Postman 构造请求:
此时用户未登录,说明 session.getAttrbute(“Constants.SESSION_USER_KEY”) 没有拿到值;
这时候,我们可以先构造一个用户登录请求发送给后端;
后端会调用登录接口,执行 session.setAttribute(“Constants.SESSION_USER_KEY”, userInfo) 方法
此时,Postman 成功接收到后端响应,会接收到 set-cookie,并且存储好 cookie
此时重新构造获取图书列表请求:
此时获取图书列表接口还是强制我们登录,这种情况说明 Session 未被正确传递或识别,煮啵进一步排查,发现这两个请求的端口号一个是 8080,一个是 9090,我们重新统 一 端口号 9090:
思维导图
图书管理系统/强制登录/思维导图/强制登录功能实现.pdf · 9ef0356 · XiaoLei/思维导图 - Gitee.com
修改客户端代码
由于后端接口发生变化,所以前端接口也需要进行调整
- 这也就是为什么前后端交互接口一旦定义好,尽量不要发生变化。
- 所以后端接口返回的数据类型一般不定义为基本类型,包装类型或者集合类等,而是定义为自定义对象。方便后续做扩展
下面的逻辑表示的是,如果后端返回结果为空,或者返回结果的列表为空,就不进行下面展示列表框及对列表框赋值的逻辑,直接返回;
现在,我们已经在获取图书列表接口中,加上强制登录逻辑,所以我们要修改一下前端处理后端返回响应的逻辑;
后端返回的结果,在原有数据的基础上,还增加了状态码、错误信息
;
这时候前端的逻辑应该改为:
- 如果
后端返回的响应(状态码、错误信息、接口数据)为空
,说明列表页面不支持访问,跳转登录页面,强制退出
; - 如果响应中获取到的
状态码为“UNLOGIN”
,就直接跳转登录页面,强制用户登录
;
我们把 list 页面中,原来的 result 部分,全部修改为 result.data
接口测试
ctrl+s 保存代码,重新启动程序;
用户未登录情况,访问图书列表:http://127.0.0.1:8080/book_list.html
点击确定,发现跳转到了登录页面 :
登录用户,图书列表正常返回
思考
强制登录的模块,我们只实现了一个图书列表,上述还有图书修改、图书删除等接口,也需要实现。
如果应用程序功能更多的话,这样写下来会非常浪费时间,并且容易出错。
有没有更简单的处理办法呢?
接下来我们学习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;
}
更多推荐
所有评论(0)