用户管理,用户登录,登录校验(界面+接口)
用户表设计与持久层代码生成
基本的增删改查
用户名不能重复(特殊需求)
关于密码的两层加密处理:加密传输+加密存储(特殊需求)
重置密码(特殊需求)

登录功能
前端登录接口
后端登录接口
登录成功后的处理
退出登录

用户表设计与持久层代码生成

首先在数据库当中执行下面的sql, 其中主键是id,但是对login_name做了唯一键处理,就是在整个系统当中,login_name是不能重复的

1. 用户表设计

  1. drop table if exists `user`;
  2. create table `user` (
  3. `id` bigint not null comment 'ID',
  4. `login_name` varchar(50) not null comment '登录名',
  5. `name` varchar(50) comment '昵称',
  6. `password` char(32) not null comment '密码',
  7. primary key (`id`),
  8. unique key `login_name_unique` (`login_name`)
  9. ) engine=innodb default charset=utf8mb4 comment='用户';

2. 代码生成

修改generator-config.xml当中的最后一行,然后执行mybatis-generator命令生成user的持久层代码:domain/User.java, domain/UserExample.java,mapper/UserMapper.java, UserMapper.xml

  1. <table tableName="user"/>

用户表的增删改查

下面我们就按照电子书管理,复制出一套用户管理的代码:

1. 后端代码展示

(1)controller部分

直接从EbookController当中将内容复制到UserController当中,然后将Ebook替换成User,将ebook替换成user。

(2)service部分

直接从EbookService当中将内容复制到UserService当中,然后将Ebook替换成User,将ebook替换成user。然后在list方法当中的动态sql部分进行修改一下:

  1. // 动态sql的写法
  2. if(!ObjectUtils.isEmpty(req.getLoginName())) {
  3. criteria.andLoginNameEqualTo(req.getLoginName());
  4. }

因为查询具体的用户一般都是通过用户的名称来进行条件查询。

(3)req部分

这里就包含两个文件,一个是UserQueryReq(和domain当中的User.java是一样的),一个是UserSaveReq,内容如下:

  1. package com.taopoppy.wiki.req;
  2. import javax.validation.constraints.NotNull;
  3. import javax.validation.constraints.Pattern;
  4. public class UserSaveReq {
  5. private Long id;
  6. @NotNull(message = "【用户名】不能为空")
  7. private String loginName;
  8. @NotNull(message = "【昵称】不能为空")
  9. private String name;
  10. @NotNull(message = "【密码】不能为空")
  11. // @Length(min = 6, max = 20, message = "【密码】6~20位")
  12. @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】至少包含 数字和英文,长度6-32")
  13. private String password;
  14. public Long getId() {
  15. return id;
  16. }
  17. public void setId(Long id) {
  18. this.id = id;
  19. }
  20. public String getLoginName() {
  21. return loginName;
  22. }
  23. public void setLoginName(String loginName) {
  24. this.loginName = loginName;
  25. }
  26. public String getName() {
  27. return name;
  28. }
  29. public void setName(String name) {
  30. this.name = name;
  31. }
  32. public String getPassword() {
  33. return password;
  34. }
  35. public void setPassword(String password) {
  36. this.password = password;
  37. }
  38. @Override
  39. public String toString() {
  40. StringBuilder sb = new StringBuilder();
  41. sb.append(getClass().getSimpleName());
  42. sb.append(" [");
  43. sb.append("Hash = ").append(hashCode());
  44. sb.append(", id=").append(id);
  45. sb.append(", loginName=").append(loginName);
  46. sb.append(", name=").append(name);
  47. sb.append(", password=").append(password);
  48. sb.append("]");
  49. return sb.toString();
  50. }
  51. }

(4)resp部分

UserQueryResp.java的内容和domain当中User.java也是一样的

2. 前端代码展示

前端代码用户管理实际上和电子书管理几乎一致,页面主要集中在views\admin\admin-user.vue,src\views\doc.vue, src\router\index.ts和src\components\the-header.vue当中,具体的这里就不展示了。

用户名重复校验与自定义异常

1. 新增用户增加重复校验

