前后端分离部署+登陆权限认证
初次使用vue,使用过程趟了不少坑。创建项目,安装vue等都自行百度吧。
文章目录
前言
实现功能:
1、用户登录及token验证;
2、在未登录时跳转到登录页面 ;
3、通过nginx实现前后端分离部署。
一、SpringBoot
1.配置文件
我这里使用的是properties,平平无奇,注意mapper/ *.xml 中间有个空格,别忘了删了。我这里路径有/luck,请求时注意在端口号后面加上/luck。例:http://localhost:8080/luck/api/login。
server.port=8887
server.servlet.context-path=/luck
server.tomcat.connection-timeout=60000
server.tomcat.accept-count=1000
server.tomcat.max-connections=10000
server.tomcat.max-threads=800
server.tomcat.min-spare-threads=100
server.connection-timeout=60000
# mapper 扫描路径
mybatis.mapper-locations=classpath:mapper/ *.xml
# 本地数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/lucky?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>luck</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>luck Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
</parent>
<dependencies>
<!-- <dependency>-->
<!-- <groupId>junit</groupId>-->
<!-- <artifactId>junit</artifactId>-->
<!-- <version>4.11</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<!--thymeleaf模板引擎配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>JDK15</classifier>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
<!-- tomcat-jdbc -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!-- token-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.luck.Application</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>
</project>
2.Controller - Service - Mapper
咱们做的用户登录权限认证,直接写个LoginController。
UserBean实体类有个username,password行了
Service Mapper 自己写吧。
GResponse 给大家贴上代码了
package com.luck.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.luck.entity.UserBean;
import com.luck.respnse.GResponse;
import com.luck.service.LoginService;
import com.luck.util.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
/******************************************
* @Descripttion :
* @Author :jlz
* @Date :Created in 2020/12/14 14:00
* @Version 1.0
******************************************/
@RestController
@RequestMapping("/api")
public class LoginController {
@Autowired
LoginService loginService;
@RequestMapping(value = "/login")
public GResponse login(@RequestBody UserBean user, HttpServletRequest request) throws JsonProcessingException {
System.out.println("username : "+user.getUsername());
System.out.println("password : "+user.getPassword());
UserBean u1 = loginService.login(user);
if (u1 == null) {
return GResponse.failure("用户名或密码错误!");
} else {
String token= TokenUtil.sign(user);
HashMap<String,Object> hs=new HashMap<>();
hs.put("token","token"+user.getUsername()+user.getPassword());
return GResponse.success(u1,token);
}
}
}
GResponse
package com.luck.respnse;
public class GResponse {
private boolean status; //状态
private String message; //信息
private int code; //编码
private Object data; //数据
private String token;
public boolean getStatus() {
return status;
}
public GResponse setStatus(boolean status) {
this.status = status;
return this;
}
public String getMessage() {
return message;
}
public GResponse setMessage(String message) {
this.message = message;
return this;
}
public String getToken() {
return token;
}
public GResponse setToken(String token) {
this.token = token;
return this;
}
public int getCode() {
return code;
}
public GResponse setCode(int code) {
this.code = code;
return this;
}
public Object getData() {
return data;
}
public GResponse setData(Object data) {
this.data = data;
return this;
}
public static GResponse success() {
;
return new GResponse().setStatus(true).setMessage("").setCode(60000).setData(null);
}
public static GResponse success(Object data) {
return new GResponse().setStatus(true).setMessage("").setCode(60000).setData(data);
}
public static GResponse success(Object data, Object tag0) {
return new GResponse().setStatus(true).setMessage("").setCode(60000).setData(data);
}
public static GResponse success(Object data,String token) {
return new GResponse().setStatus(true).setMessage("").setCode(60000).setData(data).setToken(token);
}
public static GResponse failure(String message, int code, Object data) {
return new GResponse().setStatus(false).setMessage(message).setCode(code).setData(data);
}
public static GResponse failure(String message) {
return new GResponse().setStatus(false).setMessage(message).setCode(1).setData(null);
}
public static GResponse failure(int code) {
return new GResponse().setStatus(false).setMessage("failure").setCode(code).setData(null);
}
public static GResponse failure(int code, String message) {
return new GResponse().setStatus(false).setMessage(message).setCode(code).setData(null);
}
}
3.拦截器 + token签名
WebConfiguration配置类,包含拦截,跨域处理,白名单
package com.luck.config;
import com.luck.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.web.servlet.config.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
private TokenInterceptor tokenInterceptor;
//构造方法
public WebConfiguration(TokenInterceptor tokenInterceptor){
this.tokenInterceptor = tokenInterceptor;
}
//跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*");
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer){
configurer.setTaskExecutor(new ConcurrentTaskExecutor(Executors.newFixedThreadPool(3)));
configurer.setDefaultTimeout(30000);
}
@Override
public void addInterceptors(InterceptorRegistry registry){
List<String> excludePath = new ArrayList<>();
//排除拦截
excludePath.add("/api/login"); //登录
excludePath.add("/api/register");//注册
excludePath.add("/static/**"); //静态资源
excludePath.add("/assets/**"); //静态资源
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
}
}
TokenInterceptor
package com.luck.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.luck.util.TokenUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
//有时一次请求,到拦截器会变成两次,第一次请求方式OPTIONS,直接返回200
if(request.getMethod().equals("OPTIONS")){
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
response.setCharacterEncoding("utf-8");
//获取请求头中的token
String token = request.getHeader("Authorization");
if(token != null){
boolean result = TokenUtil.verify(token);
if(result){
System.out.println("通过拦截器");
return true;
}
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try{
//token认证失败或token不存在
JSONObject json = new JSONObject();
json.put("msg","token verify fail");
json.put("code","50000");
response.getWriter().append(json.toJSONString());
System.out.println("认证失败,未通过拦截器");
}catch (Exception e){
e.printStackTrace();
response.sendError(500);
return false;
}
return false;
}
}
最后是TokenUtil ,加签验签
package com.luck.util;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.luck.entity.UserBean;
import com.auth0.jwt.JWT;
import java.util.Date;
public class TokenUtil {
private static final long EXPIRE_TIME= 10*60*60*1000;
private static final String TOKEN_SECRET="luck"; //密钥
/**
* 签名生成
* @param user
* @return
*/
public static String sign(UserBean user){
String token = null;
try {
Date expiresAt = new Date(System.currentTimeMillis() + EXPIRE_TIME);
token = JWT.create()
.withIssuer("auth0")
.withClaim("username", user.getUsername())
.withExpiresAt(expiresAt)
// 使用了HMAC256加密算法。
.sign(Algorithm.HMAC256(TOKEN_SECRET));
} catch (Exception e){
e.printStackTrace();
}
return token;
}
/**
* 签名验证
* @param token
* @return
*/
public static boolean verify(String token){
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
DecodedJWT jwt = verifier.verify(token);
System.out.println("认证通过:");
System.out.println("username: " + jwt.getClaim("username").asString());
System.out.println("过期时间: " + jwt.getExpiresAt());
return true;
} catch (Exception e){
return false;
}
}
}
到此springboot完事。
二、Vue
因为头一次用这玩意儿,小小的跳页困住我一天(因为vue版本),后来请教了一位小姐姐帮忙解决。反正vue我还是不会1.配置文件
Vue CLI 4.5
先给大家看看版本,自行百度。
package.json
{
"name": "zlucky",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"element-ui": "^2.4.5",
"less-loader": "^10.0.1",
"node-sass": "^4.14.1",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "^4.5.13",
"@vue/cli-plugin-vuex": "^4.5.13",
"@vue/cli-service": "~4.5.0",
"axios": "^0.18.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"sass-loader": "^8.0.2",
"vue-cli-plugin-axios": "0.0.4",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
2.权限登录配置
与package.json同级创建vue.config.js
module.exports = {
//打包输出
outputDir:'dist',
publicPath:"/",
devServer:{
//端口
port: 80,
//所有通过'api'调用的请求都会转发到'http://localhost:8887/'下面
proxy:{
'/api':{
target :'http://localhost:8887/luck/api/',
changeOrigin: true,
//替换api,替换了个寂寞
pathRewrite: {
"^/api": ""
}
}
}
}
}
接下来main.js与App.vue
axios router store element 这四个去百度找命令下。或者到项目目录cmd->vue ui 里面下
import Vue from 'vue'
import axios from 'axios'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
Vue.prototype.$axios = axios
Vue.config.productionTip = false
// 添加请求拦截器,在请求头中加token
axios.interceptors.request.use(
config => {
if (localStorage.getItem('Authorization')) {
config.headers.Authorization = localStorage.getItem('Authorization');
}
return config;
},
error => {
return Promise.reject(error);
});
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
app.vue 里面router-view标签别忘了加,跳页就指他呢
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app',
components: {
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
下面是router和store
router/index.js里面的 mode: ‘history’ 去掉请求路径的#
beforeEach是判断请求的啥路径,/直接到登录页,其他的判断有没有token,没有的话去登录页
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/login/Login.vue'
import Home from '../views/home/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'login',
component: Login
},
{
path: '/home',
name: 'home',
component: Home
}
]
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-debugger
if (to.path === '/') {
next();
} else {
let token = localStorage.getItem('Authorization');
if (token === null || token === '') {
next('/');
} else {
next();
}
}
})
export default router
store将token存放Authorization中
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 存储token
Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : ''
},
mutations: {
// 修改token,并将token存入localStorage
changeLogin (state, user) {
state.Authorization = user.Authorization;
localStorage.setItem('Authorization', user.Authorization);
}
},
actions: {
},
modules: {
}
})
最后登录页 axios
<template>
<el-form :rules="rules" :model="loginForm" ref="loginFormRef" class="login-container" label-position="left"
label-width="0px" v-loading="loading">
<h3 class="login_title">系统登录</h3>
<el-form-item prop="name">
<el-input type="text" v-model="loginForm.name" auto-complete="off" placeholder="账号"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" auto-complete="off" placeholder="密码"></el-input>
</el-form-item>
<el-checkbox class="login_remember" v-model="checked" label-position="left">记住密码</el-checkbox>
<el-form-item style="width: 100%">
<el-button type="primary" @click.native.prevent="submitClick" style="width: 100%">登录</el-button>
</el-form-item>
</el-form>
</template>
<script>
import {mapMutations} from "vuex";
export default{
data(){
return {
rules: {
name: [{required: true, message: '请输入用户名', trigger: 'blur'}],
password: [{required: true, message: '请输入密码', trigger: 'blur'}]
},
checked: true,
loginForm: {
name: '',
password: ''
},
loading: false
}
},
methods: {
...mapMutations(['changeLogin']),
submitClick() {
this.$refs.loginFormRef.validate((valid) => {
if(valid){
var _this = this;
this.loading = true;
this.$axios({
url:'/api/login',
method:'post',
data:{
username:this.loginForm.name,
password:this.loginForm.password
}
}).then(resp=> {
_this.loading = false;
var data = resp.data;
if (resp.status == 200) { // eslint-disable-next-line no-debugger
//成功
var code = data.code;
if (code == '60000') {
_this.userToken = data.token;
// 将用户token保存到vuex中
_this.changeLogin({ Authorization: _this.userToken });
_this.$message("登陆成功");
//跳页
this.$router.push({path:'home'})
} else {
_this.$message(data.message);
}
} else {
//失败
_this.$message(data.message);
}
});
}else{
_this.$message('密码或用户名错误');
}
})
}
}
}
</script>
<style>
.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: auto;
width: 350px;
padding: 35px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login_title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.login_remember {
margin: 0px 0px 35px 0px;
text-align: left;
}
</style>
下面是Home.vue
这里的HelloWorld用的项目生成时自带的HelloWorld,这页有“退出登录”按钮直接清除token,用来验证是否可以未登录跳到登录页。
<template>
<div class="home">
<!-- <img alt="Vue logo" src="../../assets/logo.png"> -->
<HelloWorld msg="Welcome to Your Vue.js App"/>
<!-- <el-container>
<el-button @click="exit">退出登录</el-button>
<el-button @click="test">携带token的测试请求</el-button>
</el-container> -->
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
components: {
HelloWorld
},
methods:{
exit(){
//退出登录,清空token
localStorage.removeItem('Authorization');
// this.$router.push('/');
},
test(){
this.$axios({
url:'/api/loginTest',
method:'get',
}).then(function(res){
console.log("res",res);
}).catch(function(err){
console.log("err",err);
})
}
}
}
</script>
到目前为止,SpringBoot Vue 已经写完了。
三、Nginx部署项目
1.项目打包
Springboot直接使用Mavan中的 package打成jar包,配置都在上面pom.xml中
Vue 使用vscode 终端-npm:build,打成dist静态资源文件 。(npm run build 看个人喜好)2.nginx配置信息
上面我们说到,vue所有包含/api的请求都转换为http://localhost:8887/*/api,到nginx配置中,我又觉得转了个寂寞
nginxi配置路径+文件名:nginx/conf/nginx.conf
# worker_processes 值越大,可以支持的并发处理量也越多
# 工作进程:数目。根据硬件调整,通常等于CPU数量或者2倍于CPU
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
# events 块涉及的指令主要影响 Nginx 服务器与用户的网络连接
# 表示每个 work process 支持的最大连接数为 1024
events {
worker_connections 1024;
}
http {
# http全局块
# http全局块配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# 负载均衡地址
upstream backend {
server localhost:8887;
}
server {
# 监听端口80,root 就是vue打好dist包的路径
listen 80;
server_name localhost;
client_max_body_size 10m;
root D:vueProject/zlucky/dist;
# 在dist中找index.html
location / {
try_files $uri /index.html; # vue history模式必须的配置
index index.html;
}
# 禁止缓存index.html页面
location /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# 缓存静态文件
location ^~ /static/ {
add_header Cache-Control "public,max-age=31536000";
}
# 与后端api的请求匹配api,所以我说vue转了个寂寞,这又匹配了,而且和vue配置的毫无相关
location ^~ /api{
proxy_pass http://localhost:8887/luck/api;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300;
proxy_send_timeout 300;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
我把打好的jar、dist以及 nginx 放在Windows server 服务器上。不用Linux,因为不会太多命令,也费劲。
nginx直接运行nginx.exe文件,再启动Spring boot,大功告成
java -jar 项目打成的jar包名.jar
四、页面展示