山东大学创新实训——基于LLM的面向小学学段的AI辅助学习app(二)

后端选用springboot+mybatis进行开发,数据库采用mysql,预计会引入redis

项目架构

项目架构
对于主要的代码文件,从上到下依次为:

  1. annotation: 自定义注解,如使用自定义注解实现身份token鉴权
  2. config: 全局设置,如配置拦截器在请求处理前获取并解析token
  3. context: 上下文,标识当前进行用户的一些信息,包含id、角色
  4. controller: api访问控制层,定义如何通过url访问到对应方法以及参数
  5. data:定义一些实例类型,分别为:
    • constant:常量,多为枚举类型
    • po:持久化存储对象,实际定义的对象需要存入数据库中
    • vo:值对象,主要为不同层之间传递信息,一般封装一个Result对象用于返回
  6. handler: 全局处理类,如全局异常处理或websocket等方式的处理逻辑定义
  7. interceptor: 拦截器,定义具体对请求的前/后处理
  8. mapper: mybatis进行方法和sql的映射,底层
  9. service:具体处理请求的逻辑
  10. util: 工具类

项目按照这种形式规范进行开发。

token进行鉴权的流程

流程说明

token是标识一个用户的一种凭证,能够简化权限认证的过程,因此需要加密(防纂改),也需要设置凭证到期时效(增加安全性)。
从生成token开始,基本流程如下:

  1. 用户输入账号密码进行验证,若正确则需要生成token
  2. 提取需要的信息(如id、role)进行对称加密,生成有到期时间的token
  3. 分发token,用户后续请求携带token,根据token鉴权

从获取请求到从token获取信息,流程如下:

  1. 在请求到来时进行拦截,得到token
  2. 用加密密钥进行解密,得到token携带的payload信息
  3. 根据具体信息进行下一步操作,如根据权限判断是否合法

采用拦截器的主要原因是代码简洁,不需要在service层重复调用

加解密、token生成与提取信息

依赖

使用jwt库进行加密相关操作,pom.xml依赖配置:

		<dependency>
    		<groupId>com.auth0</groupId>
    		<artifactId>java-jwt</artifactId>
    		<version>3.10.3</version>
		</dependency>

实现

utils/下配置一个简单的JWTUtil进行相关逻辑实现,可以直接复用
此处主要关注用户的id和role信息
token鉴权若无效或者过期,抛出AuthenticationException

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.time.Instant;
import java.util.Map;
import java.util.HashMap;
@Component
public class JWTUtil {

    @Value("${jwt.token}")
    private String SECRET_KEY;

    @Value("${jwt.expiration}")
    private Integer EXPIRATION;

    public static String getToken(Map<String, String> map, int expireTime, String key) {
        //获取时间,设置token过期时间
        Instant instant = Instant.now();
        Instant newInstant = instant.plusSeconds(expireTime);

        //JWT添加payload
        JWTCreator.Builder builder = JWT.create();
        map.forEach(builder::withClaim);

        //JWT过期时间 + signature
        return builder.withExpiresAt(Date.from(newInstant)).sign(Algorithm.HMAC256(key));
    }
    public String getTokenWithPayLoad(String id, String role) {
        Map<String, String> map = new HashMap<>();
        map.put("id", id);
        map.put("role", role);
        return JWTUtil.getToken(map, EXPIRATION, SECRET_KEY);
    }
    // 验证token
    public static void verify(String token, String key) {
        JWT.require(Algorithm.HMAC256(key)).build().verify(token);
    }

    // 返回token内容
    public static DecodedJWT getTokenInfo(String token, String key) {
        return JWT.require(Algorithm.HMAC256(key)).build().verify(token);
    }
}

拦截器逻辑及配置

注解

对于不同的方法,可能需要不同的权限进行访问控制,且在运行时进行确定
定义一个RequiredLogin注解判断权限是否合法
在annotations/下进行定义

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD) // 方法注解
@Retention(RetentionPolicy.RUNTIME) // 运行时才能判断请求的权限合法性
public @interface RequiredLogin {
    boolean required() default true;
    String roles() default "ADMIN";
}