当我们在新增用户的时候,应该先去检查一下数据库当中是否存在用户,然后再去进行新增:

  1. /**
  2. * 根据用户名查找用户
  3. * @param LoginName
  4. * @return
  5. */
  6. public User selectByLoginName(String LoginName) {
  7. UserExample userExample = new UserExample();
  8. // createCriteria相当于where条件
  9. UserExample.Criteria criteria = userExample.createCriteria();
  10. // 动态sql的写法
  11. criteria.andLoginNameEqualTo(LoginName);
  12. List<User> usersList = userMapper.selectByExample(userExample);
  13. // 判断List是否为空
  14. if(CollectionUtils.isEmpty(usersList)) {
  15. return null;
  16. } else {
  17. return usersList.get(0);
  18. }
  19. }
  20. /**
  21. * 电子书保存接口(保存包括编辑保存->更新, 也包括新增保存->新增, 根据req是否有id来判断)
  22. * @param req 电子书保存参数
  23. */
  24. public void save(UserSaveReq req) {
  25. User user = CopyUtil.copy(req, User.class);
  26. if(ObjectUtils.isEmpty(req.getId())) {
  27. // 判断用户名是否在数据库存在
  28. User userDB = selectByLoginName(req.getLoginName());
  29. if(ObjectUtils.isEmpty(userDB)) {
  30. // 用户名不存在就新增
  31. user.setId(snowFlake.nextId());
  32. userMapper.insert(user);
  33. } else {
  34. // 提示已存在(利用自定义异常,抛出一个自定义异常)
  35. throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
  36. }
  37. } else {
  38. // 更新
  39. // 因为updateByPrimaryKey传递的User类型的参数,所以需要将UserSaveReq 转化成User
  40. userMapper.updateByPrimaryKey(user);
  41. }
  42. }

2. 校验重复抛出自定义异常

上面我们在检查到用户名在数据库当中已经存在的时候,抛出了一个BusinessException的异常,这个异常是我们自己定义的,同时我们在之前就说过,抛出的异常全部在ControllerExceptionHandler当中进行捕获和处理,我们下面来说明一下:

  1. package com.taopoppy.wiki.exception;
  2. public class BusinessException extends RuntimeException{
  3. private BusinessExceptionCode code;
  4. public BusinessException (BusinessExceptionCode code) {
  5. super(code.getDesc());
  6. this.code = code;
  7. }
  8. public BusinessExceptionCode getCode() {
  9. return code;
  10. }
  11. public void setCode(BusinessExceptionCode code) {
  12. this.code = code;
  13. }
  14. /**
  15. * 不写入堆栈信息,提高性能
  16. */
  17. @Override
  18. public Throwable fillInStackTrace() {
  19. return this;
  20. }
  21. }
  1. package com.taopoppy.wiki.exception;
  2. public enum BusinessExceptionCode {
  3. USER_LOGIN_NAME_EXIST("登录名已存在"),
  4. LOGIN_USER_ERROR("用户名不存在或密码错误"),
  5. VOTE_REPEAT("您已点赞过"),
  6. ;
  7. private String desc;
  8. BusinessExceptionCode(String desc) {
  9. this.desc = desc;
  10. }
  11. public String getDesc() {
  12. return desc;
  13. }
  14. public void setDesc(String desc) {
  15. this.desc = desc;
  16. }
  17. }

