最近学习了前后端分离的思想以及做了几个前后端分离的项目。
想拿出来跟大家分享分享。
今天想聊的是前后端分离是否真的安全或者说可能存在哪些漏洞以及前后端分离具体解决了那些个问题。
我们去渗透一个目标的站点的时候,是否看到类似于Node.js + vue + webpack 等等这一类型的站点。
如下图:
没错这就是典型的前后端分离。
前后端分离相当于说,前端写前端的 后端写后端的。咋们互不干扰。
我后端只需要给你提供接口,你前端拿数据即可。
以前的前端人员只写一些html css js等那些东西。
可能一些小公司后端把前端的活都给包了。
这就造成了后端的资源压力,前后端分离之后可以减少后端的资源压力,前后端因为使用 json 进行数据传输而为前后端分离测试提供了极大的方便。也就是我们常说的API接口。
那么这个接口规范我们怎么定义,比如说前端需要这个接口格式的数据:
{"id":"1","name":"admin","age":"admin"}
那么后端就需要提供返回的数据就是这样的接口格式的。
但是如果前端需要更改的话,那么后端也要接着更改。如果更改频繁的话,这样就造成了前端人员和后端人员交流很麻烦。
所以出现了swagger API文档工具。
大家可能在渗透中见到过这样的情况,这就是swagger文档管理工具。这里可以进行测试我们的api接口。
比如我们测试一个posttt,他会进行请求以及响应。
后端代码是这样的:
这里说一下如果设置enable为false的话,那么swagger他就不能访问了。
@Beanpublic Docket docket(Environment environment){//设置要显示的swagger环境Profiles profiles = Profiles.of("dwadwa","test");//获取项目的环境//通过environment.acceptsProfiles判断是否处在自己设定环境当中boolean flag = environment.acceptsProfiles(profiles);//return new Docket(DocumentationType.SWAGGER_2)//enable:是否启用swagger 如果为false 则swagger不能在浏览器中访问.apiInfo(apiInfo()).enable(true)//groupName 分组.groupName("张三")//RequestHandlerSelectors.basePackage指定要扫描的包//RequestHandlerSelectors.any() 扫描全部//RequestHandlerSelectors.none() 都不扫描//withClassAnnotation:扫描类上的注解 参数使用一个注解的反射对象//withMethodAnnotation:扫描方法上的注解.select().apis(RequestHandlerSelectors.basePackage("com.springboot.controller"))// .paths(PathSelectors.ant("/springboot")).build(); //build}
如下图,这样就无法访问了。
紧接着跟大家分享一个小案例,自我感觉挺抽象的。
我们都知道swagger是可以分组的。比如: 在这里是可以选择分组的。
但是我们会发现分组之后似乎enable失效了。
后端代码:这里我设置了几个分组。分别是A,B,C
package com.springboot.config;import com.springboot.controller.HelloController;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.core.env.Profiles;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;import java.util.ArrayList;import static springfox.documentation.service.ApiInfo.DEFAULT_CONTACT;@Configuration@EnableSwagger2 //开启swagger2public class SwaggerConfig {Contact contact = new Contact("张三", "https://www.baidu.com", "[email protected]");//配置了swagger的Docket的bean实例@Beanpublic Docket docket1(){return new Docket(DocumentationType.SWAGGER_2).groupName("A");}@Beanpublic Docket docket2(){return new Docket(DocumentationType.SWAGGER_2).groupName("B");}@Beanpublic Docket docket3(){return new Docket(DocumentationType.SWAGGER_2).groupName("C");}@Beanpublic Docket docket(Environment environment){//设置要显示的swagger环境Profiles profiles = Profiles.of("dwadwa","test");//获取项目的环境//通过environment.acceptsProfiles判断是否处在自己设定环境当中boolean flag = environment.acceptsProfiles(profiles);//return new Docket(DocumentationType.SWAGGER_2)//enable:是否启用swagger 如果为false 则swagger不能在浏览器中访问.apiInfo(apiInfo()).enable(false)//groupName 分组.groupName("张三")//RequestHandlerSelectors.basePackage指定要扫描的包//RequestHandlerSelectors.any() 扫描全部//RequestHandlerSelectors.none() 都不扫描//withClassAnnotation:扫描类上的注解 参数使用一个注解的反射对象//withMethodAnnotation:扫描方法上的注解.select().apis(RequestHandlerSelectors.basePackage("com.springboot.controller"))// .paths(PathSelectors.ant("/springboot")).build(); //build}//配置swagger信息 = apiInfoprivate ApiInfo apiInfo(){return new ApiInfo("张三 swaggerAPI文档", "作者", "v1.0", "http://localhost/userlist", contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList());}}
我们的环境是这样的。默认springboot的端口为8080,我们这里没有指定他的端口。也没有指定他用那一套环境。
但是我们去访问8080端口,访问swagger的时候,会发现依旧是可以访问的。这时我们已经在配置类中吧enable设置为了false,禁止访问的。但是依旧是可以访问的。
但是我们去访问其他端口的时候会发现他提示我们。这里的其他端口指的是我们其他配置文件设置的端口。会发现他会弹一个窗口。
那么我们在渗透过程中的话,是不是可以猜测访问其他的端口来进行访问swagger接口文档。
我这里的环境是Springboot2.7.9的。
好了,swagger聊完,我们继续接着上面的继续说。
那么如果前后端分离的话,是通过什么去请求后端的接口呢?
我们都知道之前我们没有前后端分离的时候,是可以通过Ajax进行请求的。
但是我们现在如果使用了Vue 那么我们就需要使用axios来进行请求接口。
axios其实跟Ajax是很像的,axios是可以直接在Vue中安装的,可以使用Npm进行安装。
那么Npm是什么呢?Npm就是我们的Node.js,我们可以通过node.js来构建Vue项目以及安装axios或者element-ui等等
那么就比如如下这个项目:前端拿到数据负责进行页面展示,后端只需要提供接口以及接口安全校验即可。
就比如说我们的前端项目为: localhost:8080
我们的后端项目为: localhost:8081
那么这样就产生一个问题。也就是跨域请求的一个问题。
所以我们就需要在我们的后端配置类中写一个CORS跨域的配置类。
如下代码:
package com.wms.common;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**")//是否发送Cookie.allowCredentials(true)//放⾏哪些原始域.allowedOriginPatterns("*").allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}).allowedHeaders("*").exposedHeaders("*");}}
那么既然有跨域的话,那么会不会可能存在CORS相关的一些漏洞呢?
这个问题我们后面再探究。
紧接着我们前面说过,后端需要提供接口,前端需要通过axios进行请求我们的接口来获取JSON格式的数据。
那么他是怎么请求的呢?就好比我们这里有一个请求后端 /user/list接口的请求。那么前面$httpurl是什么呢?
$httpUrl就是我们后端的请求链接。也就是说我们访问后端的http://localhost:8090/user/listPageC1 这个接口来获取数据然后赋值给tableData,前端拿到数据之后进行展示。
那我们访问一下这个后端的接口看他返回的是什么?可以看到这里返回的是一个Json格式的数据。那么前端进行接收,然后展示。
可以看到这里返回的是一个List的接口。
那么就想到如果这个接口没有任何权限管理,这就有可能造成信息泄露。
所以后端使用了JWT来进行权限管理。
JWT大家都知道他的全名是java web token,他就是为了解决前后端分离中数据的安全传输的问题。
以前我们都知道一般登录系统的话,需要用户名和密码然后进行登录。之后将我们的用户名存入到session中。
以及在拦截器或者过滤器中进行session判断,看我们的request或者model一些域中是否有我们的session信息,如果有的话那么就通过,如果没有的话,那么就返回false。
但是如果每一个用户都去登录这个系统,这样会造成服务器的内存一系列的问题。
所以JWT出来之后。解决了相关的问题。
JWT是使用3部分组成的,我们的header信息 + payload载荷 + 签名信息。
header:
{"alg": "HS256","typ": "JWT"}
payload: 通常我们的payload中会存储一些用户名或者用户id等信息
{//默认字段"sub":"主题123",//自定义字段"name":"java技术爱好者","isAdmin":"true","loginTime":"2021-12-05 12:00:03"}
Signature
签名信息是这样生成的,他需要一个(secret)秘钥,这个秘钥是存储在服务中的,这个部分需要 base64URL 加密后的 header 和 base64URL 加密后的 payload 使用 . 连接组成的字符串,然后通过header 中声明的加密算法 进行加盐secret组合加密,然后就得出一个签名哈希。签名的作用是JWT没有被篡改过,这也就是为什么我们拿到签名可以伪造的原因。
他是通过这三部分组成的。
那么他是如何认证的呢?
就比如说用户去登录这个系统,如果登录成功的话,我们的服务端生成JWTToken,然后存储在用户的本地。
如果用户需要请求一系列的接口,比如查询信息,修改信息等,都需要携带这个token,拦截器进行验证,如果验证通过那么就响应数据,如果验证不通过那么返回错误信息。
我们都知道JWT前面两个部分是通过Base64进行编码的,那么如果进行篡改了其中的数据,会不会造成安全问题呢?
我们将Token发送给用户之后,用户拿到3部分信息也就是header + payload + Signature
发送到服务端之后,服务端会进行验证签名比对。服务端会讲我们的header 和 payload 拿到之后,然后再加上我们的秘钥生成的签名对你发送过来的签名进行比对,如果比对成功,那么响应数据,如果失败,返回错误信息。
我们直接上代码:
后端拦截器的代码:这里表示我们从header头信息中获取到我们服务端给客户端发送的JWTToken。然后进行签名比对。
package com.wms.common;import com.auth0.jwt.exceptions.SignatureVerificationException;import com.auth0.jwt.exceptions.TokenExpiredException;import com.fasterxml.jackson.databind.ObjectMapper;import com.wms.utils.JWTUtils;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.HashMap;import java.util.Map;public class JWTInterceptors implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取请求头中的令牌Map<String, Object> map = new HashMap<String, Object>();String token = request.getHeader("token");String token1 = response.getHeader("token");try{JWTUtils.verify(token);//验证令牌//map.put("status",true);map.put("msg","请求成功");return true; //放行请求}catch (SignatureVerificationException e){map.put("msg","无效签名");e.printStackTrace();}catch (TokenExpiredException e){map.put("msg","token已过期");e.printStackTrace();}catch (Exception e){e.printStackTrace();}map.put("status",false); //设置状态//将map转为json jacksonString s = new ObjectMapper().writeValueAsString(map);response.setContentType("application/json;charset=UTF-8");response.getWriter().println(s);return false;}}
controller层代码:这里我们在登录的时候讲生成token。在访问/user/test接口的时候进行验证token。
package com.springboot.controller;import com.auth0.jwt.exceptions.SignatureVerificationException;import com.auth0.jwt.exceptions.TokenExpiredException;import com.auth0.jwt.interfaces.DecodedJWT;import com.springboot.pojo.User;import com.springboot.service.UserService;import com.springboot.utils.JWTUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpRequest;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;import java.security.SignatureException;import java.util.HashMap;import java.util.Map;@RestController@Slf4jpublic class UserConrtoller {@Autowiredprivate UserService userService;@PostMapping("/user/login")public Map<String,Object> login(@RequestBody User user){log.info(user.getName());log.info(user.getPassword());Map<String, Object> map = new HashMap<String, Object>();try{User login = userService.login(user);Map<String,String> payload = new HashMap<>();payload.put("userId",login.getId());payload.put("userName", login.getName());//生成JWT令牌String token = JWTUtils.getToken(payload);map.put("status",true);map.put("msg","登录成功");map.put("token",token); //响应Token}catch (Exception e){map.put("status",false);map.put("msg",e.getMessage());}return map;}@PostMapping("/user/test")public Map<String,Object> test(HttpServletRequest request){Map<String, Object> map = new HashMap<String, Object>();String token = request.getHeader("token");DecodedJWT token1 = JWTUtils.getToken(token);String userId = token1.getClaim("userId").asString();String userName = token1.getClaim("userName").asString();map.put("status",true);map.put("msg","请求成功");map.put("userId",userId);map.put("userName",userName);return map;}}
比如说我们现在访问Login,他会给我们返回一个签名。(这是postman测试工具),然后我们去访问test接口。
可以看到当我们没有设置token的时候,他会返回false。
好了进行就聊到这里了,我们有空再聊,最后预祝高考的师傅,高考加油。
如果有什么问题可以加我WX探讨:
Get__Post