zoukankan      html  css  js  c++  java
  • Spring Cloud微服务安全实战_5-8_基于Cookie的SSO

    前几篇说的都是基于session的SSO(客户端应用的session、认证服务器的session),客户端应用拿到认证服务器返回的token后,将其存在自己的session, 用户登录状态是存在服务器端的。

    本篇要说的是,要实现一个基于浏览器cookie的SSO,客户端应用获取到令牌后,不是将其存到session,而是写入浏览器cookie,这个改变会带来一些列问题,本篇将解决这些问题。

    在OAuth授权回调里处理

    客户端应用 客户token后的改造,在OAuth授权回调里处理,拿到token后写入cookie:

    CookieTokenFilter 

    在客户端应用,引入zuul的依赖,写一个CookieTokenFilter,从cookie拿出token 加在请求头里。

    package com.nb.security.admin;
    
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.http.*;
    import org.springframework.stereotype.Component;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.client.RestTemplate;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 从cookie获取token,统一加到请求头中去
     */
    @Component
    public class CookieTokenFilter extends ZuulFilter {
    
        private RestTemplate restTemplate = new RestTemplate();
    
        @Override
        public Object run() throws ZuulException {
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
            HttpServletResponse response = requestContext.getResponse();
    
            String accessToken = getCookie("nb_access_token");
            if(StringUtils.isNotBlank(accessToken)){
                //令牌放到请求头
                requestContext.addZuulRequestHeader("Authorization","Bearer "+accessToken);
            }else {
                //从cookie把不到token说明token已过期,刷新令牌
                String refreshToken = getCookie("nb_refresh_token");
                if(StringUtils.isNotBlank(refreshToken)){
                    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("refresh_token",refreshToken);//授权码
                    params.add("grant_type","refresh_token");//授权类型-刷新令牌
    
    
                    HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers);
    
                    //刷新令牌的时候,可能refresh_token也过期了,这里进行处理,让用户重新走授权流程
                    try{
                        ResponseEntity<AccessToken> newToken = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, AccessToken.class);
                        //令牌放到请求头
                        requestContext.addZuulRequestHeader("Authorization","Bearer "+newToken.getBody().getAccess_token());
                        //基于 Cookie的SSO,拿到token后写入浏览器Cookie
                        Cookie accessTokenCookie = new Cookie("nb_access_token",newToken.getBody().getAccess_token());
                        accessTokenCookie.setMaxAge(newToken.getBody().getExpires_in().intValue()-3);//有效期
                        accessTokenCookie.setDomain("nb.com");//所有以nb.com结尾的二级域名都可以访问到cookie
                        accessTokenCookie.setPath("/");
                        response.addCookie(accessTokenCookie);
    
                        Cookie refreshTokenCookie = new Cookie("nb_refresh_token",newToken.getBody().getRefresh_token());
                        refreshTokenCookie.setMaxAge(2592000);//这里随便写一个很大的值(没用),如果是过期的token服务器将处理的。
                        refreshTokenCookie.setDomain("nb.com");//所有以nb.com结尾的二级域名都可以访问到cookie
                        refreshTokenCookie.setPath("/");
                        response.addCookie(refreshTokenCookie);
                    }catch (Exception e){
                        //有异常,重新登录
                        requestContext.setSendZuulResponse(false);//zuul过滤器不往下走了
                        requestContext.setResponseStatusCode(500);//响应状态码
                        requestContext.setResponseBody("{"message":"refresh fail"}");
                        requestContext.getResponse().setContentType("application/json");
                    }
                }else {
                    //没用refresh——token,重新登录
                    requestContext.setSendZuulResponse(false);//zuul过滤器不往下走了
                    requestContext.setResponseStatusCode(500);//响应状态码
                    requestContext.setResponseBody("{"message":"refresh fail"}");
                    requestContext.getResponse().setContentType("application/json");
                }
            }
    
            return null;
        }
    
        private String getCookie(String name) {
            String result = null;
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
            Cookie[] cookies = request.getCookies();
            for(Cookie cookie : cookies){
                if(StringUtils.equals(cookie.getName(),name)){
                    result = cookie.getValue();
                    break;
                }
            }
            return result;
        }
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 1;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    }

    客户端判断用户登录状态

    在前端服务器判断用户是否登录,之前基于session的SSO的处理是,会在客户端应用admin里的session里着token,往前端服务器发了一个/me请求,session如果有东西说明用户已登录,现在客户端应用session里已经不存token了,客户端应用没办法知道你是否已经等了,所以这里需要换一下,就换成,在客户端应用的页面,往网关发一个/api/user/me请求,因为yml里已经配置了,/api/开头的请求,都会转发到网关。

    客户端页面的改造:

     前端服务器Controller在基于session-token方案时候判断用户登录状态,不用了:

     

     在网关上,由于从客户端应用admin过来的请求,会在请求头里带一个token,然后经过了网关的权限过滤器后,会从token解析出用户名,放在请求头传下去:

    这里加一个MeFilter,排序Order在授权过滤器之后,专门映射处理/user/me请求,它不往任何一个服务转发,只是从请求头拿username,如果拿得到,就说明用户是登录状态。

     实验

    1,启动4个服务

     2,访问客户端应用 admin

     3,点击去登录,跳转到认证服务器的登录页

     3,输入用户名aaa(随便输入,认证服务器没校验)密码123456 (认证服务器写死的),点击登录,可以看到,一级域名nb.com下的cookie里已经存入了access_token、refresh_token 。

    点击【获取订单信息】按钮,调用订单服务,会携带cookie里的token,然后在客户端admin上, CookieTokenFilter 从cookie里读取到access_token和refresh_token,携带到请求头,转发给网关,网关校验token后,再将请求转发给订单服务。

     到现在已经实现了基于cookie 的SSO,token信息是存在cookie里的,客户端应用的session里没有存token信息。

    模拟access_token失效后,客户端应用admin 拿refresh_token 去认证服务器换取access_token。

    客户端应用配置表里,access_token失效时间是20秒,refresh_token 失效时间是30秒

    访问订单服务 正常是70多毫秒,大概在17秒(cookie失效时间是20-3秒)后,可以看到访问订单服务时间是200多毫秒,此时在admin上是拿refresh_token去认证服务器刷新了acces_token。

    30秒后,refresh_token也失效了,调用订单服务,会返回异常,捕获这个异常,前端做判断,给用户提示,让用户退出登录。

     logout

     function logout() {
            //1浏览器cookie失效掉
            $.removeCookie('nb_access_token',{domain:'nb.com',path:'/'});
            $.removeCookie('nb_refresh_token',{domain:'nb.com',path:'/'});
            //2,将认证服务器的session失效, /logout 是SpringSecurity OAuth默认的退出过滤器
            // org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
            window.location.href = "http://auth.nb.com:9090/logout?redirect_uri=http://admin.nb.com:8080/index";
        }

     这样就在refresh_token失效后,就完全退出登录,跳转客户端的登录页

     目前的架构是这样的,token信息都存在了浏览器cookie,客户端应用并没有存token信息

    基于cookie的SSO的优缺点

    1,登录状态  用户的登录状态存在了浏览器的cookie,当cookie里的refresh_token失效的时候才会去认证服务器做登录 。这种方案不需要在认证服务器上设置有效期很长的session,只要一个很短的就可以了,比如30分钟,因为决定能不能访问服务的不是认证服务器的session,而是浏览器cookie里的refresh_token

    2,安全性低,token存在了浏览器,有一定的风险(使用https,缩短access_token有效期)
    3,可控性低,refresh_token和access_token存在了客户的浏览器里,没办法主动失效掉。
    4,跨域:cookie只能放在nb.com ,只有nb.com的二级域名(admin.nb.com 、order.nb.com等)可以做SSO

    好处:
    复杂程度低,相对于基于session的SSO来说,只需要做access_token和refresh_token过期的处理
    不占服务器的资源,适合于海量用户。

     代码github : https://github.com/lhy1234/springcloud-security/tree/chapt-5-7-tokensso 如果对你帮助了,给个小星星呗

  • 相关阅读:
    搭建公共DNS服务器[转]
    zabbix3.4 yum快速安装
    centos7安装图形化界面并远程连接
    记一次排查黑客入侵
    centos6安装图形界面并远程连接
    树莓派做NAS
    document.documentElement.scrollTop||document.body.scrollTop;
    矩阵连乘
    母函数详解
    Catalan 数
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/12169337.html
Copyright © 2011-2022 走看看