1.1.什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
-
服务端保存大量数据,增加服务端压力
-
服务端保存用户状态,无法进行水平扩展
-
客户端请求依赖服务端,多次请求必须访问同一台服务器
1.2.什么是无状态
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
-
服务端不保存任何客户端请求者信息
-
客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
带来的好处是什么呢?
-
客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
-
服务端的集群和状态对客户端透明
-
服务端可以任意的迁移和伸缩
-
减小服务端存储压力
1.3.如何实现无状态
无状态登录的流程:
-
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
-
认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
-
以后每次请求,客户端都携带认证的token
-
服务的对token进行解密,判断是否有效。
、
整个登录过程中,最关键的点是什么?
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用JWT + RSA非对称加密
1.4.JWT
1.4.1.简介
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
1.4.2.数据格式
JWT包含三部分数据:
-
Header:头部,通常头部有两部分信息:
-
声明类型,这里是JWT
我们会对头部进行base64编码,得到第一部分数据
-
-
Payload:载荷,就是有效数据,一般包含下面信息:
-
用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
-
注册声明:如token的签发时间,过期时间,签发人等
这部分也会采用base64编码,得到第二部分数据
-
-
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
生成的数据格式:token==个人证件 jwt=个人身份证
1.5.1.没有RSA加密时
在微服务架构中,我们可以把服务的鉴权操作放到网关中,将未通过鉴权的请求直接拦截,如图:
-
1、用户请求登录
-
2、Zuul将请求转发到授权中心,请求授权
-
3、授权中心校验完成,颁发JWT凭证
-
4、客户端请求其它功能,携带JWT
-
5、Zuul将jwt交给授权中心校验,通过后放行
-
6、用户请求到达微服务
-
7、微服务将jwt交给鉴权中心,鉴权同时解析用户信息
-
8、鉴权中心返回用户数据给微服务
-
9、微服务处理请求,返回响应
发现什么问题了?
每次鉴权都需要访问鉴权中心,系统间的网络请求频率过高,效率略差,鉴权中心的压力较大。
1.5.2.结合RSA的鉴权
直接看图:
-
我们首先利用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在Zuul和各个信任的微服务
-
用户请求登录
-
授权中心校验,通过后用私钥对JWT进行签名加密
-
返回jwt给用户
-
用户携带JWT访问
-
Zuul直接通过公钥解密JWT,进行验证,验证通过则放行
-
请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心
2.授权中心
2.1.创建授权中心
授权中心的主要职责:
-
用户鉴权:
-
接收用户的登录请求,通过用户中心的接口进行校验,通过后生成JWT
-
使用私钥生成JWT并返回
-
-
服务鉴权:微服务间的调用不经过Zuul,会有风险,需要鉴权中心进行认证
-
原理与用户鉴权类似,但逻辑稍微复杂一些(此处我们不做实现)
-
因为生成jwt,解析jwt这样的行为以后在其它微服务中也会用到,因此我们会抽取成工具。我们把鉴权中心进行聚合,一个工具module,一个提供服务的module
关于GWT得令牌得响应顺序和响应得响应流程
开始搭建授权微服务得得工程:leyou-auth ,leyou-auth-common,leyou-auth-service,第一个是父工程,剩下两个是子工程,其实基本得逻辑和前面得一样,配置启动类,引入依赖,添加配置文件,就可以启动,这里一以leyou-service这个微服务位例子
添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.user</groupId>
<artifactId>leyou-user-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
添加配置文件
server:
port: 8087
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 10
instance:
lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
添加引导类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouAuthApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouAuthApplication.class, args);
}
}
这个授权微服务搭建完成,下一个就是引入jwt得Utils 工具类,然后在测试用例里面生成令牌,首先引入工具类
接下来就是创建公钥和私钥,可以看出来对应得文件已经生成成功
接下来就是生成jwt令牌,可以看出封装了一个载荷进去,并且生成了一个jwt得令牌
解析:对封装过得jwt令牌,用公钥进行解密,可以得到里面得载荷得信息
下来开始编写授权得接口测试
Controller
@PostMapping("accredit")
public ResponseEntity<Void> accredt(@RequestParam("username")String username, @RequestParam("password")String password, HttpServletResponse response, HttpServletRequest request){
String token= authService.accredit(username,password);
if (token==null){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
CookieUtils.setCookie(request,response,token,jwtProperties.getCookieNme(),jwtProperties.getExpire()*60);
return ResponseEntity.ok(null);
}
Service:
public String accredit(String username, String password) {
User user = userClient.query(username, password);
if (user==null){
return null;
}
UserInfo userInfo=new UserInfo();
userInfo.setId(user.getId());
userInfo.setUsername(user.getUsername());
try {
return JwtUtils.generateToken(userInfo,jwtProperties.getPrivateKey(),jwtProperties.getExpire());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
接口进行测试:可以看到Cookie设值成功
domin域得问题:这个domin域设置得时候又一些问题,其中要在nginx中配置一定要注意这个是在api.leyou.com里面配置得,如果配置到起码得www.leyou.com或者mangage.leyou.com都是无法生效得然后就是在gateway网关中进行配置
完成之后就可以获取cookie中得信息
延长首页得Cookie时间
Controller:
@GetMapping("verify")
public ResponseEntity<UserInfo> verify(@CookieValue("LY_TOKEN")String token,HttpServletRequest request,HttpServletResponse response){
try {
UserInfo userInfo = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey());
if (userInfo==null){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
token=JwtUtils.generateToken(userInfo,jwtProperties.getPrivateKey(),jwtProperties.getExpire());
CookieUtils.setCookie(request,response,token,jwtProperties.getCookieNme(),jwtProperties.getExpire()*60);
return ResponseEntity.ok(userInfo);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
出现错误
出现了一个非常大得BUG 我得CookieName 和CookieValue 值设置反了
这个里面还又一个问题就是又可能token得公钥和这个匹配不上,把原来得文件删除了就行,在生成一次,还有一个比较坑得地方,这个地方,变量名一要是user,因为响应得数据里面如果是出来user之外得熟悉,resp,在前端页面就渲染不出来
关于添拦截器,URL和URI是不一样得注意
public class LoginFilter extends ZuulFilter {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private filterProperties filterProperties;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
List<String> allowPaths = filterProperties.getAllowPaths();
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String getRequestURI = request.getRequestURI().toString();
for (String allowPath : allowPaths) {
if (StringUtils.contains(getRequestURI,allowPath))
{
return false;
}
}
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String token = CookieUtils.getCookieValue(request, jwtProperties.getPubKeyPath());
try {
JwtUtils.getInfoFromToken(token,jwtProperties.getPublicKey());
} catch (Exception e) {
e.printStackTrace();
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
拦截成功
今日总结:
访问流程
1.无状态登陆
在服务器端保存session
无状态不需要session,把登陆状态保存在cookie中
2.jwt+rsa
token:登陆时,jwt oath2
jwt:头信息(jwt) 载荷(用户信息,签发人 签发时间 有效时间) 签名(头信息+载荷:通过加密算法生成。作用:校验前两部分内容的合法性)
3.zuul网关访问的流程
4.搭建授权中心
聚合工程:leyou-auth-common(jwt相关的工具类) leyou-auth-service(微服务)
5.jwtUtils
根据载荷还有私钥生成jwt类型的token
根据公钥解析jwt类型的token,获取载荷信息(userInfo)
rsaUtils(生成公钥和私钥文件,并且读取公钥和私钥文件返回公钥和私钥对象)
6.登陆功能
1.调用user-service中的queryUser接口,验证用户是否正确
2.判断返回的用户信息是否为空
3.生成jwt类型的token
4.token信息设置到cookie中
7.首页用户名回显
1.获取Cookie中jwt类型的token @CookieValue("LY_TOKEN")
2.jwtUtils解析jwt,获取用户信息
判断用户是否为空
3.刷新jwt时间 cookie时间
4.响应用户信息
8.在zuul网关添加过滤器
获取zuul的上下文对象
获取request
通过cookieutils获取token
解析token
白名单;