目前的架构
到目前为止,已经实现了在前后端分离的架构,微服务的环境下,一个完整的业务逻辑,包括用户的登录,获取令牌,拿着令牌调服务,退出。(流程如下)
目前的架构是基于oauth2的password模式的,是存在一些问题的:
1,用户输入用户名密码,是提交给了前端服务器的,前端服务器的开发人员,都会接触到用户的用户名密码,存在安全问题
2,每个客户端应用都要处理登录逻辑,一旦登录逻辑有变化,所有的客户端都要改,重新部署,有耦合。
希望的场景
用户需要登录的时候,前端服务器直接将用户引导到 认证服务器 上,认证的动作是在认证服务器上完成的。
这样做的好处是:
1,客户端应用完全接触不到用户的用户名密码,保证了安全性。
2,客户端应用没有登录逻辑,登录逻辑都在认证服务器上,如果登录逻辑有变化,直接修改认证服务器一处,减少了耦合。
将password授权模式改为授权码模式
+++++++++++++++++++++++ 番外, 讲一些 OAuth协议的4种授权模型++++++++++++++++++++++++
1,密码模式
也是之前一直在用的授权模式,适用场景:手机app ,这个客户端应用是你完全可以信任的,你的app就是自己公司开发的。但是这个模式并不适合在web场景下用,在web下,用户名密码并不是直接填给自己写的应用的,而是填在浏览器呈现的一个页面上的,这个浏览器是客户端应用的一个代理,浏览器是没法保证安全性的。(还是不是很理解哪里不安全)
2,授权码模式
1,用户访问客户端应用
2,引导用户到认证服务器进行登录(此步骤需要携带客户端应用的clientId,可以是html直接转发认证服务器),用户输入用户名、密码
3,认证成功后,认证服务器向客户端应用发一个授权码code
4,客户端应用拿着授权码code,和clientId,clientSecret,去换取access_token
5,返回access_token给客户端应用
这种场景下,用户名、密码、客户端应用信息,都没有直接暴露在浏览器,是web下是最安全的。
3,隐式授权/简化授权
授权码模式的简化,用户认证成功后,直接将token返回给浏览器。因为某些应用没有前端服务器,只有一堆静态的html(很少见),这种模式,一般不用。
4,客户端证书
客户端应用直接发 clientId、clientSecret给认证服务器,发的令牌是针对客户端应用的,不是针对用户的。跟没授权一样,令牌不能识别用户身份。
+++++++++++++++++++++++++++++++++我是分割线结束+++++++++++++++++++++++++++++++++++++++++
改造nb-admin代码为授权码模式
1,删掉登录页面,登录是在认证服务器上完成的,所以这里删除nb-admin的登录页面
2,进入 index.html,点击登录,将用户引导到认证服务器提供的登录页去。
index.html:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <h1>welcome to 系统1</h1> <div id="loginTip"></div> <p><button onclick="getOrderInfo()">获取订单信息</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> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> function getOrderInfo(){ $.get("api/order/orders/1",function(data){ $("#orderId").val(data.id); $("#productId").val(data.productId); }); } $(document).ready(function(){ $.get("/me",function(data,status){ if(data){ //已登录 var htm = "已登录,<a href='/logout'>退出</a>"; $("#loginTip").html(htm); }else{ //未登录 var href = "<a href='/toAuthLogin'>未登录,去登录</a>"; $("#loginTip").append(href); } }); }); </script> </html>
点击登录按钮,转发到认证服务器,这个可以放在前端做,因为只有clientId信息,没有安全问题。
/** * 重定向到认证服务器的登录页 * @param response * @throws IOException */ @GetMapping("/toAuthLogin") public void toAuthLogin(HttpServletResponse response) throws IOException{ String redirectUrl = "http://auth.nb.com:9090/oauth/authorize?" +"client_id=admin&" +"redirect_uri=http://admin.nb.com:8080/oauth/callback&" +"response_type=code&" +"state=/index"; //state参数传过去啥传回来啥,一般记录跳转之前的路径 response.sendRedirect(redirectUrl); }
授权回调:
/** * 授权回调 * @param code 授权码 * @param state 自定义参数 * @param session */ @GetMapping("/oauth/callback") public String callback(@RequestParam String code,String state ,HttpSession session){ log.info("code is {}, state is {}",code,state); //认证服务器验token地址 /oauth/check_token 是 spring .security.oauth2的验token端点 String oauthServiceUrl = "http://gateway.nb.com:9070/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json请求 //网关的appId,appSecret,需要在数据库oauth_client_details注册 headers.setBasicAuth("admin","123456"); MultiValueMap<String,String> params = new LinkedMultiValueMap<>(); params.add("code",code);//授权码 params.add("grant_type","authorization_code");//授权类型-授权码模式 //认证服务器会对比数据库客户端信息的的redirect_uri和这里的是不是一致,不一致就报错 params.add("redirect_uri","http://admin.nb.com:8080/oauth/callback"); HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers); ResponseEntity<AccessToken> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, AccessToken.class); session.setAttribute("token",response.getBody()); return "redirect:/index"; }
给admin应用添加授权码模式:
启动admin服务,认证服务,订单服务,网关,访问admin
点击去登录,跳转到了认证服务器,这是自带的页面
可以重写认证服务器的安全配置方法,自定义登录页
到目前为止已将OAuth2的授权模式由 【密码模式】改造成了 【 授权码】 授权流程,完成改造的同时,也实现了SSO,微服务环境前后端分离模式下的单点登录。
目前的单点登录实际上是基于session的,目前有两个session,一个是认证服务器的session,一个是客户端应用的session,用户在客户端应用上点击登录按钮,跳转到认证服务器上做登录,登录成功后认证服务器上存了该用户的session信息,客户端应用拿到认证服务器返回的access_token后,将token存到自己的session,这样就认为该用户已登录:
但是还存在很多问题,比如,现在的退出登录:
@GetMapping("/logout") public String logout(HttpSession session){ session.invalidate(); return "index"; }
只是将nb-admin客户端应用的session失效掉了,里面没有token了,但是认证服务器上的session并没有失效,重启nb-admin,客户端应用的session是失效掉了,重新访问 http://admin.nb.com:8080/index ,出现未登录:
点击登录,跳转到认证服务器,由于认证服务器session上还有客户端该用户的登录信息,所以不会出现登录表单,用户的感觉是没有再做登录就已经登录,给人的感觉是,我已经点了退出登录,但是点击登录按钮后,没让我再输用户名密码,感觉是“没有退出去”。其实F12可以看到,已经请求了认证服务器,只不过是认证服务器的session没有失效,直接就登录成功了而已 :
1,请求认证服务器登录页 http://auth.nb.com:9090/oauth/authorize?client_id=admin&redirect_uri=http://admin.nb.com:8080/oauth/callback&response_type=code&state=/index
2,登录成功后的回调 http://admin.nb.com:8080/oauth/callback?code=IMbu7Y&state=/index
还有目前认证服务器的session是存在内存的,生产环境下认证服务器要保证高可用,是一个集群,要保证session共享,所以直接用session是不行的,接下来来处理这些问题。
本节代码github:https://github.com/lhy1234/springcloud-security/tree/chapt-5-2-authcode,如果帮助到了你,给个小星星吧