Spring Boot之前后端分離(三):登錄档押、登出澳盐、頁面認(rèn)證

前言

來啦老鐵!

筆者學(xué)習(xí)Spring Boot有一段時間了令宿,附上Spring Boot系列學(xué)習(xí)文章叼耙,歡迎取閱、賜教:

  1. 5分鐘入手Spring Boot;
  2. Spring Boot數(shù)據(jù)庫交互之Spring Data JPA;
  3. Spring Boot數(shù)據(jù)庫交互之Mybatis;
  4. Spring Boot視圖技術(shù);
  5. Spring Boot之整合Swagger;
  6. Spring Boot之junit單元測試踩坑;
  7. 如何在Spring Boot中使用TestNG;
  8. Spring Boot之整合logback日志;
  9. Spring Boot之整合Spring Batch:批處理與任務(wù)調(diào)度;
  10. Spring Boot之整合Spring Security: 訪問認(rèn)證;
  11. Spring Boot之整合Spring Security: 授權(quán)管理;
  12. Spring Boot之多數(shù)據(jù)庫源:極簡方案;
  13. Spring Boot之使用MongoDB數(shù)據(jù)庫源;
  14. Spring Boot之多線程粒没、異步:@Async;
  15. Spring Boot之前后端分離(一):Vue前端;
  16. Spring Boot之前后端分離(二):后端筛婉、前后端集成

在前2篇文章,我們一起學(xué)習(xí)了Spring Boot之前后端分離的前端癞松、后端爽撒、前后端集成,不過前后端集成的時候沒有演示登錄成功后的頁面跳轉(zhuǎn)响蓉。

這兩天從登錄后頁面跳轉(zhuǎn)出發(fā)硕勿,準(zhǔn)備來個前后端交互升級版,卻發(fā)現(xiàn)登出API沒有工作正常厕妖,幾經(jīng)琢磨后終于解決首尼,決定再寫一篇文章,主要介紹:

1. 后端登出API的具體配置及實現(xiàn)言秸;
2. JSESSIONID失效設(shè)置软能;
3. 登錄后頁面跳轉(zhuǎn)、頁面訪問認(rèn)證举畸、登出實現(xiàn)查排;
4. 演示登錄后頁面跳轉(zhuǎn)、頁面訪問認(rèn)證抄沮、登出跋核;

項目代碼已更新至Git Hub倉庫,歡迎取閱:

1. 后端登出API的具體配置及實現(xiàn)叛买;

我們之前直接使用Spring Security的默認(rèn)登出API:/logout砂代,感覺應(yīng)該不會有問題,也沒試過率挣,可是當(dāng)我真正使用它的時候卻發(fā)現(xiàn)刻伊,翻車了?椒功?捶箱?

登出翻車?

檢查了一下后端config包內(nèi)的WebSecurityConfig.java類动漾,關(guān)于登出的配置如下:

...
.and()
.logout()
.permitAll()
...

后經(jīng)查資料和親自實踐丁屎,發(fā)現(xiàn)加上logoutSuccessHandler就成功解決了,具體步驟如下:

1). domain包內(nèi)創(chuàng)建SignOutResponse.java實體類旱眯,定義登出API的返回晨川;
package com.github.dylanz666.domain;

import com.alibaba.fastjson.JSONArray;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author : dylanz
 * @since : 10/10/2020
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class SignOutResponse implements Serializable {
    private static final long serialVersionUID = 1L;

    private int code;
    private String status;
    private String message;

    @Override
    public String toString() {
        return "{" +
                "\"code\":" + code + "," +
                "\"status\":\"" + status + "\"," +
                "\"message\":\"" + message + "\"" +
                "}";
    }
}
2). 修改config包內(nèi)WebSecurityConfig.java類的登出配置部分;
...
.logout()
.deleteCookies("JSESSIONID")
.logoutSuccessHandler((request, response, authentication) -> {
    response.setContentType("application/json");
    response.setStatus(200);

    SignOutResponse signOutResponse = new SignOutResponse();
    signOutResponse.setCode(200);
    signOutResponse.setStatus("success");
    signOutResponse.setMessage("success");

    PrintWriter out = response.getWriter();
    out.write(signOutResponse.toString());
    out.flush();
    out.close();
})
.permitAll()
...
這里主要做了2件事删豺,第一個础爬,刪除cookie中的JSESSIONID,第二個吼鳞,聲明logoutSuccessHandler看蚜;

