Spring Boot请求拦截及请求参数加解密

代码已上传至github,如遇到问题,可参照代码

https://github.com/dfyang55/auth
以下介绍的只是一种思路,这种东西不是死的
在这里插入图片描述

1)加密实现

后台代码实现:CodecUtil
这里我生成两个AES的私钥,一个只是提高SHA1加密的复杂度(这个可以不要,或者可以说任意的,类似于盐),另一个才是用于AES的加解密

/** AES密钥长度,支持128、192、256 */
private static final int AES_SECRET_KEY_LENGTH = 128;
private static String generateAESSecretKeyBase64(String key) {
    try {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(AES_SECRET_KEY_LENGTH);
        SecretKey secretKey = keyGenerator.generateKey();
        return Base64Utils.encodeToString(secretKey.getEncoded());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
/** AES加密密钥 */
public static final byte[] AES_SECRET_KEY_BYTES = Base64Utils.decodeFromString("XjjkaLnlzAFbR399IP4kdQ==");
/** SHA1加密密钥(用于增加加密的复杂度) */
public static final String SHA1_SECRET_KEY = "QGZUanpSaSy9DEPQFVULJQ==";

使用AES实现加密解密

public static String aesEncrypt(String data) {
    try {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 加密算法/工作模式/填充方式
        byte[] dataBytes = data.getBytes();
        cipher.init(Cipher.ENCRYPT_MODE,  new SecretKeySpec(AES_SECRET_KEY_BYTES, "AES"));
        byte[] result = cipher.doFinal(dataBytes);
        return Base64Utils.encodeToString(result);
    } catch (Exception e) {
        log.error("执行CodecUtil.aesEncrypt失败:data={},异常:{}", data, e);
    }
    return null;
}
public static String aesDecrypt(String encryptedDataBase64) {
    try {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 加密算法/工作模式/填充方式
        byte[] dataBytes = Base64Utils.decodeFromString(encryptedDataBase64);
        cipher.init(Cipher.DECRYPT_MODE,  new SecretKeySpec(AES_SECRET_KEY_BYTES, "AES"));
        byte[] result = cipher.doFinal(dataBytes);
        return new String(result);
    } catch (Exception e) {
        log.error("执行CodecUtil.aesDecrypt失败:data={},异常:{}", encryptedDataBase64, e);
    }
    return null;
}

使用SHA1加密

public static String sha1Encrypt(String data) {
    return DigestUtils.sha1Hex(data + SHA1_SECRET_KEY);
}

前端加密示例,这里是一个登陆的请求例子,先对数据进行加密,再用加密数据同时间戳和提高复杂度的AES密钥使用SHA1加密生成签名,最终将数据组装发送到后台。
注意这里有两个AES密钥,需要和后台对应。

$(function () {
    $("#login_submit").click(function() {
        var username = $("#username").val();
        var password = $("#password").val();
        if (username != undefined && username != null && username != ""
            && password != undefined && password != null && password != "") {
            var loginJSON = JSON.stringify({"username": username,"password": password});
            var encryptedData = CryptoJS.AES.encrypt(loginJSON, CryptoJS.enc.Base64.parse('XjjkaLnlzAFbR399IP4kdQ=='), {
                mode: CryptoJS.mode.ECB,
                padding: CryptoJS.pad.Pkcs7,
                length: 128
            }).toString();
            var timestamp = new Date().getTime();
            var sign = CryptoJS.SHA1(encryptedData + timestamp + "QGZUanpSaSy9DEPQFVULJQ==").toString();
            $.ajax({
                url: "/user/login",
                contentType: "application/json",
                type: "post",
                data: JSON.stringify({"sign": sign, "encryptedData": encryptedData, "timestamp": timestamp}),
                dataType: "json",
                success: function (data) {
                    document.cookie = "authToken=" + data.data.authToken;
                }
            })
        } else {
            alert("用户名或密码不能为空");
        }
    });
});
2)解密实现

首先创建一个类用于接收前端传过来的加密请求。

@Data
public class EncryptedReq<T> {
    /** 签名 */
    @NotBlank(message = "用户签名不能为空")
    private String sign;
    /** 加密请求数据 */
    @NotBlank(message = "加密请求不能为空")
    private String encryptedData;
    /** 原始请求数据(解密后回填到对象) */
    private T data;
    /** 请求的时间戳 */
    @NotNull(message = "时间戳不能为空")
    private Long timestamp;
}

这里将使用AOP在切面中进行解密的操作,首先创建注解,在接收加密请求的接口方法上添加该注解,然后对该方法的EncryptedReq参数进行验签及解密。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptAndVerify {
    /** 解密后的参数类型 */
    Class<?> decryptedClass();
}

AOP代码如下,具体步骤就是参数校验,验证及解密,数据回填。

@Slf4j
@Aspect
@Component
public class DecryptAndVerifyAspect {
    @Pointcut("@annotation(com.dfy.auth.annotation.DecryptAndVerify)")
    public void pointCut() {}
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) {
            throw new DecryptAndVerifyException(joinPoint.getSignature().getName() + ",参数为空");
        }
        EncryptedReq encryptedReq = null;
        for (Object obj : args) {
            if (obj instanceof EncryptedReq) {
                encryptedReq = (EncryptedReq) obj;
                break;
            }
        }
        if (encryptedReq == null) {
            throw new DecryptAndVerifyException(joinPoint.getSignature().getName() + ",参数中无待解密类");
        }
        String decryptedData = decryptAndVerify(encryptedReq);
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        DecryptAndVerify annotation = methodSignature.getMethod().getAnnotation(DecryptAndVerify.class);
        if (annotation == null || annotation.decryptedClass() == null) {
            throw new DecryptAndVerifyException(joinPoint.getSignature().getName() + ",未指定解密类型");
        }
        encryptedReq.setData(JSON.parseObject(decryptedData, annotation.decryptedClass()));
        return joinPoint.proceed();
    }

    private String decryptAndVerify(EncryptedReq encryptedReq) {
        String sign = CodecUtil.sha1Encrypt(encryptedReq.getEncryptedData() + encryptedReq.getTimestamp());
        if (sign.equals(encryptedReq.getSign())) {
            return CodecUtil.aesDecrypt(encryptedReq.getEncryptedData());
        } else {
            throw new DecryptAndVerifyException("验签失败:" + JSON.toJSONString(encryptedReq));
        }
    }
}

