撸一个基于VUE的WEB管理后台(二)

目前我们已经拥有了一个前端登录界面,也了解了前端对于用户识别的相关机制,现在开始撸后端api server,完成用户登录功能。

和语言本身无关,Javascript和Java的生态比C++简直好了不要太多,社区力量超级活跃,而C++生态就像一个资历颇高的垂暮老人,一个17标准直到现在都没有让各类主流编译器全部实现,开发人员要为了在不同平台实现一些基本功能重复造轮子。Javascript和Java太多开箱即用的东西了,从软件工程角度来看完爆C++。不过对于一门语言,封装的东西越多,概念越抽象,坑也就隐藏得更深更隐蔽,这会使得开发人员容易入门,成为专家却更难。

话说IDEA真是一个好工具,简单几步操作就能生成一个restful api服务器框架,用习惯了就会感受到其作为一个IDE功能无比强大!

API Server工程的创建

  • New->Project->Spring Initializr->Next
  • 稍微等待以后填入Group,Artifact,工程管理我这边选择的是Maven,继续点击Next后出现列表界面
  • 由于我只是想做一个纯的api server,用户认证机制也全部自己来实现,所以只需要选择Spring Web,Spring Data JPA,MYSQL Driver这几个模块就行,当然Lombok也要带上,懒人必备。最后的选项如下图所示
    在这里插入图片描述
  • 继续Next,选择好项目路径即Finish了

工程自带一个空的application.properties,这是整个项目的配置文件,我比较喜欢yml格式的配置文件,把它改名称application.yml,先做好WEB端口,数据库连接等基本配置:

server:
  servlet:
    context-path: /
  port: 10200
  
spring:
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://localhost:3306/pf_report?serverTimezone=Hongkong&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

  servlet:
    multipart:
      max-file-size: 1000MB
      max-request-size: 1000MB

刚创建工程后,maven会下载一堆依赖包,我NAS上部署了一个Nexus仓库,常用依赖包的下载会快不少。等依赖包下载完成就可以运行了。
配置好对应的数据库服务后,我们现在开始来一步步完善这个api server。

API Server登录接口的实现

上一篇文章总结过,前端request组件会对服务端响应结果进行拦截,这对服务端的响应格式有一定要求,因此,我们先定义一个通用的VO结构,用于服务端向客户端返回数据。这个类的名称我定为ApiResult,一共3个成员,分别是code、message和data。code和message是前端统一要求的,而data成员则可以存放各个接口想要返回的任意数据。

package com.zy.report.vo;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ApiResult {
    private long code;
    private Object data;
    private String message;

    public ApiResult(long code){this.code = code; data = null;}
    public ApiResult(long code, Object data){this.code = code; this.data = data;}
}

这里用到了lombok的注解,能够为我们的类自动生成构造函数、getter和setter。

在实现login接口前先查阅一下前端代码关于该接口的参数定义,可以在 views/login/index.vue 中看到以下代码片段

  loginForm: {
    username: "admin",
    password: "111111"
  },
//此处省略
      this.$store
        .dispatch("user/login", this.loginForm)
        .then(() => {
          this.$router.push({ path: this.redirect || "/" });
          this.loading = false;
        })

在api/user.js中能查看到登录接口的URL定义

export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

于是,我们先定义一个请求对象,然后根据api的url为服务端添加Controller

package com.zy.report.req;

import lombok.Data;
import lombok.AllArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReqLogin {
    private String username;
    private String password;
}

注意哦,这里一定要加上@NoArgsConstructor注解

package com.zy.report.controller;

import com.zy.report.req.ReqLogin;
import com.zy.report.vo.ApiResult;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/user")
public class LoginController {
    @RequestMapping("/login")
    @ResponseBody
    public ApiResult login(HttpServletRequest request, @RequestBody ReqLogin params) {
        if(!(params.getUsername().equals("admin") && params.getPassword().equals("111111")))
            return new ApiResult(20001, null, "用户名或密码不正确");
        Map<String, Object> r = new HashMap<>();
        r.put("token", "test-token");
        return new ApiResult(20000, r);
    }
}

当然,controller里面的login接口其实什么也没做,只是校验了一下前端传来的用户名和口令是否为默认值,然后按照接口约定,返回一个测试用的token字段而已。如果前面步骤正确,api server就可以正常运行了。

初遇跨域错误

回到显示着登录界面的浏览器,使用默认用户名和密码,点击登录按钮,却出现了这个错误:
在这里插入图片描述
F12 查看控制台

Access to XMLHttpRequest at 'http://localhost:10200/user/login' from origin 'http://localhost:9527' has been blocked by 
CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header 
is present on the requested resource.

嗯,虽然api server和前端都是运行在本地,但我们的api server是运行在10200端口,而vue-clie运行的前端是在9527端口,这的确会产生跨域问题。在真实场景部署下,我们可以通过ngnix将api server和前端web服务代理到同一个端口上,但对于开发过程来说,每次调试都要重新部署确实有些麻烦,我们可以通过对api server进行配置来启用跨域。

我们在api server项目中添加一个新的配置类

