最近在做一个项目,需要集成智能设备,一些操作智能设备的业务逻辑是通用的,所以独立了一个API子工程出来,这样容易扩展、容易维护,但是安全性上就需要多考虑一些了。基于此,我制定了一个接口调用的安全方案如下。

方案

API接口安全性实践_System


1、发送请求之前,先从本地缓存拿到token。如果token能拿到,则进行步骤6,拿不到进行步骤2。

2、申请token,携带client_id和client_secret参数。

3、验证身份,接口系统根据client_id和client_secret鉴别终端身份,身份识别成功签发token,时效15天;身份识别失败无法继续下一步。

4、生成token,生成access_token并使用redis进行记录,时效15天。

5、存储token,终端使用本地缓存存储token。

6、写入header,发送请求时在header中使用Authorization->Bearer token方式携带token。

7、验证token,token验证通过后调用接口逻辑。

8、接口逻辑,执行完后返回应答信息。

client_id和client_secret是终端在API接口系统中的身份标识,需要提前获取到。

token工具类

  • JJWTUtils
public class JJWTUtils {

	public static final String JJWT_SERCETKEY = "china_shandong";
	public static final String JJWT_AES = "AES"; // 加密标准
	public static final String JJWT_TOKEN = "token";
	public static final String JJWT_EXPIRATION = "expiration";
	/** 单位: 秒 */
	/** token缓存时效:基准单位时间+延长时效 */
	/** token过期时间(基准单位时间)*/
	public static final Integer JWT_TOKEN_TIMEOUT = 60*30;
	/** WEB端token失效的过渡期(延长时效) */
	public static final Integer JWT_WEB_EXPIRE_INTERIM_PERIOD = 60*3;
	/** 手机端token失效的过渡期(延长时效)【当前默认为7天,该常量可根据业务场景进行调整,建议最长设置为30天】 */
	public static final Integer JWT_MOBILE_EXPIRE_INTERIM_PERIOD = 60*60*24*7;
	/** token刷新的临界点 */
	public static final Integer JWT_REFRESH_DIVIDING_POINT = 60*5;
	/** 白名单超时时间 */
	public static final Integer JWT_TOKEN_BLACKLIST_TIMEOUT = 30;
	/** 客户端标识 */
	public static final String HTTP_HEADER_BEARER = "Bearer ";

	/**
	 * 生成SecretKey
	 * @param secret
	 * @return
	 */
	private static SecretKey generateKey(String secret) {
		byte[] encodedKey = Base64.decodeBase64(secret);
		return new SecretKeySpec(encodedKey, 0, encodedKey.length, JJWT_AES);
	}

	/**
	 * 新生成token
	 *
	 * @param clientId
	 * @param exp
	 * @return
	 * @throws JsonGenerationException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public static String createToken(String clientId, Long exp) throws JsonGenerationException, JsonMappingException, IOException {
		Map<String, String> tokenMap = new HashMap<String, String>();
		Claims claims = new DefaultClaims();
		JwtClient jwtClient = new JwtClient();
		if (!StringUtils.isEmpty(clientId)) {
			jwtClient.setMobile(clientId);

			long expVal = 0;
			if (exp != null) {
				// milliseconds是毫秒  1000毫秒=1秒
				expVal = System.currentTimeMillis() + exp*1000;
			} else {
				expVal = System.currentTimeMillis() + JWT_TOKEN_TIMEOUT*1000;
			}
			jwtClient.setExp(String.valueOf(expVal));

			claims.setExpiration(new Date(expVal));

			try {
				claims.setSubject(JSON.marshal(jwtClient));
			} catch (Exception e) {
				e.printStackTrace();
			}

			String compactJws = Jwts.builder()
					.setClaims(claims)
					.signWith(SignatureAlgorithm.HS256, generateKey(JJWT_SERCETKEY))
					.compact();
			tokenMap.put(JJWT_TOKEN, compactJws);
			tokenMap.put(JJWT_EXPIRATION, String.valueOf(claims.getExpiration().getTime()));
			try {
				return JSON.marshal(tokenMap);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return null;
	}

	/**
	 * 解析token
	 * @param token
	 * @return
	 * @throws Exception
	 */
	public static Claims parseJWT(String token) throws ExpiredJwtException {
		Claims claims = Jwts.parser()
				.setSigningKey(generateKey(JJWT_SERCETKEY))
				.parseClaimsJws(token).getBody();
		return claims;
	}