前面我们对参数校验的时候对BindException类型的错误进行的了处理,现在我们同样在ControllerExceptionHandler.java当中对BusinessException和普遍的错误Exception做处理:

  1. package com.taopoppy.wiki.controller;
  2. import com.taopoppy.wiki.exception.BusinessException;
  3. import com.taopoppy.wiki.resp.CommonResp;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.validation.BindException;
  7. import org.springframework.web.bind.annotation.ControllerAdvice;
  8. import org.springframework.web.bind.annotation.ExceptionHandler;
  9. import org.springframework.web.bind.annotation.ResponseBody;
  10. /**
  11. * 统一异常处理、数据预处理等
  12. */
  13. @ControllerAdvice
  14. public class ControllerExceptionHandler {
  15. private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
  16. /**
  17. * 校验异常统一处理
  18. * @param e
  19. * @return
  20. */
  21. @ExceptionHandler(value = BindException.class)
  22. @ResponseBody
  23. public CommonResp validExceptionHandler(BindException e) {
  24. CommonResp commonResp = new CommonResp();
  25. LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
  26. commonResp.setSuccess(false);
  27. commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
  28. return commonResp;
  29. }
  30. /**
  31. * 校验异常统一处理
  32. * @param e
  33. * @return
  34. */
  35. @ExceptionHandler(value = BusinessException.class)
  36. @ResponseBody
  37. public CommonResp validExceptionHandler(BusinessException e) {
  38. CommonResp commonResp = new CommonResp();
  39. LOG.warn("业务异常:{}", e.getCode().getDesc()); // 获取到业务异常的枚举
  40. commonResp.setSuccess(false);
  41. commonResp.setMessage(e.getCode().getDesc());
  42. return commonResp;
  43. }
  44. /**
  45. * 校验异常统一处理
  46. * @param e
  47. * @return
  48. */
  49. @ExceptionHandler(value = Exception.class)
  50. @ResponseBody
  51. public CommonResp validExceptionHandler(Exception e) {
  52. CommonResp commonResp = new CommonResp();
  53. LOG.error("系统异常:", e);
  54. commonResp.setSuccess(false);
  55. commonResp.setMessage("系统出现异常,请联系管理员");
  56. return commonResp;
  57. }
  58. }

3. 用户名无法修改

同时用户名也不能随便修改,所以我们在编辑的时候是需要判断的。

判断也分两个步骤,第一是前端判断,对用户名的输入框进行判断,如果是新增的话,就可以编辑,如果是编辑用户信息的话,用户名的输入框就设置disabled。

可是前端的校验是不安全的,因为可以被绕过(前端可以打开F12把输入框的disabled属性去掉),所以我们还需要在后端进行校验,下面这个updateByPrimaryKeySelective方法表示只有user里面的属性有值,才会去更新,没有值就不更新这个字段,所以即便前端传来了新的用户名,在这里我们手动置为null,数据库就不会去更新这个字段。

  1. // 更新
  2. user.setLoginName(null);
  3. userMapper.updateByPrimaryKeySelective(user);

关于密码的两层加密处理

密码保存在数据库当中不能以明文的方式存储,必须以密文的方式,也就是将明文进行一个MD5的处理即可。

1. 密码加密存储

  1. @PostMapping("/save")
  2. public CommonResp save(@Valid @RequestBody UserSaveReq req) {
  3. // 将密码进行md5加密,也就是加密后变成了一个32位16进制的字符串
  4. req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
  5. CommonResp resp = new CommonResp<>();
  6. userService.save(req);
  7. return resp;
  8. }

2. 密码加密传输

在前端我们有一个md5.js文件的,其中的hexMd5方法和java当中DigestUtils.md5DigestAsHex() 对相同的字符串加密的结果是一样的,这个文件是在public/index.html当中是进行引入了的。

前端我们要在点击保存按钮的时候对输入的密码进行加密,然后加密的字符串传到后端再进行一次加密放在数据库当中,这就是两层加密。前端对输入的密码加密是为了保证传输的过程当中不被截获,后端对加密的密码再加密为了保证数据的安全性。

前端的加密代码在admin-user.vue当中:

  1. declare let hexMd5: any; // 告诉TS这两个变量定义过了,实际在md5.js当中定义在了全局
  2. declare let KEY: any;
  3. user.value.password = hexMd5(user.value.password + KEY);

其中KEY表示盐值,也定义在md5.js当中,帮助增强加密的复杂性的。

增加重置密码功能

1. 修改用户时,不能修改密码

我们新增用户的时候,密码是需要手动输入的,但是在编辑用户的时候,是不能修改面的,所以在修改用户信息的时候,直接可以不显示密码的输入框,因为后端返回来的是一堆密文,也看不懂。

前端在新增用户和修改用户是判断一下是否显示用户密码的输入框即可。后端也需要和前面将登录名置为null一样,将密码也置为null

  1. user.setLoginName(null);
  2. user.setPassword(null); // 将密码也置为null
  3. userMapper.updateByPrimaryKeySelective(user);

