农产品销售管理系统实战——后端工具封装

说明

工欲善其事,必先利其器。为了我们之后的开发能够方便,我们先来封装一些工具。

创建工具包

src/main/java/cn/edu/ujn/argi下创建文件夹util

Result和ResultHelper封装

想必你已经发现了,我们在TestController中描述的方法是这样的

public ResponseEntity<?> helloworld(){
    return ResponseEntity.ok("hello world");
}

它是返回一个ResponseEntity<?>类型,如果你调用ResponseEntity.ok方法,它对于浏览器就会返回一个Http Code为200的响应报文,其携带内容为hello world。但是在很多时候我们想要自己控制响应码Code以及消息内容。

例如,我们想让原来hello world的部分换成一个json格式的消息,大体结构如下

{
    code: 200,
    msg: "success",
    data: [...]
}

但是去创建一个这样对应的Java类,并通过构造函数去将数据部分传入对应的data字段又很繁琐,因此我们可以通过一个Result类统一消息格式,然后通过ResultHelper类的静态方法去生成一个Result并在Controller方法里调用作为返回值。

src/main/java/cn/edu/ujn/argi/util下创建Result.javaResultHelper.java

Result.java内容如下,其中@Data注解是lombok用来设置GetterSetter,例如我们想要外部获取code字段,就可以用result.getCode()来获取;如果是设置字段的值,则可以result.setCode(200)

// src/java/main/cn/edu/ujn/argi/util/Result.java
package cn.edu.ujn.argi.util;

import lombok.Data;

/**
 * 响应结果封装类
 */
@Data
public class Result {
    private int code;               // 响应码
    private String msg;             // 响应信息
    private Object data = null;     // 响应数据

    public Result(int code, String msg, Object data){
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

ResultHelper.java内容如下

package cn.edu.ujn.argi.util;

import org.springframework.http.ResponseEntity;

/**
 * 帮助返回Result
 * 注意返回类型使ResponseEntity<Result>
 * 对应JSON结构为{code: ..., message:..., data:{这里是Result},...}
 * 也就是说前端接收的对象外面还包了一圈,应当提取data部分的code, message, data
 */
public class ResultHelper {
    /**
     * 返回200成功,自定义数据,消息为success
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> success(Object data){
        return ResponseEntity.ok(new Result(200, "success", data));
    }

    /**
     * 返回200成功,消息success,数据为null
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> success(){
        return ResponseEntity.ok(new Result(200, "success", null));
    }

    /**
     * 返回200成功,自定义消息,自定义数据
     * @param message 消息
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> success(String message, Object data){
        return ResponseEntity.ok(new Result(200, message, data));
    }

    /**
     * 返回500错误,数据为null,消息为error
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> error(){
        return ResponseEntity.ok(new Result(500, "error", null));
    }

    /**
     * 返回500错误,自定义消息,自定义数据
     * @param message 消息
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> error(String message, Object data){
        return ResponseEntity.ok(new Result(500, message, data));
    }

    /**
     * 返回500错误,消息error, 自定义数据
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> error(Object data){
        return ResponseEntity.ok(new Result(500,"error", data));
    }

    /**
     * 未认证401 消息为unauthorized 数据为null
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> unauth(){
        return ResponseEntity.ok(new Result(401, "unauthorized", null));
    }

    /**
     * 未认证401 自定义数据 消息为unauthorized
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> unauth(Object data){
        return ResponseEntity.ok(new Result(401, "unauthorized", data));
    }

    /**
     * 未认证401 自定义消息 自定义数据
     * @param message 消息
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> unauth(String message, Object data){
        return ResponseEntity.ok(new Result(401, message, data));
    }

    /**
     * 自定义Result
     * @param code 状态码
     * @param message 消息
     * @param data 数据
     * @return ResponseEntity<Result>
     */
    public static ResponseEntity<Result> customResult(int code, String message, Object data){
        return ResponseEntity.ok(new Result(code, message, data));
    }

}

现在我们返回TestController.java去试试,将内容改为如下

package cn.edu.ujn.argi.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.edu.ujn.argi.util.Result;
import cn.edu.ujn.argi.util.ResultHelper;

@RestController
@RequestMapping("/testController")
public class TestController {
    
    @GetMapping("helloworld")
    public ResponseEntity<Result> helloworld(){
        return ResultHelper.success("hello world");
    }
}

然后运行项目,去浏览器访问一下,发现页面变成这样了。

对于Firefox浏览器你可能看到的内容不太直观,因为它对于JSON格式数据展示做了UI上的优化,你可以点击原始数据来查看它本来的面目。

JWT工具封装

对于JWT的概念我们这里过多讲解,感兴趣的可以去看看这篇文章JWT详细讲解(保姆级教程)-阿里云开发者社区

这里我们简单的说一下,JWT通过私钥(就是我们设置的secret)对一串包含你非隐私信息的Json字符串做签名得到Token,然后第三方通过JWT公钥解密获取信息,由于私钥仅由JWT服务器所知,第三方不可能得知,这样我们就可以保证消息没有篡改(即消息的不可否认性)以及用户识别。

由此可见JWT对于接下来的用户登录以及接口的角色校验是非常重要的,所以我们相关库的基础上封装一些JWT的工具类。

首先在src/main/java/cn/edu/ujn/argi/util下创建jwt文件夹,并在jwt文件夹下创建java类 JwtField.java用于读取配置文件application.yaml的内容。这里有必要说明下@Component注解被Spring用于自动扫描和依赖注入。

package cn.edu.ujn.argi.util.jwt;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import lombok.Data;

@Data
@Component
public class JwtField {
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expired}")
    private int expired;
}

此外我们还需要利用这些配置参数结合一些登录传入的参数来生成Token颁发给用户,我们还是在此目录下创建java类JwtUtil.java。这里的@Autowired是用于向Spring容器获取刚才注入的JwtField。

package cn.edu.ujn.argi.util.jwt;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

@Component
public class JwtUtil {
    @Autowired
    private JwtField jwtField;