在驗證登出API之前,我們先另外建一個需要訪問認(rèn)證的API赔桌,我稱之為認(rèn)證API供炎,步驟如下:
1). 在controller包內(nèi)創(chuàng)建AuthController.java類;
2). 在AuthController.java類中創(chuàng)建/api/auth API疾党,代碼如下:

package com.github.dylanz666.controller;

import com.alibaba.fastjson.JSONArray;
import com.github.dylanz666.domain.SignInResponse;
import com.github.dylanz666.domain.SignOutResponse;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;

/**
 * @author : dylanz
 * @since : 10/09/2020
 */
@RestController
public class AuthController {
    @GetMapping("/api/auth")
    public SignInResponse getAuth() {
        Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = userDetails.getUsername();

        SignInResponse signInResponse = new SignInResponse();
        signInResponse.setCode(200);
        signInResponse.setStatus("success");
        signInResponse.setMessage("success");
        signInResponse.setUsername(username);
        JSONArray userRoles = new JSONArray();
        for (GrantedAuthority authority : authorities) {
            String userRole = authority.getAuthority();
            if (!userRole.equals("")) {
                userRoles.add(userRole);
            }
        }
        signInResponse.setUserRoles(userRoles);

        return signInResponse;
    }
}

以上步驟完成后音诫,我們再次用postman驗證一下后端的登錄、登出過程:

未登錄訪問API
登錄
登錄后訪問API
登出
登出后訪問API

我們會發(fā)現(xiàn)登出后雪位,Cookie中的JSESSIONID已經(jīng)變了竭钝,這是由于后端把原有的JSESSIONID刪除了,并自動分配了另外的JSESSIONID,且是個沒有權(quán)限的JSESSIONID香罐。不僅如此卧波,如果我們用原有的JSESSIONID再次訪問API,也是沒有權(quán)限的庇茫!

因此港粱,我們完成了登出的配置,登出功能已實現(xiàn)5┣2槠骸!

2. JSESSIONID失效設(shè)置宁炫;

通常偿曙,處于安全考慮,我們希望如果一個用戶在一定時間內(nèi)對網(wǎng)站沒有任何操作羔巢,那么關(guān)閉與該用戶的會話遥昧,即將該用戶的JSESSIONID設(shè)置為失效,那么這個問題在Spring Security中該如何做到呢朵纷?

其實這塊Spring Security已經(jīng)幫我們做好了炭臭,默認(rèn)情況下,用戶60秒無操作袍辞,則該用戶的JSESSIONID將失效鞋仍,如果我們想更改該時間,也很簡單搅吁,只需要在項目的resources下的application.properties文件中加入如下配置:

server.servlet.session.timeout=600

這代表威创,用戶600秒無操作,則該用戶的JSESSIONID將失效(注意單位為秒)谎懦!
JSESSIONID失效后肚豺,用戶再次登錄后才能繼續(xù)訪問我們的應(yīng)用!

后端再一次準(zhǔn)備好了界拦,接下來我打算做一下登錄后的頁面跳轉(zhuǎn)和前端頁面的認(rèn)證吸申。

3. 登錄后頁面跳轉(zhuǎn)、頁面訪問認(rèn)證享甸、登出實現(xiàn)截碴;

1). 前端基于axios編寫logout API調(diào)用方法和認(rèn)證API的調(diào)用方法;
import request from '@/utils/request'

export function getAuth() {
    return request({
        url: '/api/auth',
        method: 'get',
        params: {}
    })
}