2. 单独开发重置密码表单和接口

我们新添加一个重置密码的按钮,弹出一个表单,表单当中只有一个新密码的输入框,整个也就是说你前端是没办法看到原始密码的明文的。

(1)后端代码展示

  1. @PostMapping("/reset-password")
  2. public CommonResp resetPassword(@Valid @RequestBody UserResetPasswordReq req) {
  3. // 将密码进行md5加密,也就是加密后变成了一个32位16进制的字符串
  4. req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
  5. CommonResp resp = new CommonResp<>();
  6. userService.resetPassword(req);
  7. return resp;
  8. }
  1. package com.taopoppy.wiki.req;
  2. import javax.validation.constraints.NotNull;
  3. import javax.validation.constraints.Pattern;
  4. public class UserResetPasswordReq extends PageReq{
  5. private Long id;
  6. @NotNull(message = "【密码】不能为空")
  7. // @Length(min = 6, max = 20, message = "【密码】6~20位")
  8. @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】至少包含 数字和英文,长度6-32")
  9. private String password;
  10. public Long getId() {
  11. return id;
  12. }
  13. public void setId(Long id) {
  14. this.id = id;
  15. }
  16. public String getPassword() {
  17. return password;
  18. }
  19. public void setPassword(String password) {
  20. this.password = password;
  21. }
  22. @Override
  23. public String toString() {
  24. StringBuilder sb = new StringBuilder();
  25. sb.append(getClass().getSimpleName());
  26. sb.append(" [");
  27. sb.append("Hash = ").append(hashCode());
  28. sb.append(", id=").append(id);
  29. sb.append(", password=").append(password);
  30. sb.append("]");
  31. return sb.toString();
  32. }
  33. }
  1. /**
  2. * 重置密码
  3. * @param req 电子书保存参数
  4. */
  5. public void resetPassword(UserResetPasswordReq req) {
  6. User user = CopyUtil.copy(req, User.class); // user当中只有id和password两个属性值
  7. userMapper.updateByPrimaryKeySelective(user);
  8. }

(2)前端代码展示

前端代码均在admin-user.vue当中展示,实际上重置密码的表单和修改用户几乎差不多,这里就不做过多展示。

单点登录和JWT

登录和校验我们分开来讲:

1. 登录

  • 前端在登录界面输入用户名和密码
  • 用户名和密码传入后端进行校验
  • 校验成功生成token
  • 后端将token保存在redis当中
  • token传给前端,前端也保存一份

2. 校验

  • 前端请求时,带上token(放在header当中)
  • 后端有登录拦截器,校验token(到redis获取token,能获取的到说明token有效,获取不到说明token无效或者过期)
  • 校验成功则继续后面的业务
  • 校验不成功则返回标识,前端拿到标识就返回登录页面

3. 单点登录系统

  • 假如一个公司内有好几个系统,A,B,C,好比淘宝,支付宝,花呗都是同一个公司下的系统,假如登录了A,然后跳到B还要重新登录这是不可能的,然后不可能每个系统都自己做一套登录系统。

  • 那么登录系统就会单独做一套X单点登录系统,它负责所有系统ABC的登录业务,包括用户管理,登录,登录检验,退出登录。一般有两种情况,A,B,C自己画页面,接口全部由X单点系统提供,第二种情况就是登录页面都由X单点系统来提供

4. token和JWT

token:本质就是唯一的一串字符串,相当于证明用户登录过的有效期令牌。token字符串本身是没有任何含义的,只要保证它是唯一的即可,所以可以创建token的方法很多,比如雪花算法,UUID,JWT也是生成token的技术。

JWT:是一种加密方式,使得token变成加密字符串,同时包含了业务信息,这个业务信息就是用户的信息,所以token字符串就变得有意义了,意义在于包含了用户的相关信息和登录信息

JWT加密后的字符串实际包含三个部分:

  • 第一部分:头部,会标识该JWT使用的什么加密算法
  • 第二部分:业务信息,即用户的登录信息
  • 第三部分:签名信息