    /**
     * 生成token,用于认证
     * @param username 用户名
     * @param role 角色 例如:user、seller
     * @param id 用户id
     * @return token字符串
    */
    public String generateToken(String username, String role, Long id){
        // 使用Hmac256算法,使用secret作为密钥
        Algorithm algo = Algorithm.HMAC256(jwtField.getSecret());
        return JWT.create()
                .withSubject(username)
                .withClaim("role", role)
                .withClaim("id", id)
                .sign(algo);
    }

    /**
     * 验证token
     * @param token token字符串
     * @return 验证通过返回DecodedJWT对象,否则返回null
     */
    public DecodedJWT verifyTokne(String token){
        try{
            Algorithm algorithm = Algorithm.HMAC256(
                jwtField.getSecret()
            );
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return jwt;
        }catch(Exception e){
            return null;
        }
    }

    /**
     * 获取token中的角色信息
     * @param token token字符串
     * @return 角色信息字符串
     */
    public String getRole(String token){
        try{
            Algorithm algorithm = Algorithm.HMAC256(
                jwtField.getSecret()
            );
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getClaim("role").asString();
        }catch(JWTVerificationException e){
            return null;
        }
    }

    /**
     * 获取token中的用户名
     * @param token token字符串
     * @return 用户名
     */
    public String getUsername(String token){
        try{
            Algorithm algorithm = Algorithm.HMAC256(
                jwtField.getSecret()
            );
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getSubject();
        }catch(JWTVerificationException e){
            return null;
        }
    }

    /**
     * 获取token中的用户ID
     * @param token token字符串
     * @return ID
     */
    public Long getId(String token){
        try{
            Algorithm algorithm = Algorithm.HMAC256(
                jwtField.getSecret()
            );
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getClaim("id").asLong();
        }catch(JWTVerificationException e){
            return null;
        }
    }
}

这些工具极大简便了我们我们对于Token的签发与认证,但是这还不够。试想我们如果在每个Controller的每个方法内部都调用JwtUtil,然后通过Spring获取Token进行解码认证,再根据返回结果,判断是否合法,如果非法则返回未认证的消息,如果合法则进行业务逻辑执行,这一系列过程如果要反复去写也挺令人头疼的。

有没有什么简单的方法呢,最好是对某个方法标记一下自动就进行Token验证以及角色判断。不难观察,我们的业务逻辑总是在Token验证和角色判断之后执行的,那我们就可以写一个注解,然后设置Aop通知,在执行业务逻辑之前进行Token自动判断不就行了。

src/main/java/cn/edu/ujn/argi/util/jwt下创建文件RequireRole.java,里面实现一个注解

package cn.edu.ujn.argi.util.jwt;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String[] value();
}

