前言

  作为分布式项目,单点登录是必不可少的,文本基于之前的的博客(猛戳:SpringCloud系列——Zuul 动态路由
<https://www.cnblogs.com/huanzi-qch/p/10142395.html>,SpringBoot系列——Redis
<https://www.cnblogs.com/huanzi-qch/p/10239888.html>
)记录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 <https://www.cnblogs.com/huanzi-qch/p/10239888.html>)



  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 {
@Autowiredprivate 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
<https://blog.csdn.net/lindan1984/article/details/79308396>