springboot实现简单的鉴权校验

背景

最近公司的项目在跟第三方对接,第三方要通过消息平台服务来向我们的系统推送消息,最一开始是给了一套方案,但是他们说没有登录态,所以开发了一个不走系统集成的登录鉴权,但是还是需要一个简单的鉴权来进行控制和获取一些信息。因此记录一下

我的项目版本为:Static BadgeStatic Badge

需求

我这里在开发的时候使用的方案是请求接口时候传递appKey和appSecret,然后使用拦截器拦截,从数据库中获取对应appKey的信息来进行校验,同时添加了时间进行校验,防止重提交。

实现

以下是拦截器的实现

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

@Component
public class AuthInterceptor implements HandlerInterceptor{

    // 此处可以换成从数据库中获取
    private final Map<String, AuthConfig> apiKeys = new HashMap<>();

    public AuthService() {
        // 初始化一些测试用的API密钥
        apiKeys.put("app001", new AuthConfig("app001", "secret123456", true, "aaa"));
        apiKeys.put("app002", new AuthConfig("app002", "secret789012", true, "aaa"));
    }
    
    
    @Override
    public boolean preHandle(@NonNull HttpServletRequest request,
                             @NonNull HttpServletResponse response,
                             @NonNull Object handler) throws Exception {

        // 获取请求头中的认证信息
        String accessToken = request.getHeader("AccessToken");
        String authorization = request.getHeader("Authorization");

        // 验证必需的认证信息
        if (accessToken == null || authorization == null) {
            sendErrorResponse(response, 401, "缺少认证信息,需要AccessToken和Authorization");
            return false;
        }

        // 解析Authorization获取appId和时间戳
        AuthUtil.AuthInfo authInfo = AuthUtil.parseAuthorization(authorization);
        String appKey = authInfo.getAppKey();
        String timestamp = authInfo.getTimestamp();
        // 在此处获取配置,是可以把一些额外的参数代到request中,方便接口获取,比如租户信息
        // 此处可以换成从对应的服务中获取
        AuthConfig config = apiKeys.get(appId);

        // 验证认证信息
        if (!validateAuth(accessToken, config, timestamp)) {
            sendErrorResponse(response, 403, "认证失败");
            return false;
        }

        // 认证成功,将appCode存入request属性中供后续使用
        // 获取appId并存储到request属性中
        request.setAttribute("tId", config.getTId());
        return true;
    }
    
    // 返回错误的信息
    private void sendErrorResponse(HttpServletResponse response, int status, String message) throws Exception {
        response.setStatus(status);
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> errorResponse = Maps.newHashMap();
        errorResponse.put("code", status);
        errorResponse.put("message", message);
        errorResponse.put("timestamp", System.currentTimeMillis());

        ObjectMapper objectMapper = new ObjectMapper();
        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }

}


这里简单给一下AuthConfig的结构

import com.alibaba.fastjson2.JSON;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class AuthConfig{
    /**
     * 第三方app_key
     */
    private String appKey;

    /**
     * 第三方app_secret
     */
    private String appSecret;

    /**
     * 有效标识
     */
    private Boolean valid;
    
    /**
     * 额外参数
     */
    private String aaa;
    
    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

然后是验证的逻辑,这里我简单封了一个工具类,如下所示

public class AuthUtil {

    private static final String TIMESTAMP_FORMAT = "yyyyMMddHHmmss";
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(TIMESTAMP_FORMAT);

    /**
     * Base64解码
     */
    public static String base64Decode(String encoded) {
        try {
            return Base64.decodeStr(encoded);
        } catch (Exception e) {
            throw new RuntimeException("Base64解码失败", e);
        }
    }

    /**
     * 生成AccessToken
     * 使用 sha256 加密(appKey + appSecret + 时间戳)
     */
    public static String generateAccessToken(String appKey, String appSecret, String timestamp) {
        return SecureUtil.sha256(appKey + appSecret + timestamp);
    }

    /**
     * 解析Authorization获取appKey和时间戳
     */
    public static AuthInfo parseAuthorization(String authorization) {
        try {
            String decoded = base64Decode(authorization);
            String[] parts = decoded.split(":");
            if (parts.length != 2) {
                throw new RuntimeException("Authorization格式错误");
            }
            return new AuthInfo(parts[0], parts[1]);
        } catch (Exception e) {
            throw new RuntimeException("解析Authorization失败", e);
        }
    }

    /**
     * 验证时间戳是否在有效期内(5分钟)
     */
    public static boolean isTimestampValid(String timestamp) {
        try {
            LocalDateTime timestampDateTime = LocalDateTime.parse(timestamp, FORMATTER);
            LocalDateTime currentDateTime = LocalDateTime.now();

            // 计算时间差(秒)
            long diffSeconds = Math.abs(java.time.Duration.between(timestampDateTime, currentDateTime).getSeconds());

            // 5分钟内有效
            return diffSeconds <= 300;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 认证信息类
     */
    @Getter
    @AllArgsConstructor
    public static class AuthInfo {

        private String appKey;

        private String timestamp;

    }
}

然后就是实现校验的逻辑,此方法放置在拦截器中

@Component
public class AuthInterceptor implements HandlerInterceptor{
    // 前面是刚才的拦截实现
    
    private boolean validateAuth(String accessToken, MsgThirdPartyAuthConfigDTO config, String timestamp) {
        try {
            // 1. 验证时间戳有效性
            if (!AuthUtil.isTimestampValid(timestamp)) {
                return false;
            }

            // 2. 根据appId获取应用信息
            if (config == null || !config.getValid()) {
                return false;
            }

            // 3. 使用appId + appSecret + 时间戳生成期望的AccessToken
            String expectedAccessToken = AuthUtil.generateAccessToken(config.getAppKey(), config.getAppSecret(), timestamp);

            // 4. 比对AccessToken
            return expectedAccessToken.equals(accessToken);

        } catch (Exception e) {
            return false;
        }
    }
}

此时就可以实现拦截和校验了

扩展

因为此鉴权是仅针对系统中单独的某些接口进行校验,所以还需要配置一下路由

@Configuration
public class AuthConfig implements WebMvcConfigurer {

    @Resource
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                // 需要鉴权的路径
                .addPathPatterns("xxxxx")
                // 排除鉴权的路径
                .excludePathPatterns("xxxxx");
    }

}

后记

总归,核心的理念就是首先通过时间校验,然后从authorization中读取到key,再生成对应的token进行比对,以达成校验的目的

嘿嘿,又水一篇

posted @ 2025-07-03 19:46  忆故人  阅读(64)  评论(0)    收藏  举报