	/**
	 * 根据token获取username
	 * @param token
	 * @return
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public static String getClientIdByToken(String token) {
		Claims claims = parseJWT(token);
		if (claims != null) {
			JwtClient jwtClient = null;
			try {
				jwtClient = JSON.unmarshal(claims.getSubject(), JwtClient.class);
			} catch (Exception e) {
				e.printStackTrace();
			}
			return jwtClient.getMobile();
		}

		return null;
	}

	public static JwtClient getJwtClientByToken(String token) {
		Claims claims = parseJWT(token);
		if (claims != null) {
			JwtClient jwtClient = null;
			try {
				jwtClient = JSON.unmarshal(claims.getSubject(), JwtClient.class);
			} catch (Exception e) {
				e.printStackTrace();
			}
			return jwtClient;
		}

		return null;
	}

	/**
	 * 验证token
	 * @param token
	 * @return
	 */
	public static boolean validateToken(String token) {
		try {
			Jwts.parser()
					.setSigningKey(generateKey(JJWT_SERCETKEY))
					.parseClaimsJws(token);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * 生成Claims
	 *
	 * @param clientId
	 * @param exp
	 * @return
	 */
	public static Claims getClaims(String clientId, Long exp) {
		Claims claims = new DefaultClaims();
		JwtClient jwtClient = new JwtClient();
		if (!StringUtils.isEmpty(clientId)) {
			jwtClient.setMobile(clientId);
			try {
				claims.setSubject(JSON.marshal(jwtClient));
			} catch (Exception e) {
				e.printStackTrace();
			}
			if (exp != null) {
				claims.setExpiration(new Date(System.currentTimeMillis() + exp*1000));
			} else {
				claims.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_TIMEOUT*1000));
			}
			return claims;
		}
		return null;
	}