JWT从加密技术层面来说三个部分是息息相关的,可以解密,整个技术应用并不复杂,前面我们说的是token+redis,现在我们说的是JWT+redis,这是一个通用的做法,关于JWT的使用呢其实引入一个依赖包即可:

  1. <dependency>
  2. <groupId>com.auth0</groupId>
  3. <artifactId>java-jwt</artifactId>
  4. <version>3.5.0</version>
  5. </dependency>

然后主要使用的两个方法就是JwtUtil.sign和JwtUtil.verity,一个加密一个解密。

登录功能开发

1. 后端增加登录接口

前端输入用户名和密码后,点击登录,前端会进行一次加密,后端接收的请求包含用户名和一次加密的密码,然后后端的逻辑是先去查用户名,用户名是唯一的,然后再将二次加密的密码和该用户的数据库密码进行比对,如果成功则返回用户信息,不成功返回响应的提示:

  1. package com.taopoppy.wiki.req;
  2. import javax.validation.constraints.NotNull;
  3. import javax.validation.constraints.Pattern;
  4. public class UserLoginReq {
  5. @NotEmpty(message = "【用户名】不能为空")
  6. private String loginName;
  7. @NotEmpty(message = "【密码】不能为空")
  8. // @Length(min = 6, max = 20, message = "【密码】6~20位")
  9. @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】规则不正确")
  10. private String password;
  11. public String getLoginName() {
  12. return loginName;
  13. }
  14. public void setLoginName(String loginName) {
  15. this.loginName = loginName;
  16. }
  17. public String getPassword() {
  18. return password;
  19. }
  20. public void setPassword(String password) {
  21. this.password = password;
  22. }
  23. @Override
  24. public String toString() {
  25. StringBuilder sb = new StringBuilder();
  26. sb.append(getClass().getSimpleName());
  27. sb.append(" [");
  28. sb.append("Hash = ").append(hashCode());
  29. sb.append(", loginName=").append(loginName);
  30. sb.append(", password=").append(password);
  31. sb.append("]");
  32. return sb.toString();
  33. }
  34. }
  1. package com.taopoppy.wiki.resp;
  2. public class UserLoginResp {
  3. private Long id;
  4. private String loginName;
  5. private String name;
  6. public Long getId() {
  7. return id;
  8. }
  9. public void setId(Long id) {
  10. this.id = id;
  11. }
  12. public String getLoginName() {
  13. return loginName;
  14. }
  15. public void setLoginName(String loginName) {
  16. this.loginName = loginName;
  17. }
  18. public String getName() {
  19. return name;
  20. }
  21. public void setName(String name) {
  22. this.name = name;
  23. }
  24. @Override
  25. public String toString() {
  26. StringBuilder sb = new StringBuilder();
  27. sb.append(getClass().getSimpleName());
  28. sb.append(" [");
  29. sb.append("Hash = ").append(hashCode());
  30. sb.append(", id=").append(id);
  31. sb.append(", loginName=").append(loginName);
  32. sb.append(", name=").append(name);
  33. sb.append("]");
  34. return sb.toString();
  35. }
  36. }
  1. @PostMapping("/login")
  2. public CommonResp login(@Valid @RequestBody UserLoginReq req) {
  3. // 这里同样要将前端加密的密文再加密,两次加密后才能和数据库当中保存的一致
  4. req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
  5. CommonResp<UserLoginResp> resp = new CommonResp<>();
  6. UserLoginResp userLoginResp = userService.login(req);
  7. resp.setContent(userLoginResp);
  8. return resp;
  9. }
  1. /**
  2. * 登录
  3. * @param req
  4. */
  5. public UserLoginResp login(UserLoginReq req) {
  6. // 先按照用户名去查
  7. User userDB = selectByLoginName(req.getLoginName());
  8. // 查出来用户名对应的用户再去对比密码
  9. if(ObjectUtils.isEmpty(userDB)) {
  10. // 用户名不存在
  11. LOG.info("用户名不存在,{}", req.getLoginName());
  12. throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
  13. } else {
  14. if(userDB.getPassword().equals(req.getPassword())) {
  15. // 登录成功
  16. UserLoginResp userLoginResp = CopyUtil.copy(userDB, UserLoginResp.class);
  17. return userLoginResp;
  18. } else {
  19. // 密码输入错误
  20. LOG.info("密码不对,输入密码,{},数据库密码,{}", req.getLoginName());
  21. throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
  22. }
  23. }
  24. }