拦截器

重写Inteceptor方法,在preHandler下定义验证、解析并提取token信息
主要需要通过反射获取请求对应的方法,再判断是否有对应注解需要验证

import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import org.apache.catalina.connector.Response;
import org.example.backend.annotation.RequiredLogin;
import org.example.backend.context.UserContext;
import org.example.backend.data.constant.Role;
import org.example.backend.util.JWTUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;


import javax.naming.AuthenticationException;
import java.lang.reflect.Method;
import java.util.Objects;


/**
 * 拦截器,判断用户权限,返回值为true则放行,false则中止
 */
@Component
public class Interceptor implements HandlerInterceptor {
    @Value("${jwt.token}")
    private String SECRET_KEY;

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
        // 判断调用的是不是一个接口方法
        if(!(handler instanceof HandlerMethod)){
            return true;
        }

        // 获取请求头里"token"内数据
        String token = request.getHeader("token");

        response.setContentType("application/json;charset=UTF-8");

        // java反射
        Method method = ((HandlerMethod) handler).getMethod();

        // 如果类上有该注解,则执行
        if(method.isAnnotationPresent(RequiredLogin.class)){
            // 如果注解中required = true则执行
            if(method.getAnnotation(RequiredLogin.class).required()){
                // 获取需求的权限
                String requiredRole = method.getAnnotation(RequiredLogin.class).roles();

                try{
                    // 验证token
                    JWTUtil.verify(token, SECRET_KEY);

                    // 获取token的payload中信息
                    DecodedJWT info = JWTUtil.getTokenInfo(token, SECRET_KEY);

                    // 获取用户权限
                    String userRole = info.getClaim("role").asString();
                    UserContext.id = Integer.parseInt(info.getClaim("id").asString());
                    UserContext.role = userRole;
                    // 放行ADMIN
                    if (Objects.equals(userRole, Role.ADMIN.role)) return true;

                    // 权限需求为ALL则全放行
                    if (Objects.equals(requiredRole, "ALL")) return true;

                    // 进行权限比对
                    if(!Objects.equals(userRole, requiredRole)) throw new AuthenticationException();
                    return true;
                }catch (AuthenticationException e){
                    response.sendError(Response.SC_FORBIDDEN, "Permission Denied");
                    return false;
                } catch (Exception e){
                    response.sendError(Response.SC_UNAUTHORIZED, "Authentication failed");
                    return false;
                }
            }
        }

        return true;
    }
}

拦截器配置

在configs/下声明InteceptorConfig:
可以配置多个拦截器

import jakarta.annotation.Resource;
import org.example.backend.interceptor.Interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Resource
    Interceptor interceptor;
    /*
    添加拦截器,进行token验证
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor)
                .addPathPatterns("/**");
    }
}

在配置完之后,只需要在方法上加上
@RequiredLogin(roles = "ALL")
则放行所有携带有效token的请求,需要对应权限修改ALL为对应权限名称即可,权限采用枚举类型

全局配置

除了拦截器的配置之外,也可以实现一些比较便捷的全局配置,如Transactional注解的方法若抛出错误则不会返回有效的请求,可以通过全局异常处理来捕获。
主要通过声明异常的类型进行捕获处理,如mybatis访问数据库出错,需要回滚,则不会在控制台抛出错误信息并中止请求,而是返回一个有意义的报错信息

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import org.apache.catalina.connector.Response;
import org.example.backend.data.vo.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

/*
 全局异常处理
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MybatisPlusException.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ResponseEntity<Object> handleDatabaseException(MybatisPlusException e) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.APPLICATION_JSON)
                .body(Result.error(Response.SC_INTERNAL_SERVER_ERROR,"数据库异常,回滚:" + e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ResponseEntity<Object> handleException(Exception e) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.APPLICATION_JSON)
                .body(Result.error(Response.SC_INTERNAL_SERVER_ERROR,"出现异常:" + e.getMessage()));
    }
}

通过以上形式实现了一个简单有效、逻辑清晰的token鉴权访问机制,易读性比较好,后端会在此基础上进行下一步详细逻辑的开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值