package com.zy.report.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class CORSConfiguration extends WebMvcConfigurationSupport {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("*")
                .allowedOrigins("*")
                .allowedHeaders("*");
        super.addCorsMappings(registry);
    }
}

withCredentials引起的跨域错误

这样,api server上所有的接口就应该支持跨域了。重启api server,回到浏览器点击登录
在这里插入图片描述

Network Error依旧!!!怎么回事?还是查看控制台,发现这次的错误提示有些不一样:

Access to XMLHttpRequest at 'http://localhost:10200/user/login' from origin 'http://localhost:9527' has been blocked by CORS 
policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in 
the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests 
initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

提示里说的很明白,页面在发起XHR时,启用了withCredentials参数,那服务端在返回Access-Control-Allow-Origin就不能使用通配符*了。
withCredentials参数为true时,意味着浏览器发现XHR是跨域访问时,也要把原网站的Cookie数据一并发送出去,因为Cookie里经常会存放一些用户敏感数据,这样的跨域访问的确是会带来安全风险,所以在这种情况下XHR就直接被浏览器阻止了。

改服务端代码有点麻烦,而且我们的用户认证机制并没有使用cookie,查看前端代码request组件的实现时,发现withCredentials设置为true,那我们就关掉这个参数再试试?在 utils/request.js 中注释掉withCredentials这一行。

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
});

保存,等待vue-cli热刷新完毕,再次回到浏览器点击登录,Network Error依旧!
查看控制台,会发现这次错误又不一样了,值得庆祝的是跨域问题终于解决了,服务端也正确的返回了token
在这里插入图片描述

继续探索前端登录流程

我们也不难发现这次的错误是由于服务端一个未实现的info接口引起的。
在这里插入图片描述
看来前端代码在获取到token以后会继续通过info接口向服务端获取用户的详细数据。我们只需在服务端实现info接口就可以解决这个问题,但我还想继续探索前端项目的登录流程,于是硬着头皮接着分析前端登录部分的代码。

之前我们分析前端登录代码时忽略了一个细节,那就是前端login接口调用成功后,会将将当前路由设置为’/’,具体代码在 vies/login/index.vue中:

  this.$store.dispatch('user/login', this.loginForm)
    .then(() => {
      this.$router.push({ path: this.redirect || '/' })
      this.loading = false
    })
    .catch(() => {
      this.loading = false
    })

虽然对vue里router组件了解不多,但大致能够知道router组件控制着前端页面的切换,一是半会儿我没有想到router的切换是如何与info接口调用关联起来的,但通过搜索代码,不难发现在 src/permission.js 中有着这么一段代码实现:

import router from './router'
router.beforeEach(async(to, from, next) => {
  //省略
  if (hasToken) {
    if (to.path === '/login') {
      //省略
    } else {
      // determine whether the user has obtained his permission roles through getInfo
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // get user info
          // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
          const { roles } = await store.dispatch('user/getInfo')

这不正是router的拦截器吗,在用户切换router之前,如果store中还没有用户的权限信息,那就调用info接口获取。先简单看一下info接口之后,前端还做了什么。

  // generate accessible routes map based on roles
  const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

  // dynamically add accessible routes
  router.addRoutes(accessRoutes)

  // hack method to ensure that addRoutes is complete
  // set the replace: true, so the navigation will not leave a history record
  next({ ...to, replace: true })

嗯,可以看出info接口成功后,就根据返回的角色信息来设置路由了。我也不难理解为什么这个文件叫做permission.js——正是有了这个拦截器,就可以实现不同角色的用户可以访问不同的页面。

OK,摸清楚前端的登录流程后,我们继续思考如何解决之前遇到的问题。

  • 我们可以在api server上实现这个info接口
  • 我们可以将login接口和info接口合并到一起,将用户的基本信息在login时一并返回

简单起见,我还是选择在api server上实现info接口好了。

info接口的实现

有了前面实现login接口的经验,查找info接口的参数定义也不是什么困难事了,这里直接贴上info接口的代码,跟Login接口放在同一个Controller里就好:

@RequestMapping("/info")
@ResponseBody
public ApiResult info(HttpServletRequest request){
    Map<String, Object> r = new HashMap<>();
    r.put("username", "111111");
    r.put("name", "管理员");

    List<String> roles = new ArrayList<>();
    roles.add("admin");
    r.put("roles", roles);
    return new ApiResult(20000, r);
}

重启api server,回到浏览器点击登录

在这里插入图片描述
终于看到Dashboard页面了!点击菜单-页面权限,可以看到前端认为登陆的用户是管理员,这与我们info接口返回的数据一致。
在这里插入图片描述
至此,我们回顾一下整个前端登录的流程

  1. 用户点击登录按钮 浏览器发起login请求
  2. 服务端校验用户名和密码,返回token
  3. 前端将token记录到store,并切换路由
  4. 切换路由操作被拦截器拦截,并通过info请求获取用户权限信息,获取成功后根据用户权限设置路由,最终通过next路由到指定页面

最后,我们虽然弄清楚了登录的原理,并成功进入后台首页,但服务端对用户身份信息的识别还需要进一步实现,这部分就留到下次在写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值