WebMVC+JWT实现API接口鉴权

为什么需要接口(API)鉴权

从API接口安全性设计考虑
防止API接口被他人随意调用
保障接口数据安全,通过接口鉴权来限制调用方的操作权限

接口权限范围

管理员鉴权

指具备管理员权限的用户调用接口时不限制操作范围。

一般鉴权

指不具备管理员权限的用户进行接口调用时限制其操作范围。

举例说明

用户信息查询接口:/user/info/{id}
管理员用户调用时允许查看任意人员的信息(管理员鉴权)
非管理员用户仅能查看其自身的信息(一般鉴权)

如何实现接口(API)鉴权

实现方式

通过Jwt Token令牌来实现API接口鉴权

实现原理

给调用方分配账号、密码
调用方通过登录从服务器获取授权Token,返回的Token包含用户ID
客户端保存Token,后续的请求都携带此Token进行访问
接口服务端从请求头中获取授权Token,校验Token是否有效以及验证用户是否具备特定权限

实现步骤

自定义注解@ApiAdminToken,在需要鉴权的API方法上使用该注解并设定用户的权限范围。
通过拦截器(Interceptor)验证授权Token。
AOP切面验证用户接口调用的权限范围。

接口(API)鉴权实现源码(仅供参考)

代码不完整,无法直接使用,仅提供参考。

依赖导入

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.0</version>
</dependency>

拦截器

AccessTokenInterceptor

package com.platform.web.interceptor;

import com.alibaba.fastjson.JSON;
import com.platform.common.ResultObject;
import com.platform.common.constant.RequestConstant;
import com.platform.common.jwt.AccessTokenService;
import com.platform.common.jwt.JwtTokenUtil;
import com.platform.web.aop.ApiAdminToken;
import com.platform.web.util.IpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录Token拦截及自动刷新
 *
 * @author jiangqi
 * @date 2021-5-19
 */
@Component
public class AccessTokenInterceptor extends HandlerInterceptorAdapter {
    private static Logger log = LoggerFactory.getLogger(AccessTokenInterceptor.class);

    @Autowired
    private AccessTokenService accessTokenService;

    /**
     * 拦截器预处理
     * Response响应仅在此方法中修改才会生效
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 验证请求头中是否携带Access_Token,未携带视为未登录
        String userId = request.getHeader(RequestConstant.USER_ID);
        String accessToken = request.getHeader(RequestConstant.ACCESS_TOKEN);
        String tokenUserId = JwtTokenUtil.getTokenUserId(accessToken);

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            ApiAdminToken apiAdminToken = handlerMethod.getMethodAnnotation(ApiAdminToken.class);
            /*
             * 如果请求的方法上有 @ApiAdminToken 注解,
             * 并且开启了Admin-Token验证,
             * 并且请求头携带了Admin-Token,
             * 则直接放行。
             * */
            String adminToken = request.getHeader(RequestConstant.ADMIN_TOKEN);
            if (apiAdminToken != null && apiAdminToken.openValid() && !StringUtils.isEmpty(adminToken)) {
                return true;
            }
        }

        /*
         * 验证Access_Token是否合法
         * */
        log.info("IP({})请求验证用户({})Access-Token", IpUtil.getIpAddress(request), userId);
        boolean isCheckOk = accessTokenService.verifyToken(accessToken);
        if (!isCheckOk || StringUtils.isEmpty(userId) || !userId.equals(tokenUserId)) {
            response.setStatus(200);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(ResultObject.newTokenErrorMsg("您未登录,请先登录!")));
            response.getWriter().flush();
            response.getWriter().close();
            return false;
        }

        /*
         * 判断token是否已经过期
         * 如果过期,则刷新token 并 返回新的token
         * */
        if (JwtTokenUtil.isTokenExpire(accessToken)) {
            log.info("刷新用户({})Access-Token", userId);
            // 刷新Access-Token
            String newAccessToken = accessTokenService.refreshByUserId(userId);
            response.setHeader(RequestConstant.ACCESS_TOKEN, newAccessToken);
        }
        return true;
    }

    /**
     * 拦截器器的后处理
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           @Nullable ModelAndView modelAndView) throws Exception {

    }

    /**
     * 请求完毕后的回调方法
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }

}

AdminTokenInterceptor

package com.platform.web.interceptor;

import com.alibaba.fastjson.JSON;
import com.platform.common.ResultObject;
import com.platform.common.constant.RequestConstant;
import com.platform.common.jwt.AccessTokenService;
import com.platform.common.jwt.JwtTokenUtil;
import com.platform.web.util.IpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 管理员登录Token拦截及自动刷新
 *
 * @author jiangqi
 * @date 2021-5-19
 */
