1、前后端分离架构
1.1、前后端半分离
工作流程大致是,访问html页面,加载css、js等静态资源,加载到浏览器时,js会发送一些ajax请求由nginx转发到后端服务器,后端服务器给出相应的json数据,页面解析Json数据,通过Dom操作渲染页面。
在这样的架构下,存在一些明显的弊端:
SEO( 搜索引擎优化)非常不方便,由于搜索引擎的爬虫无法爬下JS异步渲染的数据,导致这样的页面,SEO会存在一定的问题;
在Json返回的数据量比较大的情况下,渲染的十分缓慢,会出现页面卡顿的情况;
1.2、前后端分离
将nginx替换成了NodeJS,浏览器请求NodeJS,NodeJS在发http请求去服务器获取json数据,NodeJS收到json后再渲染出HTML页面,NodeJS直接将HTML页面flush到浏览器,这样,浏览器得到的就是普通的HTML页面,而不用再发Ajax去请求服务器了。
在这里,前端不仅包括浏览器,还包括一个NodeJS服务器,后端只负责数据的处理。
2、前后端分离的OAuth2认证架构
在之前我们实现的OAuth2认证架构中,客户端应用,其实是使用的一些http请求工具,如Restlet Client、Postman等。我们将架构改造为如下,客户端应用,其实应该由浏览器和前端服务器组成。(在这里前端服务器,我们使用SpringBoot模拟实现,在实际工作中,应该是由前端人员使用NodeJS来实现)
3、项目改造实现认证流程
由上图可知,我们需要改造的有以下两点:第一步请求令牌时,将http请求工具更换为前端服务器来请求令牌;第五步由前端服务器携带令牌请求服务。
3.1、搭建前端服务器(由SpringBoot模拟实现)
3.1.1、项目结构
3.1.2、页面index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>cfq security</title> </head> <body> <h1>Study Security</h1> <div id="login-success"> <h1>登陆成功!</h1> <p> <button onclick="getOrder()">获取订单信息</button> </p> <table> <tr> <td>order id</td> <td><input id="orderId"/></td> </tr> <tr> <td>order product id</td> <td><input id="productId"/></td> </tr> </table> </hr> <p> <button onclick="logout()">退出</button> </p> </div> <div id="goto-login"> <table> <tr> <td>用户名</td> <td><input id="username" name="username" value=""/></td> </tr> <tr> <td>密码</td> <td><input id="password" name="password" value=""/></td> </tr> <tr> <td colspan="2" align="right"> <button id="submit" onclick="login()">登录</button> </td> </tr> </table> </div> <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> <script> //判断用户是否登陆 $(function () { $.get("/me", function (data) { if (data) { //已登录 $("#login-success").show(); $("#goto-login").hide(); } else { //未登录 $("#login-success").hide(); $("#goto-login").show(); } }); }); //登陆方法 function login() { var username = $("#username").val(); var password = $("#password").val(); $.ajax({ type: "POST", url: "/login", contentType: "application/json;charset=utf-8", data: JSON.stringify({"username": username, "password": password}), success: function (msg) { location.href = "/"; }, error: function (msg) { alert("登录失败!!") } }); } //获取订单信息,通过/api转发到网关,通过/order转发到order微服务 function getOrder() { $.get("/api/order/orders/1", function (data) { $("#orderId").val(data.id); $("#productId").val(data.productId); }); } //退出 function logout() { $.get("/logout",function(){}); location.href = "/"; } </script> </body> </html>
3.1.3、提供登陆方法,使用password模式像认证服务器获取令牌,将原来由http客户端的动作,放到代码中
/** * 模拟前端服务器 * * @author caofanqi * @date 2020/2/5 16:34 */ @Slf4j @RestController @EnableZuulProxy @SpringBootApplication public class WebAppApplication { private RestTemplate restTemplate = new RestTemplate(); public static void main(String[] args) { SpringApplication.run(WebAppApplication.class, args); } /** * 获取当前认证的token信息 */ @GetMapping("/me") public TokenInfoDTO me(HttpServletRequest request){ return (TokenInfoDTO)request.getSession().getAttribute("token"); } /** * 登陆方法 * 向认证服务器获取令牌,将token信息放到session中 */ @PostMapping("/login") public void login(@RequestBody UserDTO userDTO, HttpServletRequest request) { String oauthTokenUrl = "http://gateway.caofanqi.cn:9010/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("webApp", "123456"); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.set("username", userDTO.getUsername()); params.set("password", userDTO.getPassword()); params.set("grant_type", "password"); params.set("scope", "read write"); HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers); ResponseEntity<TokenInfoDTO> response = restTemplate.exchange(oauthTokenUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class); request.getSession().setAttribute("token", response.getBody()); log.info("tokenInfo : {}",response.getBody()); } /** * 退出 */ @RequestMapping("/logout") public void logout(HttpServletRequest request){ request.getSession().invalidate(); } }
3.1.4、通过zuul的转发功能,我们再每个请求头中添加token令牌
/** * 将session中的token取出放到请求头中 * * @author caofanqi * @date 2020/2/6 0:34 */ @Component public class SessionTokenFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); TokenInfoDTO token = (TokenInfoDTO) request.getSession().getAttribute("token"); if (token != null) { requestContext.addZuulRequestHeader("Authorization","bearer " + token.getAccess_token()); } return null; } }
3.1.5、修改hosts文件,便于区别各服务
3.1.6、application.yml配置文件,将/api/**的请求都转发到gateway网关服务
spring: application: name: webApp server: port: 9000 zuul: routes: api: path: /api/** url: http://gateway.caofanqi.cn:9010 sensitive-headers:
3.2、启动各服务,测试
3.2.1、访问http://web.caofanqi.cn:9000/,因为没有登陆,所以显示登陆界面
3.2.2、输入zhangsan,123456,登陆成功
认证服务器,返回给webApp的token信息
3.2.3、点击获取订单信息,可以获得订单,SessionTokenFilter会在请求头中添加token,并且请求是以/api开头的会转发到网关,再由网关转发到order服务。
3.2.4、点击退出,又到了登陆页面
项目源码:https://github.com/caofanqi/study-security/tree/dev-web-password
原文地址:https://www.cnblogs.com/caofanqi/p/12268421.html