最后写一个接口进行测试

@PostMapping("/login")
@ResponseBody
@DecryptAndVerify(decryptedClass = UserLoginReq.class)
public ResponseVo login(@RequestBody @Validated EncryptedReq<UserLoginReq> encryptedReq, HttpServletRequest request) {
    UserLoginReq userLoginReq = encryptedReq.getData();
    // TODO 从数据库核实用户登录信息,这里懒得查数据库了
    if (userLoginReq.getUsername().equals("admin") && userLoginReq.getPassword().equals("admin")) {
        request.getSession().setAttribute("username", userLoginReq.getUsername());
        return ResponseVo.getSuccess();
    } else {
        return ResponseVo.getFailure(ResponseStatusEnum.USER_AUTH_FAILURE);
    }
}

访问localhost:8080/user/login,传入加密数据,即可获得正确的响应(加密过程工具类有,这里不展示了)

{"encryptedData":"AN8LpQrOTFEFi8l4MQYyYriUDsKTwLhWtkaI9q6Ck/zjlm1PY/5rQObOeOAFBipY","sign":"ba8dac258b7802b9a407911524ba6f8448e8ea25","timestamp":1585702530560}
3)请求拦截

这里从上往下开始介绍,先介绍配置类。
这里配置了拦截所有路径,然后添加了三个启动参数,用于在拦截器中获取并分别进行处理。

@Configuration
public class MainConfig {
    /** 不作拦截的URL路径 */
    private String excludedURLPaths = "/index,/user/login,/user/register,/**/*.jpg,/**/*.css,/test/**";
    private String logoutURL = "/user/logout";
    private String loginURI = "/user/login";
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setFilter(new AuthFilter());
        filterRegistrationBean.addInitParameter(AuthConstants.EXCLUDED_URI_PATHS, excludedURLPaths);
        filterRegistrationBean.addInitParameter(AuthConstants.LOGOUT_URI, logoutURL);
        filterRegistrationBean.addInitParameter(AuthConstants.LOGIN_URI, loginURI);
        return filterRegistrationBean;
    }
}

接下来介绍拦截器,具体逻辑就是判断请求是否需要拦截,如果需要判断用户是否登录,如果已登录判断是否为登出

public class AuthFilter implements Filter {
    private static String loginURI;
    private static String logoutURI;
    /** 用于识别出不需要拦截的URI */
    private static ExcludedURIUtil excludedURIUtil;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String[] excludedURLPaths = filterConfig.getInitParameter(AuthConstants.EXCLUDED_URI_PATHS).split(",");
        excludedURIUtil = ExcludedURIUtil.getInstance(excludedURLPaths);
        logoutURI = filterConfig.getInitParameter(AuthConstants.LOGOUT_URI);
        loginURI = filterConfig.getInitParameter(AuthConstants.LOGIN_URI);
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String requestURI = request.getRequestURI();
        if (excludedURIUtil.match(requestURI)) { // 如果请求URI不需要进行拦截
            filterChain.doFilter(request, response);
            return;
        }
        if (!UserLoginUtil.verify(request)) { // 如果用户未登录
            response.sendRedirect(loginURI);
        } else {
            if (requestURI.equals(logoutURI)) { // 用户登出时删除相关数据
                UserLoginUtil.logout(request);
            }
            filterChain.doFilter(request, response);
        }
    }
}

判断URI是否需要拦截这里使用的是正则表达式,将不需要拦截的URI转换为正则存起来,之后直接用请求的URI来匹配 (这里的正则也是现学现用,百度了半天写出来的,如果有更好的,可替代)

