JavaWeb前后端分离架构
前后端分离已成为互联网项目开发的业界标准使用方式,通过 nginx+tomcat的方式有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS 等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。
核心思想是前端 html 页面通过 ajax 调用后端的 restuful api 接口并使用 json数据进行交互。
前后分离的优势
- 可以实现真正的前后端解耦,前端服务器使用 nginx/tomcat。前端/WEB服务器放的是 css,js,图片等等一系列静态资源,前端服务器负责控制页面引用,跳转,路由.
- 发现 bug,可以快速定位是谁的问题,不会出现互相踢皮球的现象。页面逻辑,跳转错误,浏览器兼容性问题,脚本错误,页面样式等问题,全部由前端工程师来负责。接口数据出错,数据没有提交成功,应答超时等问题,全部由后端工程师来解决。
- 减少后端服务器的负载压力。除了接口以外的其他所有 http 请求全部转移到前端服务器上。
- 即使后端服务暂时超时或者宕机了,前端页面也会正常访问,只不过数据刷不出来而已。
- 也许你也需要有微信相关的轻应用,那样你的接口完全可以共用,如果也有app 相关的服务,那么只要通过一些代码重构,也可以大量复用接口,提升效率。(多端应用)
- 页面显示的东西再多也不怕,因为是异步加载。
- nginx 支持页面热部署,不用重启服务器,前端升级更无缝。
- 增加代码的维护性&易读性(前后端混在一起的代码读起来相当费劲)。
- 提升开发效率,因为可以前后端并行开发,而不是像以前的强依赖。
- 在 nginx 中部署证书,外网使用 https 访问,并且只开放 443 和 80 端口,其他端口一律关闭(防止黑客端口扫描),内网使用 http,性能和安全都有保障。
- 前端大量的组件代码得以复用,组件化,提升开发效率
在本机中部署项目
前端
- 在Vue-cli项目中输入命令npm run build 打包
- 将dist包中的内容移至nginx/html下
- 启动nginx
后端
- 将springboot项目打jar包
- cmd中输入命令java -jar springboot8080.jar启动服务
- 启动放置图片的服务tomcat:apache-tomcat-9.0.43(img)\bin\startup.bat
访问 localhost+nginx端口
关于验证
Cookie & Session
Session主要作用就是在服务端记录用户状态和信息,保存在服务器,安全性更高,tomcat默认有效期30min
Cookie保存在客户端浏览器,可存储一些不敏感的信息
认证流程
Cookie+Session
用户成功登录,服务器会存储一个Session对象里边能放用户信息,并将SessionID发给客户端存在Cookie中,客户端之后发送的请求都会携带SessionID,服务端就能拿到用户信息
缺点
- Session保存在服务端,服务器挂了session就没了;
- Session过多占用服务器资源;
- 移动端没有cookie;
- 分布式多机器,只能固定访问一台,扩展性低;
Cookie 和 Session 的区别?
- 作用范围不同,Cookie 保存在客户端,Session 保存在服务器端。
- 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能;Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
- 隐私策略不同,Cookie 存储在客户端,容易被窃取;Session 存储在服务端,安全性好一些。
- 存储大小不同, 单个 Cookie 保存的数据不能超过 4K;对于 Session 来说存储没有上限
Token & JWT
为解决传统的Cookie+Session认证的不便,JWT(Json web Token),他可以在服务端不用保存Session,只用在客户端保存服务端返回的Token就可以,扩展性提高
JWT本质就是一段签名的JSON格式的数据。由于带有数字签名,所以这些信息是可信的。
优点
- 简洁:JWT Token数据量小,传输速度也很快
- 自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 跨语言:因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 扩展性:不需要在服务端保存会话信息,特别适用于分布式微服务
token验证流程
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
- 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
- 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
- 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
- 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
- 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
JWT的构成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MCwiZXhwIjoxNjQxMjY1NjY5LCJhY2NvdW50IjoiYWRtaW4ifQ.d8V0-IHMVFi5OhmlhFK5SMcOZN2nteLjrWyjubTaYbo
三部分 标头(Header)、有效载荷(Payload,用户的信息)和签名(Signature)
在传输的时候,会将JWT的3部分分别进行Base64编码后用.
进行连接形成最终传输的字符串
第一部分 标头 header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC HS256
{
"alg": "HS256",
"typ": "JWT"
}
将这段json进行base64转码后就成为token的第一部分
第二部分 有效荷载 payload
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}
存放用户的个人信息,只是经过base64转码,但不加密,所有不要存放隐私信息,JWT只是适合在网络中传输一些非敏感的信息
第三部分 签名 signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64转码后的header和base64转码后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分
JWT搭建使用
-
导入jar坐标
<!-- jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.2</version> </dependency>
-
创建JWTUtil类
/** * JWT常用方法类 * * @author Deevan */ public class JwtUtil { /** * jwt生成token */ public static String token(Integer id, String account, Integer type) { String token = ""; try { //过期时间 为1970.1.1 0:0:0 至 过期时间 当前的毫秒值 + 有效时间 Date expireDate = new Date(System.currentTimeMillis() + 300 * 1000); //秘钥及加密算法 Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE"); //设置头部信息 Map<String, Object> header = new HashMap<>(); header.put("typ", "JWT"); header.put("alg", "HS256"); //携带id,账号信息,生成签名 token = JWT.create() .withHeader(header) .withClaim("id", id) .withClaim("account", account) .withClaim("type", type) .withExpiresAt(expireDate) .sign(algorithm); } catch (Exception e) { e.printStackTrace(); return null; } return token; } /** * 验证token是否有效 */ public static boolean verify(String token) { try { //验签 Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE"); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception e) {//当传过来的token如果有问题,抛出异常 return false; } } /** * 获得token 中playload部分数据,按需使用 */ public static DecodedJWT getTokenInfo(String token) { return JWT.require(Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE")).build().verify(token); } }
-
登录验证时生成token并返回给前端
@RequestMapping("/login") public CommonResult<Admin> login(@RequestBody Admin admin) { CommonResult<Admin> commonResult = null; System.out.println(admin); try { Admin adminBack = loginService.loginCheck(admin); if (adminBack != null) { String token = JwtUtil.token(adminBack.getId(), adminBack.getAccount(), adminBack.getType()); adminBack.setToken(token); commonResult = new CommonResult<>(200, "登录成功", adminBack); } else { commonResult = new CommonResult<>(201, "密码错误", null); } } catch (Exception e) { e.printStackTrace(); commonResult = new CommonResult<>(500, "服务器忙", null); } return commonResult; }
-
前端接收token并存入sessionStorage中
login() { var _this = this; //存储vue对象 this.$http.post("/login/login", this.form).then(function(res) { //密码错误 if (res.data.code === 201) { _this.$message({ message: res.data.msg, type: 'warning' }); return; } window.sessionStorage.setItem("account", res.data.data.account) window.sessionStorage.setItem("token", res.data.data.token) //路由跳转 _this.$router.push("/main"); }) }
-
前端请求拦截中为请求头中加入token,使得每一次请求都带有token字段
//axios 请求拦截 axios.interceptors.request.use(config => { //为请求头对象,添加 Token 验证的 token 字段 config.headers.token = window.sessionStorage.getItem('token'); return config; })
-
路由导航守卫中验证token
//路由导航守卫,在每次发生组件路由的时候,会自动出发 rout.beforeEach((to, from, next) => { //如果用户访问的登录页, 直接放行 if (to.path == '/login') { return next(); } else { //验证token,拦截没有token的路由 var token = window.sessionStorage.getItem("token"); if (token == null) { return next("/login"); } else { next(); } } })
-
后端拦截器中进行"是否登录"验证
创建LoginInterceptor拦截器
/** * 判断是否登录拦截器 * * @author Deevan */ public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); //验证请求中的token 是否有问题 boolean res = JwtUtil.verify(token); if (!res) { response.getWriter().print(401); } return res; } }
配置此拦截器
/** * 拦截器配置类 * * @author Deevan */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { //拿到自己注册的拦截器 InterceptorRegistration inter = registry.addInterceptor(new LoginInterceptor()); //拦截的地址 inter.addPathPatterns("/**"); //放行的地址 inter.excludePathPatterns("/api/login/login"); } }