2. 前端增加登录模态框

前端的登录模态框是写在the-header.vue当中的,可以到文件当中去查看

✿登录成功处理

1. 后端保存信息

后端保存用户信息,需要集成redis,登录成功后,生成token,以token为key,以用户信息为value,放入redis当中,前端发来请求需要登录校验的接口,后端就会拿着前端发来的token去redis找,找不到说明token在redis当中已经过期或者压根不在redis当中

(1)集成redis

在pom.xml当中去添加下面的依赖

  1. <!--整合redis-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>

然后在application.properties当中添加redis的连接配置:

  1. # redis配置
  2. spring.redis.host=r-uf6ljbcdaxobsifyctpd.redis.rds.aliyuncs.com
  3. spring.redis.port=6379
  4. spring.redis.password=Redis000

(2)改造代码

由于我们后端在登录成功后要做两个事情

  • 将生成token进行返回给前端,
  • 将token存储在redis当中

所以我们首先先修改一下UserLoginResp.java, 添加下面的内容,就是添加了一个新属性token

  1. package com.taopoppy.wiki.resp;
  2. public class UserLoginResp {
  3. private String token;
  4. public String getToken() {
  5. return token;
  6. }
  7. public void setToken(String token) {
  8. this.token = token;
  9. }
  10. }

然后将token要保存在redis当中,我们就要修改UserController.java:

  1. @PostMapping("/login")
  2. public CommonResp login(@Valid @RequestBody UserLoginReq req) {
  3. // 这里同样要将前端加密的密文再加密,两次加密后才能和数据库当中保存的一致
  4. req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
  5. CommonResp<UserLoginResp> resp = new CommonResp<>();
  6. UserLoginResp userLoginResp = userService.login(req);
  7. // 登录成功后,生成单点登录token,并放入redis当中
  8. Long token = snowFlake.nextId();
  9. LOG.info("生成单点登录token,{},并放入redis", token);
  10. userLoginResp.setToken(token.toString());
  11. redisTemplate.opsForValue().set(token, JSONObject.toJSONString(userLoginResp),3600*24, TimeUnit.SECONDS);
  12. resp.setContent(userLoginResp);
  13. return resp;
  14. }

2. 前端保存信息

前端显示登录用户,header显示登录昵称,使用vuex+sessionStorage保存登录信息

前端登录成功后,拿到后端返回的用户信息还有token,要进行持久化存储,使用到vuex和sessionStorage,所以我们前端拿到数据后,就要进行向vuex和sessionStorage当中存储的工作:

  1. // 登录
  2. const login = () => {
  3. console.log("开始登录");
  4. loginModalLoading.value = true;
  5. loginUser.value.password = hexMd5(loginUser.value.password + KEY);
  6. axios.post('/user/login', loginUser.value).then((response) => {
  7. loginModalLoading.value = false;
  8. const data = response.data;
  9. if (data.success) {
  10. loginModalVisible.value = false;
  11. message.success("登录成功!");
  12. store.commit("setUser", data.content);
  13. } else {
  14. message.error(data.message);
  15. }
  16. });
  17. };
  1. import { createStore } from 'vuex'
  2. declare let SessionStorage: any;
  3. const USER = "USER";
  4. const store = createStore({
  5. state: {
  6. user: SessionStorage.get(USER) || {}
  7. },
  8. mutations: {
  9. setUser (state, user) {
  10. console.log("store user:", user);
  11. state.user = user;
  12. SessionStorage.set(USER, user);
  13. }
  14. },
  15. actions: {
  16. },
  17. modules: {
  18. }
  19. });
  20. export default store;
  1. //sessionStorage只能存字符串,所以object这里我们做个封装
  2. SessionStorage = {
  3. get: function (key) {
  4. var v = sessionStorage.getItem(key);
  5. if (v && typeof(v) !== "undefined" && v !== "undefined") {
  6. return JSON.parse(v);
  7. }
  8. },
  9. set: function (key, data) {
  10. sessionStorage.setItem(key, JSON.stringify(data));
  11. },
  12. remove: function (key) {
  13. sessionStorage.removeItem(key);
  14. },
  15. clearAll: function () {
  16. sessionStorage.clear();
  17. }
  18. };

