一、处理登录
实现只有登录之后才能访问到里面的页面要用到sessions,因为每一个页面前都要进行判断是否登录。
go get github.com/gin-contrib/sessions
rootRouter.GET("/login", func(c *gin.Context) {
// session ---- 是一种所有请求之间的数据共享机制,为什么会出现session,是因为http请求是一种无状态。
// 什么叫无状态:就是指,用户在浏览器输入方位地址的时候,地址请求到服务区,到响应服务,并不会存储任何数据在客户端或者服务端,
// 也是就:一次request---response就意味着内存消亡,也就以为整个过程请求和响应过程结束。
// 但是往往在开发中,我们可能要存存储一些信息,让各个请求之间进行共享。所有就出现了session会话机制
// session会话机制其实是一种服务端存储技术,底层原理是一个map
// 比如:我登录的时候,要把用户信息存储session中,然后给 map[key]any =
// key = sdf365454klsdflsd --sessionid
// 初始化session对象
session := sessions.Default(c)
// 存放用户信息到session
session.Set("user", "feige") //map[sessionid] == map[user][feige]
// 记住一定调用save方法,否则内存不会写入进去
session.Save()
c.JSON(http.StatusOK, "我是gin")
})
相当于,登录页面设置成功登录之后,会拿到一个session,然后我们在其他页面设置有session的才给通过。但是这个session是存在服务端的会消耗我们的资源。
在每个页面加,例如
很明显,如果路由有很多,那么代码会很臃肿 。
所以要用到中间件。JWT。
把session放到上下文中,c.set。
将会话的上下文过渡到c的上下文里面来。
然后在路由组里面配
用.use
下一步,把路由封装,不要把写在main中 。
二、
结构体的方法在别的包使用前一定要先new
user:=User{},这是普通,生成的是一个值,如果要将他变为指针,则fmt.println(&user).
user4:=new(User),这种就是生成一个指针。
这种方法可以接受普通。
这种就只接受指针
指针类型经典
c.shouldbindjson。
三、配置化
用viper。
数据配置的设定 tab对齐,shift+tab 回退对齐。
监听配置文件
将从配置文件拿到的东西,设置为全局变量。
用这个
用结构体,
map里面的键都是小写。
我比较喜欢这种,如果数量比较少的话。将配置文件映射到对应的结构体中。
app:
host: "localhost"
port: 8080
debug: true
package config
type AppConfig struct {
Host string
Port int
Debug bool
}
package main
import (
"fmt"
"github.com/spf13/viper"
)
// AppConfig 是用来存放配置文件数据的结构体
type AppConfig struct {
Host string
Port int
Debug bool
}
func main() {
// 创建一个新的 viper 实例
config := viper.New()
// 设置配置文件的类型,例如 yaml, toml, json 等
config.SetConfigType("yaml")
// 添加配置文件的搜索路径,可以添加多个路径
config.AddConfigPath(".")
// 设置配置文件的名称,不包含扩展名
config.SetConfigName("config")
// 读取配置文件
if err := config.ReadInConfig(); err != nil {
panic(fmt.Errorf("Fatal error config file: %w", err))
}
// 将配置文件中的数据映射到结构体变量
var appConfig AppConfig
if err := config.Unmarshal(&appConfig); err != nil {
panic(fmt.Errorf("unable to decode into struct, %w", err))
}
// 打印配置信息,查看是否成功读取
fmt.Println("Application config: ", appConfig)
// 接下来可以使用 appConfig 中的配置信息...
}
四、继承和整合gorm,实现登录
覆盖生成表
比如微信支付v2版本很完善了,但现在要升级到v3该怎么做。
不能改别人代码,先把接口用结构体规范起来,
比如旧的
新的代码
然后调用也是
五、实现登录接口
定义路由
注册路由
获取参数
与数据库交互
创建一个用户表
nil 是go空值处理,必须是指针类型 。
加了unique,会自动创建索引.
状态码不要乱写,特别是需要重定向的时候,只有303才可以跳。
状态码,成功的只有一个,失败的有n个,具体原因具体分析。
有一种:只要能请求,都设置成http.statusOK,但是在业务层做返回。
六、验证码
session服务存储的方案:
- 在go的web服务生成一个图形验证码。比如生成:98477
- 然后把98477存储到服务端(session)
- 然后生成图形验证码,*使用img标签,进行加载,可以看到图像验证码
- 然后用户输入图形验证码信息,然后发起登录请求
- 然后在登录请求中,根据用户输入图像验证码和服务端存储的验证码进行比较
- 如果相同代表就是合法,如果不同说明输入验证码有误,就提示用户。
- 在go的base64Captcha组件的原理
- 会初始化一个组件对象,然后生成一个base64的图片地址,和一个id
- 这个id会返回给客户端。
- 然后用户根据base64的图片地址使用img标签进行渲染,根据显示图片信息
- 然后输入验证码,然后发起请求登录,同时携带验证码的id
- 根据id和用户输入验证码,进行校验,如果校验true.代表就合法验证码。就直接去登录
- 否则提示验证码输入有误。
02、Go整合验证码
- 官网:https://github.com/mojocn/base64Captcha
- 下载模块:
```
go get github.com/mojocn/base64Captcha
```
- 验证码模块
前端的验证码就是一个 img标签和go生成的验证码图片地址。
验证码校验,后端
前端路由
前端页面。配置路由,如何对验证码,异步请求验证码 ,调用验证码的接口和登录接口。安装axios。
浏览器是8777端口,访问服务器8989端口,显示跨域问题 。要在服务端设置gin里设置跨域问题
校验验证码,如果输入错的验证码 返回一个状态码,根据这个状态码来判断 是否刷新验证码。
七、jwt
单体架构
说白了,就是把所有的数据的管理和处理都交由服务端来完成。
把路由的处理和资源访问都由服务端来定义和规范,用户只需要按照服务端定义的路由,可以找到服务端的资源(数据库数据,文件)。
接下来就就会出现一个问题,请求和请求之间的数据共享的问题。
发起一个请求和响应一个请求,整个过程是无状态(一个请求:go语言会开辟一个线程(协程)去执行请求)。如果request到response结束了。线程肯定要么回收,要么就是被销毁。在整个请求线程数据是无状态就不会任何的保留。比如登录:
用户输入账号和密码,点击登录按钮,请求服务端,把用户查询数据查询出来,显示到页面。那么这里查询出来的用户数据,是不可能维持状态,也就说这个用户信息只能在当前登录请求才会看的到。别人请求是获取不到该用户信息的。那么就出现一个问题。请求和请求的数据共享。你怎么实现?
如果不解决会出现什么情况
- 小维—-thread01———-登录—request–response—用户信息是可以拿到—-也可以在浏览器显示出来。—–进入首页(显示当前用户信息)
- 小维—首页—-课程商品—购买———-thread02—–执行购买的服务方法——-这个时候我们必须要知道是谁在操作购买需要?–那么你必须要获取登录的用户信息。
如果不想办法解决这个,你肯定在登录后续的所有的请求中你肯定是拿不到用户信息。那么你必须:
- 每次请求都执行一次登录。那这样的话肯定是不显示
- 可以把用户信息返回浏览器,使用浏览器的存储技术(cookie)然后把登录用户信息放入cookie。然后在每次请求的时候把userId携带上。但是这个出现安全问题。 http://www.xxxx.com/course/buy/100?userId=100
加解密不行。速度慢,而且被人破解就麻烦了;用置换技术
这种安全问题不能再客户端(浏览器)存,安全问题太大了。只能存在服务端。
session其实就是一种服务端存储技术,主要就是用来解决,请求和请求之间数据共享的一种机制。所以上面问题很明显。userId=100 存在在浏览器(B客户端)肯定不现实。客户端自然不行。那就服务端来完成这个事情。而且服务端是把数据写到操作系统的内容中的。除非你能把服务器攻破。并且对go操作内存你才能攻破,所以存在在服务端是非常安全的。那么如何完成的呢?
session的底层原理就是: map[string]map[string]any{}
userId=100&id=1&name=zhangsan——map/struct—user{userid:100,id:1,name:zhangsan}—-
session机制:它必须可以存储一切, 所以map的值必须interface{} (any)
因为是map的map,不是单map。
那信息已经存进去了,如何在付款的时候能找到对应的人呢?如何在map的map找到对应的人,那就是设置一个sessionId进行查找,登录的时候把sessionID返回给浏览器
这样才知道拿到谁的数据。每次请求过程中都要带上cookie。
要将存在登录信息在1服务里面的数据共享给服务2。
做一个redis服务器。
这些都要花钱。
session为什么过时?
不适合前后端分离架构,session技术毕竟是一个服务端的存在技术,消耗go的内存,用户量小没有毛病;用户暴增,如果把用户信息放入服务端来存储肯定会出现内存问题。
session
如何解决——————使用token技术
token时限
什么是jwt
go整合jwt
在Golang语言中,**[jwt-go](https://link.zhihu.com/?target=https%3A//github.com/dgrijalva/jwt-go)**库提供了一些jwt编码和验证的工具,因此我们很容易使用该库来实现token认证。
另外,我们也知道**[gin](https://link.zhihu.com/?target=https%3A//github.com/gin-gonic/gin)**框架中支持用户自定义middleware,我们可以很好的将jwt相关的逻辑封装在middleware中,然后对具体的接口进行认证。
到期了怎么办?写一个续期。
这里的时间.add要time.second*10,十秒。类似。为什么生效时间设置时要减秒数,因为要他快速生效。
业务中使用token
set了之后,可以让后续的路由可以直接通过c.get
只要时接口,全部使用jwt拦截
从头部传递token。
测试token的有效性
为用户创建一个token,用用户的id和名字作为载体。一定要写过期时间。
多久过期才好。
如何将token续期?
原理:只要在有效期内的一个时间点,进行续期接口,重新生成一个新的token,然后替换,旧的token要拉入黑名单。设置过期时间,换的时候要加锁
方案1:在指定的某个时间点
方案2:利用缓存时间86400秒
如果过期时间-当前时间<缓冲时间,则重新创造一个token替换旧的。利用redis,有个自动过期。
在中间件里面判断token要不要续,设置ep。
如果生成了新的token,那么旧的token怎么办。
数据库写一个token的黑名单,存进去。在jwt中间件,查一遍看看在不在,但是最好的是用redis。因为mysql要做一个定时清除表。
八、跳转到别的页面
vue2
vue3
router和route
登录
token放到一个公共的地方,就是状态管理,pinia。
如果b的id是100 ,d的id是100,现在改了头像或者其他信息,b的id变成了10,d还是100,这样就不行了。
pinia
在前端的路由跳转过程中,路由和路由直接如果要实现数据共享,除了通过参数的层层传递以外。也可以使用状态共享一种机制来完成。这个就所谓:路由与路由数据共享的机制,这个避免了传递问题,响应式的问题。
因为路由通过传递参数确实可以解决一些问题,但是参数 不具备响应式的。如果有数据更改要立即生效。
在早期vue生态其实:vuex
第一步安装:
```js
yarn add pinia
# 或者使用 npm
npm install pinia
```
第二步:开始定义stores*目录新建一个index.js*
```js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
// vue3的版本
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
```
第三步:注册到vue
```js
import './assets/base.css'
import { createApp } from 'vue'
import App from './App.vue'
const pinia = createPinia();
app.use(pinia)
app.mount('#app')
```
第四步:新建一个user.js的模块
```js
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useUserStore = defineStore('userStore', {
// 其他配置...
})
```
第五步:使用
```js
import {useUserStore} from '@/stores/user.js'
const userStore = useUserStore();
```
接下来userStore对象,就可以调用方法所以得:state/getters/actions
```js
import { defineStore } from 'pinia'
import axios from 'axios'
//https://blog.csdn.net/weixin_62897746/article/details/129124364
//https://prazdevs.github.io/pinia-plugin-persistedstate/guide/
export const userStore = defineStore('user', {
// 定义状态
state: () => ({
user: {},
username: '',
userId: '',
token: ''
}),
// 定义动作
actions: {
toLogin(loginUser){
this.user = {id:100}
this.userId = 100
this.username = "feige"
this.token = 324234234
}
}
})
```
## Pinia状态管理
选项式的定义
```js
import { defineStore } from 'pinia'
import axios from 'axios'
//https://blog.csdn.net/weixin_62897746/article/details/129124364
//https://prazdevs.github.io/pinia-plugin-persistedstate/guide/
export const useUserStore = defineStore('user', {
// 定义状态
state: () => ({
user: {},
username: '',
userId: '',
token: '',
male:1,
roles:[],
permissions:[]
}),
// 就是一种计算属性的机制,定义的是函数,使用的是属性就相当于computed
getters:{
malestr(state){
if(state.male==1)return "男"
if(state.male==0)return "女"
if(state.male==1)return "保密"
},
roleName(state){
return state.roles.map(r=>r.name).join(",")
},
permissionCode(state){
return state.permissions.map(r=>r.code).join(",")
}
},
// 定义动作
actions: {
async toLogin(loginUser){
const resp = await axios.post("http://localhost:8989/login/toLogin", loginUser)
if (resp.data.code === 20000) {
// 这个会回退,回退登录页
var { user ,token,roles,permissions } = resp.data.data
// 把数据放入到状态管理中
this.user = user
this.userId = user.id
this.username = user.name
this.token = token
this.roles = roles
this.permissions = permissions
return Promise.resolve(resp)
} else {
return Promise.reject(resp)
}
}
},
persist: {
key: 'pinia-userstore',
storage: localStorage,
}
})
```
组合式方式
```js
import { defineStore } from 'pinia'
import axios from 'axios'
import { ref,computed } from 'vue'
//https://blog.csdn.net/weixin_62897746/article/details/129124364
//https://prazdevs.github.io/pinia-plugin-persistedstate/guide/
export const useUserStore = defineStore('user', ()=>{
// 相当于 state
const user = ref({})
const username = ref('')
const userId = ref('')
const token = ref('')
const roles = ref([])
const permissions = ref([])
// 使用计算属性处理状态数据
const roleName = computed(()=>{
return roles.value.map(r=>r.name).join(",")
})
const permissionCode = computed(()=>{
return permissions.value.map(r=>r.code).join(",")
})
// 定义函数---actions
const toLogin = async (loginUser) => {
const resp = await axios.post("http://localhost:8989/login/toLogin", loginUser)
if (resp.data.code === 20000) {
// 这个会回退,回退登录页
var result = resp.data.data
console.log("result",result)
// 把数据放入到状态管理中
user.value = result.user
userId.value = result.user.id
username.value = result.user.name
token.value = result.token
roles.value = result.roles
permissions.value = result.permissions
return Promise.resolve(resp)
} else {
return Promise.reject(resp)
}
}
return {
user,userId,username,token,toLogin,roles,permissions,roleName,permissionCode
}
})
```
使用
```js
import {useUserStore} from '@/stores/user.js'
const userStore = useUserStore();
// 获取状态属性
userStore.user = ""
// 获取getter属性
userStore.roleName
userStore.permissionCode
// 调用actions的方法
userStore.toLogin(xxxx)
```
关于mapGetter 、mapState、mapActions 的用法
```vue
<!-- <template>
我是一个首页
<div> {{ store.token }}</div>
<div>{{ store.userId }}</div>
<div>{{ store.username }}</div>
<div><button @click="handleChangeStore">改变</button></div>
<div><button @click="handleResetStore">重置</button></div>
</template>
<script setup>
import { userStore } from '@/stores/user.js'
const store = userStore()
const handleChangeStore = () => {
store.username = 'feifei'
}
const handleResetStore = () => {
store.$reset()
}
</script> -->
<!--
<template>
我是一个首页
<div>{{ userStore.userId }}</div>
<div>{{ userStore.username }}</div>
<div>{{ userStore.roles }}</div>
<div>{{ userStore.roleName }}</div>
<div>{{ userStore.permissions }}</div>
<div>{{ userStore.permissionCode }}</div>
</template>
<script setup>
import {useUserStore} from '@/stores/user.js'
const userStore = useUserStore(); -->
<!-- </script> -->
<!-- <template>
我是一个首页
<div>{{ userStore.userId }}</div>
<div>{{ userStore.roles }}</div>
<div>{{ userStore.roleName }}</div>
<div>{{ userStore.permissions }}</div>
<div>{{ userStore.permissionCode }}</div>
<hr>
<div>{{ userStore.username }}</div>
<div>{{ userStore.token }}</div>
<hr>
<div>{{ username }}</div>
<div>{{ token }}</div>
</template>
<script setup>
import {useUserStore} from '@/stores/user.js'
import { computed } from 'vue';
const userStore = useUserStore();
const username =computed(()=>userStore.username)
const token =computed(()=>userStore.token)
</script> -->
<!-- <template>
我是一个首页
<div>{{ username }}</div>
<div>{{ token }}</div>
</template>
<script>
import {useUserStore} from '@/stores/user.js'
const userStore = useUserStore();
//选项式
export default {
computed: {
username(){
return userStore.username
},
token(){
return userStore.token
}
}
}
</script> -->
<template>
我是一个首页
<div>{{ username }}</div>
<div>{{ token }}</div>
<button @click="toLogin">去登录</button>
</template>
<script>
import { mapState,mapActions } from 'pinia'
import {useUserStore} from '@/stores/user.js'
// mapState
// mapGetter 就是一种简化在计算属性中的一种状态管理获取状态属性或者getter属性的一种简化机制。
//选项式
export default {
computed: {
...mapState(useUserStore,["username","token"]),
},
methods:{
...mapActions(useUserStore,["toLogin"]),
}
}
</script>
```
通过一个动作去修改状态。
Vue3 的状态管理库(Pinia)_vue3 pinia-CSDN博客 这个很好
pinia持久化,用插件。
九、初始化页面布局
样式重置
导这个包。
饿了么组件导入
十、进场动画
如何实现
实现步骤:1
## 如何实现头部nprogress动画
1:安装NProgress组件
```sh
npm install nprogress
yarn add nprogress
pnpm install nprogress
```
2:使用nprogress
```js
import NProgress from 'nprogress'
// 显示右上角螺旋加载提示
NProgress.configure({ showSpinner: true })
//开启进度条
NProgress.start()
//完成进度条
NProgress.done()
```
3: 结合业务
**登录的时候使用:**
login.vue
```js
import NProgress from 'nprogress'
// 显示右上角螺旋加载提示
NProgress.configure({ showSpinner: true })
// 提交表单
const handleSubmit = async () => {
// axios.post ---application/json---gin-request.body
if(!loginUser.code){
alert("请输入验证码")
return;
}
if(!loginUser.account){
alert("请输入账号")
return;
}
if(!loginUser.password){
alert("请输入密码")
return;
}
//开启进度条
NProgress.start()
// 把数据放入到状态管理中
const resp = await userStore.toLogin(loginUser)
if (resp.data.code === 20000) {
router.push("/")
//完成进度条
NProgress.done()
} else {
//完成进度条
NProgress.done()
if (resp.data.code === 60002) {
alert(e.data.msg);
loginUser.code = "";
handleGetCapatcha();//重新激活生成新的验证码
}
}
}
```
加载路由登录的场景
```js
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress'
// 获取状态管理的token
import { useUserStore } from '@/stores/user.js'
// 显示右上角螺旋加载提示
NProgress.configure({ showSpinner: true })
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
meta: { title: "index" },
component: () => import('@/views/index.vue')
},
{
path: '/login',
name: 'login',
meta: { title: "login" },
component: () => import('@/views/login.vue')
},
{
path:'/:pathMatch(.*)*',
name:'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
})
router.beforeEach((to, from, next) => {
//开启进度条
NProgress.start()
next()
})
router.afterEach(() => {
//完成进度条
NProgress.done()
})
export default router
```
## 如何实现数字动画
参考:https://blog.csdn.net/weixin_44439675/article/details/121117468
参考:http://www.manongjc.com/detail/42-ftlenknixxjlktb.html
**vue2**
1 安装插件
```sh
npm install vue-animate-number
```
2 在main.js中引入
```js
import Vue from 'vue'
import VueAnimateNumber from 'vue-animate-number'
Vue.use(VueAnimateNumber)
```
3: 使用
```vue
<template>
<div>
<animate-number
from="1"
to="10"
duration="1000"
easing="easeOutQuad"
:formatter="formatter"
></animate-number>
<!-- 最简单的案例,from是开始值,to是结束值 -->
<animate-number from="1" to="10"></animate-number>
<animate-number ref="myNum" from="0" to="10" mode="manual" :formatter="formatter"></animate-number><br>
<!-- 也可以通过按钮去触发-->
<button type="button" @click="startAnimate()"> animate! </button>
</div>
</template>
<script>
export default {
methods: {
formatter: function (num) {
return num.toFixed(2)
},
startAnimate: function () {
this.$refs.myNum.start()
}
}
}
</script>
```
**vue3**
1.安装
```sh
npm install animated-number-vue3
yarn add animated-number-vue3
pnpm add animated-number-vue3
```
2.在main.js中引入
```js
import AnimatedNumber from 'animated-number-vue3'
app.use(AnimatedNumber)
```
3:使用
```vue
<animated-number from="0" :to="toNum" :key="toNum" duration="2000"></animated-number>
```
```vue
<template>
<div>
<animated-number from="0" :to="toNum1" :key="toNum"></animated-number>
<animated-number from="0" :to="toNum2" :key="toNum"></animated-number>
<animated-number from="0" :to="toNum3" :key="toNum"></animated-number>
</div>
</template>
<script setup>
import { ref } from 'vue'
const toNum1 = ref(4654)
const toNum2 = ref(1152)
const toNum3 = ref(5486)
</script>
```
十一 、环境隔离
在开发中,项目分为:生成阶段、开发阶段、测试阶段。如何无缝切换。
服务端-------nacos(统一配置)
客户端:.env环境配置
把要拦截的接口放在中间件后面,不用拦截的放在前面。
十二、
设置页面落脚点router-view 的位置,页面多的时候异步加载。
十三、用户
用户登出,token要放黑名单里。
十四、打包部署
打包go,linux和windows
十五、日志
logrus搭配lumberjack,实现滚动日志。
func InitLogrus() {
log1 := logrus.New()
// 创建一个文本格式化器并设置时间格式
customFormatter := &logrus.TextFormatter{
FullTimestamp: true, // 显示完整时间戳
TimestampFormat: "2006-01-02 15:04:05",
}
//使用lumberjack.Logger作为日志输出
log1.SetOutput(&lumberjack.Logger{
Filename: "./logs/err-" + getCurrentDate() + ".log",
MaxSize: 10, //文件大小(MB)
MaxBackups: 100, //最多备份文件数量
MaxAge: 30, //文件最长保存天数
Compress: false,
})
// 设置格式化器
log1.Formatter = customFormatter
log1.SetLevel(logrus.DebugLevel)
Log = log1
}
func getCurrentDate() string {
return time.Now().Format("2006-01-02")
}
十六、nginx
十七、集群docker
不要单节运行,使用集群的方式部署。
jenkins 小公司(开发阶段)
单项目与微服务
不要乱搞微服务,业务小就应该用单项目。
grpc基于底层的tcp。
web集群就是把若干个项目运行对外提供。
要同一个ip段上。192.168
如果是云服务器就用上面写着的私有地址。
集群部署一定是使用奇数节点,不能使用偶数。偶数成本高。(3个集群基本开端,为什么不是两台,因为如果第一台崩了,然后全部的请求去了第二台,那么第二台肯定也是承受不住。)
单机部署多应用:
把一个项目打包三次,每次的打包的项目的端口需要改变。
集群 :在nginx里使用负载均衡upstream
会轮询8081->8082->8083->8081,可以做一个权重轮询。
nginx的负载均衡知识点:
有一个weight
Docker
宿主机,docker容器镜像
环境隔离
读取环境变量
golang会缓存一份环境变量,如果改了环境变量但go没读到,应该重启golang。