关于后台登录模快的设计和开发
组件
element-plus + 自定义
01 设计Login.vue页面
考察点:布局 + form表单组件的调用
windicss的屏幕高度:
.min-h-screen{
min-height:100vh
}
布局
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>
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'
},
]
})
登录接口的调用
后端
准备服务接受参数
参数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;
}
登录服务接口的定义和编写
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;
}
}
配置跨域
在后端 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秒内,不需要再发送预检验请求,可以缓存该结果
}
}
02:前端
安装axios
npm install axios
前端定义跨域代理配置
在前端的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()]
})
这里注意:在未来的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: 请求
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
前端调用登录接口
定义LoginService.js
import request from '@/utils/request'
export default {
/**
* 登录逻辑
* @param {} user
* @returns
*/
toLogin(user) {
return request.post("/login/toLogin", user, { istoken: false });
}
}
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;
小结
- 在进行登录接口调用的时候,记得在拦截器的时候把登录和未来支付的回调一定要排除掉,否则就会出现问题
- 前后端分离的开放模式,肯定会存在跨域的问题。所以一定要在后端打开跨域配置,同时,在前端的vite.config.js配置服务的跨域代理。才跨域完成本地服务和前端工程的数据交互
- 至于后续如果发布的服务器,和真实的服务数据进行交互。请看后续的发布与部署的课程。
登录状态管理–vuex
1: 安装
npm install vuex@next vuex-persist --save
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;
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')
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 != "";
}
}
}
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;
用户下线的业务
登录流程如下:
登录数据如下:
后台代码
/**
* 登录
*
* @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;
}
挤下线的原理是:
- 通过用户ID去维持一个唯一状态(UUID) 。
- 如果这个UUID发生了变化,说明用户肯定去别的地方登录了。
关于程序使用Long精度丢失问题
1: fastjson依赖
<!--json处理-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
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);
}
}
- long类型经度的问题会自动解决
- 注意:customConverters 不能随意更改
关于服务返回错误信息两种捕捉方式
关于axios异步请求的response捕捉服务器返回错误信息的解释说明:
request.interceptors.response.use(function(response){
// 成功的返回 success
},function(err){
// 错误的返回
});
成功的返回
- 业务接口,确实没有任何和毛病,直接进入success
- 就那业务的,比如springmvc使用拦截器,这些被R化的错误信息,全部也会进入到success中。比如:
Vsserts.isEmptyEx(tokenUuid, AdminUserResultEnum.USER_LOGIN_UUID_EMPTY);
if (!tokenUuid.equalsIgnoreCase(cacheUuid)) {
throw new PugValidatorException(AdminUserResultEnum.USER_LOGIN_SAME);
}
因为上面的这些抛出,会被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(); }
登录校验码
验证码的依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
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;
}
}
- 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;
}
}
记住,和登录一样,验证码也是不需要拦截登录的,所以要在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");
}
接口地址
http://localhost:8877/admin/captcha
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;
Login.vue导入验证码
<script setup>
// 导入模块
import useLogin from '@/api/UseLogin.js';
// 把需要暴露的方法和相应属性全部导入
const {
userFormRef,
user,
userLoginRules,
captchaData,
handleLoginSubmit
} = useLogin();
</script>
定义图片
<img :src="captchaData" alt="">
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);
});
@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);
}
}