这样之后,前端任何页面任何组件内想要拿到用户信息都可以通过下面这种方式,在template当中就可以使用user了,比如user.name,user.id等等

  1. setup() {
  2. const user = computed(() => store.state.user);
  3. return {
  4. user
  5. }
  6. }

后端接口增加登录校验

1. 后端增加拦截器校验token

后端是书写了一个拦截器,用来对需要进行登录校验的接口进行登录校验:

  1. package com.taopoppy.wiki.interceptor;
  2. import com.alibaba.fastjson.JSON;
  3. import com.taopoppy.wiki.resp.UserLoginResp;
  4. import com.taopoppy.wiki.util.LoginUserContext;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.data.redis.core.RedisTemplate;
  8. import org.springframework.http.HttpStatus;
  9. import org.springframework.stereotype.Component;
  10. import org.springframework.web.servlet.HandlerInterceptor;
  11. import org.springframework.web.servlet.ModelAndView;
  12. import javax.annotation.Resource;
  13. import javax.servlet.http.HttpServletRequest;
  14. import javax.servlet.http.HttpServletResponse;
  15. /**
  16. * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
  17. */
  18. @Component
  19. public class LoginInterceptor implements HandlerInterceptor {
  20. private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);
  21. @Resource
  22. private RedisTemplate redisTemplate;
  23. @Override
  24. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  25. // 打印请求信息
  26. LOG.info("------------- LoginInterceptor 开始 -------------");
  27. long startTime = System.currentTimeMillis();
  28. request.setAttribute("requestStartTime", startTime);
  29. // OPTIONS请求不做校验,
  30. // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
  31. if(request.getMethod().toUpperCase().equals("OPTIONS")){
  32. return true;
  33. }
  34. String path = request.getRequestURL().toString();
  35. LOG.info("接口登录拦截:,path:{}", path);
  36. //获取header的token参数
  37. String token = request.getHeader("token");
  38. LOG.info("登录校验开始,token:{}", token);
  39. if (token == null || token.isEmpty()) {
  40. LOG.info( "token为空,请求被拦截" );
  41. response.setStatus(HttpStatus.UNAUTHORIZED.value());
  42. return false;
  43. }
  44. Object object = redisTemplate.opsForValue().get(token);
  45. if (object == null) {
  46. LOG.warn( "token无效,请求被拦截" );
  47. response.setStatus(HttpStatus.UNAUTHORIZED.value());
  48. return false;
  49. } else {
  50. LOG.info("已登录:{}", object);
  51. LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginResp.class));
  52. return true;
  53. }
  54. }
  55. @Override
  56. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  57. long startTime = (Long) request.getAttribute("requestStartTime");
  58. LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
  59. }
  60. @Override
  61. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  62. // LOG.info("LogInterceptor 结束");
  63. }
  64. }

写好的拦截器在config/SpringMvcConfig.java当中配置应用:

  1. package com.taopoppy.wiki.config;
  2. import com.taopoppy.wiki.interceptor.LoginInterceptor;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. import javax.annotation.Resource;
  7. @Configuration
  8. public class SpringMvcConfig implements WebMvcConfigurer {
  9. @Resource
  10. LoginInterceptor loginInterceptor;
  11. public void addInterceptors(InterceptorRegistry registry) {
  12. registry.addInterceptor(loginInterceptor)
  13. .addPathPatterns("/**")
  14. .excludePathPatterns(
  15. "/test/**",
  16. "/redis/**",
  17. "/user/login",
  18. "/category/all",
  19. "/ebook/list",
  20. "/doc/all/**",
  21. "/doc/vote/**",
  22. "/doc/find-content/**",
  23. "/ebook-snapshot/**"
  24. );
  25. }
  26. }

