认证和SSO(二)-OAuth2四种授权模式及项目改造为授权码模式实现单点登陆SSO
1、OAuth2四种模式
1.1、密码模式
这也是我们之前一直使用的模式,流程如下;这种模式下,用户敏感信息直接泄漏给了客户端应用,因此这种模式只能用于客户端应用是我们自己开发的。因此密码模式一般用于自己开发的App或单页面应用。
1.2、授权码模式
授权码模式是四种模式中最繁琐也是最安全的一种模式。用户向客户端发起请求时,客户端应用引导用户去授权服务器进行认证(需要有客户端id和回调地址),认证成功授权服务器会将授权码发送给客户端应用,客户端应用再通过授权码,客户端id,客户端密码去授权服务器换取令牌,授权服务器验证无误后,将令牌发送给客户端应用。这种场景下,用户的敏感信息没有暴漏给客户端应用,保证了安全。一般适用与客户端应用是Web服务器或第三方的App。
1.3、简化模式(隐式授权模式)
简化模式相对于授权码模式省略了,通过授权码换取令牌过程。一般用于没有服务器的前端应用。
4、客户端模式
最简单的模式,发出的令牌与用户无关。因此,一般适用于我们完全信任的客户端应用(服务器端服务),并且不需要用户参与。
2、改造项目为授权码认证方式
目前我们使用的是OAuth2的密码模式,我们来修改为使用授权码模式。
2.1、修改客户端应用,需要用户进行认证时直接引导去认证服务器完成认证
2.1.1、修改index.html用户未登录时,直接跳转到认证服务器进行获取授权码,需提供客户端id(client_id)、回调地址(redirect_uri)、返回值类型(response_type)为code、状态标记(state)传什么返回什么,为可选项。
<!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> <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(); location.href = "http://auth.caofanqi.cn:9020/oauth/authorize?" + "client_id=webApp&" + "redirect_uri=http://web.caofanqi.cn:9000/oauth/callback&" + "response_type=code&" + "state=abc"; } }); }); //登陆方法 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>
2.1.2、提供回调方法
/** * 回调方法 * 接收认证服务器发来的授权码,并换取令牌 * @param code 授权码 * @param state 请求授权服务器时发送的state */ @GetMapping("/oauth/callback") public void oauthCallback(@RequestParam String code, String state, HttpServletRequest request, HttpServletResponse response) throws IOException { 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("code",code); params.set("grant_type", "authorization_code"); params.set("redirect_uri", "http://web.caofanqi.cn:9000/oauth/callback"); HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers); ResponseEntity<TokenInfoDTO> authResult = restTemplate.exchange(oauthTokenUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class); request.getSession().setAttribute("token", authResult.getBody()); log.info("tokenInfo : {}",authResult.getBody()); log.info("state :{}",state); //一般会根据state记录需要登陆时的路由 response.sendRedirect("/"); }
2.2、认证服务器配置支持授权码模式
2.3、启动各服务,进行测试
2.3.1、访问http://web.caofanqi.cn:9000/ 会直接跳转到认证服务器
2.3.3、输入zhangsan,123456进行登陆,会进行权限选择
如果想跨过权限选择,可以设置autoapprove为true
2.3.4、点击Authorize会调转会我们设定的页面,再WebApp服务的日志中可以看到我们传的state给原封不动的返回来了
2.3.5、点击获取订单信息,可以正常获得
2.3、到目前为止,我们实现了由OAuth2密码模式替换为OAuth2授权码模式,同时也实现了SSO,微服务环境下前后端分离的单点登陆。为什么说实现了SSO呢,我们可以把再启动一个webApp服务,并把涉及到的端口改为8090,客户端表中加入一条记录,各修改如下
访问http://web.caofanqi.cn:8090/ ,并没有让我们进行登陆,直接就是登陆状态
这是为什么呢?因为在这种模式下,登陆的位置是认证服务器,只要在认证服务器的session没过期,客户端在任何时候跳过去,认证服务器就知道你是谁,就不会让你输入用户名和密码了,直接跳回客户端应用去,如果之前的令牌还有效,就直接发给客户端应用,无效了,就会新生成一个发给客户端应用。
但是这种方式还有一些问题,我们在后面进行探讨。
项目源码:https://github.com/caofanqi/study-security/tree/dev-web-authorization_code