• SpringCloud系列——SSO 单点登录


    前言

      作为分布式项目,单点登录是必不可少的,文本基于之前的的博客(猛戳:SpringCloud系列——Zuul 动态路由SpringBoot系列——Redis)记录Zuul配合Redis实现一个简单的sso单点登录实例

      sso单点登录思路:

      1、访问分布式系统的任意请求,被Zuul的Filter拦截过滤

      2、在run方法里实现过滤规则:cookie有令牌accessToken且作为key存在于Redis,或者访问的是登录页面、登录请求则放行

      3、否则,将重定向到sso-server的登录页面且原先的请求路径作为一个参数;response.sendRedirect("http://localhost:10010/sso-server/sso/loginPage?url=" + url);

      4、登录成功,sso-server生成accessToken,并作为key(用户名+时间戳,这里只是demo,正常项目的令牌应该要更为复杂)存到Redis,value值存用户id作为value(或者直接存储可暴露的部分用户信息也行)设置过期时间(我这里设置3分钟);设置cookie:new Cookie("accessToken",accessToken);,设置maxAge(60*3);、path("/");

      5、sso-server单点登录服务负责校验用户信息、获取用户信息、操作Redis缓存,提供接口,在eureka上注册

      代码编写

      sso-server

      首先我们创建一个单点登录服务sso-server,并在eureka上注册(创建项目请参考之前的SpringCloud系列博客跟 SpringBoot系列——Redis

      login.html

      我们这里需要用到页面,要先maven引入thymeleaf

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    复制代码
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录页面</title>
    </head>
    <body>
        <form action="/sso-server/sso/login" method="post">
            <input name="url" type="hidden" th:value="${url}"/>
            用户名:<input name="username" type="text"/>
            密码:<input name="password" type="password"/>
            <input value="登录" type="submit"/>
        </form>
    </body>
    </html>
    复制代码

      提供如下接口

    复制代码
    @RestController
    @EnableEurekaClient
    @SpringBootApplication
    public class SsoServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SsoServerApplication.class, args);
        }
    
        @Autowired
        private StringRedisTemplate template;
    
        /**
         * 判断key是否存在
         */
        @RequestMapping("/redis/hasKey/{key}")
        public Boolean hasKey(@PathVariable("key") String key) {
            try {
                return template.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 校验用户名密码,成功则返回通行令牌(这里写死huanzi/123456)
         */
        @RequestMapping("/sso/checkUsernameAndPassword")
        private String checkUsernameAndPassword(String username, String password) {
            //通行令牌
            String flag = null;
            if ("huanzi".equals(username) && "123456".equals(password)) {
                //用户名+时间戳(这里只是demo,正常项目的令牌应该要更为复杂)
                flag = username + System.currentTimeMillis();
                //令牌作为key,存用户id作为value(或者直接存储可暴露的部分用户信息也行)设置过期时间(我这里设置3分钟)
                template.opsForValue().set(flag, "1", (long) (3 * 60), TimeUnit.SECONDS);
            }
            return flag;
        }
    
        /**
         * 跳转登录页面
         */
        @RequestMapping("/sso/loginPage")
        private ModelAndView loginPage(String url) {
            ModelAndView modelAndView = new ModelAndView("login");
            modelAndView.addObject("url", url);
            return modelAndView;
        }
    
        /**
         * 页面登录
         */
        @RequestMapping("/sso/login")
        private String login(HttpServletResponse response, String username, String password, String url) {
            String check = checkUsernameAndPassword(username, password);
            if (!StringUtils.isEmpty(check)) {
                try {
                    Cookie cookie = new Cookie("accessToken", check);
                    cookie.setMaxAge(60 * 3);
                    //设置域
    //                cookie.setDomain("huanzi.cn");
                    //设置访问路径
                    cookie.setPath("/");
                    response.addCookie(cookie);
                    //重定向到原先访问的页面
                    response.sendRedirect(url);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            return "登录失败";
        }
    }
    复制代码

      zuul-server

      引入feign,用于调用sso-server服务

            <!-- feign -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>

      创建SsoFeign.java接口

    复制代码
    @FeignClient(name = "sso-server", path = "/")
    public interface SsoFeign {
        /**
         * 判断key是否存在
         */
        @RequestMapping("redis/hasKey/{key}")
        public Boolean hasKey(@PathVariable("key") String key);
    
    }
    复制代码

      启动类加入@EnableFeignClients注解,否则启动会报错,无法注入SsoFeign对象

    复制代码
    @EnableZuulProxy
    @EnableEurekaClient
    @EnableFeignClients
    @SpringBootApplication
    public class ZuulServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ZuulServerApplication.class, args);
        }
    
        @Bean
        public AccessFilter accessFilter() {
            return new AccessFilter();
        }
    }
    复制代码

      修改AccessFilter过滤逻辑,注入feign接口,用于调用sso-server检查Redis,修改run方法的过滤逻辑

    复制代码
    /**
     * Zuul过滤器,实现了路由检查
     */
    public class AccessFilter extends ZuulFilter {
    
        @Autowired
        private SsoFeign ssoFeign;
    
        /**
         * 通过int值来定义过滤器的执行顺序
         */
        @Override
        public int filterOrder() {
            // PreDecoration之前运行
            return PRE_DECORATION_FILTER_ORDER - 1;
        }
    
        /**
         * 过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型:
         * public static final String ERROR_TYPE = "error";
         * public static final String POST_TYPE = "post";
         * public static final String PRE_TYPE = "pre";
         * public static final String ROUTE_TYPE = "route";
         */
        @Override
        public String filterType() {
            return PRE_TYPE;
        }
    
        /**
         * 过滤器的具体逻辑
         */
        @Override
        public Object run() {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            HttpServletResponse response = ctx.getResponse();
    
            //访问路径
            String url = request.getRequestURL().toString();
    
            //从cookie里面取值(Zuul丢失Cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396)
            String accessToken = request.getParameter("accessToken");
            for (Cookie cookie : request.getCookies()) {
                if ("accessToken".equals(cookie.getName())) {
                    accessToken = cookie.getValue();
                }
            }
            //过滤规则:cookie有令牌且存在于Redis,或者访问的是登录页面、登录请求则放行
            if (url.contains("sso-server/sso/loginPage") || url.contains("sso-server/sso/login") || (!StringUtils.isEmpty(accessToken) && ssoFeign.hasKey(accessToken))) {
                ctx.setSendZuulResponse(true);
                ctx.setResponseStatusCode(200);
                return null;
            } else {
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(401);
                //重定向到登录页面
                try {
                    response.sendRedirect("http://localhost:10010/sso-server/sso/loginPage?url=" + url);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
    
        /**
         * 返回一个boolean类型来判断该过滤器是否要执行
         */
        @Override
        public boolean shouldFilter() {
            return true;
        }
    }
    复制代码

      修改配置文件,映射sso-server代理路径,超时时间与丢失cookie的解决

    复制代码
    zuul.routes.sso-server.path=/sso-server/**
    zuul.routes.sso-server.service-id=sso-server
    
    
    zuul.host.socket-timeout-millis=60000
    zuul.host.connect-timeout-millis=10000
    #Zuul丢失Cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396
    zuul.sensitive-headers=
    复制代码

      

      测试效果

      启动eureka、zuul-server、sso-server、config-server、myspringboot、springdatajpa(由两个应用组成,实现了ribbon负载均衡),记得启动我们的RabbitMQ服务和Redis服务!

      刚开始,没有cookie且无Redis的情况下,浏览器访问 http://localhost:10010/myspringboot/feign/ribbon,被zuul-server拦截重定向到sso-server登录页面

      开始登录校验,为了方便演示,我将密码的type改成text

      登录失败,返回提示语

      登录成功,重定向到之前的请求

       cookie的值,以及过期时间

      3分钟后我们再次访问 http://localhost:10010/myspringboot/feign/ribbon,cookie、Redis失效,需要从新登录

       后记

      sso单点登录就记录到这里,这里只是实现了单机版的sso,以后在进行升级吧。

      问题报错:我们在sso-server设置cookie后,在zuul-server的run方法里获取不到设置的cookie,去浏览器查看,cookie没有设置成功,Zuul丢失Cookie

      解决方案:Zuul丢失Cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396

  • 相关阅读:
    2407: C语言习题 整数转换成字符串
    2484: 字母图形
    1658: Easier Done Than Said?
    Easier Done Than Said?(应用到的知识)
    1653: Champion of the Swordsmanship
    1614: 五位以内的对称素数
    1612: 最小公倍数
    $ vs $()
    数学
    计算机科学与技术课程
  • 原文地址:https://www.cnblogs.com/smallfa/p/10251544.html
Copyright © 2020-2023  润新知