目录
一、今日目标
二、SpringBoot后端实现
2.1 新增UserLoginParam
2.2 修改UserController
2.3 UserServiceImpl代码
2.4 创建用户上下文工具类
2.5 通过token校验用户(重要)
2.6 创建WebMvcConfig
2.7 用户权限校验拦截器
一、今日目标
上篇文章链接:【wiki知识库】08.添加用户登录功能--前端Vue部分修改-CSDN博客
这篇文章主要是实现一下用户登录功能的后端部分,登录功能需要使用redis,不懂redis可以看我之前的一篇文章。
Redis文章链接:【Spring】SpringBoot整合Redis,用Redis实现限流(附Redis解压包)_springboot 限流 redis-CSDN博客
那么为什么要用到Redis呢?
这个问题关系到整个系统的用户校验,当我们登录成功的时候,后端会生成一个用于用户校验的token值,然后把这个值传给前端,每次用户请求后端的时候都要带上这个token值,这个token的值当中记录了当前登录的用户是谁,还有过期时间等信息,这样子就可以防止那些没有登陆的用户去直接访问我们的后端调用接口。所以这个token还是需要妥善保管的,一旦token丢失别人就可能用你的token去发送请求,修改你的数据。
二、SpringBoot后端实现
2.1 新增UserLoginParam
这里也做了校验,其实这个事情完全可以放到前端实现,但是也要考虑到有直接调用接口的情况,这时也要给出错误提示。
@Data public class UserLoginParam { @NotEmpty(message = "【用户名】不能为空") private String loginName; @NotEmpty(message = "【密码】不能为空") @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】规则不正确") private String password; }
2.2 修改UserController
直接上代码吧。这里拿到了用户的账号和用户的密码,然后判断加密后的密码和数据库中取出来的用户密码是否相同,如果相同那么就可以登陆。登陆后通过工具类生成一个不会重复的Long类型的值作为该用户的token,然后以token为key,登录用户创建的对象作为值,保存到redis当中,以便于后续用户访问接口时,通过用户token来判断是哪个用户访问接口。
@PostMapping("/login") public CommonResp login(@Valid @RequestBody UserLoginParam req) { req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes())); System.out.println(req); UserLoginVo userLoginResp = userService.login(req); Long token = snowFlake.nextId(); userLoginResp.setToken(token.toString()); redisTemplate.opsForValue().set(token.toString(), JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS); return new CommonResp(true,"登录成功",userLoginResp); } @GetMapping("/logout/{token}") public CommonResp logout(@PathVariable String token) { boolean res = redisTemplate.delete(token); String message = Boolean.TRUE.equals(res) ? "登出成功":"登出失败"; return new CommonResp(true,message,null); }
2.3 UserServiceImpl代码
这个代码没有什么好说的,就是查找一次数据库进行账号密码的匹配。
public UserLoginVo login(UserLoginParam req) { User userDb = selectByLoginName(req.getLoginName()); if (ObjectUtils.isEmpty(userDb)) { // 用户名不存在 throw new RuntimeException("用户名不存在"); } else { if (userDb.getPassword().equals(req.getPassword())) { // 登录成功 UserLoginVo userLoginResp = CopyUtil.copy(userDb, UserLoginVo.class); return userLoginResp; } else { // 密码不对 throw new RuntimeException("密码错误"); } } }
2.4 创建用户上下文工具类
这个工具类用户用户登录后保存当前用户的上下文。
public class LoginUserContext implements Serializable { private static ThreadLocal<UserLoginVo> user = new ThreadLocal<>(); public static UserLoginVo getUser() { return user.get(); } public static void setUser(UserLoginVo user) { LoginUserContext.user.set(user); } }
2.5 通过token校验用户(重要)
校验用户token需要使用到拦截器或者过滤器,这里我使用拦截器进行用户token的校验。整体的校验流程如下
以下就是登录拦截器的代码,
/** * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 */ @Component public class LoginInterceptor implements HandlerInterceptor { private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class); @Resource private RedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // OPTIONS请求不做校验, // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验 if (request.getMethod().toUpperCase().equals("OPTIONS")) { return true; } String path = request.getRequestURL().toString(); LOG.info("接口登录拦截:,path:{}", path); //获取header的token参数 String token = request.getHeader("token"); LOG.info("登录校验开始,token:{}", token); if (token == null || token.isEmpty()) { LOG.info("token为空,请求被拦截"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } Object object = redisTemplate.opsForValue().get(token); // 证明redis中的用户信息过期了,需要重新登陆 if (object == null) { LOG.warn("token无效,请求被拦截"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } else { LOG.info("已登录:{}", object); LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginVo.class)); return true; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
接下来要把这个拦截器注册到配置当中。
2.6 创建WebMvcConfig
在config包下创建该类。这个类当中配置了两个拦截器,一个是登录拦截器,另一个是用户权限校验拦截器。用户校验拦截器下边再说。登录拦截器只需要部分接口进行拦截就可以了,毕竟有的接口不需要登陆用户就可以访问。
有一点值得注意的是,在这个配置类中配置的拦截器的顺序会影响校验结果,校验的流程是根据你配置的拦截器的顺序从上往下校验的,如果你把拦截器配置写反了就会出错。
addInterceptor 注册一个拦截器 addPathPatterns 该拦截器需要拦截的路径 excludePathPatterns 该拦截器不需要拦截的路径 @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Resource LoginInterceptor loginInterceptor; @Resource ActionInterceptor actionInterceptor; public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/test/**", "/redis/**", "/user/login", "/user/logout/**", "/category/all", "/ebook/list", "/doc/all/**", "/doc/find-content/**", ); registry.addInterceptor(actionInterceptor) .addPathPatterns( "/*/save", "/*/delete/**", "/*/reset-password"); } }
2.7 用户权限校验拦截器
看到下方的代码你应该知道了用户上下文的作用,通过用户上下文拿到用户的信息来判断该用户是否有访问该接口的权利,我们拒绝非admin用户外的用户进行增删改操作。
但是这种方法有点不太好不知道你们有没有感觉到,一旦用户多了之后,如果你想给用户分配权限,你就要添加很多的用户在这里。所以一种更好的方式就是RBAC权限校验,大家可以自己了解一下,也有更好的权限校验框架SpringSecurity,但是作为一个比较简单的项目,引入这个框架的学习成本就太大了。
/** * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 */ @Component public class ActionInterceptor implements HandlerInterceptor { private static final Logger LOG = LoggerFactory.getLogger(ActionInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { // OPTIONS请求不做校验, // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验 if ("OPTIONS".equals(request.getMethod().toUpperCase())) { return true; } UserLoginVo userLoginResp = LoginUserContext.getUser(); if ("admin".equals(userLoginResp.getLoginName())) { // admin用户不拦截 return true; } LOG.info("操作被拦截"); response.setStatus(HttpStatus.OK.value()); CommonResp commonResp = new CommonResp(false,"普通用户暂不开放增删改操作",null); response.setContentType("application/json;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); response.getWriter().print(JSONObject.toJSON(commonResp)); return false; } }