H2内存数据库
- 用 Java 开发的嵌入式(内存级别)数据库,它本身只是一个类库,也就是只有一个 jar 文件,可以直接嵌入到项目中。
- H2数据库又被称为内存数据库,因为它支持在内存中创建数据库和表。
- 最常使用的用途就在于可以同应用程序一起打包发布,可以非常方便地存储少量的结构化数据
- 项目部署到服务器时也不需要额外安装数据库,运行jar包h2数据库也会同时启动,适合数据需求小的项目
存储模式
(1) 内存模式(数据不持久化)
- 配置:
jdbc:h2:mem:testdb
- 特点:
- 数据仅存在于内存中,项目重启后数据丢失。
- 适用于临时测试或单元测试场景,无需持久化数据。
(2) 文件模式(数据持久化)
- 配置示例:
jdbc:h2:file:/path/to/your/database/testdb
(如 Windows:jdbc:h2:file:./sql/testdb
当前项目根目录的sql命令下,Linux/Mac:jdbc:h2:file:~/data/testdb
) - 特点:
- 数据保存在磁盘文件中,项目重启后数据仍然存在。
- 适用于需要持久化存储的开发或生产环境。
配置
shaoguan云服务器后端 application.yml
配置
- 以下为开启文件模式,数据持久化
- 开启h2控制台
- 目前只支持 单服务端连接,且需要在h2控制台操作建表,在idea操作项目无法访问
spring:
datasource:
# h2内存服务器 http://localhost:8090/api/h2-console
driver-class-name: org.h2.Driver
# url: jdbc:h2:mem:testdb;DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1 # 内存模式,数据不保存
# DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1 强制大小写不变
# # AUTO_SERVER=TRUE TCP模式,允许多客户端连接
url: jdbc:h2:file:./storage/sql/qjport;DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE; # 文件模式 数据存储在项目根目录下的storage; 部署jar包在和jar包同目录下的storage
username: qjqx
password:
# 开启H2控制台
h2:
console:
enabled: true
path: /h2-console
搭建对应mapper service controller层代码
登录方案 JWT
由于微信小程序不支持session会话,故改为使用 token 进行登录验证
每次请求在headers中携带 token,后端解析token获取用户信息、权限、时间的内容
配置
后端
- moven 引入配置
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
- 建立 JWT 的 token生成、验证器
public class JWTUtil {
private static final String SECRET_KEY = "xxxxx-SECRET-KEY"; // 设置自己的密钥
private static final long EXPIRATION_MS = 3600_000; // 1小时
// 生成 Token
public static String generateToken(LoginUserVO user) {
return JWT.create()
.withSubject(user.getId().toString())
.withClaim("userRole", user.getUserRole())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.sign(Algorithm.HMAC256(SECRET_KEY));
}
// 验证并解析 Token
public static DecodedJWT verifyToken(String token) {
try {
return JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token);
} catch (JWTVerificationException e) {
throw new BusinessException(ErrorCode.TOKEN_INVALID);
}
}
}
-
建立 JWT 拦截器,设置白名单过滤
注意 需要放行所有 OPTION 请求,浏览器进行该请求时不会携带token
public class JwtAuthInterceptor implements HandlerInterceptor {
// 白名单
private static final List<String> WHITE_LIST = Arrays.asList(
// 放行接口文档
"/api/doc.html",
"/api/v2/api-docs",
"/api/swagger-resources",
"/api/doc.html/**",
"/api/webjars/**",
"/api/favicon.ico",
"/api/error",
"/api/user/login",
"/api/user/register",
// 放行数据接收
"/api/receive/**",
"/api/radarCutData/**"
);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
/*
放行所有OPTIONS请求
浏览器访问时会先发送预检请求,接口存在之后才会请求接口(预检请求不会携带token,故直接放行)
*/
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.OK.value());
return true;
}
// 0. 白名单检查
if (isPublicUri(request.getRequestURI())) return true;
// 1. 获取Token
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
String token = authHeader.substring(7);
// 2. 解析验证Token
DecodedJWT jwt = JWTUtil.verifyToken(token); // 内部验证签名和有效期
// 3. 注入用户信息到请求上下文
request.setAttribute("currentUserId", jwt.getSubject());
request.setAttribute("currentUserRole", jwt.getClaim("userRole").asString());
return true;
}
// Ant路径匹配器
private final AntPathMatcher pathMatcher = new AntPathMatcher();
// 检查是否在白名单
private boolean isPublicUri(String uri) {
// System.out.println("白名单审查:" + uri);
return WHITE_LIST.stream()
.anyMatch(pattern -> pathMatcher.match(pattern, uri));
}
}
Config
中注册拦截器 和 跨域配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* 注册JWT连接器
* 使用JWT登录方式
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtAuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/user/login",
"/user/register",
"/**/OPTIONS" // 关键放行预检请求
);
}
/**
* JWT 跨域配置
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
-
原来使用 session 登录时使用 @AuthCheck 注解直接进行权限判断,现在无法使用
jwt拦截器在通过后,会回去token中的id和权限信息 存入会话
需要在Controller层后端取出用户信息 进行判断
@PostMapping("/list/page") public BaseResponse<Page<User>> listUserByPage(@RequestBody UserQueryRequest userQueryRequest, HttpServletRequest request) { String currentUserId = (String) request.getAttribute("currentUserId"); String currentUserRole = (String) request.getAttribute("currentUserRole"); // ... 判断用户权限 }
vue前端
前端在登录成功后,将得到的token存入pinia 全局管理
axios, 建立全局请求拦截器,为请求加上token
pinia
全局管理 token 值
// 全局token管理
import { defineStore } from 'pinia'
import type { LoginUserVO } from '@/api/generated'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('token') || '',
loginUser: {} as LoginUserVO
}),
actions: {
setToken(newToken: string) {
this.token = newToken
localStorage.setItem('token', newToken)
},
clearToken() {
this.token = ''
localStorage.removeItem('token')
}
}
})
src/request.ts
全局请求时在请求头 添加token
import axios from 'axios';
import { useAuthStore } from '@/store/api/tokenStore'
// 创建 Axios 实例
const myAxios = axios.create({
baseURL: baseURL,
timeout: 60000,
withCredentials: true, // 携带登录凭证
});
// 全局请求拦截器
myAxios.interceptors.request.use(config => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
简易权限管理 登录验证
-
登陆成功后,后端返回的 LoginUserVO 增加 登录时间字段、权限字段
export type LoginUserVO = { id?: number; token?: string; userAccount?: string; userName?: string; userRole?: string; loginTime?: string; };
-
在 pinia 全局管理中添加 用户信息管理
// 全局token管理 import { defineStore } from 'pinia' import type { LoginUserVO } from '@/api/generated' export const tokenUserStore = defineStore('auth', { state: () => ({ token: localStorage.getItem('token') || '', loginUser: (() => { try { const data = localStorage.getItem('loginUser') return data ? JSON.parse(data) : {} } catch (e) { return {} } })() as LoginUserVO }), actions: { setToken(newToken: string) { this.token = newToken localStorage.setItem('token', newToken) }, setLoginUser(userData: LoginUserVO) { this.loginUser = userData localStorage.setItem('loginUser', JSON.stringify(userData)) }, clearToken() { this.token = '' localStorage.removeItem('token') }, clearLoginUser() { this.loginUser = {} as LoginUserVO localStorage.removeItem('loginUser') } }, getters: { // 获取标准化用户数据 normalizedUser(): LoginUserVO { return { id: this.loginUser.id || 0, userAccount: this.loginUser.userAccount || '', userName: this.loginUser.userName || '未登录', userRole: this.loginUser.userRole || 'guest', token: this.token, loginTime: this.loginUser.loginTime ? new Date(this.loginUser.loginTime) : new Date() } } } })
-
由于项目简易,直接在某些需要 根据权限隐藏菜单、按钮的功能下 验证
若是权限复杂项目可在 router.ts 中为每个页面设定权限验证
示例:
onMounted(() => { const authStore = tokenUserStore(); // 有管理员权限,显示后台按钮 if(authStore.loginUser.userRole === ACCESS_ENUMS.ADMIN) { showUserBtn.value = true; } })
-
用户登录验证、登录超时验证
简易版示例:
在axios定义请求响应拦截器,通过识别后端返回的响应状态码,判断用户是否已登录,是否登录超时
// 全局请求拦截器 myAxios.interceptors.request.use(config => { // 判断是否登录(携带token) const authStore = tokenUserStore() if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}` } return config }) // 全局响应拦截器 myAxios.interceptors.response.use( function (response) { const { data } = response; // 未登录 if (data.code === 40100 || data.code === 40102) { // 清空全局登录用户信息 const authStore = tokenUserStore(); authStore.clearLoginUser() // 非登录请求 if ( !response.request.responseURL.includes('user/get/login') && !window.location.pathname.includes('/user/login') ) { // router.push('/login'); window.location.href = `/login` setTimeout(function() { if (data.code === 40100) ElMessage.warning('请先登录'); else ElMessage.warning('登录过期或状态异常'); }, 1000) } } return response; }, function (error) { return Promise.reject(error); } );
后端定义全局异常响应,当未登录、token异常时直接抛出异常,后端会报错并恢复前端对应的响应码
public enum ErrorCode { SUCCESS(0, "ok"), PARAMS_ERROR(40000, "请求参数错误"), NOT_LOGIN_ERROR(40100, "未登录"), NO_AUTH_ERROR(40101, "无权限"), TOKEN_INVALID(40102, "token错误"), NOT_FOUND_ERROR(40400, "请求数据不存在"), FORBIDDEN_ERROR(40300, "禁止访问"), SYSTEM_ERROR(50000, "系统内部异常"), OPERATION_ERROR(50001, "操作失败"), API_REQUEST_ERROR(50010, "接口调用错误"); // 其余方法略。。。。 } // 示例抛出异常 token错误 throw new BusinessException(ErrorCode.TOKEN_INVALID);
双重防护
在全局请求拦截器中,设定请求白名单 (复杂项目可以在 rouer.ts 设置每个页面的权限(未登录、普通用户、管理员等等))
当请求其余路径时,判断pinia 用户登录时间是否超时,超时直接在前端拦截请求