• 后台登录模块以及验证码登录


    关于后台登录模快的设计和开发

    组件

    element-plus + 自定义

    01 设计Login.vue页面

    考察点:布局 + form表单组件的调用

    windicss的屏幕高度:

    .min-h-screen{
      min-height:100vh
    }
    
    • 1
    • 2
    • 3

    布局

    element-plus把布局分割成了24列。行成row+column 。让开发者可以方便进行弹性布局。

    链接

    <el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
                <div class="grid-content ep-bg-purple min-h-full bg-green-300" >
                    1
                div>
            el-col>
         <el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
                <div class="grid-content ep-bg-purple min-h-full bg-red-400" >
                    2       
                div>
            el-col>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    element-plus把row-column分割24列。进行布局。并且为了适应各种分辨率。定义不同尺寸下的适配的目的。适配分辨率如下:

    是以一种移动优先原则设定栅格布局:

    • xs < 768px 响应式栅格数或者栅格属性对象
    • sm ≥ 768px 响应式栅格数或者栅格属性对象
    • md ≥ 992px 响应式栅格数或者栅格属性对象
    • lg ≥ 1200px 响应式栅格数或者栅格属性对象
    • xl ≥ 1920px 响应式栅格数或者栅格属性对象

    在xl(2k屏幕),lg(大屏幕),md(中等屏幕),sm (小屏幕,平板),xs(超小屏,类似手机)都按照你设定的行和列进行显示。但是你可以在这些分辨率下搭配显示不一样的效果。但是在xs以后就全部是 100% (自动把column–>row)

    场景1:

    • xl 大于 1920分辨率,两列:10 / 14
    • lg 大于 1200分辨率,两列: 9 / 15
    • md 大于 992分辨率,两列:12/ 12

    表单校验

    官网

    链接

    步骤如下

    1: 定义 ref=“ruleFormRef”

    2: 在script中使用 ref() 定义即可。

    3: 在form标签上定义 :rules=“rules”

    4:rules规则如下:

    blur代表失去焦点触发

    const rules = reactive({
      username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min:4 , max: 20, message: '你的用户名必须是 4 to 20', trigger: 'blur'}
      ],
      password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min:4 , max: 20, message: '你的密码必须是 4 to 20', trigger: 'blur'}
      ],
     
      code: [
        {
          required: true,
          message: '请输入验证码',
          trigger: 'blur'
        },
      ]
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    登录接口的调用

    后端

    准备服务接受参数

    参数vo

    package com.pug.zixun.vo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class LoginVo implements java.io.Serializable{
        // 用户姓名
        private String username;
        // 密码
        private String password;
        // 验证码
        private String code;
        // 验证码的UUID
        private String uuid;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    登录服务接口的定义和编写

    package com.pug.zixun.controller.login;
    
    import com.pug.zixun.bo.PugUserBo;
    import com.pug.zixun.commons.enums.AdminUserResultEnum;
    import com.pug.zixun.commons.utils.fn.asserts.Vsserts;
    import com.pug.zixun.commons.utils.pwd.MD5Util;
    import com.pug.zixun.config.jwt.JwtService;
    import com.pug.zixun.config.redis.AdminRedisKeyManager;
    import com.pug.zixun.config.redis.IJwtBlackService;
    import com.pug.zixun.controller.BaseController;
    import com.pug.zixun.pojo.User;
    import com.pug.zixun.service.user.IUserService;
    import com.pug.zixun.vo.LoginVo;
    import com.pug.zixun.vo.UserVo;
    import lombok.extern.slf4j.Slf4j;
    import org.pug.generator.anno.PugDoc;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.UUID;
    @RestController
    @Slf4j
    @PugDoc(name = "登录管理", tabname = "kss_user")
    public class PassportLoginController extends BaseController implements AdminRedisKeyManager {
    
    
        @Autowired
        private IUserService userService;
        @Autowired
        private JwtService jwtService;
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @Autowired
        @Qualifier("jwtBlackStringService")
        private IJwtBlackService jwtBlackService;
    
        /**
         * 登录
         *
         * @param loginVo
         * @return
         */
        @PostMapping("/login/toLogin")
        @PugDoc(name = "登录管理")
        public PugUserBo logined(@RequestBody LoginVo loginVo) {
            // 这里有校验,spring-validator框架来完成 或者用断言 或者用自己封装的
            Vsserts.isEmptyEx(loginVo.getUsername(), AdminUserResultEnum.USER_NAME_NOT_EMPTY);
            Vsserts.isEmptyEx(loginVo.getPassword(), AdminUserResultEnum.USER_PWD_NOT_EMPTY);
            // 根据用户名称查询用户信息
            User dbLoginUser = userService.login(loginVo);
            Vsserts.isNullEx(dbLoginUser, AdminUserResultEnum.USER_NULL_ERROR);
            // 用户输入的密码
            String inputPwd = MD5Util.md5slat(loginVo.getPassword());
            // 如果输入密码和数据库密码不一致
            boolean isLogin = dbLoginUser.getPassword().equalsIgnoreCase(inputPwd);
            // 如果输入的账号有误,isLogin=false.注意isFalseEx在里面取反的,所以会抛出异常
            Vsserts.isFalseEx(isLogin, AdminUserResultEnum.USER_INPUT_USERNAME_ERROR);
    
            PugUserBo userBo = new PugUserBo();
            // 根据用户生成token
            String token = jwtService.createToken(dbLoginUser.getId());
            userBo.setToken(token);
            // 注意把一些敏感信息全部清空返回
            dbLoginUser.setPassword(null);
            userBo.setUser(dbLoginUser);
            // 登录挤下线
            String tokenUuid = UUID.randomUUID().toString();
            String tokenUuidKey = USER_LOGIN_LOGOUT_KEY + dbLoginUser.getId();
            stringRedisTemplate.opsForValue().set(tokenUuidKey, tokenUuid);
            userBo.setTokenUuid(tokenUuid);
            // 登录创建双倍时间,用于续期
            jwtService.redisToken(token);
    
            return userBo;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    配置跨域

    在后端 WebMvcConfiguration 配置类中完成跨域的配置,如下:

    package com.pug.zixun.config.mvc;
    
    import com.pug.zixun.config.interceptor.PassportLoginInterceptor;
    import com.pug.zixun.config.interceptor.PassportLogoutInterceptor;
    import org.passay.MessageResolver;
    import org.passay.spring.SpringMessageResolver;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.MessageSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.support.ResourceBundleMessageSource;
    import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
    import org.springframework.web.servlet.LocaleResolver;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {
    
        /**
         * 解决跨域问题
         *
         * @param registry
         */
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry
                    .addMapping("/**")
                    //.allowedOrigins("http://yyy.com", "http://xxx.com") //
                    // 允许跨域的域名
                    .allowedOriginPatterns("*") // 允许所有域
                    .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                    //.allowedMethods("*") // 允许任何方法(post、get等)
                    .allowedHeaders("*") // 允许任何请求头
                    .allowCredentials(true) // 允许证书、cookie
                    .maxAge(3600L); // maxAge(3600)表明在3600秒内,不需要再发送预检验请求,可以缓存该结果
        }
    
       
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    02:前端

    安装axios

    npm install axios
    
    • 1

    前端定义跨域代理配置

    在前端的vite.config.js配置服务的跨域代理。才跨域完成本地服务和前端工程的数据交互

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import WindiCSS from 'vite-plugin-windicss'
    import path from 'path'
    
    
    export default defineConfig({
        publicPath: '/',
        outputDir: 'dist',
        assetsDir: 'static',
        resolve: {
            alias: {
                '@': path.resolve(__dirname, 'src'),
                '@i': path.resolve(__dirname, './src/assets')
            }
        },
        server: {
            // 请求代理
            proxy: {
                '/admin': {
                    // 这里的地址是后端数据接口的地址
                    target: 'http://localhost:8877/',
                    //rewrite: (path) => path.replace(/^\/admin/, ''),
                    // 允许跨域
                    changeOrigin: true
    
                }
            }
        },
        plugins: [vue(), WindiCSS()]
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这里注意:在未来的router定义过程,千万不要定义以/admin开头的路由,否则会把它当作成服务请求资源请访问服务。就报错,

    把request.js的baseUrl暂时改成相对路径/admin

    const request = axios.create({
        // 如果在执行异步请求的时候,如果baseUrl
        //baseURL: "http://api.txnh.net/admin",
        baseURL: "/admin",
        // timeout` 指定请求超时的毫秒数(0 表示无超时时间)
        timeout: 10000,
        // 默认就是会给请求头增加token 
        istoken: true
    });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    比如你调用

    // 1: 请求
    request.post("/product/list")
    // 2: 转换路径 相对路径
    /admin/product/list
    // 3: 你当前访问路径 
    http://localhost:3000/product/list
    // 4: 最终生成的路径
    http://localhost:3000/admin/product/list
    // 5: 发现你以/admin 就去vite.config.js看看是不是复合服务器代理规则
    http://localhost:8877/admin/product/list
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    前端调用登录接口

    定义LoginService.js

    import request from '@/utils/request'
    
    export default {
    
        /**
         * 登录逻辑
         * @param {} user 
         * @returns 
         */
        toLogin(user) {
            return request.post("/login/toLogin", user, { istoken: false });
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Login.vue

    在Login.vue完成登录逻辑和处理

    import { ref, reactive } from 'vue'
    import loginService from '../services/LoginService';
    
    // 定义类的方式封装
    function UserLogin() {
    
        //1: 定义form表单对象,用于校验
        const userFormRef = ref(null);
        //2: 获取用户输入的form数据,是响应式的
        const user = reactive({
            username: '',
            password: '',
            code: ''
        })
    
        //3:  登录提交表单
        const handleLoginSubmit = () => {
            // 1: 提交表单校验 校验表单输入
            userFormRef.value.validate(async(valid) => {
                // 如果为valid = false 还存在非法数据.
                if (!valid) {
                    return;
                }
    
                try {
                    // 发起登录请求
                    const res = await loginService.toLogin(user);
                } catch (err) {
                    alert(err.message)
                }
            });
        };
    
        //4: 定义验证规则
        const userLoginRules = reactive({
            username: [
                { required: true, message: '请输入用户名', trigger: 'submit' },
                { min: 4, max: 20, message: '你的用户名必须是 4 to 20', trigger: 'submit' }
            ],
            password: [
                { required: true, message: '请输入密码', trigger: 'submit' },
                { min: 4, max: 20, message: '你的密码必须是 4 to 20', trigger: 'submit' }
            ],
    
            code: [{
                required: true,
                message: '请输入验证码',
                trigger: 'submit'
            }]
        });
    
    
        // 暴露方法给页面使用
        return {
            userFormRef,
            user,
            userLoginRules,
            handleLoginSubmit
        }
    }
    
    // 5 : 导出
    export default UserLogin;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    小结

    • 在进行登录接口调用的时候,记得在拦截器的时候把登录和未来支付的回调一定要排除掉,否则就会出现问题
    • 前后端分离的开放模式,肯定会存在跨域的问题。所以一定要在后端打开跨域配置,同时,在前端的vite.config.js配置服务的跨域代理。才跨域完成本地服务和前端工程的数据交互
    • 至于后续如果发布的服务器,和真实的服务数据进行交互。请看后续的发布与部署的课程。

    登录状态管理–vuex

    1: 安装

    npm install vuex@next  vuex-persist --save
    
    • 1

    2: 定义store/index.js

    import { createStore } from 'vuex'
    // 持久化管理信息
    import VuexPersistence from 'vuex-persist'
    // 用户的状态导入进来
    import user from '@/store/modules/user.js'
    
    
    // 本地缓存vuex管理信息
    // 为什么要适应vuex-persist组件,因为vuex数据库如果不持久化有一个bug
    // 当然用户刷新F5或者右键刷新的时候,vuex数据就会自动丢失。
    const vuexLocal = new VuexPersistence({
        key: "pug-admin-web-vuex",
        storage: window.localStorage
    })
    
    
    // 创建一个新的 store 实例
    // 创建状态管
    const store = createStore({
        plugins: [vuexLocal.plugin],
        modules: {
            user
        }
    })
    
    
    
    // 导出状态管理
    export default store;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    3: 注册store到全局

    import { createApp } from 'vue'
    // 导入首页
    import App from './App.vue'
    // 全局导入windicss样式
    import 'virtual:windi.css'
    // 注册路由
    import router from '@/router'
    // 导入状态管理
    import store from '@/store'
    // 引入element-plus模块
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    /*导入所有的图标组件*/
    import * as ElementPlusIconsVue from '@element-plus/icons-vue'
    
    
    const app = createApp(App);
    // 注册插件ElementPlus
    app.use(ElementPlus)
        // 注册状态管理
    app.use(store)
        // 注册element-plus所有的图标组件
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
        app.component(key, component)
    }
    // 注册插件router
    app.use(router)
    app.mount('#app')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    4: 模块化

    import loginService from '@/services/LoginService';
    
    
    export default {
        namespaced: true,
        // 定义全局状态管理的响应式数据
        state() {
            return {
                // 管理的用户对象信息
                userId: "",
                username: "",
                avatar: "",
                token: "",
                roleList: [],
                permissionList: []
            }
        },
    
        // 定义修改状态管理的方法
        // 结论:修改state的全局状态数据只能通过mutations的方法进行修改。
        // 调用通过:store.commit方法执行和调用
        mutations: {
            // 同步state中用户的信息
            toggleUser(state, serverUserData) {
                state.token = serverUserData.token;
                state.tokenUuid = serverUserData.tokenUuid;
                state.userId = serverUserData.user.id;
                state.username = serverUserData.user.username;
                state.avatar = serverUserData.avatar;
                state.roleList = [{ name: "管理员" }];
                state.permissionList = [{ code: "-1" }];
            },
    
            // 清除状态
            clearUser(state) {
                state.userId = "";
                state.account = "";
                state.avatar = "";
                state.token = "";
                state.roleList = [];
                state.permissionList = [];
            }
        },
    
        // 异步修改状态的的方法
        // 注意:actions定义的方式,不能直接修改state的状态数据
        // 只能通过context.commit去条件mutations的方法去修改state的数据。它是一个间接的方式
        // 调用通过:store.dispatch方法执行和调用
        actions: {
    
            toLogout(context) {
                //1: 异步请求去执行服务器退出操作
                //2: 执行页面状态清空
                context.commit("clearUser");
                return "success";
            },
    
    
            // 从登录以后的axios请求中获取serverUserData
            async toLogin(context, loginUserData) {
                try {
                    // 发起登录请求
                    const serverResponse = await loginService.toLogin(loginUserData);
    
                    // 通过context.commit去修改mutations中toggleUser去同步state数据
                    context.commit("toggleUser", serverResponse.data);
    
                    // 返回,这里是promise
                    return Promise.resolve(res);
                } catch (err) {
    
                    // 为什么不在这里处理呢,因为在这里没有办法和页面进行交互。
                    return Promise.reject(err);
                }
            }
        },
    
        // 对state数据的改造和加工处理。给未来的页面和组件进行调用。
        // 从而达到一个封装的目录 computed
        getters: {
            // 组装一个角色信息返回
            getRoleName(state) {
                return state.roleList.map(role => role.name).join(",");
            },
            // 获取权限
            getPermissions(state) {
                return state.permissionList.map(role => role.code);
            },
            // 判断是否登录
            isLogin(state) {
                return state.userId != "";
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    5: vuex和登录接口

    import { ref, reactive } from 'vue'
    import store from '@/store';
    import router from '@/router';
    
    // 定义类的方式封装
    function UserLogin() {
    
        //1: 定义form表单对象,用于校验
        const userFormRef = ref(null);
        //2: 获取用户输入的form数据,是响应式的
        const user = reactive({
            username: '',
            password: '',
            code: ''
        })
    
        //3:  登录提交表单
        const handleLoginSubmit = () => {
            // 1: 提交表单校验 校验表单输入
            userFormRef.value.validate(async(valid) => {
                // 如果为valid = false 还存在非法数据.
                if (!valid) {
                    return;
                }
    
                try {
                    // 执行状态的异步请求
                    const res = await store.dispatch("user/toLogin", user);
                    // 跳转首页去
                    router.push("/")
                } catch (err) {
                    alert("2---page --->" + err.message)
                }
            });
        };
    
        //4: 定义验证规则
        const userLoginRules = reactive({
            username: [
                { required: true, message: '请输入用户名', trigger: 'submit' },
                { min: 4, max: 20, message: '你的用户名必须是 4 to 20', trigger: 'submit' }
            ],
            password: [
                { required: true, message: '请输入密码', trigger: 'submit' },
                { min: 4, max: 20, message: '你的密码必须是 4 to 20', trigger: 'submit' }
            ],
    
            code: [{
                required: true,
                message: '请输入验证码',
                trigger: 'submit'
            }]
        });
    
    
        // 暴露方法给页面使用
        return {
            userFormRef,
            user,
            userLoginRules,
            handleLoginSubmit
        }
    }
    
    // 5 : 导出
    export default UserLogin;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    用户下线的业务

    登录流程如下:
    在这里插入图片描述

    登录数据如下:
    在这里插入图片描述

    后台代码

    /**
         * 登录
         *
         * @param loginVo
         * @return
         */
        @PostMapping("/login/toLogin")
        @PugDoc(name = "登录管理")
        public PugUserBo logined(@RequestBody LoginVo loginVo) {
           
            // 登录挤下线
            String tokenUuid = UUID.randomUUID().toString();
            String tokenUuidKey = USER_LOGIN_LOGOUT_KEY + dbLoginUser.getId();
            stringRedisTemplate.opsForValue().set(tokenUuidKey, tokenUuid);
    
    
            return userBo;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    挤下线的原理是:

    • 通过用户ID去维持一个唯一状态(UUID) 。
    • 如果这个UUID发生了变化,说明用户肯定去别的地方登录了。

    关于程序使用Long精度丢失问题

    1: fastjson依赖

    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2: 定义json转换的配置类

    package com.pug.zixun.config.json;
    
    import com.alibaba.fastjson.serializer.SerializeConfig;
    import com.alibaba.fastjson.serializer.SerializerFeature;
    import com.alibaba.fastjson.serializer.ToStringSerializer;
    import com.alibaba.fastjson.support.config.FastJsonConfig;
    import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
    import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    
    import java.math.BigInteger;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    @Configuration
    public class FastJsonConfiguration {
    
        @Bean
        public HttpMessageConverters customConverters() {
            Collection<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
            FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
            //自定义fastjson配置
            FastJsonConfig config = new FastJsonConfig();
            config.setSerializerFeatures(
                    SerializerFeature.WriteMapNullValue,        // 是否输出值为null的字段,默认为false,我们将它打开
                    SerializerFeature.WriteNullListAsEmpty,     // 将Collection类型字段的字段空值输出为[]
                    SerializerFeature.WriteNullStringAsEmpty,   // 将字符串类型字段的空值输出为空字符串
                    SerializerFeature.WriteNullNumberAsZero,    // 将数值类型字段的空值输出为0
                    SerializerFeature.WriteBigDecimalAsPlain,  // 讲long类型转化返回
                    SerializerFeature.WriteDateUseDateFormat,
                    SerializerFeature.WriteEnumUsingToString,
                    SerializerFeature.DisableCircularReferenceDetect    // 禁用循环引用
            );
    
            SerializeConfig serializeConfig = SerializeConfig.globalInstance;
            serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
            // 对你的Long类型进行经度转换
            serializeConfig.put(Long.class, ToStringSerializer.instance);
            serializeConfig.put(Long.TYPE, ToStringSerializer.instance);
            config.setSerializeConfig(serializeConfig);
    
            config.setDateFormat("yyyy-MM-dd HH:mm:ss");
            fastJsonHttpMessageConverter.setFastJsonConfig(config);
            // 添加支持的MediaTypes;不添加时默认为*/*,也就是默认支持全部
            // 但是MappingJackson2HttpMessageConverter里面支持的MediaTypes为application/json
            // 参考它的做法, fastjson也只添加application/json的MediaType
            List<MediaType> fastMediaTypes = new ArrayList<>();
            fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
            fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
    
    
            messageConverters.add(fastJsonHttpMessageConverter);
            return new HttpMessageConverters(true, messageConverters);
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • long类型经度的问题会自动解决
    • 注意:customConverters 不能随意更改

    关于服务返回错误信息两种捕捉方式

    关于axios异步请求的response捕捉服务器返回错误信息的解释说明:

    request.interceptors.response.use(function(response){
    	// 成功的返回 success
    },function(err){
    	// 错误的返回
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    成功的返回

    • 业务接口,确实没有任何和毛病,直接进入success
    • 就那业务的,比如springmvc使用拦截器,这些被R化的错误信息,全部也会进入到success中。比如:
     Vsserts.isEmptyEx(tokenUuid, AdminUserResultEnum.USER_LOGIN_UUID_EMPTY);
    
    • 1
    if (!tokenUuid.equalsIgnoreCase(cacheUuid)) {
                throw new PugValidatorException(AdminUserResultEnum.USER_LOGIN_SAME);
            }
    
    • 1
    • 2
    • 3

    因为上面的这些抛出,会被springmvc全局异常统一处理掉。然后进入R化,其实是一种正常的200返回。

    • 上面这种被R化所谓“被包装的错误信息”,永远都不会进入错误返回中。
    • 被R化定义的状态,根本就不是服务响应的状态。因为你处理以后当前正常的返回,这个服务状态永远是200
      在这里插入图片描述

    错误的返回

    • timeout超时会进入.比如请求接口的实际超过timeout直接进入

    • 服务器状态不是 200 的响应,全部进入到错误返回。并且不想进入成功处理在后台设置状态并且使用 response输出即可

      public void sendResponse(Object data){
          response.setCharacterEncoding("UTF-8")
          response.setStatus(500);
      	response.setContentType("application/json;charset=utf-8");
          PrintWirter writer= response.getWriter()
          writer.print(data);
          writer.flush();
          writer.close();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

    登录校验码

    验证码的依赖

    <dependency>
        <groupId>com.github.pengglegroupId>
        <artifactId>kaptchaartifactId>
        <version>2.3.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1: 验证码生成器

    package com.pug.zixun.controller.code;
    
    import com.google.code.kaptcha.impl.DefaultKaptcha;
    import com.pug.zixun.commons.utils.pwd.Base64;
    import lombok.extern.slf4j.Slf4j;
    import org.pug.generator.anno.PugDocIgnore;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.imageio.ImageIO;
    import javax.servlet.http.HttpSession;
    import java.awt.image.BufferedImage;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    @RestController
    @Slf4j
    @PugDocIgnore
    public class KaptchaCodeController {
    
        @Autowired
        private DefaultKaptcha producer;
        @Autowired
        StringRedisTemplate stringRedisTemplate;
    
        @ResponseBody
        @RequestMapping("/admin/captcha")
        public Map<String, Object> captcha(HttpSession session) throws IOException {
            /**
             * 前后端分离 登录验证码 方案
             * 后端生成图片 验证码字符串 uuid
             * uuid为key  验证码字符串为value
             * 传递bs64图片 和uuid给前端
             * 前端在登录的时候 传递 账号 密码 验证码 uuid
             * 通过uuid获取 验证码 验证
             */
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            //获取验证码 5898
            String text = producer.createText();
            log.info("1--------------->登录验证码:" + text);
    
            BufferedImage image = producer.createImage(text);
            ImageIO.write(image, "png", out);
            String base64bytes = Base64.encode(out.toByteArray());
            //该字符串传输至前端放入src即可显示图片,安卓可以去掉data:image/png;base64,
            String src = "data:image/png;base64," + base64bytes;
            // 生成一个key
            String redisTokenKey = UUID.randomUUID().toString();
            // 验证码信息
            Map<String, Object> map = new HashMap<>(2);
            // 这个验证码key
            map.put("codeToken", redisTokenKey);
            // 这个验证码的图片地址
            map.put("img", src);
            // 把生成的验证码放入到session中 spring-session
            //session.setAttribute("code", text);// 自动放入到redis
            // 这里为什么要设置时间,因为如果不设置时间,验证生成很频繁,其实一直放在内存中其实没必要的事情,所有设置一个有效期,自动从redis内存中删除
            stringRedisTemplate.opsForValue().set(redisTokenKey, text,5, TimeUnit.MINUTES);
            return map;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • codeToken主要是用来登录时候,传递给登录逻辑去redis获取验证值。
    • img : 验证码图片地址
    • text 具体验证码。 redis.set(token,text)
    • 缓存一定要设置时间。
      • 验证码,用完就应该失效,那些使用过或者失效全部全部存储内存中没有任何意义

    存在问题:如果用户五分钟输入验证,明明是正确但是提示失败。

    • 前台使用定时器setInterval(()=>创建验证码,1000 * 60 * 4)
    • 为什么code不放在session, 因为session是通过cookie来维持的,前端和后端根本不是一个cookie(host/port/path)。在服务端确实可以放入session。

    2: 验证码的配置类

    package com.ksd.pug.config.code;
    
    import com.google.code.kaptcha.impl.DefaultKaptcha;
    import com.google.code.kaptcha.util.Config;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Properties;
    
    /**
     * @author wl
     * @date 2019-4-23
     * 生成验证码的配置
     */
    @Configuration
    public class KaptchaConfig {
        @Bean
        public DefaultKaptcha producer() {
            Properties properties = new Properties();
            // 图片边框
            properties.setProperty("kaptcha.border", "yes");
            // 边框颜色
            properties.setProperty("kaptcha.border.color", "217,217,217");
            // 字体颜色
            properties.setProperty("kaptcha.textproducer.font.color", "black");
            // 图片宽
            properties.setProperty("kaptcha.image.width", "110");
            // 图片高
            properties.setProperty("kaptcha.image.height", "40");
            // 字体大小
            properties.setProperty("kaptcha.textproducer.font.size", "30");
            // session key
            properties.setProperty("kaptcha.session.key", "code");
            // 验证码长度
            properties.setProperty("kaptcha.textproducer.char.length", "4");
            // 字体
            properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
            // 干扰线
            properties.put("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    记住,和登录一样,验证码也是不需要拦截登录的,所以要在WebMvcConfiguration.java类中排除。切记。。。。。切记。。。。。

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //国际化切换
        registry.addInterceptor(localeChangeInterceptor());
        // 下线拦截器
        registry.addInterceptor(passportLogoutInterceptor).addPathPatterns("/admin/**")
            .excludePathPatterns("/admin/login/**","/admin/captcha");
        // 设置passportlogin的规则。以/admin开头的所有请求都要进行token校验
        registry.addInterceptor(passportLoginInterceptor).addPathPatterns("/admin/**")
            .excludePathPatterns("/admin/login/**","/admin/captcha");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    接口地址

    http://localhost:8877/admin/captcha
    
    • 1

    3: 页面对接验证码

    import { ref, reactive, onMounted } from 'vue'
    import store from '@/store';
    import router from '@/router';
    import captchaService from '@/services/code/CaptchaService'
    
    // 定义类的方式封装
    function UserLogin() {
    
        //1: 定义form表单对象,用于校验
        const userFormRef = ref(null);
        //1: 定义form表单对象,用于校验
        const captchaData = ref(null);
        //2: 获取用户输入的form数据,是响应式的
        const user = reactive({
            username: '',
            password: '',
            code: ''
        })
    
        //3:  登录提交表单
        const handleLoginSubmit = () => {
            // 1: 提交表单校验 校验表单输入
            userFormRef.value.validate(async(valid) => {
                // 如果为valid = false 还存在非法数据.
                if (!valid) {
                    return;
                }
    
                try {
                    // 执行状态的异步请求
                    const res = await store.dispatch("user/toLogin", user);
                    // 跳转首页去
                    router.push("/")
                } catch (err) {
                    alert("2---page --->" + err.message)
                }
            });
        };
    
        //4: 定义验证规则
        const userLoginRules = reactive({
            username: [
                { required: true, message: '请输入用户名', trigger: 'submit' },
                { min: 4, max: 20, message: '你的用户名必须是 4 to 20', trigger: 'submit' }
            ],
            password: [
                { required: true, message: '请输入密码', trigger: 'submit' },
                { min: 4, max: 20, message: '你的密码必须是 4 to 20', trigger: 'submit' }
            ],
    
            code: [{
                required: true,
                message: '请输入验证码',
                trigger: 'submit'
            }]
        });
    
        // 5 : 生成验证码
        const createCaptcha = async() => {
            try {
                var serverCode = await captchaService.createCaptcha();
                captchaData.value = serverCode.data.img;
            } catch (err) {
    
            }
        };
    
    
        // 6 : 生命周期初始化验证码
        onMounted(() => {
            // 执行创建验证码
            createCaptcha();
        });
    
    
        // 暴露方法给页面使用
        return {
            userFormRef,
            user,
            userLoginRules,
            captchaData,
            handleLoginSubmit
        }
    }
    
    // 5 : 导出
    export default UserLogin;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    Login.vue导入验证码

    <script setup>  
    // 导入模块
    import useLogin from '@/api/UseLogin.js';
    
    // 把需要暴露的方法和相应属性全部导入
    const {
        userFormRef,
        user,
        userLoginRules,
        captchaData,
        handleLoginSubmit
    } = useLogin();
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    定义图片

    
    
    • 1

    4: 查看效果
    在这里插入图片描述

    5:登录的校验验证码
    伪代码:

    //3:  登录提交表单
        const handleLoginSubmit = () => {
            // 1: 提交表单校验 校验表单输入
            userFormRef.value.validate(async(valid) => {
                // 如果为valid = false 还存在非法数据.
                if (!valid) {
                    return;
                }
    
                try {
                    showFullLoading();
                    loading.value = true;
                    // 加密传输
                    var username = encryptByDES(user.username);
                    var password = encryptByDES(user.password);
                    var { code, codeUuid } = user;
                    // 执行状态的异步请求
                    await store.dispatch("user/toLogin", { username, password, codeUuid, code });
                    // 跳转首页去
                    router.push("/")
                } catch (err) {
                    toastError(err.msg);
                    if (err.field == "code") {
                        user.code = "";
                        document.getElementById("code").focus();
                    }
    
                    if (err.field == "password") {
                        user.password = "";
                        user.code = "";
                        document.getElementById("password").focus();
                        // 重新生成新的验证码
                        createCaptcha();
                    }
                } finally {
                    loading.value = false;
                    hideFullLoading();
                }
            });
        };
           // 5 : 生成验证码
        const createCaptcha = async() => {
            try {
                var serverCode = await captchaService.createCaptcha();
                captchaData.value = serverCode.data.img;
                // 每次加载新的都把最新的uuid给用户登录对象
                user.codeUuid = serverCode.data.codeToken;
            } catch (err) {
    
            }
        };
    
    
        // 6 : 生命周期初始化验证码
        onMounted(() => {
    
            // 如果已经登录过了。直接跳转到首页去
            const isLogin = store.getters["user/isLogin"];
            if (isLogin) {
                router.push("/");
                return;
            }
    
            // 执行创建验证码
            createCaptcha();
            // 定时每隔四分钟执行一次重新生成验证码
            setInterval(createCaptcha, 4 * 60 * 1000);
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
     @PostMapping("/login/toLogin")
        @PugDoc(name = "登录管理")
        public PugUserBo logined(@RequestBody LoginVo loginVo) {
      // 这里有校验,spring-validator框架来完成 或者用断言 或者用自己封装的
            Vsserts.isEmptyEx(loginVo.getUsername(), AdminUserResultEnum.USER_NAME_NOT_EMPTY);
            Vsserts.isEmptyEx(loginVo.getPassword(), AdminUserResultEnum.USER_PWD_NOT_EMPTY);
            Vsserts.isEmptyEx(loginVo.getCode(), AdminUserResultEnum.USER_CODE_NOT_EMPTY);
            // 根据uuid获取redis缓存中的验证码信息
            String redisCode = stringRedisTemplate.opsForValue().get(loginVo.getCodeUuid());
            Vsserts.isEmptyEx(redisCode, AdminUserResultEnum.USER_CODE_NOT_EMPTY);
            // 把用户输入的code和缓存的redisCode
            if (! redisCode.equalsIgnoreCase(loginVo.getCode())){
                throw new PugValidatorException(AdminUserResultEnum.USER_CODE_INPUT_ERROR);
            }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    innodb不同场景下加锁分析总结
    Ansible中的任务执行控制
    spring Environment上下文环境参数变量
    面向对象的特点
    工程师文化
    一代人有一代人的使命
    mysql优化之索引
    【元胞自动机】基于matlab元胞自动机考虑驾驶行为的自动—求解手动驾驶混合交通流问题【含Matlab源码 2060期】
    工薪信用贷款全攻略:条件、流程与选择
    java计算机毕业设计医院门诊预约系统源码+系统+mysql数据库+lw文档
  • 原文地址:https://blog.csdn.net/Jiaodaqiaobiluo/article/details/126232875