@Component
public class AdminTokenInterceptor extends HandlerInterceptorAdapter {
    private static Logger log = LoggerFactory.getLogger(AccessTokenInterceptor.class);

    @Autowired
    private AccessTokenService accessTokenService;

    /**
     * 拦截器预处理
     * Response响应仅在此方法中修改才会生效
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 验证请求头中是否携带Access_Token,未携带视为未登录
        String userId = request.getHeader(RequestConstant.USER_ID);
        String adminToken = request.getHeader(RequestConstant.ADMIN_TOKEN);
        String tokenUserId = JwtTokenUtil.getTokenUserId(adminToken);
        /*
         * 验证Admin_Token是否合法
         * */
        log.info("IP({})请求验证用户({})Admin-Token", IpUtil.getIpAddress(request), userId);
        boolean isCheckOk = accessTokenService.verifyToken(adminToken);
        if (!isCheckOk || StringUtils.isEmpty(userId) || !userId.equals(tokenUserId)) {
            response.setStatus(200);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(ResultObject.newTokenErrorMsg("您未登录,请先登录!")));
            response.getWriter().flush();
            response.getWriter().close();
            return false;
        }

        /*
         * 判断token是否已经过期
         * 如果过期,则刷新token 并 返回新的token
         * */
        if (JwtTokenUtil.isTokenExpire(adminToken)) {
            log.info("刷新用户({})Admin-Token", userId);
            // 刷新Access-Token
            String newAccessToken = accessTokenService.refreshByUserId(userId);
            response.setHeader(RequestConstant.ADMIN_TOKEN, newAccessToken);
        }
        return true;
    }

    /**
     * 拦截器器的后处理
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           @Nullable ModelAndView modelAndView) throws Exception {

    }

    /**
     * 请求完毕后的回调方法
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
}

自定义注解@ApiAdminToken

ApiAdminToken

package com.platform.web.aop;

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

/**
 * 标记需要管理员权限才能操作的接口
 * <p>
 * 或
 * 标记接口需要限制数据查看范围
 * <p>
 * openValid = false时,argName与requestParam不能为空
 * <p>
 * argName:用户ID的参数名,例如 id、uuid、userId等
 * <p>
 * <p>
 * requestParam = true(默认值),标识argName表示的参数在请求参数上
 * <p>
 * requestParam = false,标识argName表示的参数在请求路径上
 * <p>
 * <p>
 * openValid = true(默认值),开启Admin-Token验证,但不验证Access-Token(即限制该接口仅管理员能够调用)
 * <p>
 * openValid = false,验证argName表示的用户ID,需要验证Access-Token,
 * 且argName表示的用户ID需要与Access-Token中保存的用户ID一致(即限制该接口仅能查询到与调用者相关的信息)
 *
 * @author jiangqi
 * @date 2021-6-16
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiAdminToken {
    /**
     * openValid = false时不能为空
     * <p>
     * 用户ID的参数名,例如 id、uuid、userId等
     */
    String argName() default "";

    /**
     * openValid = false时不能为空
     * <p>
     * requestParam = true(默认值)时,标识argName表示的参数在请求参数上
     * <p>
     * requestParam = false时,标识argName表示的参数在请求路径上
     */
    boolean requestParam() default true;

    /**
     * openValid = true(默认值)并且 请求头携带Admin-Token,开启Admin-Token验证,但不验证Access-Token(即限制该接口仅管理员能够调用)
     * <p>
     * openValid = true(默认值) 并且 请求头未携带Admin-Token,视为 openValid = false
     * <p>
     * 当openValid = false时,argName与requestParam不能为空。
     * 不论请求头是否携带Admin-Token,不验证Admin-Token。验证Access-Token 和 argName表示的用户ID
     * 且 argName表示的用户ID需要与Access-Token中保存的用户ID一致(即限制该接口仅能查询到与调用者相关的信息)
     */
    boolean openValid() default true;
}

