SprongBoot 使用H2内存数据库+JWT 快速实现基本用户管理、权限验证(适合轻量项目、小程序、web 快速实现交互)

H2内存数据库

  1. 用 Java 开发的嵌入式(内存级别)数据库,它本身只是一个类库,也就是只有一个 jar 文件,可以直接嵌入到项目中。
  2. H2数据库又被称为内存数据库,因为它支持在内存中创建数据库和表。
  3. 最常使用的用途就在于可以同应用程序一起打包发布,可以非常方便地存储少量的结构化数据
  4. 项目部署到服务器时也不需要额外安装数据库,运行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获取用户信息、权限、时间的内容

在这里插入图片描述

配置

后端

  1. moven 引入配置
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.2</version>
        </dependency>
  1. 建立 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);
        }
    }
}
  1. 建立 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));
    }
}
  1. 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);
    }
}

  1. 原来使用 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

  1. 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')
    }
  }
})
  1. 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
})

简易权限管理 登录验证

  1. 登陆成功后,后端返回的 LoginUserVO 增加 登录时间字段、权限字段

    export type LoginUserVO = {
        id?: number;
        token?: string;
        userAccount?: string;
        userName?: string;
        userRole?: string;
        loginTime?: string;
    };
    
  2. 在 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()
          }
        }
      }
    })
    
    
  3. 由于项目简易,直接在某些需要 根据权限隐藏菜单、按钮的功能下 验证

    若是权限复杂项目可在 router.ts 中为每个页面设定权限验证

    示例:

    onMounted(() => {
      const authStore = tokenUserStore();
    
      // 有管理员权限,显示后台按钮
      if(authStore.loginUser.userRole === ACCESS_ENUMS.ADMIN) {
        showUserBtn.value = true;
      }
    })
    
  4. 用户登录验证、登录超时验证

    简易版示例:

    在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 用户登录时间是否超时,超时直接在前端拦截请求

小程序前端

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值