农产品销售管理系统实战——后端工具封装
说明
工欲善其事,必先利其器。为了我们之后的开发能够方便,我们先来封装一些工具。
创建工具包
在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.java
和ResultHelper.java
Result.java
内容如下,其中@Data
注解是lombok用来设置Getter
和Setter
,例如我们想要外部获取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);
}
}
结束
经过编写你的项目目录结构应该是以下这样的
当然这些工具并不是全部,不过不用担心,我们会在后面的程序编写需要的时候再进行封装。