• 认证和SSO(五)-基于token的SSO


    1、修改项目使其基于浏览器cookie的SSO

    1.1、修改回调方法,获得到token后,由存放到session改为存放到cookie

       /**
         * 回调方法
         * 接收认证服务器发来的授权码,并换取令牌
         *
         * @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);
    
            log.info("tokenInfo : {}", authResult.getBody());
            //将token放到session中
            //request.getSession().setAttribute("token", authResult.getBody().init());
    
            //将token放到cookie中
            Cookie accessTokenCookie = new Cookie("access_token",authResult.getBody().getAccess_token());
            accessTokenCookie.setMaxAge(authResult.getBody().getExpires_in().intValue() - 5);
            accessTokenCookie.setDomain("caofanqi.cn");
            accessTokenCookie.setPath("/");
            response.addCookie(accessTokenCookie);
    
            Cookie refreshTokenCookie = new Cookie("refresh_token",authResult.getBody().getRefresh_token());
            refreshTokenCookie.setMaxAge(2592000);
            refreshTokenCookie.setDomain("caofanqi.cn");
            refreshTokenCookie.setPath("/");
            response.addCookie(refreshTokenCookie);
    
            log.info("state :{}", state);
            //一般会根据state记录需要登陆时的路由
            response.sendRedirect("/");
        }

    1.2、写一个CookieTokenFilter,将token从cookie中取出来

    /**
     * 将cookie中的token取出放到请求头中
     *
     * @author caofanqi
     * @date 2020/2/6 0:34
     */
    @Slf4j
    @Component
    public class CookieTokenFilter extends ZuulFilter {
    
        private RestTemplate restTemplate = new RestTemplate();
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 1;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletResponse response = requestContext.getResponse();
    
            String accessToken = getCookie("access_token");
            if (StringUtils.isNotBlank(accessToken)) {
                // 有值说明没过期
                requestContext.addZuulRequestHeader("Authorization", "bearer " + accessToken);
            } else {
                //使用refresh_token刷新令牌
                String refreshToken = getCookie("refresh_token");
                if (StringUtils.isNotBlank(refreshToken)) {
                    //去认证服务器刷新令牌
                    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("grant_type", "refresh_token");
                    params.set("refresh_token", refreshToken);
    
                    HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
    
                    try {
                        ResponseEntity<TokenInfoDTO> refreshTokenResult = restTemplate.exchange(oauthTokenUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class);
                        requestContext.addZuulRequestHeader("Authorization", "bearer " + refreshTokenResult.getBody().getAccess_token());
    
                        Cookie accessTokenCookie = new Cookie("access_token", refreshTokenResult.getBody().getAccess_token());
                        accessTokenCookie.setMaxAge(refreshTokenResult.getBody().getExpires_in().intValue() - 5);
                        accessTokenCookie.setDomain("caofanqi.cn");
                        accessTokenCookie.setPath("/");
                        response.addCookie(accessTokenCookie);
    
                        Cookie refreshTokenCookie = new Cookie("refresh_token", refreshTokenResult.getBody().getRefresh_token());
                        refreshTokenCookie.setMaxAge(2592000);
                        refreshTokenCookie.setDomain("caofanqi.cn");
                        refreshTokenCookie.setPath("/");
                        response.addCookie(refreshTokenCookie);
    
                        log.info("refresh_token......");
                    } catch (Exception e) {
                        //刷新令牌失败
                        log.info("token refresh fail");
                        requestContext.setSendZuulResponse(false);
                        requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
                        requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
                        requestContext.setResponseBody("{"message":"token refresh fail"}");
                    }
                } else {
                    //过期了,无法刷新令牌
                    log.info("refresh_token not exist");
                    requestContext.setSendZuulResponse(false);
                    requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
                    requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
                    requestContext.setResponseBody("{"message":"token refresh fail"}");
                }
            }
    
            return null;
        }
    
        /**
         * 获取cookie的值
         */
        private String getCookie(String cookieName) {
    
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            Cookie[] cookies = request.getCookies();
    
            for (Cookie cookie : cookies) {
                if (StringUtils.equals(cookieName, cookie.getName())) {
                    return cookie.getValue();
                }
            }
    
            return null;
        }
    }

    1.3、判断用户登陆状态,从网关中获取,MeFilter放到授权Filter之后。因为之间基于session,直接从客户端服务器中获取就行,现在不急于session,客户端不知道用户登陆状态,去网关获取。

      之前配置了以api开头的请求会转发到网关

       网关配置

       网关过滤器MeFilter

    /**
     * 用户判断当前用户是否认证
     *
     * @author caofanqi
     * @date 2020/2/7 21:43
     */
    @Component
    public class MeFilter extends ZuulFilter {
    
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 6;
        }
    
        /**
         *  只处理/user/me请求
         */
        @Override
        public boolean shouldFilter() {
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
            return StringUtils.equals(request.getRequestURI(),"/user/me");
        }
    
        /**
         *  判断请求头中有没有我们放入的username,后直接返回,不继续往下走
         */
        @Override
        public Object run() throws ZuulException {
            RequestContext requestContext = RequestContext.getCurrentContext();
            String username = requestContext.getZuulRequestHeaders().get("username");
            if(StringUtils.isNotBlank(username)) {
                requestContext.setResponseBody("{"username":""+username+""}");
            }
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.OK.value());
            requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
    
            return null;
        }
    }

    1.4、启动各项目,进行测试

    过期时间设置如下

      访问http://web.caofanqi.cn:9000/ ,自动跳转到登陆页面,进行登陆,间隔时间获取订单信息,webApp控制台打印如下

       查看浏览器cookie如下

    1.5、但是现在还有一个问题,认证信息放在cookie中,退出时,也要将cookie删除

        //退出
        function logout() {
            $.get("/logout", function () {
            });
            //将浏览器中的cookie也删除
            $.removeCookie('access_token', { domain:'caofanqi.cn', path: '/' });
            $.removeCookie('refresh_token', { domain:'caofanqi.cn', path: '/' });
            //客户端session失效后,将认证服务器session也失效掉,添加重定向url
            location.href = "http://auth.caofanqi.cn:9020/logout?redirect_uri=http://web.caofanqi.cn:9000";
        }

    2、基于token的SSO优缺点

    2.1、优点:

      复杂度低,相对于基于session的SSO来说,只需要做access_token和refresh_token的过期处理。

      不占用服务器资源,适合用户量特别大的系统。因为token存在浏览器cookie中,只有cookie中的refresh_token失效时,才会去认证服务器登陆。不需要认证服务器设置有效期很长的session。因为通过token就可以访问微服务。

    2.2、缺点:

      安全性低:token存在浏览器,有一定的风险。可以使用https,缩短access_token的有效期来防范。

      可控性低:token存在浏览器,没办法主动失效掉。

      跨域问题:cookie只能放在顶级域名下(caofanqi.cn),只有二级域名(web.caofanqi.cn、order.caofanqi.cn)才可以做SSO。如果要与baidu.com做SSO的话,需要同时设置多个cookie。

    项目源码:https://github.com/caofanqi/study-security/tree/dev-web-sso-token

  • 相关阅读:
    Unsupported major.minor version 51.0(JDK版本错误)
    String、StringBuilder和StringBuffer的区别
    循环依赖常问问题,spring三级缓存解决循环依赖解析图
    ens33网卡失效ipaddr查询不到ip: 出现:ens33: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 00:0c:29:2c:8d:e1 brd ff:ff:ff:ff:ff:ff
    redis被攻击,导致redis连接不上,RDB异常解决方案
    项目集成seata和mybatisplus冲突问题解决方案:(分页插件失效, 自动填充失效, 自己注入的id生成器失效 找不到mapper文件解决方案)
    seata服务端搭建和客户端配置(使用nacos进行注册发现,使用mysql进行数据持久化),以及过程中可能会出现的问题与解决方案
    通过串口(蓝牙WiFi)与Arduino通信
    Python callable函数判断某个对象是否可调用
    Python 通过PyUserInput模拟键鼠操作
  • 原文地址:https://www.cnblogs.com/caofanqi/p/12275403.html
Copyright © 2020-2023  润新知