export function logout() {
    return request({
        url: '/logout',
        method: 'get',
        params: {}
    })
}
2). 修改前端config/index.js文件中的proxyTable蛉威,如下:
...
const backendBaseUrl = 'http://127.0.0.1:8080/';
...
proxyTable: {
  '/api': {
    target: backendBaseUrl,
    changeOrigin: true,
    pathRewrite: {
      '^/api': '/api'
    }
  },
  '/login': {
    target: backendBaseUrl,
    changeOrigin: true,
    pathRewrite: {
      '^/login': '/login'
    }
  },
  '/logout': {
    target: backendBaseUrl,
    changeOrigin: true,
    pathRewrite: {
      '^/logout': '/logout'
    }
  },
}
3). 前端src/views底下創(chuàng)建home文件夾日丹,home文件夾內(nèi)創(chuàng)建index.vue文件,代碼如下:

(我們暫時不考慮將某些功能組件化蚯嫌,此處只做演示)

<template>
  <el-container class="tac">
    <el-menu
      mode="vertical"
      unique-opened
      default-active="1"
      class="el-menu-vertical-demo"
      @open="handleOpen"
      @close="handleClose"
      background-color="#006699"
      text-color="#fff"
      active-text-color="#ffd04b"
      v-bind:style="menuStyle"
    >
      <el-submenu index="1">
        <template slot="title">
          <i class="el-icon-location"></i>
          <span>Navigator One</span>
        </template>
        <el-menu-item-group title="Group One">
          <el-menu-item index="1-1">item one</el-menu-item>
          <el-menu-item index="1-2">item one</el-menu-item>
        </el-menu-item-group>
        <el-menu-item-group title="Group Two">
          <el-menu-item index="1-3">item three</el-menu-item>
        </el-menu-item-group>
        <el-submenu index="1-4">
          <template slot="title">item four</template>
          <el-menu-item index="1-4-1">item one</el-menu-item>
        </el-submenu>
      </el-submenu>
      <el-menu-item index="2">
        <i class="el-icon-menu"></i>
        <span>Navigator Two</span>
      </el-menu-item>
      <el-menu-item index="3" disabled>
        <i class="el-icon-document"></i>
        <span>Navigator Three</span>
      </el-menu-item>
      <el-menu-item index="4">
        <i class="el-icon-setting"></i>
        <span>Navigator Four</span>
      </el-menu-item>
    </el-menu>

    <el-main>
      <el-row>
        <el-col :span="18" align="left">
          <el-page-header @back="goBack" title="HOME"></el-page-header>
        </el-col>

        <el-col :span="2" style="margin-top: -10px">
          <!-- 當(dāng)前角色 -->
          <el-button
            type="text"
            icon="el-icon-user-solid"
            disabled
            size="medium"
            >{{ username }}({{ currentUserRole }})</el-button
          >
        </el-col>
        <el-col :span="2" style="margin-top: -10px">
          <!-- 登出 -->
          <el-button
            type="text"
            icon="el-icon-caret-right"
            size="medium"
            @click="logout"
            >Sign Out</el-button
          >
        </el-col>

        <el-col :span="2" style="margin-top: -10px">
          <!-- 角色下拉框 -->
          <el-dropdown @command="changeRole">
            <span class="el-dropdown-link">
              <el-button type="text" size="medium" icon="el-icon-s-tools"
                >User Role</el-button
              >
              <i class="el-icon-arrow-down el-icon--right"></i>
            </span>
            <el-dropdown-menu slot="dropdown">
              <div v-for="(item, index) in userRoles" :key="item">
                <el-dropdown-item
                  icon="el-icon-user"
                  size="medium"
                  :command="userRoles[index]"
                  >{{ userRoles[index] }}</el-dropdown-item
                >
              </div>
            </el-dropdown-menu>
          </el-dropdown>
        </el-col>
      </el-row>
      <hr />

      <!-- demo form -->
      <div style="width: 45%">
        <el-form
          ref="form"
          :model="form"
          label-width="120px"
          label-position="left"
          align="left"
        >
          <el-form-item label="Activity name">
            <el-input v-model="form.name"></el-input>
          </el-form-item>
          <el-form-item label="Activity zone">
            <el-select
              v-model="form.region"
              placeholder="please select your zone"
            >
              <el-option label="Zone one" value="shanghai"></el-option>
              <el-option label="Zone two" value="beijing"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="Activity time" align="center">
            <el-col :span="11">
              <el-date-picker
                type="date"
                placeholder="Pick a date"
                v-model="form.date1"
                style="width: 100%"
              ></el-date-picker>
            </el-col>
            <el-col class="line" :span="2">-</el-col>
            <el-col :span="11">
              <el-time-picker
                placeholder="Pick a time"
                v-model="form.date2"
                style="width: 100%"
              ></el-time-picker>
            </el-col>
          </el-form-item>
          <el-form-item label="Instant delivery">
            <el-switch v-model="form.delivery"></el-switch>
          </el-form-item>
          <el-form-item label="Activity type">
            <el-checkbox-group v-model="form.type">
              <el-checkbox label="Online activities" name="type"></el-checkbox>
              <el-checkbox
                label="Promotion activities"
                name="type"
              ></el-checkbox>
              <el-checkbox label="Offline activities" name="type"></el-checkbox>
              <el-checkbox
                label="Simple brand exposure"
                name="type"
              ></el-checkbox>
            </el-checkbox-group>
          </el-form-item>
          <el-form-item label="Resources">
            <el-radio-group v-model="form.resource">
              <el-radio label="Sponsor"></el-radio>
              <el-radio label="Venue"></el-radio>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="Activity form">
            <el-input type="textarea" v-model="form.desc"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="onSubmit">Create</el-button>
            <el-button>Cancel</el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-main>
  </el-container>