AOP切面实现

AdminTokenService

实际使用时需要重写 Boolean adminTokenValid(String adminToken) 方法。

package com.platform.data.api;

/**
 * Admin-Token验证用户身份
 *
 * @author jiangqi
 * @date 2021-7-2
 * */
public interface AdminTokenService {
    /**
     * 根据Admin-Token验证用户是否为管理员
     *
     * @param adminToken Token中包含用户Id
     * @return 是否管理员用户
     * */
    default Boolean adminTokenValid(String adminToken){
        return false;
    }
}

ApiAdminTokenAspect

package com.platform.data.api;

import com.platform.common.ServiceException;
import com.platform.common.constant.RequestConstant;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * 敏感数据Api接口鉴权切面
 *
 * @author jiangqi
 * @date 2021-3-8 14:13:03
 */
@Aspect
@Component
public class ApiAdminTokenAspect {
    private Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private AdminTokenService adminTokenService;

    @Pointcut("@annotation(com.platform.data.api.ApiAdminToken)")
    public void pointcut() {

    }

    /**
     * 前置处理
     */
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        // 前置通知抛出的异常不会终止方法的继续执行
    }

    /**
     * 环绕处理
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 环绕通知抛出的异常会终止方法的继续执行
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 获取管理员用户Admin-Token
        String adminToken = request.getHeader(RequestConstant.ADMIN_TOKEN);
        // 请求的方法参数值
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        ApiAdminToken apiAdminToken = method.getAnnotation(ApiAdminToken.class);
        if (apiAdminToken.openValid() && StringUtils.isNotEmpty(adminToken)) {
            // 限制管理员用户可调用
            Boolean isAdmin = adminTokenService.adminTokenValid(adminToken);
            if (!isAdmin) {
                throw new ServiceException("访问权限不足!");
            }
            return proceedingJoinPoint.proceed();
        }

        // 限制操作范围
        if (StringUtils.isEmpty(apiAdminToken.argName())) {
            throw new ServiceException("接口鉴权失败!argName不能为空");
        }
        String userIdParam;
        if (apiAdminToken.requestParam()) {
            // 从请求参数获取用户ID
            userIdParam = request.getParameter(apiAdminToken.argName());
        } else {
            // 从请求路径获取用户ID
            Map map = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            userIdParam = String.valueOf(map.get(apiAdminToken.argName()));
        }
        if (StringUtils.isEmpty(userIdParam)) {
            throw new ServiceException("接口鉴权失败!从" + (apiAdminToken.requestParam() ? "请求参数上" : "请求路径上") + "未获取到 " + apiAdminToken.argName());
        }
        // 限制操作范围,非管理员仅能查看自身数据
        String currentUserId = request.getHeader(RequestConstant.USER_ID);
        if (!currentUserId.equals(userIdParam)) {
            // 请求头的User-Id在拦截器中已与Access-Token进行过验证,是一致且安全的,可用作鉴权依据
            throw new ServiceException("访问权限不足!");
        }
        return proceedingJoinPoint.proceed();
    }

    /**
     * 正常返回处理
     */
    @AfterReturning(pointcut = "pointcut()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {

    }

    /**
     * 异常返回处理
     */
    @AfterThrowing(pointcut = "pointcut()", throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint, Exception exception) {

    }

}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值