文章目录
1 用户认证
1.1 用户认证流程分析
用户认证流程如下:
业务流程说明如下:
1、客户端请求认证服务进行认证。
2、认证服务认证通过向浏览器cookie写入token(身份令牌)
认证服务请求用户中心查询用户信息。
认证服务请求Spring Security申请令牌。
认证服务将token(身份令牌)和jwt令牌存储至redis中。
认证服务向cookie写入 token(身份令牌)。
3、前端携带token请求认证服务获取jwt令牌
前端获取到jwt令牌并存储在sessionStorage。
前端从jwt令牌中解析中用户信息并显示在页面。
4、前端携带cookie中的token身份令牌及jwt令牌访问资源服务
前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt令牌前端请求资源服务前在http header上添加jwt请求资源
5、网关校验token的合法性
用户请求必须携带token身份令牌和jwt令牌
网关校验redis中token是否合法,已过期则要求用户重新登录
6、资源服务校验jwt的合法性并完成授权
资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
1.2 认证服务查询数据库
1.2.1 需求分析
认证服务根据数据库中的用户信息去校验用户的身份,即校验账号和密码是否匹配。
认证服务不直接连接数据库,而是通过用户中心服务去查询用户中心数据库。
完整的流程图如下:
1.2.2 搭建环境 略
1.2.3 查询用户接口 略
1.2.4 调用用户查询接口 略
1.3 用户登录前端
1.3.1 需求分析
点击用户登录固定跳转到用户中心前端的登录页面,如下:
输入账号和密码,登录成功,跳转到首页。
用户中心前端(xc-ui-pc-learning工程)提供登录页面,所有子系统连接到此页面。
说明:
页面有“登录|注册”链接的前端系统有:门户系统、搜索系统、用户中心。
本小节修改门户系统的页头,其它三处可参考门户修改。
2 前端显示当前用户
2.1 需求分析
用户登录成功在页头显示当前登录的用户名称。
数据流程如下图:
1、用户请求认证服务,登录成功。
2、用户登录成功,认证服务向cookie写入身份令牌,向redis写入user_token(身份令牌及授权jwt授权令牌)
3、客户端携带cookie中的身份令牌请求认证服务获取jwt令牌。
4、客户端解析jwt令牌,并将解析的用户信息存储到sessionStorage中。
jwt令牌中包括了用户的基本信息,客户端解析jwt令牌即可获取用户信息。
5、客户端从sessionStorage中读取用户信息,并在页头显示。
sessionStorage :
sessionStorage 是H5的一个会话存储对象,在SessionStorage中保存的数据只在同一窗口或同一标签页中有效,在关闭窗口之后将会删除SessionStorage中的数据。
seesionStorage的存储方式采用key/value的方式,可保存5M左右的数据(不同的浏览器会有区别).
3 用户退出 略
4 Zuul网关
4.1 需求分析
网关的作用相当于一个过虑器、拦截器,它可以拦截多个系统的请求。
本章节要使用网关校验用户的身份是否合法。
4.2 Zuul介绍
什么是Zuul?
Spring Cloud Zuul是整合Netflix公司的Zuul开源项目实现的微服务网关,它实现了请求路由、负载均衡、校验过虑等 功能。
官方:https://github.com/Netflix/zuul
什么是网关?
服务网关是在微服务前边设置一道屏障,请求先到服务网关,网关会对请求进行过虑、校验、路由等处理。有了服务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问。
Zuul与Nginx怎么配合使用?
Zuul与Nginx在实际项目中需要配合使用,如下图,Nginx的作用是反向代理、负载均衡,Zuul的作用是保障微服务的安全访问,拦截微服务请求,校验合法性及负载均衡。
4.3 搭建网关工程
@EnableZuulProxy
注意在启动类上使用@EnableZuulProxy注解标识此工程为Zuul网关,启动类代码如下:
@SpringBootApplication
@EnableZuulProxy//此工程是一个zuul网关
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4.4 路由配置
4.4.1需求分析
Zuul网关具有代理的功能,根据请求的url转发到微服务,如下图:
4.4.2 路由配置
zuul:
routes:
manage‐course: #路由名称,名称任意,保持所有路由名称唯一
path: /course/**
serviceId: xc-service-manage-course #指定服务id,从Eureka中找到服务的ip和端口
#url: http://localhost:31200 #也可指定url
strip‐prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀
sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名
# 单,如果设置了具体的头信息则不会传到下游服务
# ignoredHeaders: Authorization
xc‐service‐manage‐order:
path: /order/**
serviceId: xc‐service‐manage‐order
strip‐prefix: false
sensitiveHeaders:
**serviceId:**推荐使用serviceId,zuul会从Eureka中找到服务id对应的ip和端口。
strip-prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀,例如,为true请求/course/coursebase/get/…,代理转发到/coursebase/get/,如果为false则代理转发到/course/coursebase/get
**sensitiveHeaders:**敏感头设置,默认会过虑掉cookie,这里设置为空表示不过虑
**ignoredHeaders:**可以设置过虑的头信息,默认为空表示不过虑任何头
4.5 过虑器
Zuul的核心就是过虑器,通过过虑器实现请求过虑,身份校验等。
4.5.1 ZuulFilter
自定义过虑器需要继承 ZuulFilter,ZuulFilter是一个抽象类,需要覆盖它的四个方法,如下:
1、 shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true表示要执行此过虑器,否则不执
行。
2、 run:过滤器的业务逻辑。
3、 filterType:返回字符串代表过滤器的类型,如下 pre:请求在被路由之前执行 routing:在路由请求时调用 post:在routing和errror过滤器之后调用 error:处理请求时发生错误调用
4、 filterOrder:此方法返回整型数值,通过此数值来定义过滤器的执行顺序,数字越小优先级越高。
4.5.2测试
过虑所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
定义过虑器,使用@Component标识为bean。
@Component
public class LoginFilterTest extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(LoginFilterTest.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
HttpServletRequest request = requestContext.getRequest();
//取出头部信息Authorization
String authorization = request.getHeader("Authorization");
if (StringUtils.isEmpty(authorization)) {
requestContext.setSendZuulResponse(false);// 拒绝访问
requestContext.setResponseStatusCode(200);// 设置响应状态码
ResponseResult unauthenticated = new ResponseResult(CommonCode.UNAUTHENTICATED);
String jsonString = JSON.toJSONString(unauthenticated);
requestContext.setResponseBody(jsonString);
requestContext.getResponse().setContentType("application/json;charset=UTF‐8");
return null;
}
return null;
}
}
测试:
请求:http://localhost:50201/api/course/coursebase/get/4028e581617f945f01617f9dabc40000查询课程信息
1、Header中不设置Authorization
响应结果:
5 身份校验
5.1 需求分析
本小节实现网关连接Redis校验令牌:
1、从cookie查询用户身份令牌是否存在,不存在则拒绝访问
2、从http header查询jwt令牌是否存在,不存在则拒绝访问
3、从Redis查询user_token令牌是否过期,过期则拒绝访问
5.2 编码代码
1、配置application.yml
配置 redis链接参数:
spring:
application:
name: xc-govern-gateway
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
timeout: 5000 #连接超时 毫秒
jedis:
pool:
maxActive: 3
maxIdle: 3
minIdle: 1
maxWait: -1 #连接池最大等行时间 -1没有限制
2、使用StringRedisTemplate查询key的有效期
在service包下定义AuthService类:
@Service
public class AuthService {
@Autowired
StringRedisTemplate stringRedisTemplate;
//查询身份令牌
public String getTokenFromCookie(HttpServletRequest request) {
Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid");
String access_token = cookieMap.get("uid");
if (StringUtils.isEmpty(access_token)) {
return null;
}
return access_token;
}
//从header中查询jwt令牌
public String getJwtFromHeader(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (StringUtils.isEmpty(authorization)) {
//拒绝访问
return null;
}
if (!authorization.startsWith("Bearer ")) {
//拒绝访问
return null;
}
return authorization;
}
//查询令牌的有效期
public long getExpire(String access_token) {
//token在redis中的key
String key = "user_token:" + access_token;
Long expire = stringRedisTemplate.getExpire(key);
return expire;
}
}
说明:由于令牌存储时采用String序列化策略,所以这里用 StringRedisTemplate来查询,使用RedisTemplate无
法完成查询。
3、定义LoginFilter
@Component
public class LoginFilter extends ZuulFilter {
private static final Logger LOGG = LoggerFactory.getLogger(LoginFilter.class);
@Autowired
AuthService authService;
@Override
public String filterType() {
//四种类型:pre、routing、post、error
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
//上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
//请求对象
HttpServletRequest request = requestContext.getRequest();
//查询身份令牌
String access_token = authService.getTokenFromCookie(request);
if (access_token == null) {
//拒绝访问
access_denied();
}
//从redis中校验身份令牌是否过期
long expire = authService.getExpire(access_token);
if (expire <= 0) {
//拒绝访问
access_denied();
}
//查询jwt令牌
String jwt = authService.getJwtFromHeader(request);
if (jwt == null) {
//拒绝访问
access_denied();
}
return null;
}
//拒绝访问
private void access_denied() {
//上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
requestContext.setSendZuulResponse(false);//拒绝访问
//设置响应内容
ResponseResult responseResult = new ResponseResult(CommonCode.UNAUTHENTICATED);
String responseResultString = JSON.toJSONString(responseResult);
requestContext.setResponseBody(responseResultString);
//设置状态码
requestContext.setResponseStatusCode(200);
HttpServletResponse response = requestContext.getResponse();
response.setContentType("application/json;charset=utf‐8");
}
}