</template>

<script>
import { getAuth, logout } from "@/api/auth";

export default {
  name: "home",
  data() {
    return {
      username: "",
      userRoles: [],
      currentUserRole: "",
      menuStyle: {
        height: null,
      },
      form: {
        name: "",
        region: "",
        date1: "",
        date2: "",
        delivery: false,
        type: [],
        resource: "",
        desc: "",
      },
    };
  },
  created() {
    this.menuStyle.height = document.documentElement.clientHeight + "px";
    getAuth().then((response) => {
      if (response.code == 200 && response.message == "success") {
        this.username = response.username;
        this.userRoles = response.userRoles;
        this.currentUserRole = this.userRoles[0]
          ? this.userRoles[0].substring(5, this.userRoles[0].length)
          : "";
      }
    });
  },
  methods: {
    logout() {
      logout().then((response) => {
        if (response.code == 200 && response.status == "success") {
          window.location.href = "/#/login.html";
        }
      });
    },
    changeRole(role) {
      this.currentUserRole = role
        ? role.substring(5, this.userRoles[0].length)
        : "";
    },
    goBack() {
      console.log("back to home");
    },
    onSubmit() {
      this.$notify({
        title: "submit",
        message: "success",
        type: "success",
        offset: 100,
      });
    },
    handleOpen(key, keyPath) {
      console.log(key, keyPath);
    },
    handleClose(key, keyPath) {
      console.log(key, keyPath);
    },
  },
};
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
  display: inline-block;
  font-size: 14px;
  line-height: 50px;
  margin-left: 10px;
  .no-redirect {
    color: #97a8be;
    cursor: text;
  }

  .right-menu {
    float: right;
    height: 100%;
    &:focus {
      outline: none;
    }
    .right-menu-item {
      display: inline-block;
      margin: 0 8px;
    }
    .screenfull {
      height: 20px;
    }
    .international {
      vertical-align: top;
    }
    .theme-switch {
      vertical-align: 15px;
    }
    .avatar-container {
      height: 50px;
      margin-right: 30px;
      .avatar-wrapper {
        cursor: pointer;
        margin-top: 5px;
        position: relative;
        .user-avatar {
          width: 40px;
          height: 40px;
          border-radius: 10px;
        }
        .el-icon-caret-bottom {
          position: absolute;
          right: -20px;
          top: 25px;
          font-size: 12px;
        }
      }
    }
  }
}
</style>

稍微解讀一下:

  • 關(guān)注點一:我們created()方法中調(diào)用后端的/api/auth方法哲虾,驗證用戶是否已登錄丙躏,如果已登錄,則展示用戶的角色信息和當(dāng)前角色信息束凑。如果未登錄晒旅,則src/utils/request.js中會將頁面重定向到登錄頁面,src/utils/request.js的改動見下文湘今;
  • 關(guān)注點二:我們在頁面上設(shè)置了一個Sign Out入口敢朱,點擊Sign Out則調(diào)用后端/logout API剪菱,并且在調(diào)用成功后重定向到登錄頁面摩瞎;