	/**
	 * 封装JWT 的标准字段
	 *   iss
	 *   sub
	 *   aud
	 *   exp
	 *   nbf
	 *   iat
	 *   jti
	 * @param clientId
	 * @param exp milliseconds(1000ms=1s) default 1000*60*10(10分钟)
	 * @return
	 */
	private Map<String, Object> getClaimsMap(String clientId, Long exp) {
		Map<String, Object> claimsMap = new HashMap<String, Object>();
		JwtClient jwtClient = new JwtClient();
		if (!StringUtils.isEmpty(clientId)) {
			jwtClient.setMobile(clientId);
			try {
				claimsMap.put("sub", JSON.marshal(jwtClient));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		if (exp != null) {
			claimsMap.put("exp", new Date().getTime() + exp*1000);
		} else {
			claimsMap.put("exp", new Date().getTime() + 60*10*1000);
		}
		return claimsMap;
	}

	/**
	 * 去掉token中的 'Bearer '
	 * @param token
	 * @return
	 */
	public static String removeBearerFromToken(String token) {
		if (token != null) {
			return token.contains(HTTP_HEADER_BEARER) ?
					StringUtils.removeStart(token, HTTP_HEADER_BEARER) : token;
		}
		return null;
	}

	/**
	 * 判断token 是否已经超时(已废弃请使用 {@link #isRefresh(String)})
	 * @param token
	 * @return
	 */
	@Deprecated
	public static boolean isExpireTime(String token) {
		Claims claims = JJWTUtils.parseJWT(token);
		LocalDateTime expTime = LocalDateTime.ofInstant(claims.getExpiration().toInstant(), ZoneId.systemDefault());
		return LocalDateTime.now().isAfter(expTime);
	}

	/**
	 * 判断是否需要刷新token
	 *
	 * @param token
	 * @return
	 */
	public static boolean isRefresh(String token) {
		try {
			Claims claims = JJWTUtils.parseJWT(token);
			LocalDateTime expTime = LocalDateTime.ofInstant(claims.getExpiration().toInstant(), ZoneId.systemDefault());
			return LocalDateTime.now().isAfter(expTime);
		} catch (ExpiredJwtException e) {
			return true;
		}
	}
	
	public static String generateToken(String appid) throws Exception {
		String tokenJson = createToken(appid,60 * 60 * 24 * 365 * 100L);
		@SuppressWarnings("unchecked")
		Map<String, String> tokenMap = JSON.unmarshal(tokenJson, Map.class);
		String token = tokenMap.get("token");
		return token;
	}
}
  • JSON
public class JSON {
    public static final String DEFAULT_FAIL = "\"Parse failed\"";
    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();

    public static void marshal(File file, Object value) throws Exception
    {
        try
        {
            objectWriter.writeValue(file, value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static void marshal(OutputStream os, Object value) throws Exception
    {
        try
        {
            objectWriter.writeValue(os, value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static String marshal(Object value) throws Exception
    {
        try
        {
            return objectWriter.writeValueAsString(value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static byte[] marshalBytes(Object value) throws Exception
    {
        try
        {
            return objectWriter.writeValueAsBytes(value);
        }
        catch (JsonGenerationException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(File file, Class<T> valueType) throws Exception
    {
        try
        {
            return objectMapper.readValue(file, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(InputStream is, Class<T> valueType) throws Exception
    {
        try
        {
            return objectMapper.readValue(is, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(String str, Class<T> valueType) throws Exception
    {
        try
        {
            return objectMapper.readValue(str, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    public static <T> T unmarshal(byte[] bytes, Class<T> valueType) throws Exception
    {
        try
        {
            if (bytes == null)
            {
                bytes = new byte[0];
            }
            return objectMapper.readValue(bytes, 0, bytes.length, valueType);
        }
        catch (JsonParseException e)
        {
            throw new Exception(e);
        }
        catch (JsonMappingException e)
        {
            throw new Exception(e);
        }
        catch (IOException e)
        {
            throw new Exception(e);
        }
    }

    /**
     * 将Object转成json串
     * @param obj
     * @return
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonGenerationException
     */
    public static String objToJson(Object obj) throws JsonGenerationException, JsonMappingException, IOException{

        String objstr = objectMapper.writeValueAsString(obj) ;
        if(objstr.indexOf("'")!=-1){
            //将单引号转义一下,因为JSON串中的字符串类型可以单引号引起来的
            objstr = objstr.replaceAll("'", "\\'");
        }
        if(objstr.indexOf("\"")!=-1){
            //将双引号转义一下,因为JSON串中的字符串类型可以单引号引起来的
            objstr = objstr.replaceAll("\"", "\\\"");
        }

        if(objstr.indexOf("\r\n")!=-1){
            //将回车换行转换一下,因为JSON串中字符串不能出现显式的回车换行
            objstr = objstr.replaceAll("\r\n", "\\u000d\\u000a");
        }
        if(objstr.indexOf("\n")!=-1){
            //将换行转换一下,因为JSON串中字符串不能出现显式的换行
            objstr = objstr.replaceAll("\n", "\\u000a");
        }
        return objstr;
    }
}
  • JwtClient
public class JwtClient {

    private String mobile;

    private String exp;

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getExp() {
        return exp;
    }

    public void setExp(String exp) {
        this.exp = exp;
    }

    @Override
    public String toString() {
        return "JwtClient{" +
                "mobile='" + mobile + '\'' +
                ", exp='" + exp + '\'' +
                '}';
    }
}

过滤器

  • JwtFilter
public class JwtFilter implements Filter {

    protected Logger logger = LoggerFactory.getLogger(JwtFilter.class);

    /**
     * 排除链接
     */
    public List<String> excludes = new ArrayList<>();

    /**
     * jwt过滤开关
     */
    public boolean enabled = false;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        // 是否需要验证token
        if (handleExcludeURL(req, resp))
        {
            chain.doFilter(request, response);
            return;
        }
        // 验证token, true为通过
        if (checkToken(req, resp))
        {
            chain.doFilter(request, response);
            return;
        }

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        logger.info("jwtFilter init");
        String tempExcludes = filterConfig.getInitParameter("excludes");
        String tempEnabled = filterConfig.getInitParameter("enabled");
        if (StringUtils.isNotEmpty(tempExcludes))
        {
            String[] url = tempExcludes.split(",");
            for (int i = 0; url != null && i < url.length; i++)
            {
                excludes.add(url[i]);
            }
        }
        if (StringUtils.isNotEmpty(tempEnabled))
        {
            enabled = Boolean.valueOf(tempEnabled);
        }
    }

    @Override
    public void destroy() {
        logger.info("jwtFilter destroy");
    }

    /**
     * 当前访问接口是否排除在外
     * @param request
     * @param response
     * @return
     */
    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response)
    {
        if (!enabled)
        {
            return true;
        }
        if (excludes == null || excludes.isEmpty())
        {
            return false;
        }
        String url = request.getServletPath();
        for (String pattern : excludes)
        {
            Pattern p = Pattern.compile("^" + pattern);
            Matcher m = p.matcher(url);
            if (m.find())
            {
                return true;
            }
        }
        return false;
    }

    private boolean checkToken(HttpServletRequest request, HttpServletResponse response) {
        logger.error("call checkToken start");
        String jwtToken = request.getHeader("Authorization");
        if (jwtToken == null || "".equals(jwtToken)) {
            logger.error("请求头部未携带token数据");
            return false;
        }

        String token = JJWTUtils.removeBearerFromToken(jwtToken);

        String mobile = JJWTUtils.getClientIdByToken(token);

        if (StringUtils.isNotEmpty(mobile)) {
            logger.info("mobile: [{}]", mobile);
            request.setAttribute("u_mobile", mobile);
            return true;
        }
        return false;
    }

}

自动装配

  • FilterConfig
@Configuration
public class FilterConfig {

    @Value("${jwt.enabled}")
    private String enabled;

    @Value("${jwt.excludes}")
    private String excludes;

    @Value("${jwt.urlPatterns}")
    private String urlPatterns;

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean jwtFilterRegistration()
    {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setFilter(new JwtFilter());
        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
        registration.setName("jwtFilter");
        registration.setOrder(Integer.MAX_VALUE);
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("excludes", excludes);
        initParameters.put("enabled", enabled);
        registration.setInitParameters(initParameters);
        return registration;
    }

}
  • yml文件配置项
# token解析
jwt:
  # 过滤开关
  enabled: true
  # 排除链接(多个用逗号分隔)
  excludes: /appLogin
  # 匹配链接
  urlPatterns: /system/*,/user/*

应答数据结构

响应体数据结构约定:

{
    “code”: 0,
    “msg”:”描述信息”,
    “data”:{
    
    }
}

实践

只有返回应答body中code为0时,说明接口操作成功。否则操作视为失败。