分布式-单点登录
原理:通过动态路由zuul,访问服务器前做鉴权,然后把信息存入redis,cookie,请求服务前先走动态路由,会验证。
一:新建项目:sso-server
1.依赖:
<dependencies>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies
2.配置文件:
server.port=1113
spring.application.name=sso-server
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:1111/eureka/
eureka.client.healthcheck.enabled=true
eureka.instance.lease-renewal-interval-in-seconds=10
eureka.instance.lease-expiration-duration-in-seconds=10
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=http://${spring.cloud.client.ip-address}:${server.port}
eureka.instance.hostname= ${spring.cloud.client.ip-address}
#redisIp
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=123456
3.新建配置文件login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
</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="text"/>
<input value="登录" type="submit"/>
</form>
</body>
</html>
4.接口提供:
/**
* 判断key是否存在
*/
@RequestMapping("/redis/hasKey/{key}")
public Boolean hasKey(@PathVariable("key") String key) {
try {
Boolean aBoolean = template.hasKey(key);
return aBoolean;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 校验用户名密码,成功则返回通行令牌
*/
@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 * 10);
//设置域
//cookie.setDomain("sso.com");
//设置访问路径
cookie.setPath("/");
response.addCookie(cookie);
//重定向到原先访问的页面
response.sendRedirect(url);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
return "登录失败";
}
二:动态路由系统:zuul-server(已经搭建,搭建教程见分布式-Zuul 动态路由)
1.更改过滤器具体逻辑:
public class AccessFilter extends ZuulFilter {
//令牌桶限流:峰值每秒可以处理10个请求,正常每秒可以处理3个请求
private RateLimiter rateLimiter = new RateLimiter(2, 1);
@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();
//限流
if (!rateLimiter.execute()) {
try {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
//直接写入浏览器
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println("系统繁忙,请稍后在试!<br/>System busy, please try again later!");
writer.flush();
System.out.println("系统繁忙,请稍后在试!");
return null;
} catch (Exception e) {
e.printStackTrace();
}
}
//访问路径
StringBuilder url = new StringBuilder(request.getRequestURL().toString());
//从cookie里面取值(Zuul丢失Cookie的解决方案:https://blog.csdn.net/lindan1984/article/details/79308396)
String accessToken = request.getParameter("accessToken");
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) {
accessToken = cookie.getValue();
}
}
}
//过滤规则:
//访问的是登录页面、登录请求则放行
Boolean aBoolean = ssoFeign.hasKey(accessToken);
if (url.toString().contains("sso-server/sso/loginPage") ||
url.toString().contains("sso-server/sso/login") ||
//cookie有令牌且存在于Redis
(!StringUtils.isEmpty(accessToken) && aBoolean)
) {
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
return null;
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
//如果是get请求处理参数,其他请求统统跳转到首页
String method = request.getMethod();
if ("GET".equals(method)) {
url.append("?");
Map<String, String[]> parameterMap = request.getParameterMap();
Object[] keys = parameterMap.keySet().toArray();
for (int i = 0; i < keys.length; i++) {
String key = (String) keys[i];
String value = parameterMap.get(key)[0];
url.append(key).append("=").append(value).append("&");
}
//处理末尾的&符合
url.delete(url.length() - 1, url.length());
} else {
//首页链接,或者其他固定页面
url = new StringBuilder("XXX");
}
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;
}
}
2.配置文件添加:
zuul.sensitive-headers=
ribbon.ReadTimeout=60000
ribbon.ConnectTimeout=60000
zuul.routes.sso-server.path=/sso-server/**
zuul.routes.sso-server.service-id=sso-server
3.新建调用sso的消费者接口:
@FeignClient(name = "sso-server", path = "/",/*fallback = SsoFeign.SsoFeignFallback.class,*/fallbackFactory = SsoFeign.SsoFeignFallbackFactory.class)
public interface SsoFeign {
/**
* 判断key是否存在
*/
@RequestMapping("redis/hasKey/{key}")
public Boolean hasKey(@PathVariable("key") String key);
/**
* 容错处理(服务提供者发生异常,将会进入这里)
*/
@Component
public class SsoFeignFallback implements SsoFeign {
@Override
public Boolean hasKey(String key) {
System.out.println("调用sso-server失败,进行SsoFeignFallback.hasKey处理:return false;");
return false;
}
}
/**
* 只打印异常,容错处理仍交给 SsoFeignFallback
*/
@Component
public class SsoFeignFallbackFactory implements FallbackFactory<SsoFeign> {
private final SsoFeignFallback ssoFeignFallback;
public SsoFeignFallbackFactory(SsoFeignFallback ssoFeignFallback) {
this.ssoFeignFallback = ssoFeignFallback;
}
@Override
public SsoFeign create(Throwable cause) {
cause.printStackTrace();
return ssoFeignFallback;
}
}
}
4.开启注册中心,sso-server ,tx-lcn事务管理,消费者server-a ,动态路由 zuul-server
5.通过动态路由访问消费者server-a
访问 http://localhost:10010/service-a/ribbon 会跳转到下面的界面
6.登录
7.成功访问到serer-a