4). 修改src/utils/request.js文件,如下:
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'

// 創(chuàng)建axios實例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 15000 // 請求超時時間
})

// request攔截器
/*
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers['X-Token'] = getToken() // 讓每個請求攜帶自定義token 請根據(jù)實際情況自行修改
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})
*/

// respone攔截器
service.interceptors.response.use(
  response => {
    /**
     * code為非200是拋錯 可結(jié)合自己業(yè)務(wù)進行修改
     */
    const res = response.data;
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000
      })
    }
    return res;
  },
  error => {
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    });
    window.location.href = "/#/login.html";
    return Promise.reject(error)
  }
)

export default service

唯一變化是在error內(nèi)增加了window.location.href = "/#/login.html"; 這行代碼孝常,這是使得訪問后端API的時候旗们,如果遇到400,401构灸,500等錯誤上渴,直接跳轉(zhuǎn)到登錄頁面(可另外設(shè)置頁面,此處只做演示)喜颁,是全局的設(shè)置稠氮,避免每個頁面都寫一遍類似的代碼(非必需,可根據(jù)實際情況設(shè)置)半开。

5). 修改src/views/login/index.vue中methods的login方法隔披,如下:
login(formName) {
  const from = this.$route.query.from || "home.html";
  this.$refs[formName].validate((valid) => {
    if (!valid) {
      return false;
    }
    login(this.ruleForm.username, this.ruleForm.password).then(
      (response) => {
        this.showSuccessNotify(response);
        this.ruleForm.username = "";
        this.ruleForm.password = "";
        if (response.code == 200 && response.status == "success") {
          this.$router.push({ path: `/${from}` });
        }
      }
    );
  });
}

這是為了讓從其他頁面跳轉(zhuǎn)到登錄頁面,登錄成功后可直接跳轉(zhuǎn)回原有頁面寂拆,是個小技巧奢米,非必需。如果不是從其他頁面跳轉(zhuǎn)到登錄頁面的纠永,則登錄成功后默認(rèn)跳轉(zhuǎn)至127.0.0.1/#/home.html頁面鬓长。

6). 設(shè)置訪問前端路由外的報錯頁面,即404頁面尝江;
  • src/assets內(nèi)添加404頁面用到的圖片資源:


    404圖片資源
  • 新建src/views/404.vue文件并編寫404頁面涉波;
<template>
  <div style="background:#f0f2f5;margin-top: -20px;">
    <div class="wscn-http404">
      <div class="pic-404">
        <img class="pic-404__parent" :src="img_404" alt="404">
        <img class="pic-404__child left" :src="img_404_cloud" alt="404">
        <img class="pic-404__child mid" :src="img_404_cloud" alt="404">
        <img class="pic-404__child right" :src="img_404_cloud" alt="404">
      </div>
      <div class="bullshit">
        <div class="bullshit__oops">OOPS!</div>
        <div class="bullshit__info">版權(quán)所有<a class="link-type"  target='_blank'>華爾街見聞</a></div>
        <div class="bullshit__headline">{{ message }}</div>
        <div class="bullshit__info">請檢查您輸入的網(wǎng)址是否正確,請點擊以下按鈕返回主頁或者發(fā)送錯誤報告</div>
        <a href="/" class="bullshit__return-home">返回首頁</a>
      </div>
    </div>
  </div>
</template>

<script>
import img_404 from '@/assets/404_images/404.png'
import img_404_cloud from '@/assets/404_images/404_cloud.png'