public class ExcludedURIUtil {
    /** 单例 */
    private static ExcludedURIUtil excludedUriUtil;
    /** uri、正则表达式映射表 */
    private static Map<String, String> uriRegexMap = new HashMap<String, String>();
    private ExcludedURIUtil() {}
    public static ExcludedURIUtil getInstance(String[] uris) {
        if (excludedUriUtil == null) {
            synchronized (ExcludedURIUtil.class) {
                if (excludedUriUtil == null) {
                    excludedUriUtil = new ExcludedURIUtil();
                    if (uris != null && uris.length > 0) {
                        for (String uri : uris) {
                            String regex = uri2Regex(uri);
                            uriRegexMap.put(uri, regex);
                        }
                    }
                }
            }
        }
        return excludedUriUtil;
    }
    /**
     * 判断给定uri是否匹配映射表中的正则表达式
     */
    public boolean match(String uri) {
        for (String regex : uriRegexMap.values()) {
            if (uri.matches(regex)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 将URI转换为正则表达式
     */
    public static String uri2Regex(String uri) {
        int lastPointIndex = uri.lastIndexOf('.');
        char[] uriChars = uri.toCharArray();
        StringBuilder regexBuilder = new StringBuilder();
        for (int i = 0, length = uriChars.length; i < length; i++) {
            if (uriChars[i] == '*' && uriChars[i + 1] == '*') {
                regexBuilder.append("(/[^/]*)*");
                i++;
            } else if (uriChars[i] == '*') {
                regexBuilder.append("/[^/]*");
            } else if (uriChars[i] == '.' && i == lastPointIndex) {
                regexBuilder.append("\\.");
                regexBuilder.append(uri.substring(i + 1));
                break;
            } else if (uriChars[i] == '/') {
                if (!uri.substring(i + 1, i + 2).equals("*")) {
                    regexBuilder.append("/");
                }
            } else {
                regexBuilder.append(uriChars[i]);
            }
        }
        return regexBuilder.toString();
    }
}

用户登录认证逻辑具体步骤归纳为:用户登录时认证信息正确则生成一个authToken返回,前端保存到cookie,当请求需要认证时从cookie中获取,没有再从header中获取,再判读authToken有效性。

public class UserLoginUtil {
    /** 用户名,token缓存映射,有效时间2小时 */
    private static Cache<String,String> usernameAuthTokenCache = CacheBuilder.newBuilder()
            .expireAfterWrite(2, TimeUnit.HOURS)
            .build();
    /**
     * 为保证token的唯一性,这里使用用户名+UUID+时间戳,最后通过SHA1算法进行加密生成唯一token
     */
    public static String generate(String username) {
        StringBuilder sb = new StringBuilder();
        sb.append(username)
                .append(UUID.randomUUID().toString().replaceAll("-", ""))
                .append(System.currentTimeMillis());
        String authToken = CodecUtil.sha1Encrypt(sb.toString());
        usernameAuthTokenCache.put(username, authToken);
        return authToken;
    }
    /**
     * 验证用户token是否存在或是否过期
     */
    public static boolean isValid(String username, String authToken) {
        if (username == null || authToken == null) return false;
        String findAuthToken = usernameAuthTokenCache.getIfPresent(username);
        return findAuthToken != null && authToken.equals(findAuthToken);
    }
    /**
     * 验证用户当前登录是否登录(先从cookie中获取,再从header中获取)
     */
    public static boolean verify(HttpServletRequest request) {
        String username = (String) request.getSession().getAttribute("username");
        for (Cookie cookie : request.getCookies()) {
            if (cookie.getName().equals("authToken") && isValid(username, cookie.getValue())) {
                return true;
            }
        }
        String findAuthToken = request.getHeader("authToken");
        if (isValid(username, findAuthToken)) {
            return true;
        }
        return false;
    }
    /**
     * 用户登出时删除缓存中的数据
     */
    public static void logout(HttpServletRequest request) {
        String username = (String) request.getSession().getAttribute("username");
        usernameAuthTokenCache.invalidate(username);
    }
}

最后修改我们的接口进行测试,如果我们直接访问/user/info则跳转登录页面,只有访问/user/login post成功登录后才能访问需认证的页面。

@Controller
@RequestMapping("/user")
public class UserController {
    @GetMapping("/login")
    public String login() {
        return "/login";
    }
    @PostMapping("/login")
    @ResponseBody
    @DecryptAndVerify(decryptedClass = UserLoginReq.class)
    public ResponseVo login(@RequestBody @Validated EncryptedReq<UserLoginReq> encryptedReq, HttpServletRequest request) {
        System.out.println(request.getRequestURI());
        UserLoginReq userLoginReq = encryptedReq.getData();
        // TODO 从数据库核实用户登录信息,这里懒得查数据库了
        if (userLoginReq.getUsername().equals("admin") && userLoginReq.getPassword().equals("admin")) {
            request.getSession().setAttribute("username", userLoginReq.getUsername());
            String authToken = UserLoginUtil.generate(userLoginReq.getUsername());
            return ResponseVo.getSuccess(new UserLoginRes(authToken));
        } else {
            return ResponseVo.getFailure(ResponseStatusEnum.USER_AUTH_FAILURE);
        }
    }
    @GetMapping("/info")
    @ResponseBody
    public ResponseVo info() {
        return ResponseVo.getSuccess("用户信息");
    }
}

详情可查看github:https://github.com/dfyang55/auth

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值