AOP实现接口(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) {
}
}