export default {
  data() {
    return {
      img_404,
      img_404_cloud
    }
  },
  computed: {
    message() {
      return '特朗普說這個頁面你不能進......'
    }
  }
}
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.wscn-http404 {
  position: relative;
  width: 1200px;
  margin: 20px auto 60px;
  padding: 0 100px;
  overflow: hidden;
  .pic-404 {
    position: relative;
    float: left;
    width: 600px;
    padding: 150px 0;
    overflow: hidden;
    &__parent {
      width: 100%;
    }
    &__child {
      position: absolute;
      &.left {
        width: 80px;
        top: 17px;
        left: 220px;
        opacity: 0;
        animation-name: cloudLeft;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      &.mid {
        width: 46px;
        top: 10px;
        left: 420px;
        opacity: 0;
        animation-name: cloudMid;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1.2s;
      }
      &.right {
        width: 62px;
        top: 100px;
        left: 500px;
        opacity: 0;
        animation-name: cloudRight;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      @keyframes cloudLeft {
        0% {
          top: 17px;
          left: 220px;
          opacity: 0;
        }
        20% {
          top: 33px;
          left: 188px;
          opacity: 1;
        }
        80% {
          top: 81px;
          left: 92px;
          opacity: 1;
        }
        100% {
          top: 97px;
          left: 60px;
          opacity: 0;
        }
      }
      @keyframes cloudMid {
        0% {
          top: 10px;
          left: 420px;
          opacity: 0;
        }
        20% {
          top: 40px;
          left: 360px;
          opacity: 1;
        }
        70% {
          top: 130px;
          left: 180px;
          opacity: 1;
        }
        100% {
          top: 160px;
          left: 120px;
          opacity: 0;
        }
      }
      @keyframes cloudRight {
        0% {
          top: 100px;
          left: 500px;
          opacity: 0;
        }
        20% {
          top: 120px;
          left: 460px;
          opacity: 1;
        }
        80% {
          top: 180px;
          left: 340px;
          opacity: 1;
        }
        100% {
          top: 200px;
          left: 300px;
          opacity: 0;
        }
      }
    }
  }
  .bullshit {
    position: relative;
    float: left;
    width: 300px;
    padding: 150px 0;
    overflow: hidden;
    &__oops {
      font-size: 32px;
      font-weight: bold;
      line-height: 40px;
      color: #1482f0;
      opacity: 0;
      margin-bottom: 20px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-fill-mode: forwards;
    }
    &__headline {
      font-size: 20px;
      line-height: 24px;
      color: #1482f0;
      opacity: 0;
      margin-bottom: 10px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.1s;
      animation-fill-mode: forwards;
    }
    &__info {
      font-size: 13px;
      line-height: 21px;
      color: grey;
      opacity: 0;
      margin-bottom: 30px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.2s;
      animation-fill-mode: forwards;
    }
    &__return-home {
      display: block;
      float: left;
      width: 110px;
      height: 36px;
      background: #1482f0;
      border-radius: 100px;
      text-align: center;
      color: #ffffff;
      opacity: 0;
      font-size: 14px;
      line-height: 36px;
      cursor: pointer;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.3s;
      animation-fill-mode: forwards;
    }
    @keyframes slideUp {
      0% {
        transform: translateY(60px);
        opacity: 0;
      }
      100% {
        transform: translateY(0);
        opacity: 1;
      }
    }
  }
}

</style>

項目啟動后炭序,訪問未在前端設(shè)置路由的路徑怠蹂,如http://127.0.0.1:9528/#/test.htmlhttp://127.0.0.1:9528/#/test,則跳轉(zhuǎn)到http://127.0.0.1:9528/#/404少态,頁面長這樣:

404頁面

怎么樣城侧,404頁面還是挺好看的吧!

7). 將src/views/home/index.vue和src/views/404.vue加入前端路由配置src/router/index.js中:

import Vue from 'vue'
import Router from 'vue-router'
import login from '@/views/login/index'
import home from '@/views/home/index'
import notFoundPage from '@/views/404'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'index',
      component: login
    },
    {
      path: '/login.html',
      name: 'login',
      component: login
    },
    {
      path: '/home.html',
      name: 'home',
      component: home
    },
    {
      path: '/404',
      component: notFoundPage,
      hidden: true
    },
    {
      path: '*',
      redirect: '/404',
      hidden: true
    }
  ]
})

4. 演示登錄后頁面跳轉(zhuǎn)彼妻、頁面訪問認(rèn)證嫌佑、登出豆茫;

1). 啟動前端:
npm start
啟動前端
2). 啟動后端:
啟動后端
啟動后端
3). 登錄前訪問home頁面:http://127.0.0.1:9528/#/home.html
登錄前訪問home頁面

