17-第16课:Spring Cloud 实例详解——基础框架搭建(三)
本文我们将集成 Redis,实现 API 鉴权机制。
Redis 的集成
Spring Boot 集成 Redis 相当简单,只需要在 pom 里加入如下依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
由于每个模块都可能用到 Redis,因此我们可以考虑将 Redis 的依赖放到 common 工程下
然后创建一个类实现基本的 Redis 操作:
@Component
public class Redis {
@Autowired
private StringRedisTemplate template;
// expire为过期时间,秒为单位
public void set(String key, String value, long expire) {
template.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
public void set(String key, String value) { template.opsForValue().set(key, value); }
public Object get(String key) { return template.opsForValue().get(key); }
public void delete(String key) { template.delete(key); }
}
如果具体的模块需要操作 Redis 还需要在配置文件配置 Redis 的连接信息,这里我们在 Git 仓库创建一个 yaml 文件 redis.yaml,并加入以下内容:
spring:
redis:
host: localhost
port: 6379
password:
最后在需要操作 Redis 的工程的 bootstrap.yml 文件中加上 Redis 配置文件名即可,如下:
spring:
cloud:
config:
# 这里加入 redis
name: user,feign,database,redis,key
label: master
discovery:
enabled: true
serviceId: config
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8888/eureka/
这样在工程想操作 Redis 的地方注入 Redis 类:
@Autowired
private Redis redis;
但这样启动工程会报错,原因是 CommonScan 默认从工程根目录开始扫描,我们工程的根包名是:com.lynn.xxx(其中 xxx 为工程名),而 Redis 类在 com.lynn.common 下,因此我们需要手动指定开始扫描的包名,我们发现二者都有 com.lynn,所以指定为 comm.lynn 即可。
在每个工程的 Application 类加入如下注解:
@SpringCloudApplication
@ComponentScan(basePackages = "com.lynn")
@EnableHystrixDashboard
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
API 鉴权
互联网发展至今,已由传统的前后端统一架构演变为如今的前后端分离架构,最初的前端网页大多由 JSP、ASP、PHP 等动态网页技术生成,前后端十分耦合,也不利于扩展。现在的前端分支很多,如 Web 前端、Android 端、iOS 端,甚至还有物联网等。前后端分离的好处就是后端只需要实现一套界面,所有前端即可通用。
前后端通过 HTTP 进行传输,也带来了一些安全问题,如抓包、模拟请求、洪水攻击、参数劫持、网络爬虫等。如何对非法请求进行有效拦截,保护合法请求的权益是这篇文章需要讨论的。
我依据多年互联网后端开发经验,总结出了以下提升网络安全的方式:
- 采用 HTTPS 协议;
- 密钥存储到服务端而非客户端,客户端应从服务端动态获取密钥;
- 请求隐私接口,利用 Token 机制校验其合法性;
- 对请求参数进行合法性校验;
- 对请求参数进行签名认证,防止参数被篡改;
- 对输入输出参数进行加密,客户端加密输入参数,服务端加密输出参数。
接下来,将对以上方式展开做详细说明。
HTTP VS HTTPS
普通的 HTTP 协议是以明文形式进行传输,不提供任何方式的数据加密,很容易解读传输报文。而 HTTPS 协议在 HTTP 基础上加入了 SSL 层,而 SSL 层通过证书来验证服务器的身份,并为浏览器和服务器之间的通信加密,保护了传输过程中的数据安全。
动态密钥的获取
对于可逆加密算法,是需要通过密钥进行加解密,如果直接放到客户端,那么很容易反编译后拿到密钥,这是相当不安全的做法,因此考虑将密钥放到服务端,由服务端提供接口,让客户端动态获取密钥,具体做法如下:
- 客户端先通过 RSA 算法生成一套客户端的公私钥对(clientPublicKey 和 clientPrivateKey);
- 调用 getRSA 接口,服务端会返回 serverPublicKey;
- 客户端拿到 serverPublicKey 后,用 serverPublicKey 作为公钥,clientPublicKey 作为明文对 clientPublicKey 进行 RSA 加密,调用 getKey 接口,将加密后的 clientPublicKey 传给服务端,服务端接收到请求后会传给客户端 RSA 加密后的密钥;
- 客户端拿到后以 clientPrivateKey 为私钥对其解密,得到最终的密钥,此流程结束。
注: 上述提到数据均不能保存到文件里,必须保存到内存中,因为只有保存到内存中,黑客才拿不到这些核心数据,所以每次使用获取的密钥前先判断内存中的密钥是否存在,不存在,则需要获取。
为了便于理解,我画了一个简单的流程图:

- 客户端启动,发送请求到服务端,服务端用RSA算法生成一对公钥和私钥,我们简称为pubkey1,prikey1,将公钥pubkey1返回给客户端。
- 客户端拿到服务端返回的公钥pubkey1后,自己用RSA算法生成一对公钥和私钥,我们简称为pubkey2,prikey2,并将公钥pubkey2通过公钥pubkey1加密,加密之后传输给服务端。
- 此时服务端收到客户端传输的密文,用私钥prikey1进行解密,因为数据是用公钥pubkey1加密的,通过解密就可以得到客户端生成的公钥pubkey2;
然后自己在生成对称加密,也就是我们的AES,其实也就是相对于我们配置中的那个16的长度的加密key,生成了这个key之后我们就用公钥pubkey2进行加密,返回给客户端,因为只有客户端有pubkey2对应的私钥prikey2。 - 只有客户端才能解密,客户端得到数据之后,用prikey2进行解密操作,得到AES的加密key,最后就用加密key进行数据传输的加密,至此整个流程结束。
接口请求的合法性校验
对于一些隐私接口(即必须要登录才能调用的接口),我们需要校验其合法性,即只有登录用户才能成功调用,具体思路如下:
- 调用登录或注册接口成功后,服务端会返回 Token(设置较短有效时间)和 refreshToken(设定较长有效时间);
- 隐私接口每次请求接口在请求头带上 Token,如 header(“token”, token),若服务端 返回 403 错误,则调用 refreshToken 接口获取新的 Token 重新调用接口,若 refreshToken 接口继续返回 403,则跳转到登录界面。
这种算法较为简单,这里就不写出具体实现了。
输入参数的合法性校验
一般情况下,客户端会进行参数的合法性校验,这个只是为了减轻服务端的压力,针对于普通用户做的校验,如果黑客通过直接调用接口地址,就可绕过客户端的校验,这时要求我们服务端也应该做同样的校验。
SpringMVC 提供了专门用于校验的注解,我们通过 AOP 即可实现统一的参数校验,下面请看代码:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Slf4j
@Aspect
@Component
public class WebExceptionAspect {
//凡是注解了RequestMapping的方法都被拦截
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void webPointcut() {
}
/**
* 拦截 web 层异常,记录异常日志,并返回友好信息到前端 目前只拦截 Exception,是否要拦截 Error 需再做考虑
* @param e 异常对象
*/
@AfterThrowing(pointcut = "webPointcut()", throwing = "e")
public void handleThrowing(Exception e) {
e.printStackTrace();
logger.error("发现异常!" + e.getMessage());
logger.error(JSON.toJSONString(e.getStackTrace()));
try {
if(StringUtils.isEmpty(e.getMessage())){
writeContent(JSON.toJSONString(SingleResult.buildFailure()));
} else {
writeContent(JSON.toJSONString(SingleResult.buildFailure(Code.ERROR,e.getMessage())));
}
} catch (Exception ex) { ex.printStackTrace(); }
}
/**
* 将内容输出到浏览器
* @param content 输出内容
*/
private void writeContent(String content)throws Exception {
HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
response.reset();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/plain;charset=UTF-8");
response.setHeader("icop-content-type", "exception");
response.getWriter().print(content);
response.getWriter().close();
}
}
在 Controller 提供共有方法:接口输入参数合法性校验
protected void validate(BindingResult result) {
if (result.hasFieldErrors()) {
List<FieldError> errorList = result.getFieldErrors();
errorList.stream().forEach(item -> Assert.isTrue(false, item.getDefaultMessage()));
}
}
每个接口的输入参数都需要加上 @Valid
注解,并且在参数后面加上 BindResult 类:
@RequestMapping(value = "/hello",method = RequestMethod.POST)
public SingleResult<String> hello(@Valid @RequestBody TestRequest request, BindingResult result) {
validate(result);
return "name="+name;
}
@Data
public class TestRequest {
@NotNull(message = "name不能为空")
private String name;
}
输入参数签名认证
我们请求的接口是通过 HTTP/HTTPS 传输的,一旦参数被拦截,很有可能被黑客篡改,并传回给服务端,为了防止这种情况发生,我们需要对参数进行签名认证,保证传回的参数是合法性,具体思路如下。
请求接口前,将 Token、Timstamp 和接口需要的参数按照 ASCII 升序排列,拼接成 url=key1=value1&key2=value2
,如 name=xxx×tamp=xxx&token=xxx
,进行 MD5(url+salt),得到 Signature,将 Token、Signature、Timestamp 放到请求头传给服务端,如 header(“token”, token)、header(“timestamp”, timestamp)、header(“signature”, signature)。
注: salt 即为动态获取的密钥。
下面请看具体的实现,应该在拦截器里统一处理:
public class ApiInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ApiInterceptor.class);
private String salt="ed4ffcd453efab32";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
logger.info("进入拦截器");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "application/json;charset=utf8");
StringBuilder urlBuilder = getUrlAuthenticationApi(request); // 这里是MD5加密算法
String sign = MD5(urlBuilder.toString() + salt);
String signature = request.getHeader("signature");
logger.info("加密前传入的签名" + signature);
logger.info("后端加密后的签名" + sign);
if(sign.equals(signature)) {
return true;
} else { //签名错误
response.getWriter().print("签名错误");
response.getWriter().close();
return false;
}
}
private StringBuilder getUrlAuthenticationApi(HttpServletRequest request) {
Enumeration<String> paramesNames = request.getParameterNames();
List<String> nameList = new ArrayList<>();
nameList.add("token");
nameList.add("timestamp");
while (paramesNames.hasMoreElements()) {
nameList.add(paramesNames.nextElement());
}
StringBuilder urlBuilder = new StringBuilder();
nameList.stream().sorted().forEach(name -> {
if ("token".equals(name) || "timestamp".equals(name)){
if("token".equals(name) && null ==request.getHeader(name)){
return;
}
urlBuilder.append('&');
urlBuilder.append(name).append('=').append(request.getHeader(name));
} else {
urlBuilder.append('&');
urlBuilder.append(name).append('=').append(request.getParameter(name));
}
});
urlBuilder.deleteCharAt(0);
logger.info("url : " + urlBuilder.toString());
return urlBuilder;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { }
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception { }
}
输入输出参数加密
为了保护数据,比如说防爬虫,需要对输入输出参数进行加密,客户端加密输入参数传回服务端,服务端解密输入参数执行请求;服务端返回数据时对其加密,客户端拿到数据后解密数据,获取最终的数据。这样,即便别人知道了参数地址,也无法模拟请求数据。
至此,基础框架就已经搭建完成,下篇我们将开始实现具体的需求。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 tuyrk@qq.com
文章标题:17-第16课:Spring Cloud 实例详解——基础框架搭建(三)
文章字数:2.8k
本文作者:神秘的小岛岛
发布时间:2020-07-06, 16:00:24
最后更新:2020-07-14, 23:05:24
原始链接:https://www.tuyrk.cn/gitchat/springcloud-quickly/17-framework-build-03/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。