当前位置: 首页 > news >正文

【博客系统】博客系统第五弹:基于令牌技术实现用户登录接口

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


使用令牌机制实现用户登录接口


用户登录流程


  1. 登录页面用户名密码提交给服务器
  2. 服务器端验证用户名密码是否正确。如果正确,服务器生成令牌,下发给客户端。
  3. 客户端把令牌存储起来(比如 Cookie、localStorage 等),后续请求时,把 token 发给服务器。
  4. 服务器对令牌进行校验。如果令牌正确,进行下一步操作。

image-20250422195546769


约定前后端交互接口


  • [请求]

    /user/login
    
  • [参数]

    {"userName": "test","password": "123456"
    }
    
  • [响应]

    {"code": 200,"errMsg": null,"data": {"userId": 1,"token": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiaWF0IjoxNzI0OTE5NjgyLCJleHAiOjE3MjQ5MjE0ODJ9.5hwKlAh2jPPBNn3uPja4JTGguZNB3QrpRoPqCep7qME"}
    }
    
    • 验证成功,返回 token;验证失败返回空字符串。

实现服务器代码


创建JWT 工具类


image-20250517222703178

public class JwtUtils {// (1) 该工具类有两个功能: 1. 生成 token;  2. 校验 token;// (6) 生成的密钥 Key 是被 genToken()、parseToken() 共用的, 因此提取出来使用// (7) 固定密钥字符串, 要设置为静态private static String SECRET_STRING = "sYAN5HvB8HQRzX1QTEFRhseSsgXIDJsggPhC1gNLa0Y";// (8) 根据固定密钥字符串, 生成静态密钥对象private static Key key = Keys.hmacShaKeyFor(SECRET_STRING.getBytes(StandardCharsets.UTF_8)); private static Key key1 = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_STRING)); // (9) SECRET_STRING.getBytes(StandardCharsets.UTF_8)、Decoders.BASE64.decode(SECRET_STRING)// 这两种方法都可以生成字符数组// (2) 将 Map 类型的令牌参数传入 genToken(), 根据该令牌生成 String 类型的 token 并返回public static String genToken(Map<String, Object> claims){String compact = Jwts.builder().setClaims(claims).signWith(key).compact();return compact;}// (5) 校验方法返回类型, 是根据 getBody() 的返回值可以被 Claims 类型接收设置的public static Claims parseToken(String token){// (3) 创建解析器,设置签名密钥JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims = build.parseClaimsJws(token).getBody();// (4) Claims 继承了 Map<String, Object> 接口, 可以简单认为 Claims 类似于一个 Mapreturn claims;}
}

创建请求和响应实体类


为了处理用户登录请求并返回响应,我们创建了一个新的类 UserLoginResponse,作为用户登录接口的返回类型。

image-20250519203328190

@Data
public class UserLoginResponse {private Integer userId;private String token;
}

实现 Controller


image-20250519203841782


@RequestMapping("/user")
@RestController
public class UserController {@RequestMapping("/login")public UserLoginResponse login(){}
}

在实现图书管理系统时,我们在给该接口传递参数是直接使用 userInfo

image-20250519203959709

@Data
public class UserInfo {@TableId(type = IdType.AUTO)private Integer id;private String userName;private String password;private String githubUrl;private Integer deleteFlag;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDate createTime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDate updateTime;
}

在登录接口的实现中,前端仅需传递 userNamepassword 作为参数,但现有的 userInfo 实体类包含额外属性,这些属性不应在调用登录接口时被传递

因此,我们需要创建一个新的实体类 UserLoginRequest,专门用于接收登录接口的参数,以确保接口的简洁性和安全性。

image-20250519204722086

@Data
public class UserLoginRequest {// 直接在 UserLoginRequest 对象的属性中进行校验, 而不是在 Login 接口中校验// 如果要使用下面的注解, 对传入的 UserLoginRequest 对象进行校验, 需要在 login 接口参数前加上注解 @Validated @NotNull(message = "用户名不能为空")@Length(max = 20, min = 1, message = "用户名长度不合法")private String userName;@NotNull(message = "密码不能为空")private String password;
}

接下来,我们将通过 UserLoginRequest 实体类接收登录接口的参数,并使用 @Validated 对其进行校验。

image-20250519211436251

public class UserController {// @Autowired// (6) UserService 接口只有一个实现类 UserServiceImpl , 所以可以使用 @Autowired, 会自动匹配到 UserServiceImpl 对象@Resource(name = "userServiceImpl")  // 注意, bean 名称不是 UserServiceImplprivate UserService userService;// (7) 直接使用 @Resource , 传入对象名称, 显示指定注入对象 UserServiceImpl@RequestMapping("/login")public UserLoginResponse login(@RequestBody @Validated UserLoginRequest userLoginRequest){// (1) 传入的参数是 JSON 格式的,因此需要在参数前加上 @RequestBody// (2) 对传入的参数对象 UserLoginRequest 先进行校验// (3) 在参数名前加上 @Validated 注解, 才会对 UserLoginRequest 对象中的属性进行校验// (4) 校验完成, 先打印一个日志, 只打印用户名, 不用打印密码log.info("用户登录, 用户名:" + userLoginRequest.getUserName());// (5) 接下来, 需要调用 Service 层对数据进行处理, 并且返回 Service 层处理的结果// (8) Service 层主要是先校验 userName 对应的 password 是否正确, 再返回用户的数据return userService.checkPassword(userLoginRequest);}
}

校验通过后,将 UserLoginRequest 对象传递给 Service 层进行进一步处理。


实现 Service


image-20250519211525250

UserService

public interface UserService {UserLoginResponse checkPassword(UserLoginRequest userLoginRequest);
}

UserServiceImpl

@Service
public class UserServiceImpl implements UserService {// (1) 明确一下, Service 层写相关的业务逻辑@Autowiredprivate UserInfoMapper userInfoMapper;// (3) 查询数据库, 就需要先注入 Mapper 对象@Overridepublic UserLoginResponse checkPassword(UserLoginRequest userLoginRequest) {// (4) 构造查询条件 SQL, 根据 userName 和 deleteFlag 查询数据库中的数据QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();queryWrapper.lambda().eq(UserInfo::getUserName, userLoginRequest.getUserName()).eq(UserInfo::getDeleteFlag, 0);// (2) 先查询数据库, 使用 selectOne() 只查询一条数据, 如果查出多条数据直接报错// (3) 最好可以加上一个 try catchUserInfo userInfo = userInfoMapper.selectOne(queryWrapper);if(userInfo == null){// (4) 用户名不存在, 在 common.exception 中重新定义一个只需要传 errMsg 参数的异常 BlogExceptionthrow new BlogException("用户不存在");}// (5) 判断密码是否正确if(!userLoginRequest.getPassword().equals(userInfo.getPassword())){throw new BlogException("用户密码错误");}// (6) 接下来处理的是密码正确的逻辑, 需要返回一个 tokenMap<String, Object> map = new HashMap<>();map.put("id",  userInfo.getId());map.put("name", userInfo.getUserName());String token = JwtUtils.genToken(map);// (7) 需要给 UserLoginResponse 对象的属性赋值, 所以在 UserLoginResponse 前加 @AllArgsConstructorreturn new UserLoginResponse(userInfo.getId(), token);}
}

checkPassword() 方法的注解 (4)注解 (7)

image-20250519214446110

@Data
public class BlogException extends RuntimeException{private int code;private String errMsg;public BlogException(int code, String errMsg) {this.code = code;this.errMsg = errMsg;}// (4) 新加的 BlogException, 只需要传 errMsgpublic BlogException(String errMsg) {this.errMsg = errMsg;}
}@Data
@AllArgsConstructor  // (7) 加上全参构造函数, 方便 new UserLoginResponse(userInfo.getId(), token)
public class UserLoginResponse {private Integer userId;private String token;
}

测试接口


image-20250519222122370


传参的数据需要是 JSON 格式

image-20250519224005021


密码错误的情况:

image-20250519224126930


用户不存在的情况:

image-20250520210223479

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Overridepublic UserLoginResponse checkPassword(UserLoginRequest userLoginRequest) {QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();queryWrapper.lambda().eq(UserInfo::getUserName, userLoginRequest.getUserName()).eq(UserInfo::getDeleteFlag, 0);if(userInfo == null){// 传入的 UserLoginRequest 对象的 userName 在数据库中不存在, 说明用户名不存在throw new BlogException("用户不存在");}// (5) 判断密码是否正确if(!userLoginRequest.getPassword().equals(userInfo.getPassword())){throw new BlogException("用户密码错误");}// ......}
}@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {@ExceptionHandlerpublic Result exceptionHandle(Exception exception){log.error("发生异常, e", exception);return Result.fail(exception.getMessage());}@ExceptionHandlerpublic Result exceptionHandler(BlogException exception){log.error("发生异常, e", exception);return Result.fail(exception.getErrMsg());  // 用户不存在, 要拿 exception.getErrMsg() 而不是 exception.getMessage()}
}

image-20250520204320906


密码为空字符串的情况:

image-20250520210308807

@Data
public class UserLoginRequest {@NotNull(message = "用户名不能为空")@Length(max = 20, min = 1, message = "用户名长度不合法")private String userName;@NotNull(message = "密码不能为空")  @Length(min = 5)private String password;
}

image-20250520210419155


此时返回的响应中,errMsg 返回的内容是不好读的,我们需要对 password 为""这种异常再次进行统一异常处理;先来看后端打印的错误日志中出现的异常:

image-20250520210628467


对该异常(MethodArgumentNotValidException)进行统一异常处理:

image-20250520211217057

package com.bit.springblogdemo.common.advice;@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {// 其他异常的处理.....@ExceptionHandlerpublic Result exceptionHandler(MethodArgumentNotValidException exception){// 记不住什么时候加 {} , 就全部都加 {}log.error("发生异常, e: {}", exception.getMessage());return Result.fail("密码长度不合法");}
}

在拦截器中处理MethodArgumentNotValidException ,也可以使用在 @Length 注解中设置错误信息 Message,并作为响应信息的 errMsg 返回:

image-20250520213129838

@Data
public class UserLoginRequest {// 直接在 UserLoginRequest 对象的属性中进行校验, 而不是在 Login 接口中校验@NotNull(message = "用户名不能为空")@Length(max = 20, min = 1, message = "用户名长度不合法")private String userName;@NotNull(message = "密码不能为空")@Length(min = 5, message = "不是牢底, 一个密码都输不明白, 采九朵莲啊......")private String password;
}

重新运行程序,输入空字符串密码:

image-20250520213406337


使用 @Length 设置了 default message,接下来,我们要在统一异常处理类中,对,拿到default message

image-20250520214010100


开启调试模式后,在 Postman 中重新发送请求,再回到控制台:

image-20250520214502267


接下来,我们就需要改进这个对 MethodArgumentNotValidException异常进行处理的拦截器方法:

// 改进前
@ExceptionHandler
public Result exceptionHandler(MethodArgumentNotValidException exception){log.error("发生异常, e: {}", exception.getMessage());return Result.fail("密码长度不合法");
}// 改进后
@ExceptionHandler
public Result exceptionHandler(MethodArgumentNotValidException exception){String msg = exception.getBindingResult().getFieldError().getDefaultMessage();// 注意: 因为刚刚 err 列表中, 只有一个元素, 说明只有一个报错, 就可以使用 getFieldError(), 表示获取 err 列表的第一个元素// 最好先做一些空指针判断, 避免获取 msg 时出现空指针异常, 这里就先不做了log.error("发生异常, e: {}", exception.getMessage());return Result.fail(msg);
}

改进好异常处理拦截器后,重新发送请求:

image-20250520215244435


以上演示的使用断点,获取MethodArgumentNotValidException exception 中的 defaultMessage 属性,对于其他异常获取该属性,也是类似的,先通过打断点,找到 err 中类似于defaultMessage的属性,然后通过异常的引用,不断调用 get 方法来获取,举一反三;

image-20250520221005665


先打断点,点击小绿虫,然后调用获取博客详情接口,传空参数触发 HandlerMethodValidationException异常:

image-20250520221140663


可以看到,HandlerMethodValidationExceptiondefaultMessage 存放的地方与MethodArgumentNotValidException 是不同的:

image-20250520221613287


如果觉得此时的defaultMessage 不够好,可以直接在对应的 @NotNull 注解中写入自定义的 Message

@RequestMapping("/getBlogDetail")
public BlogInfoReponse getBlogDetail(@NotNull(message = "不是牢底, 你把你要搜的博客 id 输明白啊, 你啥都不输是几个意思呢?") Integer blogId){log.info("获取博客详情, blogId: {}", blogId);return blogService.getBlogDetail(blogId);
}

修改对应的异常处理器

    @ExceptionHandlerpublic Result exceptionHandler(HandlerMethodValidationException exception){String msg = exception.getAllErrors().stream().findFirst().get().getDefaultMessage();log.error("发生异常, e: {}", exception.getMessage());return Result.fail(msg);}

重新运行程序,并且发送 blogId == null 的请求:

image-20250520222514501


值得一提:

image-20250520223511080


实现客户端和服务端交互


实现登录页面login.html


image-20250520223610095

前端收到 userIdtoken 之后,保存在 localStorage 中。

<script src="js/jquery.min.js"></script>
<script>function login() {// location.assign("blog_list.html");  // 这里先注掉, 等完成登录校验后, 再跳转页面// (1) location.assign("blog_list.html") 等同于 location.href = blog_list.html, 都是页面跳转的一种方式// (2) 输入账号密码, 点击登录按钮后, 调用后端接口校验账号密码是否正确$.ajax({type : "post",url : "/user/login",contentType : "application/json",  // (4) 还要声明请求头类型是 application/jsondata : JSON.stringify({    // (3) 要传给后端的是 JSON 字符串, stringify() 会把 JSON 对象转为 JSON 字符串userName : $("#username").val(),password : $("#password").val()// (5) 使用 id 选择器, 选择输入框中输入的账号和密码}),// (6) 使用 Postman 测试不同账号和密码的输入场景, 根据后端返回的响应格式, 确定前端如何解析响应数据的属性success: function (result) {if(result != null && result.code == "SUCCESS"){// (10) 密码正确, result.data 包含 userId 和 token 两个属性, 需将 token 存储到浏览器的 localStorage 或 cookie 中// (11) token 存在 cookie 中, 一般是通过后端进行 setCookie 操作的, 本次我们选择将 token 存到 Local storage 中// (12) localStorage.setItem() -> 存/更新、 localStorage.getItem() -> 取、 localStorage.removeItem() - >删除localStorage.setItem("loginUserId", result.data.userId);localStorage.setItem("userToken", result.data.token);// (13) 这三个方法的参数都是 (key: String, value: String), 我们需要分别把 userId、 token 作为参数传入 setItem() 方法中// (14) 这里同样省略了对 result.data 的非空校验location.href = "blog_list.html"; // (15) 登录成功, 跳转到博客列表页面}else {// (7) 密码错误, 密码长度不符合要求, 账号名不存在......等情况// (8) result == null 的情况未处理// (9) 即便 success 触发意味着 result 不为 null, 在多人协作中, 仍需处理 result == null 的情况, 以确保代码的健壮性alert(result.errMsg);}}})
}
</script>

token 存到浏览器Local storage 中,可以在浏览器中查看:

在这里插入图片描述


Local Storage 相关操作

存储数据

localStorage.setItem("user_token", "value");

读取数据

localStorage.getItem("user_token");

删除数据

localStorage.removeItem("user_token");

Ajax 发生异常时,进行异常处理,公共处理,可以提取到 common.js

$(document).ajaxError(function(event, xhr, options, exc) {if (xhr.status == 400) {alert("参数校验失败");}
});

部署程序,验证效果。


测试接口


用户名为空的情况

image-20250522205118019


用户名不存在的情况

image-20250522205000748


用户名正确,密码错误的情况

image-20250522205046209


用户名正确,密码为空的情况

image-20250522205153664


用户密码输入正确,跳转博客列表

image-20250522205229352

通过以上的操作,煮波就带着大家过了一遍关于用户登录接口实现的步骤咯~~~


在这里插入图片描述

在这里插入图片描述

相关文章:

  • 【C++/控制台】迷宫游戏
  • SQL每日一练
  • CloudWeGo-Netpoll:高性能NIO网络库浅析
  • python web 开发-Flask-Login使用详解
  • AtCoder AT_abc407_c [ABC407C] Security 2
  • 开发者工具箱-鸿蒙设备信息功能开发实践
  • 神经算子与FNO技术详解
  • 浅析Spring AOP 代理的生成机制
  • 实现Web网站冷启动的全面指南
  • [软件测试_4] 沟通技巧 | 测试用例 | 设计方法
  • 基于cornerstone3D的dicom影像浏览器 第二十二章 mpr + vr
  • 基于AI生成测试用例的处理过程
  • TestHubo V1.0.8版本发布,支持按模块树筛选用例,让查询更便捷
  • A-Teacher: Asymmetric Network for 3D Semi-Supervised Object Detection
  • c/c++的opencv像素级操作二值化
  • 【RAG文档切割】从基础拆分到语义分块实战指南
  • 【动态规划】P12223 [蓝桥杯 2023 国 Java B] 非对称二叉树|普及+
  • 使用ps为图片添加水印
  • Gitlab-Runner安装
  • 【人工智能】AI的炼金术:大模型训练的秘密配方
  • 用花生壳做网站/如何做网络销售产品
  • 常州微信网站建设流程/网络工程师培训一般多少钱
  • 政府网站建设 报价/推广发帖网站
  • 凉山彝族自治州网站建站/友情链接多久有效果
  • 做网站必须注册的商标/西安seo关键词查询
  • 小程序询价表/seo百度点击软件