我們會發(fā)現(xiàn)頁面自動重定向到登錄頁面屋摇,并且顯示了一個401方面的錯誤信息揩魂,這是我們事先在src/utils/request.js內(nèi)寫好的喲,該行為比較符合實際使用需求炮温!

4). 前端登錄及登錄跳轉(zhuǎn):
前端登錄-錯誤信息
前端登錄-正確信息
登錄跳轉(zhuǎn)
我們會發(fā)現(xiàn):
  • 只有正確的用戶登錄信息才能登錄火脉,錯誤的用戶登錄信息是無法登錄的,這也是符合符合實際情況的!
  • 登錄后成功跳轉(zhuǎn)至home頁面柒啤,home頁面進行了訪問認(rèn)證倦挂,并且認(rèn)證通過,頁面正常展示担巩,符合實際情況方援!
5). 登出操作、登出后訪問home頁面:http://127.0.0.1:9528/#/home.html涛癌;
  • 點擊頁面右上角的Sign Out按鈕進行登出犯戏;
登出
  • 登出時調(diào)用后端logout API;
登出時調(diào)用后端logout API
  • 登出后訪問home頁面拳话;
登出后訪問home頁面

我們會發(fā)現(xiàn)先匪,登出后home頁面已無權(quán)限訪問!

從JSESSIONID的角度來看:

  • 登錄后站點Cookies中JSESSIONID的情況:
登錄后JSESSIONID
  • 登出后站點Cookies中JSESSIONID的情況:
登出后JSESSIONID

可見弃衍,登出后呀非,站點Cookies中JSESSIONID的確被刪除掉了,這是后端WebSecurityConfig.java類中寫好的笨鸡,符合我們的預(yù)期姜钳!

至此,我們實現(xiàn)了登錄成功后的跳轉(zhuǎn)形耗,登出功能哥桥、頁面認(rèn)證,并完整地做了演示激涤。由于同時涉及前后端拟糕,整個過程稍微有點長而復(fù)雜,完整看完的話需要點耐心倦踢,我鼓勵大家動手跟著實現(xiàn)一遍送滞!

而我也將不斷學(xué)習(xí),不斷更新Spring Boot相關(guān)知識辱挥,以及Spring Boot前后端分離相關(guān)內(nèi)容犁嗅。道阻且長,但不忘初心晤碘!

如果本文對您有幫助褂微,麻煩點贊功蜓、關(guān)注!

謝謝宠蚂!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末式撼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子求厕,更是在濱河造成了極大的恐慌著隆,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呀癣,死亡現(xiàn)場離奇詭異美浦,居然都是意外死亡,警方通過查閱死者的電腦和手機十艾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門抵代,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腾节,“玉大人忘嫉,你說我怎么就攤上這事“赶伲” “怎么了庆冕?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長劈榨。 經(jīng)常有香客問我访递,道長,這世上最難降的妖魔是什么同辣? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任拷姿,我火速辦了婚禮,結(jié)果婚禮上旱函,老公的妹妹穿的比我還像新娘响巢。我一直安慰自己,他們只是感情好棒妨,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布踪古。 她就那樣靜靜地躺著,像睡著了一般券腔。 火紅的嫁衣襯著肌膚如雪伏穆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天纷纫,我揣著相機與錄音枕扫,去河邊找鬼。 笑死辱魁,一個胖子當(dāng)著我的面吹牛烟瞧,可吹牛的內(nèi)容都是我干的偷厦。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼燕刻,長吁一口氣:“原來是場噩夢啊……” “哼只泼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起卵洗,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤请唱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后过蹂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體十绑,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年酷勺,在試婚紗的時候發(fā)現(xiàn)自己被綠了本橙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡脆诉,死狀恐怖甚亭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情击胜,我是刑警寧澤亏狰,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站偶摔,受9級特大地震影響暇唾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辰斋,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一策州、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宫仗,春花似錦够挂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汁胆,卻和暖如春梭姓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫩码。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工誉尖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人铸题。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓铡恕,卻偏偏與公主長得像琢感,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子探熔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350