然后创建RoleAop.java文件,内容如下:

package cn.edu.ujn.argi.util.jwt;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;

import cn.edu.ujn.argi.util.Result;
import cn.edu.ujn.argi.util.ResultHelper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

@Aspect
@Component
@Slf4j
public class RoleAop {
    @Autowired
    private JwtUtil jwtUtil;

    @Around("@annotation(cn.edu.ujn.argi.util.jwt.RequireRole)")
    public ResponseEntity<Result> checkRole(ProceedingJoinPoint joinPoint) throws Throwable{
        // 从Spring的服务器消息中获取请求头中的token
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("Authorization");
        if(token == null || "".equals(token)){
            // 如果不存在token则返回未认证的错误信息
            return ResultHelper.unauth();
        }

        // Token值的格式为Bearer xxxxxx,需要去掉Bearer前缀
        token = token.replace("Bearer ", "");
        String userRole = jwtUtil.getRole(token);
        if(userRole == null){
            // 获取用户角色失败
            return ResultHelper.unauth();
        }
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RequireRole requireRole = method.getAnnotation(RequireRole.class);
        String[] allowedRoles = requireRole.value();

        boolean hasAccess = false;
        for (String role : allowedRoles) {
            if (role.equals(userRole)) {
                hasAccess = true;
                break;
            }
        }

        if (!hasAccess) {
            return ResultHelper.unauth();
        };

        // 继续执行业务逻辑,返回业务逻辑的值
        return (ResponseEntity<Result>)joinPoint.proceed();
    }
}

PwdHelper

当用户注册和登录的时候,我们需要比对数据库内部的密码和用户传入的密码。但为了安全性,我们往往不是以明文的形式存入数据库的,通常情况下是将明文与一段称为“盐”的随机字符串拼接起来,然后使用一个Hash算法进行计算,并将得到的结果存入数据库。

当然在登录的时候,用户传入的明文与数据库存放的盐进行拼接,然后通过Hash运算得到的结果与数据库的值进行比对,如果值一致则密码正确,反之则登陆失败。

为什么需要盐?一段固定的字符串,通过Hash运算得到的结果是唯一的。但是用户如果在该平台与其他平台所用相同的密码一致,在没有盐的情况下,只要一处平台的密文泄露,黑客就可以通过跑字典的暴力方式获取明文,从而导致其他平台密码也泄露。如果使用了盐,拼接的字符串由于盐的不同,所得密文也不同(甚至可以说非常不同,因为Hash函数的雪崩效应)。

src/main/java/cn/edu/ujn/argi/util/jwt添加文件PwdHelper.java

package cn.edu.ujn.argi.util;

import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;

import org.springframework.util.DigestUtils;

public class PwdHelper {
    private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private static final SecureRandom secureRandom = new SecureRandom();

    /**
     * md5 hash
     * @param val 待hash的字符串
     * @return hash后的字符串
     * @throws UnsupportedEncodingException 
     */
    public static String md5Hash(String val) throws UnsupportedEncodingException{
        String md5 = DigestUtils.md5DigestAsHex(val.getBytes("utf-8"));
        return md5;
    }

    /**
     * 生成指定长度的随机字符串作为盐
     * @param length 随机字符串的长度 
     * @return salt
     */
    public static String getRandomSalt(int length){
        if (length < 0)
            return "";
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<length; i++){
            int randomIndex = secureRandom.nextInt(CHARACTERS.length());
            sb.append(CHARACTERS.charAt(randomIndex));
        }
        return sb.toString();
    }

    /**
     * 生成随机字符串为盐值,默认长度为4
     * @return salt
     */
    public static String getRandomSalt(){
        return getRandomSalt(4);
    }
}

结束

经过编写你的项目目录结构应该是以下这样的

当然这些工具并不是全部,不过不用担心,我们会在后面的程序编写需要的时候再进行封装。

最后修改:2025 年 04 月 15 日
如果觉得我的文章对你有用,请随意赞赏