还有一个文件,暂时不知道是什么功能:

  1. package com.taopoppy.wiki.util;
  2. import com.taopoppy.wiki.resp.UserLoginResp;
  3. import java.io.Serializable;
  4. public class LoginUserContext implements Serializable {
  5. private static ThreadLocal<UserLoginResp> user = new ThreadLocal<>();
  6. public static UserLoginResp getUser() {
  7. return user.get();
  8. }
  9. public static void setUser(UserLoginResp user) {
  10. LoginUserContext.user.set(user);
  11. }
  12. }

2. 前端请求增加token参数

前端请求接口的时候,应该根据vuex当中的登录信息将token放在header当中一并传入后端:后端如果返回401这种未授予权限的状态码,就直接跳转到首页(这种情况多半出现在一个网页显示了很久,后端token在redis已经过期,然后再点击网页上某个按钮请求需要登录校验的接口,接口返回401,说明token过期,网页就跳转到首页或者登录页)

  1. /**
  2. * axios拦截器
  3. */
  4. axios.interceptors.request.use(function (config) {
  5. console.log('请求参数:', config);
  6. const token = store.state.user.token;
  7. if (Tool.isNotEmpty(token)) {
  8. config.headers.token = token;
  9. console.log("请求headers增加token:", token);
  10. }
  11. return config;
  12. }, error => {
  13. return Promise.reject(error);
  14. });
  15. axios.interceptors.response.use(function (response) {
  16. console.log('返回结果:', response);
  17. return response;
  18. }, error => {
  19. console.log('返回错误:', error);
  20. const response = error.response;
  21. const status = response.status;
  22. if (status === 401) {
  23. // 判断状态码是401 跳转到首页或登录页
  24. console.log("未登录,跳到首页");
  25. store.commit("setUser", {});
  26. message.error("未登录或登录超时");
  27. router.push('/');
  28. }
  29. return Promise.reject(error);
  30. });

前端界面增加登录校验

1. 未登录隐藏管理菜单

用户未登录的时候,管理菜单是不能显示的,登录之后才有权利查看电子书管理,用户管理。

  1. <a-menu-item key="/admin/user" :style="user.id? {} : {display:'none'}">
  2. <router-link to="/admin/user">用户管理</router-link>
  3. </a-menu-item>
  4. <a-menu-item key="/admin/ebook" :style="user.id? {} : {display:'none'}">
  5. <router-link to="/admin/ebook">电子书管理</router-link>
  6. </a-menu-item>
  7. <a-menu-item key="/admin/category" :style="user.id? {} : {display:'none'}">
  8. <router-link to="/admin/category">分类管理</router-link>
  9. </a-menu-item>

2. 对路由做判断,防止手敲url

在用户未登录的时候,从界面上看不到用户管理,电子书管理等tab入口,但是可以通过手敲url进入到管理页面,所以管理页面的路由需要对登录进行校验:

  • 给需要登录校验的路由添加一个loginRequire为true的meta:
  • 对loginRequire做判断,判断用户是否登录 ```javascript const routes: Array = [ { path: ‘/admin/user’, name: ‘AdminUser’, component: AdminUser, meta: {
    1. loginRequire: true
    } }, { path: ‘/admin/ebook’, name: ‘AdminEbook’, component: AdminEbook, meta: {
    1. loginRequire: true
    } }, { path: ‘/admin/category’, name: ‘AdminCategory’, component: AdminCategory, meta: {
    1. loginRequire: true
    } }, { path: ‘/admin/doc’, name: ‘AdminDoc’, component: AdminDoc, meta: {
    1. loginRequire: true
    } }, ]

// 路由登录拦截 router.beforeEach((to, from, next) => { // 要不要对meta.loginRequire属性做监控拦截 if (to.matched.some(function (item) { console.log(item, “是否需要登录校验:”, item.meta.loginRequire); return item.meta.loginRequire })) { const loginUser = store.state.user; if (Tool.isEmpty(loginUser)) { console.log(“用户未登录!”); next(‘/‘); } else { next(); } } else { next(); } }); ``` 最后这里要强调一下:使用sessionStorage的原因是因为:sessionStorage的有效期和窗口或者浏览器的标签页是一样的,所以在同一个窗口通过route跳转,sessionStorage有效,但是如果关闭窗口,下一次手敲的话,新的窗口就没有sessionStorage

总结图

image.png
image.png